Skip to content

Commit dcf5d74

Browse files
authored
Use concise date format when plotting (#8449)
* 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
1 parent 7e6eba0 commit dcf5d74

File tree

4 files changed

+90
-35
lines changed

4 files changed

+90
-35
lines changed

doc/whats-new.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ v2023.11.1 (unreleased)
2323
New Features
2424
~~~~~~~~~~~~
2525

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

2729
Breaking changes
2830
~~~~~~~~~~~~~~~~

xarray/plot/dataarray_plot.py

Lines changed: 8 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
_rescale_imshow_rgb,
2828
_resolve_intervals_1dplot,
2929
_resolve_intervals_2dplot,
30+
_set_concise_date,
3031
_update_axes,
3132
get_axis,
3233
label_from_attrs,
@@ -525,14 +526,8 @@ def line(
525526
assert hueplt is not None
526527
ax.legend(handles=primitive, labels=list(hueplt.to_numpy()), title=hue_label)
527528

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

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

@@ -1087,14 +1082,12 @@ def _add_labels(
10871082
add_labels: bool | Iterable[bool],
10881083
darrays: Iterable[DataArray | None],
10891084
suffixes: Iterable[str],
1090-
rotate_labels: Iterable[bool],
10911085
ax: Axes,
10921086
) -> None:
10931087
"""Set x, y, z labels."""
10941088
add_labels = [add_labels] * 3 if isinstance(add_labels, bool) else add_labels
1095-
for axis, add_label, darray, suffix, rotate_label in zip(
1096-
("x", "y", "z"), add_labels, darrays, suffixes, rotate_labels
1097-
):
1089+
axes: tuple[Literal["x", "y", "z"], ...] = ("x", "y", "z")
1090+
for axis, add_label, darray, suffix in zip(axes, add_labels, darrays, suffixes):
10981091
if darray is None:
10991092
continue
11001093

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

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

11151102

11161103
@overload
@@ -1265,7 +1252,7 @@ def scatter(
12651252
kwargs.update(s=sizeplt.to_numpy().ravel())
12661253

12671254
plts_or_none = (xplt, yplt, zplt)
1268-
_add_labels(add_labels, plts_or_none, ("", "", ""), (True, False, False), ax)
1255+
_add_labels(add_labels, plts_or_none, ("", "", ""), ax)
12691256

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

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

16651646
return primitive
16661647

xarray/plot/utils.py

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from collections.abc import Hashable, Iterable, Mapping, MutableMapping, Sequence
77
from datetime import datetime
88
from inspect import getfullargspec
9-
from typing import TYPE_CHECKING, Any, Callable, overload
9+
from typing import TYPE_CHECKING, Any, Callable, Literal, overload
1010

1111
import numpy as np
1212
import pandas as pd
@@ -1827,3 +1827,27 @@ def _guess_coords_to_plot(
18271827
_assert_valid_xy(darray, dim, k)
18281828

18291829
return coords_to_plot
1830+
1831+
1832+
def _set_concise_date(ax: Axes, axis: Literal["x", "y", "z"] = "x") -> None:
1833+
"""
1834+
Use ConciseDateFormatter which is meant to improve the
1835+
strings chosen for the ticklabels, and to minimize the
1836+
strings used in those tick labels as much as possible.
1837+
1838+
https://matplotlib.org/stable/gallery/ticks/date_concise_formatter.html
1839+
1840+
Parameters
1841+
----------
1842+
ax : Axes
1843+
Figure axes.
1844+
axis : Literal["x", "y", "z"], optional
1845+
Which axis to make concise. The default is "x".
1846+
"""
1847+
import matplotlib.dates as mdates
1848+
1849+
locator = mdates.AutoDateLocator()
1850+
formatter = mdates.ConciseDateFormatter(locator)
1851+
_axis = getattr(ax, f"{axis}axis")
1852+
_axis.set_major_locator(locator)
1853+
_axis.set_major_formatter(formatter)

xarray/tests/test_plot.py

Lines changed: 55 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -787,12 +787,17 @@ def test_plot_nans(self) -> None:
787787
self.darray[1] = np.nan
788788
self.darray.plot.line()
789789

790-
def test_x_ticks_are_rotated_for_time(self) -> None:
790+
def test_dates_are_concise(self) -> None:
791+
import matplotlib.dates as mdates
792+
791793
time = pd.date_range("2000-01-01", "2000-01-10")
792794
a = DataArray(np.arange(len(time)), [("t", time)])
793795
a.plot.line()
794-
rotation = plt.gca().get_xticklabels()[0].get_rotation()
795-
assert rotation != 0
796+
797+
ax = plt.gca()
798+
799+
assert isinstance(ax.xaxis.get_major_locator(), mdates.AutoDateLocator)
800+
assert isinstance(ax.xaxis.get_major_formatter(), mdates.ConciseDateFormatter)
796801

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

1359-
def test_x_ticks_are_rotated_for_time(self) -> None:
1364+
def test_dates_are_concise(self) -> None:
1365+
import matplotlib.dates as mdates
1366+
13601367
time = pd.date_range("2000-01-01", "2000-01-10")
13611368
a = DataArray(np.random.randn(2, len(time)), [("xx", [1, 2]), ("t", time)])
1362-
a.plot(x="t")
1363-
rotation = plt.gca().get_xticklabels()[0].get_rotation()
1364-
assert rotation != 0
1369+
self.plotfunc(a, x="t")
1370+
1371+
ax = plt.gca()
1372+
1373+
assert isinstance(ax.xaxis.get_major_locator(), mdates.AutoDateLocator)
1374+
assert isinstance(ax.xaxis.get_major_formatter(), mdates.ConciseDateFormatter)
13651375

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

1901+
@pytest.mark.xfail(
1902+
reason=(
1903+
"Failing inside matplotlib. Should probably be fixed upstream because "
1904+
"other plot functions can handle it. "
1905+
"Remove this test when it works, already in Common2dMixin"
1906+
)
1907+
)
1908+
def test_dates_are_concise(self) -> None:
1909+
import matplotlib.dates as mdates
1910+
1911+
time = pd.date_range("2000-01-01", "2000-01-10")
1912+
a = DataArray(np.random.randn(2, len(time)), [("xx", [1, 2]), ("t", time)])
1913+
self.plotfunc(a, x="t")
1914+
1915+
ax = plt.gca()
1916+
1917+
assert isinstance(ax.xaxis.get_major_locator(), mdates.AutoDateLocator)
1918+
assert isinstance(ax.xaxis.get_major_formatter(), mdates.ConciseDateFormatter)
1919+
18911920
@pytest.mark.slow
18921921
def test_imshow_called(self) -> None:
18931922
# Having both statements ensures the test works properly
@@ -2032,6 +2061,25 @@ class TestSurface(Common2dMixin, PlotTestCase):
20322061
plotfunc = staticmethod(xplt.surface)
20332062
subplot_kws = {"projection": "3d"}
20342063

2064+
@pytest.mark.xfail(
2065+
reason=(
2066+
"Failing inside matplotlib. Should probably be fixed upstream because "
2067+
"other plot functions can handle it. "
2068+
"Remove this test when it works, already in Common2dMixin"
2069+
)
2070+
)
2071+
def test_dates_are_concise(self) -> None:
2072+
import matplotlib.dates as mdates
2073+
2074+
time = pd.date_range("2000-01-01", "2000-01-10")
2075+
a = DataArray(np.random.randn(2, len(time)), [("xx", [1, 2]), ("t", time)])
2076+
self.plotfunc(a, x="t")
2077+
2078+
ax = plt.gca()
2079+
2080+
assert isinstance(ax.xaxis.get_major_locator(), mdates.AutoDateLocator)
2081+
assert isinstance(ax.xaxis.get_major_formatter(), mdates.ConciseDateFormatter)
2082+
20352083
def test_primitive_artist_returned(self) -> None:
20362084
artist = self.plotmethod()
20372085
assert isinstance(artist, mpl_toolkits.mplot3d.art3d.Poly3DCollection)

0 commit comments

Comments
 (0)