From 8a1a8a1f13462440543581cd12fb96f22f0748bc Mon Sep 17 00:00:00 2001 From: Julius Busecke Date: Thu, 7 Feb 2019 19:11:13 -0500 Subject: [PATCH] enable internal plotting with cftime datetime (#2665) enable internal plotting with cftime datetime --- ci/requirements-py37-windows.yml | 1 + ci/requirements-py37.yml | 1 + doc/installing.rst | 4 +- doc/plotting.rst | 9 +++- doc/time-series.rst | 31 +++++-------- doc/whats-new.rst | 6 ++- xarray/core/common.py | 2 +- xarray/plot/plot.py | 11 ----- xarray/plot/utils.py | 35 +++++++++++--- xarray/tests/__init__.py | 2 + xarray/tests/test_plot.py | 80 +++++++++++++++++++++++--------- xarray/util/print_versions.py | 1 + 12 files changed, 120 insertions(+), 63 deletions(-) diff --git a/ci/requirements-py37-windows.yml b/ci/requirements-py37-windows.yml index 24a7f556b2c..fb4b97cde7c 100644 --- a/ci/requirements-py37-windows.yml +++ b/ci/requirements-py37-windows.yml @@ -4,6 +4,7 @@ channels: dependencies: - python=3.7 - cftime + - nc-time-axis - dask - distributed - h5py diff --git a/ci/requirements-py37.yml b/ci/requirements-py37.yml index 1a98e6b285c..4f4d2b1728b 100644 --- a/ci/requirements-py37.yml +++ b/ci/requirements-py37.yml @@ -4,6 +4,7 @@ channels: dependencies: - python=3.7 - cftime + - nc-time-axis - dask - distributed - h5py diff --git a/doc/installing.rst b/doc/installing.rst index 8054a601870..f624da18611 100644 --- a/doc/installing.rst +++ b/doc/installing.rst @@ -61,6 +61,8 @@ For plotting :ref:`plot-maps` - `seaborn `__: for better color palettes +- `nc-time-axis `__: for plotting + cftime.datetime objects (1.2.0 or later) Instructions @@ -109,4 +111,4 @@ To run these benchmark tests in a local machine, first install - `airspeed-velocity `__: a tool for benchmarking Python packages over their lifetime. and run -``asv run # this will install some conda environments in ./.asv/envs`` \ No newline at end of file +``asv run # this will install some conda environments in ./.asv/envs`` diff --git a/doc/plotting.rst b/doc/plotting.rst index 1cb7aebe96d..a705c683594 100644 --- a/doc/plotting.rst +++ b/doc/plotting.rst @@ -23,6 +23,11 @@ Matplotlib syntax and function names were copied as much as possible, which makes for an easy transition between the two. Matplotlib must be installed before xarray can plot. +To use xarray's plotting capabilities with time coordinates containing +``cftime.datetime`` objects +`nc-time-axis `_ v1.2.0 or later +needs to be installed. + For more extensive plotting applications consider the following projects: - `Seaborn `_: "provides @@ -226,7 +231,7 @@ Step plots ~~~~~~~~~~ As an alternative, also a step plot similar to matplotlib's ``plt.step`` can be -made using 1D data. +made using 1D data. .. ipython:: python @@ -248,7 +253,7 @@ when plotting data grouped with :py:func:`xarray.Dataset.groupby_bins`. plt.ylim(-20,30) @savefig plotting_example_step_groupby.png width=4in plt.title('Zonal mean temperature') - + In this case, the actual boundaries of the bins are used and the ``where`` argument is ignored. diff --git a/doc/time-series.rst b/doc/time-series.rst index b716d6cbc24..3249dad2ec6 100644 --- a/doc/time-series.rst +++ b/doc/time-series.rst @@ -74,7 +74,7 @@ will be used for indexing. :py:class:`~xarray.CFTimeIndex` enables a subset of the indexing functionality of a :py:class:`pandas.DatetimeIndex` and is only fully compatible with the standalone version of ``cftime`` (not the version packaged with earlier versions ``netCDF4``). See :ref:`CFTimeIndex` for more -information. +information. Datetime indexing ----------------- @@ -215,7 +215,7 @@ For more examples of using grouped operations on a time dimension, see .. _CFTimeIndex: - + Non-standard calendars and dates outside the Timestamp-valid range ------------------------------------------------------------------ @@ -224,14 +224,14 @@ Through the standalone ``cftime`` library and a custom subclass of functionality enabled through the standard :py:class:`pandas.DatetimeIndex` for dates from non-standard calendars commonly used in climate science or dates using a standard calendar, but outside the `Timestamp-valid range`_ -(approximately between years 1678 and 2262). +(approximately between years 1678 and 2262). .. note:: As of xarray version 0.11, by default, :py:class:`cftime.datetime` objects will be used to represent times (either in indexes, as a - :py:class:`~xarray.CFTimeIndex`, or in data arrays with dtype object) if - any of the following are true: + :py:class:`~xarray.CFTimeIndex`, or in data arrays with dtype object) if + any of the following are true: - The dates are from a non-standard calendar - Any dates are outside the Timestamp-valid range. @@ -252,7 +252,7 @@ coordinate with dates from a no-leap calendar and a dates = [DatetimeNoLeap(year, month, 1) for year, month in product(range(1, 3), range(1, 13))] da = xr.DataArray(np.arange(24), coords=[dates], dims=['time'], name='foo') - + xarray also includes a :py:func:`~xarray.cftime_range` function, which enables creating a :py:class:`~xarray.CFTimeIndex` with regularly-spaced dates. For instance, we can create the same dates and DataArray we created above using: @@ -261,12 +261,12 @@ instance, we can create the same dates and DataArray we created above using: dates = xr.cftime_range(start='0001', periods=24, freq='MS', calendar='noleap') da = xr.DataArray(np.arange(24), coords=[dates], dims=['time'], name='foo') - + For data indexed by a :py:class:`~xarray.CFTimeIndex` xarray currently supports: - `Partial datetime string indexing`_ using strictly `ISO 8601-format`_ partial datetime strings: - + .. ipython:: python da.sel(time='0001') @@ -274,7 +274,7 @@ For data indexed by a :py:class:`~xarray.CFTimeIndex` xarray currently supports: - Access of basic datetime components via the ``dt`` accessor (in this case just "year", "month", "day", "hour", "minute", "second", "microsecond", - "season", "dayofyear", and "dayofweek"): + "season", "dayofyear", and "dayofweek"): .. ipython:: python @@ -323,14 +323,7 @@ For data indexed by a :py:class:`~xarray.CFTimeIndex` xarray currently supports: da.resample(time='81T', closed='right', label='right', base=3).mean() .. note:: - - While much of the time series functionality that is possible for standard - dates has been implemented for dates from non-standard calendars, there are - still some remaining important features that have yet to be implemented, - for example: - - - Built-in plotting of data with :py:class:`cftime.datetime` coordinate axes - (:issue:`2164`). + For some use-cases it may still be useful to convert from a :py:class:`~xarray.CFTimeIndex` to a :py:class:`pandas.DatetimeIndex`, @@ -351,8 +344,8 @@ For data indexed by a :py:class:`~xarray.CFTimeIndex` xarray currently supports: do not depend on differences between dates (e.g. differentiation, interpolation, or upsampling with resample), as these could introduce subtle and silent errors due to the difference in calendar types between the dates - encoded in your data and the dates stored in memory. - + encoded in your data and the dates stored in memory. + .. _Timestamp-valid range: https://pandas.pydata.org/pandas-docs/stable/timeseries.html#timestamp-limitations .. _ISO 8601-format: https://en.wikipedia.org/wiki/ISO_8601 .. _partial datetime string indexing: https://pandas.pydata.org/pandas-docs/stable/timeseries.html#partial-string-indexing diff --git a/doc/whats-new.rst b/doc/whats-new.rst index 694b6b27ce8..1b9ba5707e8 100644 --- a/doc/whats-new.rst +++ b/doc/whats-new.rst @@ -24,7 +24,7 @@ Breaking changes - Remove support for Python 2. This is the first version of xarray that is Python 3 only. (:issue:`1876`). By `Joe Hamman `_. -- The `compat` argument to `Dataset` and the `encoding` argument to +- The `compat` argument to `Dataset` and the `encoding` argument to `DataArray` are deprecated and will be removed in a future release. (:issue:`1188`) By `Maximilian Roos `_. @@ -34,6 +34,10 @@ Breaking changes Enhancements ~~~~~~~~~~~~ +- Internal plotting now supports ``cftime.datetime`` objects as time series. + (:issue:`2164`) + By `Julius Busecke `_ and + `Spencer Clark `_. - Add ``data=False`` option to ``to_dict()`` methods. (:issue:`2656`) By `Ryan Abernathey `_ - :py:meth:`~xarray.DataArray.coarsen` and diff --git a/xarray/core/common.py b/xarray/core/common.py index 30ea56f3496..2f32ca941be 100644 --- a/xarray/core/common.py +++ b/xarray/core/common.py @@ -713,7 +713,7 @@ def resample(self, indexer=None, skipna=None, closed=None, label=None, array([ 0. , 0.032258, 0.064516, ..., 10.935484, 10.967742, 11. ]) Coordinates: * time (time) datetime64[ns] 1999-12-15 1999-12-16 1999-12-17 ... - + Limit scope of upsampling method >>> da.resample(time='1D').nearest(tolerance='1D') diff --git a/xarray/plot/plot.py b/xarray/plot/plot.py index 5b60f8d73a1..8e2457603d6 100644 --- a/xarray/plot/plot.py +++ b/xarray/plot/plot.py @@ -10,8 +10,6 @@ import numpy as np import pandas as pd -from xarray.core.common import contains_cftime_datetimes - from .facetgrid import _easy_facetgrid from .utils import ( _add_colorbar, _ensure_plottable, _infer_interval_breaks, _infer_xy_labels, @@ -139,15 +137,6 @@ def plot(darray, row=None, col=None, col_wrap=None, ax=None, hue=None, """ darray = darray.squeeze() - if contains_cftime_datetimes(darray): - raise NotImplementedError( - 'Built-in plotting of arrays of cftime.datetime objects or arrays ' - 'indexed by cftime.datetime objects is currently not implemented ' - 'within xarray. A possible workaround is to use the ' - 'nc-time-axis package ' - '(https://github.com/SciTools/nc-time-axis) to convert the dates ' - 'to a plottable type and plot your data directly with matplotlib.') - plot_dims = set(darray.dims) plot_dims.discard(row) plot_dims.discard(col) diff --git a/xarray/plot/utils.py b/xarray/plot/utils.py index 6d812fbc2bc..21523ede4cd 100644 --- a/xarray/plot/utils.py +++ b/xarray/plot/utils.py @@ -10,6 +10,16 @@ from ..core.options import OPTIONS from ..core.utils import is_scalar +from distutils.version import LooseVersion + +try: + import nc_time_axis + if LooseVersion(nc_time_axis.__version__) < LooseVersion('1.2.0'): + nc_time_axis_available = False + else: + nc_time_axis_available = True +except ImportError: + nc_time_axis_available = False ROBUST_PERCENTILE = 2.0 @@ -471,16 +481,29 @@ def _ensure_plottable(*args): """ numpy_types = [np.floating, np.integer, np.timedelta64, np.datetime64] other_types = [datetime] - + try: + import cftime + cftime_datetime = [cftime.datetime] + except ImportError: + cftime_datetime = [] + other_types = other_types + cftime_datetime for x in args: if not (_valid_numpy_subdtype(np.array(x), numpy_types) or _valid_other_type(np.array(x), other_types)): raise TypeError('Plotting requires coordinates to be numeric ' - 'or dates of type np.datetime64 or ' - 'datetime.datetime or pd.Interval.') - - -def _ensure_numeric(arr): + 'or dates of type np.datetime64, ' + 'datetime.datetime, cftime.datetime or ' + 'pd.Interval.') + if (_valid_other_type(np.array(x), cftime_datetime) + and not nc_time_axis_available): + raise ImportError('Plotting of arrays of cftime.datetime ' + 'objects or arrays indexed by ' + 'cftime.datetime objects requires the ' + 'optional `nc-time-axis` (v1.2.0 or later) ' + 'package.') + + +def _numeric(arr): numpy_types = [np.floating, np.integer] return _valid_numpy_subdtype(arr, numpy_types) diff --git a/xarray/tests/__init__.py b/xarray/tests/__init__.py index a7eafa92bd7..281fc662197 100644 --- a/xarray/tests/__init__.py +++ b/xarray/tests/__init__.py @@ -63,6 +63,8 @@ def LooseVersion(vstring): has_pynio, requires_pynio = _importorskip('Nio') has_pseudonetcdf, requires_pseudonetcdf = _importorskip('PseudoNetCDF') has_cftime, requires_cftime = _importorskip('cftime') +has_nc_time_axis, requires_nc_time_axis = _importorskip('nc_time_axis', + minversion='1.2.0') has_cftime_1_0_2_1, requires_cftime_1_0_2_1 = _importorskip( 'cftime', minversion='1.0.2.1') has_dask, requires_dask = _importorskip('dask') diff --git a/xarray/tests/test_plot.py b/xarray/tests/test_plot.py index 3b08ce706f5..c0e03b5791c 100644 --- a/xarray/tests/test_plot.py +++ b/xarray/tests/test_plot.py @@ -17,7 +17,9 @@ from . import ( assert_array_equal, assert_equal, raises_regex, requires_cftime, - requires_matplotlib, requires_matplotlib2, requires_seaborn) + requires_matplotlib, requires_matplotlib2, requires_seaborn, + requires_nc_time_axis) +from . import has_nc_time_axis # import mpl and change the backend before other mpl imports try: @@ -1828,6 +1830,61 @@ def test_datetime_line_plot(self): self.darray.plot.line() +@requires_nc_time_axis +@requires_cftime +class TestCFDatetimePlot(PlotTestCase): + @pytest.fixture(autouse=True) + def setUp(self): + ''' + Create a DataArray with a time-axis that contains cftime.datetime + objects. + ''' + # case for 1d array + data = np.random.rand(4, 12) + time = xr.cftime_range(start='2017', + periods=12, + freq='1M', + calendar='noleap') + darray = DataArray(data, dims=['x', 'time']) + darray.coords['time'] = time + + self.darray = darray + + def test_cfdatetime_line_plot(self): + self.darray.isel(x=0).plot.line() + + def test_cfdatetime_pcolormesh_plot(self): + self.darray.plot.pcolormesh() + + def test_cfdatetime_contour_plot(self): + self.darray.plot.contour() + + +@requires_cftime +@pytest.mark.skipif(has_nc_time_axis, reason='nc_time_axis is installed') +class TestNcAxisNotInstalled(PlotTestCase): + @pytest.fixture(autouse=True) + def setUp(self): + ''' + Create a DataArray with a time-axis that contains cftime.datetime + objects. + ''' + month = np.arange(1, 13, 1) + data = np.sin(2 * np.pi * month / 12.0) + darray = DataArray(data, dims=['time']) + darray.coords['time'] = xr.cftime_range(start='2017', + periods=12, + freq='1M', + calendar='noleap') + + self.darray = darray + + def test_ncaxis_notinstalled_line_plot(self): + with raises_regex(ImportError, + 'optional `nc-time-axis`'): + self.darray.plot.line() + + @requires_seaborn def test_import_seaborn_no_warning(): # GH1633 @@ -1844,27 +1901,6 @@ def test_plot_seaborn_no_import_warning(): assert len(record) == 0 -@requires_cftime -def test_plot_cftime_coordinate_error(): - cftime = _import_cftime() - time = cftime.num2date(np.arange(5), units='days since 0001-01-01', - calendar='noleap') - data = DataArray(np.arange(5), coords=[time], dims=['time']) - with raises_regex(TypeError, - 'requires coordinates to be numeric or dates'): - data.plot() - - -@requires_cftime -def test_plot_cftime_data_error(): - cftime = _import_cftime() - data = cftime.num2date(np.arange(5), units='days since 0001-01-01', - calendar='noleap') - 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)))] diff --git a/xarray/util/print_versions.py b/xarray/util/print_versions.py index cb624155634..50389df85cb 100755 --- a/xarray/util/print_versions.py +++ b/xarray/util/print_versions.py @@ -97,6 +97,7 @@ def show_versions(as_json=False): ("Nio", lambda mod: mod.__version__), ("zarr", lambda mod: mod.__version__), ("cftime", lambda mod: mod.__version__), + ("nc_time_axis", lambda mod: mod.__version__), ("PseudonetCDF", lambda mod: mod.__version__), ("rasterio", lambda mod: mod.__version__), ("cfgrib", lambda mod: mod.__version__),