From 86f8eb36b033d1e6151e8c47b9c0072b2f351218 Mon Sep 17 00:00:00 2001 From: Thomas Nicholas Date: Fri, 12 Oct 2018 18:13:28 +0100 Subject: [PATCH 01/30] Added a global option to always keep or discard attrs. --- xarray/core/options.py | 15 +++++++++++++++ xarray/tests/test_options.py | 14 +++++++++++++- 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/xarray/core/options.py b/xarray/core/options.py index 04ea0be7172..2100897a5d3 100644 --- a/xarray/core/options.py +++ b/xarray/core/options.py @@ -6,6 +6,8 @@ FILE_CACHE_MAXSIZE = 'file_cache_maxsize' CMAP_SEQUENTIAL = 'cmap_sequential' CMAP_DIVERGENT = 'cmap_divergent' +KEEP_ATTRS = 'keep_attrs' + OPTIONS = { DISPLAY_WIDTH: 80, @@ -14,6 +16,7 @@ FILE_CACHE_MAXSIZE: 128, CMAP_SEQUENTIAL: 'viridis', CMAP_DIVERGENT: 'RdBu_r', + KEEP_ATTRS: 'default' } _JOIN_OPTIONS = frozenset(['inner', 'outer', 'left', 'right', 'exact']) @@ -28,6 +31,7 @@ def _positive_integer(value): ARITHMETIC_JOIN: _JOIN_OPTIONS.__contains__, ENABLE_CFTIMEINDEX: lambda value: isinstance(value, bool), FILE_CACHE_MAXSIZE: _positive_integer, + KEEP_ATTRS: lambda choice: choice in [True, False, 'default'] } @@ -41,6 +45,17 @@ def _set_file_cache_maxsize(value): } +def _set_keep_attrs(func_default): + global_choice = OPTIONS['keep_attrs'] + + if global_choice is 'default': + return func_default + elif global_choice in [True, False]: + return global_choice + else: + raise ValueError('The global option keep_attrs is set to an invalid value.') + + class set_options(object): """Set options for xarray in a controlled context. diff --git a/xarray/tests/test_options.py b/xarray/tests/test_options.py index 4441375a1b1..2c40c9bfb38 100644 --- a/xarray/tests/test_options.py +++ b/xarray/tests/test_options.py @@ -3,7 +3,7 @@ import pytest import xarray -from xarray.core.options import OPTIONS +from xarray.core.options import OPTIONS, _set_keep_attrs from xarray.backends.file_manager import FILE_CACHE @@ -44,6 +44,18 @@ def test_file_cache_maxsize(): assert FILE_CACHE.maxsize == original_size +def test_keep_attrs(): + with pytest.raises(ValueError): + xarray.set_options(keep_attrs='invalid_str') + with xarray.set_options(keep_attrs=True): + assert OPTIONS['keep_attrs'] + with xarray.set_options(keep_attrs=False): + assert not OPTIONS['keep_attrs'] + with xarray.set_options(keep_attrs='default'): + assert _set_keep_attrs(func_default=True) + assert _set_keep_attrs(func_default=False) is False + + def test_nested_options(): original = OPTIONS['display_width'] with xarray.set_options(display_width=1): From 483e28d29b537c2e9a367b881069b8d5096b87b4 Mon Sep 17 00:00:00 2001 From: Thomas Nicholas Date: Fri, 12 Oct 2018 18:28:06 +0100 Subject: [PATCH 02/30] Updated docs and options docstring to describe new keep_attrs global option --- doc/faq.rst | 3 ++- xarray/core/options.py | 5 +++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/doc/faq.rst b/doc/faq.rst index 9313481f50a..44bc021024b 100644 --- a/doc/faq.rst +++ b/doc/faq.rst @@ -119,7 +119,8 @@ conventions`_. (An exception is serialization to and from netCDF files.) An implication of this choice is that we do not propagate ``attrs`` through most operations unless explicitly flagged (some methods have a ``keep_attrs`` -option). Similarly, xarray does not check for conflicts between ``attrs`` when +option, and there is a global flag for setting this to be always True or +False). Similarly, xarray does not check for conflicts between ``attrs`` when combining arrays and datasets, unless explicitly requested with the option ``compat='identical'``. The guiding principle is that metadata should not be allowed to get in the way. diff --git a/xarray/core/options.py b/xarray/core/options.py index 2100897a5d3..ee7f657847a 100644 --- a/xarray/core/options.py +++ b/xarray/core/options.py @@ -78,6 +78,11 @@ class set_options(object): - ``cmap_divergent``: colormap to use for divergent data plots. Default: ``RdBu_r``. If string, must be matplotlib built-in colormap. Can also be a Colormap object (e.g. mpl.cm.magma) + - ``keep_attrs``: rule for whether to keep attributes on xarray + Datasets/dataarrays after operations. Either ``True`` to always keep + attrs, ``False`` to always discard them, or ``'default'`` to use original + logic that attrs should only be kept in unambiguous circumstances. + Default: ``'default'``. f You can use ``set_options`` either as a context manager: From 8df30be49281a06c6f92b6ffd4a98cd48dc59b0f Mon Sep 17 00:00:00 2001 From: Thomas Nicholas Date: Fri, 12 Oct 2018 19:12:47 +0100 Subject: [PATCH 03/30] Updated all default keep_attrs arguments to check global option --- xarray/core/common.py | 16 ++++++++++------ xarray/core/dataarray.py | 10 ++++++---- xarray/core/dataset.py | 18 ++++++++++-------- xarray/core/groupby.py | 19 ++++++++++--------- xarray/core/missing.py | 7 ++++--- xarray/core/ops.py | 5 +++-- xarray/core/resample.py | 3 ++- xarray/core/variable.py | 5 +++-- 8 files changed, 48 insertions(+), 35 deletions(-) diff --git a/xarray/core/common.py b/xarray/core/common.py index c74b1fa080b..b5b3ae5814c 100644 --- a/xarray/core/common.py +++ b/xarray/core/common.py @@ -11,6 +11,7 @@ from .arithmetic import SupportsArithmetic from .pycompat import OrderedDict, basestring, dask_array_type, suppress from .utils import Frozen, ReprObject, SortedKeysDict, either_dict_or_kwargs +from .options import _set_keep_attrs # Used as a sentinel value to indicate a all dimensions ALL_DIMS = ReprObject('') @@ -21,12 +22,12 @@ class ImplementsArrayReduce(object): def _reduce_method(cls, func, include_skipna, numeric_only): if include_skipna: def wrapped_func(self, dim=None, axis=None, skipna=None, - keep_attrs=False, **kwargs): + keep_attrs=_set_keep_attrs(False), **kwargs): return self.reduce(func, dim, axis, keep_attrs=keep_attrs, skipna=skipna, allow_lazy=True, **kwargs) else: - def wrapped_func(self, dim=None, axis=None, keep_attrs=False, - **kwargs): + def wrapped_func(self, dim=None, axis=None, + keep_attrs=_set_keep_attrs(False), **kwargs): return self.reduce(func, dim, axis, keep_attrs=keep_attrs, allow_lazy=True, **kwargs) return wrapped_func @@ -51,13 +52,15 @@ class ImplementsDatasetReduce(object): @classmethod def _reduce_method(cls, func, include_skipna, numeric_only): if include_skipna: - def wrapped_func(self, dim=None, keep_attrs=False, skipna=None, + def wrapped_func(self, dim=None, + keep_attrs=_set_keep_attrs(False), skipna=None, **kwargs): return self.reduce(func, dim, keep_attrs, skipna=skipna, numeric_only=numeric_only, allow_lazy=True, **kwargs) else: - def wrapped_func(self, dim=None, keep_attrs=False, **kwargs): + def wrapped_func(self, dim=None, + keep_attrs=_set_keep_attrs(False), **kwargs): return self.reduce(func, dim, keep_attrs, numeric_only=numeric_only, allow_lazy=True, **kwargs) @@ -590,7 +593,8 @@ def rolling(self, dim=None, min_periods=None, center=False, **dim_kwargs): center=center) def resample(self, freq=None, dim=None, how=None, skipna=None, - closed=None, label=None, base=0, keep_attrs=False, **indexer): + closed=None, label=None, base=0, + keep_attrs=_set_keep_attrs(False), **indexer): """Returns a Resample object for performing resampling operations. Handles both downsampling and upsampling. If any intervals contain no diff --git a/xarray/core/dataarray.py b/xarray/core/dataarray.py index f131b003a69..9790479670f 100644 --- a/xarray/core/dataarray.py +++ b/xarray/core/dataarray.py @@ -16,7 +16,7 @@ assert_coordinate_consistent, remap_label_indexers) from .dataset import Dataset, merge_indexes, split_indexes from .formatting import format_item -from .options import OPTIONS +from .options import OPTIONS, _set_keep_attrs from .pycompat import OrderedDict, basestring, iteritems, range, zip from .utils import ( decode_numpy_dict_values, either_dict_or_kwargs, ensure_us_time_resolution) @@ -1559,7 +1559,8 @@ def combine_first(self, other): """ return ops.fillna(self, other, join="outer") - def reduce(self, func, dim=None, axis=None, keep_attrs=False, **kwargs): + def reduce(self, func, dim=None, axis=None, + keep_attrs=_set_keep_attrs(False), **kwargs): """Reduce this array by applying `func` along some dimension(s). Parameters @@ -2270,7 +2271,8 @@ def sortby(self, variables, ascending=True): ds = self._to_temp_dataset().sortby(variables, ascending=ascending) return self._from_temp_dataset(ds) - def quantile(self, q, dim=None, interpolation='linear', keep_attrs=False): + def quantile(self, q, dim=None, interpolation='linear', + keep_attrs=_set_keep_attrs(False)): """Compute the qth quantile of the data along the specified dimension. Returns the qth quantiles(s) of the array elements. @@ -2316,7 +2318,7 @@ def quantile(self, q, dim=None, interpolation='linear', keep_attrs=False): q, dim=dim, keep_attrs=keep_attrs, interpolation=interpolation) return self._from_temp_dataset(ds) - def rank(self, dim, pct=False, keep_attrs=False): + def rank(self, dim, pct=False, keep_attrs=_set_keep_attrs(False)): """Ranks the data. Equal values are assigned a rank that is the average of the ranks that diff --git a/xarray/core/dataset.py b/xarray/core/dataset.py index c8586d1d408..4e3a59312ce 100644 --- a/xarray/core/dataset.py +++ b/xarray/core/dataset.py @@ -28,7 +28,7 @@ from .merge import ( dataset_merge_method, dataset_update_method, merge_data_and_coords, merge_variables) -from .options import OPTIONS +from .options import OPTIONS, _set_keep_attrs from .pycompat import ( OrderedDict, basestring, dask_array_type, integer_types, iteritems, range) from .utils import ( @@ -2870,7 +2870,7 @@ def combine_first(self, other): out = ops.fillna(self, other, join="outer", dataset_join="outer") return out - def reduce(self, func, dim=None, keep_attrs=False, numeric_only=False, + def reduce(self, func, dim=None, keep_attrs=_set_keep_attrs(False), numeric_only=False, allow_lazy=False, **kwargs): """Reduce this dataset by applying `func` along some dimension(s). @@ -2940,7 +2940,7 @@ def reduce(self, func, dim=None, keep_attrs=False, numeric_only=False, attrs = self.attrs if keep_attrs else None return self._replace_vars_and_dims(variables, coord_names, attrs=attrs) - def apply(self, func, keep_attrs=False, args=(), **kwargs): + def apply(self, func, keep_attrs=_set_keep_attrs(False), args=(), **kwargs): """Apply a function over the data variables in this dataset. Parameters @@ -3288,7 +3288,7 @@ def from_dict(cls, d): return obj @staticmethod - def _unary_op(f, keep_attrs=False): + def _unary_op(f, keep_attrs=_set_keep_attrs(False)): @functools.wraps(f) def func(self, *args, **kwargs): ds = self.coords.to_dataset() @@ -3649,7 +3649,7 @@ def sortby(self, variables, ascending=True): return aligned_self.isel(**indices) def quantile(self, q, dim=None, interpolation='linear', - numeric_only=False, keep_attrs=False): + numeric_only=False, keep_attrs=_set_keep_attrs(False)): """Compute the qth quantile of the data along the specified dimension. Returns the qth quantiles(s) of the array elements for each variable @@ -3735,7 +3735,7 @@ def quantile(self, q, dim=None, interpolation='linear', new.coords['quantile'] = q return new - def rank(self, dim, pct=False, keep_attrs=False): + def rank(self, dim, pct=False, keep_attrs=_set_keep_attrs(False)): """Ranks the data. Equal values are assigned a rank that is the average of the ranks that @@ -3838,11 +3838,13 @@ def differentiate(self, coord, edge_order=1, datetime_unit=None): @property def real(self): - return self._unary_op(lambda x: x.real, keep_attrs=True)(self) + return self._unary_op(lambda x: x.real, + keep_attrs=_set_keep_attrs(True))(self) @property def imag(self): - return self._unary_op(lambda x: x.imag, keep_attrs=True)(self) + return self._unary_op(lambda x: x.imag, + keep_attrs=_set_keep_attrs(True))(self) def filter_by_attrs(self, **kwargs): """Returns a ``Dataset`` with variables that match specific conditions. diff --git a/xarray/core/groupby.py b/xarray/core/groupby.py index 3842c642047..f4ee763662c 100644 --- a/xarray/core/groupby.py +++ b/xarray/core/groupby.py @@ -13,6 +13,7 @@ from .pycompat import integer_types, range, zip from .utils import hashable, maybe_wrap_array, peek_at, safe_cast_to_index from .variable import IndexVariable, Variable, as_variable +from .options import _set_keep_attrs def unique_value_groups(ar, sort=True): @@ -407,12 +408,12 @@ def _first_or_last(self, op, skipna, keep_attrs): return self.reduce(op, self._group_dim, skipna=skipna, keep_attrs=keep_attrs, allow_lazy=True) - def first(self, skipna=None, keep_attrs=True): + def first(self, skipna=None, keep_attrs=_set_keep_attrs(True)): """Return the first element of each group along the group dimension """ return self._first_or_last(duck_array_ops.first, skipna, keep_attrs) - def last(self, skipna=None, keep_attrs=True): + def last(self, skipna=None, keep_attrs=_set_keep_attrs(True)): """Return the last element of each group along the group dimension """ return self._first_or_last(duck_array_ops.last, skipna, keep_attrs) @@ -538,8 +539,8 @@ def _combine(self, applied, shortcut=False): combined = self._maybe_unstack(combined) return combined - def reduce(self, func, dim=None, axis=None, keep_attrs=False, - shortcut=True, **kwargs): + def reduce(self, func, dim=None, axis=None, + keep_attrs=_set_keep_attrs(False), shortcut=True, **kwargs): """Reduce the items in this group by applying `func` along some dimension(s). @@ -589,12 +590,12 @@ def reduce_array(ar): def _reduce_method(cls, func, include_skipna, numeric_only): if include_skipna: def wrapped_func(self, dim=DEFAULT_DIMS, axis=None, skipna=None, - keep_attrs=False, **kwargs): + keep_attrs=_set_keep_attrs(False), **kwargs): return self.reduce(func, dim, axis, keep_attrs=keep_attrs, skipna=skipna, allow_lazy=True, **kwargs) else: def wrapped_func(self, dim=DEFAULT_DIMS, axis=None, - keep_attrs=False, **kwargs): + keep_attrs=_set_keep_attrs(False), **kwargs): return self.reduce(func, dim, axis, keep_attrs=keep_attrs, allow_lazy=True, **kwargs) return wrapped_func @@ -650,7 +651,7 @@ def _combine(self, applied): combined = self._maybe_unstack(combined) return combined - def reduce(self, func, dim=None, keep_attrs=False, **kwargs): + def reduce(self, func, dim=None, keep_attrs=_set_keep_attrs(False), **kwargs): """Reduce the items in this group by applying `func` along some dimension(s). @@ -700,13 +701,13 @@ def reduce_dataset(ds): @classmethod def _reduce_method(cls, func, include_skipna, numeric_only): if include_skipna: - def wrapped_func(self, dim=DEFAULT_DIMS, keep_attrs=False, + def wrapped_func(self, dim=DEFAULT_DIMS, keep_attrs=_set_keep_attrs(False), skipna=None, **kwargs): return self.reduce(func, dim, keep_attrs, skipna=skipna, numeric_only=numeric_only, allow_lazy=True, **kwargs) else: - def wrapped_func(self, dim=DEFAULT_DIMS, keep_attrs=False, + def wrapped_func(self, dim=DEFAULT_DIMS, keep_attrs=_set_keep_attrs(False), **kwargs): return self.reduce(func, dim, keep_attrs, numeric_only=numeric_only, allow_lazy=True, diff --git a/xarray/core/missing.py b/xarray/core/missing.py index 3f4e0fc3ac9..a025c702369 100644 --- a/xarray/core/missing.py +++ b/xarray/core/missing.py @@ -14,6 +14,7 @@ from .pycompat import iteritems from .utils import OrderedSet, datetime_to_numeric, is_scalar from .variable import Variable, broadcast_variables +from .options import _set_keep_attrs class BaseInterpolator(object): @@ -218,7 +219,7 @@ def interp_na(self, dim=None, use_coordinate=True, method='linear', limit=None, output_dtypes=[self.dtype], dask='parallelized', vectorize=True, - keep_attrs=True).transpose(*self.dims) + keep_attrs=_set_keep_attrs(True)).transpose(*self.dims) if limit is not None: arr = arr.where(valids) @@ -269,7 +270,7 @@ def ffill(arr, dim=None, limit=None): return apply_ufunc(bn.push, arr, dask='parallelized', - keep_attrs=True, + keep_attrs=_set_keep_attrs(True), output_dtypes=[arr.dtype], kwargs=dict(n=_limit, axis=axis)).transpose(*arr.dims) @@ -283,7 +284,7 @@ def bfill(arr, dim=None, limit=None): return apply_ufunc(_bfill, arr, dask='parallelized', - keep_attrs=True, + keep_attrs=_set_keep_attrs(True), output_dtypes=[arr.dtype], kwargs=dict(n=_limit, axis=axis)).transpose(*arr.dims) diff --git a/xarray/core/ops.py b/xarray/core/ops.py index a0dd2212a8f..84a521960cb 100644 --- a/xarray/core/ops.py +++ b/xarray/core/ops.py @@ -14,6 +14,7 @@ from . import dtypes, duck_array_ops from .nputils import array_eq, array_ne from .pycompat import PY3 +from .options import _set_keep_attrs try: import bottleneck as bn @@ -153,7 +154,7 @@ def fillna(data, other, join="left", dataset_join="left"): dask="allowed", dataset_join=dataset_join, dataset_fill_value=np.nan, - keep_attrs=True) + keep_attrs=_set_keep_attrs(True)) def where_method(self, cond, other=dtypes.NA): @@ -179,7 +180,7 @@ def where_method(self, cond, other=dtypes.NA): join=join, dataset_join=join, dask='allowed', - keep_attrs=True) + keep_attrs=_set_keep_attrs(True)) def _call_possibly_missing_method(arg, name, args, kwargs): diff --git a/xarray/core/resample.py b/xarray/core/resample.py index bd84e04487e..13b5dab2dc0 100644 --- a/xarray/core/resample.py +++ b/xarray/core/resample.py @@ -3,6 +3,7 @@ from . import ops from .groupby import DEFAULT_DIMS, DataArrayGroupBy, DatasetGroupBy from .pycompat import OrderedDict, dask_array_type +from .options import _set_keep_attrs RESAMPLE_DIM = '__resample_dim__' @@ -273,7 +274,7 @@ def apply(self, func, **kwargs): return combined.rename({self._resample_dim: self._dim}) - def reduce(self, func, dim=None, keep_attrs=False, **kwargs): + def reduce(self, func, dim=None, keep_attrs=_set_keep_attrs(False), **kwargs): """Reduce the items in this group by applying `func` along the pre-defined resampling dimension. diff --git a/xarray/core/variable.py b/xarray/core/variable.py index c003d52aab2..8b9ff1a07cd 100644 --- a/xarray/core/variable.py +++ b/xarray/core/variable.py @@ -18,6 +18,7 @@ from .pycompat import ( OrderedDict, basestring, dask_array_type, integer_types, zip) from .utils import OrderedSet, either_dict_or_kwargs +from .options import _set_keep_attrs try: import dask.array as da @@ -1303,8 +1304,8 @@ def fillna(self, value): def where(self, cond, other=dtypes.NA): return ops.where_method(self, cond, other) - def reduce(self, func, dim=None, axis=None, keep_attrs=False, - allow_lazy=False, **kwargs): + def reduce(self, func, dim=None, axis=None, + keep_attrs=_set_keep_attrs(False), allow_lazy=False, **kwargs): """Reduce this array by applying `func` along some dimension(s). Parameters From 8d8391dfcc3eb305edb862c2ed44efe5fe6e0220 Mon Sep 17 00:00:00 2001 From: Thomas Nicholas Date: Tue, 30 Oct 2018 22:58:53 +0000 Subject: [PATCH 04/30] Completed merge --- xarray/core/options.py | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/xarray/core/options.py b/xarray/core/options.py index cebaa6bf25f..eb3013d5233 100644 --- a/xarray/core/options.py +++ b/xarray/core/options.py @@ -45,7 +45,6 @@ def _set_file_cache_maxsize(value): } -<<<<<<< HEAD def _get_keep_attrs(default): global_choice = OPTIONS['keep_attrs'] @@ -55,17 +54,6 @@ def _get_keep_attrs(default): return global_choice else: raise ValueError("The global option keep_attrs must be one of True, False or 'default'.") -======= -def _set_keep_attrs(func_default): - global_choice = OPTIONS['keep_attrs'] - - if global_choice is 'default': - return func_default - elif global_choice in [True, False]: - return global_choice - else: - raise ValueError('The global option keep_attrs is set to an invalid value.') ->>>>>>> 842a16d55db185cae53ac19d9b06381775a1adf2 class set_options(object): From 274c50fd689af81a17c313aa340b93fdf6edf429 Mon Sep 17 00:00:00 2001 From: Thomas Nicholas Date: Tue, 30 Oct 2018 23:38:06 +0000 Subject: [PATCH 05/30] Finished merge --- xarray/core/variable.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/xarray/core/variable.py b/xarray/core/variable.py index cb5a2927773..f185c01a7e2 100644 --- a/xarray/core/variable.py +++ b/xarray/core/variable.py @@ -18,11 +18,7 @@ from .pycompat import ( OrderedDict, basestring, dask_array_type, integer_types, zip) from .utils import OrderedSet, either_dict_or_kwargs -<<<<<<< HEAD from .options import _get_keep_attrs -======= -from .options import _set_keep_attrs ->>>>>>> 842a16d55db185cae53ac19d9b06381775a1adf2 try: import dask.array as da From 5be244152edf086b0aeda173ef6b9128ca51c915 Mon Sep 17 00:00:00 2001 From: Thomas Nicholas Date: Tue, 29 Jan 2019 19:48:07 +0000 Subject: [PATCH 06/30] Fixed logic for setting line data --- xarray/plot/plot.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/xarray/plot/plot.py b/xarray/plot/plot.py index 13d6ec31104..2d348146822 100644 --- a/xarray/plot/plot.py +++ b/xarray/plot/plot.py @@ -200,18 +200,22 @@ def _infer_line_data(darray, x, y, hue): 'for line plots.') if ndims == 1: - dim, = darray.dims # get the only dimension name huename = None hueplt = None huelabel = '' - if (x is None and y is None) or x == dim: - xplt = darray[dim] + if x is not None: + xplt = darray[x] yplt = darray - else: - yplt = darray[dim] + elif y is not None: xplt = darray + yplt = darray[y] + + else: # Both x & y are None + dim = darray.dims[0] + xplt = darray[dim] + yplt = darray else: if x is None and y is None and hue is None: From 1686e3e744ea253dc0e73bbf7c50dbcebcd3ad45 Mon Sep 17 00:00:00 2001 From: Thomas Nicholas Date: Tue, 29 Jan 2019 20:06:17 +0000 Subject: [PATCH 07/30] Added tests to check line data matches values of correct coords --- xarray/plot/plot.py | 2 +- xarray/tests/test_plot.py | 15 +++++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/xarray/plot/plot.py b/xarray/plot/plot.py index 2d348146822..9178dd8f031 100644 --- a/xarray/plot/plot.py +++ b/xarray/plot/plot.py @@ -212,7 +212,7 @@ def _infer_line_data(darray, x, y, hue): xplt = darray yplt = darray[y] - else: # Both x & y are None + else: # Both x & y are None dim = darray.dims[0] xplt = darray[dim] yplt = darray diff --git a/xarray/tests/test_plot.py b/xarray/tests/test_plot.py index 529d35db865..3b08ce706f5 100644 --- a/xarray/tests/test_plot.py +++ b/xarray/tests/test_plot.py @@ -4,6 +4,7 @@ import numpy as np import pandas as pd import pytest +from numpy.testing import assert_array_equal import xarray as xr import xarray.plot as xplt @@ -140,6 +141,20 @@ def test_1d_x_y_kw(self): with raises_regex(ValueError, 'None'): da.plot(x='z', y='f') + # Test for bug in GH issue #2725 + def test_infer_line_data(self): + current = DataArray(name='I', data=np.array([5, 8]), dims=['t'], + coords={'t': (['t'], np.array([0.1, 0.2])), + 'V': (['t'], np.array([100, 200]))}) + + # Plot current against voltage + line = current.plot.line(x='V')[0] + assert_array_equal(line.get_xdata(), current.coords['V'].values) + + # Plot current against time + line = current.plot.line()[0] + assert_array_equal(line.get_xdata(), current.coords['t'].values) + def test_2d_line(self): with raises_regex(ValueError, 'hue'): self.darray[:, :, 0].plot.line() From fcada8b4b298c35649a3a5712553d66f3c25ea3b Mon Sep 17 00:00:00 2001 From: Thomas Nicholas Date: Tue, 29 Jan 2019 20:17:39 +0000 Subject: [PATCH 08/30] Recorded bugfix for line plots --- doc/whats-new.rst | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/doc/whats-new.rst b/doc/whats-new.rst index 184cee05ae2..574b568b238 100644 --- a/doc/whats-new.rst +++ b/doc/whats-new.rst @@ -54,6 +54,8 @@ Bug fixes from higher frequencies to lower frequencies. Datapoints outside the bounds of the original time coordinate are now filled with NaN (:issue:`2197`). By `Spencer Clark `_. +- Line plots with the `x` argument set to a coord now plot the correct data. + (:issue:`27251). By `Tom Nicholas `_. .. _whats-new.0.11.3: @@ -67,7 +69,7 @@ Bug fixes (e.g. '2000-01-01T00:00:00-05:00') no longer raises an error (:issue:`2649`). By `Spencer Clark `_. - Fixed performance regression with ``open_mfdataset`` (:issue:`2662`). - By `Tom Nicholas `_. + - Fixed supplying an explicit dimension in the ``concat_dim`` argument to to ``open_mfdataset`` (:issue:`2647`). By `Ben Root `_. From e6347b0eb7e5df0534ca2758f6e1f80562cdbd09 Mon Sep 17 00:00:00 2001 From: Thomas Nicholas Date: Wed, 30 Jan 2019 15:25:00 +0000 Subject: [PATCH 09/30] Skeleton of pseudocode for animating a single line --- xarray/plot/facetgrid.py | 3 +- xarray/plot/plot.py | 108 ++++++++++++++++++++++++++++++++++----- 2 files changed, 98 insertions(+), 13 deletions(-) diff --git a/xarray/plot/facetgrid.py b/xarray/plot/facetgrid.py index 2a4c67036d6..392b3be9638 100644 --- a/xarray/plot/facetgrid.py +++ b/xarray/plot/facetgrid.py @@ -298,9 +298,10 @@ def map_dataarray_line(self, x=None, y=None, hue=None, **kwargs): ax=ax, _labels=False, **kwargs) self._mappables.append(mappable) + animate_over = kwargs.pop('animate_over', None) _, _, hueplt, xlabel, ylabel, huelabel = _infer_line_data( darray=self.data.loc[self.name_dicts.flat[0]], - x=x, y=y, hue=hue) + x=x, y=y, hue=hue, animate_over=animate_over) self._hue_var = hueplt self._hue_label = huelabel diff --git a/xarray/plot/plot.py b/xarray/plot/plot.py index 9178dd8f031..2482c0d6d61 100644 --- a/xarray/plot/plot.py +++ b/xarray/plot/plot.py @@ -100,7 +100,7 @@ def _line_facetgrid(darray, row=None, col=None, hue=None, def plot(darray, row=None, col=None, col_wrap=None, ax=None, hue=None, - rtol=0.01, subplot_kws=None, **kwargs): + rtol=0.01, animate_over=None, subplot_kws=None, **kwargs): """ Default plot of DataArray using matplotlib.pyplot. @@ -126,6 +126,10 @@ def plot(darray, row=None, col=None, col_wrap=None, ax=None, hue=None, If passed, make faceted line plots with hue on this dimension name col_wrap : integer, optional Use together with ``col`` to wrap faceted plots + animate_over: str, optional + Dimension or coord in the DataArray over which to animate. If this + argument is supplied then ``animatplot`` will be used to animate the + corresponding plot. ax : matplotlib axes, optional If None, uses the current axis. Not applicable when using facets. rtol : number, optional @@ -149,7 +153,24 @@ def plot(darray, row=None, col=None, col_wrap=None, ax=None, hue=None, '(https://github.com/SciTools/nc-time-axis) to convert the dates ' 'to a plottable type and plot your data directly with matplotlib.') + if animate_over is not None: + if animate_over not in darray.dims and animate_over not in darray.coords: + raise ValueError("Can only animate over a dimension or coordinate " + "present in the DataArray") + if animate_over in darray.coords: + animate_dim = darray[animate_over].dims + if not len(animate_dim) == 1: + raise ValueError("Cannot animate over a multidimensional " + "coordinate") + else: + animate_dim = animate_over + kwargs['animate_over'] = animate_over + else: + animate_dim = None + plot_dims = set(darray.dims) + if animate_over is not None: + plot_dims= plot_dims - set(animate_dim) plot_dims.discard(row) plot_dims.discard(col) plot_dims.discard(hue) @@ -181,10 +202,13 @@ def plot(darray, row=None, col=None, col_wrap=None, ax=None, hue=None, kwargs['ax'] = ax + if animate_over is not None and plotfunc is not line: + raise NotImplementedError + return plotfunc(darray, **kwargs) -def _infer_line_data(darray, x, y, hue): +def _infer_line_data(darray, x, y, hue, animate_over): error_msg = ('must be either None or one of ({0:s})' .format(', '.join([repr(dd) for dd in darray.dims]))) ndims = len(darray.dims) @@ -199,11 +223,19 @@ def _infer_line_data(darray, x, y, hue): raise ValueError('You cannot specify both x and y kwargs' 'for line plots.') - if ndims == 1: + animate_ndim = 1 if animate_over is not None else 0 + if ndims - animate_ndim == 1: huename = None hueplt = None huelabel = '' + if animate_over is not None: + animation_axis = darray.dims.index(animate_over) + # Set animation dimension to be along last axis of data + # TODO this won't work on a tuple + otherdims = darray.dims - animate_over + darray = darray.transpose(otherdims, animate_over) + if x is not None: xplt = darray[x] yplt = darray @@ -218,6 +250,9 @@ def _infer_line_data(darray, x, y, hue): yplt = darray else: + if animate_over is not None: + raise NotImplementedError + if x is None and y is None and hue is None: raise ValueError('For 2D inputs, please' 'specify either hue, x or y.') @@ -243,6 +278,7 @@ def _infer_line_data(darray, x, y, hue): yplt = darray[yname] if yplt.ndim > 1: if huename in darray.dims: + # TODO bug in xarray? Should otherdim be same as otherindex? otherindex = 1 if darray.dims.index(huename) == 0 else 0 xplt = darray.transpose(otherdim, huename) else: @@ -272,7 +308,8 @@ def line(darray, *args, **kwargs): Parameters ---------- darray : DataArray - Must be 1 dimensional + Must be 1 dimensional, unless ``animate_over`` is specified, in which + it must be 2 dimensional. figsize : tuple, optional A tuple (width, height) of the figure in inches. Mutually exclusive with ``size`` and ``ax``. @@ -288,6 +325,10 @@ def line(darray, *args, **kwargs): hue : string, optional Dimension or coordinate for which you want multiple lines plotted. If plotting against a 2D coordinate, ``hue`` must be a dimension. + animate_over: str, optional + Dimension or coord in the DataArray over which to animate. If this + argument is supplied then ``animatplot`` will be used to animate the + corresponding plot. x, y : string, optional Dimensions or coordinates for x, y axis. Only one of these may be specified. @@ -310,15 +351,22 @@ def line(darray, *args, **kwargs): """ + animate_over = kwargs.pop('animate_over', None) + # Handle facetgrids first row = kwargs.pop('row', None) col = kwargs.pop('col', None) if row or col: + if animate_over is not None: + raise NotImplementedError allargs = locals().copy() allargs.update(allargs.pop('kwargs')) return _line_facetgrid(**allargs) - ndims = len(darray.dims) + if animate_over is not None: + ndims = len(darray[animate_over].dims) + else: + ndims = len(darray.dims) if ndims > 2: raise ValueError('Line plots are for 1- or 2-dimensional DataArrays. ' 'Passed DataArray has {ndims} ' @@ -347,7 +395,7 @@ def line(darray, *args, **kwargs): ax = get_axis(figsize, size, aspect, ax) xplt, yplt, hueplt, xlabel, ylabel, huelabel = \ - _infer_line_data(darray, x, y, hue) + _infer_line_data(darray, x, y, hue, animate_over) # Remove pd.Intervals if contained in xplt.values. if _valid_other_type(xplt.values, [pd.Interval]): @@ -372,7 +420,24 @@ def line(darray, *args, **kwargs): _ensure_plottable(xplt_val, yplt_val) - primitive = ax.plot(xplt_val, yplt_val, *args, **kwargs) + if animate_over is None: + primitive = ax.plot(xplt_val, yplt_val, *args, **kwargs) + else: + # TODO some better way of handling the optional imports + from animatplot.blocks import Line + line_block = Line(xplt_val, yplt_val, ax=ax, t_axis=-1, **kwargs) + + from animatplot.timeline import Timeline + if animate_over in darray.coords: + t_array = darray.coords[animate_over].values + else: # animating over a dimension without coords + t_array = np.arange(darray.dims[animate_over]) + fps = kwargs.pop('fps', 10) + if darray.coords[animate_over].attrs.get('units'): + units = ' [{}]'.format(darray.coords[animate_over].attrs['units']) + else: + units = '' + timeline = Timeline(t_array, units=units, fps=fps) if _labels: if xlabel is not None: @@ -381,10 +446,23 @@ def line(darray, *args, **kwargs): if ylabel is not None: ax.set_ylabel(ylabel) - ax.set_title(darray._title_for_slice()) - - if darray.ndim == 2 and add_legend: - ax.legend(handles=primitive, + if animate_over is None: + ax.set_title(darray._title_for_slice()) + else: + # TODO not sure this will work for dim rather than coord + # Would be nicer if we had something like in GH issue #266 + frame_titles = [darray.isel(animate_over=i)._title_for_slice() + for i in range(darray.dims[animate_over])] + from animatplot.blocks import Title + title_block = Title(frame_titles, ax=ax) + + if ndims == 2 and add_legend: + if animate_over is not None: + # TODO how might this work for multiple lines? + line_handles = line_block.line + else: + line_handles = primitive + ax.legend(handles=line_handles, labels=list(hueplt.values), title=huelabel) @@ -400,7 +478,13 @@ def line(darray, *args, **kwargs): _update_axes(ax, xincrease, yincrease, xscale, yscale, xticks, yticks, xlim, ylim) - return primitive + if animate_over is None: + return primitive + else: + from animatplot.animation import Animation + anim = Animation([line_block, title_block], timeline=timeline) + anim.controls(timeline_slider_args={'text': animate_over, 'ax': ax}) + return anim def step(darray, *args, **kwargs): From ffd03c0c683f994c2b9aedc5ce4543288cc7be48 Mon Sep 17 00:00:00 2001 From: Thomas Nicholas Date: Wed, 30 Jan 2019 19:30:52 +0000 Subject: [PATCH 10/30] Added a simple test, which passes --- xarray/plot/plot.py | 27 +++++++++++++++++---------- xarray/tests/test_plot.py | 35 +++++++++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+), 10 deletions(-) diff --git a/xarray/plot/plot.py b/xarray/plot/plot.py index 2482c0d6d61..9409549760b 100644 --- a/xarray/plot/plot.py +++ b/xarray/plot/plot.py @@ -230,11 +230,10 @@ def _infer_line_data(darray, x, y, hue, animate_over): huelabel = '' if animate_over is not None: - animation_axis = darray.dims.index(animate_over) # Set animation dimension to be along last axis of data - # TODO this won't work on a tuple - otherdims = darray.dims - animate_over - darray = darray.transpose(otherdims, animate_over) + dims = list(darray.dims) + dims.remove(animate_over) + darray = darray.transpose(*dims, animate_over) if x is not None: xplt = darray[x] @@ -424,10 +423,9 @@ def line(darray, *args, **kwargs): primitive = ax.plot(xplt_val, yplt_val, *args, **kwargs) else: # TODO some better way of handling the optional imports + from animatplot.timeline import Timeline from animatplot.blocks import Line - line_block = Line(xplt_val, yplt_val, ax=ax, t_axis=-1, **kwargs) - from animatplot.timeline import Timeline if animate_over in darray.coords: t_array = darray.coords[animate_over].values else: # animating over a dimension without coords @@ -439,6 +437,14 @@ def line(darray, *args, **kwargs): units = '' timeline = Timeline(t_array, units=units, fps=fps) + if ylim is None: + ylim = [np.min(yplt_val), np.max(yplt_val)] + + # animatplot assumes that the x positions might vary over time too + xplt_val = np.repeat(xplt_val[..., np.newaxis], + repeats=len(timeline), axis=-1) + line_block = Line(x=xplt_val, y=yplt_val, ax=ax, t_axis=-1, **kwargs) + if _labels: if xlabel is not None: ax.set_xlabel(xlabel) @@ -449,10 +455,9 @@ def line(darray, *args, **kwargs): if animate_over is None: ax.set_title(darray._title_for_slice()) else: - # TODO not sure this will work for dim rather than coord # Would be nicer if we had something like in GH issue #266 - frame_titles = [darray.isel(animate_over=i)._title_for_slice() - for i in range(darray.dims[animate_over])] + frame_titles = [darray[{animate_over: i}]._title_for_slice() + for i in range(len(timeline))] from animatplot.blocks import Title title_block = Title(frame_titles, ax=ax) @@ -483,7 +488,9 @@ def line(darray, *args, **kwargs): else: from animatplot.animation import Animation anim = Animation([line_block, title_block], timeline=timeline) - anim.controls(timeline_slider_args={'text': animate_over, 'ax': ax}) + # TODO I think ax should be passed to timeline_slider args + # but that just plots a single huge timeline and no line plot?! + anim.controls(timeline_slider_args={'text': animate_over}) return anim diff --git a/xarray/tests/test_plot.py b/xarray/tests/test_plot.py index 3b08ce706f5..a10dbad6514 100644 --- a/xarray/tests/test_plot.py +++ b/xarray/tests/test_plot.py @@ -455,6 +455,41 @@ def test_slice_in_title(self): assert 'd = 10' == title +class TestAnimateLine: + @pytest.fixture(autouse=True) + def setUp(self): + d = np.array([[0.0, 1.1, 0.0, 2], + [0.1, 1.3, 0.2, 2.1], + [0.1, 1.4, 0.3, 2.2], + [0.2, 1.3, 0.2, 2.3], + [0.1, 1.2, 0.2, 2.2]]) + self.darray = DataArray(d, name='height', + coords={'time': 10*np.arange(d.shape[0]), + 'position': 0.1*np.arange(d.shape[1])}, + dims=('time', 'position'), + attrs={'units': 'm'}) + self.darray.time.attrs['units'] = 's' + self.darray.position.attrs['units'] = 'cm' + + @pytest.mark.slow + def test_animate_single_line(self): + from animatplot.animation import Animation + print(self.darray) + a = self.darray.plot.line(animate_over='time') + assert isinstance(a, Animation) + + from animatplot import blocks + line_block, title_block = a.blocks + assert isinstance(line_block, blocks.Line) + assert isinstance(title_block, blocks.Title) + + assert len(line_block) == 5 + assert len(line_block) == len(title_block) + + # TODO check many more things here + # (also better testing in animatplot needed) + + class TestPlotStep(PlotTestCase): @pytest.fixture(autouse=True) def setUp(self): From 3ad911953efb5c57cb2cb35cc560420ed2fd8cc9 Mon Sep 17 00:00:00 2001 From: Thomas Nicholas Date: Thu, 31 Jan 2019 10:26:52 +0000 Subject: [PATCH 11/30] Refactored animation code and tests out into separate module --- xarray/plot/__init__.py | 2 + xarray/plot/animate.py | 170 +++++++++++++++++++++++++++++++++++ xarray/plot/plot.py | 122 +++++++------------------ xarray/plot/utils.py | 35 ++++++++ xarray/tests/__init__.py | 2 + xarray/tests/test_animate.py | 46 ++++++++++ xarray/tests/test_plot.py | 36 -------- 7 files changed, 286 insertions(+), 127 deletions(-) create mode 100644 xarray/plot/animate.py create mode 100644 xarray/tests/test_animate.py diff --git a/xarray/plot/__init__.py b/xarray/plot/__init__.py index 51712e78bf8..74195310ade 100644 --- a/xarray/plot/__init__.py +++ b/xarray/plot/__init__.py @@ -3,6 +3,8 @@ from .facetgrid import FacetGrid +from .animate import animate_line + __all__ = [ 'plot', 'line', diff --git a/xarray/plot/animate.py b/xarray/plot/animate.py new file mode 100644 index 00000000000..8ee237a27c7 --- /dev/null +++ b/xarray/plot/animate.py @@ -0,0 +1,170 @@ +""" +Use this module directly: + import xarray.animate as xanim + +Or use the methods on a DataArray: + DataArray.plot.animate_____ + +Or supply an ``animate_over`` keyword +argument to a normal plotting function: + DataArray.plot._____(animate_over='__') +""" + +import numpy as np +import pandas as pd + +from .plot import _infer_line_data +from .utils import (_ensure_plottable, _interval_to_mid_points, _update_axes, + _valid_other_type, get_axis, _rotate_date_xlabels, + _check_animate_over, _transpose_before_animation) + +from animatplot.blocks import Line, Title +from animatplot.animation import Animation, Timeline + + +def animate_line(darray, animate_over=None, **kwargs): + """ + Line plot of DataArray index against values + + Wraps :func:`animatplot:animatplot.blocks.Line` + + Parameters + ---------- + darray : DataArray + Must be 2 dimensional. + animate_over: str + Dimension or coord in the DataArray over which to animate. + ``animatplot.blocks.Line`` will be used to animate the plot over this + dimension. + figsize : tuple, optional + A tuple (width, height) of the figure in inches. + Mutually exclusive with ``size`` and ``ax``. + aspect : scalar, optional + Aspect ratio of plot, so that ``aspect * size`` gives the width in + inches. Only used if a ``size`` is provided. + size : scalar, optional + If provided, create a new figure for the plot with the given size. + Height (in inches) of each plot. See also: ``aspect``. + ax : matplotlib axes object, optional + Axis on which to plot this figure. By default, use the current axis. + Mutually exclusive with ``size`` and ``figsize``. + x, y : string, optional + Dimensions or 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 : optional + 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. + yincrease : None, True, or False, optional + Should the values on the y axes be increasing from top to bottom? + if None, use the default for the matplotlib function. + **kwargs : optional + Additional arguments to animatplot.blocks.Line + + """ + + row = kwargs.pop('row', None) + col = kwargs.pop('col', None) + if row or col: + raise NotImplementedError + + hue = kwargs.pop('hue', None) + if hue: + raise NotImplementedError + + _check_animate_over(darray, animate_over) + darray = _transpose_before_animation(darray, animate_over) + + ndims = len(darray[animate_over].dims) + if ndims > 1: + raise NotImplementedError + + # Ensures consistency with .plot method + figsize = kwargs.pop('figsize', None) + aspect = kwargs.pop('aspect', None) + size = kwargs.pop('size', None) + ax = kwargs.pop('ax', None) + hue = kwargs.pop('hue', None) + x = kwargs.pop('x', None) + y = kwargs.pop('y', None) + 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) + _labels = kwargs.pop('_labels', True) + + ax = get_axis(figsize, size, aspect, ax) + xplt, yplt, hueplt, xlabel, ylabel, huelabel = \ + _infer_line_data(darray, x, y, hue, animate_over) + + # Remove pd.Intervals if contained in xplt.values. + if _valid_other_type(xplt.values, [pd.Interval]): + # Is it a step plot? (see matplotlib.Axes.step) + if kwargs.get('linestyle', '').startswith('steps-'): + raise NotImplementedError + else: + xplt_val = _interval_to_mid_points(xplt.values) + yplt_val = yplt.values + xlabel += '_center' + else: + xplt_val = xplt.values + yplt_val = yplt.values + + _ensure_plottable(xplt_val, yplt_val) + + fps = kwargs.pop('fps', 10) + timeline = _create_timeline(darray, animate_over, fps) + + if ylim is None: + ylim = [np.min(yplt_val), np.max(yplt_val)] + + # animatplot assumes that the x positions might vary over time too + xplt_val = np.repeat(xplt_val[..., np.newaxis], + repeats=len(timeline), axis=-1) + line_block = Line(x=xplt_val, y=yplt_val, ax=ax, t_axis=-1, **kwargs) + + if _labels: + if xlabel is not None: + ax.set_xlabel(xlabel) + + if ylabel is not None: + ax.set_ylabel(ylabel) + + # Would be nicer if we had something like in GH issue #266 + frame_titles = [darray[{animate_over: i}]._title_for_slice() + for i in range(len(timeline))] + title_block = Title(frame_titles, ax=ax) + + _rotate_date_xlabels(xplt, ax) + + _update_axes(ax, xincrease, yincrease, xscale, yscale, + xticks, yticks, xlim, ylim) + + anim = Animation([line_block, title_block], timeline=timeline) + # TODO I think ax should be passed to timeline_slider args + # but that just plots a single huge timeline and no line plot?! + anim.controls(timeline_slider_args={'text': animate_over}) + return anim + + +def _create_timeline(darray, animate_over, fps): + if animate_over in darray.coords: + t_array = darray.coords[animate_over].values + else: # animating over a dimension without coords + t_array = np.arange(darray.sizes[animate_over]) + + if darray.coords[animate_over].attrs.get('units'): + units = ' [{}]'.format(darray.coords[animate_over].attrs['units']) + else: + units = '' + return Timeline(t_array, units=units, fps=fps) diff --git a/xarray/plot/plot.py b/xarray/plot/plot.py index d1885cbaae4..64a2c54d1cd 100644 --- a/xarray/plot/plot.py +++ b/xarray/plot/plot.py @@ -18,7 +18,8 @@ _interval_to_double_bound_points, _interval_to_mid_points, _process_cmap_cbar_kwargs, _rescale_imshow_rgb, _resolve_intervals_2dplot, _update_axes, _valid_other_type, get_axis, import_matplotlib_pyplot, - label_from_attrs) + label_from_attrs, _rotate_date_xlabels, _check_animate_over, + _transpose_before_animation) def _infer_line_data(darray, x, y, hue, animate_over): @@ -43,12 +44,6 @@ def _infer_line_data(darray, x, y, hue, animate_over): hueplt = None huelabel = '' - if animate_over is not None: - # Set animation dimension to be along last axis of data - dims = list(darray.dims) - dims.remove(animate_over) - darray = darray.transpose(*dims, animate_over) - if x is not None: xplt = darray[x] yplt = darray @@ -91,7 +86,6 @@ def _infer_line_data(darray, x, y, hue, animate_over): yplt = darray[yname] if yplt.ndim > 1: if huename in darray.dims: - # TODO bug in xarray? pycharm says otherdim is undefined here? otherindex = 1 if darray.dims.index(huename) == 0 else 0 xplt = darray.transpose(otherdim, huename) else: @@ -140,7 +134,8 @@ def plot(darray, row=None, col=None, col_wrap=None, ax=None, hue=None, animate_over: str, optional Dimension or coord in the DataArray over which to animate. If this argument is supplied then ``animatplot`` will be used to animate the - corresponding plot. + corresponding plot. The DataArray must have 1 more dimension than + specified in the table above. ax : matplotlib axes, optional If None, uses the current axis. Not applicable when using facets. rtol : number, optional @@ -165,42 +160,33 @@ def plot(darray, row=None, col=None, col_wrap=None, ax=None, hue=None, 'to a plottable type and plot your data directly with matplotlib.') if animate_over is not None: - if animate_over not in darray.dims and animate_over not in darray.coords: - raise ValueError("Can only animate over a dimension or coordinate " - "present in the DataArray") - if animate_over in darray.coords: - animate_dim = darray[animate_over].dims - if not len(animate_dim) == 1: - raise ValueError("Cannot animate over a multidimensional " - "coordinate") - else: - animate_dim = animate_over + animate_dim = _check_animate_over(darray, animate_over) kwargs['animate_over'] = animate_over else: animate_dim = None - plot_dims = set(darray.dims) + dims = set(darray.dims) if animate_over is not None: - plot_dims= plot_dims - set(animate_dim) + plot_dims = dims - set([animate_dim]) plot_dims.discard(row) plot_dims.discard(col) plot_dims.discard(hue) - ndims = len(plot_dims) + nplotdims = len(plot_dims) error_msg = ('Only 1d and 2d plots are supported for facets in xarray. ' 'See the package `Seaborn` for more options.') - if ndims in [1, 2]: + if nplotdims in [1, 2]: if row or col: kwargs['row'] = row kwargs['col'] = col kwargs['col_wrap'] = col_wrap kwargs['subplot_kws'] = subplot_kws - if ndims == 1: + if nplotdims == 1: plotfunc = line kwargs['hue'] = hue - elif ndims == 2: + elif nplotdims == 2: if hue: plotfunc = line kwargs['hue'] = hue @@ -213,8 +199,12 @@ def plot(darray, row=None, col=None, col_wrap=None, ax=None, hue=None, kwargs['ax'] = ax - if animate_over is not None and plotfunc is not line: - raise NotImplementedError + if animate_over is not None: + if plotfunc is line: + from .animate import animate_line + plotfunc = animate_line + else: + raise NotImplementedError return plotfunc(darray, **kwargs) @@ -231,7 +221,7 @@ def line(darray, *args, **kwargs): ---------- darray : DataArray Must be 1 dimensional, unless ``animate_over`` is specified, in which - it must be 2 dimensional. + case it must be 2 dimensional. figsize : tuple, optional A tuple (width, height) of the figure in inches. Mutually exclusive with ``size`` and ``ax``. @@ -249,8 +239,8 @@ def line(darray, *args, **kwargs): If plotting against a 2D coordinate, ``hue`` must be a dimension. animate_over: str, optional Dimension or coord in the DataArray over which to animate. If this - argument is supplied then ``animatplot`` will be used to animate the - corresponding plot. + argument is supplied then this function will redirect to + ``xarray.animate.animate_line``. x, y : string, optional Dimensions or coordinates for x, y axis. Only one of these may be specified. @@ -274,6 +264,10 @@ def line(darray, *args, **kwargs): """ animate_over = kwargs.pop('animate_over', None) + if animate_over is not None: + darray, animate_dim = _transpose_before_animation(darray, animate_over) + from .animate import animate_line + return animate_line(darray, animate_over=animate_over, *args, **kwargs) # Handle facetgrids first row = kwargs.pop('row', None) @@ -286,10 +280,7 @@ def line(darray, *args, **kwargs): allargs.pop('darray') return _easy_facetgrid(darray, line, kind='line', **allargs) - if animate_over is not None: - ndims = len(darray[animate_over].dims) - else: - ndims = len(darray.dims) + ndims = len(darray.dims) if ndims > 2: raise ValueError('Line plots are for 1- or 2-dimensional DataArrays. ' 'Passed DataArray has {ndims} ' @@ -318,7 +309,7 @@ def line(darray, *args, **kwargs): ax = get_axis(figsize, size, aspect, ax) xplt, yplt, hueplt, xlabel, ylabel, huelabel = \ - _infer_line_data(darray, x, y, hue, animate_over) + _infer_line_data(darray, x, y, hue, animate_over=None) # Remove pd.Intervals if contained in xplt.values. if _valid_other_type(xplt.values, [pd.Interval]): @@ -343,31 +334,7 @@ def line(darray, *args, **kwargs): _ensure_plottable(xplt_val, yplt_val) - if animate_over is None: - primitive = ax.plot(xplt_val, yplt_val, *args, **kwargs) - else: - # TODO some better way of handling the optional imports - from animatplot.timeline import Timeline - from animatplot.blocks import Line - - if animate_over in darray.coords: - t_array = darray.coords[animate_over].values - else: # animating over a dimension without coords - t_array = np.arange(darray.dims[animate_over]) - fps = kwargs.pop('fps', 10) - if darray.coords[animate_over].attrs.get('units'): - units = ' [{}]'.format(darray.coords[animate_over].attrs['units']) - else: - units = '' - timeline = Timeline(t_array, units=units, fps=fps) - - if ylim is None: - ylim = [np.min(yplt_val), np.max(yplt_val)] - - # animatplot assumes that the x positions might vary over time too - xplt_val = np.repeat(xplt_val[..., np.newaxis], - repeats=len(timeline), axis=-1) - line_block = Line(x=xplt_val, y=yplt_val, ax=ax, t_axis=-1, **kwargs) + primitive = ax.plot(xplt_val, yplt_val, *args, **kwargs) if _labels: if xlabel is not None: @@ -376,46 +343,19 @@ def line(darray, *args, **kwargs): if ylabel is not None: ax.set_ylabel(ylabel) - if animate_over is None: - ax.set_title(darray._title_for_slice()) - else: - # Would be nicer if we had something like in GH issue #266 - frame_titles = [darray[{animate_over: i}]._title_for_slice() - for i in range(len(timeline))] - from animatplot.blocks import Title - title_block = Title(frame_titles, ax=ax) + ax.set_title(darray._title_for_slice()) if ndims == 2 and add_legend: - if animate_over is not None: - # TODO how might this work for multiple lines? - line_handles = line_block.line - else: - line_handles = primitive - ax.legend(handles=line_handles, + ax.legend(handles=primitive, labels=list(hueplt.values), title=huelabel) - # 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_ha('right') + _rotate_date_xlabels(xplt, ax) _update_axes(ax, xincrease, yincrease, xscale, yscale, xticks, yticks, xlim, ylim) - if animate_over is None: - return primitive - else: - from animatplot.animation import Animation - anim = Animation([line_block, title_block], timeline=timeline) - # TODO I think ax should be passed to timeline_slider args - # but that just plots a single huge timeline and no line plot?! - anim.controls(timeline_slider_args={'text': animate_over}) - return anim + return primitive def step(darray, *args, **kwargs): diff --git a/xarray/plot/utils.py b/xarray/plot/utils.py index 6d812fbc2bc..eae7731469b 100644 --- a/xarray/plot/utils.py +++ b/xarray/plot/utils.py @@ -390,6 +390,17 @@ def label_from_attrs(da, extra=''): return '\n'.join(textwrap.wrap(name + extra + units, 30)) +def _rotate_date_xlabels(xdata, ax): + # 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(xdata.dtype, np.datetime64): + for xlabels in ax.get_xticklabels(): + xlabels.set_rotation(30) + xlabels.set_ha('right') + + def _interval_to_mid_points(array): """ Helper function which returns an array @@ -679,3 +690,27 @@ def _process_cmap_cbar_kwargs(func, kwargs, data): cmap_params = _determine_cmap_params(**cmap_kwargs) return cmap_params, cbar_kwargs + + +def _check_animate_over(darray, animate_over): + if animate_over is None: + raise ValueError + + if animate_over not in darray.coords and animate_over not in darray.dims: + raise ValueError("Can only animate over a dimension or coordinate " + "present in the DataArray") + + anim_coord = darray[animate_over].variable + if anim_coord.ndim != 1: + raise ValueError('Coordinate {} must be 1 dimensional but is {}' + ' dimensional'.format(anim_coord, anim_coord.ndim)) + anim_dim = anim_coord.dims[0] + return anim_dim + + +# TODO _transpose_before_animation should be a decorator applied to animate_line etc? +def _transpose_before_animation(darray, animate_over): + # Set animation dimension to be along last axis of data + dims = list(darray.dims) + dims.remove(animate_over) + return darray.transpose(*dims, animate_over) diff --git a/xarray/tests/__init__.py b/xarray/tests/__init__.py index a7eafa92bd7..0b4b20d00ef 100644 --- a/xarray/tests/__init__.py +++ b/xarray/tests/__init__.py @@ -73,6 +73,8 @@ def LooseVersion(vstring): has_np113, requires_np113 = _importorskip('numpy', minversion='1.13.0') has_iris, requires_iris = _importorskip('iris') has_cfgrib, requires_cfgrib = _importorskip('cfgrib') +has_animatplot, requires_animatplot = _importorskip('animatplot', + minversion='0.3.0') # some special cases has_scipy_or_netCDF4 = has_scipy or has_netCDF4 diff --git a/xarray/tests/test_animate.py b/xarray/tests/test_animate.py new file mode 100644 index 00000000000..004c9903459 --- /dev/null +++ b/xarray/tests/test_animate.py @@ -0,0 +1,46 @@ +import numpy as np +import pytest + +from xarray import DataArray +from . import requires_animatplot + +# TODO should check that matplotlib >= 2.2 is present first? +try: + import animatplot as amp +except ImportError: + pass + + +@requires_animatplot +class TestAnimateLine: + @pytest.fixture(autouse=True) + def setUp(self): + d = np.array([[0.0, 1.1, 0.0, 2], + [0.1, 1.3, 0.2, 2.1], + [0.1, 1.4, 0.3, 2.2], + [0.2, 1.3, 0.2, 2.3], + [0.1, 1.2, 0.2, 2.2]]) + self.darray = DataArray(d, name='height', + coords={'time': 10*np.arange(d.shape[0]), + 'position': 0.1*np.arange(d.shape[1])}, + dims=('time', 'position'), + attrs={'units': 'm'}) + self.darray.time.attrs['units'] = 's' + self.darray.position.attrs['units'] = 'cm' + + @pytest.mark.slow + def test_animate_single_line(self): + + print(self.darray) + a = self.darray.plot.line(animate_over='time') + assert isinstance(a, amp.animation.Animation) + + line_block, title_block = a.blocks + assert isinstance(line_block, amp.blocks.Line) + assert isinstance(title_block, amp.blocks.Title) + + assert len(line_block) == 5 + assert len(line_block) == len(title_block) + + # TODO check many more things here + # (also better testing in animatplot needed) diff --git a/xarray/tests/test_plot.py b/xarray/tests/test_plot.py index a10dbad6514..245f7f3a75c 100644 --- a/xarray/tests/test_plot.py +++ b/xarray/tests/test_plot.py @@ -4,7 +4,6 @@ import numpy as np import pandas as pd import pytest -from numpy.testing import assert_array_equal import xarray as xr import xarray.plot as xplt @@ -455,41 +454,6 @@ def test_slice_in_title(self): assert 'd = 10' == title -class TestAnimateLine: - @pytest.fixture(autouse=True) - def setUp(self): - d = np.array([[0.0, 1.1, 0.0, 2], - [0.1, 1.3, 0.2, 2.1], - [0.1, 1.4, 0.3, 2.2], - [0.2, 1.3, 0.2, 2.3], - [0.1, 1.2, 0.2, 2.2]]) - self.darray = DataArray(d, name='height', - coords={'time': 10*np.arange(d.shape[0]), - 'position': 0.1*np.arange(d.shape[1])}, - dims=('time', 'position'), - attrs={'units': 'm'}) - self.darray.time.attrs['units'] = 's' - self.darray.position.attrs['units'] = 'cm' - - @pytest.mark.slow - def test_animate_single_line(self): - from animatplot.animation import Animation - print(self.darray) - a = self.darray.plot.line(animate_over='time') - assert isinstance(a, Animation) - - from animatplot import blocks - line_block, title_block = a.blocks - assert isinstance(line_block, blocks.Line) - assert isinstance(title_block, blocks.Title) - - assert len(line_block) == 5 - assert len(line_block) == len(title_block) - - # TODO check many more things here - # (also better testing in animatplot needed) - - class TestPlotStep(PlotTestCase): @pytest.fixture(autouse=True) def setUp(self): From 7a73ccc7d96596ddcd9274eb8071b50f29ad53cb Mon Sep 17 00:00:00 2001 From: Thomas Nicholas Date: Thu, 31 Jan 2019 13:49:43 +0000 Subject: [PATCH 12/30] Added timeline tests --- xarray/tests/test_animate.py | 41 +++++++++++++++++++++++++++++++----- 1 file changed, 36 insertions(+), 5 deletions(-) diff --git a/xarray/tests/test_animate.py b/xarray/tests/test_animate.py index 004c9903459..12ba8acf8fb 100644 --- a/xarray/tests/test_animate.py +++ b/xarray/tests/test_animate.py @@ -1,4 +1,5 @@ import numpy as np +import numpy.testing as npt import pytest from xarray import DataArray @@ -10,6 +11,38 @@ except ImportError: pass +from xarray.plot.animate import animate_line, _create_timeline + + +@requires_animatplot +class TestTimeline: + def test_coord_timeline(self): + da = DataArray([1, 2, 3], + coords={'duration': ('time', [0.1, 0.2, 0.3])}, + dims='time') + da.coords['duration'].attrs['units'] = 's' + timeline = _create_timeline(da, animate_over='duration', fps=5) + + assert isinstance(timeline, amp.animation.Timeline) + assert len(timeline) == len(da.coords['duration']) + assert timeline.units == ' [s]' + npt.assert_equal(timeline.t, da.coords['duration'].values) + assert timeline.fps == 5 + + def test_dim_timeline(self): + da = DataArray([10, 20], dims='Time') + timeline = _create_timeline(da, animate_over='Time', fps=5) + + assert isinstance(timeline, amp.animation.Timeline) + assert len(timeline) == da.sizes['Time'] + assert timeline.units == '' + npt.assert_equal(timeline.t, np.array([0, 1])) + assert timeline.fps == 5 + + @pytest.mark.xfail + def test_datetimeline(self): + assert False + @requires_animatplot class TestAnimateLine: @@ -30,12 +63,10 @@ def setUp(self): @pytest.mark.slow def test_animate_single_line(self): + anim = self.darray.plot(animate_over='time') + assert isinstance(anim, amp.animation.Animation) - print(self.darray) - a = self.darray.plot.line(animate_over='time') - assert isinstance(a, amp.animation.Animation) - - line_block, title_block = a.blocks + line_block, title_block = anim.blocks assert isinstance(line_block, amp.blocks.Line) assert isinstance(title_block, amp.blocks.Title) From e94c802388c3677e4c989c2d943f5285ac41e725 Mon Sep 17 00:00:00 2001 From: Thomas Nicholas Date: Thu, 31 Jan 2019 17:57:06 +0000 Subject: [PATCH 13/30] Now formats datetimes more nicely --- xarray/plot/animate.py | 11 ++++++++++- xarray/tests/test_animate.py | 9 +++++++-- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/xarray/plot/animate.py b/xarray/plot/animate.py index 8ee237a27c7..220ca2f2351 100644 --- a/xarray/plot/animate.py +++ b/xarray/plot/animate.py @@ -10,6 +10,8 @@ DataArray.plot._____(animate_over='__') """ +import datetime + import numpy as np import pandas as pd @@ -153,13 +155,20 @@ def animate_line(darray, animate_over=None, **kwargs): anim = Animation([line_block, title_block], timeline=timeline) # TODO I think ax should be passed to timeline_slider args # but that just plots a single huge timeline and no line plot?! - anim.controls(timeline_slider_args={'text': animate_over}) + anim.controls(timeline_slider_args={'text': animate_over, 'valfmt': '%s'}) return anim def _create_timeline(darray, animate_over, fps): + if animate_over in darray.coords: t_array = darray.coords[animate_over].values + + # Format datetimes in a nicer way + if isinstance(t_array[0], datetime.date) \ + or np.issubdtype(t_array.dtype, np.datetime64): + t_array = [pd.to_datetime(date) for date in t_array] + else: # animating over a dimension without coords t_array = np.arange(darray.sizes[animate_over]) diff --git a/xarray/tests/test_animate.py b/xarray/tests/test_animate.py index 12ba8acf8fb..19ca7a62bcb 100644 --- a/xarray/tests/test_animate.py +++ b/xarray/tests/test_animate.py @@ -39,9 +39,14 @@ def test_dim_timeline(self): npt.assert_equal(timeline.t, np.array([0, 1])) assert timeline.fps == 5 - @pytest.mark.xfail def test_datetimeline(self): - assert False + dates = np.array(['2000-01-01', '2000-01-02', '2000-01-03'], + dtype=np.datetime64) + da = DataArray([1, 2, 3], + coords={'date': ('time', dates)}, dims='time') + timeline = _create_timeline(da, animate_over='date', fps=5) + + assert str(timeline.t[0]) == '2000-01-01 00:00:00' @requires_animatplot From ff8215abd14c98a65c22c352ac792f0a0ee8f599 Mon Sep 17 00:00:00 2001 From: Thomas Nicholas Date: Sat, 2 Feb 2019 16:01:10 +0000 Subject: [PATCH 14/30] Update to match slight changing of input arg format in animatplot --- xarray/plot/animate.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/xarray/plot/animate.py b/xarray/plot/animate.py index 220ca2f2351..2cfb2eb6c11 100644 --- a/xarray/plot/animate.py +++ b/xarray/plot/animate.py @@ -131,9 +131,7 @@ def animate_line(darray, animate_over=None, **kwargs): ylim = [np.min(yplt_val), np.max(yplt_val)] # animatplot assumes that the x positions might vary over time too - xplt_val = np.repeat(xplt_val[..., np.newaxis], - repeats=len(timeline), axis=-1) - line_block = Line(x=xplt_val, y=yplt_val, ax=ax, t_axis=-1, **kwargs) + line_block = Line(xplt_val, yplt_val, ax=ax, t_axis=-1, **kwargs) if _labels: if xlabel is not None: From 0b0981de322d0dadb54561b8508a6e2afbc1358f Mon Sep 17 00:00:00 2001 From: Thomas Nicholas Date: Fri, 8 Feb 2019 11:49:21 +0000 Subject: [PATCH 15/30] bugfix --- xarray/plot/plot.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/xarray/plot/plot.py b/xarray/plot/plot.py index 64a2c54d1cd..9d1c743cbfc 100644 --- a/xarray/plot/plot.py +++ b/xarray/plot/plot.py @@ -168,6 +168,9 @@ def plot(darray, row=None, col=None, col_wrap=None, ax=None, hue=None, dims = set(darray.dims) if animate_over is not None: plot_dims = dims - set([animate_dim]) + else: + plot_dims = dims + plot_dims.discard(row) plot_dims.discard(col) plot_dims.discard(hue) From d449076a94bf975a95b5c0655b5f13830ed8a606 Mon Sep 17 00:00:00 2001 From: Thomas Nicholas Date: Thu, 21 Feb 2019 12:19:40 +0000 Subject: [PATCH 16/30] Updated CI to install animatplot --- .travis.yml | 1 + ci/requirements-py36-animatplot.yml | 25 +++++++++++++++++++++++++ setup.cfg | 2 ++ 3 files changed, 28 insertions(+) create mode 100644 ci/requirements-py36-animatplot.yml diff --git a/.travis.yml b/.travis.yml index fbc01b4815d..8714bbf9a7e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -17,6 +17,7 @@ matrix: - env: - CONDA_ENV=py36 - EXTRA_FLAGS="--run-flaky --run-network-tests" + - env: CONDA_ENV=py36-animatplot - env: CONDA_ENV=py36-dask-dev - env: CONDA_ENV=py36-pandas-dev - env: CONDA_ENV=py36-bottleneck-dev diff --git a/ci/requirements-py36-animatplot.yml b/ci/requirements-py36-animatplot.yml new file mode 100644 index 00000000000..993819dba98 --- /dev/null +++ b/ci/requirements-py36-animatplot.yml @@ -0,0 +1,25 @@ +name: test_env +channels: + - conda-forge +dependencies: + - python=3.6 + - animatplot + - cftime + - dask + - distributed + - h5py + - h5netcdf + - matplotlib + - netcdf4 + - pytest + - pytest-cov + - pytest-env + - coveralls + - pycodestyle + - numpy + - pandas + - scipy + - seaborn + - toolz + - bottleneck + - zarr diff --git a/setup.cfg b/setup.cfg index c80ff300a60..74bd6e05b41 100644 --- a/setup.cfg +++ b/setup.cfg @@ -21,6 +21,8 @@ known_first_party=xarray multi_line_output=4 # Most of the numerical computing stack doesn't have type annotations yet. +[mypy-animatplot.*] +ignore_missing_imports = True [mypy-bottleneck.*] ignore_missing_imports = True [mypy-cdms2.*] From 0e37d08cde23cab0396b9acaccdcc2e809d580b3 Mon Sep 17 00:00:00 2001 From: Thomas Nicholas Date: Thu, 21 Feb 2019 12:37:17 +0000 Subject: [PATCH 17/30] Finished merge properly --- xarray/core/variable.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/xarray/core/variable.py b/xarray/core/variable.py index b02594ab7be..b675317d83d 100644 --- a/xarray/core/variable.py +++ b/xarray/core/variable.py @@ -1325,11 +1325,7 @@ def where(self, cond, other=dtypes.NA): return ops.where_method(self, cond, other) def reduce(self, func, dim=None, axis=None, -<<<<<<< HEAD keep_attrs=None, allow_lazy=False, **kwargs): -======= - keep_attrs=_set_keep_attrs(False), allow_lazy=False, **kwargs): ->>>>>>> 842a16d55db185cae53ac19d9b06381775a1adf2 """Reduce this array by applying `func` along some dimension(s). Parameters From ca2cb25151c7d73fcba6a2cd9c8d8ddacb8e5b46 Mon Sep 17 00:00:00 2001 From: Tom Nicholas <35968931+TomNicholas@users.noreply.github.com> Date: Thu, 21 Feb 2019 13:41:10 +0000 Subject: [PATCH 18/30] Removed accidental inclusions from another branch --- xarray/core/missing.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/xarray/core/missing.py b/xarray/core/missing.py index 26d7fa06fc6..50c420206cd 100644 --- a/xarray/core/missing.py +++ b/xarray/core/missing.py @@ -12,7 +12,6 @@ from .duck_array_ops import dask_array_type, datetime_to_numeric from .utils import OrderedSet, is_scalar from .variable import Variable, broadcast_variables -from .options import _set_keep_attrs class BaseInterpolator(object): @@ -217,7 +216,7 @@ def interp_na(self, dim=None, use_coordinate=True, method='linear', limit=None, output_dtypes=[self.dtype], dask='parallelized', vectorize=True, - keep_attrs=_set_keep_attrs(True)).transpose(*self.dims) + keep_attrs=True).transpose(*self.dims) if limit is not None: arr = arr.where(valids) @@ -268,7 +267,7 @@ def ffill(arr, dim=None, limit=None): return apply_ufunc(bn.push, arr, dask='parallelized', - keep_attrs=_set_keep_attrs(True), + keep_attrs=True, output_dtypes=[arr.dtype], kwargs=dict(n=_limit, axis=axis)).transpose(*arr.dims) @@ -282,7 +281,7 @@ def bfill(arr, dim=None, limit=None): return apply_ufunc(_bfill, arr, dask='parallelized', - keep_attrs=_set_keep_attrs(True), + keep_attrs=True, output_dtypes=[arr.dtype], kwargs=dict(n=_limit, axis=axis)).transpose(*arr.dims) From e2654ebe5ab70443060c03509a192da1f1c7e699 Mon Sep 17 00:00:00 2001 From: Thomas Nicholas Date: Thu, 21 Feb 2019 14:17:48 +0000 Subject: [PATCH 19/30] Only import animatplot when required --- xarray/plot/animate.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/xarray/plot/animate.py b/xarray/plot/animate.py index 2cfb2eb6c11..331ebf503a9 100644 --- a/xarray/plot/animate.py +++ b/xarray/plot/animate.py @@ -20,9 +20,6 @@ _valid_other_type, get_axis, _rotate_date_xlabels, _check_animate_over, _transpose_before_animation) -from animatplot.blocks import Line, Title -from animatplot.animation import Animation, Timeline - def animate_line(darray, animate_over=None, **kwargs): """ @@ -71,6 +68,9 @@ def animate_line(darray, animate_over=None, **kwargs): """ + from animatplot.blocks import Line, Title + from animatplot.animation import Animation + row = kwargs.pop('row', None) col = kwargs.pop('col', None) if row or col: @@ -159,6 +159,8 @@ def animate_line(darray, animate_over=None, **kwargs): def _create_timeline(darray, animate_over, fps): + from animatplot.animation import Timeline + if animate_over in darray.coords: t_array = darray.coords[animate_over].values From 981dcdcdf7acd39d8f4585b3376c3f7f3a5b986e Mon Sep 17 00:00:00 2001 From: Thomas Nicholas Date: Sat, 23 Feb 2019 11:37:40 +0000 Subject: [PATCH 20/30] Trigger new CI build From 507ca440aa6ba324738e0a04cb4aaaf65e89ef10 Mon Sep 17 00:00:00 2001 From: Thomas Nicholas Date: Fri, 8 Mar 2019 23:11:45 +0000 Subject: [PATCH 21/30] animate_over -> animate --- xarray/plot/animate.py | 36 ++++++++++++++++---------------- xarray/plot/facetgrid.py | 4 ++-- xarray/plot/plot.py | 40 ++++++++++++++++++------------------ xarray/plot/utils.py | 17 +++++++-------- xarray/tests/test_animate.py | 13 ++++++------ 5 files changed, 56 insertions(+), 54 deletions(-) diff --git a/xarray/plot/animate.py b/xarray/plot/animate.py index 331ebf503a9..ba631955c67 100644 --- a/xarray/plot/animate.py +++ b/xarray/plot/animate.py @@ -5,9 +5,9 @@ Or use the methods on a DataArray: DataArray.plot.animate_____ -Or supply an ``animate_over`` keyword +Or supply an ``animate`` keyword argument to a normal plotting function: - DataArray.plot._____(animate_over='__') + DataArray.plot._____(animate='__') """ import datetime @@ -18,10 +18,10 @@ from .plot import _infer_line_data from .utils import (_ensure_plottable, _interval_to_mid_points, _update_axes, _valid_other_type, get_axis, _rotate_date_xlabels, - _check_animate_over, _transpose_before_animation) + _check_animate, _transpose_before_animation) -def animate_line(darray, animate_over=None, **kwargs): +def animate_line(darray, animate=None, **kwargs): """ Line plot of DataArray index against values @@ -31,7 +31,7 @@ def animate_line(darray, animate_over=None, **kwargs): ---------- darray : DataArray Must be 2 dimensional. - animate_over: str + animate: str Dimension or coord in the DataArray over which to animate. ``animatplot.blocks.Line`` will be used to animate the plot over this dimension. @@ -80,10 +80,10 @@ def animate_line(darray, animate_over=None, **kwargs): if hue: raise NotImplementedError - _check_animate_over(darray, animate_over) - darray = _transpose_before_animation(darray, animate_over) + _check_animate(darray, animate) + darray = _transpose_before_animation(darray, animate) - ndims = len(darray[animate_over].dims) + ndims = len(darray[animate].dims) if ndims > 1: raise NotImplementedError @@ -107,7 +107,7 @@ def animate_line(darray, animate_over=None, **kwargs): ax = get_axis(figsize, size, aspect, ax) xplt, yplt, hueplt, xlabel, ylabel, huelabel = \ - _infer_line_data(darray, x, y, hue, animate_over) + _infer_line_data(darray, x, y, hue, animate) # Remove pd.Intervals if contained in xplt.values. if _valid_other_type(xplt.values, [pd.Interval]): @@ -125,7 +125,7 @@ def animate_line(darray, animate_over=None, **kwargs): _ensure_plottable(xplt_val, yplt_val) fps = kwargs.pop('fps', 10) - timeline = _create_timeline(darray, animate_over, fps) + timeline = _create_timeline(darray, animate, fps) if ylim is None: ylim = [np.min(yplt_val), np.max(yplt_val)] @@ -141,7 +141,7 @@ def animate_line(darray, animate_over=None, **kwargs): ax.set_ylabel(ylabel) # Would be nicer if we had something like in GH issue #266 - frame_titles = [darray[{animate_over: i}]._title_for_slice() + frame_titles = [darray[{animate: i}]._title_for_slice() for i in range(len(timeline))] title_block = Title(frame_titles, ax=ax) @@ -153,16 +153,16 @@ def animate_line(darray, animate_over=None, **kwargs): anim = Animation([line_block, title_block], timeline=timeline) # TODO I think ax should be passed to timeline_slider args # but that just plots a single huge timeline and no line plot?! - anim.controls(timeline_slider_args={'text': animate_over, 'valfmt': '%s'}) + anim.controls(timeline_slider_args={'text': animate, 'valfmt': '%s'}) return anim -def _create_timeline(darray, animate_over, fps): +def _create_timeline(darray, animate, fps): from animatplot.animation import Timeline - if animate_over in darray.coords: - t_array = darray.coords[animate_over].values + if animate in darray.coords: + t_array = darray.coords[animate].values # Format datetimes in a nicer way if isinstance(t_array[0], datetime.date) \ @@ -170,10 +170,10 @@ def _create_timeline(darray, animate_over, fps): t_array = [pd.to_datetime(date) for date in t_array] else: # animating over a dimension without coords - t_array = np.arange(darray.sizes[animate_over]) + t_array = np.arange(darray.sizes[animate]) - if darray.coords[animate_over].attrs.get('units'): - units = ' [{}]'.format(darray.coords[animate_over].attrs['units']) + if darray.coords[animate].attrs.get('units'): + units = ' [{}]'.format(darray.coords[animate].attrs['units']) else: units = '' return Timeline(t_array, units=units, fps=fps) diff --git a/xarray/plot/facetgrid.py b/xarray/plot/facetgrid.py index 3fee3d2fa61..9396bbf7def 100644 --- a/xarray/plot/facetgrid.py +++ b/xarray/plot/facetgrid.py @@ -267,10 +267,10 @@ def map_dataarray_line(self, func, x, y, **kwargs): mappable = func(subset, x=x, y=y, ax=ax, **func_kwargs) self._mappables.append(mappable) - animate_over = kwargs.pop('animate_over', None) + animate = kwargs.pop('animate', None) _, _, hueplt, xlabel, ylabel, huelabel = _infer_line_data( darray=self.data.loc[self.name_dicts.flat[0]], - x=x, y=y, hue=func_kwargs['hue'], animate_over=animate_over) + x=x, y=y, hue=func_kwargs['hue'], animate=animate) self._hue_var = hueplt self._hue_label = huelabel diff --git a/xarray/plot/plot.py b/xarray/plot/plot.py index 381369131b2..321dde6b186 100644 --- a/xarray/plot/plot.py +++ b/xarray/plot/plot.py @@ -16,11 +16,11 @@ _interval_to_double_bound_points, _interval_to_mid_points, _process_cmap_cbar_kwargs, _rescale_imshow_rgb, _resolve_intervals_2dplot, _update_axes, _valid_other_type, get_axis, import_matplotlib_pyplot, - label_from_attrs, _rotate_date_xlabels, _check_animate_over, + label_from_attrs, _rotate_date_xlabels, _check_animate, _transpose_before_animation) -def _infer_line_data(darray, x, y, hue, animate_over): +def _infer_line_data(darray, x, y, hue, animate): error_msg = ('must be either None or one of ({0:s})' .format(', '.join([repr(dd) for dd in darray.dims]))) ndims = len(darray.dims) @@ -36,7 +36,7 @@ def _infer_line_data(darray, x, y, hue, animate_over): 'for line plots.') # TODO there must be a neat one-line way of doing this check - animate_ndim = 1 if animate_over is not None else 0 + animate_ndim = 1 if animate is not None else 0 if ndims - animate_ndim == 1: huename = None hueplt = None @@ -56,7 +56,7 @@ def _infer_line_data(darray, x, y, hue, animate_over): yplt = darray else: - if animate_over is not None: + if animate is not None: raise NotImplementedError if x is None and y is None and hue is None: @@ -103,7 +103,7 @@ def _infer_line_data(darray, x, y, hue, animate_over): def plot(darray, row=None, col=None, col_wrap=None, ax=None, hue=None, - rtol=0.01, animate_over=None, subplot_kws=None, **kwargs): + rtol=0.01, animate=None, subplot_kws=None, **kwargs): """ Default plot of DataArray using matplotlib.pyplot. @@ -129,7 +129,7 @@ def plot(darray, row=None, col=None, col_wrap=None, ax=None, hue=None, If passed, make faceted line plots with hue on this dimension name col_wrap : integer, optional Use together with ``col`` to wrap faceted plots - animate_over: str, optional + animate: str, optional Dimension or coord in the DataArray over which to animate. If this argument is supplied then ``animatplot`` will be used to animate the corresponding plot. The DataArray must have 1 more dimension than @@ -148,14 +148,14 @@ def plot(darray, row=None, col=None, col_wrap=None, ax=None, hue=None, """ darray = darray.squeeze() - if animate_over is not None: - animate_dim = _check_animate_over(darray, animate_over) - kwargs['animate_over'] = animate_over + if animate is not None: + animate_dim = _check_animate(darray, animate) + kwargs['animate'] = animate else: animate_dim = None dims = set(darray.dims) - if animate_over is not None: + if animate is not None: plot_dims = dims - set([animate_dim]) else: plot_dims = dims @@ -191,7 +191,7 @@ def plot(darray, row=None, col=None, col_wrap=None, ax=None, hue=None, kwargs['ax'] = ax - if animate_over is not None: + if animate is not None: if plotfunc is line: from .animate import animate_line plotfunc = animate_line @@ -212,8 +212,8 @@ def line(darray, *args, **kwargs): Parameters ---------- darray : DataArray - Must be 1 dimensional, unless ``animate_over`` is specified, in which - case it must be 2 dimensional. + Must be 1 dimensional, unless ``animate`` is specified, in which case + it must be 2 dimensional. figsize : tuple, optional A tuple (width, height) of the figure in inches. Mutually exclusive with ``size`` and ``ax``. @@ -229,7 +229,7 @@ def line(darray, *args, **kwargs): hue : string, optional Dimension or coordinate for which you want multiple lines plotted. If plotting against a 2D coordinate, ``hue`` must be a dimension. - animate_over: str, optional + animate: str, optional Dimension or coord in the DataArray over which to animate. If this argument is supplied then this function will redirect to ``xarray.animate.animate_line``. @@ -255,17 +255,17 @@ def line(darray, *args, **kwargs): """ - animate_over = kwargs.pop('animate_over', None) - if animate_over is not None: - darray, animate_dim = _transpose_before_animation(darray, animate_over) + animate = kwargs.pop('animate', None) + if animate is not None: + darray, animate_dim = _transpose_before_animation(darray, animate) from .animate import animate_line - return animate_line(darray, animate_over=animate_over, *args, **kwargs) + return animate_line(darray, animate=animate, *args, **kwargs) # Handle facetgrids first row = kwargs.pop('row', None) col = kwargs.pop('col', None) if row or col: - if animate_over is not None: + if animate is not None: raise NotImplementedError allargs = locals().copy() allargs.update(allargs.pop('kwargs')) @@ -301,7 +301,7 @@ def line(darray, *args, **kwargs): ax = get_axis(figsize, size, aspect, ax) xplt, yplt, hueplt, xlabel, ylabel, huelabel = \ - _infer_line_data(darray, x, y, hue, animate_over=None) + _infer_line_data(darray, x, y, hue, animate=None) # Remove pd.Intervals if contained in xplt.values. if _valid_other_type(xplt.values, [pd.Interval]): diff --git a/xarray/plot/utils.py b/xarray/plot/utils.py index 2606b2669e6..7e5a029f8a2 100644 --- a/xarray/plot/utils.py +++ b/xarray/plot/utils.py @@ -715,15 +715,15 @@ def _process_cmap_cbar_kwargs(func, kwargs, data): return cmap_params, cbar_kwargs -def _check_animate_over(darray, animate_over): - if animate_over is None: +def _check_animate(darray, animate): + if animate is None: raise ValueError - if animate_over not in darray.coords and animate_over not in darray.dims: + if animate not in darray.coords and animate not in darray.dims: raise ValueError("Can only animate over a dimension or coordinate " "present in the DataArray") - anim_coord = darray[animate_over].variable + anim_coord = darray[animate].variable if anim_coord.ndim != 1: raise ValueError('Coordinate {} must be 1 dimensional but is {}' ' dimensional'.format(anim_coord, anim_coord.ndim)) @@ -731,9 +731,10 @@ def _check_animate_over(darray, animate_over): return anim_dim -# TODO _transpose_before_animation should be a decorator applied to animate_line etc? -def _transpose_before_animation(darray, animate_over): +# TODO _transpose_before_animation should be a decorator applied to +# animate_line etc? +def _transpose_before_animation(darray, animate): # Set animation dimension to be along last axis of data dims = list(darray.dims) - dims.remove(animate_over) - return darray.transpose(*dims, animate_over) + dims.remove(animate) + return darray.transpose(*dims, animate) diff --git a/xarray/tests/test_animate.py b/xarray/tests/test_animate.py index 19ca7a62bcb..52131430749 100644 --- a/xarray/tests/test_animate.py +++ b/xarray/tests/test_animate.py @@ -21,7 +21,7 @@ def test_coord_timeline(self): coords={'duration': ('time', [0.1, 0.2, 0.3])}, dims='time') da.coords['duration'].attrs['units'] = 's' - timeline = _create_timeline(da, animate_over='duration', fps=5) + timeline = _create_timeline(da, animate='duration', fps=5) assert isinstance(timeline, amp.animation.Timeline) assert len(timeline) == len(da.coords['duration']) @@ -31,7 +31,7 @@ def test_coord_timeline(self): def test_dim_timeline(self): da = DataArray([10, 20], dims='Time') - timeline = _create_timeline(da, animate_over='Time', fps=5) + timeline = _create_timeline(da, animate='Time', fps=5) assert isinstance(timeline, amp.animation.Timeline) assert len(timeline) == da.sizes['Time'] @@ -44,7 +44,7 @@ def test_datetimeline(self): dtype=np.datetime64) da = DataArray([1, 2, 3], coords={'date': ('time', dates)}, dims='time') - timeline = _create_timeline(da, animate_over='date', fps=5) + timeline = _create_timeline(da, animate='date', fps=5) assert str(timeline.t[0]) == '2000-01-01 00:00:00' @@ -58,9 +58,10 @@ def setUp(self): [0.1, 1.4, 0.3, 2.2], [0.2, 1.3, 0.2, 2.3], [0.1, 1.2, 0.2, 2.2]]) + coords = {'time': 10 * np.arange(d.shape[0]), + 'position': 0.1 * np.arange(d.shape[1])} self.darray = DataArray(d, name='height', - coords={'time': 10*np.arange(d.shape[0]), - 'position': 0.1*np.arange(d.shape[1])}, + coords=coords, dims=('time', 'position'), attrs={'units': 'm'}) self.darray.time.attrs['units'] = 's' @@ -68,7 +69,7 @@ def setUp(self): @pytest.mark.slow def test_animate_single_line(self): - anim = self.darray.plot(animate_over='time') + anim = self.darray.plot(animate='time') assert isinstance(anim, amp.animation.Animation) line_block, title_block = anim.blocks From cfa2d4a8eedb8ad229e2084127cf6bd507afa466 Mon Sep 17 00:00:00 2001 From: Thomas Nicholas Date: Sun, 10 Mar 2019 12:58:38 +0000 Subject: [PATCH 22/30] Restructuring to try to access animation functions as methods (incomplete) --- xarray/plot/__init__.py | 2 - xarray/plot/animate.py | 66 ++++++++++++++- xarray/plot/plot.py | 157 ++++------------------------------- xarray/plot/utils.py | 142 +++++++++++++++++++++++++++++++ xarray/tests/test_animate.py | 24 +++++- 5 files changed, 244 insertions(+), 147 deletions(-) diff --git a/xarray/plot/__init__.py b/xarray/plot/__init__.py index 74195310ade..51712e78bf8 100644 --- a/xarray/plot/__init__.py +++ b/xarray/plot/__init__.py @@ -3,8 +3,6 @@ from .facetgrid import FacetGrid -from .animate import animate_line - __all__ = [ 'plot', 'line', diff --git a/xarray/plot/animate.py b/xarray/plot/animate.py index ba631955c67..386dbf85659 100644 --- a/xarray/plot/animate.py +++ b/xarray/plot/animate.py @@ -3,7 +3,7 @@ import xarray.animate as xanim Or use the methods on a DataArray: - DataArray.plot.animate_____ + DataArray.plot.animate._____ Or supply an ``animate`` keyword argument to a normal plotting function: @@ -11,17 +11,59 @@ """ import datetime +import functools import numpy as np import pandas as pd -from .plot import _infer_line_data +from .utils import _infer_line_data, _infer_plot_type from .utils import (_ensure_plottable, _interval_to_mid_points, _update_axes, _valid_other_type, get_axis, _rotate_date_xlabels, _check_animate, _transpose_before_animation) -def animate_line(darray, animate=None, **kwargs): +def animate(darray, animate=None, **kwargs): + """ + Default plot of DataArray using animatplot. + + Calls xarray animated plotting function based on the dimensions of + darray.squeeze() + + =============== =========================== + Dimensions Plotting function + --------------- --------------------------- + 2 :py:func:`xarray.plot.animate.line` + Anything else Not yet implemented + =============== =========================== + + Parameters + ---------- + darray : DataArray + row : string, optional + If passed, make row faceted plots on this dimension name + col : string, optional + If passed, make column faceted plots on this dimension name + hue : string, optional + If passed, make faceted line plots with hue on this dimension name + col_wrap : integer, optional + Use together with ``col`` to wrap faceted plots + animate: str + Dimension or coord in the DataArray over which to animate. + ax : matplotlib axes, optional + If None, uses the current axis. Not applicable when using facets. + rtol : number, optional + Relative tolerance used to determine if the indexes + are uniformly spaced. Usually a small positive number. + subplot_kws : dict, optional + Dictionary of keyword arguments for matplotlib subplots. Only applies + to FacetGrid plotting. + **kwargs : optional + Additional keyword arguments to matplotlib + """ + return _AnimateMethods(darray, animate=animate, **kwargs) + + +def line(darray, animate=None, **kwargs): """ Line plot of DataArray index against values @@ -177,3 +219,21 @@ def _create_timeline(darray, animate, fps): else: units = '' return Timeline(t_array, units=units, fps=fps) + + +class _AnimateMethods: + """ + Enables use of xarray.plot.animate functions as attributes on a DataArray. + For example, DataArray.plot.animate.line + """ + + def __init__(self, darray, animate, **kwargs): + self._da = darray + self._animate = animate + + def __call__(self, **kwargs): + return animate(self._da, self._animate, **kwargs) + + @functools.wraps(line) + def line(self, animate, **kwargs): + return line(self._da, animate, **kwargs) diff --git a/xarray/plot/plot.py b/xarray/plot/plot.py index 321dde6b186..1dec163d0a6 100644 --- a/xarray/plot/plot.py +++ b/xarray/plot/plot.py @@ -14,92 +14,12 @@ from .utils import ( _add_colorbar, _ensure_plottable, _infer_interval_breaks, _infer_xy_labels, _interval_to_double_bound_points, _interval_to_mid_points, + _infer_line_data, _process_cmap_cbar_kwargs, _rescale_imshow_rgb, _resolve_intervals_2dplot, _update_axes, _valid_other_type, get_axis, import_matplotlib_pyplot, label_from_attrs, _rotate_date_xlabels, _check_animate, - _transpose_before_animation) - - -def _infer_line_data(darray, x, y, hue, animate): - error_msg = ('must be either None or one of ({0:s})' - .format(', '.join([repr(dd) for dd in darray.dims]))) - ndims = len(darray.dims) - - if x is not None and x not in darray.dims and x not in darray.coords: - raise ValueError('x ' + error_msg) - - if y is not None and y not in darray.dims and y not in darray.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.') - - # TODO there must be a neat one-line way of doing this check - animate_ndim = 1 if animate is not None else 0 - if ndims - animate_ndim == 1: - huename = None - hueplt = None - huelabel = '' - - if x is not None: - xplt = darray[x] - yplt = darray - - elif y is not None: - xplt = darray - yplt = darray[y] - - else: # Both x & y are None - dim = darray.dims[0] - xplt = darray[dim] - yplt = darray - - else: - if animate is not None: - raise NotImplementedError - - if x is None and y is None and hue is None: - raise ValueError('For 2D inputs, please' - 'specify either hue, x or y.') - - if y is None: - xname, huename = _infer_xy_labels(darray=darray, x=x, y=hue) - xplt = darray[xname] - if xplt.ndim > 1: - if huename in darray.dims: - otherindex = 1 if darray.dims.index(huename) == 0 else 0 - otherdim = darray.dims[otherindex] - yplt = darray.transpose(otherdim, huename) - xplt = xplt.transpose(otherdim, huename) - else: - raise ValueError('For 2D inputs, hue must be a dimension' - + ' i.e. one of ' + repr(darray.dims)) - - else: - yplt = darray.transpose(xname, huename) - - else: - yname, huename = _infer_xy_labels(darray=darray, x=y, y=hue) - yplt = darray[yname] - if yplt.ndim > 1: - if huename in darray.dims: - otherindex = 1 if darray.dims.index(huename) == 0 else 0 - xplt = darray.transpose(otherdim, huename) - else: - raise ValueError('For 2D inputs, hue must be a dimension' - + ' i.e. one of ' + repr(darray.dims)) - - else: - xplt = darray.transpose(yname, huename) - - huelabel = label_from_attrs(darray[huename]) - hueplt = darray[huename] - - xlabel = label_from_attrs(xplt) - ylabel = label_from_attrs(yplt) - - return xplt, yplt, hueplt, xlabel, ylabel, huelabel + _transpose_before_animation, _infer_plot_type) +from .animate import _AnimateMethods def plot(darray, row=None, col=None, col_wrap=None, ax=None, hue=None, @@ -146,59 +66,9 @@ def plot(darray, row=None, col=None, col_wrap=None, ax=None, hue=None, Additional keyword arguments to matplotlib """ - darray = darray.squeeze() - - if animate is not None: - animate_dim = _check_animate(darray, animate) - kwargs['animate'] = animate - else: - animate_dim = None - - dims = set(darray.dims) - if animate is not None: - plot_dims = dims - set([animate_dim]) - else: - plot_dims = dims - - plot_dims.discard(row) - plot_dims.discard(col) - plot_dims.discard(hue) - - nplotdims = len(plot_dims) - - error_msg = ('Only 1d and 2d plots are supported for facets in xarray. ' - 'See the package `Seaborn` for more options.') - - if nplotdims in [1, 2]: - if row or col: - kwargs['row'] = row - kwargs['col'] = col - kwargs['col_wrap'] = col_wrap - kwargs['subplot_kws'] = subplot_kws - if nplotdims == 1: - plotfunc = line - kwargs['hue'] = hue - elif nplotdims == 2: - if hue: - plotfunc = line - kwargs['hue'] = hue - else: - plotfunc = pcolormesh - else: - if row or col or hue: - raise ValueError(error_msg) - plotfunc = hist - - kwargs['ax'] = ax - - if animate is not None: - if plotfunc is line: - from .animate import animate_line - plotfunc = animate_line - else: - raise NotImplementedError - - return plotfunc(darray, **kwargs) + return _infer_plot_type(darray, row=row, col=col, col_wrap=col_wrap, + ax=ax, hue=hue, rtol=rtol, animate=animate, + subplot_kws=subplot_kws, **kwargs) # This function signature should not change so that it can use @@ -257,9 +127,10 @@ def line(darray, *args, **kwargs): animate = kwargs.pop('animate', None) if animate is not None: - darray, animate_dim = _transpose_before_animation(darray, animate) - from .animate import animate_line - return animate_line(darray, animate=animate, *args, **kwargs) + animate_dim = _check_animate(darray, animate) + darray = _transpose_before_animation(darray, animate) + from .animate import line as animate_line + return animate_line(darray, animate=animate, **kwargs) # Handle facetgrids first row = kwargs.pop('row', None) @@ -445,7 +316,7 @@ def hist(darray, figsize=None, size=None, aspect=None, ax=None, **kwargs): # MUST run before any 2d plotting functions are defined since # _plot2d decorator adds them as methods here. -class _PlotMethods(object): +class _PlotMethods: """ Enables use of xarray.plot functions as attributes on a DataArray. For example, DataArray.plot.imshow @@ -469,6 +340,12 @@ def line(self, *args, **kwargs): def step(self, *args, **kwargs): return step(self._da, *args, **kwargs) + from .animate import animate + + @functools.wraps(animate) + def animate(self, **kwargs): + return _AnimateMethods(self._da, **kwargs) + def _plot2d(plotfunc): """ diff --git a/xarray/plot/utils.py b/xarray/plot/utils.py index 7e5a029f8a2..3c34a6a20e7 100644 --- a/xarray/plot/utils.py +++ b/xarray/plot/utils.py @@ -273,6 +273,148 @@ def _determine_cmap_params(plot_data, vmin=None, vmax=None, cmap=None, levels=levels, norm=norm) +def _infer_plot_type(darray, row=None, col=None, col_wrap=None, ax=None, + hue=None, rtol=0.01, animate=None, subplot_kws=None, + **kwargs): + from .plot import line, pcolormesh, hist + + darray = darray.squeeze() + + if animate is not None: + animate_dim = _check_animate(darray, animate) + kwargs['animate'] = animate + else: + animate_dim = None + + dims = set(darray.dims) + if animate is not None: + plot_dims = dims - set([animate_dim]) + else: + plot_dims = dims + + plot_dims.discard(row) + plot_dims.discard(col) + plot_dims.discard(hue) + + nplotdims = len(plot_dims) + + error_msg = ('Only 1d and 2d plots are supported for facets in xarray. ' + 'See the package `Seaborn` for more options.') + + if nplotdims in [1, 2]: + if row or col: + kwargs['row'] = row + kwargs['col'] = col + kwargs['col_wrap'] = col_wrap + kwargs['subplot_kws'] = subplot_kws + if nplotdims == 1: + plotfunc = line + kwargs['hue'] = hue + elif nplotdims == 2: + if hue: + plotfunc = line + kwargs['hue'] = hue + else: + plotfunc = pcolormesh + else: + if row or col or hue: + raise ValueError(error_msg) + plotfunc = hist + + kwargs['ax'] = ax + + if animate is not None: + if plotfunc is line: + from .animate import line as animate_line + plotfunc = animate_line + else: + raise NotImplementedError + + return plotfunc(darray, **kwargs) + + +def _infer_line_data(darray, x, y, hue, animate): + error_msg = ('must be either None or one of ({0:s})' + .format(', '.join([repr(dd) for dd in darray.dims]))) + ndims = len(darray.dims) + + if x is not None and x not in darray.dims and x not in darray.coords: + raise ValueError('x ' + error_msg) + + if y is not None and y not in darray.dims and y not in darray.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.') + + # TODO there must be a neat one-line way of doing this check + animate_ndim = 1 if animate is not None else 0 + if ndims - animate_ndim == 1: + huename = None + hueplt = None + huelabel = '' + + if x is not None: + xplt = darray[x] + yplt = darray + + elif y is not None: + xplt = darray + yplt = darray[y] + + else: # Both x & y are None + dim = darray.dims[0] + xplt = darray[dim] + yplt = darray + + else: + if animate is not None: + raise NotImplementedError + + if x is None and y is None and hue is None: + raise ValueError('For 2D inputs, please' + 'specify either hue, x or y.') + + if y is None: + xname, huename = _infer_xy_labels(darray=darray, x=x, y=hue) + xplt = darray[xname] + if xplt.ndim > 1: + if huename in darray.dims: + otherindex = 1 if darray.dims.index(huename) == 0 else 0 + otherdim = darray.dims[otherindex] + yplt = darray.transpose(otherdim, huename) + xplt = xplt.transpose(otherdim, huename) + else: + raise ValueError('For 2D inputs, hue must be a dimension' + + ' i.e. one of ' + repr(darray.dims)) + + else: + yplt = darray.transpose(xname, huename) + + else: + yname, huename = _infer_xy_labels(darray=darray, x=y, y=hue) + yplt = darray[yname] + if yplt.ndim > 1: + if huename in darray.dims: + otherindex = 1 if darray.dims.index(huename) == 0 else 0 + xplt = darray.transpose(otherdim, huename) + else: + raise ValueError('For 2D inputs, hue must be a dimension' + + ' i.e. one of ' + repr(darray.dims)) + + else: + xplt = darray.transpose(yname, huename) + + huelabel = label_from_attrs(darray[huename]) + hueplt = darray[huename] + + xlabel = label_from_attrs(xplt) + ylabel = label_from_attrs(yplt) + + return xplt, yplt, hueplt, xlabel, ylabel, huelabel + + def _infer_xy_labels_3d(darray, x, y, rgb): """ Determine x and y labels for showing RGB images. diff --git a/xarray/tests/test_animate.py b/xarray/tests/test_animate.py index 52131430749..064446fc97d 100644 --- a/xarray/tests/test_animate.py +++ b/xarray/tests/test_animate.py @@ -11,7 +11,8 @@ except ImportError: pass -from xarray.plot.animate import animate_line, _create_timeline +from xarray.plot.animate import _create_timeline +import xarray.plot.animate @requires_animatplot @@ -67,7 +68,6 @@ def setUp(self): self.darray.time.attrs['units'] = 's' self.darray.position.attrs['units'] = 'cm' - @pytest.mark.slow def test_animate_single_line(self): anim = self.darray.plot(animate='time') assert isinstance(anim, amp.animation.Animation) @@ -81,3 +81,23 @@ def test_animate_single_line(self): # TODO check many more things here # (also better testing in animatplot needed) + + def test_animate_as_function(self): + anim = xarray.plot.animate.line(self.darray, animate='time') + assert isinstance(anim, amp.animation.Animation) + + def test_animate_as_argument(self): + anim = self.darray.plot(animate='time') + assert isinstance(anim, amp.animation.Animation) + + anim = self.darray.plot.line(animate='time') + assert isinstance(anim, amp.animation.Animation) + + # TODO make this test pass + @pytest.mark.xfail + def test_animate_as_module(self): + anim = self.darray.plot.animate(animate='time') + assert isinstance(anim, amp.animation.Animation) + + anim = self.darray.plot.animate.line(animate='time') + assert isinstance(anim, amp.animation.Animation) From 08ef5bb4caa741cafcc97edabbd97122830172e7 Mon Sep 17 00:00:00 2001 From: Thomas Nicholas Date: Sun, 10 Mar 2019 13:38:04 +0000 Subject: [PATCH 23/30] Move pd.Intervals logic into infer_line_data --- xarray/plot/animate.py | 20 ++++---------------- xarray/plot/facetgrid.py | 3 ++- xarray/plot/plot.py | 28 ++++------------------------ xarray/plot/utils.py | 26 +++++++++++++++++++++++--- 4 files changed, 33 insertions(+), 44 deletions(-) diff --git a/xarray/plot/animate.py b/xarray/plot/animate.py index 386dbf85659..91b5012df03 100644 --- a/xarray/plot/animate.py +++ b/xarray/plot/animate.py @@ -137,6 +137,7 @@ def line(darray, animate=None, **kwargs): hue = kwargs.pop('hue', None) x = kwargs.pop('x', None) y = kwargs.pop('y', None) + linestyle = kwargs.get('linestyle', '') 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 @@ -148,21 +149,8 @@ def line(darray, animate=None, **kwargs): _labels = kwargs.pop('_labels', True) ax = get_axis(figsize, size, aspect, ax) - xplt, yplt, hueplt, xlabel, ylabel, huelabel = \ - _infer_line_data(darray, x, y, hue, animate) - - # Remove pd.Intervals if contained in xplt.values. - if _valid_other_type(xplt.values, [pd.Interval]): - # Is it a step plot? (see matplotlib.Axes.step) - if kwargs.get('linestyle', '').startswith('steps-'): - raise NotImplementedError - else: - xplt_val = _interval_to_mid_points(xplt.values) - yplt_val = yplt.values - xlabel += '_center' - else: - xplt_val = xplt.values - yplt_val = yplt.values + xplt_val, yplt_val, hueplt, xlabel, ylabel, huelabel = \ + _infer_line_data(darray, x, y, hue, animate, linestyle) _ensure_plottable(xplt_val, yplt_val) @@ -187,7 +175,7 @@ def line(darray, animate=None, **kwargs): for i in range(len(timeline))] title_block = Title(frame_titles, ax=ax) - _rotate_date_xlabels(xplt, ax) + _rotate_date_xlabels(xplt_val, ax) _update_axes(ax, xincrease, yincrease, xscale, yscale, xticks, yticks, xlim, ylim) diff --git a/xarray/plot/facetgrid.py b/xarray/plot/facetgrid.py index 9396bbf7def..dbf5ebfd2c0 100644 --- a/xarray/plot/facetgrid.py +++ b/xarray/plot/facetgrid.py @@ -270,7 +270,8 @@ def map_dataarray_line(self, func, x, y, **kwargs): animate = kwargs.pop('animate', None) _, _, hueplt, xlabel, ylabel, huelabel = _infer_line_data( darray=self.data.loc[self.name_dicts.flat[0]], - x=x, y=y, hue=func_kwargs['hue'], animate=animate) + x=x, y=y, hue=func_kwargs['hue'], animate=animate, + linestyle=func_kwargs.get('linestyle', '')) self._hue_var = hueplt self._hue_label = huelabel diff --git a/xarray/plot/plot.py b/xarray/plot/plot.py index 1dec163d0a6..50299631099 100644 --- a/xarray/plot/plot.py +++ b/xarray/plot/plot.py @@ -157,6 +157,7 @@ def line(darray, *args, **kwargs): hue = kwargs.pop('hue', None) x = kwargs.pop('x', None) y = kwargs.pop('y', None) + linestyle = kwargs.get('linestyle', '') 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 @@ -171,29 +172,8 @@ def line(darray, *args, **kwargs): args = kwargs.pop('args', ()) ax = get_axis(figsize, size, aspect, ax) - xplt, yplt, hueplt, xlabel, ylabel, huelabel = \ - _infer_line_data(darray, x, y, hue, animate=None) - - # Remove pd.Intervals if contained in xplt.values. - if _valid_other_type(xplt.values, [pd.Interval]): - # Is it a step plot? (see matplotlib.Axes.step) - if kwargs.get('linestyle', '').startswith('steps-'): - xplt_val, yplt_val = _interval_to_double_bound_points(xplt.values, - yplt.values) - # Remove steps-* to be sure that matplotlib is not confused - kwargs['linestyle'] = (kwargs['linestyle'] - .replace('steps-pre', '') - .replace('steps-post', '') - .replace('steps-mid', '')) - if kwargs['linestyle'] == '': - kwargs.pop('linestyle') - else: - xplt_val = _interval_to_mid_points(xplt.values) - yplt_val = yplt.values - xlabel += '_center' - else: - xplt_val = xplt.values - yplt_val = yplt.values + xplt_val, yplt_val, hueplt, xlabel, ylabel, huelabel = \ + _infer_line_data(darray, x, y, hue, animate, linestyle) _ensure_plottable(xplt_val, yplt_val) @@ -213,7 +193,7 @@ def line(darray, *args, **kwargs): labels=list(hueplt.values), title=huelabel) - _rotate_date_xlabels(xplt, ax) + _rotate_date_xlabels(xplt_val, ax) _update_axes(ax, xincrease, yincrease, xscale, yscale, xticks, yticks, xlim, ylim) diff --git a/xarray/plot/utils.py b/xarray/plot/utils.py index 3c34a6a20e7..a177c3a2ece 100644 --- a/xarray/plot/utils.py +++ b/xarray/plot/utils.py @@ -333,7 +333,7 @@ def _infer_plot_type(darray, row=None, col=None, col_wrap=None, ax=None, return plotfunc(darray, **kwargs) -def _infer_line_data(darray, x, y, hue, animate): +def _infer_line_data(darray, x, y, hue, animate, linestyle): error_msg = ('must be either None or one of ({0:s})' .format(', '.join([repr(dd) for dd in darray.dims]))) ndims = len(darray.dims) @@ -412,7 +412,27 @@ def _infer_line_data(darray, x, y, hue, animate): xlabel = label_from_attrs(xplt) ylabel = label_from_attrs(yplt) - return xplt, yplt, hueplt, xlabel, ylabel, huelabel + # Remove pd.Intervals if contained in xplt.values. + if _valid_other_type(xplt.values, [pd.Interval]): + # Is it a step plot? (see matplotlib.Axes.step) + if linestyle.startswith('steps-'): + xplt_val, yplt_val = _interval_to_double_bound_points(xplt.values, + yplt.values) + # Remove steps-* to be sure that matplotlib is not confused + linestyle = (linestyle.replace('steps-pre', '') + .replace('steps-post', '') + .replace('steps-mid', '')) + #if kwargs['linestyle'] == '': + # kwargs.pop('linestyle') + else: + xplt_val = _interval_to_mid_points(xplt.values) + yplt_val = yplt.values + xlabel += '_center' + else: + xplt_val = xplt.values + yplt_val = yplt.values + + return xplt_val, yplt_val, hueplt, xlabel, ylabel, huelabel def _infer_xy_labels_3d(darray, x, y, rgb): @@ -547,7 +567,7 @@ def _rotate_date_xlabels(xdata, ax): # 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(xdata.dtype, np.datetime64): + if np.issubdtype(np.array(xdata).dtype, np.datetime64): for xlabels in ax.get_xticklabels(): xlabels.set_rotation(30) xlabels.set_ha('right') From 9556ef1ade2b186b290c7d35d856d58b940ab2ee Mon Sep 17 00:00:00 2001 From: Thomas Nicholas Date: Tue, 12 Mar 2019 11:17:35 +0000 Subject: [PATCH 24/30] Removed syntax for darray.plot.animate() --- xarray/plot/animate.py | 81 +++++------------------------------- xarray/plot/plot.py | 14 ++----- xarray/tests/test_animate.py | 9 ---- 3 files changed, 13 insertions(+), 91 deletions(-) diff --git a/xarray/plot/animate.py b/xarray/plot/animate.py index 91b5012df03..6e9ba0d5299 100644 --- a/xarray/plot/animate.py +++ b/xarray/plot/animate.py @@ -2,65 +2,19 @@ Use this module directly: import xarray.animate as xanim -Or use the methods on a DataArray: - DataArray.plot.animate._____ - Or supply an ``animate`` keyword argument to a normal plotting function: DataArray.plot._____(animate='__') """ import datetime -import functools import numpy as np import pandas as pd -from .utils import _infer_line_data, _infer_plot_type -from .utils import (_ensure_plottable, _interval_to_mid_points, _update_axes, - _valid_other_type, get_axis, _rotate_date_xlabels, - _check_animate, _transpose_before_animation) - - -def animate(darray, animate=None, **kwargs): - """ - Default plot of DataArray using animatplot. - - Calls xarray animated plotting function based on the dimensions of - darray.squeeze() - - =============== =========================== - Dimensions Plotting function - --------------- --------------------------- - 2 :py:func:`xarray.plot.animate.line` - Anything else Not yet implemented - =============== =========================== - - Parameters - ---------- - darray : DataArray - row : string, optional - If passed, make row faceted plots on this dimension name - col : string, optional - If passed, make column faceted plots on this dimension name - hue : string, optional - If passed, make faceted line plots with hue on this dimension name - col_wrap : integer, optional - Use together with ``col`` to wrap faceted plots - animate: str - Dimension or coord in the DataArray over which to animate. - ax : matplotlib axes, optional - If None, uses the current axis. Not applicable when using facets. - rtol : number, optional - Relative tolerance used to determine if the indexes - are uniformly spaced. Usually a small positive number. - subplot_kws : dict, optional - Dictionary of keyword arguments for matplotlib subplots. Only applies - to FacetGrid plotting. - **kwargs : optional - Additional keyword arguments to matplotlib - """ - return _AnimateMethods(darray, animate=animate, **kwargs) +from .utils import (_infer_line_data, _ensure_plottable, _update_axes, + get_axis, _rotate_date_xlabels, _check_animate, + _transpose_before_animation) def line(darray, animate=None, **kwargs): @@ -116,18 +70,21 @@ def line(darray, animate=None, **kwargs): row = kwargs.pop('row', None) col = kwargs.pop('col', None) if row or col: - raise NotImplementedError + raise NotImplementedError("Animated FacetGrids not yet implemented") hue = kwargs.pop('hue', None) if hue: - raise NotImplementedError + raise NotImplementedError("Animating multiple lines at once is not yet" + "implemented") _check_animate(darray, animate) darray = _transpose_before_animation(darray, animate) ndims = len(darray[animate].dims) - if ndims > 1: - raise NotImplementedError + if ndims > 2: + raise ValueError('Animated line plots are for 2- or 3-dimensional ' + 'DataArrays. Passed DataArray has {ndims} ' + 'dimensions'.format(ndims=ndims+1)) # Ensures consistency with .plot method figsize = kwargs.pop('figsize', None) @@ -207,21 +164,3 @@ def _create_timeline(darray, animate, fps): else: units = '' return Timeline(t_array, units=units, fps=fps) - - -class _AnimateMethods: - """ - Enables use of xarray.plot.animate functions as attributes on a DataArray. - For example, DataArray.plot.animate.line - """ - - def __init__(self, darray, animate, **kwargs): - self._da = darray - self._animate = animate - - def __call__(self, **kwargs): - return animate(self._da, self._animate, **kwargs) - - @functools.wraps(line) - def line(self, animate, **kwargs): - return line(self._da, animate, **kwargs) diff --git a/xarray/plot/plot.py b/xarray/plot/plot.py index 50299631099..d814a1c9270 100644 --- a/xarray/plot/plot.py +++ b/xarray/plot/plot.py @@ -13,13 +13,11 @@ from .facetgrid import _easy_facetgrid from .utils import ( _add_colorbar, _ensure_plottable, _infer_interval_breaks, _infer_xy_labels, - _interval_to_double_bound_points, _interval_to_mid_points, - _infer_line_data, - _process_cmap_cbar_kwargs, _rescale_imshow_rgb, _resolve_intervals_2dplot, - _update_axes, _valid_other_type, get_axis, import_matplotlib_pyplot, + _infer_line_data, _process_cmap_cbar_kwargs, _rescale_imshow_rgb, + _resolve_intervals_2dplot, + _update_axes, get_axis, import_matplotlib_pyplot, label_from_attrs, _rotate_date_xlabels, _check_animate, _transpose_before_animation, _infer_plot_type) -from .animate import _AnimateMethods def plot(darray, row=None, col=None, col_wrap=None, ax=None, hue=None, @@ -320,12 +318,6 @@ def line(self, *args, **kwargs): def step(self, *args, **kwargs): return step(self._da, *args, **kwargs) - from .animate import animate - - @functools.wraps(animate) - def animate(self, **kwargs): - return _AnimateMethods(self._da, **kwargs) - def _plot2d(plotfunc): """ diff --git a/xarray/tests/test_animate.py b/xarray/tests/test_animate.py index 064446fc97d..d93070ad265 100644 --- a/xarray/tests/test_animate.py +++ b/xarray/tests/test_animate.py @@ -92,12 +92,3 @@ def test_animate_as_argument(self): anim = self.darray.plot.line(animate='time') assert isinstance(anim, amp.animation.Animation) - - # TODO make this test pass - @pytest.mark.xfail - def test_animate_as_module(self): - anim = self.darray.plot.animate(animate='time') - assert isinstance(anim, amp.animation.Animation) - - anim = self.darray.plot.animate.line(animate='time') - assert isinstance(anim, amp.animation.Animation) From 3c6b81803d07adb96095d49e5a91420bcc7129b5 Mon Sep 17 00:00:00 2001 From: Thomas Nicholas Date: Wed, 10 Apr 2019 16:00:42 +0100 Subject: [PATCH 25/30] More tests, also resets current axis ready for next plot --- xarray/plot/animate.py | 9 ++++++--- xarray/tests/test_animate.py | 32 +++++++++++++++++++++++++++++--- 2 files changed, 35 insertions(+), 6 deletions(-) diff --git a/xarray/plot/animate.py b/xarray/plot/animate.py index 6e9ba0d5299..400b4c14f35 100644 --- a/xarray/plot/animate.py +++ b/xarray/plot/animate.py @@ -14,7 +14,7 @@ from .utils import (_infer_line_data, _ensure_plottable, _update_axes, get_axis, _rotate_date_xlabels, _check_animate, - _transpose_before_animation) + _transpose_before_animation, import_matplotlib_pyplot) def line(darray, animate=None, **kwargs): @@ -138,9 +138,12 @@ def line(darray, animate=None, **kwargs): xticks, yticks, xlim, ylim) anim = Animation([line_block, title_block], timeline=timeline) - # TODO I think ax should be passed to timeline_slider args - # but that just plots a single huge timeline and no line plot?! anim.controls(timeline_slider_args={'text': animate, 'valfmt': '%s'}) + + # Stop subsequent matplotlib plotting calls plotting onto the pause button! + plt = import_matplotlib_pyplot() + plt.sca(ax) + return anim diff --git a/xarray/tests/test_animate.py b/xarray/tests/test_animate.py index d93070ad265..efcdb5d8b72 100644 --- a/xarray/tests/test_animate.py +++ b/xarray/tests/test_animate.py @@ -5,6 +5,13 @@ from xarray import DataArray from . import requires_animatplot +# import mpl and change the backend before other mpl imports +try: + import matplotlib as mpl + import matplotlib.pyplot as plt +except ImportError: + pass + # TODO should check that matplotlib >= 2.2 is present first? try: import animatplot as amp @@ -68,19 +75,38 @@ def setUp(self): self.darray.time.attrs['units'] = 's' self.darray.position.attrs['units'] = 'cm' - def test_animate_single_line(self): + def test_animate_single_line_classes(self): anim = self.darray.plot(animate='time') assert isinstance(anim, amp.animation.Animation) line_block, title_block = anim.blocks + assert isinstance(line_block, amp.blocks.Line) assert isinstance(title_block, amp.blocks.Title) + def test_animate_single_line_data(self): + line_block, title_block = self.darray.plot(animate='time').blocks + assert len(line_block) == 5 assert len(line_block) == len(title_block) - # TODO check many more things here - # (also better testing in animatplot needed) + npt.assert_equal(line_block.y, self.darray.transpose().values) + npt.assert_equal(line_block.x[:, 0], + self.darray.coords['position'].values) + + def test_animate_single_line_text(self): + anim = self.darray.plot(animate='time') + line_block, title_block = anim.blocks + + assert title_block.titles[0] == 'time = 0' + assert line_block.ax.get_xlabel() == 'position [cm]' + assert anim.timeline.units == ' [s]' + + def test_animate_single_line_axes(self): + line_block, title_block = self.darray.plot(animate='time').blocks + + # Check current axes is the plot (not the timeline etc.) + assert plt.gca() is line_block.ax def test_animate_as_function(self): anim = xarray.plot.animate.line(self.darray, animate='time') From 6fbe9de0b77a4b39411c6f936e3a550beb119e5f Mon Sep 17 00:00:00 2001 From: Thomas Nicholas Date: Wed, 10 Apr 2019 19:04:47 +0100 Subject: [PATCH 26/30] Added test for animated step plots --- xarray/tests/test_animate.py | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/xarray/tests/test_animate.py b/xarray/tests/test_animate.py index efcdb5d8b72..2a8e11b5fd7 100644 --- a/xarray/tests/test_animate.py +++ b/xarray/tests/test_animate.py @@ -1,7 +1,10 @@ +from functools import partial + import numpy as np import numpy.testing as npt import pytest +import xarray as xr from xarray import DataArray from . import requires_animatplot @@ -18,6 +21,8 @@ except ImportError: pass +from .test_plot import PlotTestCase, easy_array + from xarray.plot.animate import _create_timeline import xarray.plot.animate @@ -58,7 +63,7 @@ def test_datetimeline(self): @requires_animatplot -class TestAnimateLine: +class TestAnimateLine(PlotTestCase): @pytest.fixture(autouse=True) def setUp(self): d = np.array([[0.0, 1.1, 0.0, 2], @@ -102,6 +107,9 @@ def test_animate_single_line_text(self): assert line_block.ax.get_xlabel() == 'position [cm]' assert anim.timeline.units == ' [s]' + def test_can_pass_in_axis(self): + self.pass_in_axis(partial(self.darray.plot, animate='time')) + def test_animate_single_line_axes(self): line_block, title_block = self.darray.plot(animate='time').blocks @@ -118,3 +126,19 @@ def test_animate_as_argument(self): anim = self.darray.plot.line(animate='time') assert isinstance(anim, amp.animation.Animation) + + +@requires_animatplot +class TestAnimateStep(PlotTestCase): + @pytest.fixture(autouse=True) + def setUp(self): + self.darray = DataArray(easy_array((4, 5, 6))) + + def test_coord_with_interval_step(self): + bins = [-1, 0, 1, 2] + da = self.darray.groupby_bins('dim_0', bins).mean(xr.ALL_DIMS) + da = xr.concat([da, da*2, da*1.7], dim='new_dim') + + anim = da.plot.step(animate='new_dim') + assert len(plt.gca().lines[0].get_xdata()) == ((len(bins) - 1) * 2) + npt.assert_equal(anim.timeline.t, np.array([0, 1, 2])) From 62833ac52ac44ccc45eda616ff34b869968266e2 Mon Sep 17 00:00:00 2001 From: Thomas Nicholas Date: Wed, 10 Apr 2019 21:57:32 +0100 Subject: [PATCH 27/30] Additional test if 2D line plot handles y kwarg right --- xarray/tests/test_plot.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/xarray/tests/test_plot.py b/xarray/tests/test_plot.py index e9762b19d5e..e92998b6f6f 100644 --- a/xarray/tests/test_plot.py +++ b/xarray/tests/test_plot.py @@ -186,6 +186,13 @@ def test_2d_line_accepts_x_kw(self): self.darray[:, :, 0].plot.line(x='dim_1') assert plt.gca().get_xlabel() == 'dim_1' + def test_2d_line_accepts_y_kw(self): + self.darray[:, :, 0].plot.line(y='dim_0') + assert plt.gca().get_ylabel() == 'dim_0' + plt.cla() + self.darray[:, :, 0].plot.line(y='dim_1') + assert plt.gca().get_ylabel() == 'dim_1' + def test_2d_line_accepts_hue_kw(self): self.darray[:, :, 0].plot.line(hue='dim_0') assert (plt.gca().get_legend().get_title().get_text() From a8fe6919a701be97f8985eb9901a83dc4bda297d Mon Sep 17 00:00:00 2001 From: Thomas Nicholas Date: Thu, 11 Apr 2019 00:47:05 +0100 Subject: [PATCH 28/30] (Incomplete) support for animating multiple lines --- xarray/plot/animate.py | 35 ++++++--- xarray/plot/utils.py | 46 +++++++++--- xarray/tests/test_animate.py | 137 +++++++++++++++++++++++++++++++---- 3 files changed, 181 insertions(+), 37 deletions(-) diff --git a/xarray/plot/animate.py b/xarray/plot/animate.py index 400b4c14f35..c81483e02a6 100644 --- a/xarray/plot/animate.py +++ b/xarray/plot/animate.py @@ -43,6 +43,9 @@ def line(darray, animate=None, **kwargs): ax : matplotlib axes object, optional Axis on which to plot this figure. By default, use the current axis. Mutually exclusive with ``size`` and ``figsize``. + hue : string, optional + 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. Only one of these may be specified. @@ -59,6 +62,8 @@ def line(darray, animate=None, **kwargs): yincrease : None, True, or False, optional Should the values on the y axes be increasing from top to bottom? if None, use the default for the matplotlib function. + add_legend : boolean, optional + Add legend with y axis coordinates (3D inputs only). **kwargs : optional Additional arguments to animatplot.blocks.Line @@ -72,16 +77,11 @@ def line(darray, animate=None, **kwargs): if row or col: raise NotImplementedError("Animated FacetGrids not yet implemented") - hue = kwargs.pop('hue', None) - if hue: - raise NotImplementedError("Animating multiple lines at once is not yet" - "implemented") - _check_animate(darray, animate) darray = _transpose_before_animation(darray, animate) - ndims = len(darray[animate].dims) - if ndims > 2: + ndims = len(darray.dims) + if ndims > 3: raise ValueError('Animated line plots are for 2- or 3-dimensional ' 'DataArrays. Passed DataArray has {ndims} ' 'dimensions'.format(ndims=ndims+1)) @@ -103,6 +103,7 @@ def line(darray, animate=None, **kwargs): 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) ax = get_axis(figsize, size, aspect, ax) @@ -117,9 +118,17 @@ def line(darray, animate=None, **kwargs): if ylim is None: ylim = [np.min(yplt_val), np.max(yplt_val)] - # animatplot assumes that the x positions might vary over time too - line_block = Line(xplt_val, yplt_val, ax=ax, t_axis=-1, **kwargs) + # TODO this currently breaks step plots because they have a list of arrays for yplt_val + num_lines = len(hueplt) if hueplt is not None else 1 + # We transposed in _infer_line_data so that animate is last dim and hue is second-last dim + # TODO think of a more robust way of doing this + hueaxis = -2 if hue else 0 + line_blocks = [Line(xplt_val, yplt_val_line.squeeze(), + ax=ax, t_axis=-1, **kwargs) + for yplt_val_line in np.split(yplt_val, num_lines, hueaxis)] + + # TODO if not _labels then no Title block is needed if _labels: if xlabel is not None: ax.set_xlabel(xlabel) @@ -132,12 +141,18 @@ def line(darray, animate=None, **kwargs): for i in range(len(timeline))] title_block = Title(frame_titles, ax=ax) + if ndims == 3 and add_legend: + # TODO ensure the legend stays in the same place throughout the animation + ax.legend(handles=[block.line for block in line_blocks], + labels=list(hueplt.values), + title=huelabel) + _rotate_date_xlabels(xplt_val, ax) _update_axes(ax, xincrease, yincrease, xscale, yscale, xticks, yticks, xlim, ylim) - anim = Animation([line_block, title_block], timeline=timeline) + anim = Animation([*line_blocks, title_block], timeline=timeline) anim.controls(timeline_slider_args={'text': animate, 'valfmt': '%s'}) # Stop subsequent matplotlib plotting calls plotting onto the pause button! diff --git a/xarray/plot/utils.py b/xarray/plot/utils.py index a177c3a2ece..d2600cc0150 100644 --- a/xarray/plot/utils.py +++ b/xarray/plot/utils.py @@ -283,6 +283,8 @@ def _infer_plot_type(darray, row=None, col=None, col_wrap=None, ax=None, if animate is not None: animate_dim = _check_animate(darray, animate) kwargs['animate'] = animate + if col is not None or row is not None: + raise NotImplementedError("Animated FacetGrids not yet supported") else: animate_dim = None @@ -369,17 +371,18 @@ def _infer_line_data(darray, x, y, hue, animate, linestyle): yplt = darray else: - if animate is not None: - raise NotImplementedError - if x is None and y is None and hue is None: raise ValueError('For 2D inputs, please' 'specify either hue, x or y.') if y is None: - xname, huename = _infer_xy_labels(darray=darray, x=x, y=hue) + xname, huename = _infer_xy_labels(darray=darray, x=x, y=hue, + animate=animate) xplt = darray[xname] if xplt.ndim > 1: + if animate is not None: + raise NotImplementedError + if huename in darray.dims: otherindex = 1 if darray.dims.index(huename) == 0 else 0 otherdim = darray.dims[otherindex] @@ -390,9 +393,15 @@ def _infer_line_data(darray, x, y, hue, animate, linestyle): + ' i.e. one of ' + repr(darray.dims)) else: - yplt = darray.transpose(xname, huename) + if animate is not None: + yplt = darray.transpose(xname, huename, animate) + else: + yplt = darray.transpose(xname, huename) else: + if animate is not None: + raise NotImplementedError + yname, huename = _infer_xy_labels(darray=darray, x=y, y=hue) yplt = darray[yname] if yplt.ndim > 1: @@ -485,28 +494,41 @@ def _infer_xy_labels_3d(darray, x, y, rgb): return _infer_xy_labels(darray.isel(**{rgb: 0}), x, y) -def _infer_xy_labels(darray, x, y, imshow=False, rgb=None): +def _infer_xy_labels(darray, x, y, animate=None, imshow=False, rgb=None): """ Determine x and y labels. For use in _plot2d darray must be a 2 dimensional data array, or 3d for imshow only. """ assert x is None or x != y + if animate is not None: + assert animate != x and animate != y + if imshow and darray.ndim == 3: + if animate is not None: + raise NotImplementedError return _infer_xy_labels_3d(darray, x, y, rgb) + # TODO there must be a more pythonic way of doing this + dims = list(darray.dims) + if animate in dims: + dims.remove(animate) + plotdims = tuple(dims) + if x is None and y is None: - if darray.ndim != 2: - raise ValueError('DataArray must be 2d') - y, x = darray.dims + required_ndims = 2 if animate is None else 3 + if darray.ndim != required_ndims: + raise ValueError('DataArray must be {}d'.format(required_ndims)) + y, x = plotdims elif x is None: if y not in darray.dims and y not in darray.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] + x = plotdims[0] if y == plotdims[1] else plotdims[1] elif y is None: if x not in darray.dims and x not in darray.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] + raise ValueError( + 'x must be a dimension name if y is not supplied') + y = plotdims[0] if x == plotdims[1] else plotdims[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') return x, y diff --git a/xarray/tests/test_animate.py b/xarray/tests/test_animate.py index 2a8e11b5fd7..fa75ac5bfad 100644 --- a/xarray/tests/test_animate.py +++ b/xarray/tests/test_animate.py @@ -62,23 +62,53 @@ def test_datetimeline(self): assert str(timeline.t[0]) == '2000-01-01 00:00:00' +@pytest.fixture +def linedata(): + dat1 = np.array([[0.0, 1.1, 0.0, 2], + [0.1, 1.3, 0.2, 2.1], + [0.1, 1.4, 0.3, 2.2], + [0.2, 1.3, 0.2, 2.3], + [0.1, 1.2, 0.2, 2.2]]) + dat2 = np.array([[0.0, 1.1, 0.0, 2], + [0.1, 1.3, 0.2, 2.1], + [0.1, 1.4, 0.3, 2.2], + [0.2, 1.3, 0.2, 2.3], + [0.1, 1.2, 0.2, 2.2]]) + das = [] + for data in [dat1, dat2]: + coords = {'time': 10 * np.arange(data.shape[0]), + 'position': 0.1 * np.arange(data.shape[1])} + da = DataArray(data, name='height', coords=coords, + dims=('time', 'position'), attrs={'units': 'm'}) + da.time.attrs['units'] = 's' + da.position.attrs['units'] = 'cm' + + das.append(da) + + player = DataArray(name='player', data=['Tom', 'Bhavin'], dims='player') + return xr.concat(das, dim=player) + + @requires_animatplot class TestAnimateLine(PlotTestCase): @pytest.fixture(autouse=True) - def setUp(self): - d = np.array([[0.0, 1.1, 0.0, 2], - [0.1, 1.3, 0.2, 2.1], - [0.1, 1.4, 0.3, 2.2], - [0.2, 1.3, 0.2, 2.3], - [0.1, 1.2, 0.2, 2.2]]) - coords = {'time': 10 * np.arange(d.shape[0]), - 'position': 0.1 * np.arange(d.shape[1])} - self.darray = DataArray(d, name='height', - coords=coords, - dims=('time', 'position'), - attrs={'units': 'm'}) - self.darray.time.attrs['units'] = 's' - self.darray.position.attrs['units'] = 'cm' + def setUp(self, linedata): + self.darray = linedata.sel(player='Tom') + + def test_2d_animated_line_accepts_x_kw(self): + self.darray.plot.line(x='position', animate='time') + assert plt.gca().get_xlabel() == 'position [cm]' + plt.cla() + self.darray.plot.line(x='time', animate='position') + assert plt.gca().get_xlabel() == 'time [s]' + + @pytest.mark.skip + def test_2d_animated_line_accepts_y_kw(self): + self.darray.plot.line(y='position', animate='time') + assert plt.gca().get_ylabel() == 'position [cm]' + plt.cla() + self.darray.plot.line(y='time', animate='position') + assert plt.gca().get_ylabel() == 'time [s]' def test_animate_single_line_classes(self): anim = self.darray.plot(animate='time') @@ -103,10 +133,15 @@ def test_animate_single_line_text(self): anim = self.darray.plot(animate='time') line_block, title_block = anim.blocks - assert title_block.titles[0] == 'time = 0' + assert title_block.titles[0] == 'time = 0, player = Tom' assert line_block.ax.get_xlabel() == 'position [cm]' assert anim.timeline.units == ' [s]' + # TODO test that omitting title block is handled gracefully + @pytest.mark.skip + def test_no_labels(self): + ... + def test_can_pass_in_axis(self): self.pass_in_axis(partial(self.darray.plot, animate='time')) @@ -128,6 +163,8 @@ def test_animate_as_argument(self): assert isinstance(anim, amp.animation.Animation) +@pytest.mark.xfail(reason="np.splitting the y data doesn't work for step plots" + "because they have lists of arrays for some reason") @requires_animatplot class TestAnimateStep(PlotTestCase): @pytest.fixture(autouse=True) @@ -142,3 +179,73 @@ def test_coord_with_interval_step(self): anim = da.plot.step(animate='new_dim') assert len(plt.gca().lines[0].get_xdata()) == ((len(bins) - 1) * 2) npt.assert_equal(anim.timeline.t, np.array([0, 1, 2])) + + +@requires_animatplot +class TestAnimateMultipleLines(PlotTestCase): + @pytest.fixture(autouse=True) + def setUp(self, linedata): + self.darray = linedata + + def test_2d_animated_line_accepts_hue_kw(self): + da = self.darray + print(da) + da.plot.line(hue='player', animate='time') + assert (plt.gca().get_legend().get_title().get_text() + == 'player') + plt.cla() + self.darray.plot.line(hue='time', animate='player') + assert (plt.gca().get_legend().get_title().get_text() + == 'time [s]') + + def test_animate_multiple_lines_classes(self): + anim = self.darray.plot(animate='time', hue='player') + assert isinstance(anim, amp.animation.Animation) + + line_block1, line_block2, title_block = anim.blocks + + assert isinstance(line_block1, amp.blocks.Line) + assert isinstance(line_block2, amp.blocks.Line) + assert isinstance(title_block, amp.blocks.Title) + + def test_animate_multiple_lines_data(self): + anim = self.darray.plot(animate='time', hue='player') + line_block1, _, title_block = anim.blocks + + assert len(line_block1) == 5 + assert len(line_block1) == len(title_block) + + expected = self.darray.isel(player=0).transpose('position', 'time') + npt.assert_equal(line_block1.y, expected.values) + npt.assert_equal(line_block1.x[:, 0], + self.darray.coords['position'].values) + + def test_animate_multiple_lines_text(self): + anim = self.darray.plot(animate='time', hue='player') + line_block1, _, title_block = anim.blocks + + assert title_block.titles[0] == 'time = 0' + assert line_block1.ax.get_xlabel() == 'position [cm]' + assert anim.timeline.units == ' [s]' + + # TODO check legend is correct + + def test_can_pass_in_axis(self): + self.pass_in_axis(partial(self.darray.plot, + animate='time', hue='player')) + + def test_animate_multiple_line_axes(self): + line_block1, line_block2, _ = self.darray.plot(animate='time', + hue='player').blocks + assert line_block1.ax is line_block2.ax + + # Check current axes is the plot (not the timeline etc.) + assert plt.gca() is line_block1.ax + + +class TestAnimatedFacetGrid: + def test_faceting_not_implemented(self): + da = DataArray(easy_array(2, 3, 4)) + + with pytest.raises(NotImplementedError): + da.plot(animate='dim_0', col='dim_1') From a689dd7208dce2fb1c62ad7f534c9426f2f7e718 Mon Sep 17 00:00:00 2001 From: Thomas Nicholas Date: Thu, 11 Apr 2019 10:45:01 +0100 Subject: [PATCH 29/30] Fixed repetition in what's new caused by merging master --- doc/whats-new.rst | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/doc/whats-new.rst b/doc/whats-new.rst index 2599809cca7..0994654bd25 100644 --- a/doc/whats-new.rst +++ b/doc/whats-new.rst @@ -144,11 +144,9 @@ Bug fixes from higher frequencies to lower frequencies. Datapoints outside the bounds of the original time coordinate are now filled with NaN (:issue:`2197`). By `Spencer Clark `_. -- Line plots with the `x` argument set to a coord now plot the correct data. -- Line plots with the `x` argument set to a non-dimensional coord now plot the correct data for 1D DataArrays. - (:issue:`27251). By `Tom Nicholas `_. -- Line plots with the ``x`` argument set to a non-dimensional coord now plot the correct data for 1D DataArrays. - (:issue:`27251`). By `Tom Nicholas `_. +- Line plots with the ``x`` argument set to a non-dimensional coord now plot the + correct data for 1D DataArrays (:issue:`27251`). + By `Tom Nicholas `_. - Subtracting a scalar ``cftime.datetime`` object from a :py:class:`CFTimeIndex` now results in a :py:class:`pandas.TimedeltaIndex` instead of raising a ``TypeError`` (:issue:`2671`). By `Spencer Clark From 2e3b5059031c6f0577bc30d4ee9e2e877f69f0e3 Mon Sep 17 00:00:00 2001 From: Thomas Nicholas Date: Tue, 30 Apr 2019 17:04:15 +0100 Subject: [PATCH 30/30] Fixed linting --- xarray/plot/animate.py | 10 ++++++---- xarray/plot/utils.py | 2 +- xarray/tests/test_animate.py | 18 +++++++++--------- 3 files changed, 16 insertions(+), 14 deletions(-) diff --git a/xarray/plot/animate.py b/xarray/plot/animate.py index c81483e02a6..fb26ddeb2ce 100644 --- a/xarray/plot/animate.py +++ b/xarray/plot/animate.py @@ -84,7 +84,7 @@ def line(darray, animate=None, **kwargs): if ndims > 3: raise ValueError('Animated line plots are for 2- or 3-dimensional ' 'DataArrays. Passed DataArray has {ndims} ' - 'dimensions'.format(ndims=ndims+1)) + 'dimensions'.format(ndims=ndims + 1)) # Ensures consistency with .plot method figsize = kwargs.pop('figsize', None) @@ -118,9 +118,11 @@ def line(darray, animate=None, **kwargs): if ylim is None: ylim = [np.min(yplt_val), np.max(yplt_val)] - # TODO this currently breaks step plots because they have a list of arrays for yplt_val + # TODO this currently breaks step plots because they have a list of arrays + # for yplt_val num_lines = len(hueplt) if hueplt is not None else 1 - # We transposed in _infer_line_data so that animate is last dim and hue is second-last dim + # We transposed in _infer_line_data so that animate is last dim and hue is + # second-last dim # TODO think of a more robust way of doing this hueaxis = -2 if hue else 0 @@ -142,7 +144,7 @@ def line(darray, animate=None, **kwargs): title_block = Title(frame_titles, ax=ax) if ndims == 3 and add_legend: - # TODO ensure the legend stays in the same place throughout the animation + # TODO ensure the legend stays in the same place throughout animation ax.legend(handles=[block.line for block in line_blocks], labels=list(hueplt.values), title=huelabel) diff --git a/xarray/plot/utils.py b/xarray/plot/utils.py index d2600cc0150..a319dd90afd 100644 --- a/xarray/plot/utils.py +++ b/xarray/plot/utils.py @@ -431,7 +431,7 @@ def _infer_line_data(darray, x, y, hue, animate, linestyle): linestyle = (linestyle.replace('steps-pre', '') .replace('steps-post', '') .replace('steps-mid', '')) - #if kwargs['linestyle'] == '': + # if kwargs['linestyle'] == '': # kwargs.pop('linestyle') else: xplt_val = _interval_to_mid_points(xplt.values) diff --git a/xarray/tests/test_animate.py b/xarray/tests/test_animate.py index fa75ac5bfad..b7ba72ee76b 100644 --- a/xarray/tests/test_animate.py +++ b/xarray/tests/test_animate.py @@ -65,15 +65,15 @@ def test_datetimeline(self): @pytest.fixture def linedata(): dat1 = np.array([[0.0, 1.1, 0.0, 2], - [0.1, 1.3, 0.2, 2.1], - [0.1, 1.4, 0.3, 2.2], - [0.2, 1.3, 0.2, 2.3], - [0.1, 1.2, 0.2, 2.2]]) + [0.1, 1.3, 0.2, 2.1], + [0.1, 1.4, 0.3, 2.2], + [0.2, 1.3, 0.2, 2.3], + [0.1, 1.2, 0.2, 2.2]]) dat2 = np.array([[0.0, 1.1, 0.0, 2], - [0.1, 1.3, 0.2, 2.1], - [0.1, 1.4, 0.3, 2.2], - [0.2, 1.3, 0.2, 2.3], - [0.1, 1.2, 0.2, 2.2]]) + [0.1, 1.3, 0.2, 2.1], + [0.1, 1.4, 0.3, 2.2], + [0.2, 1.3, 0.2, 2.3], + [0.1, 1.2, 0.2, 2.2]]) das = [] for data in [dat1, dat2]: coords = {'time': 10 * np.arange(data.shape[0]), @@ -174,7 +174,7 @@ def setUp(self): def test_coord_with_interval_step(self): bins = [-1, 0, 1, 2] da = self.darray.groupby_bins('dim_0', bins).mean(xr.ALL_DIMS) - da = xr.concat([da, da*2, da*1.7], dim='new_dim') + da = xr.concat([da, da * 2, da * 1.7], dim='new_dim') anim = da.plot.step(animate='new_dim') assert len(plt.gca().lines[0].get_xdata()) == ((len(bins) - 1) * 2)