From b87781b111f67a77e5335166e1946007a764ec5f Mon Sep 17 00:00:00 2001 From: Philipp Sommer Date: Fri, 15 Oct 2021 15:25:24 +0200 Subject: [PATCH 1/8] check dimensions prior to update --- psyplot/data.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/psyplot/data.py b/psyplot/data.py index ca41979..9dab783 100755 --- a/psyplot/data.py +++ b/psyplot/data.py @@ -145,7 +145,7 @@ def _fix_times(dims): # https://github.com/pydata/xarray/issues/4283 for key, val in dims.items(): if np.issubdtype(np.asarray(val).dtype, np.datetime64): - dims[key] = to_datetime(val) + dims[key] = to_datetime([val])[0] @docstrings.get_sections(base='setup_coords') @@ -2661,7 +2661,7 @@ def filter_attrs(item): else: self._idims = None for key, val in six.iteritems(self.arr.coords): - if key != 'variable': + if key in base_var.dims and key != 'variable': dims.setdefault(key, val) kws = dims.copy() # the sel method does not work with slice objects @@ -2724,7 +2724,8 @@ def filter_attrs(item): self._idims = None old_dims = self.arr.dims[:] for key, val in six.iteritems(self.arr.coords): - dims.setdefault(key, val) + if key in base_var.dims: + dims.setdefault(key, val) kws = dims.copy() # the sel method does not work with slice objects if not any(isinstance(idx, slice) for idx in dims.values()): From ad8c8d351d9baa18bee4f6469d7ce428cc4826b2 Mon Sep 17 00:00:00 2001 From: Philipp Sommer Date: Fri, 15 Oct 2021 15:42:37 +0200 Subject: [PATCH 2/8] minor fix for ci --- .appveyor.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.appveyor.yml b/.appveyor.yml index 84b744b..13649da 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -24,7 +24,7 @@ install: - cmd: endlocal - cmd: 'SET PYTHONWARNINGS=ignore:mode:DeprecationWarning:docutils.io:245' - cmd: "IF NOT DEFINED APPVEYOR_REPO_TAG_NAME (SET GIT_BRANCH=%APPVEYOR_REPO_BRANCH%)" - - cmd: "IF NOT DEFINED APPVEYOR_REPO_TAG_NAME (conda config --add channels psyplot/label/%APPVEYOR_REPO_BRANCH%)" + - cmd: "IF NOT DEFINED APPVEYOR_REPO_TAG_NAME (conda config --add channels psyplot/label/%APPVEYOR_REPO_BRANCH:/=-%)" build: off @@ -37,7 +37,7 @@ test_script: deploy_script: - cmd: " IF NOT DEFINED APPVEYOR_REPO_TAG_NAME ( - deploy-conda-recipe -l %APPVEYOR_REPO_BRANCH% -py %PYTHON_VERSION% ci/conda-recipe + deploy-conda-recipe -l %APPVEYOR_REPO_BRANCH:/=-% -py %PYTHON_VERSION% ci/conda-recipe ) ELSE ( deploy-conda-recipe -py %PYTHON_VERSION% ci/conda-recipe )" From df9cb4ec6d85943a62e46c694009ecf8e81568f0 Mon Sep 17 00:00:00 2001 From: Philipp Sommer Date: Fri, 15 Oct 2021 22:03:42 +0200 Subject: [PATCH 3/8] small fix for variable name --- psyplot/data.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/psyplot/data.py b/psyplot/data.py index 9dab783..38d0af5 100755 --- a/psyplot/data.py +++ b/psyplot/data.py @@ -2650,10 +2650,11 @@ def filter_attrs(item): name = dims.pop('name') else: name = list(self.arr.coords['variable'].values) + base_dims = self.base[name].dims if method == 'isel': self.idims.update(dims) dims = self.idims - for dim in set(self.base[name].dims) - set(dims): + for dim in set(base_dims) - set(dims): dims[dim] = slice(None) for dim in set(dims) - set(self.base[name].dims): del dims[dim] @@ -2661,7 +2662,7 @@ def filter_attrs(item): else: self._idims = None for key, val in six.iteritems(self.arr.coords): - if key in base_var.dims and key != 'variable': + if key in base_dims and key != 'variable': dims.setdefault(key, val) kws = dims.copy() # the sel method does not work with slice objects From 682782c151098ffbe624fc62bf6c9b6f9184376e Mon Sep 17 00:00:00 2001 From: Philipp Sommer Date: Mon, 18 Oct 2021 09:44:20 +0200 Subject: [PATCH 4/8] first select the data, then transform to dataset --- psyplot/data.py | 45 +++++++++++++++++++++++++++------------------ 1 file changed, 27 insertions(+), 18 deletions(-) diff --git a/psyplot/data.py b/psyplot/data.py index 38d0af5..c406f91 100755 --- a/psyplot/data.py +++ b/psyplot/data.py @@ -3666,7 +3666,8 @@ def from_dataset(cls, base, method='isel', default_slice=None, default_slice: indexer Index (e.g. 0 if `method` is 'isel') that shall be used for dimensions not covered by `dims` and `furtherdims`. If None, the - whole slice will be used. + whole slice will be used. Note that the `default_slice` is always + based on the `isel` method. decoder: CFDecoder or dict Arguments for the decoder. This can be one of @@ -3773,19 +3774,22 @@ def sel_method(key, dims, name=None): elif (isinstance(name, six.string_types) or not utils.is_iterable(name)): arr = base[name] + decoder = get_decoder(arr) + dims = decoder.correct_dims(arr, dims) else: arr = base[list(name)] - add_missing_dimensions(arr) - if not isinstance(arr, xr.DataArray): - arr = ds2arr(arr) + decoder = get_decoder(base[name[0]]) + dims = decoder.correct_dims(base[name[0]], dims) def_slice = slice(None) if default_slice is None else \ default_slice - decoder = get_decoder(arr) - dims = decoder.correct_dims(arr, dims) dims.update({ dim: def_slice for dim in set(arr.dims).difference( dims) if dim != 'variable'}) - ret = squeeze_array(arr.isel(**dims)) + add_missing_dimensions(arr) + ret = arr.isel(**dims) + if not isinstance(ret, xr.DataArray): + ret = ds2arr(ret) + ret = squeeze_array(ret) # delete the variable dimension for the idims dims.pop('variable', None) ret.psy.init_accessor(arr_name=key, base=base, idims=dims, @@ -3798,28 +3802,33 @@ def sel_method(key, dims, name=None): elif (isinstance(name, six.string_types) or not utils.is_iterable(name)): arr = base[name] + decoder = get_decoder(arr) + dims = decoder.correct_dims(arr, dims) else: arr = base[list(name)] - add_missing_dimensions(arr) - if not isinstance(arr, xr.DataArray): - arr = ds2arr(arr) - # idims will be calculated by the array (maybe not the most - # efficient way...) - decoder = get_decoder(arr) - dims = decoder.correct_dims(arr, dims) + decoder = get_decoder(base[name[0]]) + dims = decoder.correct_dims(base[name[0]], dims) if default_slice is not None: dims.update({ - key: default_slice for key in set(arr.dims).difference( - dims) if key != 'variable'}) + dim: default_slice for dim in set(arr.dims).difference( + dims) if dim != 'variable'}) kws = dims.copy() + kws['method'] = method # the sel method does not work with slice objects - if not any(isinstance(idx, slice) for idx in dims.values()): - kws['method'] = method + for dim, val in dims.items(): + if isinstance(val, slice): + if val == slice(None): + kws.pop(dim) # the full slice is the default + else: + kws.pop("method", None) + add_missing_dimensions(arr) try: ret = arr.sel(**kws) except KeyError: _fix_times(kws) ret = arr.sel(**kws) + if not isinstance(ret, xr.DataArray): + ret = ds2arr(ret) ret = squeeze_array(ret) ret.psy.init_accessor(arr_name=key, base=base, decoder=decoder) return maybe_load(ret) From 3dc5a61fc9f67f70f179d6bc6565e9196d050ada Mon Sep 17 00:00:00 2001 From: Philipp Sommer Date: Mon, 18 Oct 2021 09:45:12 +0200 Subject: [PATCH 5/8] always use isel for default_slice in from_dataset --- psyplot/data.py | 13 ++++++++++--- tests/test_data.py | 12 ++++++++++++ 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/psyplot/data.py b/psyplot/data.py index c406f91..65d2478 100755 --- a/psyplot/data.py +++ b/psyplot/data.py @@ -3809,9 +3809,16 @@ def sel_method(key, dims, name=None): decoder = get_decoder(base[name[0]]) dims = decoder.correct_dims(base[name[0]], dims) if default_slice is not None: - dims.update({ - dim: default_slice for dim in set(arr.dims).difference( - dims) if dim != 'variable'}) + if isinstance(default_slice, slice): + dims.update({ + dim: default_slice + for dim in set(arr.dims).difference(dims) + if dim != 'variable'}) + else: + dims.update({ + dim: arr.coords[dim][default_slice] + for dim in set(arr.dims).difference(dims) + if dim != 'variable'}) kws = dims.copy() kws['method'] = method # the sel method does not work with slice objects diff --git a/tests/test_data.py b/tests/test_data.py index 6170a79..e3aca3f 100755 --- a/tests/test_data.py +++ b/tests/test_data.py @@ -1335,6 +1335,18 @@ def test_from_dataset_15_decoder_kws(self): decoder=dict(x={'myx'})) self.assertEqual(l[0].psy.decoder.x, {'myx'}) + def test_from_dataset_16_default_slice(self): + """Test selection with default_slice=0""" + variables, coords = self._from_dataset_test_variables + ds = xr.Dataset(variables, coords) + l = self.list_class.from_dataset(ds, ydim=2, default_slice=0, method=None, + name=['v0', 'v2']) + self.assertEqual(len(l), 2) + self.assertEqual(set(l.names), {'v0', 'v2'}) + for arr in l: + self.assertEqual(arr.ydim, 2, + msg="Wrong ydim slice for " + arr.name) + def test_array_info(self): variables, coords = self._from_dataset_test_variables From 68750654314bbf068d68634728464f0cc71d2718 Mon Sep 17 00:00:00 2001 From: Philipp Sommer Date: Thu, 21 Oct 2021 16:17:06 +0200 Subject: [PATCH 6/8] add convert_coordinate method for Plotter and Formatoption --- psyplot/plotter.py | 57 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/psyplot/plotter.py b/psyplot/plotter.py index 9884388..620c6de 100755 --- a/psyplot/plotter.py +++ b/psyplot/plotter.py @@ -752,6 +752,42 @@ def remove(self): are removed by the usual :meth:`matplotlib.axes.Axes.clear` method.""" pass + @docstrings.get_extended_summary(base="Formatoption.convert_coordinate") + @docstrings.get_sections( + base="Formatoption.convert_coordinate", + sections=["Parameters", "Returns"] + ) + def convert_coordinate(self, coord, *variables): + """Convert a coordinate to units necessary for the plot. + + This method takes a single coordinate variable (e.g. the `bounds` of a + coordinate, or the coordinate itself) and transforms the units that the + plotter requires. + + One might also provide additional `variables` that are supposed to be + on the same unit, in case the given `coord` does not specify a `units` + attribute. `coord` might be a CF-conform `bounds` variable, and one of + the variables might be the corresponding `coordinate`. + + Parameters + ---------- + coord: xr.Variable + The variable to transform + ``*variables`` + The variables that are on the same unit as `coord` + + Returns + ------- + xr.Variable + The transformed `coord` + + Notes + ----- + By default, this method uses the :meth:`~Plotter.convert_coordinate` + method of the :attr:`plotter`. + """ + return self.plotter.convert_coordinate(coord, *variables) + class DictFormatoption(Formatoption): """ @@ -2434,3 +2470,24 @@ def _get_sample_projection(cls): """Returns None. May be subclassed to return a projection that can be used when creating a subplot""" pass + + @docstrings.dedent + def convert_coordinate(self, coord, *variables): + """Convert a coordinate to units necessary for the plot. + + %(Formatoption.convert_coordinate.summary_ext)s + + Parameters + ---------- + %(Formatoption.convert_coordinate.parameters)s + + Returns + ------- + %(Formatoption.convert_coordinate.returns)s + + Notes + ----- + This method is supposed to be implemented by subclasses. The default + implementation by the :class:`Plotter` class does nothing. + """ + return coord From 68328c32ae6323242d694ee5c6c61a161de63af9 Mon Sep 17 00:00:00 2001 From: Philipp Sommer Date: Fri, 22 Oct 2021 22:14:35 +0200 Subject: [PATCH 7/8] use psyplot-ci-orb@1.5.27 --- .circleci/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 60cbec7..7a6b8d4 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -1,7 +1,7 @@ version: 2.1 orbs: - psyplot: psyplot/psyplot-ci-orb@1.5.24 + psyplot: psyplot/psyplot-ci-orb@1.5.27 mattermost-plugin-notify: nathanaelhoun/mattermost-plugin-notify@1.2.0 executors: From 87a948cf82b3ddf5da436095b5fb49c170b54678 Mon Sep 17 00:00:00 2001 From: Philipp Sommer Date: Sun, 21 Nov 2021 23:20:00 +0100 Subject: [PATCH 8/8] [skip ci] Update Changelog --- CHANGELOG.rst | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index a941efb..bc85dc8 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,3 +1,28 @@ +v1.4.1 +====== +Compatibility fixes and minor improvements + +Added +----- +- An abstract ``convert_coordinate`` method has been implemented for the + ``Plotter`` and ``Formatoption`` class that can be used in subclasses to + convert coordinates for the required visualization. The default + implementation does nothing (see + `#39 `__) + +Fixed +----- +- the update method now only takes the coordinates that are dimensions in the + dataset see `#39 `__ + +Changed +------- +- loading more than one variables into a ``DataArray`` now first selects the + corresponding dimensions, then puts it into a single ``DataArray``. This + avoids loading the entire data (see + `#39 `__) + + v1.4.0 ====== Compatibility fixes and LGPL license