From 9ddf6daf52bf63e7078301ec7bd68d096818fd17 Mon Sep 17 00:00:00 2001 From: Mathias Hauser Date: Mon, 6 Apr 2020 00:08:09 +0200 Subject: [PATCH 01/12] allow multiindex levels in plots --- doc/whats-new.rst | 2 ++ xarray/plot/plot.py | 16 ++++++++++++--- xarray/plot/utils.py | 30 +++++++++++++++++++++++---- xarray/tests/test_plot.py | 43 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 84 insertions(+), 7 deletions(-) diff --git a/doc/whats-new.rst b/doc/whats-new.rst index 5e890022e36..6b4158ed8d2 100644 --- a/doc/whats-new.rst +++ b/doc/whats-new.rst @@ -48,6 +48,8 @@ New Features By `Todd Jennings `_ - Allow plotting of boolean arrays. (:pull:`3766`) By `Marek Jacob `_ +- Enable using MultiIndex levels as cordinates in 1D and 2D plots (:issue:`3927`). + By `Mathias Hauser `_. Bug fixes ~~~~~~~~~ diff --git a/xarray/plot/plot.py b/xarray/plot/plot.py index 0fe302833d4..b8db48650a6 100644 --- a/xarray/plot/plot.py +++ b/xarray/plot/plot.py @@ -34,14 +34,24 @@ def _infer_line_data(darray, x, y, hue): ) ndims = len(darray.dims) - if x is not None and x not in darray.dims and x not in darray.coords: + if ( + x is not None + and x not in darray.dims + and x not in darray.coords + and x not in darray._level_coords + ): raise ValueError("x " + error_msg) - if y is not None and y not in darray.dims and y not in darray.coords: + if ( + y is not None + and y not in darray.dims + and y not in darray.coords + and y not in darray._level_coords + ): raise ValueError("y " + error_msg) if x is not None and y is not None: - raise ValueError("You cannot specify both x and y kwargs" "for line plots.") + raise ValueError("You cannot specify both x and y kwargs for line plots.") if ndims == 1: huename = None diff --git a/xarray/plot/utils.py b/xarray/plot/utils.py index 2734e446728..fc8fd0b2180 100644 --- a/xarray/plot/utils.py +++ b/xarray/plot/utils.py @@ -360,15 +360,37 @@ def _infer_xy_labels(darray, x, y, imshow=False, rgb=None): raise ValueError("DataArray must be 2d") y, x = darray.dims elif x is None: - if y not in darray.dims and y not in darray.coords: + if ( + y not in darray.dims + and y not in darray.coords + and y not in darray._level_coords + ): raise ValueError("y must be a dimension name if x is not supplied") x = darray.dims[0] if y == darray.dims[1] else darray.dims[1] elif y is None: - if x not in darray.dims and x not in darray.coords: + if ( + x not in darray.dims + and x not in darray.coords + and x not in darray._level_coords + ): raise ValueError("x must be a dimension name if y is not supplied") y = darray.dims[0] if x == darray.dims[1] else darray.dims[1] - elif any(k not in darray.coords and k not in darray.dims for k in (x, y)): - raise ValueError("x and y must be coordinate variables") + else: + if any( + k not in darray.coords + and k not in darray.dims + and k not in darray._level_coords + for k in (x, y) + ): + raise ValueError("x and y must be coordinate variables") + elif ( + all(k in darray._level_coords for k in (x, y)) + and darray._level_coords[x] == darray._level_coords[y] + ): + raise ValueError("x and y cannot be levels of the same MultiIndex") + elif darray._level_coords.get(x, x) == darray._level_coords.get(y, y): + raise ValueError("x and y cannot be a MultiIndex and one of its levels") + return x, y diff --git a/xarray/tests/test_plot.py b/xarray/tests/test_plot.py index 6482aa2400c..ad59e3242b2 100644 --- a/xarray/tests/test_plot.py +++ b/xarray/tests/test_plot.py @@ -164,6 +164,22 @@ def test_1d_x_y_kw(self): with raises_regex(ValueError, "None"): da.plot(x="z", y="f") + def test_multiindex_level_as_coord(self): + da = xr.DataArray( + np.arange(5), + dims="x", + coords=dict(a=("x", np.arange(5)), b=("x", np.arange(5, 10))), + ) + da = da.set_index(x=["a", "b"]) + + for x in ["a", "b"]: + h = da.plot(x=x)[0] + assert_array_equal(h.get_xdata(), da[x].values) + + for y in ["a", "b"]: + h = da.plot(y=y)[0] + assert_array_equal(h.get_ydata(), da[y].values) + # Test for bug in GH issue #2725 def test_infer_line_data(self): current = DataArray( @@ -1025,6 +1041,16 @@ def test_nonnumeric_index_raises_typeerror(self): with raises_regex(TypeError, r"[Pp]lot"): self.plotfunc(a) + def test_multiindex_raises_typeerror(self): + a = DataArray( + easy_array((3, 2)), + dims=("x", "y"), + coords=dict(x=("x", [0, 1, 2]), a=("y", [0, 1]), b=("y", [2, 3])), + ) + a = a.set_index(y=("a", "b")) + with raises_regex(TypeError, r"[Pp]lot"): + self.plotfunc(a) + def test_can_pass_in_axis(self): self.pass_in_axis(self.plotmethod) @@ -1176,6 +1202,23 @@ def test_non_linked_coords_transpose(self): # simply ensure that these high coords were passed over assert np.min(ax.get_xlim()) > 100.0 + def test_multiindex_level_as_coord(self): + da = DataArray( + easy_array((3, 2)), + dims=("x", "y"), + coords=dict(x=("x", [0, 1, 2]), a=("y", [0, 1]), b=("y", [2, 3])), + ) + da = da.set_index(y=["a", "b"]) + + for x, y in (("a", "x"), ("b", "x"), ("x", "a"), ("x", "b")): + self.plotfunc(da, x=x, y=y) + + with raises_regex(ValueError, "levels of the same MultiIndex"): + self.plotfunc(da, x="a", y="b") + + with raises_regex(ValueError, "MultiIndex and one of its levels"): + self.plotfunc(da, x="a", y="y") + def test_default_title(self): a = DataArray(easy_array((4, 3, 2)), dims=["a", "b", "c"]) a.coords["c"] = [0, 1] From dadfc8ccd4fc86e5d6347f9a32931d93c5a708cd Mon Sep 17 00:00:00 2001 From: Mathias Hauser Date: Wed, 15 Apr 2020 21:18:11 +0200 Subject: [PATCH 02/12] query label for test --- xarray/tests/test_plot.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/xarray/tests/test_plot.py b/xarray/tests/test_plot.py index 5c7fb2a52c4..db8f1e017ef 100644 --- a/xarray/tests/test_plot.py +++ b/xarray/tests/test_plot.py @@ -1219,6 +1219,10 @@ def test_multiindex_level_as_coord(self): for x, y in (("a", "x"), ("b", "x"), ("x", "a"), ("x", "b")): self.plotfunc(da, x=x, y=y) + ax = plt.gca() + assert x == ax.get_xlabel() + assert y == ax.get_ylabel() + with raises_regex(ValueError, "levels of the same MultiIndex"): self.plotfunc(da, x="a", y="b") From 6734d0227930122284d96a7dcc552dcebdbc022d Mon Sep 17 00:00:00 2001 From: Mathias Hauser Date: Wed, 15 Apr 2020 21:49:46 +0200 Subject: [PATCH 03/12] 2D plts adapt err msg --- xarray/plot/utils.py | 11 ++++++----- xarray/tests/test_plot.py | 11 ++++------- 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/xarray/plot/utils.py b/xarray/plot/utils.py index 17c43ba82d1..3fdef575617 100644 --- a/xarray/plot/utils.py +++ b/xarray/plot/utils.py @@ -364,6 +364,7 @@ def _infer_xy_labels(darray, x, y, imshow=False, rgb=None): if imshow and darray.ndim == 3: return _infer_xy_labels_3d(darray, x, y, rgb) + error_msg = "must be a dimension, coordinate or MultiIndex level name" if x is None and y is None: if darray.ndim != 2: raise ValueError("DataArray must be 2d") @@ -374,7 +375,7 @@ def _infer_xy_labels(darray, x, y, imshow=False, rgb=None): and y not in darray.coords and y not in darray._level_coords ): - raise ValueError("y must be a dimension name if x is not supplied") + raise ValueError(f"'y' {error_msg}") x = darray.dims[0] if y == darray.dims[1] else darray.dims[1] elif y is None: if ( @@ -382,7 +383,7 @@ def _infer_xy_labels(darray, x, y, imshow=False, rgb=None): and x not in darray.coords and x not in darray._level_coords ): - raise ValueError("x must be a dimension name if y is not supplied") + raise ValueError(f"'x' {error_msg}") y = darray.dims[0] if x == darray.dims[1] else darray.dims[1] else: if any( @@ -391,14 +392,14 @@ def _infer_xy_labels(darray, x, y, imshow=False, rgb=None): and k not in darray._level_coords for k in (x, y) ): - raise ValueError("x and y must be coordinate variables") + raise ValueError(f"'x' and 'y' {error_msg}s") elif ( all(k in darray._level_coords for k in (x, y)) and darray._level_coords[x] == darray._level_coords[y] ): - raise ValueError("x and y cannot be levels of the same MultiIndex") + raise ValueError("'x' and 'y' cannot be levels of the same MultiIndex") elif darray._level_coords.get(x, x) == darray._level_coords.get(y, y): - raise ValueError("x and y cannot be a MultiIndex and one of its levels") + raise ValueError("'x' and 'y' cannot be a MultiIndex and one of its levels") return x, y diff --git a/xarray/tests/test_plot.py b/xarray/tests/test_plot.py index db8f1e017ef..cfc34597415 100644 --- a/xarray/tests/test_plot.py +++ b/xarray/tests/test_plot.py @@ -1165,15 +1165,12 @@ def test_positional_coord_string(self): assert "y_long_name [y_units]" == ax.get_ylabel() def test_bad_x_string_exception(self): - with raises_regex(ValueError, "x and y must be coordinate variables"): + error_msg = "must be a dimension, coordinate or MultiIndex level name" + with raises_regex(ValueError, f"'x' and 'y' {error_msg}"): self.plotmethod("not_a_real_dim", "y") - with raises_regex( - ValueError, "x must be a dimension name if y is not supplied" - ): + with raises_regex(ValueError, f"'x' {error_msg}"): self.plotmethod(x="not_a_real_dim") - with raises_regex( - ValueError, "y must be a dimension name if x is not supplied" - ): + with raises_regex(ValueError, f"'y' {error_msg}"): self.plotmethod(y="not_a_real_dim") self.darray.coords["z"] = 100 From 7b0f80728eb6b0bb90321f1af8c12174a63bcd67 Mon Sep 17 00:00:00 2001 From: Mathias Hauser Date: Wed, 15 Apr 2020 22:11:01 +0200 Subject: [PATCH 04/12] 1D plts adapt err msg --- xarray/plot/plot.py | 8 +++----- xarray/tests/test_plot.py | 7 ++++--- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/xarray/plot/plot.py b/xarray/plot/plot.py index 4706a8e2185..d7adfd76a6b 100644 --- a/xarray/plot/plot.py +++ b/xarray/plot/plot.py @@ -29,9 +29,7 @@ def _infer_line_data(darray, x, y, hue): - error_msg = "must be either None or one of ({:s})".format( - ", ".join([repr(dd) for dd in darray.dims]) - ) + error_msg = "must be a dimension, coordinate, MultiIndex level name or None" ndims = len(darray.dims) if ( @@ -40,7 +38,7 @@ def _infer_line_data(darray, x, y, hue): and x not in darray.coords and x not in darray._level_coords ): - raise ValueError("x " + error_msg) + raise ValueError(f"x {error_msg}") if ( y is not None @@ -48,7 +46,7 @@ def _infer_line_data(darray, x, y, hue): and y not in darray.coords and y not in darray._level_coords ): - raise ValueError("y " + error_msg) + raise ValueError(f"y {error_msg}") if x is not None and y is not None: raise ValueError("You cannot specify both x and y kwargs for line plots.") diff --git a/xarray/tests/test_plot.py b/xarray/tests/test_plot.py index cfc34597415..4533fbd6754 100644 --- a/xarray/tests/test_plot.py +++ b/xarray/tests/test_plot.py @@ -155,13 +155,14 @@ def test_1d_x_y_kw(self): for aa, (x, y) in enumerate(xy): da.plot(x=x, y=y, ax=ax.flat[aa]) - with raises_regex(ValueError, "cannot"): + with raises_regex(ValueError, "cannot specify both"): da.plot(x="z", y="z") - with raises_regex(ValueError, "None"): + error_msg = "must be a dimension, coordinate, MultiIndex level name or None" + with raises_regex(ValueError, f"x {error_msg}"): da.plot(x="f", y="z") - with raises_regex(ValueError, "None"): + with raises_regex(ValueError, f"y {error_msg}"): da.plot(x="z", y="f") def test_multiindex_level_as_coord(self): From 22ff19f05e0efab0617b1c86c2809123ff57c020 Mon Sep 17 00:00:00 2001 From: Mathias Hauser Date: Wed, 15 Apr 2020 22:24:25 +0200 Subject: [PATCH 05/12] add errmsg x==y --- xarray/plot/utils.py | 4 +++- xarray/tests/test_plot.py | 4 ++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/xarray/plot/utils.py b/xarray/plot/utils.py index 3fdef575617..34f597ca31d 100644 --- a/xarray/plot/utils.py +++ b/xarray/plot/utils.py @@ -360,7 +360,9 @@ def _infer_xy_labels(darray, x, y, imshow=False, rgb=None): darray must be a 2 dimensional data array, or 3d for imshow only. """ - assert x is None or x != y + if (x is not None) and (x == y): + raise ValueError("'x' and 'y' cannot be equal.") + if imshow and darray.ndim == 3: return _infer_xy_labels_3d(darray, x, y, rgb) diff --git a/xarray/tests/test_plot.py b/xarray/tests/test_plot.py index 4533fbd6754..f9acaa3299b 100644 --- a/xarray/tests/test_plot.py +++ b/xarray/tests/test_plot.py @@ -1166,6 +1166,10 @@ def test_positional_coord_string(self): assert "y_long_name [y_units]" == ax.get_ylabel() def test_bad_x_string_exception(self): + + with raises_regex(ValueError, "'x' and 'y' cannot be equal."): + self.plotmethod(x="y", y="y") + error_msg = "must be a dimension, coordinate or MultiIndex level name" with raises_regex(ValueError, f"'x' and 'y' {error_msg}"): self.plotmethod("not_a_real_dim", "y") From 015a4bc8d3d1251b1aff1bd5c682bbfa939c16b8 Mon Sep 17 00:00:00 2001 From: Mathias Hauser Date: Tue, 21 Apr 2020 20:40:16 +0200 Subject: [PATCH 06/12] WIP _assert_xy_valid --- xarray/plot/utils.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/xarray/plot/utils.py b/xarray/plot/utils.py index 34f597ca31d..3710c17bcaf 100644 --- a/xarray/plot/utils.py +++ b/xarray/plot/utils.py @@ -406,6 +406,16 @@ def _infer_xy_labels(darray, x, y, imshow=False, rgb=None): return x, y +def _assert_valid_xy(darray, xy, name): + + # find MultiIndex + multiindex = set([darray._level_coords[lc] for lc in darray._level_coords]) + + valid_xy = ( + set(darray.dims) | set(darray.coords) | set(darray._level_coords) + ) - multiindex + + def get_axis(figsize, size, aspect, ax): import matplotlib as mpl import matplotlib.pyplot as plt From d9c984c872f226f56f0d92ac6604e9242d542ae0 Mon Sep 17 00:00:00 2001 From: Mathias Hauser Date: Tue, 21 Apr 2020 21:35:56 +0200 Subject: [PATCH 07/12] _assert_valid_xy --- xarray/plot/plot.py | 26 ++++++++----------------- xarray/plot/utils.py | 41 ++++++++++++++++----------------------- xarray/tests/test_plot.py | 22 ++++++++++----------- 3 files changed, 36 insertions(+), 53 deletions(-) diff --git a/xarray/plot/plot.py b/xarray/plot/plot.py index d7adfd76a6b..4c6d2a482dd 100644 --- a/xarray/plot/plot.py +++ b/xarray/plot/plot.py @@ -14,6 +14,7 @@ from .facetgrid import _easy_facetgrid from .utils import ( _add_colorbar, + _assert_valid_xy, _ensure_plottable, _infer_interval_breaks, _infer_xy_labels, @@ -29,27 +30,16 @@ def _infer_line_data(darray, x, y, hue): - error_msg = "must be a dimension, coordinate, MultiIndex level name or None" ndims = len(darray.dims) - if ( - x is not None - and x not in darray.dims - and x not in darray.coords - and x not in darray._level_coords - ): - raise ValueError(f"x {error_msg}") + if x is not None and y is not None: + raise ValueError("Cannot specify both x and y kwargs for line plots.") - if ( - y is not None - and y not in darray.dims - and y not in darray.coords - and y not in darray._level_coords - ): - raise ValueError(f"y {error_msg}") + if x is not None: + _assert_valid_xy(darray, x, "x") - if x is not None and y is not None: - raise ValueError("You cannot specify both x and y kwargs for line plots.") + if y is not None: + _assert_valid_xy(darray, y, "y") if ndims == 1: huename = None @@ -260,7 +250,7 @@ def line( Dimension or coordinate for which you want multiple lines plotted. If plotting against a 2D coordinate, ``hue`` must be a dimension. x, y : string, optional - Dimensions or coordinates for x, y axis. + Dimensions, coordinates or MultiIndex level for x, y axis. Only one of these may be specified. The other coordinate plots values from the DataArray on which this plot method is called. diff --git a/xarray/plot/utils.py b/xarray/plot/utils.py index 3710c17bcaf..238c91a508f 100644 --- a/xarray/plot/utils.py +++ b/xarray/plot/utils.py @@ -366,55 +366,48 @@ def _infer_xy_labels(darray, x, y, imshow=False, rgb=None): if imshow and darray.ndim == 3: return _infer_xy_labels_3d(darray, x, y, rgb) - error_msg = "must be a dimension, coordinate or MultiIndex level name" if x is None and y is None: if darray.ndim != 2: raise ValueError("DataArray must be 2d") y, x = darray.dims elif x is None: - if ( - y not in darray.dims - and y not in darray.coords - and y not in darray._level_coords - ): - raise ValueError(f"'y' {error_msg}") + _assert_valid_xy(darray, y, "y") x = darray.dims[0] if y == darray.dims[1] else darray.dims[1] elif y is None: - if ( - x not in darray.dims - and x not in darray.coords - and x not in darray._level_coords - ): - raise ValueError(f"'x' {error_msg}") + _assert_valid_xy(darray, x, "x") y = darray.dims[0] if x == darray.dims[1] else darray.dims[1] else: - if any( - k not in darray.coords - and k not in darray.dims - and k not in darray._level_coords - for k in (x, y) - ): - raise ValueError(f"'x' and 'y' {error_msg}s") - elif ( + _assert_valid_xy(darray, x, "x") + _assert_valid_xy(darray, y, "y") + + if ( all(k in darray._level_coords for k in (x, y)) and darray._level_coords[x] == darray._level_coords[y] ): raise ValueError("'x' and 'y' cannot be levels of the same MultiIndex") - elif darray._level_coords.get(x, x) == darray._level_coords.get(y, y): - raise ValueError("'x' and 'y' cannot be a MultiIndex and one of its levels") return x, y def _assert_valid_xy(darray, xy, name): + """ + make sure x and y passed to plotting functions are valid + """ - # find MultiIndex + # MultiIndex cannot be plotted; no point in allowing them here multiindex = set([darray._level_coords[lc] for lc in darray._level_coords]) valid_xy = ( set(darray.dims) | set(darray.coords) | set(darray._level_coords) ) - multiindex + if xy not in valid_xy: + valid_xy_str = "', '".join(sorted(list(valid_xy))) + if len(valid_xy) == 1: + raise ValueError(f"'{name}' must be None or '{valid_xy_str}'") + else: + raise ValueError(f"'{name}' must be one of None, '{valid_xy_str}'") + def get_axis(figsize, size, aspect, ax): import matplotlib as mpl diff --git a/xarray/tests/test_plot.py b/xarray/tests/test_plot.py index f9acaa3299b..026a5f9ba94 100644 --- a/xarray/tests/test_plot.py +++ b/xarray/tests/test_plot.py @@ -136,7 +136,7 @@ def test_label_from_attrs(self): def test1d(self): self.darray[:, 0, 0].plot() - with raises_regex(ValueError, "None"): + with raises_regex(ValueError, "'x' must be None or 'dim_0'"): self.darray[:, 0, 0].plot(x="dim_1") with raises_regex(TypeError, "complex128"): @@ -155,15 +155,15 @@ def test_1d_x_y_kw(self): for aa, (x, y) in enumerate(xy): da.plot(x=x, y=y, ax=ax.flat[aa]) - with raises_regex(ValueError, "cannot specify both"): + with raises_regex(ValueError, "Cannot specify both"): da.plot(x="z", y="z") - error_msg = "must be a dimension, coordinate, MultiIndex level name or None" - with raises_regex(ValueError, f"x {error_msg}"): - da.plot(x="f", y="z") + error_msg = "must be None or 'z'" + with raises_regex(ValueError, f"'x' {error_msg}"): + da.plot(x="f") - with raises_regex(ValueError, f"y {error_msg}"): - da.plot(x="z", y="f") + with raises_regex(ValueError, f"'y' {error_msg}"): + da.plot(y="f") def test_multiindex_level_as_coord(self): da = xr.DataArray( @@ -228,7 +228,7 @@ def test_2d_line(self): self.darray[:, :, 0].plot.line(x="dim_0", hue="dim_1") self.darray[:, :, 0].plot.line(y="dim_0", hue="dim_1") - with raises_regex(ValueError, "cannot"): + with raises_regex(ValueError, "Cannot"): self.darray[:, :, 0].plot.line(x="dim_1", y="dim_0", hue="dim_1") def test_2d_line_accepts_legend_kw(self): @@ -1170,8 +1170,8 @@ def test_bad_x_string_exception(self): with raises_regex(ValueError, "'x' and 'y' cannot be equal."): self.plotmethod(x="y", y="y") - error_msg = "must be a dimension, coordinate or MultiIndex level name" - with raises_regex(ValueError, f"'x' and 'y' {error_msg}"): + error_msg = "must be one of None, 'x', 'x2d', 'y', 'y2d'" + with raises_regex(ValueError, f"'x' {error_msg}"): self.plotmethod("not_a_real_dim", "y") with raises_regex(ValueError, f"'x' {error_msg}"): self.plotmethod(x="not_a_real_dim") @@ -1228,7 +1228,7 @@ def test_multiindex_level_as_coord(self): with raises_regex(ValueError, "levels of the same MultiIndex"): self.plotfunc(da, x="a", y="b") - with raises_regex(ValueError, "MultiIndex and one of its levels"): + with raises_regex(ValueError, "'y' must be one of None, 'a', 'b', 'x'"): self.plotfunc(da, x="a", y="y") def test_default_title(self): From 658cc0d501ecf6e14990f879e3c95f218262e60d Mon Sep 17 00:00:00 2001 From: Mathias Hauser Date: Wed, 22 Apr 2020 18:47:18 +0200 Subject: [PATCH 08/12] add 1D example --- doc/plotting.rst | 37 ++++++++++++++++++++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/doc/plotting.rst b/doc/plotting.rst index f3d9c0213de..28859c945ca 100644 --- a/doc/plotting.rst +++ b/doc/plotting.rst @@ -13,7 +13,7 @@ labels can also be used to easily create informative plots. xarray's plotting capabilities are centered around :py:class:`DataArray` objects. To plot :py:class:`Dataset` objects -simply access the relevant DataArrays, ie ``dset['var1']``. +simply access the relevant DataArrays, i.e. ``dset['var1']``. Dataset specific plotting routines are also available (see :ref:`plot-dataset`). Here we focus mostly on arrays 2d or larger. If your data fits nicely into a pandas DataFrame then you're better off using one of the more @@ -208,6 +208,41 @@ entire figure (as for matplotlib's ``figsize`` argument). .. _plotting.multiplelines: +========================= + Determine x-axis values +========================= + +Per default dimension coordinates are used for the x-axis (here the time coordinates). +However, you can also use non-dimension coordinates, MultiIndex levels, and dimensions +without coordinates along the x-axis. To illustrate this, let's calculate a 'decimal day' (epoch) +from the time and assign this as a non-dimension coordinate: + +.. ipython:: python + + decimal_day = (air1d.time - air1d.time[0]) / pd.Timedelta('1d') + air1d_multi = air1d.assign_coords(decimal_day=("time", decimal_day)) + air1d_multi + +To use ``'decimal_day'`` as x coordinate it must be explicitly specified: + +.. ipython:: python + + air1d_multi.plot(x="decimal_day") + +It is also possible to use a MultiIndex level: + +.. ipython:: python + + air1d_multi = air1d_multi.set_index(date=("time", "decimal_day")) + air1d_multi.plot(x="decimal_day") + +Finally, if a dataset does not have any coordinates it enumerates all data points: + +.. ipython:: python + + air1d_multi = air1d_multi.drop("date") + air1d_multi.plot() + ==================================================== Multiple lines showing variation along a dimension ==================================================== From 4a5f8a2ecd26b15aa28315ceaaaa400903a1990a Mon Sep 17 00:00:00 2001 From: Mathias Hauser Date: Thu, 23 Apr 2020 08:57:38 +0200 Subject: [PATCH 09/12] update docs --- doc/plotting.rst | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/doc/plotting.rst b/doc/plotting.rst index 28859c945ca..d1665cd3d08 100644 --- a/doc/plotting.rst +++ b/doc/plotting.rst @@ -229,7 +229,8 @@ To use ``'decimal_day'`` as x coordinate it must be explicitly specified: air1d_multi.plot(x="decimal_day") -It is also possible to use a MultiIndex level: +Creating a new MultiIndex named ``'date'`` from ``'time'`` and ``'decimal_day'``, +it is also possible to use a MultiIndex level as x-axis: .. ipython:: python @@ -243,6 +244,8 @@ Finally, if a dataset does not have any coordinates it enumerates all data point air1d_multi = air1d_multi.drop("date") air1d_multi.plot() +The same applies to 2D plots below. + ==================================================== Multiple lines showing variation along a dimension ==================================================== From 0a486bf26cf216840fb41ea413d1bbb789fb7221 Mon Sep 17 00:00:00 2001 From: Mathias Hauser Date: Thu, 23 Apr 2020 09:00:54 +0200 Subject: [PATCH 10/12] simplify error msg --- xarray/plot/utils.py | 7 ++----- xarray/tests/test_plot.py | 16 ++++++++-------- 2 files changed, 10 insertions(+), 13 deletions(-) diff --git a/xarray/plot/utils.py b/xarray/plot/utils.py index 238c91a508f..3d179c91fe4 100644 --- a/xarray/plot/utils.py +++ b/xarray/plot/utils.py @@ -402,11 +402,8 @@ def _assert_valid_xy(darray, xy, name): ) - multiindex if xy not in valid_xy: - valid_xy_str = "', '".join(sorted(list(valid_xy))) - if len(valid_xy) == 1: - raise ValueError(f"'{name}' must be None or '{valid_xy_str}'") - else: - raise ValueError(f"'{name}' must be one of None, '{valid_xy_str}'") + valid_xy_str = "', '".join(sorted(valid_xy)) + raise ValueError(f"'{name}' must be one of None, '{valid_xy_str}'") def get_axis(figsize, size, aspect, ax): diff --git a/xarray/tests/test_plot.py b/xarray/tests/test_plot.py index 026a5f9ba94..809453600b8 100644 --- a/xarray/tests/test_plot.py +++ b/xarray/tests/test_plot.py @@ -136,7 +136,7 @@ def test_label_from_attrs(self): def test1d(self): self.darray[:, 0, 0].plot() - with raises_regex(ValueError, "'x' must be None or 'dim_0'"): + with raises_regex(ValueError, "x must be one of None, 'dim_0'"): self.darray[:, 0, 0].plot(x="dim_1") with raises_regex(TypeError, "complex128"): @@ -158,11 +158,11 @@ def test_1d_x_y_kw(self): with raises_regex(ValueError, "Cannot specify both"): da.plot(x="z", y="z") - error_msg = "must be None or 'z'" - with raises_regex(ValueError, f"'x' {error_msg}"): + error_msg = "must be one of None, 'z'" + with raises_regex(ValueError, f"x {error_msg}"): da.plot(x="f") - with raises_regex(ValueError, f"'y' {error_msg}"): + with raises_regex(ValueError, f"y {error_msg}"): da.plot(y="f") def test_multiindex_level_as_coord(self): @@ -1171,11 +1171,11 @@ def test_bad_x_string_exception(self): self.plotmethod(x="y", y="y") error_msg = "must be one of None, 'x', 'x2d', 'y', 'y2d'" - with raises_regex(ValueError, f"'x' {error_msg}"): + with raises_regex(ValueError, f"x {error_msg}"): self.plotmethod("not_a_real_dim", "y") - with raises_regex(ValueError, f"'x' {error_msg}"): + with raises_regex(ValueError, f"x {error_msg}"): self.plotmethod(x="not_a_real_dim") - with raises_regex(ValueError, f"'y' {error_msg}"): + with raises_regex(ValueError, f"y {error_msg}"): self.plotmethod(y="not_a_real_dim") self.darray.coords["z"] = 100 @@ -1228,7 +1228,7 @@ def test_multiindex_level_as_coord(self): with raises_regex(ValueError, "levels of the same MultiIndex"): self.plotfunc(da, x="a", y="b") - with raises_regex(ValueError, "'y' must be one of None, 'a', 'b', 'x'"): + with raises_regex(ValueError, "y must be one of None, 'a', 'b', 'x'"): self.plotfunc(da, x="a", y="y") def test_default_title(self): From 28e22ba37c6822531bece1f8e8d9b84e7362e397 Mon Sep 17 00:00:00 2001 From: Mathias Hauser Date: Thu, 23 Apr 2020 09:01:51 +0200 Subject: [PATCH 11/12] remove ' --- xarray/plot/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/xarray/plot/utils.py b/xarray/plot/utils.py index 3d179c91fe4..66d36d51be1 100644 --- a/xarray/plot/utils.py +++ b/xarray/plot/utils.py @@ -403,7 +403,7 @@ def _assert_valid_xy(darray, xy, name): if xy not in valid_xy: valid_xy_str = "', '".join(sorted(valid_xy)) - raise ValueError(f"'{name}' must be one of None, '{valid_xy_str}'") + raise ValueError(f"{name} must be one of None, '{valid_xy_str}'") def get_axis(figsize, size, aspect, ax): From 0a0016b8ff0bea1be95cb86998f05eb9db212732 Mon Sep 17 00:00:00 2001 From: Mathias Hauser Date: Thu, 23 Apr 2020 09:20:44 +0200 Subject: [PATCH 12/12] Apply suggestions from code review --- doc/plotting.rst | 2 +- xarray/plot/plot.py | 2 +- xarray/plot/utils.py | 4 ++-- xarray/tests/test_plot.py | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/doc/plotting.rst b/doc/plotting.rst index d1665cd3d08..de89005959c 100644 --- a/doc/plotting.rst +++ b/doc/plotting.rst @@ -215,7 +215,7 @@ entire figure (as for matplotlib's ``figsize`` argument). Per default dimension coordinates are used for the x-axis (here the time coordinates). However, you can also use non-dimension coordinates, MultiIndex levels, and dimensions without coordinates along the x-axis. To illustrate this, let's calculate a 'decimal day' (epoch) -from the time and assign this as a non-dimension coordinate: +from the time and assign it as a non-dimension coordinate: .. ipython:: python diff --git a/xarray/plot/plot.py b/xarray/plot/plot.py index 4c6d2a482dd..099401f0af9 100644 --- a/xarray/plot/plot.py +++ b/xarray/plot/plot.py @@ -250,7 +250,7 @@ def line( Dimension or coordinate for which you want multiple lines plotted. If plotting against a 2D coordinate, ``hue`` must be a dimension. x, y : string, optional - Dimensions, coordinates or MultiIndex level for x, y axis. + Dimension, coordinate or MultiIndex level for x, y axis. Only one of these may be specified. The other coordinate plots values from the DataArray on which this plot method is called. diff --git a/xarray/plot/utils.py b/xarray/plot/utils.py index 66d36d51be1..30029784a69 100644 --- a/xarray/plot/utils.py +++ b/xarray/plot/utils.py @@ -361,7 +361,7 @@ def _infer_xy_labels(darray, x, y, imshow=False, rgb=None): darray must be a 2 dimensional data array, or 3d for imshow only. """ if (x is not None) and (x == y): - raise ValueError("'x' and 'y' cannot be equal.") + raise ValueError("x and y cannot be equal.") if imshow and darray.ndim == 3: return _infer_xy_labels_3d(darray, x, y, rgb) @@ -384,7 +384,7 @@ def _infer_xy_labels(darray, x, y, imshow=False, rgb=None): all(k in darray._level_coords for k in (x, y)) and darray._level_coords[x] == darray._level_coords[y] ): - raise ValueError("'x' and 'y' cannot be levels of the same MultiIndex") + raise ValueError("x and y cannot be levels of the same MultiIndex") return x, y diff --git a/xarray/tests/test_plot.py b/xarray/tests/test_plot.py index 809453600b8..e009cef610a 100644 --- a/xarray/tests/test_plot.py +++ b/xarray/tests/test_plot.py @@ -1167,7 +1167,7 @@ def test_positional_coord_string(self): def test_bad_x_string_exception(self): - with raises_regex(ValueError, "'x' and 'y' cannot be equal."): + with raises_regex(ValueError, "x and y cannot be equal."): self.plotmethod(x="y", y="y") error_msg = "must be one of None, 'x', 'x2d', 'y', 'y2d'"