From 4973c49f68ae9348d132533f5ca88abba95a1939 Mon Sep 17 00:00:00 2001 From: dcherian Date: Mon, 11 Jun 2018 17:54:26 -0700 Subject: [PATCH 1/9] Support xscale, yscale, xticks, yticks, xlim, ylim kwargs. --- xarray/plot/plot.py | 71 +++++++++++++++++++++++++++++++++------ xarray/tests/test_plot.py | 64 +++++++++++++++++++++++++++++++++++ 2 files changed, 125 insertions(+), 10 deletions(-) diff --git a/xarray/plot/plot.py b/xarray/plot/plot.py index 2a7fb08efda..3404a2e78e9 100644 --- a/xarray/plot/plot.py +++ b/xarray/plot/plot.py @@ -270,6 +270,10 @@ def line(darray, *args, **kwargs): Coordinates 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. + xscale, yscale : 'linear', 'symlog', 'log', 'logit', optional + Specifies scaling for the x- and y-axes respectively + xticks, yticks : Specify tick locations for x- and y-axes + xlim, ylim : Specify x- and y-axes limits xincrease : None, True, or False, optional Should the values on the x axes be increasing from left to right? if None, use the default for the matplotlib function. @@ -305,8 +309,14 @@ def line(darray, *args, **kwargs): hue = kwargs.pop('hue', None) x = kwargs.pop('x', None) y = kwargs.pop('y', None) - xincrease = kwargs.pop('xincrease', True) - yincrease = kwargs.pop('yincrease', True) + xincrease = kwargs.pop('xincrease', None) # default needs to be None + yincrease = kwargs.pop('yincrease', None) + xscale = kwargs.pop('xscale', None) # default needs to be None + yscale = kwargs.pop('yscale', None) + xticks = kwargs.pop('xticks', None) + yticks = kwargs.pop('yticks', None) + xlim = kwargs.pop('xlim', None) + ylim = kwargs.pop('ylim', None) add_legend = kwargs.pop('add_legend', True) _labels = kwargs.pop('_labels', True) if args is (): @@ -343,7 +353,8 @@ def line(darray, *args, **kwargs): xlabels.set_rotation(30) xlabels.set_ha('right') - _update_axes_limits(ax, xincrease, yincrease) + _update_axes(ax, xincrease, yincrease, xscale, yscale, + xticks, yticks, xlim, ylim) return primitive @@ -378,23 +389,35 @@ def hist(darray, figsize=None, size=None, aspect=None, ax=None, **kwargs): """ ax = get_axis(figsize, size, aspect, ax) + xincrease = kwargs.pop('xincrease', None) # default needs to be None + yincrease = kwargs.pop('yincrease', None) + xscale = kwargs.pop('xscale', None) # default needs to be None + yscale = kwargs.pop('yscale', None) + xticks = kwargs.pop('xticks', None) + yticks = kwargs.pop('yticks', None) + xlim = kwargs.pop('xlim', None) + ylim = kwargs.pop('ylim', None) + no_nan = np.ravel(darray.values) no_nan = no_nan[pd.notnull(no_nan)] primitive = ax.hist(no_nan, **kwargs) ax.set_ylabel('Count') - ax.set_title('Histogram') ax.set_xlabel(label_from_attrs(darray)) + _update_axes(ax, xincrease, yincrease, xscale, yscale, + xticks, yticks, xlim, ylim) + return primitive -def _update_axes_limits(ax, xincrease, yincrease): +def _update_axes(ax, xincrease, yincrease, + xscale=None, yscale=None, + xticks=None, yticks=None, xlim=None, ylim=None): """ - Update axes in place to increase or decrease - For use in _plot2d + Update axes with provided parameters """ if xincrease is None: pass @@ -410,6 +433,26 @@ def _update_axes_limits(ax, xincrease, yincrease): elif not yincrease: ax.set_ylim(sorted(ax.get_ylim(), reverse=True)) + # The default xscale, yscale needs to be None. + # If we set a scale it resets the axes formatters, + # This means that set_xscale('linear') on a datetime axis + # will remove the date labels. So only set the scale when explicitly + # asked to. https://github.com/matplotlib/matplotlib/issues/8740 + if xscale is not None: + ax.set_xscale(xscale) + if yscale is not None: + ax.set_yscale(yscale) + + if xticks is not None: + ax.set_xticks(xticks) + if yticks is not None: + ax.set_yticks(yticks) + + if xlim is not None: + ax.set_xlim(xlim) + if ylim is not None: + ax.set_ylim(ylim) + # MUST run before any 2d plotting functions are defined since # _plot2d decorator adds them as methods here. @@ -500,6 +543,10 @@ def _plot2d(plotfunc): If passed, make column faceted plots on this dimension name col_wrap : integer, optional Use together with ``col`` to wrap faceted plots + xscale, yscale : 'linear', 'symlog', 'log', 'logit', optional + Specifies scaling for the x- and y-axes respectively + xticks, yticks : Specify tick locations for x- and y-axes + xlim, ylim : Specify x- and y-axes limits xincrease : None, True, or False, optional Should the values on the x axes be increasing from left to right? if None, use the default for the matplotlib function. @@ -577,7 +624,8 @@ def newplotfunc(darray, x=None, y=None, figsize=None, size=None, cmap=None, center=None, robust=False, extend=None, levels=None, infer_intervals=None, colors=None, subplot_kws=None, cbar_ax=None, cbar_kwargs=None, - **kwargs): + xscale=None, yscale=None, xticks=None, yticks=None, + xlim=None, ylim=None, **kwargs): # All 2d plots in xarray share this function signature. # Method signature below should be consistent. @@ -723,7 +771,8 @@ def newplotfunc(darray, x=None, y=None, figsize=None, size=None, raise ValueError("cbar_ax and cbar_kwargs can't be used with " "add_colorbar=False.") - _update_axes_limits(ax, xincrease, yincrease) + _update_axes(ax, xincrease, yincrease, xscale, yscale, + xticks, yticks, xlim, ylim) # Rotate dates on xlabels if np.issubdtype(xval.dtype, np.datetime64): @@ -739,7 +788,9 @@ def plotmethod(_PlotMethods_obj, x=None, y=None, figsize=None, size=None, add_labels=True, vmin=None, vmax=None, cmap=None, colors=None, center=None, robust=False, extend=None, levels=None, infer_intervals=None, subplot_kws=None, - cbar_ax=None, cbar_kwargs=None, **kwargs): + cbar_ax=None, cbar_kwargs=None, + xscale=None, yscale=None, xticks=None, yticks=None, + xlim=None, ylim=None, **kwargs): """ The method should have the same signature as the function. diff --git a/xarray/tests/test_plot.py b/xarray/tests/test_plot.py index 90d30946c9c..085c88a2c62 100644 --- a/xarray/tests/test_plot.py +++ b/xarray/tests/test_plot.py @@ -1675,3 +1675,67 @@ def test_plot_cftime_data_error(): data = DataArray(data, coords=[np.arange(5)], dims=['x']) with raises_regex(NotImplementedError, 'cftime.datetime'): data.plot() + + +test_da_list = [DataArray(easy_array((10, ))), + DataArray(easy_array((10, 3))), + DataArray(easy_array((10, 3, 2)))] + + +class TestAxesKwargs(object): + @pytest.mark.parametrize('da', test_da_list) + @pytest.mark.parametrize('xincrease', [True, False]) + def test_xincrease_kwarg(self, da, xincrease): + plt.clf() + da.plot(xincrease=xincrease) + assert(plt.gca().xaxis_inverted() == (not xincrease)) + + @pytest.mark.parametrize('da', test_da_list) + @pytest.mark.parametrize('yincrease', [True, False]) + def test_xincrease_kwarg(self, da, yincrease): + plt.clf() + da.plot(yincrease=yincrease) + assert(plt.gca().yaxis_inverted() == (not yincrease)) + + @pytest.mark.parametrize('da', test_da_list) + @pytest.mark.parametrize('xscale', ['linear', 'log', 'logit', 'symlog']) + def test_xscale_kwarg(self, da, xscale): + plt.clf() + da.plot(xscale=xscale) + assert(plt.gca().get_xscale() == xscale) + + @pytest.mark.parametrize('da', [DataArray(easy_array((10, ))), + DataArray(easy_array((10, 3)))]) + @pytest.mark.parametrize('yscale', ['linear', 'log', 'logit', 'symlog']) + def test_yscale_kwarg(self, da, yscale): + plt.clf() + da.plot(yscale=yscale) + assert(plt.gca().get_yscale() == yscale) + + @pytest.mark.parametrize('da', test_da_list) + def test_xlim_kwarg(self, da): + plt.clf() + expected = (0.0, 1000.0) + da.plot(xlim=[0, 1000]) + assert(plt.gca().get_xlim() == expected) + + @pytest.mark.parametrize('da', test_da_list) + def test_ylim_kwarg(self, da): + plt.clf() + da.plot(ylim=[0, 1000]) + expected = (0.0, 1000.0) + assert(plt.gca().get_ylim() == expected) + + @pytest.mark.parametrize('da', test_da_list) + def test_xticks_kwarg(self, da): + plt.clf() + da.plot(xticks=np.arange(5)) + expected = np.arange(5).tolist() + assert(np.all(plt.gca().get_xticks() == expected)) + + @pytest.mark.parametrize('da', test_da_list) + def test_yticks_kwarg(self, da): + plt.clf() + da.plot(yticks=np.arange(5)) + expected = np.arange(5) + assert(np.all(plt.gca().get_yticks() == expected)) From 0ab162cac1b2953817997f322f5f8058275ceabf Mon Sep 17 00:00:00 2001 From: dcherian Date: Tue, 10 Jul 2018 10:48:38 -0600 Subject: [PATCH 2/9] Forgot to replace autofmt_xdate for 2D plots. --- xarray/plot/plot.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/xarray/plot/plot.py b/xarray/plot/plot.py index 3404a2e78e9..55b19b97ff9 100644 --- a/xarray/plot/plot.py +++ b/xarray/plot/plot.py @@ -775,8 +775,13 @@ def newplotfunc(darray, x=None, y=None, figsize=None, size=None, 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(xval.dtype, np.datetime64): - ax.get_figure().autofmt_xdate() + for xlabels in ax.get_xticklabels(): + xlabels.set_rotation(30) + xlabels.set_ha('right') return primitive From 9f220ba6d4b2f25cb27cdc570132fe58998f8c59 Mon Sep 17 00:00:00 2001 From: dcherian Date: Mon, 16 Jul 2018 17:00:52 -0600 Subject: [PATCH 3/9] Use matplotlib's axis inverting methods. --- xarray/plot/plot.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/xarray/plot/plot.py b/xarray/plot/plot.py index 55b19b97ff9..fbbb0aaf8de 100644 --- a/xarray/plot/plot.py +++ b/xarray/plot/plot.py @@ -421,17 +421,17 @@ def _update_axes(ax, xincrease, yincrease, """ if xincrease is None: pass - elif xincrease: - ax.set_xlim(sorted(ax.get_xlim())) - elif not xincrease: - ax.set_xlim(sorted(ax.get_xlim(), reverse=True)) + elif xincrease and ax.xaxis_inverted(): + ax.invert_xaxis() + elif not xincrease and not ax.xaxis_inverted(): + ax.invert_xaxis() if yincrease is None: pass - elif yincrease: - ax.set_ylim(sorted(ax.get_ylim())) - elif not yincrease: - ax.set_ylim(sorted(ax.get_ylim(), reverse=True)) + elif yincrease and ax.yaxis_inverted(): + ax.invert_yaxis() + elif not yincrease and not ax.yaxis_inverted(): + ax.invert_yaxis() # The default xscale, yscale needs to be None. # If we set a scale it resets the axes formatters, From 83f1d246bbaaeca1bd672636b8df135a69983dbf Mon Sep 17 00:00:00 2001 From: dcherian Date: Mon, 16 Jul 2018 17:09:05 -0600 Subject: [PATCH 4/9] Add what's new and docs. --- doc/plotting.rst | 7 ++++--- doc/whats-new.rst | 3 +++ 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/doc/plotting.rst b/doc/plotting.rst index 54fa2f57ac8..f35eb765053 100644 --- a/doc/plotting.rst +++ b/doc/plotting.rst @@ -212,8 +212,6 @@ If required, the automatic legend can be turned off using ``add_legend=False``. ``hue`` can be passed directly to :py:func:`xarray.plot` as `air.isel(lon=10, lat=[19,21,22]).plot(hue='lat')`. - - Dimension along y-axis ~~~~~~~~~~~~~~~~~~~~~~ @@ -224,7 +222,7 @@ It is also possible to make line plots such that the data are on the x-axis and @savefig plotting_example_xy_kwarg.png air.isel(time=10, lon=[10, 11]).plot(y='lat', hue='lon') -Changing Axes Direction +Other axes kwargs ----------------------- The keyword arguments ``xincrease`` and ``yincrease`` let you control the axes direction. @@ -234,6 +232,9 @@ The keyword arguments ``xincrease`` and ``yincrease`` let you control the axes d @savefig plotting_example_xincrease_yincrease_kwarg.png air.isel(time=10, lon=[10, 11]).plot.line(y='lat', hue='lon', xincrease=False, yincrease=False) +In addition, one can use ``xscale, yscale`` to set axes scaling; ``xticks, yticks`` to set axes ticks and ``xlim, ylim`` to set axes limits. These accept the same values as the matplotlib methods ``Axes.set_(x,y)scale()``, ``Axes.set_(x,y)ticks()``, ``Axes.set_(x,y)lim()`` respectively. + + Two Dimensions -------------- diff --git a/doc/whats-new.rst b/doc/whats-new.rst index 48be14f6d50..d0e070a55d1 100644 --- a/doc/whats-new.rst +++ b/doc/whats-new.rst @@ -61,6 +61,9 @@ Breaking changes Enhancements ~~~~~~~~~~~~ +- :py:meth:`plot()` now accepts the kwargs ``xscale, yscale, xlim, ylim, xticks, yticks`` just like Pandas. Also ``xincrease=False, yincrease=False`` now use matplotlib's axis inverting methods instead of setting limits. + By `Deepak Cherian `_. (:issue:`2224`) + - :py:meth:`~xarray.DataArray.interp_like` and :py:meth:`~xarray.Dataset.interp_like` methods are newly added. (:issue:`2218`) From 092dd29ea5137efc88e5c39e5f96e0814394b8a9 Mon Sep 17 00:00:00 2001 From: dcherian Date: Mon, 16 Jul 2018 17:26:06 -0600 Subject: [PATCH 5/9] doh --- xarray/tests/test_plot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/xarray/tests/test_plot.py b/xarray/tests/test_plot.py index 085c88a2c62..d57013d51ff 100644 --- a/xarray/tests/test_plot.py +++ b/xarray/tests/test_plot.py @@ -1692,7 +1692,7 @@ def test_xincrease_kwarg(self, da, xincrease): @pytest.mark.parametrize('da', test_da_list) @pytest.mark.parametrize('yincrease', [True, False]) - def test_xincrease_kwarg(self, da, yincrease): + def test_yincrease_kwarg(self, da, yincrease): plt.clf() da.plot(yincrease=yincrease) assert(plt.gca().yaxis_inverted() == (not yincrease)) From b90a7ef9873a67dd8e151620a5ae0c87b575d2f3 Mon Sep 17 00:00:00 2001 From: dcherian Date: Tue, 17 Jul 2018 12:04:56 -0600 Subject: [PATCH 6/9] Minor fixes. Don't automatically set histogram ylabel to be 'count'. hist can be used to plot PDFs, in which case that label would be wrong. --- xarray/plot/plot.py | 4 ++-- xarray/tests/test_plot.py | 5 +---- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/xarray/plot/plot.py b/xarray/plot/plot.py index fbbb0aaf8de..179f41e9e42 100644 --- a/xarray/plot/plot.py +++ b/xarray/plot/plot.py @@ -403,7 +403,6 @@ def hist(darray, figsize=None, size=None, aspect=None, ax=None, **kwargs): primitive = ax.hist(no_nan, **kwargs) - ax.set_ylabel('Count') ax.set_title('Histogram') ax.set_xlabel(label_from_attrs(darray)) @@ -415,7 +414,8 @@ def hist(darray, figsize=None, size=None, aspect=None, ax=None, **kwargs): def _update_axes(ax, xincrease, yincrease, xscale=None, yscale=None, - xticks=None, yticks=None, xlim=None, ylim=None): + xticks=None, yticks=None, + xlim=None, ylim=None): """ Update axes with provided parameters """ diff --git a/xarray/tests/test_plot.py b/xarray/tests/test_plot.py index d57013d51ff..091e52d80aa 100644 --- a/xarray/tests/test_plot.py +++ b/xarray/tests/test_plot.py @@ -426,10 +426,6 @@ def test_xlabel_uses_name(self): self.darray.plot.hist() assert 'testpoints [testunits]' == plt.gca().get_xlabel() - def test_ylabel_is_count(self): - self.darray.plot.hist() - assert 'Count' == plt.gca().get_ylabel() - def test_title_is_histogram(self): self.darray.plot.hist() assert 'Histogram' == plt.gca().get_title() @@ -1682,6 +1678,7 @@ def test_plot_cftime_data_error(): DataArray(easy_array((10, 3, 2)))] +@requires_matplotlib class TestAxesKwargs(object): @pytest.mark.parametrize('da', test_da_list) @pytest.mark.parametrize('xincrease', [True, False]) From f82ae0a7836709cadfb94743d71e336858ffb70c Mon Sep 17 00:00:00 2001 From: Stephan Hoyer Date: Tue, 17 Jul 2018 22:02:46 -0700 Subject: [PATCH 7/9] fix section header --- doc/plotting.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/plotting.rst b/doc/plotting.rst index f35eb765053..43faa83b9da 100644 --- a/doc/plotting.rst +++ b/doc/plotting.rst @@ -223,7 +223,7 @@ It is also possible to make line plots such that the data are on the x-axis and air.isel(time=10, lon=[10, 11]).plot(y='lat', hue='lon') Other axes kwargs ------------------------ +----------------- The keyword arguments ``xincrease`` and ``yincrease`` let you control the axes direction. From 8947d4319c7ac8c329aa9bbd9d3db7704f3d0894 Mon Sep 17 00:00:00 2001 From: dcherian Date: Wed, 18 Jul 2018 10:17:52 -0700 Subject: [PATCH 8/9] Fix assert statements. --- xarray/tests/test_plot.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/xarray/tests/test_plot.py b/xarray/tests/test_plot.py index 091e52d80aa..4e5ea8fc623 100644 --- a/xarray/tests/test_plot.py +++ b/xarray/tests/test_plot.py @@ -1685,21 +1685,21 @@ class TestAxesKwargs(object): def test_xincrease_kwarg(self, da, xincrease): plt.clf() da.plot(xincrease=xincrease) - assert(plt.gca().xaxis_inverted() == (not xincrease)) + assert plt.gca().xaxis_inverted() == (not xincrease) @pytest.mark.parametrize('da', test_da_list) @pytest.mark.parametrize('yincrease', [True, False]) def test_yincrease_kwarg(self, da, yincrease): plt.clf() da.plot(yincrease=yincrease) - assert(plt.gca().yaxis_inverted() == (not yincrease)) + assert plt.gca().yaxis_inverted() == (not yincrease) @pytest.mark.parametrize('da', test_da_list) @pytest.mark.parametrize('xscale', ['linear', 'log', 'logit', 'symlog']) def test_xscale_kwarg(self, da, xscale): plt.clf() da.plot(xscale=xscale) - assert(plt.gca().get_xscale() == xscale) + assert plt.gca().get_xscale() == xscale @pytest.mark.parametrize('da', [DataArray(easy_array((10, ))), DataArray(easy_array((10, 3)))]) @@ -1707,32 +1707,32 @@ def test_xscale_kwarg(self, da, xscale): def test_yscale_kwarg(self, da, yscale): plt.clf() da.plot(yscale=yscale) - assert(plt.gca().get_yscale() == yscale) + assert plt.gca().get_yscale() == yscale @pytest.mark.parametrize('da', test_da_list) def test_xlim_kwarg(self, da): plt.clf() expected = (0.0, 1000.0) da.plot(xlim=[0, 1000]) - assert(plt.gca().get_xlim() == expected) + assert plt.gca().get_xlim() == expected @pytest.mark.parametrize('da', test_da_list) def test_ylim_kwarg(self, da): plt.clf() da.plot(ylim=[0, 1000]) expected = (0.0, 1000.0) - assert(plt.gca().get_ylim() == expected) + assert plt.gca().get_ylim() == expected @pytest.mark.parametrize('da', test_da_list) def test_xticks_kwarg(self, da): plt.clf() da.plot(xticks=np.arange(5)) expected = np.arange(5).tolist() - assert(np.all(plt.gca().get_xticks() == expected)) + assert np.all(plt.gca().get_xticks() == expected) @pytest.mark.parametrize('da', test_da_list) def test_yticks_kwarg(self, da): plt.clf() da.plot(yticks=np.arange(5)) expected = np.arange(5) - assert(np.all(plt.gca().get_yticks() == expected)) + assert np.all(plt.gca().get_yticks() == expected) From 4c767d3b84b3dac764c663deb92e2e994cfed0c3 Mon Sep 17 00:00:00 2001 From: dcherian Date: Wed, 18 Jul 2018 10:21:24 -0700 Subject: [PATCH 9/9] fix whats-new --- doc/whats-new.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/whats-new.rst b/doc/whats-new.rst index d0e070a55d1..ea8f7a75c96 100644 --- a/doc/whats-new.rst +++ b/doc/whats-new.rst @@ -36,6 +36,9 @@ Documentation Enhancements ~~~~~~~~~~~~ +- :py:meth:`plot()` now accepts the kwargs ``xscale, yscale, xlim, ylim, xticks, yticks`` just like Pandas. Also ``xincrease=False, yincrease=False`` now use matplotlib's axis inverting methods instead of setting limits. + By `Deepak Cherian `_. (:issue:`2224`) + Bug fixes ~~~~~~~~~ @@ -61,9 +64,6 @@ Breaking changes Enhancements ~~~~~~~~~~~~ -- :py:meth:`plot()` now accepts the kwargs ``xscale, yscale, xlim, ylim, xticks, yticks`` just like Pandas. Also ``xincrease=False, yincrease=False`` now use matplotlib's axis inverting methods instead of setting limits. - By `Deepak Cherian `_. (:issue:`2224`) - - :py:meth:`~xarray.DataArray.interp_like` and :py:meth:`~xarray.Dataset.interp_like` methods are newly added. (:issue:`2218`)