Skip to content

Commit

Permalink
enable internal plotting with cftime datetime (#2665)
Browse files Browse the repository at this point in the history
enable internal plotting with cftime datetime
  • Loading branch information
Julius Busecke authored and dcherian committed Feb 8, 2019
1 parent e097763 commit 8a1a8a1
Show file tree
Hide file tree
Showing 12 changed files with 120 additions and 63 deletions.
1 change: 1 addition & 0 deletions ci/requirements-py37-windows.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ channels:
dependencies:
- python=3.7
- cftime
- nc-time-axis
- dask
- distributed
- h5py
Expand Down
1 change: 1 addition & 0 deletions ci/requirements-py37.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ channels:
dependencies:
- python=3.7
- cftime
- nc-time-axis
- dask
- distributed
- h5py
Expand Down
4 changes: 3 additions & 1 deletion doc/installing.rst
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ For plotting
:ref:`plot-maps`
- `seaborn <https://stanford.edu/~mwaskom/software/seaborn/>`__: for better
color palettes
- `nc-time-axis <https://github.com/SciTools/nc-time-axis>`__: for plotting
cftime.datetime objects (1.2.0 or later)


Instructions
Expand Down Expand Up @@ -109,4 +111,4 @@ To run these benchmark tests in a local machine, first install
- `airspeed-velocity <https://asv.readthedocs.io/en/latest/>`__: a tool for benchmarking Python packages over their lifetime.

and run
``asv run # this will install some conda environments in ./.asv/envs``
``asv run # this will install some conda environments in ./.asv/envs``
9 changes: 7 additions & 2 deletions doc/plotting.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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 <https://github.com/SciTools/nc-time-axis>`_ v1.2.0 or later
needs to be installed.

For more extensive plotting applications consider the following projects:

- `Seaborn <http://seaborn.pydata.org/>`_: "provides
Expand Down Expand Up @@ -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
Expand All @@ -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.

Expand Down
31 changes: 12 additions & 19 deletions doc/time-series.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
-----------------
Expand Down Expand Up @@ -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
------------------------------------------------------------------

Expand All @@ -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.
Expand All @@ -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:
Expand All @@ -261,20 +261,20 @@ 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')
da.sel(time=slice('0001-05', '0002-02'))
- 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
Expand Down Expand Up @@ -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`,
Expand All @@ -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
6 changes: 5 additions & 1 deletion doc/whats-new.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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 <https://github.com/jhamman>`_.
- 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 <https://github.com/max-sixty>`_.
Expand All @@ -34,6 +34,10 @@ Breaking changes
Enhancements
~~~~~~~~~~~~

- Internal plotting now supports ``cftime.datetime`` objects as time series.
(:issue:`2164`)
By `Julius Busecke <https://github.com/jbusecke>`_ and
`Spencer Clark <https://github.com/spencerkclark>`_.
- Add ``data=False`` option to ``to_dict()`` methods. (:issue:`2656`)
By `Ryan Abernathey <https://github.com/rabernat>`_
- :py:meth:`~xarray.DataArray.coarsen` and
Expand Down
2 changes: 1 addition & 1 deletion xarray/core/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -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')
<xarray.DataArray (time: 337)>
Expand Down
11 changes: 0 additions & 11 deletions xarray/plot/plot.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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)
Expand Down
35 changes: 29 additions & 6 deletions xarray/plot/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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)

Expand Down
2 changes: 2 additions & 0 deletions xarray/tests/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down
80 changes: 58 additions & 22 deletions xarray/tests/test_plot.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand All @@ -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)))]
Expand Down
1 change: 1 addition & 0 deletions xarray/util/print_versions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__),
Expand Down

0 comments on commit 8a1a8a1

Please sign in to comment.