Skip to content

Commit

Permalink
Use concise date format when plotting (#8449)
Browse files Browse the repository at this point in the history
* Add concise date format

* Update utils.py

* Update dataarray_plot.py

* Update dataarray_plot.py

* Update whats-new.rst

* Cleanup

* Clarify xfail reason

* Update whats-new.rst
  • Loading branch information
Illviljan authored Nov 21, 2023
1 parent 7e6eba0 commit dcf5d74
Show file tree
Hide file tree
Showing 4 changed files with 90 additions and 35 deletions.
2 changes: 2 additions & 0 deletions doc/whats-new.rst
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ v2023.11.1 (unreleased)
New Features
~~~~~~~~~~~~

- Use a concise format when plotting datetime arrays. (:pull:`8449`).
By `Jimmy Westling <https://github.com/illviljan>`_.

Breaking changes
~~~~~~~~~~~~~~~~
Expand Down
35 changes: 8 additions & 27 deletions xarray/plot/dataarray_plot.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
_rescale_imshow_rgb,
_resolve_intervals_1dplot,
_resolve_intervals_2dplot,
_set_concise_date,
_update_axes,
get_axis,
label_from_attrs,
Expand Down Expand Up @@ -525,14 +526,8 @@ def line(
assert hueplt is not None
ax.legend(handles=primitive, labels=list(hueplt.to_numpy()), title=hue_label)

# Rotate dates on xlabels
# Do this without calling autofmt_xdate so that x-axes ticks
# on other subplots (if any) are not deleted.
# https://stackoverflow.com/questions/17430105/autofmt-xdate-deletes-x-axis-labels-of-all-subplots
if np.issubdtype(xplt.dtype, np.datetime64):
for xlabels in ax.get_xticklabels():
xlabels.set_rotation(30)
xlabels.set_horizontalalignment("right")
_set_concise_date(ax, axis="x")

_update_axes(ax, xincrease, yincrease, xscale, yscale, xticks, yticks, xlim, ylim)

Expand Down Expand Up @@ -1087,14 +1082,12 @@ def _add_labels(
add_labels: bool | Iterable[bool],
darrays: Iterable[DataArray | None],
suffixes: Iterable[str],
rotate_labels: Iterable[bool],
ax: Axes,
) -> None:
"""Set x, y, z labels."""
add_labels = [add_labels] * 3 if isinstance(add_labels, bool) else add_labels
for axis, add_label, darray, suffix, rotate_label in zip(
("x", "y", "z"), add_labels, darrays, suffixes, rotate_labels
):
axes: tuple[Literal["x", "y", "z"], ...] = ("x", "y", "z")
for axis, add_label, darray, suffix in zip(axes, add_labels, darrays, suffixes):
if darray is None:
continue

Expand All @@ -1103,14 +1096,8 @@ def _add_labels(
if label is not None:
getattr(ax, f"set_{axis}label")(label)

if rotate_label and np.issubdtype(darray.dtype, np.datetime64):
# Rotate dates on xlabels
# Do this without calling autofmt_xdate so that x-axes ticks
# on other subplots (if any) are not deleted.
# https://stackoverflow.com/questions/17430105/autofmt-xdate-deletes-x-axis-labels-of-all-subplots
for labels in getattr(ax, f"get_{axis}ticklabels")():
labels.set_rotation(30)
labels.set_horizontalalignment("right")
if np.issubdtype(darray.dtype, np.datetime64):
_set_concise_date(ax, axis=axis)


@overload
Expand Down Expand Up @@ -1265,7 +1252,7 @@ def scatter(
kwargs.update(s=sizeplt.to_numpy().ravel())

plts_or_none = (xplt, yplt, zplt)
_add_labels(add_labels, plts_or_none, ("", "", ""), (True, False, False), ax)
_add_labels(add_labels, plts_or_none, ("", "", ""), ax)

xplt_np = None if xplt is None else xplt.to_numpy().ravel()
yplt_np = None if yplt is None else yplt.to_numpy().ravel()
Expand Down Expand Up @@ -1653,14 +1640,8 @@ def newplotfunc(
ax, xincrease, yincrease, xscale, yscale, xticks, yticks, xlim, ylim
)

# Rotate dates on xlabels
# Do this without calling autofmt_xdate so that x-axes ticks
# on other subplots (if any) are not deleted.
# https://stackoverflow.com/questions/17430105/autofmt-xdate-deletes-x-axis-labels-of-all-subplots
if np.issubdtype(xplt.dtype, np.datetime64):
for xlabels in ax.get_xticklabels():
xlabels.set_rotation(30)
xlabels.set_horizontalalignment("right")
_set_concise_date(ax, "x")

return primitive

Expand Down
26 changes: 25 additions & 1 deletion xarray/plot/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from collections.abc import Hashable, Iterable, Mapping, MutableMapping, Sequence
from datetime import datetime
from inspect import getfullargspec
from typing import TYPE_CHECKING, Any, Callable, overload
from typing import TYPE_CHECKING, Any, Callable, Literal, overload

import numpy as np
import pandas as pd
Expand Down Expand Up @@ -1827,3 +1827,27 @@ def _guess_coords_to_plot(
_assert_valid_xy(darray, dim, k)

return coords_to_plot


def _set_concise_date(ax: Axes, axis: Literal["x", "y", "z"] = "x") -> None:
"""
Use ConciseDateFormatter which is meant to improve the
strings chosen for the ticklabels, and to minimize the
strings used in those tick labels as much as possible.
https://matplotlib.org/stable/gallery/ticks/date_concise_formatter.html
Parameters
----------
ax : Axes
Figure axes.
axis : Literal["x", "y", "z"], optional
Which axis to make concise. The default is "x".
"""
import matplotlib.dates as mdates

locator = mdates.AutoDateLocator()
formatter = mdates.ConciseDateFormatter(locator)
_axis = getattr(ax, f"{axis}axis")
_axis.set_major_locator(locator)
_axis.set_major_formatter(formatter)
62 changes: 55 additions & 7 deletions xarray/tests/test_plot.py
Original file line number Diff line number Diff line change
Expand Up @@ -787,12 +787,17 @@ def test_plot_nans(self) -> None:
self.darray[1] = np.nan
self.darray.plot.line()

def test_x_ticks_are_rotated_for_time(self) -> None:
def test_dates_are_concise(self) -> None:
import matplotlib.dates as mdates

time = pd.date_range("2000-01-01", "2000-01-10")
a = DataArray(np.arange(len(time)), [("t", time)])
a.plot.line()
rotation = plt.gca().get_xticklabels()[0].get_rotation()
assert rotation != 0

ax = plt.gca()

assert isinstance(ax.xaxis.get_major_locator(), mdates.AutoDateLocator)
assert isinstance(ax.xaxis.get_major_formatter(), mdates.ConciseDateFormatter)

def test_xyincrease_false_changes_axes(self) -> None:
self.darray.plot.line(xincrease=False, yincrease=False)
Expand Down Expand Up @@ -1356,12 +1361,17 @@ def test_xyincrease_true_changes_axes(self) -> None:
diffs = xlim[0] - 0, xlim[1] - 14, ylim[0] - 0, ylim[1] - 9
assert all(abs(x) < 1 for x in diffs)

def test_x_ticks_are_rotated_for_time(self) -> None:
def test_dates_are_concise(self) -> None:
import matplotlib.dates as mdates

time = pd.date_range("2000-01-01", "2000-01-10")
a = DataArray(np.random.randn(2, len(time)), [("xx", [1, 2]), ("t", time)])
a.plot(x="t")
rotation = plt.gca().get_xticklabels()[0].get_rotation()
assert rotation != 0
self.plotfunc(a, x="t")

ax = plt.gca()

assert isinstance(ax.xaxis.get_major_locator(), mdates.AutoDateLocator)
assert isinstance(ax.xaxis.get_major_formatter(), mdates.ConciseDateFormatter)

def test_plot_nans(self) -> None:
x1 = self.darray[:5]
Expand Down Expand Up @@ -1888,6 +1898,25 @@ def test_interval_breaks_logspace(self) -> None:
class TestImshow(Common2dMixin, PlotTestCase):
plotfunc = staticmethod(xplt.imshow)

@pytest.mark.xfail(
reason=(
"Failing inside matplotlib. Should probably be fixed upstream because "
"other plot functions can handle it. "
"Remove this test when it works, already in Common2dMixin"
)
)
def test_dates_are_concise(self) -> None:
import matplotlib.dates as mdates

time = pd.date_range("2000-01-01", "2000-01-10")
a = DataArray(np.random.randn(2, len(time)), [("xx", [1, 2]), ("t", time)])
self.plotfunc(a, x="t")

ax = plt.gca()

assert isinstance(ax.xaxis.get_major_locator(), mdates.AutoDateLocator)
assert isinstance(ax.xaxis.get_major_formatter(), mdates.ConciseDateFormatter)

@pytest.mark.slow
def test_imshow_called(self) -> None:
# Having both statements ensures the test works properly
Expand Down Expand Up @@ -2032,6 +2061,25 @@ class TestSurface(Common2dMixin, PlotTestCase):
plotfunc = staticmethod(xplt.surface)
subplot_kws = {"projection": "3d"}

@pytest.mark.xfail(
reason=(
"Failing inside matplotlib. Should probably be fixed upstream because "
"other plot functions can handle it. "
"Remove this test when it works, already in Common2dMixin"
)
)
def test_dates_are_concise(self) -> None:
import matplotlib.dates as mdates

time = pd.date_range("2000-01-01", "2000-01-10")
a = DataArray(np.random.randn(2, len(time)), [("xx", [1, 2]), ("t", time)])
self.plotfunc(a, x="t")

ax = plt.gca()

assert isinstance(ax.xaxis.get_major_locator(), mdates.AutoDateLocator)
assert isinstance(ax.xaxis.get_major_formatter(), mdates.ConciseDateFormatter)

def test_primitive_artist_returned(self) -> None:
artist = self.plotmethod()
assert isinstance(artist, mpl_toolkits.mplot3d.art3d.Poly3DCollection)
Expand Down

0 comments on commit dcf5d74

Please sign in to comment.