From 7d40e616f108c60e9e8ab4c3502c9e56340fd749 Mon Sep 17 00:00:00 2001 From: Illviljan <14371165+Illviljan@users.noreply.github.com> Date: Mon, 13 Nov 2023 21:11:38 +0100 Subject: [PATCH 1/8] Add concise date format --- xarray/plot/dataarray_plot.py | 57 +++++++++++++++++++---------------- xarray/plot/utils.py | 26 +++++++++++++++- xarray/tests/test_plot.py | 54 ++++++++++++++++++++++++++++----- 3 files changed, 103 insertions(+), 34 deletions(-) diff --git a/xarray/plot/dataarray_plot.py b/xarray/plot/dataarray_plot.py index 61f2014fbc3..b1b78693819 100644 --- a/xarray/plot/dataarray_plot.py +++ b/xarray/plot/dataarray_plot.py @@ -27,6 +27,7 @@ _rescale_imshow_rgb, _resolve_intervals_1dplot, _resolve_intervals_2dplot, + _set_concise_date, _update_axes, get_axis, label_from_attrs, @@ -525,14 +526,16 @@ 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") + + # # 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 xlabels in ax.get_xticklabels(): + # xlabels.set_rotation(30) + # xlabels.set_horizontalalignment("right") _update_axes(ax, xincrease, yincrease, xscale, yscale, xticks, yticks, xlim, ylim) @@ -1087,13 +1090,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 + for axis, add_label, darray, suffix in zip( + ("x", "y", "z"), add_labels, darrays, suffixes ): if darray is None: continue @@ -1103,14 +1105,15 @@ 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) + # # 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") @overload @@ -1265,7 +1268,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() @@ -1653,14 +1656,16 @@ 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") + + # # 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 xlabels in ax.get_xticklabels(): + # xlabels.set_rotation(30) + # xlabels.set_horizontalalignment("right") return primitive diff --git a/xarray/plot/utils.py b/xarray/plot/utils.py index 5694acc06e8..136e1af0825 100644 --- a/xarray/plot/utils.py +++ b/xarray/plot/utils.py @@ -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 @@ -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) diff --git a/xarray/tests/test_plot.py b/xarray/tests/test_plot.py index 31c23955b02..87c0f92c340 100644 --- a/xarray/tests/test_plot.py +++ b/xarray/tests/test_plot.py @@ -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) @@ -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] @@ -1888,6 +1898,21 @@ def test_interval_breaks_logspace(self) -> None: class TestImshow(Common2dMixin, PlotTestCase): plotfunc = staticmethod(xplt.imshow) + @pytest.mark.xfail( + reason="Failing, should work. remove when 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 @@ -2032,6 +2057,21 @@ class TestSurface(Common2dMixin, PlotTestCase): plotfunc = staticmethod(xplt.surface) subplot_kws = {"projection": "3d"} + @pytest.mark.xfail( + reason="Failing, should work. remove when 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) From 6dcf3d5b388f47b82e87cba031f68ff17f921b46 Mon Sep 17 00:00:00 2001 From: Illviljan <14371165+Illviljan@users.noreply.github.com> Date: Mon, 13 Nov 2023 21:39:13 +0100 Subject: [PATCH 2/8] Update utils.py --- xarray/plot/utils.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/xarray/plot/utils.py b/xarray/plot/utils.py index 136e1af0825..903780b1137 100644 --- a/xarray/plot/utils.py +++ b/xarray/plot/utils.py @@ -1848,6 +1848,6 @@ def _set_concise_date(ax: Axes, axis: Literal["x", "y", "z"] = "x") -> None: locator = mdates.AutoDateLocator() formatter = mdates.ConciseDateFormatter(locator) - axis = getattr(ax, f"{axis}axis") - axis.set_major_locator(locator) - axis.set_major_formatter(formatter) + _axis = getattr(ax, f"{axis}axis") + _axis.set_major_locator(locator) + _axis.set_major_formatter(formatter) From 6d8bd9796983e552cefce9fdd00c20c3bdf3b9c9 Mon Sep 17 00:00:00 2001 From: Illviljan <14371165+Illviljan@users.noreply.github.com> Date: Mon, 13 Nov 2023 21:43:22 +0100 Subject: [PATCH 3/8] Update dataarray_plot.py --- xarray/plot/dataarray_plot.py | 1 + 1 file changed, 1 insertion(+) diff --git a/xarray/plot/dataarray_plot.py b/xarray/plot/dataarray_plot.py index b1b78693819..35b8c300038 100644 --- a/xarray/plot/dataarray_plot.py +++ b/xarray/plot/dataarray_plot.py @@ -1094,6 +1094,7 @@ def _add_labels( ) -> None: """Set x, y, z labels.""" add_labels = [add_labels] * 3 if isinstance(add_labels, bool) else add_labels + axis: Literal["x", "y", "z"] for axis, add_label, darray, suffix in zip( ("x", "y", "z"), add_labels, darrays, suffixes ): From 5b43628c3ad8a11c4b16305e011d9ead57251019 Mon Sep 17 00:00:00 2001 From: Illviljan <14371165+Illviljan@users.noreply.github.com> Date: Mon, 13 Nov 2023 21:51:50 +0100 Subject: [PATCH 4/8] Update dataarray_plot.py --- xarray/plot/dataarray_plot.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/xarray/plot/dataarray_plot.py b/xarray/plot/dataarray_plot.py index 35b8c300038..a580e48bc72 100644 --- a/xarray/plot/dataarray_plot.py +++ b/xarray/plot/dataarray_plot.py @@ -1094,10 +1094,8 @@ def _add_labels( ) -> None: """Set x, y, z labels.""" add_labels = [add_labels] * 3 if isinstance(add_labels, bool) else add_labels - axis: Literal["x", "y", "z"] - for axis, add_label, darray, suffix in zip( - ("x", "y", "z"), add_labels, darrays, suffixes - ): + 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 From 37265745d5f21b5fe5cde7e786cbe11763436e3f Mon Sep 17 00:00:00 2001 From: Illviljan <14371165+Illviljan@users.noreply.github.com> Date: Mon, 13 Nov 2023 22:00:14 +0100 Subject: [PATCH 5/8] Update whats-new.rst --- doc/whats-new.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/doc/whats-new.rst b/doc/whats-new.rst index 157282803cc..30e576edec2 100644 --- a/doc/whats-new.rst +++ b/doc/whats-new.rst @@ -26,6 +26,8 @@ New Features By `Deepak Cherian `_. (:issue:`7764`, :pull:`8373`). - Add ``DataArray.dt.total_seconds()`` method to match the Pandas API. (:pull:`8435`). By `Ben Mares `_. +- Use a concise format when plotting datetime arrays. (:pull:`8449`). + By `Jimmy Westling `_. Breaking changes ~~~~~~~~~~~~~~~~ From 73d7ae56454a56f3c8eb2f803b552830d9d4f950 Mon Sep 17 00:00:00 2001 From: Illviljan <14371165+Illviljan@users.noreply.github.com> Date: Mon, 13 Nov 2023 22:01:49 +0100 Subject: [PATCH 6/8] Cleanup --- xarray/plot/dataarray_plot.py | 23 ----------------------- 1 file changed, 23 deletions(-) diff --git a/xarray/plot/dataarray_plot.py b/xarray/plot/dataarray_plot.py index a580e48bc72..6da97a3faf0 100644 --- a/xarray/plot/dataarray_plot.py +++ b/xarray/plot/dataarray_plot.py @@ -529,14 +529,6 @@ def line( if np.issubdtype(xplt.dtype, np.datetime64): _set_concise_date(ax, axis="x") - # # 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 xlabels in ax.get_xticklabels(): - # xlabels.set_rotation(30) - # xlabels.set_horizontalalignment("right") - _update_axes(ax, xincrease, yincrease, xscale, yscale, xticks, yticks, xlim, ylim) return primitive @@ -1106,13 +1098,6 @@ def _add_labels( if np.issubdtype(darray.dtype, np.datetime64): _set_concise_date(ax, axis=axis) - # # 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") @overload @@ -1658,14 +1643,6 @@ def newplotfunc( if np.issubdtype(xplt.dtype, np.datetime64): _set_concise_date(ax, "x") - # # 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 xlabels in ax.get_xticklabels(): - # xlabels.set_rotation(30) - # xlabels.set_horizontalalignment("right") - return primitive # we want to actually expose the signature of newplotfunc From e5d81c01c084133596b32fb872b87746f9c1677a Mon Sep 17 00:00:00 2001 From: Illviljan <14371165+Illviljan@users.noreply.github.com> Date: Mon, 20 Nov 2023 20:34:58 +0100 Subject: [PATCH 7/8] Clarify xfail reason --- xarray/tests/test_plot.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/xarray/tests/test_plot.py b/xarray/tests/test_plot.py index 87c0f92c340..102d06b0289 100644 --- a/xarray/tests/test_plot.py +++ b/xarray/tests/test_plot.py @@ -1899,7 +1899,11 @@ class TestImshow(Common2dMixin, PlotTestCase): plotfunc = staticmethod(xplt.imshow) @pytest.mark.xfail( - reason="Failing, should work. remove when works, already in Common2dMixin" + 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 @@ -2058,7 +2062,11 @@ class TestSurface(Common2dMixin, PlotTestCase): subplot_kws = {"projection": "3d"} @pytest.mark.xfail( - reason="Failing, should work. remove when works, already in Common2dMixin" + 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 From fffd08a82aee8580bdd3052e6ae16b8c16b044d1 Mon Sep 17 00:00:00 2001 From: Illviljan <14371165+Illviljan@users.noreply.github.com> Date: Mon, 20 Nov 2023 20:42:14 +0100 Subject: [PATCH 8/8] Update whats-new.rst --- doc/whats-new.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/whats-new.rst b/doc/whats-new.rst index ee58d188161..3698058cfe8 100644 --- a/doc/whats-new.rst +++ b/doc/whats-new.rst @@ -23,6 +23,8 @@ v2023.11.1 (unreleased) New Features ~~~~~~~~~~~~ +- Use a concise format when plotting datetime arrays. (:pull:`8449`). + By `Jimmy Westling `_. Breaking changes ~~~~~~~~~~~~~~~~ @@ -69,8 +71,6 @@ New Features By `Deepak Cherian `_. (:issue:`7764`, :pull:`8373`). - Add ``DataArray.dt.total_seconds()`` method to match the Pandas API. (:pull:`8435`). By `Ben Mares `_. -- Use a concise format when plotting datetime arrays. (:pull:`8449`). - By `Jimmy Westling `_. - Allow passing ``region="auto"`` in :py:meth:`Dataset.to_zarr` to automatically infer the region to write in the original store. Also implement automatic transpose when dimension order does not match the original store. (:issue:`7702`, :issue:`8421`, :pull:`8434`).