From 31019d8e045641774881c44dc559127849c9ffe1 Mon Sep 17 00:00:00 2001 From: Yohai Bar Sinai <6164157+yohai@users.noreply.github.com> Date: Wed, 11 Jul 2018 09:24:05 -0400 Subject: [PATCH 001/101] initial commit --- xarray/core/dataset.py | 8 ++++ xarray/plot/facetgrid.py | 19 +++++--- xarray/plot/plot.py | 95 +++++++++++++++++++++++++++++++++++++--- 3 files changed, 109 insertions(+), 13 deletions(-) diff --git a/xarray/core/dataset.py b/xarray/core/dataset.py index 8e039572237..e89faa9601c 100644 --- a/xarray/core/dataset.py +++ b/xarray/core/dataset.py @@ -33,6 +33,7 @@ Frozen, SortedKeysDict, either_dict_or_kwargs, decode_numpy_dict_values, ensure_us_time_resolution, hashable, maybe_wrap_array) from .variable import IndexVariable, Variable, as_variable, broadcast_variables +from ..plot.plot import dataset_scatter # list of attributes of pd.DatetimeIndex that are ndarrays of time info _DATETIMEINDEX_COMPONENTS = ['year', 'month', 'day', 'hour', 'minute', @@ -3592,6 +3593,13 @@ def real(self): def imag(self): return self._unary_op(lambda x: x.imag, keep_attrs=True)(self) + def scatter(self, x=None, y=None, hue=None, col=None, row=None, + col_wrap=None, sharex=True, sharey=True, aspect=None, + size=None, subplot_kws=None, add_legend=True, **kwargs): + lc = locals() + ds = lc.pop('self') + return dataset_scatter(ds=ds, **lc) + def filter_by_attrs(self, **kwargs): """Returns a ``Dataset`` with variables that match specific conditions. diff --git a/xarray/plot/facetgrid.py b/xarray/plot/facetgrid.py index a0d7c4dd5e2..b6b716d2740 100644 --- a/xarray/plot/facetgrid.py +++ b/xarray/plot/facetgrid.py @@ -266,7 +266,7 @@ def map_dataarray(self, func, x, y, **kwargs): return self - def map_dataarray_line(self, x=None, y=None, hue=None, **kwargs): + def map_dataarray_line(self, plotfunc, x=None, y=None, hue=None, **kwargs): """ Apply a line plot to a 2d facet subset of the data. @@ -280,7 +280,8 @@ def map_dataarray_line(self, x=None, y=None, hue=None, **kwargs): self : FacetGrid object """ - from .plot import line, _infer_line_data + from .plot import (_infer_line_data, _infer_scatter_data, + line, dataset_scatter) add_legend = kwargs.pop('add_legend', True) kwargs['add_legend'] = False @@ -289,13 +290,19 @@ def map_dataarray_line(self, x=None, y=None, hue=None, **kwargs): # None is the sentinel value if d is not None: subset = self.data.loc[d] - mappable = line(subset, x=x, y=y, hue=hue, + mappable = plotfunc(subset, x=x, y=y, hue=hue, ax=ax, _labels=False, **kwargs) self._mappables.append(mappable) - _, _, hueplt, xlabel, ylabel, huelabel = _infer_line_data( - darray=self.data.loc[self.name_dicts.flat[0]], - x=x, y=y, hue=hue) + + if plotfunc == line: + _, _, hueplt, xlabel, ylabel, huelabel = _infer_line_data( + darray=self.data.loc[self.name_dicts.flat[0]], + x=x, y=y, hue=hue) + elif plotfunc == dataset_scatter: + _, _, hueplt, xlabel, ylabel, huelabel = _infer_scatter_data( + ds=self.data.loc[self.name_dicts.flat[0]], + x=x, y=y, hue=hue) self._hue_var = hueplt self._hue_label = huelabel diff --git a/xarray/plot/plot.py b/xarray/plot/plot.py index 2a7fb08efda..3d5f356246b 100644 --- a/xarray/plot/plot.py +++ b/xarray/plot/plot.py @@ -104,7 +104,7 @@ def _line_facetgrid(darray, row=None, col=None, hue=None, g = FacetGrid(data=darray, col=col, row=row, col_wrap=col_wrap, sharex=sharex, sharey=sharey, figsize=figsize, aspect=aspect, size=size, subplot_kws=subplot_kws) - return g.map_dataarray_line(hue=hue, **kwargs) + return g.map_dataarray_line(line, hue=hue, **kwargs) def plot(darray, row=None, col=None, col_wrap=None, ax=None, hue=None, @@ -240,6 +240,47 @@ def _infer_line_data(darray, x, y, hue): return xplt, yplt, hueplt, xlabel, ylabel, huelabel +def _infer_scatter_data(ds, x, y, hue): + dvars = set(ds.data_vars.keys()) + error_msg = (' must be either one of ({0:s})' + .format(', '.join(dvars))) + + if x not in dvars: + raise ValueError(x + error_msg) + + if y not in dvars: + raise ValueError(y + error_msg) + + dims = ds[x].dims + if ds[y].dims != dims: + raise ValueError('{} and {} must have the same dimensions.' + ''.format(x, y)) + + if hue is not None and hue not in dims: + raise ValueError(hue + 'must be either one of ({0:s})' + ''.format(', '.join(dims))) + + dims = set(dims) + if hue: + dims.remove(hue) + xplt = ds[x].stack(stackdim=dims).transpose('stackdim', hue).values + yplt = ds[y].stack(stackdim=dims).transpose('stackdim', hue).values + else: + xplt = ds[x].values.flatten() + yplt = ds[y].values.flatten() + + if hue: + hueplt = ds[x].coords[hue] + huelabel = label_from_attrs(ds[x][hue]) + else: + hueplt = None + huelabel = None + + xlabel = label_from_attrs(ds[x]) + ylabel = label_from_attrs(ds[y]) + return xplt, yplt, hueplt, xlabel, ylabel, huelabel + + # This function signature should not change so that it can use # matplotlib format strings def line(darray, *args, **kwargs): @@ -289,6 +330,7 @@ def line(darray, *args, **kwargs): if row or col: allargs = locals().copy() allargs.update(allargs.pop('kwargs')) + allargs.update(allargs.pop('args')) return _line_facetgrid(**allargs) ndims = len(darray.dims) @@ -309,8 +351,6 @@ def line(darray, *args, **kwargs): yincrease = kwargs.pop('yincrease', True) add_legend = kwargs.pop('add_legend', True) _labels = kwargs.pop('_labels', True) - if args is (): - args = kwargs.pop('args', ()) ax = get_axis(figsize, size, aspect, ax) xplt, yplt, hueplt, xlabel, ylabel, huelabel = \ @@ -873,7 +913,7 @@ def _is_monotonic(coord, axis=0): return np.all(delta_pos) or np.all(delta_neg) -def _infer_interval_breaks(coord, axis=0, check_monotonic=False): +def _infer_interval_breaks(coord, axis=0): """ >>> _infer_interval_breaks(np.arange(5)) array([-0.5, 0.5, 1.5, 2.5, 3.5, 4.5]) @@ -883,7 +923,7 @@ def _infer_interval_breaks(coord, axis=0, check_monotonic=False): """ coord = np.asarray(coord) - if check_monotonic and not _is_monotonic(coord, axis=axis): + if not _is_monotonic(coord, axis=axis): raise ValueError("The input coordinate is not sorted in increasing " "order along axis %d. This can lead to unexpected " "results. Consider calling the `sortby` method on " @@ -922,8 +962,8 @@ def pcolormesh(x, y, z, ax, infer_intervals=None, **kwargs): if infer_intervals: if len(x.shape) == 1: - x = _infer_interval_breaks(x, check_monotonic=True) - y = _infer_interval_breaks(y, check_monotonic=True) + x = _infer_interval_breaks(x) + y = _infer_interval_breaks(y) else: # we have to infer the intervals on both axes x = _infer_interval_breaks(x, axis=1) @@ -941,3 +981,44 @@ def pcolormesh(x, y, z, ax, infer_intervals=None, **kwargs): ax.set_ylim(y[0], y[-1]) return primitive + + +def dataset_scatter(ds, x=None, y=None, hue=None, col=None, row=None, + col_wrap=None, sharex=True, sharey=True, aspect=None, + size=None, subplot_kws=None, add_legend=True, **kwargs): + if col or row: + ax = kwargs.pop('ax', None) + figsize = kwargs.pop('figsize', None) + if ax is not None: + raise ValueError("Can't use axes when making faceted plots.") + if aspect is None: + aspect = 1 + if size is None: + size = 3 + elif figsize is not None: + raise ValueError('cannot provide both `figsize` and `size` arguments') + + g = FacetGrid(data=ds, col=col, row=row, col_wrap=col_wrap, + sharex=sharex, sharey=sharey, figsize=figsize, + aspect=aspect, size=size, subplot_kws=subplot_kws) + return g.map_dataarray_line(x=x, y=y, hue=hue, plotfunc=dataset_scatter, **kwargs) + + xplt, yplt, hueplt, xlabel, ylabel, huelabel = _infer_scatter_data(ds, x, y, hue) + + figsize = kwargs.pop('figsize', None) + ax = kwargs.pop('ax', None) + ax = get_axis(figsize, size, aspect, ax) + primitive = ax.plot(xplt, yplt, '.') + + if kwargs.get('_labels', True): + if xlabel is not None: + ax.set_xlabel(xlabel) + + if ylabel is not None: + ax.set_ylabel(ylabel) + + if add_legend and huelabel is not None: + ax.legend(handles=primitive, + labels=list(hueplt.values), + title=huelabel) + return primitive From 5b3714c723da7f190d3cfc719f6d7bad2873b2b7 Mon Sep 17 00:00:00 2001 From: Yohai Bar Sinai <6164157+yohai@users.noreply.github.com> Date: Wed, 11 Jul 2018 09:36:44 -0400 Subject: [PATCH 002/101] formatting --- xarray/plot/facetgrid.py | 2 +- xarray/plot/plot.py | 19 +++++++++++-------- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/xarray/plot/facetgrid.py b/xarray/plot/facetgrid.py index b6b716d2740..bb053e1f753 100644 --- a/xarray/plot/facetgrid.py +++ b/xarray/plot/facetgrid.py @@ -281,7 +281,7 @@ def map_dataarray_line(self, plotfunc, x=None, y=None, hue=None, **kwargs): """ from .plot import (_infer_line_data, _infer_scatter_data, - line, dataset_scatter) + line, dataset_scatter) add_legend = kwargs.pop('add_legend', True) kwargs['add_legend'] = False diff --git a/xarray/plot/plot.py b/xarray/plot/plot.py index 3d5f356246b..b908f63cb3e 100644 --- a/xarray/plot/plot.py +++ b/xarray/plot/plot.py @@ -913,7 +913,7 @@ def _is_monotonic(coord, axis=0): return np.all(delta_pos) or np.all(delta_neg) -def _infer_interval_breaks(coord, axis=0): +def _infer_interval_breaks(coord, axis=0, check_monotonic=False): """ >>> _infer_interval_breaks(np.arange(5)) array([-0.5, 0.5, 1.5, 2.5, 3.5, 4.5]) @@ -923,7 +923,7 @@ def _infer_interval_breaks(coord, axis=0): """ coord = np.asarray(coord) - if not _is_monotonic(coord, axis=axis): + if check_monotonic and not _is_monotonic(coord, axis=axis): raise ValueError("The input coordinate is not sorted in increasing " "order along axis %d. This can lead to unexpected " "results. Consider calling the `sortby` method on " @@ -962,8 +962,8 @@ def pcolormesh(x, y, z, ax, infer_intervals=None, **kwargs): if infer_intervals: if len(x.shape) == 1: - x = _infer_interval_breaks(x) - y = _infer_interval_breaks(y) + x = _infer_interval_breaks(x, check_monotonic=True) + y = _infer_interval_breaks(y, check_monotonic=True) else: # we have to infer the intervals on both axes x = _infer_interval_breaks(x, axis=1) @@ -985,7 +985,7 @@ def pcolormesh(x, y, z, ax, infer_intervals=None, **kwargs): def dataset_scatter(ds, x=None, y=None, hue=None, col=None, row=None, col_wrap=None, sharex=True, sharey=True, aspect=None, - size=None, subplot_kws=None, add_legend=True, **kwargs): + size=None, subplot_kws=None, add_legend=True, **kwargs): if col or row: ax = kwargs.pop('ax', None) figsize = kwargs.pop('figsize', None) @@ -996,14 +996,17 @@ def dataset_scatter(ds, x=None, y=None, hue=None, col=None, row=None, if size is None: size = 3 elif figsize is not None: - raise ValueError('cannot provide both `figsize` and `size` arguments') + raise ValueError('cannot provide both `figsize` and' + '`size` arguments') g = FacetGrid(data=ds, col=col, row=row, col_wrap=col_wrap, sharex=sharex, sharey=sharey, figsize=figsize, aspect=aspect, size=size, subplot_kws=subplot_kws) - return g.map_dataarray_line(x=x, y=y, hue=hue, plotfunc=dataset_scatter, **kwargs) + return g.map_dataarray_line(x=x, y=y, hue=hue, + plotfunc=dataset_scatter, **kwargs) - xplt, yplt, hueplt, xlabel, ylabel, huelabel = _infer_scatter_data(ds, x, y, hue) + xplt, yplt, hueplt, xlabel, ylabel, huelabel = _infer_scatter_data(ds, x, + y, hue) figsize = kwargs.pop('figsize', None) ax = kwargs.pop('ax', None) From e89c27f90beb9ecce5681a231ecfbc1f594dd318 Mon Sep 17 00:00:00 2001 From: Yohai Bar Sinai <6164157+yohai@users.noreply.github.com> Date: Wed, 11 Jul 2018 22:21:08 -0400 Subject: [PATCH 003/101] fix bug --- xarray/plot/plot.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/xarray/plot/plot.py b/xarray/plot/plot.py index b908f63cb3e..c0b7d5c20b9 100644 --- a/xarray/plot/plot.py +++ b/xarray/plot/plot.py @@ -330,7 +330,6 @@ def line(darray, *args, **kwargs): if row or col: allargs = locals().copy() allargs.update(allargs.pop('kwargs')) - allargs.update(allargs.pop('args')) return _line_facetgrid(**allargs) ndims = len(darray.dims) @@ -351,6 +350,8 @@ def line(darray, *args, **kwargs): yincrease = kwargs.pop('yincrease', True) add_legend = kwargs.pop('add_legend', True) _labels = kwargs.pop('_labels', True) + if args is (): + args = kwargs.pop('args', ()) ax = get_axis(figsize, size, aspect, ax) xplt, yplt, hueplt, xlabel, ylabel, huelabel = \ From 9ef73cb8a4600b59a36d16bcea3b9bf56d12bd63 Mon Sep 17 00:00:00 2001 From: Yohai Bar Sinai <6164157+yohai@users.noreply.github.com> Date: Thu, 12 Jul 2018 22:55:20 -0400 Subject: [PATCH 004/101] refactor map_scatter --- xarray/plot/facetgrid.py | 50 +++++++++++++++++++++++++++++++++------- xarray/plot/plot.py | 9 +++----- 2 files changed, 45 insertions(+), 14 deletions(-) diff --git a/xarray/plot/facetgrid.py b/xarray/plot/facetgrid.py index bb053e1f753..f1869ba2a93 100644 --- a/xarray/plot/facetgrid.py +++ b/xarray/plot/facetgrid.py @@ -266,7 +266,7 @@ def map_dataarray(self, func, x, y, **kwargs): return self - def map_dataarray_line(self, plotfunc, x=None, y=None, hue=None, **kwargs): + def map_dataarray_line(self, x=None, y=None, hue=None, **kwargs): """ Apply a line plot to a 2d facet subset of the data. @@ -280,8 +280,7 @@ def map_dataarray_line(self, plotfunc, x=None, y=None, hue=None, **kwargs): self : FacetGrid object """ - from .plot import (_infer_line_data, _infer_scatter_data, - line, dataset_scatter) + from .plot import _infer_line_data, line add_legend = kwargs.pop('add_legend', True) kwargs['add_legend'] = False @@ -290,17 +289,52 @@ def map_dataarray_line(self, plotfunc, x=None, y=None, hue=None, **kwargs): # None is the sentinel value if d is not None: subset = self.data.loc[d] - mappable = plotfunc(subset, x=x, y=y, hue=hue, + mappable = line(subset, x=x, y=y, hue=hue, ax=ax, _labels=False, **kwargs) self._mappables.append(mappable) - if plotfunc == line: - _, _, hueplt, xlabel, ylabel, huelabel = _infer_line_data( + _, _, hueplt, xlabel, ylabel, huelabel = _infer_line_data( darray=self.data.loc[self.name_dicts.flat[0]], x=x, y=y, hue=hue) - elif plotfunc == dataset_scatter: - _, _, hueplt, xlabel, ylabel, huelabel = _infer_scatter_data( + + self._hue_var = hueplt + self._hue_label = huelabel + self._finalize_grid(xlabel, ylabel) + + if add_legend and hueplt is not None and huelabel is not None: + self.add_legend() + + return self + + def map_scatter(self, x=None, y=None, hue=None, **kwargs): + """ + Apply a line plot to a 2d facet subset of the data. + + Parameters + ---------- + x, y, hue: string + dimension names for the axes and hues of each facet + + Returns + ------- + self : FacetGrid object + + """ + from .plot import _infer_scatter_data, dataset_scatter + + add_legend = kwargs.pop('add_legend', True) + kwargs['add_legend'] = False + + for d, ax in zip(self.name_dicts.flat, self.axes.flat): + # None is the sentinel value + if d is not None: + subset = self.data.loc[d] + mappable = scatter(subset, x=x, y=y, hue=hue, + ax=ax, _labels=False, **kwargs) + self._mappables.append(mappable) + + _, _, hueplt, xlabel, ylabel, huelabel = _infer_scatter_data( ds=self.data.loc[self.name_dicts.flat[0]], x=x, y=y, hue=hue) diff --git a/xarray/plot/plot.py b/xarray/plot/plot.py index c0b7d5c20b9..8aea25221f3 100644 --- a/xarray/plot/plot.py +++ b/xarray/plot/plot.py @@ -104,7 +104,7 @@ def _line_facetgrid(darray, row=None, col=None, hue=None, g = FacetGrid(data=darray, col=col, row=row, col_wrap=col_wrap, sharex=sharex, sharey=sharey, figsize=figsize, aspect=aspect, size=size, subplot_kws=subplot_kws) - return g.map_dataarray_line(line, hue=hue, **kwargs) + return g.map_dataarray_line(hue=hue, **kwargs) def plot(darray, row=None, col=None, col_wrap=None, ax=None, hue=None, @@ -265,14 +265,11 @@ def _infer_scatter_data(ds, x, y, hue): dims.remove(hue) xplt = ds[x].stack(stackdim=dims).transpose('stackdim', hue).values yplt = ds[y].stack(stackdim=dims).transpose('stackdim', hue).values - else: - xplt = ds[x].values.flatten() - yplt = ds[y].values.flatten() - - if hue: hueplt = ds[x].coords[hue] huelabel = label_from_attrs(ds[x][hue]) else: + xplt = ds[x].values.flatten() + yplt = ds[y].values.flatten() hueplt = None huelabel = None From e6b286ad8726a97af0c5bfa8a996141aabe103be Mon Sep 17 00:00:00 2001 From: Yohai Bar Sinai <6164157+yohai@users.noreply.github.com> Date: Sun, 15 Jul 2018 00:13:48 -0400 Subject: [PATCH 005/101] colorbar --- xarray/core/dataset.py | 8 +--- xarray/plot/facetgrid.py | 27 ++++++----- xarray/plot/plot.py | 98 ++++++++++++++++++++++++++-------------- 3 files changed, 82 insertions(+), 51 deletions(-) diff --git a/xarray/core/dataset.py b/xarray/core/dataset.py index e89faa9601c..fe8682264ea 100644 --- a/xarray/core/dataset.py +++ b/xarray/core/dataset.py @@ -3593,12 +3593,8 @@ def real(self): def imag(self): return self._unary_op(lambda x: x.imag, keep_attrs=True)(self) - def scatter(self, x=None, y=None, hue=None, col=None, row=None, - col_wrap=None, sharex=True, sharey=True, aspect=None, - size=None, subplot_kws=None, add_legend=True, **kwargs): - lc = locals() - ds = lc.pop('self') - return dataset_scatter(ds=ds, **lc) + def scatter(self, x, y, **kwargs): + return dataset_scatter(ds=self, x=x, y=y, **kwargs) def filter_by_attrs(self, **kwargs): """Returns a ``Dataset`` with variables that match specific conditions. diff --git a/xarray/plot/facetgrid.py b/xarray/plot/facetgrid.py index f1869ba2a93..390199b7101 100644 --- a/xarray/plot/facetgrid.py +++ b/xarray/plot/facetgrid.py @@ -307,7 +307,8 @@ def map_dataarray_line(self, x=None, y=None, hue=None, **kwargs): return self - def map_scatter(self, x=None, y=None, hue=None, **kwargs): + def map_scatter(self, x=None, y=None, hue=None, discrete_legend=False, + **kwargs): """ Apply a line plot to a 2d facet subset of the data. @@ -325,25 +326,29 @@ def map_scatter(self, x=None, y=None, hue=None, **kwargs): add_legend = kwargs.pop('add_legend', True) kwargs['add_legend'] = False - + kwargs['discrete_legend'] = discrete_legend + kwargs['_labels'] = False for d, ax in zip(self.name_dicts.flat, self.axes.flat): # None is the sentinel value if d is not None: subset = self.data.loc[d] - mappable = scatter(subset, x=x, y=y, hue=hue, - ax=ax, _labels=False, **kwargs) + mappable = dataset_scatter(subset, x=x, y=y, hue=hue, + ax=ax, **kwargs) self._mappables.append(mappable) - _, _, hueplt, xlabel, ylabel, huelabel = _infer_scatter_data( + data = _infer_scatter_data( ds=self.data.loc[self.name_dicts.flat[0]], - x=x, y=y, hue=hue) + x=x, y=y, hue=hue, discrete_legend=discrete_legend) - self._hue_var = hueplt - self._hue_label = huelabel - self._finalize_grid(xlabel, ylabel) + self._finalize_grid(data['xlabel'], data['ylabel']) - if add_legend and hueplt is not None and huelabel is not None: - self.add_legend() + if hue and add_legend: + self._hue_var = data['hue_values'] + self._hue_label = data.pop('hue_label', None) + if discrete_legend: + self.add_legend() + else: + self.add_colorbar(label=self._hue_label) return self diff --git a/xarray/plot/plot.py b/xarray/plot/plot.py index 8aea25221f3..d1c24264cc5 100644 --- a/xarray/plot/plot.py +++ b/xarray/plot/plot.py @@ -204,7 +204,7 @@ def _infer_line_data(darray, x, y, hue): dim, = darray.dims # get the only dimension name huename = None hueplt = None - huelabel = '' + hue_label = '' if (x is None and y is None) or x == dim: xplt = darray.coords[dim] @@ -232,15 +232,20 @@ def _infer_line_data(darray, x, y, hue): yplt = darray.coords[yname] hueplt = darray.coords[huename] - huelabel = label_from_attrs(darray[huename]) + hue_label = label_from_attrs(darray[huename]) xlabel = label_from_attrs(xplt) ylabel = label_from_attrs(yplt) - return xplt, yplt, hueplt, xlabel, ylabel, huelabel + return xplt, yplt, hueplt, xlabel, ylabel, hue_label -def _infer_scatter_data(ds, x, y, hue): +def _ensure_numeric(arr): + numpy_types = [np.floating, np.integer] + return _valid_numpy_subdtype(arr, numpy_types) + + +def _infer_scatter_data(ds, x, y, hue, discrete_legend): dvars = set(ds.data_vars.keys()) error_msg = (' must be either one of ({0:s})' .format(', '.join(dvars))) @@ -256,26 +261,35 @@ def _infer_scatter_data(ds, x, y, hue): raise ValueError('{} and {} must have the same dimensions.' ''.format(x, y)) - if hue is not None and hue not in dims: - raise ValueError(hue + 'must be either one of ({0:s})' - ''.format(', '.join(dims))) + if hue is not None and hue not in ds.coords: + raise ValueError(hue + ' must be either one of ({0:s})' + ''.format(', '.join(ds.coords))) - dims = set(dims) + data = {'xlabel': label_from_attrs(ds[x]), + 'ylabel': label_from_attrs(ds[y])} if hue: + data.update({'hue_label': label_from_attrs(ds.coords[hue])}) + data.update({'hue_values': ds[x].coords[hue]}) + dims = set(dims) + if hue and discrete_legend: dims.remove(hue) xplt = ds[x].stack(stackdim=dims).transpose('stackdim', hue).values yplt = ds[y].stack(stackdim=dims).transpose('stackdim', hue).values - hueplt = ds[x].coords[hue] - huelabel = label_from_attrs(ds[x][hue]) - else: - xplt = ds[x].values.flatten() - yplt = ds[y].values.flatten() - hueplt = None - huelabel = None + data.update({'x': xplt, 'y': yplt}) + return data + + data.update({'x': ds[x].values.flatten(), + 'y': ds[y].values.flatten(), + 'color': None}) + if hue: + # this is a hack to make a dataarray of the shape of ds[x] whose + # values are the coordinate hue. There's probably a better way + color = ds[x] + color[:] = 0 + color += ds.coords[hue] + data.update({'color': color.values.flatten()}) - xlabel = label_from_attrs(ds[x]) - ylabel = label_from_attrs(ds[y]) - return xplt, yplt, hueplt, xlabel, ylabel, huelabel + return data # This function signature should not change so that it can use @@ -351,7 +365,7 @@ def line(darray, *args, **kwargs): args = kwargs.pop('args', ()) ax = get_axis(figsize, size, aspect, ax) - xplt, yplt, hueplt, xlabel, ylabel, huelabel = \ + xplt, yplt, hueplt, xlabel, ylabel, hue_label = \ _infer_line_data(darray, x, y, hue) _ensure_plottable(xplt) @@ -370,7 +384,7 @@ def line(darray, *args, **kwargs): if darray.ndim == 2 and add_legend: ax.legend(handles=primitive, labels=list(hueplt.values), - title=huelabel) + title=hue_label) # Rotate dates on xlabels # Do this without calling autofmt_xdate so that x-axes ticks @@ -983,7 +997,15 @@ def pcolormesh(x, y, z, ax, infer_intervals=None, **kwargs): def dataset_scatter(ds, x=None, y=None, hue=None, col=None, row=None, col_wrap=None, sharex=True, sharey=True, aspect=None, - size=None, subplot_kws=None, add_legend=True, **kwargs): + size=None, subplot_kws=None, add_legend=True, + discrete_legend=None, **kwargs): + + if hue and not _ensure_numeric(ds[hue].values): + if discrete_legend is None: + discrete_legend = True + elif discrete_legend is False: + raise TypeError('Cannot create a colorbar for a non numeric' + 'coordinate') if col or row: ax = kwargs.pop('ax', None) figsize = kwargs.pop('figsize', None) @@ -1000,26 +1022,34 @@ def dataset_scatter(ds, x=None, y=None, hue=None, col=None, row=None, g = FacetGrid(data=ds, col=col, row=row, col_wrap=col_wrap, sharex=sharex, sharey=sharey, figsize=figsize, aspect=aspect, size=size, subplot_kws=subplot_kws) - return g.map_dataarray_line(x=x, y=y, hue=hue, - plotfunc=dataset_scatter, **kwargs) + return g.map_scatter(x=x, y=y, hue=hue, add_legend=add_legend, + discrete_legend=discrete_legend, **kwargs) - xplt, yplt, hueplt, xlabel, ylabel, huelabel = _infer_scatter_data(ds, x, - y, hue) + if add_legend and not hue: + raise ValueError('hue must be speicifed for generating a lengend') + data = _infer_scatter_data(ds, x, y, hue, discrete_legend) figsize = kwargs.pop('figsize', None) ax = kwargs.pop('ax', None) ax = get_axis(figsize, size, aspect, ax) - primitive = ax.plot(xplt, yplt, '.') + if discrete_legend: + primitive = ax.plot(data['x'], data['y'], '.') + else: + primitive = ax.scatter(data['x'], data['y'], c=data['color']) if kwargs.get('_labels', True): - if xlabel is not None: - ax.set_xlabel(xlabel) - - if ylabel is not None: - ax.set_ylabel(ylabel) + if data.get('xlabel', None): + ax.set_xlabel(data.get('xlabel')) - if add_legend and huelabel is not None: + if data.get('ylabel', None): + ax.set_ylabel(data.get('ylabel')) + if add_legend and discrete_legend: ax.legend(handles=primitive, - labels=list(hueplt.values), - title=huelabel) + labels=list(data['hue_values'].values), + title=data.get('hue_label', None)) + if add_legend and not discrete_legend: + cbar = ax.figure.colorbar(primitive) + if data.get('hue_label', None): + cbar.ax.set_ylabel(data.get('hue_label')) + return primitive From 4c92a6202c6fb9d8a5f2b478b067d7c88932f6b2 Mon Sep 17 00:00:00 2001 From: Yohai Bar Sinai <6164157+yohai@users.noreply.github.com> Date: Sun, 15 Jul 2018 21:26:18 -0400 Subject: [PATCH 006/101] formatting --- xarray/plot/facetgrid.py | 10 +++++----- xarray/plot/plot.py | 12 +++++++----- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/xarray/plot/facetgrid.py b/xarray/plot/facetgrid.py index 390199b7101..b1507de9875 100644 --- a/xarray/plot/facetgrid.py +++ b/xarray/plot/facetgrid.py @@ -336,16 +336,16 @@ def map_scatter(self, x=None, y=None, hue=None, discrete_legend=False, ax=ax, **kwargs) self._mappables.append(mappable) - data = _infer_scatter_data( - ds=self.data.loc[self.name_dicts.flat[0]], - x=x, y=y, hue=hue, discrete_legend=discrete_legend) + data = _infer_scatter_data(ds=self.data.loc[self.name_dicts.flat[0]], + x=x, y=y, hue=hue, + discrete_legend=discrete_legend) self._finalize_grid(data['xlabel'], data['ylabel']) - if hue and add_legend: - self._hue_var = data['hue_values'] + if hue and add_legend: self._hue_label = data.pop('hue_label', None) if discrete_legend: + self._hue_var = data['hue_values'] self.add_legend() else: self.add_colorbar(label=self._hue_label) diff --git a/xarray/plot/plot.py b/xarray/plot/plot.py index d1c24264cc5..9864abb1515 100644 --- a/xarray/plot/plot.py +++ b/xarray/plot/plot.py @@ -261,21 +261,23 @@ def _infer_scatter_data(ds, x, y, hue, discrete_legend): raise ValueError('{} and {} must have the same dimensions.' ''.format(x, y)) - if hue is not None and hue not in ds.coords: + dims_coords = set(list(ds.coords)+list(ds.dims)) + if hue is not None and hue not in dims_coords: raise ValueError(hue + ' must be either one of ({0:s})' - ''.format(', '.join(ds.coords))) + ''.format(', '.join(dims_coords))) data = {'xlabel': label_from_attrs(ds[x]), 'ylabel': label_from_attrs(ds[y])} if hue: data.update({'hue_label': label_from_attrs(ds.coords[hue])}) - data.update({'hue_values': ds[x].coords[hue]}) + dims = set(dims) if hue and discrete_legend: dims.remove(hue) xplt = ds[x].stack(stackdim=dims).transpose('stackdim', hue).values yplt = ds[y].stack(stackdim=dims).transpose('stackdim', hue).values data.update({'x': xplt, 'y': yplt}) + data.update({'hue_values': ds[x].coords[hue]}) return data data.update({'x': ds[x].values.flatten(), @@ -1004,7 +1006,7 @@ def dataset_scatter(ds, x=None, y=None, hue=None, col=None, row=None, if discrete_legend is None: discrete_legend = True elif discrete_legend is False: - raise TypeError('Cannot create a colorbar for a non numeric' + raise TypeError('Cannot create a colorbar for a non numeric ' 'coordinate') if col or row: ax = kwargs.pop('ax', None) @@ -1016,7 +1018,7 @@ def dataset_scatter(ds, x=None, y=None, hue=None, col=None, row=None, if size is None: size = 3 elif figsize is not None: - raise ValueError('cannot provide both `figsize` and' + raise ValueError('cannot provide both `figsize` and ' '`size` arguments') g = FacetGrid(data=ds, col=col, row=row, col_wrap=col_wrap, From 355870a05ae314544287c18d44bc5fbd99cd0519 Mon Sep 17 00:00:00 2001 From: Yohai Bar Sinai <6164157+yohai@users.noreply.github.com> Date: Sun, 15 Jul 2018 22:09:11 -0400 Subject: [PATCH 007/101] refactor --- xarray/core/dataset.py | 13 ++++++++++--- xarray/plot/facetgrid.py | 6 +++--- xarray/plot/plot.py | 28 +++++++++++++++++++++++----- 3 files changed, 36 insertions(+), 11 deletions(-) diff --git a/xarray/core/dataset.py b/xarray/core/dataset.py index fe8682264ea..407e4b4f11e 100644 --- a/xarray/core/dataset.py +++ b/xarray/core/dataset.py @@ -33,7 +33,7 @@ Frozen, SortedKeysDict, either_dict_or_kwargs, decode_numpy_dict_values, ensure_us_time_resolution, hashable, maybe_wrap_array) from .variable import IndexVariable, Variable, as_variable, broadcast_variables -from ..plot.plot import dataset_scatter +from ..plot.plot import _Dataset_PlotMethods # list of attributes of pd.DatetimeIndex that are ndarrays of time info _DATETIMEINDEX_COMPONENTS = ['year', 'month', 'day', 'hour', 'minute', @@ -3593,8 +3593,15 @@ def real(self): def imag(self): return self._unary_op(lambda x: x.imag, keep_attrs=True)(self) - def scatter(self, x, y, **kwargs): - return dataset_scatter(ds=self, x=x, y=y, **kwargs) + @property + def plot(self): + """ + Access plotting functions. Use it as a namespace to use + xarray.plot functions as Dataset methods + >>> ds.plot.scatter(...) # equivalent to xarray.plot.scatter(ds,...) + + """ + return _Dataset_PlotMethods(self) def filter_by_attrs(self, **kwargs): """Returns a ``Dataset`` with variables that match specific conditions. diff --git a/xarray/plot/facetgrid.py b/xarray/plot/facetgrid.py index b1507de9875..bc9bef8251e 100644 --- a/xarray/plot/facetgrid.py +++ b/xarray/plot/facetgrid.py @@ -322,7 +322,7 @@ def map_scatter(self, x=None, y=None, hue=None, discrete_legend=False, self : FacetGrid object """ - from .plot import _infer_scatter_data, dataset_scatter + from .plot import _infer_scatter_data, scatter add_legend = kwargs.pop('add_legend', True) kwargs['add_legend'] = False @@ -332,8 +332,8 @@ def map_scatter(self, x=None, y=None, hue=None, discrete_legend=False, # None is the sentinel value if d is not None: subset = self.data.loc[d] - mappable = dataset_scatter(subset, x=x, y=y, hue=hue, - ax=ax, **kwargs) + mappable = scatter(subset, x=x, y=y, hue=hue, + ax=ax, **kwargs) self._mappables.append(mappable) data = _infer_scatter_data(ds=self.data.loc[self.name_dicts.flat[0]], diff --git a/xarray/plot/plot.py b/xarray/plot/plot.py index 9864abb1515..5d2d7c5612c 100644 --- a/xarray/plot/plot.py +++ b/xarray/plot/plot.py @@ -261,7 +261,7 @@ def _infer_scatter_data(ds, x, y, hue, discrete_legend): raise ValueError('{} and {} must have the same dimensions.' ''.format(x, y)) - dims_coords = set(list(ds.coords)+list(ds.dims)) + dims_coords = set(list(ds.coords) + list(ds.dims)) if hue is not None and hue not in dims_coords: raise ValueError(hue + ' must be either one of ({0:s})' ''.format(', '.join(dims_coords))) @@ -997,10 +997,10 @@ def pcolormesh(x, y, z, ax, infer_intervals=None, **kwargs): return primitive -def dataset_scatter(ds, x=None, y=None, hue=None, col=None, row=None, - col_wrap=None, sharex=True, sharey=True, aspect=None, - size=None, subplot_kws=None, add_legend=True, - discrete_legend=None, **kwargs): +def scatter(ds, x, y, hue=None, col=None, row=None, + col_wrap=None, sharex=True, sharey=True, aspect=None, + size=None, subplot_kws=None, add_legend=True, + discrete_legend=None, **kwargs): if hue and not _ensure_numeric(ds[hue].values): if discrete_legend is None: @@ -1055,3 +1055,21 @@ def dataset_scatter(ds, x=None, y=None, hue=None, col=None, row=None, cbar.ax.set_ylabel(data.get('hue_label')) return primitive + + +class _Dataset_PlotMethods(object): + """ + Enables use of xarray.plot functions as attributes on a Dataset. + For example, Dataset.plot.scatter + """ + + def __init__(self, dataset): + self._ds = dataset + + def __call__(self, *args, **kwargs): + raise ValueError('Dataset.plot cannot be called directly. Use' + 'an explicit plot method, e.g. ds.plot.scatter(...)') + + @functools.wraps(scatter) + def scatter(self, *args, **kwargs): + return scatter(self._ds, *args, **kwargs) From 16a5d186eec0be3e27410d5afb881bf8fea76e86 Mon Sep 17 00:00:00 2001 From: Yohai Bar Sinai <6164157+yohai@users.noreply.github.com> Date: Sun, 15 Jul 2018 23:54:44 -0400 Subject: [PATCH 008/101] refactor _infer_data --- xarray/plot/facetgrid.py | 36 ++++----------- xarray/plot/plot.py | 99 ++++++++++++++++++++++++---------------- 2 files changed, 69 insertions(+), 66 deletions(-) diff --git a/xarray/plot/facetgrid.py b/xarray/plot/facetgrid.py index bc9bef8251e..ce94d69b98c 100644 --- a/xarray/plot/facetgrid.py +++ b/xarray/plot/facetgrid.py @@ -308,26 +308,14 @@ def map_dataarray_line(self, x=None, y=None, hue=None, **kwargs): return self def map_scatter(self, x=None, y=None, hue=None, discrete_legend=False, - **kwargs): - """ - Apply a line plot to a 2d facet subset of the data. - - Parameters - ---------- - x, y, hue: string - dimension names for the axes and hues of each facet + add_legend=None, **kwargs): + from .plot import _infer_scatter_meta_data, scatter - Returns - ------- - self : FacetGrid object - - """ - from .plot import _infer_scatter_data, scatter - - add_legend = kwargs.pop('add_legend', True) kwargs['add_legend'] = False kwargs['discrete_legend'] = discrete_legend - kwargs['_labels'] = False + meta_data = _infer_scatter_meta_data(self.data, x, y, hue, + add_legend, discrete_legend) + kwargs['_meta_data'] = meta_data for d, ax in zip(self.name_dicts.flat, self.axes.flat): # None is the sentinel value if d is not None: @@ -336,16 +324,12 @@ def map_scatter(self, x=None, y=None, hue=None, discrete_legend=False, ax=ax, **kwargs) self._mappables.append(mappable) - data = _infer_scatter_data(ds=self.data.loc[self.name_dicts.flat[0]], - x=x, y=y, hue=hue, - discrete_legend=discrete_legend) - - self._finalize_grid(data['xlabel'], data['ylabel']) + self._finalize_grid(meta_data['xlabel'], meta_data['ylabel']) - if hue and add_legend: - self._hue_label = data.pop('hue_label', None) - if discrete_legend: - self._hue_var = data['hue_values'] + if hue and meta_data['add_legend']: + self._hue_label = meta_data.pop('hue_label', None) + if meta_data['discrete_legend']: + self._hue_var = meta_data['hue_values'] self.add_legend() else: self.add_colorbar(label=self._hue_label) diff --git a/xarray/plot/plot.py b/xarray/plot/plot.py index 5d2d7c5612c..10455ae3fd6 100644 --- a/xarray/plot/plot.py +++ b/xarray/plot/plot.py @@ -245,7 +245,7 @@ def _ensure_numeric(arr): return _valid_numpy_subdtype(arr, numpy_types) -def _infer_scatter_data(ds, x, y, hue, discrete_legend): +def _infer_scatter_meta_data(ds, x, y, hue, add_legend, discrete_legend): dvars = set(ds.data_vars.keys()) error_msg = (' must be either one of ({0:s})' .format(', '.join(dvars))) @@ -256,6 +256,18 @@ def _infer_scatter_data(ds, x, y, hue, discrete_legend): if y not in dvars: raise ValueError(y + error_msg) + if hue and add_legend is None: + add_legend = True + if add_legend and not hue: + raise ValueError('hue must be speicifed for generating a lengend') + + if hue and not _ensure_numeric(ds[hue].values): + if discrete_legend is None: + discrete_legend = True + elif discrete_legend is False: + raise TypeError('Cannot create a colorbar for a non numeric' + ' coordinate') + dims = ds[x].dims if ds[y].dims != dims: raise ValueError('{} and {} must have the same dimensions.' @@ -264,35 +276,41 @@ def _infer_scatter_data(ds, x, y, hue, discrete_legend): dims_coords = set(list(ds.coords) + list(ds.dims)) if hue is not None and hue not in dims_coords: raise ValueError(hue + ' must be either one of ({0:s})' - ''.format(', '.join(dims_coords))) + ''.format(', '.join(dims_coords))) - data = {'xlabel': label_from_attrs(ds[x]), - 'ylabel': label_from_attrs(ds[y])} if hue: - data.update({'hue_label': label_from_attrs(ds.coords[hue])}) + hue_label = label_from_attrs(ds.coords[hue]) + else: + hue_label = None - dims = set(dims) - if hue and discrete_legend: + return {'add_legend': add_legend, + 'discrete_legend': discrete_legend, + 'hue_label': hue_label, + 'xlabel': label_from_attrs(ds[x]), + 'ylabel': label_from_attrs(ds[y]), + 'hue_values': ds[x].coords[hue] if discrete_legend else None} + + +def _infer_scatter_data(ds, x, y, hue, discrete_legend): + dims = set(ds[x].dims) + if discrete_legend: dims.remove(hue) xplt = ds[x].stack(stackdim=dims).transpose('stackdim', hue).values yplt = ds[y].stack(stackdim=dims).transpose('stackdim', hue).values - data.update({'x': xplt, 'y': yplt}) - data.update({'hue_values': ds[x].coords[hue]}) + return {'x': xplt, 'y': yplt} + else: + data = {'x': ds[x].values.flatten(), + 'y': ds[y].values.flatten(), + 'color': None} + if hue: + # this is a hack to make a dataarray of the shape of ds[x] whose + # values are the coordinate hue. There's probably a better way + color = ds[x] + color[:] = 0 + color += ds.coords[hue] + data['color'] = color.values.flatten() return data - data.update({'x': ds[x].values.flatten(), - 'y': ds[y].values.flatten(), - 'color': None}) - if hue: - # this is a hack to make a dataarray of the shape of ds[x] whose - # values are the coordinate hue. There's probably a better way - color = ds[x] - color[:] = 0 - color += ds.coords[hue] - data.update({'color': color.values.flatten()}) - - return data - # This function signature should not change so that it can use # matplotlib format strings @@ -999,15 +1017,17 @@ def pcolormesh(x, y, z, ax, infer_intervals=None, **kwargs): def scatter(ds, x, y, hue=None, col=None, row=None, col_wrap=None, sharex=True, sharey=True, aspect=None, - size=None, subplot_kws=None, add_legend=True, + size=None, subplot_kws=None, add_legend=None, discrete_legend=None, **kwargs): - if hue and not _ensure_numeric(ds[hue].values): - if discrete_legend is None: - discrete_legend = True - elif discrete_legend is False: - raise TypeError('Cannot create a colorbar for a non numeric ' - 'coordinate') + if kwargs.get('_meta_data', None): + discrete_legend = kwargs['_meta_data']['discrete_legend'] + else: + meta_data = _infer_scatter_meta_data(ds, x, y, hue, + add_legend, discrete_legend) + discrete_legend = meta_data['discrete_legend'] + add_legend = meta_data['add_legend'] + if col or row: ax = kwargs.pop('ax', None) figsize = kwargs.pop('figsize', None) @@ -1027,8 +1047,6 @@ def scatter(ds, x, y, hue=None, col=None, row=None, return g.map_scatter(x=x, y=y, hue=hue, add_legend=add_legend, discrete_legend=discrete_legend, **kwargs) - if add_legend and not hue: - raise ValueError('hue must be speicifed for generating a lengend') data = _infer_scatter_data(ds, x, y, hue, discrete_legend) figsize = kwargs.pop('figsize', None) @@ -1038,21 +1056,22 @@ def scatter(ds, x, y, hue=None, col=None, row=None, primitive = ax.plot(data['x'], data['y'], '.') else: primitive = ax.scatter(data['x'], data['y'], c=data['color']) + if '_meta_data' in kwargs: # if this was called from map_scatter, + return primitive # finish here. Else, make labels - if kwargs.get('_labels', True): - if data.get('xlabel', None): - ax.set_xlabel(data.get('xlabel')) + if meta_data.get('xlabel', None): + ax.set_xlabel(meta_data.get('xlabel')) - if data.get('ylabel', None): - ax.set_ylabel(data.get('ylabel')) + if meta_data.get('ylabel', None): + ax.set_ylabel(meta_data.get('ylabel')) if add_legend and discrete_legend: ax.legend(handles=primitive, - labels=list(data['hue_values'].values), - title=data.get('hue_label', None)) + labels=list(meta_data['hue_values'].values), + title=meta_data.get('hue_label', None)) if add_legend and not discrete_legend: cbar = ax.figure.colorbar(primitive) - if data.get('hue_label', None): - cbar.ax.set_ylabel(data.get('hue_label')) + if meta_data.get('hue_label', None): + cbar.ax.set_ylabel(meta_data.get('hue_label')) return primitive From ff27ef5b91ff92bdca70f7dee8d0ca003539572b Mon Sep 17 00:00:00 2001 From: Yohai Bar Sinai <6164157+yohai@users.noreply.github.com> Date: Mon, 16 Jul 2018 00:06:29 -0400 Subject: [PATCH 009/101] added tests --- xarray/tests/test_plot.py | 82 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 81 insertions(+), 1 deletion(-) diff --git a/xarray/tests/test_plot.py b/xarray/tests/test_plot.py index 90d30946c9c..07ee65645da 100644 --- a/xarray/tests/test_plot.py +++ b/xarray/tests/test_plot.py @@ -8,7 +8,7 @@ import pytest import xarray.plot as xplt -from xarray import DataArray +from xarray import DataArray, Dataset from xarray.coding.times import _import_cftime from xarray.plot.plot import _infer_interval_breaks from xarray.plot.utils import ( @@ -1622,6 +1622,86 @@ def test_wrong_num_of_dimensions(self): self.darray.plot.line(row='row', hue='hue') +class TestScatterPlots(PlotTestCase): + def setUp(self): + das = [DataArray(np.random.randn(3, 3, 4, 4), + dims=['x', 'row', 'col', 'hue'], + coords=[range(k) for k in [3, 3, 4, 4]]) + for _ in [1, 2]] + ds = Dataset({'A': das[0], 'B': das[1]}) + ds.hue.name = 'huename' + ds.hue.attrs['units'] = 'hunits' + ds.x.attrs['units'] = 'xunits' + ds.col.attrs['units'] = 'colunits' + ds.row.attrs['units'] = 'rowunits' + ds.A.attrs['units'] = 'Aunits' + ds.B.attrs['units'] = 'Bunits' + self.ds = ds + + def test_facetgrid_shape(self): + g = self.ds.plot.scatter(x='A', y='B', row='row', col='col') + assert g.axes.shape == (len(self.ds.row), len(self.ds.col)) + + g = self.ds.plot.scatter(x='A', y='B', row='col', col='row') + assert g.axes.shape == (len(self.ds.col), len(self.ds.row)) + + def test_default_labels(self): + g = self.ds.plot.scatter('A', 'B', row='row', col='col', hue='hue') + # Rightmost column should be labeled + for label, ax in zip(self.ds.coords['row'].values, g.axes[:, -1]): + assert substring_in_axes(label, ax) + + # Top row should be labeled + for label, ax in zip(self.ds.coords['col'].values, g.axes[0, :]): + assert substring_in_axes(str(label), ax) + + # Bottom row should have name of x array name and units + for ax in g.axes[-1, :]: + assert ax.get_xlabel() == 'A [Aunits]' + + # Leftmost column should have name of y array name and units + for ax in g.axes[:, 0]: + assert ax.get_ylabel() == 'B [Bunits]' + + def test_both_x_and_y(self): + with pytest.raises(ValueError): + self.darray.plot.line(row='row', col='col', + x='x', y='hue') + + def test_axes_in_faceted_plot(self): + with pytest.raises(ValueError): + self.ds.plot.scatter(x='A', y='B', row='row', ax=plt.axes()) + + def test_figsize_and_size(self): + with pytest.raises(ValueError): + self.ds.plot.scatter(x='A', y='B', row='row', size=3, figsize=4) + + def test_bad_args(self): + with pytest.raises(ValueError): + self.ds.plot.scatter(x='A', y='B', add_legend=True) + self.ds.plot.scatter(x='A', y='The Spanish Inquisition') + self.ds.plot.scatter(x='The Spanish Inquisition', y='B') + + def test_non_numeric_legened(self): + self.ds['hue'] = pd.date_range('2000-01-01', periods=4) + lines = self.ds.plot.scatter(x='A', y='B', hue='hue') + # should make a discrete legend + assert lines[0].axes.legend_ is not None + # and raise an error if explicitly not allowed to do so + with pytest.raises(ValueError): + self.ds.plot.scatter(x='A', y='B', hue='hue', + discrete_legend=False) + + def test_add_legened_by_default(self): + sc = self.ds.plot.scatter(x='A', y='B', hue='hue') + assert len(sc.figure.axes) == 2 + + def test_not_same_dimensions(self): + self.ds['A'] = self.ds['A'].isel(x=0) + with pytest.raises(ValueError): + self.ds.plot.scatter(x='A', y='B') + + class TestDatetimePlot(PlotTestCase): def setUp(self): ''' From 3cee41d564ffc5a1dc4408e8e9b417777af2e7b1 Mon Sep 17 00:00:00 2001 From: Yohai Bar Sinai <6164157+yohai@users.noreply.github.com> Date: Mon, 16 Jul 2018 00:09:45 -0400 Subject: [PATCH 010/101] minor formatting --- xarray/plot/facetgrid.py | 4 ++-- xarray/tests/test_plot.py | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/xarray/plot/facetgrid.py b/xarray/plot/facetgrid.py index ce94d69b98c..d2016e30679 100644 --- a/xarray/plot/facetgrid.py +++ b/xarray/plot/facetgrid.py @@ -295,8 +295,8 @@ def map_dataarray_line(self, x=None, y=None, hue=None, **kwargs): self._mappables.append(mappable) _, _, hueplt, xlabel, ylabel, huelabel = _infer_line_data( - darray=self.data.loc[self.name_dicts.flat[0]], - x=x, y=y, hue=hue) + darray=self.data.loc[self.name_dicts.flat[0]], + x=x, y=y, hue=hue) self._hue_var = hueplt self._hue_label = huelabel diff --git a/xarray/tests/test_plot.py b/xarray/tests/test_plot.py index 07ee65645da..b34880f3e6b 100644 --- a/xarray/tests/test_plot.py +++ b/xarray/tests/test_plot.py @@ -1625,9 +1625,9 @@ def test_wrong_num_of_dimensions(self): class TestScatterPlots(PlotTestCase): def setUp(self): das = [DataArray(np.random.randn(3, 3, 4, 4), - dims=['x', 'row', 'col', 'hue'], - coords=[range(k) for k in [3, 3, 4, 4]]) - for _ in [1, 2]] + dims=['x', 'row', 'col', 'hue'], + coords=[range(k) for k in [3, 3, 4, 4]]) + for _ in [1, 2]] ds = Dataset({'A': das[0], 'B': das[1]}) ds.hue.name = 'huename' ds.hue.attrs['units'] = 'hunits' From fe7f16f463942412f0d5c71d4defa551375a945d Mon Sep 17 00:00:00 2001 From: Yohai Bar Sinai <6164157+yohai@users.noreply.github.com> Date: Mon, 16 Jul 2018 09:38:42 -0400 Subject: [PATCH 011/101] fixed tests --- xarray/plot/plot.py | 4 ++-- xarray/tests/test_plot.py | 13 ------------- 2 files changed, 2 insertions(+), 15 deletions(-) diff --git a/xarray/plot/plot.py b/xarray/plot/plot.py index 10455ae3fd6..9c2ffbde048 100644 --- a/xarray/plot/plot.py +++ b/xarray/plot/plot.py @@ -265,8 +265,8 @@ def _infer_scatter_meta_data(ds, x, y, hue, add_legend, discrete_legend): if discrete_legend is None: discrete_legend = True elif discrete_legend is False: - raise TypeError('Cannot create a colorbar for a non numeric' - ' coordinate') + raise ValueError('Cannot create a colorbar for a non numeric' + ' coordinate') dims = ds[x].dims if ds[y].dims != dims: diff --git a/xarray/tests/test_plot.py b/xarray/tests/test_plot.py index b34880f3e6b..f56d1f460f3 100644 --- a/xarray/tests/test_plot.py +++ b/xarray/tests/test_plot.py @@ -1601,11 +1601,6 @@ def test_set_axis_labels(self): assert 'longitude' in alltxt assert 'latitude' in alltxt - def test_both_x_and_y(self): - with pytest.raises(ValueError): - self.darray.plot.line(row='row', col='col', - x='x', y='hue') - def test_axes_in_faceted_plot(self): with pytest.raises(ValueError): self.darray.plot.line(row='row', col='col', @@ -1647,9 +1642,6 @@ def test_facetgrid_shape(self): def test_default_labels(self): g = self.ds.plot.scatter('A', 'B', row='row', col='col', hue='hue') - # Rightmost column should be labeled - for label, ax in zip(self.ds.coords['row'].values, g.axes[:, -1]): - assert substring_in_axes(label, ax) # Top row should be labeled for label, ax in zip(self.ds.coords['col'].values, g.axes[0, :]): @@ -1663,11 +1655,6 @@ def test_default_labels(self): for ax in g.axes[:, 0]: assert ax.get_ylabel() == 'B [Bunits]' - def test_both_x_and_y(self): - with pytest.raises(ValueError): - self.darray.plot.line(row='row', col='col', - x='x', y='hue') - def test_axes_in_faceted_plot(self): with pytest.raises(ValueError): self.ds.plot.scatter(x='A', y='B', row='row', ax=plt.axes()) From b80ff5d23cb8725d6e9b017a4e890658744eaa96 Mon Sep 17 00:00:00 2001 From: dcherian Date: Thu, 22 Nov 2018 10:10:53 -0700 Subject: [PATCH 012/101] Refactor out to dataset_plot.py + move utilities to utils.py --- xarray/core/dataset.py | 2 +- xarray/plot/dataset_plot.py | 160 ++++++++++++++++++++++++++++++ xarray/plot/facetgrid.py | 2 +- xarray/plot/plot.py | 189 +----------------------------------- xarray/plot/utils.py | 35 +++++++ 5 files changed, 201 insertions(+), 187 deletions(-) create mode 100644 xarray/plot/dataset_plot.py diff --git a/xarray/core/dataset.py b/xarray/core/dataset.py index 1fd710f9552..20b757f61ca 100644 --- a/xarray/core/dataset.py +++ b/xarray/core/dataset.py @@ -36,7 +36,7 @@ decode_numpy_dict_values, either_dict_or_kwargs, ensure_us_time_resolution, hashable, maybe_wrap_array) from .variable import IndexVariable, Variable, as_variable, broadcast_variables -from ..plot.plot import _Dataset_PlotMethods +from ..plot.dataset_plot import _Dataset_PlotMethods # list of attributes of pd.DatetimeIndex that are ndarrays of time info _DATETIMEINDEX_COMPONENTS = ['year', 'month', 'day', 'hour', 'minute', diff --git a/xarray/plot/dataset_plot.py b/xarray/plot/dataset_plot.py new file mode 100644 index 00000000000..878b00c4f5d --- /dev/null +++ b/xarray/plot/dataset_plot.py @@ -0,0 +1,160 @@ +from __future__ import absolute_import, division, print_function + +import functools +import warnings + +import numpy as np +import pandas as pd + +from .facetgrid import FacetGrid +from .utils import ( + ROBUST_PERCENTILE, _determine_cmap_params, _ensure_numeric, + _interval_to_double_bound_points, _interval_to_mid_points, + _valid_other_type, get_axis, + import_matplotlib_pyplot, label_from_attrs) + + +def _infer_scatter_meta_data(ds, x, y, hue, add_legend, discrete_legend): + dvars = set(ds.data_vars.keys()) + error_msg = (' must be either one of ({0:s})' + .format(', '.join(dvars))) + + if x not in dvars: + raise ValueError(x + error_msg) + + if y not in dvars: + raise ValueError(y + error_msg) + + if hue and add_legend is None: + add_legend = True + if add_legend and not hue: + raise ValueError('hue must be speicifed for generating a lengend') + + if hue and not _ensure_numeric(ds[hue].values): + if discrete_legend is None: + discrete_legend = True + elif discrete_legend is False: + raise ValueError('Cannot create a colorbar for a non numeric' + ' coordinate') + + dims = ds[x].dims + if ds[y].dims != dims: + raise ValueError('{} and {} must have the same dimensions.' + ''.format(x, y)) + + dims_coords = set(list(ds.coords) + list(ds.dims)) + if hue is not None and hue not in dims_coords: + raise ValueError(hue + ' must be either one of ({0:s})' + ''.format(', '.join(dims_coords))) + + if hue: + hue_label = label_from_attrs(ds.coords[hue]) + else: + hue_label = None + + return {'add_legend': add_legend, + 'discrete_legend': discrete_legend, + 'hue_label': hue_label, + 'xlabel': label_from_attrs(ds[x]), + 'ylabel': label_from_attrs(ds[y]), + 'hue_values': ds[x].coords[hue] if discrete_legend else None} + + +def _infer_scatter_data(ds, x, y, hue, discrete_legend): + dims = set(ds[x].dims) + if discrete_legend: + dims.remove(hue) + xplt = ds[x].stack(stackdim=dims).transpose('stackdim', hue).values + yplt = ds[y].stack(stackdim=dims).transpose('stackdim', hue).values + return {'x': xplt, 'y': yplt} + else: + data = {'x': ds[x].values.flatten(), + 'y': ds[y].values.flatten(), + 'color': None} + if hue: + # this is a hack to make a dataarray of the shape of ds[x] whose + # values are the coordinate hue. There's probably a better way + color = ds[x] + color[:] = 0 + color += ds.coords[hue] + data['color'] = color.values.flatten() + return data + + +def scatter(ds, x, y, hue=None, col=None, row=None, + col_wrap=None, sharex=True, sharey=True, aspect=None, + size=None, subplot_kws=None, add_legend=None, + discrete_legend=None, **kwargs): + + if kwargs.get('_meta_data', None): + discrete_legend = kwargs['_meta_data']['discrete_legend'] + else: + meta_data = _infer_scatter_meta_data(ds, x, y, hue, + add_legend, discrete_legend) + discrete_legend = meta_data['discrete_legend'] + add_legend = meta_data['add_legend'] + + if col or row: + ax = kwargs.pop('ax', None) + figsize = kwargs.pop('figsize', None) + if ax is not None: + raise ValueError("Can't use axes when making faceted plots.") + if aspect is None: + aspect = 1 + if size is None: + size = 3 + elif figsize is not None: + raise ValueError('cannot provide both `figsize` and ' + '`size` arguments') + + g = FacetGrid(data=ds, col=col, row=row, col_wrap=col_wrap, + sharex=sharex, sharey=sharey, figsize=figsize, + aspect=aspect, size=size, subplot_kws=subplot_kws) + return g.map_scatter(x=x, y=y, hue=hue, add_legend=add_legend, + discrete_legend=discrete_legend, **kwargs) + + data = _infer_scatter_data(ds, x, y, hue, discrete_legend) + + figsize = kwargs.pop('figsize', None) + ax = kwargs.pop('ax', None) + ax = get_axis(figsize, size, aspect, ax) + if discrete_legend: + primitive = ax.plot(data['x'], data['y'], '.') + else: + primitive = ax.scatter(data['x'], data['y'], c=data['color']) + if '_meta_data' in kwargs: # if this was called from map_scatter, + return primitive # finish here. Else, make labels + + if meta_data.get('xlabel', None): + ax.set_xlabel(meta_data.get('xlabel')) + + if meta_data.get('ylabel', None): + ax.set_ylabel(meta_data.get('ylabel')) + if add_legend and discrete_legend: + ax.legend(handles=primitive, + labels=list(meta_data['hue_values'].values), + title=meta_data.get('hue_label', None)) + if add_legend and not discrete_legend: + cbar = ax.figure.colorbar(primitive) + if meta_data.get('hue_label', None): + cbar.ax.set_ylabel(meta_data.get('hue_label')) + + return primitive + + +class _Dataset_PlotMethods(object): + """ + Enables use of xarray.plot functions as attributes on a Dataset. + For example, Dataset.plot.scatter + """ + + def __init__(self, dataset): + self._ds = dataset + + def __call__(self, *args, **kwargs): + raise ValueError('Dataset.plot cannot be called directly. Use' + 'an explicit plot method, e.g. ds.plot.scatter(...)') + + @functools.wraps(scatter) + def scatter(self, *args, **kwargs): + return scatter(self._ds, *args, **kwargs) diff --git a/xarray/plot/facetgrid.py b/xarray/plot/facetgrid.py index 48a3e090aa3..1c0ef9d73f2 100644 --- a/xarray/plot/facetgrid.py +++ b/xarray/plot/facetgrid.py @@ -316,7 +316,7 @@ def map_dataarray_line(self, x=None, y=None, hue=None, **kwargs): def map_scatter(self, x=None, y=None, hue=None, discrete_legend=False, add_legend=None, **kwargs): - from .plot import _infer_scatter_meta_data, scatter + from .dataset_plot import _infer_scatter_meta_data, scatter kwargs['add_legend'] = False kwargs['discrete_legend'] = discrete_legend diff --git a/xarray/plot/plot.py b/xarray/plot/plot.py index 2cf19f4fb03..b444a84f7b6 100644 --- a/xarray/plot/plot.py +++ b/xarray/plot/plot.py @@ -9,49 +9,19 @@ import functools import warnings -from datetime import datetime import numpy as np import pandas as pd -from xarray.core.alignment import align from xarray.core.common import contains_cftime_datetimes from xarray.core.pycompat import basestring from .facetgrid import FacetGrid from .utils import ( - ROBUST_PERCENTILE, _determine_cmap_params, _infer_xy_labels, - _interval_to_double_bound_points, _interval_to_mid_points, - _resolve_intervals_2dplot, _valid_other_type, get_axis, - import_matplotlib_pyplot, label_from_attrs) - - -def _valid_numpy_subdtype(x, numpy_types): - """ - Is any dtype from numpy_types superior to the dtype of x? - """ - # If any of the types given in numpy_types is understood as numpy.generic, - # all possible x will be considered valid. This is probably unwanted. - for t in numpy_types: - assert not np.issubdtype(np.generic, t) - - return any(np.issubdtype(x.dtype, t) for t in numpy_types) - - -def _ensure_plottable(*args): - """ - Raise exception if there is anything in args that can't be plotted on an - axis by matplotlib. - """ - numpy_types = [np.floating, np.integer, np.timedelta64, np.datetime64] - other_types = [datetime] - - for x in args: - if not (_valid_numpy_subdtype(np.array(x), numpy_types) - or _valid_other_type(np.array(x), other_types)): - raise TypeError('Plotting requires coordinates to be numeric ' - 'or dates of type np.datetime64 or ' - 'datetime.datetime or pd.Interval.') + ROBUST_PERCENTILE, _determine_cmap_params, _ensure_plottable, + _infer_xy_labels, _interval_to_double_bound_points, + _interval_to_mid_points, _resolve_intervals_2dplot, _valid_other_type, + get_axis, import_matplotlib_pyplot, label_from_attrs) def _easy_facetgrid(darray, plotfunc, x, y, row=None, col=None, @@ -261,78 +231,6 @@ def _infer_line_data(darray, x, y, hue): return xplt, yplt, hueplt, xlabel, ylabel, hue_label -def _ensure_numeric(arr): - numpy_types = [np.floating, np.integer] - return _valid_numpy_subdtype(arr, numpy_types) - - -def _infer_scatter_meta_data(ds, x, y, hue, add_legend, discrete_legend): - dvars = set(ds.data_vars.keys()) - error_msg = (' must be either one of ({0:s})' - .format(', '.join(dvars))) - - if x not in dvars: - raise ValueError(x + error_msg) - - if y not in dvars: - raise ValueError(y + error_msg) - - if hue and add_legend is None: - add_legend = True - if add_legend and not hue: - raise ValueError('hue must be speicifed for generating a lengend') - - if hue and not _ensure_numeric(ds[hue].values): - if discrete_legend is None: - discrete_legend = True - elif discrete_legend is False: - raise ValueError('Cannot create a colorbar for a non numeric' - ' coordinate') - - dims = ds[x].dims - if ds[y].dims != dims: - raise ValueError('{} and {} must have the same dimensions.' - ''.format(x, y)) - - dims_coords = set(list(ds.coords) + list(ds.dims)) - if hue is not None and hue not in dims_coords: - raise ValueError(hue + ' must be either one of ({0:s})' - ''.format(', '.join(dims_coords))) - - if hue: - hue_label = label_from_attrs(ds.coords[hue]) - else: - hue_label = None - - return {'add_legend': add_legend, - 'discrete_legend': discrete_legend, - 'hue_label': hue_label, - 'xlabel': label_from_attrs(ds[x]), - 'ylabel': label_from_attrs(ds[y]), - 'hue_values': ds[x].coords[hue] if discrete_legend else None} - - -def _infer_scatter_data(ds, x, y, hue, discrete_legend): - dims = set(ds[x].dims) - if discrete_legend: - dims.remove(hue) - xplt = ds[x].stack(stackdim=dims).transpose('stackdim', hue).values - yplt = ds[y].stack(stackdim=dims).transpose('stackdim', hue).values - return {'x': xplt, 'y': yplt} - else: - data = {'x': ds[x].values.flatten(), - 'y': ds[y].values.flatten(), - 'color': None} - if hue: - # this is a hack to make a dataarray of the shape of ds[x] whose - # values are the coordinate hue. There's probably a better way - color = ds[x] - color[:] = 0 - color += ds.coords[hue] - data['color'] = color.values.flatten() - return data - - # This function signature should not change so that it can use # matplotlib format strings def line(darray, *args, **kwargs): @@ -1188,82 +1086,3 @@ def pcolormesh(x, y, z, ax, infer_intervals=None, **kwargs): ax.set_ylim(y[0], y[-1]) return primitive - - -def scatter(ds, x, y, hue=None, col=None, row=None, - col_wrap=None, sharex=True, sharey=True, aspect=None, - size=None, subplot_kws=None, add_legend=None, - discrete_legend=None, **kwargs): - - if kwargs.get('_meta_data', None): - discrete_legend = kwargs['_meta_data']['discrete_legend'] - else: - meta_data = _infer_scatter_meta_data(ds, x, y, hue, - add_legend, discrete_legend) - discrete_legend = meta_data['discrete_legend'] - add_legend = meta_data['add_legend'] - - if col or row: - ax = kwargs.pop('ax', None) - figsize = kwargs.pop('figsize', None) - if ax is not None: - raise ValueError("Can't use axes when making faceted plots.") - if aspect is None: - aspect = 1 - if size is None: - size = 3 - elif figsize is not None: - raise ValueError('cannot provide both `figsize` and ' - '`size` arguments') - - g = FacetGrid(data=ds, col=col, row=row, col_wrap=col_wrap, - sharex=sharex, sharey=sharey, figsize=figsize, - aspect=aspect, size=size, subplot_kws=subplot_kws) - return g.map_scatter(x=x, y=y, hue=hue, add_legend=add_legend, - discrete_legend=discrete_legend, **kwargs) - - data = _infer_scatter_data(ds, x, y, hue, discrete_legend) - - figsize = kwargs.pop('figsize', None) - ax = kwargs.pop('ax', None) - ax = get_axis(figsize, size, aspect, ax) - if discrete_legend: - primitive = ax.plot(data['x'], data['y'], '.') - else: - primitive = ax.scatter(data['x'], data['y'], c=data['color']) - if '_meta_data' in kwargs: # if this was called from map_scatter, - return primitive # finish here. Else, make labels - - if meta_data.get('xlabel', None): - ax.set_xlabel(meta_data.get('xlabel')) - - if meta_data.get('ylabel', None): - ax.set_ylabel(meta_data.get('ylabel')) - if add_legend and discrete_legend: - ax.legend(handles=primitive, - labels=list(meta_data['hue_values'].values), - title=meta_data.get('hue_label', None)) - if add_legend and not discrete_legend: - cbar = ax.figure.colorbar(primitive) - if meta_data.get('hue_label', None): - cbar.ax.set_ylabel(meta_data.get('hue_label')) - - return primitive - - -class _Dataset_PlotMethods(object): - """ - Enables use of xarray.plot functions as attributes on a Dataset. - For example, Dataset.plot.scatter - """ - - def __init__(self, dataset): - self._ds = dataset - - def __call__(self, *args, **kwargs): - raise ValueError('Dataset.plot cannot be called directly. Use' - 'an explicit plot method, e.g. ds.plot.scatter(...)') - - @functools.wraps(scatter) - def scatter(self, *args, **kwargs): - return scatter(self._ds, *args, **kwargs) diff --git a/xarray/plot/utils.py b/xarray/plot/utils.py index 41f61554739..179e2773480 100644 --- a/xarray/plot/utils.py +++ b/xarray/plot/utils.py @@ -4,6 +4,8 @@ import textwrap import warnings +from datetime import datetime + import numpy as np import pandas as pd @@ -450,3 +452,36 @@ def _valid_other_type(x, types): Do all elements of x have a type from types? """ return all(any(isinstance(el, t) for t in types) for el in np.ravel(x)) + + +def _valid_numpy_subdtype(x, numpy_types): + """ + Is any dtype from numpy_types superior to the dtype of x? + """ + # If any of the types given in numpy_types is understood as numpy.generic, + # all possible x will be considered valid. This is probably unwanted. + for t in numpy_types: + assert not np.issubdtype(np.generic, t) + + return any(np.issubdtype(x.dtype, t) for t in numpy_types) + + +def _ensure_plottable(*args): + """ + Raise exception if there is anything in args that can't be plotted on an + axis by matplotlib. + """ + numpy_types = [np.floating, np.integer, np.timedelta64, np.datetime64] + other_types = [datetime] + + for x in args: + if not (_valid_numpy_subdtype(np.array(x), numpy_types) + or _valid_other_type(np.array(x), other_types)): + raise TypeError('Plotting requires coordinates to be numeric ' + 'or dates of type np.datetime64 or ' + 'datetime.datetime or pd.Interval.') + + +def _ensure_numeric(arr): + numpy_types = [np.floating, np.integer] + return _valid_numpy_subdtype(arr, numpy_types) From b839295ad874d05dff2ed349fc62ade28083b32c Mon Sep 17 00:00:00 2001 From: dcherian Date: Thu, 22 Nov 2018 10:11:12 -0700 Subject: [PATCH 013/101] Fix tests. --- xarray/tests/test_plot.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/xarray/tests/test_plot.py b/xarray/tests/test_plot.py index 39fd55fece6..9eeff5027a0 100644 --- a/xarray/tests/test_plot.py +++ b/xarray/tests/test_plot.py @@ -1792,6 +1792,7 @@ def test_wrong_num_of_dimensions(self): class TestScatterPlots(PlotTestCase): + @pytest.fixture(autouse=True) def setUp(self): das = [DataArray(np.random.randn(3, 3, 4, 4), dims=['x', 'row', 'col', 'hue'], @@ -1837,13 +1838,15 @@ def test_figsize_and_size(self): with pytest.raises(ValueError): self.ds.plot.scatter(x='A', y='B', row='row', size=3, figsize=4) - def test_bad_args(self): + @pytest.mark.parametrize('x, y, add_legend', [ + ('A', 'B', True), + ('A', 'The Spanish Inquisition', None), + ('The Spanish Inquisition', 'B', None)]) + def test_bad_args(self, x, y, add_legend): with pytest.raises(ValueError): - self.ds.plot.scatter(x='A', y='B', add_legend=True) - self.ds.plot.scatter(x='A', y='The Spanish Inquisition') - self.ds.plot.scatter(x='The Spanish Inquisition', y='B') + self.ds.plot.scatter(x, y, add_legend=add_legend) - def test_non_numeric_legened(self): + def test_non_numeric_legend(self): self.ds['hue'] = pd.date_range('2000-01-01', periods=4) lines = self.ds.plot.scatter(x='A', y='B', hue='hue') # should make a discrete legend @@ -1853,7 +1856,7 @@ def test_non_numeric_legened(self): self.ds.plot.scatter(x='A', y='B', hue='hue', discrete_legend=False) - def test_add_legened_by_default(self): + def test_add_legend_by_default(self): sc = self.ds.plot.scatter(x='A', y='B', hue='hue') assert len(sc.figure.axes) == 2 From 746930bb2c7f7ec00035186ce3b7280fb5c5783e Mon Sep 17 00:00:00 2001 From: dcherian Date: Thu, 22 Nov 2018 10:54:29 -0700 Subject: [PATCH 014/101] Fixes. --- xarray/plot/dataset_plot.py | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/xarray/plot/dataset_plot.py b/xarray/plot/dataset_plot.py index 878b00c4f5d..c77e57a3fb3 100644 --- a/xarray/plot/dataset_plot.py +++ b/xarray/plot/dataset_plot.py @@ -6,12 +6,12 @@ import numpy as np import pandas as pd +from ..core.common import ones_like from .facetgrid import FacetGrid from .utils import ( ROBUST_PERCENTILE, _determine_cmap_params, _ensure_numeric, _interval_to_double_bound_points, _interval_to_mid_points, - _valid_other_type, get_axis, - import_matplotlib_pyplot, label_from_attrs) + _valid_other_type, get_axis, import_matplotlib_pyplot, label_from_attrs) def _infer_scatter_meta_data(ds, x, y, hue, add_legend, discrete_legend): @@ -28,7 +28,7 @@ def _infer_scatter_meta_data(ds, x, y, hue, add_legend, discrete_legend): if hue and add_legend is None: add_legend = True if add_legend and not hue: - raise ValueError('hue must be speicifed for generating a lengend') + raise ValueError('hue must be specified for generating a legend') if hue and not _ensure_numeric(ds[hue].values): if discrete_legend is None: @@ -72,12 +72,8 @@ def _infer_scatter_data(ds, x, y, hue, discrete_legend): 'y': ds[y].values.flatten(), 'color': None} if hue: - # this is a hack to make a dataarray of the shape of ds[x] whose - # values are the coordinate hue. There's probably a better way - color = ds[x] - color[:] = 0 - color += ds.coords[hue] - data['color'] = color.values.flatten() + data['color'] = ((ones_like(ds[x]) * ds.coords[hue]) + .values.flatten()) return data @@ -104,7 +100,7 @@ def scatter(ds, x, y, hue=None, col=None, row=None, if size is None: size = 3 elif figsize is not None: - raise ValueError('cannot provide both `figsize` and ' + raise ValueError('Cannot provide both `figsize` and ' '`size` arguments') g = FacetGrid(data=ds, col=col, row=row, col_wrap=col_wrap, From d3e1308fe351e92d029e4b3be46faa196c4aac0f Mon Sep 17 00:00:00 2001 From: dcherian Date: Thu, 22 Nov 2018 12:22:34 -0700 Subject: [PATCH 015/101] =?UTF-8?q?discrete=5Flegend=20=E2=86=92=20add=5Fc?= =?UTF-8?q?olorbar?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- xarray/plot/dataset_plot.py | 66 ++++++++++++++++++++++++------------- xarray/plot/facetgrid.py | 12 +++---- xarray/tests/test_plot.py | 15 ++++++++- 3 files changed, 63 insertions(+), 30 deletions(-) diff --git a/xarray/plot/dataset_plot.py b/xarray/plot/dataset_plot.py index c77e57a3fb3..3f0b08a0a59 100644 --- a/xarray/plot/dataset_plot.py +++ b/xarray/plot/dataset_plot.py @@ -14,7 +14,7 @@ _valid_other_type, get_axis, import_matplotlib_pyplot, label_from_attrs) -def _infer_scatter_meta_data(ds, x, y, hue, add_legend, discrete_legend): +def _infer_scatter_meta_data(ds, x, y, hue, add_legend, add_colorbar): dvars = set(ds.data_vars.keys()) error_msg = (' must be either one of ({0:s})' .format(', '.join(dvars))) @@ -25,18 +25,38 @@ def _infer_scatter_meta_data(ds, x, y, hue, add_legend, discrete_legend): if y not in dvars: raise ValueError(y + error_msg) - if hue and add_legend is None: - add_legend = True - if add_legend and not hue: - raise ValueError('hue must be specified for generating a legend') - - if hue and not _ensure_numeric(ds[hue].values): - if discrete_legend is None: - discrete_legend = True - elif discrete_legend is False: + if hue: + if add_legend is None and add_colorbar is None: + if not _ensure_numeric(ds[hue].values): + add_legend = True + add_colorbar = False + else: + add_legend = False + add_colorbar = True + + if add_colorbar is None: + if add_legend is True: + add_colorbar = False + else: + if _ensure_numeric(ds[hue].values): + add_colorbar = True + else: + add_colorbar = False + + elif add_legend is None: + if add_colorbar is True: + add_legend = False + else: + add_legend = True + + elif add_colorbar is True and not _ensure_numeric(ds[hue].values): raise ValueError('Cannot create a colorbar for a non numeric' ' coordinate') + elif add_legend or add_colorbar: + raise ValueError('hue must be specified for generating a legend' + ' or colorbar') + dims = ds[x].dims if ds[y].dims != dims: raise ValueError('{} and {} must have the same dimensions.' @@ -53,16 +73,16 @@ def _infer_scatter_meta_data(ds, x, y, hue, add_legend, discrete_legend): hue_label = None return {'add_legend': add_legend, - 'discrete_legend': discrete_legend, + 'add_colorbar': add_colorbar, 'hue_label': hue_label, 'xlabel': label_from_attrs(ds[x]), 'ylabel': label_from_attrs(ds[y]), - 'hue_values': ds[x].coords[hue] if discrete_legend else None} + 'hue_values': ds[x].coords[hue] if add_legend else None} -def _infer_scatter_data(ds, x, y, hue, discrete_legend): +def _infer_scatter_data(ds, x, y, hue, add_legend): dims = set(ds[x].dims) - if discrete_legend: + if add_legend: dims.remove(hue) xplt = ds[x].stack(stackdim=dims).transpose('stackdim', hue).values yplt = ds[y].stack(stackdim=dims).transpose('stackdim', hue).values @@ -80,14 +100,14 @@ def _infer_scatter_data(ds, x, y, hue, discrete_legend): def scatter(ds, x, y, hue=None, col=None, row=None, col_wrap=None, sharex=True, sharey=True, aspect=None, size=None, subplot_kws=None, add_legend=None, - discrete_legend=None, **kwargs): + add_colorbar=None, **kwargs): if kwargs.get('_meta_data', None): - discrete_legend = kwargs['_meta_data']['discrete_legend'] + add_colorbar = kwargs['_meta_data']['add_colorbar'] else: meta_data = _infer_scatter_meta_data(ds, x, y, hue, - add_legend, discrete_legend) - discrete_legend = meta_data['discrete_legend'] + add_legend, add_colorbar) + add_colorbar = meta_data['add_colorbar'] add_legend = meta_data['add_legend'] if col or row: @@ -107,14 +127,14 @@ def scatter(ds, x, y, hue=None, col=None, row=None, sharex=sharex, sharey=sharey, figsize=figsize, aspect=aspect, size=size, subplot_kws=subplot_kws) return g.map_scatter(x=x, y=y, hue=hue, add_legend=add_legend, - discrete_legend=discrete_legend, **kwargs) + add_colorbar=add_colorbar, **kwargs) - data = _infer_scatter_data(ds, x, y, hue, discrete_legend) + data = _infer_scatter_data(ds, x, y, hue, add_legend) figsize = kwargs.pop('figsize', None) ax = kwargs.pop('ax', None) ax = get_axis(figsize, size, aspect, ax) - if discrete_legend: + if add_legend: primitive = ax.plot(data['x'], data['y'], '.') else: primitive = ax.scatter(data['x'], data['y'], c=data['color']) @@ -126,11 +146,11 @@ def scatter(ds, x, y, hue=None, col=None, row=None, if meta_data.get('ylabel', None): ax.set_ylabel(meta_data.get('ylabel')) - if add_legend and discrete_legend: + if add_legend: ax.legend(handles=primitive, labels=list(meta_data['hue_values'].values), title=meta_data.get('hue_label', None)) - if add_legend and not discrete_legend: + if add_colorbar: cbar = ax.figure.colorbar(primitive) if meta_data.get('hue_label', None): cbar.ax.set_ylabel(meta_data.get('hue_label')) diff --git a/xarray/plot/facetgrid.py b/xarray/plot/facetgrid.py index 1c0ef9d73f2..e884c4b87bd 100644 --- a/xarray/plot/facetgrid.py +++ b/xarray/plot/facetgrid.py @@ -314,14 +314,14 @@ def map_dataarray_line(self, x=None, y=None, hue=None, **kwargs): return self - def map_scatter(self, x=None, y=None, hue=None, discrete_legend=False, + def map_scatter(self, x=None, y=None, hue=None, add_colorbar=False, add_legend=None, **kwargs): from .dataset_plot import _infer_scatter_meta_data, scatter kwargs['add_legend'] = False - kwargs['discrete_legend'] = discrete_legend + kwargs['add_colorbar'] = add_colorbar meta_data = _infer_scatter_meta_data(self.data, x, y, hue, - add_legend, discrete_legend) + add_legend, add_colorbar) kwargs['_meta_data'] = meta_data for d, ax in zip(self.name_dicts.flat, self.axes.flat): # None is the sentinel value @@ -333,12 +333,12 @@ def map_scatter(self, x=None, y=None, hue=None, discrete_legend=False, self._finalize_grid(meta_data['xlabel'], meta_data['ylabel']) - if hue and meta_data['add_legend']: + if hue and (meta_data['add_legend'] or meta_data['add_colorbar']): self._hue_label = meta_data.pop('hue_label', None) - if meta_data['discrete_legend']: + if meta_data['add_legend']: self._hue_var = meta_data['hue_values'] self.add_legend() - else: + elif meta_data['add_colorbar']: self.add_colorbar(label=self._hue_label) return self diff --git a/xarray/tests/test_plot.py b/xarray/tests/test_plot.py index 9eeff5027a0..b9d2b756bf2 100644 --- a/xarray/tests/test_plot.py +++ b/xarray/tests/test_plot.py @@ -1846,6 +1846,19 @@ def test_bad_args(self, x, y, add_legend): with pytest.raises(ValueError): self.ds.plot.scatter(x, y, add_legend=add_legend) + @pytest.mark.parametrize( + 'add_legend, add_colorbar, expected_legend, expected_colorbar', + [(None, None, False, True), + (None, True, False, True), + (True, None, True, False)]) + def test_infer_scatter_meta_data(self, add_legend, add_colorbar, + expected_legend, expected_colorbar): + meta_data = xr.plot.dataset_plot._infer_scatter_meta_data( + self.ds, 'A', 'B', 'hue', add_legend, add_colorbar + ) + assert meta_data['add_legend'] == expected_legend + assert meta_data['add_colorbar'] == expected_colorbar + def test_non_numeric_legend(self): self.ds['hue'] = pd.date_range('2000-01-01', periods=4) lines = self.ds.plot.scatter(x='A', y='B', hue='hue') @@ -1854,7 +1867,7 @@ def test_non_numeric_legend(self): # and raise an error if explicitly not allowed to do so with pytest.raises(ValueError): self.ds.plot.scatter(x='A', y='B', hue='hue', - discrete_legend=False) + add_colorbar=True) def test_add_legend_by_default(self): sc = self.ds.plot.scatter(x='A', y='B', hue='hue') From ef3b9d1b55cb163d7791d9a6ec6c9d40849b18e9 Mon Sep 17 00:00:00 2001 From: dcherian Date: Thu, 22 Nov 2018 12:54:34 -0700 Subject: [PATCH 016/101] =?UTF-8?q?Revert=20"discrete=5Flegend=20=E2=86=92?= =?UTF-8?q?=20add=5Fcolorbar"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit d3e1308fe351e92d029e4b3be46faa196c4aac0f. --- xarray/plot/dataset_plot.py | 66 +++++++++++++------------------------ xarray/plot/facetgrid.py | 12 +++---- xarray/tests/test_plot.py | 15 +-------- 3 files changed, 30 insertions(+), 63 deletions(-) diff --git a/xarray/plot/dataset_plot.py b/xarray/plot/dataset_plot.py index 3f0b08a0a59..c77e57a3fb3 100644 --- a/xarray/plot/dataset_plot.py +++ b/xarray/plot/dataset_plot.py @@ -14,7 +14,7 @@ _valid_other_type, get_axis, import_matplotlib_pyplot, label_from_attrs) -def _infer_scatter_meta_data(ds, x, y, hue, add_legend, add_colorbar): +def _infer_scatter_meta_data(ds, x, y, hue, add_legend, discrete_legend): dvars = set(ds.data_vars.keys()) error_msg = (' must be either one of ({0:s})' .format(', '.join(dvars))) @@ -25,38 +25,18 @@ def _infer_scatter_meta_data(ds, x, y, hue, add_legend, add_colorbar): if y not in dvars: raise ValueError(y + error_msg) - if hue: - if add_legend is None and add_colorbar is None: - if not _ensure_numeric(ds[hue].values): - add_legend = True - add_colorbar = False - else: - add_legend = False - add_colorbar = True - - if add_colorbar is None: - if add_legend is True: - add_colorbar = False - else: - if _ensure_numeric(ds[hue].values): - add_colorbar = True - else: - add_colorbar = False - - elif add_legend is None: - if add_colorbar is True: - add_legend = False - else: - add_legend = True - - elif add_colorbar is True and not _ensure_numeric(ds[hue].values): + if hue and add_legend is None: + add_legend = True + if add_legend and not hue: + raise ValueError('hue must be specified for generating a legend') + + if hue and not _ensure_numeric(ds[hue].values): + if discrete_legend is None: + discrete_legend = True + elif discrete_legend is False: raise ValueError('Cannot create a colorbar for a non numeric' ' coordinate') - elif add_legend or add_colorbar: - raise ValueError('hue must be specified for generating a legend' - ' or colorbar') - dims = ds[x].dims if ds[y].dims != dims: raise ValueError('{} and {} must have the same dimensions.' @@ -73,16 +53,16 @@ def _infer_scatter_meta_data(ds, x, y, hue, add_legend, add_colorbar): hue_label = None return {'add_legend': add_legend, - 'add_colorbar': add_colorbar, + 'discrete_legend': discrete_legend, 'hue_label': hue_label, 'xlabel': label_from_attrs(ds[x]), 'ylabel': label_from_attrs(ds[y]), - 'hue_values': ds[x].coords[hue] if add_legend else None} + 'hue_values': ds[x].coords[hue] if discrete_legend else None} -def _infer_scatter_data(ds, x, y, hue, add_legend): +def _infer_scatter_data(ds, x, y, hue, discrete_legend): dims = set(ds[x].dims) - if add_legend: + if discrete_legend: dims.remove(hue) xplt = ds[x].stack(stackdim=dims).transpose('stackdim', hue).values yplt = ds[y].stack(stackdim=dims).transpose('stackdim', hue).values @@ -100,14 +80,14 @@ def _infer_scatter_data(ds, x, y, hue, add_legend): def scatter(ds, x, y, hue=None, col=None, row=None, col_wrap=None, sharex=True, sharey=True, aspect=None, size=None, subplot_kws=None, add_legend=None, - add_colorbar=None, **kwargs): + discrete_legend=None, **kwargs): if kwargs.get('_meta_data', None): - add_colorbar = kwargs['_meta_data']['add_colorbar'] + discrete_legend = kwargs['_meta_data']['discrete_legend'] else: meta_data = _infer_scatter_meta_data(ds, x, y, hue, - add_legend, add_colorbar) - add_colorbar = meta_data['add_colorbar'] + add_legend, discrete_legend) + discrete_legend = meta_data['discrete_legend'] add_legend = meta_data['add_legend'] if col or row: @@ -127,14 +107,14 @@ def scatter(ds, x, y, hue=None, col=None, row=None, sharex=sharex, sharey=sharey, figsize=figsize, aspect=aspect, size=size, subplot_kws=subplot_kws) return g.map_scatter(x=x, y=y, hue=hue, add_legend=add_legend, - add_colorbar=add_colorbar, **kwargs) + discrete_legend=discrete_legend, **kwargs) - data = _infer_scatter_data(ds, x, y, hue, add_legend) + data = _infer_scatter_data(ds, x, y, hue, discrete_legend) figsize = kwargs.pop('figsize', None) ax = kwargs.pop('ax', None) ax = get_axis(figsize, size, aspect, ax) - if add_legend: + if discrete_legend: primitive = ax.plot(data['x'], data['y'], '.') else: primitive = ax.scatter(data['x'], data['y'], c=data['color']) @@ -146,11 +126,11 @@ def scatter(ds, x, y, hue=None, col=None, row=None, if meta_data.get('ylabel', None): ax.set_ylabel(meta_data.get('ylabel')) - if add_legend: + if add_legend and discrete_legend: ax.legend(handles=primitive, labels=list(meta_data['hue_values'].values), title=meta_data.get('hue_label', None)) - if add_colorbar: + if add_legend and not discrete_legend: cbar = ax.figure.colorbar(primitive) if meta_data.get('hue_label', None): cbar.ax.set_ylabel(meta_data.get('hue_label')) diff --git a/xarray/plot/facetgrid.py b/xarray/plot/facetgrid.py index e884c4b87bd..1c0ef9d73f2 100644 --- a/xarray/plot/facetgrid.py +++ b/xarray/plot/facetgrid.py @@ -314,14 +314,14 @@ def map_dataarray_line(self, x=None, y=None, hue=None, **kwargs): return self - def map_scatter(self, x=None, y=None, hue=None, add_colorbar=False, + def map_scatter(self, x=None, y=None, hue=None, discrete_legend=False, add_legend=None, **kwargs): from .dataset_plot import _infer_scatter_meta_data, scatter kwargs['add_legend'] = False - kwargs['add_colorbar'] = add_colorbar + kwargs['discrete_legend'] = discrete_legend meta_data = _infer_scatter_meta_data(self.data, x, y, hue, - add_legend, add_colorbar) + add_legend, discrete_legend) kwargs['_meta_data'] = meta_data for d, ax in zip(self.name_dicts.flat, self.axes.flat): # None is the sentinel value @@ -333,12 +333,12 @@ def map_scatter(self, x=None, y=None, hue=None, add_colorbar=False, self._finalize_grid(meta_data['xlabel'], meta_data['ylabel']) - if hue and (meta_data['add_legend'] or meta_data['add_colorbar']): + if hue and meta_data['add_legend']: self._hue_label = meta_data.pop('hue_label', None) - if meta_data['add_legend']: + if meta_data['discrete_legend']: self._hue_var = meta_data['hue_values'] self.add_legend() - elif meta_data['add_colorbar']: + else: self.add_colorbar(label=self._hue_label) return self diff --git a/xarray/tests/test_plot.py b/xarray/tests/test_plot.py index b9d2b756bf2..9eeff5027a0 100644 --- a/xarray/tests/test_plot.py +++ b/xarray/tests/test_plot.py @@ -1846,19 +1846,6 @@ def test_bad_args(self, x, y, add_legend): with pytest.raises(ValueError): self.ds.plot.scatter(x, y, add_legend=add_legend) - @pytest.mark.parametrize( - 'add_legend, add_colorbar, expected_legend, expected_colorbar', - [(None, None, False, True), - (None, True, False, True), - (True, None, True, False)]) - def test_infer_scatter_meta_data(self, add_legend, add_colorbar, - expected_legend, expected_colorbar): - meta_data = xr.plot.dataset_plot._infer_scatter_meta_data( - self.ds, 'A', 'B', 'hue', add_legend, add_colorbar - ) - assert meta_data['add_legend'] == expected_legend - assert meta_data['add_colorbar'] == expected_colorbar - def test_non_numeric_legend(self): self.ds['hue'] = pd.date_range('2000-01-01', periods=4) lines = self.ds.plot.scatter(x='A', y='B', hue='hue') @@ -1867,7 +1854,7 @@ def test_non_numeric_legend(self): # and raise an error if explicitly not allowed to do so with pytest.raises(ValueError): self.ds.plot.scatter(x='A', y='B', hue='hue', - add_colorbar=True) + discrete_legend=False) def test_add_legend_by_default(self): sc = self.ds.plot.scatter(x='A', y='B', hue='hue') From be9d09ae166da9e76d54f1e080e90b772009fab4 Mon Sep 17 00:00:00 2001 From: dcherian Date: Sat, 15 Dec 2018 15:03:40 -0800 Subject: [PATCH 017/101] Only use scatter instead of alternating between scatter and plot. --- xarray/plot/dataset_plot.py | 33 +++++++++++++++------------------ 1 file changed, 15 insertions(+), 18 deletions(-) diff --git a/xarray/plot/dataset_plot.py b/xarray/plot/dataset_plot.py index c77e57a3fb3..065dfb6d726 100644 --- a/xarray/plot/dataset_plot.py +++ b/xarray/plot/dataset_plot.py @@ -60,21 +60,15 @@ def _infer_scatter_meta_data(ds, x, y, hue, add_legend, discrete_legend): 'hue_values': ds[x].coords[hue] if discrete_legend else None} -def _infer_scatter_data(ds, x, y, hue, discrete_legend): - dims = set(ds[x].dims) - if discrete_legend: - dims.remove(hue) - xplt = ds[x].stack(stackdim=dims).transpose('stackdim', hue).values - yplt = ds[y].stack(stackdim=dims).transpose('stackdim', hue).values - return {'x': xplt, 'y': yplt} - else: - data = {'x': ds[x].values.flatten(), - 'y': ds[y].values.flatten(), - 'color': None} - if hue: - data['color'] = ((ones_like(ds[x]) * ds.coords[hue]) - .values.flatten()) - return data +def _infer_scatter_data(ds, x, y, hue): + + data = {'x': ds[x].values.flatten(), + 'y': ds[y].values.flatten(), + 'color': None} + if hue: + data['color'] = ((ones_like(ds[x]) * ds.coords[hue]) + .values.flatten()) + return data def scatter(ds, x, y, hue=None, col=None, row=None, @@ -109,15 +103,18 @@ def scatter(ds, x, y, hue=None, col=None, row=None, return g.map_scatter(x=x, y=y, hue=hue, add_legend=add_legend, discrete_legend=discrete_legend, **kwargs) - data = _infer_scatter_data(ds, x, y, hue, discrete_legend) - figsize = kwargs.pop('figsize', None) ax = kwargs.pop('ax', None) ax = get_axis(figsize, size, aspect, ax) if discrete_legend: - primitive = ax.plot(data['x'], data['y'], '.') + primitive = [] + for label, grp in ds.groupby(ds[hue]): + data = _infer_scatter_data(grp, x, y, hue=None) + primitive.append(ax.scatter(data['x'], data['y'], label=label)) else: + data = _infer_scatter_data(ds, x, y, hue) primitive = ax.scatter(data['x'], data['y'], c=data['color']) + if '_meta_data' in kwargs: # if this was called from map_scatter, return primitive # finish here. Else, make labels From 0d2b126a58bd4e31701b7f83c7cdfe138af2d011 Mon Sep 17 00:00:00 2001 From: dcherian Date: Sat, 15 Dec 2018 15:38:30 -0800 Subject: [PATCH 018/101] Create and use plot.utils._add_colorbar --- xarray/plot/dataset_plot.py | 98 ++++++++++++++++++++++++++++++++++--- xarray/plot/plot.py | 15 +++--- xarray/plot/utils.py | 14 ++++++ 3 files changed, 111 insertions(+), 16 deletions(-) diff --git a/xarray/plot/dataset_plot.py b/xarray/plot/dataset_plot.py index 065dfb6d726..7f5c345c595 100644 --- a/xarray/plot/dataset_plot.py +++ b/xarray/plot/dataset_plot.py @@ -9,7 +9,7 @@ from ..core.common import ones_like from .facetgrid import FacetGrid from .utils import ( - ROBUST_PERCENTILE, _determine_cmap_params, _ensure_numeric, + ROBUST_PERCENTILE, _add_colorbar, _determine_cmap_params, _ensure_numeric, _interval_to_double_bound_points, _interval_to_mid_points, _valid_other_type, get_axis, import_matplotlib_pyplot, label_from_attrs) @@ -73,8 +73,75 @@ def _infer_scatter_data(ds, x, y, hue): def scatter(ds, x, y, hue=None, col=None, row=None, col_wrap=None, sharex=True, sharey=True, aspect=None, - size=None, subplot_kws=None, add_legend=None, - discrete_legend=None, **kwargs): + size=None, subplot_kws=None, add_legend=None, cbar_kwargs=None, + discrete_legend=None, cbar_ax=None, vmin=None, vmax=None, + norm=None, infer_intervals=None, center=None, levels=None, + robust=None, colors=None, extend=None, cmap=None, **kwargs): + ''' + Inputs + ------ + + ds : Dataset + x, y : string + Variable names for x, y axis. + hue: str, optional + Variable by which to color scattered points + 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 + col_wrap : integer, optional + Use together with ``col`` to wrap faceted plots + ax : matplotlib axes, optional + If None, uses the current axis. Not applicable when using facets. + subplot_kws : dict, optional + Dictionary of keyword arguments for matplotlib subplots. Only applies + to FacetGrid plotting. + 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``. + norm : ``matplotlib.colors.Normalize`` instance, optional + If the ``norm`` has vmin or vmax specified, the corresponding kwarg + must be None. + vmin, vmax : floats, optional + Values to anchor the colormap, otherwise they are inferred from the + data and other keyword arguments. When a diverging dataset is inferred, + setting one of these values will fix the other by symmetry around + ``center``. Setting both values prevents use of a diverging colormap. + If discrete levels are provided as an explicit list, both of these + values are ignored. + cmap : matplotlib colormap name or object, optional + The mapping from data values to color space. If not provided, this + will be either be ``viridis`` (if the function infers a sequential + dataset) or ``RdBu_r`` (if the function infers a diverging dataset). + When `Seaborn` is installed, ``cmap`` may also be a `seaborn` + color palette. If ``cmap`` is seaborn color palette and the plot type + is not ``contour`` or ``contourf``, ``levels`` must also be specified. + colors : discrete colors to plot, optional + A single color or a list of colors. If the plot type is not ``contour`` + or ``contourf``, the ``levels`` argument is required. + center : float, optional + The value at which to center the colormap. Passing this value implies + use of a diverging colormap. Setting it to ``False`` prevents use of a + diverging colormap. + robust : bool, optional + If True and ``vmin`` or ``vmax`` are absent, the colormap range is + computed with 2nd and 98th percentiles instead of the extreme values. + extend : {'neither', 'both', 'min', 'max'}, optional + How to draw arrows extending the colorbar beyond its limits. If not + provided, extend is inferred from vmin, vmax and the data limits. + levels : int or list-like object, optional + Split the colormap (cmap) into discrete color intervals. If an integer + is provided, "nice" levels are chosen based on the data range: this can + imply that the final number of levels is not exactly the expected one. + Setting ``vmin`` and/or ``vmax`` with ``levels=N`` is equivalent to + setting ``levels=np.linspace(vmin, vmax, N)``. + **kwargs : optional + Additional keyword arguments to matplotlib + ''' if kwargs.get('_meta_data', None): discrete_legend = kwargs['_meta_data']['discrete_legend'] @@ -84,6 +151,8 @@ def scatter(ds, x, y, hue=None, col=None, row=None, discrete_legend = meta_data['discrete_legend'] add_legend = meta_data['add_legend'] + plt = import_matplotlib_pyplot() + if col or row: ax = kwargs.pop('ax', None) figsize = kwargs.pop('figsize', None) @@ -113,7 +182,21 @@ def scatter(ds, x, y, hue=None, col=None, row=None, primitive.append(ax.scatter(data['x'], data['y'], label=label)) else: data = _infer_scatter_data(ds, x, y, hue) - primitive = ax.scatter(data['x'], data['y'], c=data['color']) + cmap_kwargs = {'plot_data': ds[hue], + 'vmin': vmin, + 'vmax': vmax, + 'cmap': colors if colors else cmap, + 'center': center, + 'robust': robust, + 'extend': extend, + 'levels': levels, + 'filled': None, + 'norm': norm, + } + cmap_params = _determine_cmap_params(**cmap_kwargs) + primitive = ax.scatter(data['x'], data['y'], c=data['color'], + vmin=cmap_kwargs['vmin'], + vmax=cmap_kwargs['vmax']) if '_meta_data' in kwargs: # if this was called from map_scatter, return primitive # finish here. Else, make labels @@ -128,9 +211,10 @@ def scatter(ds, x, y, hue=None, col=None, row=None, labels=list(meta_data['hue_values'].values), title=meta_data.get('hue_label', None)) if add_legend and not discrete_legend: - cbar = ax.figure.colorbar(primitive) - if meta_data.get('hue_label', None): - cbar.ax.set_ylabel(meta_data.get('hue_label')) + cbar_kwargs = {} if cbar_kwargs is None else cbar_kwargs + if 'label' not in cbar_kwargs: + cbar_kwargs['label'] = meta_data.get('hue_label', None) + _add_colorbar(primitive, ax, cbar_ax, cbar_kwargs, cmap_params) return primitive diff --git a/xarray/plot/plot.py b/xarray/plot/plot.py index b444a84f7b6..a650722515d 100644 --- a/xarray/plot/plot.py +++ b/xarray/plot/plot.py @@ -18,7 +18,7 @@ from .facetgrid import FacetGrid from .utils import ( - ROBUST_PERCENTILE, _determine_cmap_params, _ensure_plottable, + ROBUST_PERCENTILE, _add_colorbar, _determine_cmap_params, _ensure_plottable, _infer_xy_labels, _interval_to_double_bound_points, _interval_to_mid_points, _resolve_intervals_2dplot, _valid_other_type, get_axis, import_matplotlib_pyplot, label_from_attrs) @@ -831,15 +831,12 @@ def newplotfunc(darray, x=None, y=None, figsize=None, size=None, ax.set_title(darray._title_for_slice()) if add_colorbar: - cbar_kwargs = {} if cbar_kwargs is None else dict(cbar_kwargs) - cbar_kwargs.setdefault('extend', cmap_params['extend']) - if cbar_ax is None: - cbar_kwargs.setdefault('ax', ax) - else: - cbar_kwargs.setdefault('cax', cbar_ax) - cbar = plt.colorbar(primitive, **cbar_kwargs) if add_labels and 'label' not in cbar_kwargs: - cbar.set_label(label_from_attrs(darray)) + cbar_kwargs['label'] = label_from_attrs(darray) + + cbar = _add_colorbar(primitive, ax, cbar_ax, cbar_kwargs, + cmap_params) + elif cbar_ax is not None or cbar_kwargs is not None: # inform the user about keywords which aren't used raise ValueError("cbar_ax and cbar_kwargs can't be used with " diff --git a/xarray/plot/utils.py b/xarray/plot/utils.py index 179e2773480..943b454bc87 100644 --- a/xarray/plot/utils.py +++ b/xarray/plot/utils.py @@ -485,3 +485,17 @@ def _ensure_plottable(*args): def _ensure_numeric(arr): numpy_types = [np.floating, np.integer] return _valid_numpy_subdtype(arr, numpy_types) + + +def _add_colorbar(primitive, ax, cbar_ax, cbar_kwargs, cmap_params): + plt = import_matplotlib_pyplot() + cbar_kwargs = {} if cbar_kwargs is None else dict(cbar_kwargs) + cbar_kwargs.setdefault('extend', cmap_params['extend']) + if cbar_ax is None: + cbar_kwargs.setdefault('ax', ax) + else: + cbar_kwargs.setdefault('cax', cbar_ax) + + cbar = plt.colorbar(primitive, **cbar_kwargs) + + return cbar From a938d244d011bb4c2e94e663ed233de82037e713 Mon Sep 17 00:00:00 2001 From: dcherian Date: Sat, 15 Dec 2018 15:44:13 -0800 Subject: [PATCH 019/101] fix tests. --- xarray/plot/plot.py | 2 +- xarray/plot/utils.py | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/xarray/plot/plot.py b/xarray/plot/plot.py index a650722515d..0e44f75d6c6 100644 --- a/xarray/plot/plot.py +++ b/xarray/plot/plot.py @@ -831,9 +831,9 @@ def newplotfunc(darray, x=None, y=None, figsize=None, size=None, ax.set_title(darray._title_for_slice()) if add_colorbar: + cbar_kwargs = {} if cbar_kwargs is None else dict(cbar_kwargs) if add_labels and 'label' not in cbar_kwargs: cbar_kwargs['label'] = label_from_attrs(darray) - cbar = _add_colorbar(primitive, ax, cbar_ax, cbar_kwargs, cmap_params) diff --git a/xarray/plot/utils.py b/xarray/plot/utils.py index 943b454bc87..016c6b5b395 100644 --- a/xarray/plot/utils.py +++ b/xarray/plot/utils.py @@ -489,7 +489,6 @@ def _ensure_numeric(arr): def _add_colorbar(primitive, ax, cbar_ax, cbar_kwargs, cmap_params): plt = import_matplotlib_pyplot() - cbar_kwargs = {} if cbar_kwargs is None else dict(cbar_kwargs) cbar_kwargs.setdefault('extend', cmap_params['extend']) if cbar_ax is None: cbar_kwargs.setdefault('ax', ax) From 6440365af15b0fa7d937df37eb90739868794ee6 Mon Sep 17 00:00:00 2001 From: dcherian Date: Sat, 15 Dec 2018 16:05:04 -0800 Subject: [PATCH 020/101] More fixes to hue, cmap_kwargs. --- xarray/plot/dataset_plot.py | 51 ++++++++++++++++++++++++------------- xarray/plot/facetgrid.py | 10 +++++--- xarray/tests/test_plot.py | 2 +- 3 files changed, 41 insertions(+), 22 deletions(-) diff --git a/xarray/plot/dataset_plot.py b/xarray/plot/dataset_plot.py index 7f5c345c595..80400d3fdb2 100644 --- a/xarray/plot/dataset_plot.py +++ b/xarray/plot/dataset_plot.py @@ -30,13 +30,16 @@ def _infer_scatter_meta_data(ds, x, y, hue, add_legend, discrete_legend): if add_legend and not hue: raise ValueError('hue must be specified for generating a legend') - if hue and not _ensure_numeric(ds[hue].values): + if hue and add_legend and not _ensure_numeric(ds[hue].values): if discrete_legend is None: discrete_legend = True elif discrete_legend is False: raise ValueError('Cannot create a colorbar for a non numeric' ' coordinate') + if not hue and add_legend is None: + discrete_legend = None + dims = ds[x].dims if ds[y].dims != dims: raise ValueError('{} and {} must have the same dimensions.' @@ -49,15 +52,17 @@ def _infer_scatter_meta_data(ds, x, y, hue, add_legend, discrete_legend): if hue: hue_label = label_from_attrs(ds.coords[hue]) + hue_values = ds[x].coords[hue] if discrete_legend else None else: hue_label = None + hue_values = None return {'add_legend': add_legend, 'discrete_legend': discrete_legend, 'hue_label': hue_label, 'xlabel': label_from_attrs(ds[x]), 'ylabel': label_from_attrs(ds[y]), - 'hue_values': ds[x].coords[hue] if discrete_legend else None} + 'hue_values': hue_values} def _infer_scatter_data(ds, x, y, hue): @@ -175,30 +180,40 @@ def scatter(ds, x, y, hue=None, col=None, row=None, figsize = kwargs.pop('figsize', None) ax = kwargs.pop('ax', None) ax = get_axis(figsize, size, aspect, ax) + + kwargs = kwargs.copy() + _meta_data = kwargs.pop('_meta_data', None) + if discrete_legend: primitive = [] for label, grp in ds.groupby(ds[hue]): data = _infer_scatter_data(grp, x, y, hue=None) - primitive.append(ax.scatter(data['x'], data['y'], label=label)) + primitive.append(ax.scatter(data['x'], data['y'], label=label, + **kwargs)) else: data = _infer_scatter_data(ds, x, y, hue) - cmap_kwargs = {'plot_data': ds[hue], - 'vmin': vmin, - 'vmax': vmax, - 'cmap': colors if colors else cmap, - 'center': center, - 'robust': robust, - 'extend': extend, - 'levels': levels, - 'filled': None, - 'norm': norm, - } - cmap_params = _determine_cmap_params(**cmap_kwargs) + if hue is not None: + cmap_kwargs = {'plot_data': ds[hue], + 'vmin': vmin, + 'vmax': vmax, + 'cmap': colors if colors else cmap, + 'center': center, + 'robust': robust, + 'extend': extend, + 'levels': levels, + 'filled': None, + 'norm': norm} + cmap_params = _determine_cmap_params(**cmap_kwargs) + cmap_kwargs_subset = dict( + (vv, cmap_kwargs[vv]) + for vv in ['vmin', 'vmax', 'norm', 'cmap']) + else: + cmap_kwargs_subset = {} + primitive = ax.scatter(data['x'], data['y'], c=data['color'], - vmin=cmap_kwargs['vmin'], - vmax=cmap_kwargs['vmax']) + **cmap_kwargs_subset, **kwargs) - if '_meta_data' in kwargs: # if this was called from map_scatter, + if _meta_data: # if this was called from map_scatter, return primitive # finish here. Else, make labels if meta_data.get('xlabel', None): diff --git a/xarray/plot/facetgrid.py b/xarray/plot/facetgrid.py index 1c0ef9d73f2..717a98eec04 100644 --- a/xarray/plot/facetgrid.py +++ b/xarray/plot/facetgrid.py @@ -327,9 +327,13 @@ def map_scatter(self, x=None, y=None, hue=None, discrete_legend=False, # None is the sentinel value if d is not None: subset = self.data.loc[d] - mappable = scatter(subset, x=x, y=y, hue=hue, - ax=ax, **kwargs) - self._mappables.append(mappable) + maybe_mappable = scatter(subset, x=x, y=y, hue=hue, + ax=ax, **kwargs) + # TODO: better way to verify that an artist is mappable? + # https://stackoverflow.com/questions/33023036/is-it-possible-to-detect-if-a-matplotlib-artist-is-a-mappable-suitable-for-use-w#33023522 + if (maybe_mappable + and hasattr(maybe_mappable, 'autoscale_None')): + self._mappables.append(maybe_mappable) self._finalize_grid(meta_data['xlabel'], meta_data['ylabel']) diff --git a/xarray/tests/test_plot.py b/xarray/tests/test_plot.py index 9eeff5027a0..0dfcafb4a75 100644 --- a/xarray/tests/test_plot.py +++ b/xarray/tests/test_plot.py @@ -1791,7 +1791,7 @@ def test_wrong_num_of_dimensions(self): self.darray.plot.line(row='row', hue='hue') -class TestScatterPlots(PlotTestCase): +class TestDatasetScatterPlots(PlotTestCase): @pytest.fixture(autouse=True) def setUp(self): das = [DataArray(np.random.randn(3, 3, 4, 4), From 15d8066f284a9ceb2629e09e3619de30131eb31b Mon Sep 17 00:00:00 2001 From: dcherian Date: Sat, 15 Dec 2018 22:03:15 -0800 Subject: [PATCH 021/101] doc fixes. --- doc/api.rst | 1 - doc/dask.rst | 2 +- doc/examples/multidimensional-coords.rst | 5 +++-- doc/examples/weather-data.rst | 1 + doc/groupby.rst | 8 ++++---- doc/pandas.rst | 1 + doc/plotting.rst | 21 ++++++++++++++------- doc/reshaping.rst | 6 +++--- doc/time-series.rst | 4 ++++ doc/whats-new.rst | 8 +++++++- 10 files changed, 38 insertions(+), 19 deletions(-) diff --git a/doc/api.rst b/doc/api.rst index 662ef567710..f87ce89fe33 100644 --- a/doc/api.rst +++ b/doc/api.rst @@ -343,7 +343,6 @@ Computation :py:attr:`~DataArray.searchsorted` :py:attr:`~DataArray.round` :py:attr:`~DataArray.real` -:py:attr:`~DataArray.T` :py:attr:`~DataArray.cumsum` :py:attr:`~DataArray.cumprod` :py:attr:`~DataArray.rank` diff --git a/doc/dask.rst b/doc/dask.rst index 975111cba33..ba75eea74cc 100644 --- a/doc/dask.rst +++ b/doc/dask.rst @@ -179,7 +179,7 @@ Explicit conversion by wrapping a DataArray with ``np.asarray`` also works: Alternatively you can load the data into memory but keep the arrays as Dask arrays using the :py:meth:`~xarray.Dataset.persist` method: -.. ipython:: +.. ipython:: python ds = ds.persist() diff --git a/doc/examples/multidimensional-coords.rst b/doc/examples/multidimensional-coords.rst index eed818ba064..7c86f897a24 100644 --- a/doc/examples/multidimensional-coords.rst +++ b/doc/examples/multidimensional-coords.rst @@ -25,7 +25,7 @@ As an example, consider this dataset from the .. ipython:: python - ds = xr.tutorial.load_dataset('rasm') + ds = xr.tutorial.open_dataset('rasm').load() ds In this example, the *logical coordinates* are ``x`` and ``y``, while @@ -107,7 +107,8 @@ function to specify the output coordinates of the group. # define a label for each bin corresponding to the central latitude lat_center = np.arange(1, 90, 2) # group according to those bins and take the mean - Tair_lat_mean = ds.Tair.groupby_bins('xc', lat_bins, labels=lat_center).mean() + Tair_lat_mean = (ds.Tair.groupby_bins('xc', lat_bins, labels=lat_center) + .mean(xr.ALL_DIMS)) # plot the result @savefig xarray_multidimensional_coords_14_1.png width=5in Tair_lat_mean.plot(); diff --git a/doc/examples/weather-data.rst b/doc/examples/weather-data.rst index c13664d4ef5..5a019e637c4 100644 --- a/doc/examples/weather-data.rst +++ b/doc/examples/weather-data.rst @@ -123,6 +123,7 @@ The :py:func:`~xarray.Dataset.fillna` method on grouped objects lets you easily fill missing values by group: .. ipython:: python + :okwarning: # throw away the first half of every month some_missing = ds.tmin.sel(time=ds['time.day'] > 15).reindex_like(ds) diff --git a/doc/groupby.rst b/doc/groupby.rst index 6e42dbbc9f0..03c0881d836 100644 --- a/doc/groupby.rst +++ b/doc/groupby.rst @@ -118,7 +118,7 @@ dimensions *other than* the provided one: .. ipython:: python - ds.groupby('x').std() + ds.groupby('x').std(xr.ALL_DIMS) First and last ~~~~~~~~~~~~~~ @@ -129,7 +129,7 @@ values for group along the grouped dimension: .. ipython:: python - ds.groupby('letters').first() + ds.groupby('letters').first(xr.ALL_DIMS) By default, they skip missing values (control this with ``skipna``). @@ -144,7 +144,7 @@ coordinates. For example: .. ipython:: python - alt = arr.groupby('letters').mean() + alt = arr.groupby('letters').mean(xr.ALL_DIMS) alt ds.groupby('letters') - alt @@ -197,7 +197,7 @@ __ http://cfconventions.org/cf-conventions/v1.6.0/cf-conventions.html#_two_dimen 'lat': (['ny','nx'], [[10,10],[20,20]] ),}, dims=['ny','nx']) da - da.groupby('lon').sum() + da.groupby('lon').sum(xr.ALL_DIMS) da.groupby('lon').apply(lambda x: x - x.mean(), shortcut=False) Because multidimensional groups have the ability to generate a very large diff --git a/doc/pandas.rst b/doc/pandas.rst index e0bad61f805..1d3cc5d9a5d 100644 --- a/doc/pandas.rst +++ b/doc/pandas.rst @@ -173,6 +173,7 @@ So you can represent a Panel, in two ways: Let's take a look: .. ipython:: python + :okwarning: panel = pd.Panel(np.random.rand(2, 3, 4), items=list('ab'), major_axis=list('mno'), minor_axis=pd.date_range(start='2000', periods=4, name='date')) diff --git a/doc/plotting.rst b/doc/plotting.rst index f8ba82febb0..df6b5815af9 100644 --- a/doc/plotting.rst +++ b/doc/plotting.rst @@ -144,7 +144,7 @@ axes created by ``plt.subplots``. plt.tight_layout() @savefig plotting_example_existing_axes.png width=6in - plt.show() + plt.draw() On the right is a histogram created by :py:func:`xarray.plot.hist`. @@ -343,7 +343,7 @@ matplotlib is available. plt.tight_layout() @savefig plotting_2d_call_matplotlib.png width=4in - plt.show() + plt.draw() .. note:: @@ -359,7 +359,7 @@ matplotlib is available. air2d.plot() @savefig plotting_2d_call_matplotlib2.png width=4in - plt.show() + plt.draw() Colormaps ~~~~~~~~~ @@ -444,9 +444,11 @@ if using ``imshow`` or ``pcolormesh`` (but not with ``contour`` or ``contourf``, since levels are chosen automatically). .. ipython:: python + :okwarning: @savefig plotting_seaborn_palette.png width=4in air2d.plot(levels=10, cmap='husl') + plt.draw() .. _plotting.faceting: @@ -519,6 +521,11 @@ Other features Faceted plotting supports other arguments common to xarray 2d plots. +.. ipython:: python + :suppress: + + plt.close('all') + .. ipython:: python hasoutliers = t.isel(time=slice(0, 5)).copy() @@ -528,7 +535,7 @@ Faceted plotting supports other arguments common to xarray 2d plots. @savefig plot_facet_robust.png g = hasoutliers.plot.pcolormesh('lon', 'lat', col='time', col_wrap=3, robust=True, cmap='viridis', - cbar_kwargs={'label': 'this has outliers'}) + cbar_kwargs={'label': 'this has outliers'}) FacetGrid Objects ~~~~~~~~~~~~~~~~~ @@ -568,7 +575,7 @@ they have been plotted. bottomright.annotate('bottom right', (240, 40)) @savefig plot_facet_iterator.png - plt.show() + plt.draw() TODO: add an example of using the ``map`` method to plot dataset variables (e.g., with ``plt.quiver``). @@ -603,7 +610,7 @@ by faceting are accessible in the object returned by ``plot``: ax.coastlines() ax.gridlines() @savefig plotting_maps_cartopy_facetting.png width=100% - plt.show(); + plt.draw(); Details @@ -634,7 +641,7 @@ These are provided for user convenience; they all call the same code. xplt.line(da, ax=axes[1, 1]) plt.tight_layout() @savefig plotting_ways_to_use.png width=6in - plt.show() + plt.draw() Here the output is the same. Since the data is 1 dimensional the line plot was used. diff --git a/doc/reshaping.rst b/doc/reshaping.rst index 67d9e198e8a..0fd078c8306 100644 --- a/doc/reshaping.rst +++ b/doc/reshaping.rst @@ -186,8 +186,8 @@ labels for one or several dimensions: array array['c'] = ('x', ['a', 'b', 'c']) array.set_index(x='c') - array.set_index(x='c', inplace=True) - array.reset_index('x', drop=True) + array = array.set_index(x='c') + array = array.reset_index('x', drop=True) .. _reshape.shift_and_roll: @@ -201,7 +201,7 @@ To adjust coordinate labels, you can use the :py:meth:`~xarray.Dataset.shift` an array = xr.DataArray([1, 2, 3, 4], dims='x') array.shift(x=2) - array.roll(x=2) + array.roll(x=2, roll_coords=True) .. _reshape.sort: diff --git a/doc/time-series.rst b/doc/time-series.rst index 7f5389d3ae1..e86f071646d 100644 --- a/doc/time-series.rst +++ b/doc/time-series.rst @@ -163,6 +163,7 @@ Datetime components couple particularly well with grouped operations (see calculate the mean by time of day: .. ipython:: python + :okwarning: ds.groupby('time.hour').mean() @@ -176,6 +177,7 @@ same api as ``resample`` `in pandas`_. For example, we can downsample our dataset from hourly to 6-hourly: .. ipython:: python + :okwarning: ds.resample(time='6H') @@ -184,6 +186,7 @@ necessary for resampling. All of the reduction methods which work with ``Resample`` objects can also be used for resampling: .. ipython:: python + :okwarning: ds.resample(time='6H').mean() @@ -326,6 +329,7 @@ For data indexed by a :py:class:`~xarray.CFTimeIndex` xarray currently supports: :py:meth:`~xarray.CFTimeIndex.to_datetimeindex` method: .. ipython:: python + :okwarning: modern_times = xr.cftime_range('2000', periods=24, freq='MS', calendar='noleap') da = xr.DataArray(range(24), [('time', modern_times)]) diff --git a/doc/whats-new.rst b/doc/whats-new.rst index 91ee0d75aaa..cd1e558c4cc 100644 --- a/doc/whats-new.rst +++ b/doc/whats-new.rst @@ -174,7 +174,7 @@ Bug fixes By `Spencer Clark `_. - We now properly handle arrays of ``datetime.datetime`` and ``datetime.timedelta`` provided as coordinates. (:issue:`2512`) - By `Deepak Cherian `_. - ``xarray.DataArray.roll`` correctly handles multidimensional arrays. (:issue:`2445`) By `Keisuke Fujii `_. @@ -2216,6 +2216,7 @@ Enhancements for shifting/rotating datasets or arrays along a dimension: .. ipython:: python + :okwarning: array = xray.DataArray([5, 6, 7, 8], dims='x') array.shift(x=2) @@ -2723,6 +2724,7 @@ Enhancements need to supply the time dimension explicitly: .. ipython:: python + :verbatim: time = pd.date_range('2000-01-01', freq='6H', periods=10) array = xray.DataArray(np.arange(10), [('time', time)]) @@ -2732,6 +2734,7 @@ Enhancements options such as ``closed`` and ``label`` let you control labeling: .. ipython:: python + :verbatim: array.resample('1D', dim='time', how='sum', label='right') @@ -2739,6 +2742,7 @@ Enhancements (upsampling), xray will insert missing values: .. ipython:: python + :verbatim: array.resample('3H', 'time') @@ -2746,12 +2750,14 @@ Enhancements last examples from each group along the grouped axis: .. ipython:: python + :verbatim: array.groupby('time.day').first() These methods combine well with ``resample``: .. ipython:: python + :verbatim: array.resample('1D', dim='time', how='first') From e98fc7e21478329625eb8d2fe4131cabc0233dcb Mon Sep 17 00:00:00 2001 From: dcherian Date: Sat, 15 Dec 2018 22:03:21 -0800 Subject: [PATCH 022/101] Dataset plotting docs. --- doc/plotting.rst | 61 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/doc/plotting.rst b/doc/plotting.rst index df6b5815af9..4bee84d6f89 100644 --- a/doc/plotting.rst +++ b/doc/plotting.rst @@ -13,6 +13,7 @@ xarray's plotting capabilities are centered around :py:class:`xarray.DataArray` objects. To plot :py:class:`xarray.Dataset` objects simply access the relevant DataArrays, ie ``dset['var1']``. +Dataset specific plotting routines are also available (see :ref:`plot-dataset`). Here we focus mostly on arrays 2d or larger. If your data fits nicely into a pandas DataFrame then you're better off using one of the more developed tools there. @@ -580,6 +581,66 @@ they have been plotted. TODO: add an example of using the ``map`` method to plot dataset variables (e.g., with ``plt.quiver``). +.. _plot-dataset: + +Datasets +-------- + +``xarray`` has limited support for plotting Dataset variables against each other. +Consider this dataset + +.. ipython:: python + + A = xr.DataArray(np.zeros([3, 11, 4, 4]), dims=[ 'x', 'y', 'z', 'w'], + coords=[np.arange(3), np.linspace(0,1,11), np.arange(4), + 0.1*np.random.randn(4)]) + # fake some data + B = 0.1*A.x**2 + A.y**2.5 + 0.1*A.z*A.w + A = -0.1*A.x + A.y/(5+A.z) + A.w + + ds = xr.Dataset({'A':A, 'B':B}) + ds['w'] = ['one', 'two', 'three', 'five'] + + # add some attributes to showcase automatic labelling + ds.x.attrs['units'] = 'xunits' + ds.y.attrs['units'] = 'yunits' + ds.z.attrs['units'] = 'zunits' + ds.A.attrs['units'] = 'Aunits' + ds.B.attrs['units'] = 'Bunits' + + +Suppose we want to scatter the ``airtemps.fake`` against ``airtemps.air`` + +.. ipython:: python + + @savefig ds_simple_scatter.png + ds.plot.scatter(x='A', y='B') + +You can also set color using the ``hue`` kwarg + +.. ipython:: python + + @savefig ds_hue_scatter.png + ds.plot.scatter(x='A', y='B', hue='w') + +When ``hue`` is specified, a colorbar is added for numeric ``hue`` DataArrays by +default. Legends are also possible using the boolean ``discrete_legend`` kwarg. +This is set to True by default for non-numeric ``hue`` DataArrays. + +.. ipython:: python + + @savefig ds_discrete_legend_hue_scatter.png + ds.plot.scatter(x='A', y='B', hue='w', discrete_legend=True) + + +Faceting is also possible + +.. ipython:: python + + @savefig ds_facet_scatter.png + ds.plot.scatter(x='A', y='B', col='x', row='z', hue='w', discrete_legend=True) + + .. _plot-maps: Maps From 2f91c3d86e31cb9f853e895d56b713cffa681e4e Mon Sep 17 00:00:00 2001 From: dcherian Date: Sat, 15 Dec 2018 22:31:43 -0800 Subject: [PATCH 023/101] group existing docs under "DataArrays." --- doc/plotting.rst | 101 ++++++++++++++++++++++++++++------------------- 1 file changed, 61 insertions(+), 40 deletions(-) diff --git a/doc/plotting.rst b/doc/plotting.rst index 4bee84d6f89..bd095fa40b6 100644 --- a/doc/plotting.rst +++ b/doc/plotting.rst @@ -75,11 +75,15 @@ For these examples we'll use the North American air temperature dataset. Until :issue:`1614` is solved, you might need to copy over the metadata in ``attrs`` to get informative figure labels (as was done above). +DataArrays +---------- + One Dimension -------------- +~~~~~~~~~~~~~ -Simple Example -~~~~~~~~~~~~~~ +================ + Simple Example +================ The simplest way to make a plot is to call the :py:func:`xarray.DataArray.plot()` method. @@ -96,8 +100,9 @@ xarray uses the coordinate name along with metadata ``attrs.long_name``, ``attr air1d.attrs -Additional Arguments -~~~~~~~~~~~~~~~~~~~~~ +====================== + Additional Arguments +====================== Additional arguments are passed directly to the matplotlib function which does the work. @@ -125,8 +130,9 @@ Keyword arguments work the same way, and are more explicit. @savefig plotting_example_sin3.png width=4in air1d[:200].plot.line(color='purple', marker='o') -Adding to Existing Axis -~~~~~~~~~~~~~~~~~~~~~~~ +========================= + Adding to Existing Axis +========================= To add the plot to an existing axis pass in the axis as a keyword argument ``ax``. This works for all xarray plotting methods. @@ -151,8 +157,9 @@ On the right is a histogram created by :py:func:`xarray.plot.hist`. .. _plotting.figsize: -Controlling the figure size -~~~~~~~~~~~~~~~~~~~~~~~~~~~ +============================= + Controlling the figure size +============================= You can pass a ``figsize`` argument to all xarray's plotting methods to control the figure size. For convenience, xarray's plotting methods also @@ -191,8 +198,9 @@ entire figure (as for matplotlib's ``figsize`` argument). .. _plotting.multiplelines: -Multiple lines showing variation along a dimension -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +==================================================== + Multiple lines showing variation along a dimension +==================================================== It is possible to make line plots of two-dimensional data by calling :py:func:`xarray.plot.line` with appropriate arguments. Consider the 3D variable ``air`` defined above. We can use line @@ -213,8 +221,9 @@ If required, the automatic legend can be turned off using ``add_legend=False``. ``hue`` can be passed directly to :py:func:`xarray.plot` as `air.isel(lon=10, lat=[19,21,22]).plot(hue='lat')`. -Dimension along y-axis -~~~~~~~~~~~~~~~~~~~~~~ +======================== + Dimension along y-axis +======================== It is also possible to make line plots such that the data are on the x-axis and a dimension is on the y-axis. This can be done by specifying the appropriate ``y`` keyword argument. @@ -223,8 +232,9 @@ It is also possible to make line plots such that the data are on the x-axis and @savefig plotting_example_xy_kwarg.png air.isel(time=10, lon=[10, 11]).plot(y='lat', hue='lon') -Step plots -~~~~~~~~~~ +============ + Step plots +============ As an alternative, also a step plot similar to matplotlib's ``plt.step`` can be made using 1D data. @@ -255,7 +265,7 @@ is ignored. Other axes kwargs ------------------ +~~~~~~~~~~~~~~~~~ The keyword arguments ``xincrease`` and ``yincrease`` let you control the axes direction. @@ -269,11 +279,12 @@ In addition, one can use ``xscale, yscale`` to set axes scaling; ``xticks, ytick Two Dimensions --------------- - -Simple Example ~~~~~~~~~~~~~~ +================ + Simple Example +================ + The default method :py:meth:`xarray.DataArray.plot` calls :py:func:`xarray.plot.pcolormesh` by default when the data is two-dimensional. .. ipython:: python @@ -299,8 +310,9 @@ and ``xincrease``. If speed is important to you and you are plotting a regular mesh, consider using ``imshow``. -Missing Values -~~~~~~~~~~~~~~ +================ + Missing Values +================ xarray plots data with :ref:`missing_values`. @@ -313,8 +325,9 @@ xarray plots data with :ref:`missing_values`. @savefig plotting_missing_values.png width=4in bad_air2d.plot() -Nonuniform Coordinates -~~~~~~~~~~~~~~~~~~~~~~ +======================== + Nonuniform Coordinates +======================== It's not necessary for the coordinates to be evenly spaced. Both :py:func:`xarray.plot.pcolormesh` (default) and :py:func:`xarray.plot.contourf` can @@ -329,8 +342,9 @@ produce plots with nonuniform coordinates. @savefig plotting_nonuniform_coords.png width=4in b.plot() -Calling Matplotlib -~~~~~~~~~~~~~~~~~~ +==================== + Calling Matplotlib +==================== Since this is a thin wrapper around matplotlib, all the functionality of matplotlib is available. @@ -362,8 +376,9 @@ matplotlib is available. @savefig plotting_2d_call_matplotlib2.png width=4in plt.draw() -Colormaps -~~~~~~~~~ +=========== + Colormaps +=========== xarray borrows logic from Seaborn to infer what kind of color map to use. For example, consider the original data in Kelvins rather than Celsius: @@ -378,8 +393,9 @@ Kelvins do not have 0, so the default color map was used. .. _robust-plotting: -Robust -~~~~~~ +======== + Robust +======== Outliers often have an extreme effect on the output of the plot. Here we add two bad data points. This affects the color scale, @@ -409,8 +425,9 @@ Observe that the ranges of the color bar have changed. The arrows on the color bar indicate that the colors include data points outside the bounds. -Discrete Colormaps -~~~~~~~~~~~~~~~~~~ +==================== + Discrete Colormaps +==================== It is often useful, when visualizing 2d data, to use a discrete colormap, rather than the default continuous colormaps that matplotlib uses. The @@ -454,7 +471,7 @@ since levels are chosen automatically). .. _plotting.faceting: Faceting --------- +~~~~~~~~ Faceting here refers to splitting an array along one or two dimensions and plotting each group. @@ -480,8 +497,9 @@ So let's use a slice to pick 6 times throughout the first year. t = air.isel(time=slice(0, 365 * 4, 250)) t.coords -Simple Example -~~~~~~~~~~~~~~ +================ + Simple Example +================ The easiest way to create faceted plots is to pass in ``row`` or ``col`` arguments to the xarray plotting methods/functions. This returns a @@ -499,8 +517,9 @@ Faceting also works for line plots. @savefig plot_facet_dataarray_line.png g_simple_line = t.isel(lat=slice(0,None,4)).plot(x='lon', hue='lat', col='time', col_wrap=3) -4 dimensional -~~~~~~~~~~~~~ +=============== + 4 dimensional +=============== For 4 dimensional arrays we can use the rows and columns of the grids. Here we create a 4 dimensional array by taking the original data and adding @@ -517,8 +536,9 @@ one were much hotter. @savefig plot_facet_4d.png t4d.plot(x='lon', y='lat', col='time', row='fourth_dim') -Other features -~~~~~~~~~~~~~~ +================ + Other features +================ Faceted plotting supports other arguments common to xarray 2d plots. @@ -538,8 +558,9 @@ Faceted plotting supports other arguments common to xarray 2d plots. robust=True, cmap='viridis', cbar_kwargs={'label': 'this has outliers'}) -FacetGrid Objects -~~~~~~~~~~~~~~~~~ +=================== + FacetGrid Objects +=================== :py:class:`xarray.plot.FacetGrid` is used to control the behavior of the multiple plots. From 269518c8299434819f86023f5a6c838532be22c2 Mon Sep 17 00:00:00 2001 From: dcherian Date: Sat, 15 Dec 2018 22:33:20 -0800 Subject: [PATCH 024/101] bugfix. --- xarray/plot/facetgrid.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/xarray/plot/facetgrid.py b/xarray/plot/facetgrid.py index 717a98eec04..dd01f68405a 100644 --- a/xarray/plot/facetgrid.py +++ b/xarray/plot/facetgrid.py @@ -329,11 +329,9 @@ def map_scatter(self, x=None, y=None, hue=None, discrete_legend=False, subset = self.data.loc[d] maybe_mappable = scatter(subset, x=x, y=y, hue=hue, ax=ax, **kwargs) - # TODO: better way to verify that an artist is mappable? - # https://stackoverflow.com/questions/33023036/is-it-possible-to-detect-if-a-matplotlib-artist-is-a-mappable-suitable-for-use-w#33023522 - if (maybe_mappable - and hasattr(maybe_mappable, 'autoscale_None')): - self._mappables.append(maybe_mappable) + # TODO: this is needed to get legends to work. + # but maybe_mappable is a list in that case :/ + self._mappables.append(maybe_mappable) self._finalize_grid(meta_data['xlabel'], meta_data['ylabel']) From 14379eaa4f7e3e4b23cce4cd665e281a0751801e Mon Sep 17 00:00:00 2001 From: dcherian Date: Sat, 15 Dec 2018 22:38:25 -0800 Subject: [PATCH 025/101] Fix. --- doc/plotting.rst | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/doc/plotting.rst b/doc/plotting.rst index bd095fa40b6..ad949688c06 100644 --- a/doc/plotting.rst +++ b/doc/plotting.rst @@ -645,13 +645,14 @@ You can also set color using the ``hue`` kwarg ds.plot.scatter(x='A', y='B', hue='w') When ``hue`` is specified, a colorbar is added for numeric ``hue`` DataArrays by -default. Legends are also possible using the boolean ``discrete_legend`` kwarg. -This is set to True by default for non-numeric ``hue`` DataArrays. +default and a legend is added for non-numeric ``hue`` DataArrays (as above). +You can force a legend instead of a colorbar using the boolean ``discrete_legend`` kwarg. .. ipython:: python + ds.w.values = [1, 2, 3, 5] @savefig ds_discrete_legend_hue_scatter.png - ds.plot.scatter(x='A', y='B', hue='w', discrete_legend=True) + ds.plot.scatter(x='A', y='B', hue='w') Faceting is also possible From ca1d44b16b424da29a94393fe01fba34c62a2236 Mon Sep 17 00:00:00 2001 From: dcherian Date: Sat, 15 Dec 2018 22:51:14 -0800 Subject: [PATCH 026/101] Add whats-new --- doc/whats-new.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/doc/whats-new.rst b/doc/whats-new.rst index cd1e558c4cc..fa6755d42e2 100644 --- a/doc/whats-new.rst +++ b/doc/whats-new.rst @@ -45,7 +45,8 @@ Enhancements "dayofyear" and "dayofweek" accessors (:issue:`2597`). By `Spencer Clark `_. - Support Dask ``HighLevelGraphs`` by `Matthew Rocklin `_. - +- Dataset plotting API! Currently only :py:meth:`Dataset.plot.scatter` is implemented. + By `Yohai Bar Sinai `_ and `Deepak Cherian `_ Bug fixes ~~~~~~~~~ From 396f14821cf001667e023299e13cfb648e2c1fdc Mon Sep 17 00:00:00 2001 From: dcherian Date: Sat, 15 Dec 2018 22:53:01 -0800 Subject: [PATCH 027/101] Add api.rst. --- doc/api.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/api.rst b/doc/api.rst index f87ce89fe33..e22a3c07360 100644 --- a/doc/api.rst +++ b/doc/api.rst @@ -569,6 +569,7 @@ Plotting .. autosummary:: :toctree: generated/ + Dataset.plot.scatter DataArray.plot plot.plot plot.contourf From ab48350f6dfa33f3b5739fe50801d907b47b0f18 Mon Sep 17 00:00:00 2001 From: dcherian Date: Tue, 18 Dec 2018 15:58:01 -0800 Subject: [PATCH 028/101] Add hue_style. --- xarray/plot/dataset_plot.py | 100 ++++++++++++++++++++++-------------- xarray/plot/facetgrid.py | 18 ++++--- 2 files changed, 71 insertions(+), 47 deletions(-) diff --git a/xarray/plot/dataset_plot.py b/xarray/plot/dataset_plot.py index 80400d3fdb2..c6af838bbd6 100644 --- a/xarray/plot/dataset_plot.py +++ b/xarray/plot/dataset_plot.py @@ -6,7 +6,7 @@ import numpy as np import pandas as pd -from ..core.common import ones_like +from ..core.alignment import broadcast from .facetgrid import FacetGrid from .utils import ( ROBUST_PERCENTILE, _add_colorbar, _determine_cmap_params, _ensure_numeric, @@ -14,7 +14,8 @@ _valid_other_type, get_axis, import_matplotlib_pyplot, label_from_attrs) -def _infer_scatter_meta_data(ds, x, y, hue, add_legend, discrete_legend): +def _infer_scatter_meta_data(ds, x, y, hue, hue_style, add_colorbar, + add_legend): dvars = set(ds.data_vars.keys()) error_msg = (' must be either one of ({0:s})' .format(', '.join(dvars))) @@ -25,41 +26,55 @@ def _infer_scatter_meta_data(ds, x, y, hue, add_legend, discrete_legend): if y not in dvars: raise ValueError(y + error_msg) - if hue and add_legend is None: - add_legend = True - if add_legend and not hue: - raise ValueError('hue must be specified for generating a legend') + dims_coords = set(list(ds.coords) + list(ds.dims)) + if hue is not None and hue not in dims_coords: + raise ValueError(hue + ' must be either one of ({0:s})' + ''.format(', '.join(dims_coords))) + + if hue: + hue_is_numeric = True # _ensure_numeric(ds[hue].values) + + if hue_style is None: + hue_style = 'continuous' if hue_is_numeric else 'discrete' - if hue and add_legend and not _ensure_numeric(ds[hue].values): - if discrete_legend is None: - discrete_legend = True - elif discrete_legend is False: + if not hue_is_numeric and (hue_style == 'continuous'): raise ValueError('Cannot create a colorbar for a non numeric' - ' coordinate') + ' coordinate: ' + hue) + + if add_colorbar is None: + add_colorbar = True if hue_style == 'continuous' else False - if not hue and add_legend is None: - discrete_legend = None + if add_legend is None: + add_legend = True if hue_style == 'discrete' else False + + else: + if add_legend is True: + raise ValueError('Cannot set add_legend when hue is None.') + if add_colorbar is True: + raise ValueError('Cannot set add_colorbar when hue is None.') + add_legend = False + add_colorbar = False + + if hue_style is not None and hue_style not in ['discrete', 'continuous']: + raise ValueError('hue_style must be either None, \'discrete\' ' + 'or \'continuous\'.') dims = ds[x].dims if ds[y].dims != dims: raise ValueError('{} and {} must have the same dimensions.' ''.format(x, y)) - dims_coords = set(list(ds.coords) + list(ds.dims)) - if hue is not None and hue not in dims_coords: - raise ValueError(hue + ' must be either one of ({0:s})' - ''.format(', '.join(dims_coords))) - if hue: hue_label = label_from_attrs(ds.coords[hue]) - hue_values = ds[x].coords[hue] if discrete_legend else None + hue_values = ds[x].coords[hue] else: hue_label = None hue_values = None - return {'add_legend': add_legend, - 'discrete_legend': discrete_legend, + return {'add_colorbar': add_colorbar, + 'add_legend': add_legend, 'hue_label': hue_label, + 'hue_style': hue_style, 'xlabel': label_from_attrs(ds[x]), 'ylabel': label_from_attrs(ds[y]), 'hue_values': hue_values} @@ -71,15 +86,15 @@ def _infer_scatter_data(ds, x, y, hue): 'y': ds[y].values.flatten(), 'color': None} if hue: - data['color'] = ((ones_like(ds[x]) * ds.coords[hue]) + data['color'] = (broadcast(ds.coords[hue], ds[x])[0] .values.flatten()) return data -def scatter(ds, x, y, hue=None, col=None, row=None, +def scatter(ds, x, y, hue=None, hue_style=None, col=None, row=None, col_wrap=None, sharex=True, sharey=True, aspect=None, - size=None, subplot_kws=None, add_legend=None, cbar_kwargs=None, - discrete_legend=None, cbar_ax=None, vmin=None, vmax=None, + size=None, subplot_kws=None, add_colorbar=None, cbar_kwargs=None, + add_legend=None, cbar_ax=None, vmin=None, vmax=None, norm=None, infer_intervals=None, center=None, levels=None, robust=None, colors=None, extend=None, cmap=None, **kwargs): ''' @@ -91,6 +106,13 @@ def scatter(ds, x, y, hue=None, col=None, row=None, Variable names for x, y axis. hue: str, optional Variable by which to color scattered points + hue_style: str, optional + Hue style. + - "discrete" builds a legend. This is the default for non-numeric + `hue` variables. + - "continuous" builds a colorbar + add_legend, add_colorbar: bool, optional + Turn the legend or colorbar on/off. row : string, optional If passed, make row faceted plots on this dimension name col : string, optional @@ -148,15 +170,15 @@ def scatter(ds, x, y, hue=None, col=None, row=None, Additional keyword arguments to matplotlib ''' - if kwargs.get('_meta_data', None): - discrete_legend = kwargs['_meta_data']['discrete_legend'] + if kwargs.get('_meta_data', None): # facetgrid call + meta_data = kwargs['_meta_data'] else: - meta_data = _infer_scatter_meta_data(ds, x, y, hue, - add_legend, discrete_legend) - discrete_legend = meta_data['discrete_legend'] - add_legend = meta_data['add_legend'] + meta_data = _infer_scatter_meta_data(ds, x, y, hue, hue_style, + add_colorbar, add_legend) - plt = import_matplotlib_pyplot() + hue_style = meta_data['hue_style'] + add_legend = meta_data['add_legend'] + add_colorbar = meta_data['add_colorbar'] if col or row: ax = kwargs.pop('ax', None) @@ -174,8 +196,8 @@ def scatter(ds, x, y, hue=None, col=None, row=None, g = FacetGrid(data=ds, col=col, row=row, col_wrap=col_wrap, sharex=sharex, sharey=sharey, figsize=figsize, aspect=aspect, size=size, subplot_kws=subplot_kws) - return g.map_scatter(x=x, y=y, hue=hue, add_legend=add_legend, - discrete_legend=discrete_legend, **kwargs) + return g.map_scatter(x=x, y=y, hue=hue, add_colorbar=add_colorbar, + add_legend=add_legend, **kwargs) figsize = kwargs.pop('figsize', None) ax = kwargs.pop('ax', None) @@ -184,13 +206,13 @@ def scatter(ds, x, y, hue=None, col=None, row=None, kwargs = kwargs.copy() _meta_data = kwargs.pop('_meta_data', None) - if discrete_legend: + if hue_style == 'discrete': primitive = [] for label, grp in ds.groupby(ds[hue]): data = _infer_scatter_data(grp, x, y, hue=None) primitive.append(ax.scatter(data['x'], data['y'], label=label, **kwargs)) - else: + elif hue is None or hue_style == 'continuous': data = _infer_scatter_data(ds, x, y, hue) if hue is not None: cmap_kwargs = {'plot_data': ds[hue], @@ -218,14 +240,14 @@ def scatter(ds, x, y, hue=None, col=None, row=None, if meta_data.get('xlabel', None): ax.set_xlabel(meta_data.get('xlabel')) - if meta_data.get('ylabel', None): ax.set_ylabel(meta_data.get('ylabel')) - if add_legend and discrete_legend: + + if add_legend: ax.legend(handles=primitive, labels=list(meta_data['hue_values'].values), title=meta_data.get('hue_label', None)) - if add_legend and not discrete_legend: + if add_colorbar: cbar_kwargs = {} if cbar_kwargs is None else cbar_kwargs if 'label' not in cbar_kwargs: cbar_kwargs['label'] = meta_data.get('hue_label', None) diff --git a/xarray/plot/facetgrid.py b/xarray/plot/facetgrid.py index dd01f68405a..4d83fc1dfb4 100644 --- a/xarray/plot/facetgrid.py +++ b/xarray/plot/facetgrid.py @@ -314,15 +314,17 @@ def map_dataarray_line(self, x=None, y=None, hue=None, **kwargs): return self - def map_scatter(self, x=None, y=None, hue=None, discrete_legend=False, - add_legend=None, **kwargs): + def map_scatter(self, x=None, y=None, hue=None, hue_style=None, + add_colorbar=None, add_legend=None, **kwargs): from .dataset_plot import _infer_scatter_meta_data, scatter kwargs['add_legend'] = False - kwargs['discrete_legend'] = discrete_legend - meta_data = _infer_scatter_meta_data(self.data, x, y, hue, - add_legend, discrete_legend) + kwargs['add_colorbar'] = False + + meta_data = _infer_scatter_meta_data(self.data, x, y, hue, hue_style, + add_legend, add_colorbar) kwargs['_meta_data'] = meta_data + for d, ax in zip(self.name_dicts.flat, self.axes.flat): # None is the sentinel value if d is not None: @@ -335,12 +337,12 @@ def map_scatter(self, x=None, y=None, hue=None, discrete_legend=False, self._finalize_grid(meta_data['xlabel'], meta_data['ylabel']) - if hue and meta_data['add_legend']: + if hue: self._hue_label = meta_data.pop('hue_label', None) - if meta_data['discrete_legend']: + if add_legend: self._hue_var = meta_data['hue_values'] self.add_legend() - else: + elif add_colorbar: self.add_colorbar(label=self._hue_label) return self From 8f41aee02fb28005a06f762e82465fb79e141f53 Mon Sep 17 00:00:00 2001 From: dcherian Date: Tue, 18 Dec 2018 15:58:07 -0800 Subject: [PATCH 029/101] Update tests. --- xarray/tests/test_plot.py | 33 +++++++++++++++++++++++---------- 1 file changed, 23 insertions(+), 10 deletions(-) diff --git a/xarray/tests/test_plot.py b/xarray/tests/test_plot.py index 0dfcafb4a75..e40cbeba0f9 100644 --- a/xarray/tests/test_plot.py +++ b/xarray/tests/test_plot.py @@ -12,6 +12,7 @@ from xarray import DataArray, Dataset from xarray.coding.times import _import_cftime from xarray.plot.plot import _infer_interval_breaks +from xarray.plot.dataset_plot import _infer_scatter_meta_data from xarray.plot.utils import ( _build_discrete_cmap, _color_palette, _determine_cmap_params, import_seaborn, label_from_attrs) @@ -1838,23 +1839,35 @@ def test_figsize_and_size(self): with pytest.raises(ValueError): self.ds.plot.scatter(x='A', y='B', row='row', size=3, figsize=4) - @pytest.mark.parametrize('x, y, add_legend', [ - ('A', 'B', True), - ('A', 'The Spanish Inquisition', None), - ('The Spanish Inquisition', 'B', None)]) - def test_bad_args(self, x, y, add_legend): + @pytest.mark.parametrize('x, y, hue_style, add_legend, add_colorbar', [ + ('A', 'B', 'something', True, False), + ('A', 'B', 'discrete', True, False), + ('A', 'B', None, True, False), + ('A', 'B', None, False, True), + ('A', 'The Spanish Inquisition', None, None, None), + ('The Spanish Inquisition', 'B', None, None, None)]) + def test_bad_args(self, x, y, hue_style, add_legend, add_colorbar): with pytest.raises(ValueError): - self.ds.plot.scatter(x, y, add_legend=add_legend) + self.ds.plot.scatter(x, y, hue_style=hue_style, + add_legend=add_legend, + add_colorbar=add_colorbar) + + @pytest.mark.parametrize('hue_style', ['discrete', 'continuous']) + def test_datetime_hue(self, hue_style): + ds2 = self.ds.copy() + ds2['hue'] = pd.date_range('2000-1-1', periods=4) + ds2.plot.scatter(x='A', y='B', hue='hue', hue_style=hue_style) def test_non_numeric_legend(self): - self.ds['hue'] = pd.date_range('2000-01-01', periods=4) - lines = self.ds.plot.scatter(x='A', y='B', hue='hue') + ds2 = self.ds.copy() + ds2['hue'] = ['a', 'b', 'c', 'd'] + lines = ds2.plot.scatter(x='A', y='B', hue='hue') # should make a discrete legend assert lines[0].axes.legend_ is not None # and raise an error if explicitly not allowed to do so with pytest.raises(ValueError): - self.ds.plot.scatter(x='A', y='B', hue='hue', - discrete_legend=False) + ds2.plot.scatter(x='A', y='B', hue='hue', + hue_style='continuous') def test_add_legend_by_default(self): sc = self.ds.plot.scatter(x='A', y='B', hue='hue') From 5bb2ef64f1d9a3af43ef615f8a06f5a0662eda13 Mon Sep 17 00:00:00 2001 From: dcherian Date: Wed, 19 Dec 2018 08:16:58 -0800 Subject: [PATCH 030/101] cleanup imports. --- xarray/plot/dataset_plot.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/xarray/plot/dataset_plot.py b/xarray/plot/dataset_plot.py index c6af838bbd6..cfe0c6b660d 100644 --- a/xarray/plot/dataset_plot.py +++ b/xarray/plot/dataset_plot.py @@ -1,17 +1,14 @@ from __future__ import absolute_import, division, print_function import functools -import warnings import numpy as np -import pandas as pd from ..core.alignment import broadcast from .facetgrid import FacetGrid from .utils import ( - ROBUST_PERCENTILE, _add_colorbar, _determine_cmap_params, _ensure_numeric, - _interval_to_double_bound_points, _interval_to_mid_points, - _valid_other_type, get_axis, import_matplotlib_pyplot, label_from_attrs) + _add_colorbar, _determine_cmap_params, _ensure_numeric, + _valid_other_type, get_axis, label_from_attrs) def _infer_scatter_meta_data(ds, x, y, hue, hue_style, add_colorbar, From 08a348126cc182c173f6565653cfaf1f6ee1bc58 Mon Sep 17 00:00:00 2001 From: dcherian Date: Wed, 19 Dec 2018 09:39:43 -0700 Subject: [PATCH 031/101] facetgrid: Refactor out cmap_params, cbar_kwargs processing --- xarray/plot/facetgrid.py | 54 ++++++++++++++++++++++------------------ 1 file changed, 30 insertions(+), 24 deletions(-) diff --git a/xarray/plot/facetgrid.py b/xarray/plot/facetgrid.py index 4d83fc1dfb4..3a54274c12c 100644 --- a/xarray/plot/facetgrid.py +++ b/xarray/plot/facetgrid.py @@ -199,28 +199,7 @@ def _left_axes(self): def _bottom_axes(self): return self.axes[-1, :] - def map_dataarray(self, func, x, y, **kwargs): - """ - Apply a plotting function to a 2d facet's subset of the data. - - This is more convenient and less general than ``FacetGrid.map`` - - Parameters - ---------- - func : callable - A plotting function with the same signature as a 2d xarray - plotting method such as `xarray.plot.imshow` - x, y : string - Names of the coordinates to plot on x, y axes - kwargs : - additional keyword arguments to func - - Returns - ------- - self : FacetGrid object - - """ - + def _process_cmap(self, func, kwargs, data): cmapkw = kwargs.get('cmap') colorskw = kwargs.get('colors') cbar_kwargs = kwargs.pop('cbar_kwargs', {}) @@ -234,7 +213,7 @@ def map_dataarray(self, func, x, y, **kwargs): raise ValueError("Can't specify both cmap and colors.") # These should be consistent with xarray.plot._plot2d - cmap_kwargs = {'plot_data': self.data.values, + cmap_kwargs = {'plot_data': data.values, # MPL default 'levels': 7 if 'contour' in func.__name__ else None, 'filled': func.__name__ != 'contour', @@ -248,6 +227,34 @@ def map_dataarray(self, func, x, y, **kwargs): if colorskw is not None: cmap_params['cmap'] = None + self._cmap_extend = cmap_params.get('extend') + + return cmap_params, cbar_kwargs + + def map_dataarray(self, func, x, y, **kwargs): + """ + Apply a plotting function to a 2d facet's subset of the data. + + This is more convenient and less general than ``FacetGrid.map`` + + Parameters + ---------- + func : callable + A plotting function with the same signature as a 2d xarray + plotting method such as `xarray.plot.imshow` + x, y : string + Names of the coordinates to plot on x, y axes + kwargs : + additional keyword arguments to func + + Returns + ------- + self : FacetGrid object + + """ + + cmap_params, cbar_kwargs = self._process_cmap(func, kwargs, self.data) + # Order is important func_kwargs = kwargs.copy() func_kwargs.update(cmap_params) @@ -265,7 +272,6 @@ def map_dataarray(self, func, x, y, **kwargs): mappable = func(subset, x, y, ax=ax, **func_kwargs) self._mappables.append(mappable) - self._cmap_extend = cmap_params.get('extend') self._finalize_grid(x, y) if kwargs.get('add_colorbar', True): From c2923b2e1b366050bc05315b99fca7fe2f7e01d6 Mon Sep 17 00:00:00 2001 From: dcherian Date: Wed, 19 Dec 2018 09:40:00 -0700 Subject: [PATCH 032/101] Dataset.plot.scatter obeys cmap_params, cbar_kwargs. --- xarray/plot/dataset_plot.py | 29 +++++++++++++++++------------ xarray/plot/facetgrid.py | 8 +++++++- 2 files changed, 24 insertions(+), 13 deletions(-) diff --git a/xarray/plot/dataset_plot.py b/xarray/plot/dataset_plot.py index cfe0c6b660d..2216f68b39f 100644 --- a/xarray/plot/dataset_plot.py +++ b/xarray/plot/dataset_plot.py @@ -212,19 +212,24 @@ def scatter(ds, x, y, hue=None, hue_style=None, col=None, row=None, elif hue is None or hue_style == 'continuous': data = _infer_scatter_data(ds, x, y, hue) if hue is not None: - cmap_kwargs = {'plot_data': ds[hue], - 'vmin': vmin, - 'vmax': vmax, - 'cmap': colors if colors else cmap, - 'center': center, - 'robust': robust, - 'extend': extend, - 'levels': levels, - 'filled': None, - 'norm': norm} - cmap_params = _determine_cmap_params(**cmap_kwargs) + if _meta_data: + cbar_kwargs = _meta_data['cbar_kwargs'] + cmap_params = _meta_data['cmap_params'] + else: + cmap_kwargs = {'plot_data': ds[hue], + 'vmin': vmin, + 'vmax': vmax, + 'cmap': colors if colors else cmap, + 'center': center, + 'robust': robust, + 'extend': extend, + 'levels': levels, + 'filled': None, + 'norm': norm} + cmap_params = _determine_cmap_params(**cmap_kwargs) + cmap_kwargs_subset = dict( - (vv, cmap_kwargs[vv]) + (vv, cmap_params[vv]) for vv in ['vmin', 'vmax', 'norm', 'cmap']) else: cmap_kwargs_subset = {} diff --git a/xarray/plot/facetgrid.py b/xarray/plot/facetgrid.py index 3a54274c12c..8035b689d65 100644 --- a/xarray/plot/facetgrid.py +++ b/xarray/plot/facetgrid.py @@ -331,6 +331,12 @@ def map_scatter(self, x=None, y=None, hue=None, hue_style=None, add_legend, add_colorbar) kwargs['_meta_data'] = meta_data + if hue and meta_data['hue_style'] == 'continuous': + cmap_params, cbar_kwargs = self._process_cmap(scatter, kwargs, + self.data[hue]) + kwargs['_meta_data']['cmap_params'] = cmap_params + kwargs['_meta_data']['cbar_kwargs'] = cbar_kwargs + for d, ax in zip(self.name_dicts.flat, self.axes.flat): # None is the sentinel value if d is not None: @@ -349,7 +355,7 @@ def map_scatter(self, x=None, y=None, hue=None, hue_style=None, self._hue_var = meta_data['hue_values'] self.add_legend() elif add_colorbar: - self.add_colorbar(label=self._hue_label) + self.add_colorbar(label=self._hue_label, **cbar_kwargs) return self From 0a01e7c9ec9d1d22bf8fb210bcbcb9f670f40c98 Mon Sep 17 00:00:00 2001 From: dcherian Date: Tue, 18 Dec 2018 15:58:13 -0800 Subject: [PATCH 033/101] _determine_cmap_params supports datetime64 --- xarray/plot/utils.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/xarray/plot/utils.py b/xarray/plot/utils.py index 016c6b5b395..9797b69e38e 100644 --- a/xarray/plot/utils.py +++ b/xarray/plot/utils.py @@ -150,16 +150,23 @@ def _determine_cmap_params(plot_data, vmin=None, vmax=None, cmap=None, """ import matplotlib as mpl - calc_data = np.ravel(plot_data[np.isfinite(plot_data)]) + if np.issubdtype(plot_data.dtype, np.datetime64): + calc_data = np.ravel(plot_data[~np.isnat(plot_data)]) + possibly_divergent = False + elif np.issubdtype(plot_data.dtype, np.timedelta64): + calc_data = np.ravel(plot_data[~np.isnat(plot_data)]).astype('float64') + # Setting center=False prevents a divergent cmap + possibly_divergent = center is not False + else: + calc_data = np.ravel(plot_data[np.isfinite(plot_data)]) + # Setting center=False prevents a divergent cmap + possibly_divergent = center is not False # Handle all-NaN input data gracefully if calc_data.size == 0: # Arbitrary default for when all values are NaN calc_data = np.array(0.0) - # Setting center=False prevents a divergent cmap - possibly_divergent = center is not False - # Set center to 0 so math below makes sense but remember its state center_is_none = False if center is None: From c3bd7c8ee9b20c2f140aa5c017e896c8bea0d78f Mon Sep 17 00:00:00 2001 From: dcherian Date: Wed, 19 Dec 2018 09:51:34 -0700 Subject: [PATCH 034/101] dataset.plot.scatter supports hue=datetime64, timedelta64 --- xarray/plot/dataset_plot.py | 4 +++- xarray/tests/test_plot.py | 3 +++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/xarray/plot/dataset_plot.py b/xarray/plot/dataset_plot.py index 2216f68b39f..6ea78f73037 100644 --- a/xarray/plot/dataset_plot.py +++ b/xarray/plot/dataset_plot.py @@ -29,7 +29,9 @@ def _infer_scatter_meta_data(ds, x, y, hue, hue_style, add_colorbar, ''.format(', '.join(dims_coords))) if hue: - hue_is_numeric = True # _ensure_numeric(ds[hue].values) + hue_is_numeric = (_ensure_numeric(ds[hue].values) + or _valid_other_type(ds[hue], [np.datetime64, + np.timedelta64])) if hue_style is None: hue_style = 'continuous' if hue_is_numeric else 'discrete' diff --git a/xarray/tests/test_plot.py b/xarray/tests/test_plot.py index e40cbeba0f9..e020d1c5b5a 100644 --- a/xarray/tests/test_plot.py +++ b/xarray/tests/test_plot.py @@ -1858,6 +1858,9 @@ def test_datetime_hue(self, hue_style): ds2['hue'] = pd.date_range('2000-1-1', periods=4) ds2.plot.scatter(x='A', y='B', hue='hue', hue_style=hue_style) + ds2['hue'] = pd.timedelta_range('-1D', periods=4, freq='D') + ds2.plot.scatter(x='A', y='B', hue='hue', hue_style=hue_style) + def test_non_numeric_legend(self): ds2 = self.ds.copy() ds2['hue'] = ['a', 'b', 'c', 'd'] From 80fc91a4d3d683983d2598bb0771e9c539a00ceb Mon Sep 17 00:00:00 2001 From: dcherian Date: Wed, 19 Dec 2018 09:58:23 -0700 Subject: [PATCH 035/101] pep8 --- xarray/plot/plot.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/xarray/plot/plot.py b/xarray/plot/plot.py index 0e44f75d6c6..f3d744721b5 100644 --- a/xarray/plot/plot.py +++ b/xarray/plot/plot.py @@ -18,8 +18,8 @@ from .facetgrid import FacetGrid from .utils import ( - ROBUST_PERCENTILE, _add_colorbar, _determine_cmap_params, _ensure_plottable, - _infer_xy_labels, _interval_to_double_bound_points, + ROBUST_PERCENTILE, _add_colorbar, _determine_cmap_params, + _ensure_plottable, _infer_xy_labels, _interval_to_double_bound_points, _interval_to_mid_points, _resolve_intervals_2dplot, _valid_other_type, get_axis, import_matplotlib_pyplot, label_from_attrs) From f2704f83d2cf382a604cd3ebc54b78f0f1d75506 Mon Sep 17 00:00:00 2001 From: dcherian Date: Wed, 19 Dec 2018 10:09:18 -0700 Subject: [PATCH 036/101] Update docs. --- doc/plotting.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/plotting.rst b/doc/plotting.rst index 55bbbf531d5..f4146051012 100644 --- a/doc/plotting.rst +++ b/doc/plotting.rst @@ -646,13 +646,13 @@ You can also set color using the ``hue`` kwarg When ``hue`` is specified, a colorbar is added for numeric ``hue`` DataArrays by default and a legend is added for non-numeric ``hue`` DataArrays (as above). -You can force a legend instead of a colorbar using the boolean ``discrete_legend`` kwarg. +You can force a legend instead of a colorbar by setting ``hue_style='discrete'``. .. ipython:: python ds.w.values = [1, 2, 3, 5] @savefig ds_discrete_legend_hue_scatter.png - ds.plot.scatter(x='A', y='B', hue='w') + ds.plot.scatter(x='A', y='B', hue='w', hue_style='discrete') Faceting is also possible @@ -660,7 +660,7 @@ Faceting is also possible .. ipython:: python @savefig ds_facet_scatter.png - ds.plot.scatter(x='A', y='B', col='x', row='z', hue='w', discrete_legend=True) + ds.plot.scatter(x='A', y='B', col='x', row='z', hue='w', hue_style='discrete') .. _plot-maps: From caef62a221e842c122b632d2746eeb4b3845659b Mon Sep 17 00:00:00 2001 From: dcherian Date: Fri, 21 Dec 2018 13:31:59 -0700 Subject: [PATCH 037/101] bugfix: facetgrid now uses hue_style --- xarray/plot/dataset_plot.py | 7 ++++--- xarray/tests/test_plot.py | 10 ++++++++++ 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/xarray/plot/dataset_plot.py b/xarray/plot/dataset_plot.py index 6ea78f73037..c87311213e6 100644 --- a/xarray/plot/dataset_plot.py +++ b/xarray/plot/dataset_plot.py @@ -25,8 +25,8 @@ def _infer_scatter_meta_data(ds, x, y, hue, hue_style, add_colorbar, dims_coords = set(list(ds.coords) + list(ds.dims)) if hue is not None and hue not in dims_coords: - raise ValueError(hue + ' must be either one of ({0:s})' - ''.format(', '.join(dims_coords))) + raise ValueError('hue must be one of ({0:s}) but is {hue}' + 'instead.'.format(', '.join(dims_coords)), hue) if hue: hue_is_numeric = (_ensure_numeric(ds[hue].values) @@ -195,7 +195,8 @@ def scatter(ds, x, y, hue=None, hue_style=None, col=None, row=None, g = FacetGrid(data=ds, col=col, row=row, col_wrap=col_wrap, sharex=sharex, sharey=sharey, figsize=figsize, aspect=aspect, size=size, subplot_kws=subplot_kws) - return g.map_scatter(x=x, y=y, hue=hue, add_colorbar=add_colorbar, + return g.map_scatter(x=x, y=y, hue=hue, hue_style=hue_style, + add_colorbar=add_colorbar, add_legend=add_legend, **kwargs) figsize = kwargs.pop('figsize', None) diff --git a/xarray/tests/test_plot.py b/xarray/tests/test_plot.py index e020d1c5b5a..e39df25456e 100644 --- a/xarray/tests/test_plot.py +++ b/xarray/tests/test_plot.py @@ -1861,6 +1861,16 @@ def test_datetime_hue(self, hue_style): ds2['hue'] = pd.timedelta_range('-1D', periods=4, freq='D') ds2.plot.scatter(x='A', y='B', hue='hue', hue_style=hue_style) + @pytest.mark.parametrize('hue_style, map_type', + (['discrete', list], + ['continuous', mpl.collections.PathCollection])) + def test_facetgrid_hue_style(self, hue_style, map_type): + g = self.ds.plot.scatter(x='A', y='B', row='row', col='col', hue='hue', + hue_style=hue_style) + # for 'discrete' a list is appended to _mappables + # for 'continuous', should be single PathCollection + assert type(g._mappables[-1]) == map_type + def test_non_numeric_legend(self): ds2 = self.ds.copy() ds2['hue'] = ['a', 'b', 'c', 'd'] From 9b9478bbd3de1bca7a404e0d67041af732cc1aee Mon Sep 17 00:00:00 2001 From: dcherian Date: Fri, 21 Dec 2018 13:47:50 -0700 Subject: [PATCH 038/101] minor fixes. --- doc/api.rst | 3 ++- xarray/core/dataset.py | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/doc/api.rst b/doc/api.rst index 69d705e20e8..799ff600205 100644 --- a/doc/api.rst +++ b/doc/api.rst @@ -343,6 +343,7 @@ Computation :py:attr:`~DataArray.searchsorted` :py:attr:`~DataArray.round` :py:attr:`~DataArray.real` +:py:attr:`~DataArray.T` :py:attr:`~DataArray.cumsum` :py:attr:`~DataArray.cumprod` :py:attr:`~DataArray.rank` @@ -569,7 +570,7 @@ Plotting .. autosummary:: :toctree: generated/ - Dataset.plot.scatter + Dataset.plot DataArray.plot plot.plot plot.contourf diff --git a/xarray/core/dataset.py b/xarray/core/dataset.py index c245d525ab9..a68d090976b 100644 --- a/xarray/core/dataset.py +++ b/xarray/core/dataset.py @@ -3858,6 +3858,7 @@ def plot(self): """ Access plotting functions. Use it as a namespace to use xarray.plot functions as Dataset methods + >>> ds.plot.scatter(...) # equivalent to xarray.plot.scatter(ds,...) """ From 3d40dab22f0471472f8c1648dde68815c60a8b34 Mon Sep 17 00:00:00 2001 From: dcherian Date: Fri, 21 Dec 2018 14:16:00 -0700 Subject: [PATCH 039/101] Scatter docs --- doc/api.rst | 1 + xarray/plot/__init__.py | 2 ++ xarray/plot/dataset_plot.py | 6 ++++-- xarray/plot/plot.py | 3 ++- 4 files changed, 9 insertions(+), 3 deletions(-) diff --git a/doc/api.rst b/doc/api.rst index 799ff600205..72979db3213 100644 --- a/doc/api.rst +++ b/doc/api.rst @@ -578,6 +578,7 @@ Plotting plot.hist plot.imshow plot.line + plot.scatter plot.pcolormesh plot.FacetGrid diff --git a/xarray/plot/__init__.py b/xarray/plot/__init__.py index 4b53b22243c..af9e50a36f2 100644 --- a/xarray/plot/__init__.py +++ b/xarray/plot/__init__.py @@ -3,6 +3,7 @@ from __future__ import print_function from .plot import (plot, line, step, contourf, contour, hist, imshow, pcolormesh) +from .dataset_plot import scatter from .facetgrid import FacetGrid @@ -16,4 +17,5 @@ 'imshow', 'pcolormesh', 'FacetGrid', + 'scatter', ] diff --git a/xarray/plot/dataset_plot.py b/xarray/plot/dataset_plot.py index c87311213e6..70881feb3f1 100644 --- a/xarray/plot/dataset_plot.py +++ b/xarray/plot/dataset_plot.py @@ -97,8 +97,10 @@ def scatter(ds, x, y, hue=None, hue_style=None, col=None, row=None, norm=None, infer_intervals=None, center=None, levels=None, robust=None, colors=None, extend=None, cmap=None, **kwargs): ''' - Inputs - ------ + Scatter Dataset variables against each other. + + Input + ----- ds : Dataset x, y : string diff --git a/xarray/plot/plot.py b/xarray/plot/plot.py index f3d744721b5..9056da440ca 100644 --- a/xarray/plot/plot.py +++ b/xarray/plot/plot.py @@ -2,8 +2,9 @@ Use this module directly: import xarray.plot as xplt -Or use the methods on a DataArray: +Or use the methods on a DataArray or Dataset: DataArray.plot._____ + Dataset.plot._____ """ from __future__ import absolute_import, division, print_function From 1f0b1b1c4d29262b8036825ca868398ff53fbe56 Mon Sep 17 00:00:00 2001 From: dcherian Date: Sun, 13 Jan 2019 22:08:05 -0800 Subject: [PATCH 040/101] Refactor out more code to utils.py --- xarray/plot/facetgrid.py | 30 ++++++ xarray/plot/plot.py | 196 +++------------------------------------ xarray/plot/utils.py | 128 +++++++++++++++++++++++++ 3 files changed, 171 insertions(+), 183 deletions(-) diff --git a/xarray/plot/facetgrid.py b/xarray/plot/facetgrid.py index 8035b689d65..407c8c6a4b8 100644 --- a/xarray/plot/facetgrid.py +++ b/xarray/plot/facetgrid.py @@ -568,3 +568,33 @@ def map(self, func, *args, **kwargs): self._finalize_grid(*args[:2]) return self + + +def _easy_facetgrid(data, plotfunc, x=None, y=None, kind=None, row=None, col=None, + col_wrap=None, sharex=True, sharey=True, aspect=None, + size=None, subplot_kws=None, **kwargs): + """ + Convenience method to call xarray.plot.FacetGrid from 2d plotting methods + + kwargs are the arguments to 2d plotting method + """ + ax = kwargs.pop('ax', None) + figsize = kwargs.pop('figsize', None) + if ax is not None: + raise ValueError("Can't use axes when making faceted plots.") + if aspect is None: + aspect = 1 + if size is None: + size = 3 + elif figsize is not None: + raise ValueError('cannot provide both `figsize` and `size` arguments') + + g = FacetGrid(data=data, col=col, row=row, col_wrap=col_wrap, + sharex=sharex, sharey=sharey, figsize=figsize, + aspect=aspect, size=size, subplot_kws=subplot_kws) + + if kind == 'dataarray': + return g.map_dataarray(plotfunc, x, y, **kwargs) + elif kind == 'array line': + return g.map_dataarray_line(hue=kwargs.pop('hue'), **kwargs) + return g.map_dataset(plotfunc, x, y, **kwargs) diff --git a/xarray/plot/plot.py b/xarray/plot/plot.py index 9056da440ca..ee509c3d54a 100644 --- a/xarray/plot/plot.py +++ b/xarray/plot/plot.py @@ -17,63 +17,16 @@ from xarray.core.common import contains_cftime_datetimes from xarray.core.pycompat import basestring -from .facetgrid import FacetGrid +from .facetgrid import FacetGrid, _easy_facetgrid from .utils import ( - ROBUST_PERCENTILE, _add_colorbar, _determine_cmap_params, - _ensure_plottable, _infer_xy_labels, _interval_to_double_bound_points, - _interval_to_mid_points, _resolve_intervals_2dplot, _valid_other_type, + _add_colorbar, _determine_cmap_params, + _ensure_plottable, _infer_interval_breaks, + _infer_xy_labels, _interval_to_double_bound_points, + _interval_to_mid_points, _is_monotonic, _rescale_imshow_rgb, + _resolve_intervals_2dplot, _update_axes, _valid_other_type, get_axis, import_matplotlib_pyplot, label_from_attrs) -def _easy_facetgrid(darray, plotfunc, x, y, row=None, col=None, - col_wrap=None, sharex=True, sharey=True, aspect=None, - size=None, subplot_kws=None, **kwargs): - """ - Convenience method to call xarray.plot.FacetGrid from 2d plotting methods - - kwargs are the arguments to 2d plotting method - """ - ax = kwargs.pop('ax', None) - figsize = kwargs.pop('figsize', None) - if ax is not None: - raise ValueError("Can't use axes when making faceted plots.") - if aspect is None: - aspect = 1 - if size is None: - size = 3 - elif figsize is not None: - raise ValueError('cannot provide both `figsize` and `size` arguments') - - g = FacetGrid(data=darray, col=col, row=row, col_wrap=col_wrap, - sharex=sharex, sharey=sharey, figsize=figsize, - aspect=aspect, size=size, subplot_kws=subplot_kws) - return g.map_dataarray(plotfunc, x, y, **kwargs) - - -def _line_facetgrid(darray, row=None, col=None, hue=None, - col_wrap=None, sharex=True, sharey=True, aspect=None, - size=None, subplot_kws=None, **kwargs): - """ - Convenience method to call xarray.plot.FacetGrid for line plots - kwargs are the arguments to pyplot.plot() - """ - ax = kwargs.pop('ax', None) - figsize = kwargs.pop('figsize', None) - if ax is not None: - raise ValueError("Can't use axes when making faceted plots.") - if aspect is None: - aspect = 1 - if size is None: - size = 3 - elif figsize is not None: - raise ValueError('cannot provide both `figsize` and `size` arguments') - - g = FacetGrid(data=darray, col=col, row=row, col_wrap=col_wrap, - sharex=sharex, sharey=sharey, figsize=figsize, - aspect=aspect, size=size, subplot_kws=subplot_kws) - return g.map_dataarray_line(hue=hue, **kwargs) - - def plot(darray, row=None, col=None, col_wrap=None, ax=None, hue=None, rtol=0.01, subplot_kws=None, **kwargs): """ @@ -287,7 +240,10 @@ def line(darray, *args, **kwargs): if row or col: allargs = locals().copy() allargs.update(allargs.pop('kwargs')) - return _line_facetgrid(**allargs) + allargs.pop('darray') + allargs['data'] = darray + allargs['plotfunc'] = line + return _easy_facetgrid(kind='array line', **allargs) ndims = len(darray.dims) if ndims > 2: @@ -467,48 +423,6 @@ def hist(darray, figsize=None, size=None, aspect=None, ax=None, **kwargs): return primitive -def _update_axes(ax, xincrease, yincrease, - xscale=None, yscale=None, - xticks=None, yticks=None, - xlim=None, ylim=None): - """ - Update axes with provided parameters - """ - if xincrease is None: - pass - elif xincrease and ax.xaxis_inverted(): - ax.invert_xaxis() - elif not xincrease and not ax.xaxis_inverted(): - ax.invert_xaxis() - - if yincrease is None: - pass - elif yincrease and ax.yaxis_inverted(): - ax.invert_yaxis() - elif not yincrease and not ax.yaxis_inverted(): - ax.invert_yaxis() - - # The default xscale, yscale needs to be None. - # If we set a scale it resets the axes formatters, - # This means that set_xscale('linear') on a datetime axis - # will remove the date labels. So only set the scale when explicitly - # asked to. https://github.com/matplotlib/matplotlib/issues/8740 - if xscale is not None: - ax.set_xscale(xscale) - if yscale is not None: - ax.set_yscale(yscale) - - if xticks is not None: - ax.set_xticks(xticks) - if yticks is not None: - ax.set_yticks(yticks) - - if xlim is not None: - ax.set_xlim(xlim) - if ylim is not None: - ax.set_ylim(ylim) - - # MUST run before any 2d plotting functions are defined since # _plot2d decorator adds them as methods here. class _PlotMethods(object): @@ -536,44 +450,6 @@ def step(self, *args, **kwargs): return step(self._da, *args, **kwargs) -def _rescale_imshow_rgb(darray, vmin, vmax, robust): - assert robust or vmin is not None or vmax is not None - # TODO: remove when min numpy version is bumped to 1.13 - # There's a cyclic dependency via DataArray, so we can't import from - # xarray.ufuncs in global scope. - from xarray.ufuncs import maximum, minimum - - # Calculate vmin and vmax automatically for `robust=True` - if robust: - if vmax is None: - vmax = np.nanpercentile(darray, 100 - ROBUST_PERCENTILE) - if vmin is None: - vmin = np.nanpercentile(darray, ROBUST_PERCENTILE) - # If not robust and one bound is None, calculate the default other bound - # and check that an interval between them exists. - elif vmax is None: - vmax = 255 if np.issubdtype(darray.dtype, np.integer) else 1 - if vmax < vmin: - raise ValueError( - 'vmin=%r is less than the default vmax (%r) - you must supply ' - 'a vmax > vmin in this case.' % (vmin, vmax)) - elif vmin is None: - vmin = 0 - if vmin > vmax: - raise ValueError( - 'vmax=%r is less than the default vmin (0) - you must supply ' - 'a vmin < vmax in this case.' % vmax) - # Scale interval [vmin .. vmax] to [0 .. 1], with darray as 64-bit float - # to avoid precision loss, integer over/underflow, etc with extreme inputs. - # After scaling, downcast to 32-bit float. This substantially reduces - # memory usage after we hand `darray` off to matplotlib. - darray = ((darray.astype('f8') - vmin) / (vmax - vmin)).astype('f4') - with warnings.catch_warnings(): - warnings.filterwarnings('ignore', 'xarray.ufuncs', - PendingDeprecationWarning) - return minimum(maximum(darray, 0), 1) - - def _plot2d(plotfunc): """ Decorator for common 2d plotting logic @@ -719,8 +595,10 @@ def newplotfunc(darray, x=None, y=None, figsize=None, size=None, # Need the decorated plotting function allargs['plotfunc'] = globals()[plotfunc.__name__] + allargs['data'] = darray + del allargs['darray'] - return _easy_facetgrid(**allargs) + return _easy_facetgrid(kind='dataarray', **allargs) plt = import_matplotlib_pyplot() @@ -988,54 +866,6 @@ def contourf(x, y, z, ax, **kwargs): return primitive -def _is_monotonic(coord, axis=0): - """ - >>> _is_monotonic(np.array([0, 1, 2])) - True - >>> _is_monotonic(np.array([2, 1, 0])) - True - >>> _is_monotonic(np.array([0, 2, 1])) - False - """ - if coord.shape[axis] < 3: - return True - else: - n = coord.shape[axis] - delta_pos = (coord.take(np.arange(1, n), axis=axis) >= - coord.take(np.arange(0, n - 1), axis=axis)) - delta_neg = (coord.take(np.arange(1, n), axis=axis) <= - coord.take(np.arange(0, n - 1), axis=axis)) - return np.all(delta_pos) or np.all(delta_neg) - - -def _infer_interval_breaks(coord, axis=0, check_monotonic=False): - """ - >>> _infer_interval_breaks(np.arange(5)) - array([-0.5, 0.5, 1.5, 2.5, 3.5, 4.5]) - >>> _infer_interval_breaks([[0, 1], [3, 4]], axis=1) - array([[-0.5, 0.5, 1.5], - [ 2.5, 3.5, 4.5]]) - """ - coord = np.asarray(coord) - - if check_monotonic and not _is_monotonic(coord, axis=axis): - raise ValueError("The input coordinate is not sorted in increasing " - "order along axis %d. This can lead to unexpected " - "results. Consider calling the `sortby` method on " - "the input DataArray. To plot data with categorical " - "axes, consider using the `heatmap` function from " - "the `seaborn` statistical plotting library." % axis) - - deltas = 0.5 * np.diff(coord, axis=axis) - if deltas.size == 0: - deltas = np.array(0.0) - first = np.take(coord, [0], axis=axis) - np.take(deltas, [0], axis=axis) - last = np.take(coord, [-1], axis=axis) + np.take(deltas, [-1], axis=axis) - trim_last = tuple(slice(None, -1) if n == axis else slice(None) - for n in range(coord.ndim)) - return np.concatenate([first, coord[trim_last] + deltas, last], axis=axis) - - @_plot2d def pcolormesh(x, y, z, ax, infer_intervals=None, **kwargs): """ diff --git a/xarray/plot/utils.py b/xarray/plot/utils.py index 9797b69e38e..31fdda34e4f 100644 --- a/xarray/plot/utils.py +++ b/xarray/plot/utils.py @@ -505,3 +505,131 @@ def _add_colorbar(primitive, ax, cbar_ax, cbar_kwargs, cmap_params): cbar = plt.colorbar(primitive, **cbar_kwargs) return cbar + + +def _rescale_imshow_rgb(darray, vmin, vmax, robust): + assert robust or vmin is not None or vmax is not None + # TODO: remove when min numpy version is bumped to 1.13 + # There's a cyclic dependency via DataArray, so we can't import from + # xarray.ufuncs in global scope. + from xarray.ufuncs import maximum, minimum + + # Calculate vmin and vmax automatically for `robust=True` + if robust: + if vmax is None: + vmax = np.nanpercentile(darray, 100 - ROBUST_PERCENTILE) + if vmin is None: + vmin = np.nanpercentile(darray, ROBUST_PERCENTILE) + # If not robust and one bound is None, calculate the default other bound + # and check that an interval between them exists. + elif vmax is None: + vmax = 255 if np.issubdtype(darray.dtype, np.integer) else 1 + if vmax < vmin: + raise ValueError( + 'vmin=%r is less than the default vmax (%r) - you must supply ' + 'a vmax > vmin in this case.' % (vmin, vmax)) + elif vmin is None: + vmin = 0 + if vmin > vmax: + raise ValueError( + 'vmax=%r is less than the default vmin (0) - you must supply ' + 'a vmin < vmax in this case.' % vmax) + # Scale interval [vmin .. vmax] to [0 .. 1], with darray as 64-bit float + # to avoid precision loss, integer over/underflow, etc with extreme inputs. + # After scaling, downcast to 32-bit float. This substantially reduces + # memory usage after we hand `darray` off to matplotlib. + darray = ((darray.astype('f8') - vmin) / (vmax - vmin)).astype('f4') + with warnings.catch_warnings(): + warnings.filterwarnings('ignore', 'xarray.ufuncs', + PendingDeprecationWarning) + return minimum(maximum(darray, 0), 1) + + +def _update_axes(ax, xincrease, yincrease, + xscale=None, yscale=None, + xticks=None, yticks=None, + xlim=None, ylim=None): + """ + Update axes with provided parameters + """ + if xincrease is None: + pass + elif xincrease and ax.xaxis_inverted(): + ax.invert_xaxis() + elif not xincrease and not ax.xaxis_inverted(): + ax.invert_xaxis() + + if yincrease is None: + pass + elif yincrease and ax.yaxis_inverted(): + ax.invert_yaxis() + elif not yincrease and not ax.yaxis_inverted(): + ax.invert_yaxis() + + # The default xscale, yscale needs to be None. + # If we set a scale it resets the axes formatters, + # This means that set_xscale('linear') on a datetime axis + # will remove the date labels. So only set the scale when explicitly + # asked to. https://github.com/matplotlib/matplotlib/issues/8740 + if xscale is not None: + ax.set_xscale(xscale) + if yscale is not None: + ax.set_yscale(yscale) + + if xticks is not None: + ax.set_xticks(xticks) + if yticks is not None: + ax.set_yticks(yticks) + + if xlim is not None: + ax.set_xlim(xlim) + if ylim is not None: + ax.set_ylim(ylim) + + +def _is_monotonic(coord, axis=0): + """ + >>> _is_monotonic(np.array([0, 1, 2])) + True + >>> _is_monotonic(np.array([2, 1, 0])) + True + >>> _is_monotonic(np.array([0, 2, 1])) + False + """ + if coord.shape[axis] < 3: + return True + else: + n = coord.shape[axis] + delta_pos = (coord.take(np.arange(1, n), axis=axis) >= + coord.take(np.arange(0, n - 1), axis=axis)) + delta_neg = (coord.take(np.arange(1, n), axis=axis) <= + coord.take(np.arange(0, n - 1), axis=axis)) + return np.all(delta_pos) or np.all(delta_neg) + + +def _infer_interval_breaks(coord, axis=0, check_monotonic=False): + """ + >>> _infer_interval_breaks(np.arange(5)) + array([-0.5, 0.5, 1.5, 2.5, 3.5, 4.5]) + >>> _infer_interval_breaks([[0, 1], [3, 4]], axis=1) + array([[-0.5, 0.5, 1.5], + [ 2.5, 3.5, 4.5]]) + """ + coord = np.asarray(coord) + + if check_monotonic and not _is_monotonic(coord, axis=axis): + raise ValueError("The input coordinate is not sorted in increasing " + "order along axis %d. This can lead to unexpected " + "results. Consider calling the `sortby` method on " + "the input DataArray. To plot data with categorical " + "axes, consider using the `heatmap` function from " + "the `seaborn` statistical plotting library." % axis) + + deltas = 0.5 * np.diff(coord, axis=axis) + if deltas.size == 0: + deltas = np.array(0.0) + first = np.take(coord, [0], axis=axis) - np.take(deltas, [0], axis=axis) + last = np.take(coord, [-1], axis=axis) + np.take(deltas, [-1], axis=axis) + trim_last = tuple(slice(None, -1) if n == axis else slice(None) + for n in range(coord.ndim)) + return np.concatenate([first, coord[trim_last] + deltas, last], axis=axis) From 07bdf54411a963b93d6308f3cbb931be59366998 Mon Sep 17 00:00:00 2001 From: dcherian Date: Sun, 13 Jan 2019 22:08:19 -0800 Subject: [PATCH 041/101] =?UTF-8?q?map=5Fscatter=20=E2=86=92=20map=5Fdatas?= =?UTF-8?q?et?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- xarray/plot/dataset_plot.py | 2 +- xarray/plot/facetgrid.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/xarray/plot/dataset_plot.py b/xarray/plot/dataset_plot.py index 70881feb3f1..7302cf82dd8 100644 --- a/xarray/plot/dataset_plot.py +++ b/xarray/plot/dataset_plot.py @@ -273,7 +273,7 @@ def __init__(self, dataset): self._ds = dataset def __call__(self, *args, **kwargs): - raise ValueError('Dataset.plot cannot be called directly. Use' + raise ValueError('Dataset.plot cannot be called directly. Use ' 'an explicit plot method, e.g. ds.plot.scatter(...)') @functools.wraps(scatter) diff --git a/xarray/plot/facetgrid.py b/xarray/plot/facetgrid.py index 407c8c6a4b8..0474be6c534 100644 --- a/xarray/plot/facetgrid.py +++ b/xarray/plot/facetgrid.py @@ -320,7 +320,7 @@ def map_dataarray_line(self, x=None, y=None, hue=None, **kwargs): return self - def map_scatter(self, x=None, y=None, hue=None, hue_style=None, + def map_dataset(self, func, x=None, y=None, hue=None, hue_style=None, add_colorbar=None, add_legend=None, **kwargs): from .dataset_plot import _infer_scatter_meta_data, scatter @@ -341,8 +341,8 @@ def map_scatter(self, x=None, y=None, hue=None, hue_style=None, # None is the sentinel value if d is not None: subset = self.data.loc[d] - maybe_mappable = scatter(subset, x=x, y=y, hue=hue, - ax=ax, **kwargs) + maybe_mappable = func(subset, x=x, y=y, hue=hue, + ax=ax, **kwargs) # TODO: this is needed to get legends to work. # but maybe_mappable is a list in that case :/ self._mappables.append(maybe_mappable) From 6df10c14cde2bcaf315dbbea5cb280aed5582351 Mon Sep 17 00:00:00 2001 From: dcherian Date: Sun, 13 Jan 2019 22:40:45 -0800 Subject: [PATCH 042/101] Use some wrapping magic to generalize code. --- xarray/plot/dataset_plot.py | 246 +++++++++++++++++++++++------------- xarray/plot/facetgrid.py | 10 +- xarray/tests/test_plot.py | 1 - 3 files changed, 162 insertions(+), 95 deletions(-) diff --git a/xarray/plot/dataset_plot.py b/xarray/plot/dataset_plot.py index 7302cf82dd8..147fe4cef4d 100644 --- a/xarray/plot/dataset_plot.py +++ b/xarray/plot/dataset_plot.py @@ -11,8 +11,8 @@ _valid_other_type, get_axis, label_from_attrs) -def _infer_scatter_meta_data(ds, x, y, hue, hue_style, add_colorbar, - add_legend): +def _infer_meta_data(ds, x, y, hue, hue_style, add_colorbar, + add_legend): dvars = set(ds.data_vars.keys()) error_msg = (' must be either one of ({0:s})' .format(', '.join(dvars))) @@ -79,6 +79,32 @@ def _infer_scatter_meta_data(ds, x, y, hue, hue_style, add_colorbar, 'hue_values': hue_values} +def _easy_facetgrid(ds, plotfunc, x, y, row=None, col=None, + col_wrap=None, sharex=True, sharey=True, aspect=None, + size=None, subplot_kws=None, **kwargs): + """ + Convenience method to call xarray.plot.FacetGrid from 2d plotting methods + + kwargs are the arguments to 2d plotting method + """ + ax = kwargs.pop('ax', None) + figsize = kwargs.pop('figsize', None) + if ax is not None: + raise ValueError("Can't use axes when making faceted plots.") + if aspect is None: + aspect = 1 + if size is None: + size = 3 + elif figsize is not None: + raise ValueError('cannot provide both `figsize` and `size` arguments') + + g = FacetGrid(data=ds, col=col, row=row, col_wrap=col_wrap, + sharex=sharex, sharey=sharey, figsize=figsize, + aspect=aspect, size=size, subplot_kws=subplot_kws) + + return g.map_dataset(plotfunc, x, y, **kwargs) + + def _infer_scatter_data(ds, x, y, hue): data = {'x': ds[x].values.flatten(), @@ -90,15 +116,22 @@ def _infer_scatter_data(ds, x, y, hue): return data -def scatter(ds, x, y, hue=None, hue_style=None, col=None, row=None, - col_wrap=None, sharex=True, sharey=True, aspect=None, - size=None, subplot_kws=None, add_colorbar=None, cbar_kwargs=None, - add_legend=None, cbar_ax=None, vmin=None, vmax=None, - norm=None, infer_intervals=None, center=None, levels=None, - robust=None, colors=None, extend=None, cmap=None, **kwargs): - ''' - Scatter Dataset variables against each other. +class _Dataset_PlotMethods(object): + """ + Enables use of xarray.plot functions as attributes on a Dataset. + For example, Dataset.plot.scatter + """ + + def __init__(self, dataset): + self._ds = dataset + + def __call__(self, *args, **kwargs): + raise ValueError('Dataset.plot cannot be called directly. Use ' + 'an explicit plot method, e.g. ds.plot.scatter(...)') + +def _dsplot(plotfunc): + commondoc = """ Input ----- @@ -169,54 +202,50 @@ def scatter(ds, x, y, hue=None, hue_style=None, col=None, row=None, setting ``levels=np.linspace(vmin, vmax, N)``. **kwargs : optional Additional keyword arguments to matplotlib - ''' + """ - if kwargs.get('_meta_data', None): # facetgrid call - meta_data = kwargs['_meta_data'] - else: - meta_data = _infer_scatter_meta_data(ds, x, y, hue, hue_style, - add_colorbar, add_legend) + # Build on the original docstring + plotfunc.__doc__ = '%s\n%s' % (plotfunc.__doc__, commondoc) + + @functools.wraps(plotfunc) + def newplotfunc(ds, x=None, y=None, hue=None, hue_style=None, + col=None, row=None, ax=None, figsize=None, size=None, + col_wrap=None, sharex=True, sharey=True, aspect=None, + subplot_kws=None, add_colorbar=None, cbar_kwargs=None, + add_legend=None, cbar_ax=None, vmin=None, vmax=None, + norm=None, infer_intervals=None, center=None, levels=None, + robust=None, colors=None, extend=None, cmap=None, + **kwargs): + + if kwargs.get('_meta_data', None): # facetgrid call + meta_data = kwargs['_meta_data'] + else: + meta_data = _infer_meta_data(ds, x, y, hue, hue_style, + add_colorbar, add_legend) - hue_style = meta_data['hue_style'] - add_legend = meta_data['add_legend'] - add_colorbar = meta_data['add_colorbar'] + hue_style = meta_data['hue_style'] + add_legend = meta_data['add_legend'] + add_colorbar = meta_data['add_colorbar'] - if col or row: - ax = kwargs.pop('ax', None) - figsize = kwargs.pop('figsize', None) - if ax is not None: - raise ValueError("Can't use axes when making faceted plots.") - if aspect is None: - aspect = 1 - if size is None: - size = 3 - elif figsize is not None: - raise ValueError('Cannot provide both `figsize` and ' - '`size` arguments') - - g = FacetGrid(data=ds, col=col, row=row, col_wrap=col_wrap, - sharex=sharex, sharey=sharey, figsize=figsize, - aspect=aspect, size=size, subplot_kws=subplot_kws) - return g.map_scatter(x=x, y=y, hue=hue, hue_style=hue_style, - add_colorbar=add_colorbar, - add_legend=add_legend, **kwargs) + # handle facetgrids first + if col or row: + allargs = locals().copy() + allargs['plotfunc'] = globals()[plotfunc.__name__] - figsize = kwargs.pop('figsize', None) - ax = kwargs.pop('ax', None) - ax = get_axis(figsize, size, aspect, ax) + # TODO dcherian: why do I need to remove kwargs? + for arg in ['meta_data', 'kwargs']: + del allargs[arg] - kwargs = kwargs.copy() - _meta_data = kwargs.pop('_meta_data', None) + return _easy_facetgrid(**allargs) - if hue_style == 'discrete': - primitive = [] - for label, grp in ds.groupby(ds[hue]): - data = _infer_scatter_data(grp, x, y, hue=None) - primitive.append(ax.scatter(data['x'], data['y'], label=label, - **kwargs)) - elif hue is None or hue_style == 'continuous': - data = _infer_scatter_data(ds, x, y, hue) - if hue is not None: + figsize = kwargs.pop('figsize', None) + ax = kwargs.pop('ax', None) + ax = get_axis(figsize, size, aspect, ax) + + kwargs = kwargs.copy() + _meta_data = kwargs.pop('_meta_data', None) + + if hue_style == 'continuous' and hue is not None: if _meta_data: cbar_kwargs = _meta_data['cbar_kwargs'] cmap_params = _meta_data['cmap_params'] @@ -231,51 +260,90 @@ def scatter(ds, x, y, hue=None, hue_style=None, col=None, row=None, 'levels': levels, 'filled': None, 'norm': norm} + cmap_params = _determine_cmap_params(**cmap_kwargs) - cmap_kwargs_subset = dict( + # subset that can be passed to scatter, hist2d + cmap_params_subset = dict( (vv, cmap_params[vv]) for vv in ['vmin', 'vmax', 'norm', 'cmap']) - else: - cmap_kwargs_subset = {} - primitive = ax.scatter(data['x'], data['y'], c=data['color'], - **cmap_kwargs_subset, **kwargs) - - if _meta_data: # if this was called from map_scatter, - return primitive # finish here. Else, make labels - - if meta_data.get('xlabel', None): - ax.set_xlabel(meta_data.get('xlabel')) - if meta_data.get('ylabel', None): - ax.set_ylabel(meta_data.get('ylabel')) - - if add_legend: - ax.legend(handles=primitive, - labels=list(meta_data['hue_values'].values), - title=meta_data.get('hue_label', None)) - if add_colorbar: - cbar_kwargs = {} if cbar_kwargs is None else cbar_kwargs - if 'label' not in cbar_kwargs: - cbar_kwargs['label'] = meta_data.get('hue_label', None) - _add_colorbar(primitive, ax, cbar_ax, cbar_kwargs, cmap_params) + else: + cmap_params_subset = {} + + primitive = plotfunc(ax, ds, x, y, hue, hue_style, + cmap_params=cmap_params_subset, **kwargs) + + if _meta_data: # if this was called from Facetgrid.map_dataset, + return primitive # finish here. Else, make labels + + if meta_data.get('xlabel', None): + ax.set_xlabel(meta_data.get('xlabel')) + if meta_data.get('ylabel', None): + ax.set_ylabel(meta_data.get('ylabel')) + + if add_legend: + ax.legend(handles=primitive, + labels=list(meta_data['hue_values'].values), + title=meta_data.get('hue_label', None)) + if add_colorbar: + cbar_kwargs = {} if cbar_kwargs is None else cbar_kwargs + if 'label' not in cbar_kwargs: + cbar_kwargs['label'] = meta_data.get('hue_label', None) + _add_colorbar(primitive, ax, cbar_ax, cbar_kwargs, cmap_params) + + return primitive + + @functools.wraps(newplotfunc) + def plotmethod(_PlotMethods_obj, x=None, y=None, hue=None, + hue_style=None, col=None, row=None, ax=None, + figsize=None, + col_wrap=None, sharex=True, sharey=True, aspect=None, + size=None, subplot_kws=None, add_colorbar=None, + cbar_kwargs=None, + add_legend=None, cbar_ax=None, vmin=None, vmax=None, + norm=None, infer_intervals=None, center=None, levels=None, + robust=None, colors=None, extend=None, cmap=None, + **kwargs): + """ + The method should have the same signature as the function. + + This just makes the method work on Plotmethods objects, + and passes all the other arguments straight through. + """ + allargs = locals() + allargs['ds'] = _PlotMethods_obj._ds + allargs.update(kwargs) + for arg in ['_PlotMethods_obj', 'newplotfunc', 'kwargs']: + del allargs[arg] + return newplotfunc(**allargs) + + # Add to class _PlotMethods + setattr(_Dataset_PlotMethods, plotmethod.__name__, plotmethod) + + return newplotfunc + + +@_dsplot +def scatter(ax, ds, x, y, hue, hue_style, **kwargs): + """ Scatter Dataset data variables against each other. """ + + cmap_params = kwargs.pop('cmap_params') - return primitive + if hue_style == 'discrete': + primitive = [] + for label, grp in ds.groupby(ds[hue]): + data = _infer_scatter_data(grp, x, y, hue=None) + primitive.append(ax.scatter(data['x'], data['y'], label=label, + **kwargs)) + elif hue is None or hue_style == 'continuous': + data = _infer_scatter_data(ds, x, y, hue) -class _Dataset_PlotMethods(object): - """ - Enables use of xarray.plot functions as attributes on a Dataset. - For example, Dataset.plot.scatter - """ + primitive = ax.scatter(data['x'], data['y'], c=data['color'], + **cmap_params, **kwargs) - def __init__(self, dataset): - self._ds = dataset + return primitive - def __call__(self, *args, **kwargs): - raise ValueError('Dataset.plot cannot be called directly. Use ' - 'an explicit plot method, e.g. ds.plot.scatter(...)') - @functools.wraps(scatter) - def scatter(self, *args, **kwargs): - return scatter(self._ds, *args, **kwargs) + return primitive diff --git a/xarray/plot/facetgrid.py b/xarray/plot/facetgrid.py index 0474be6c534..7f94328d530 100644 --- a/xarray/plot/facetgrid.py +++ b/xarray/plot/facetgrid.py @@ -322,18 +322,18 @@ def map_dataarray_line(self, x=None, y=None, hue=None, **kwargs): def map_dataset(self, func, x=None, y=None, hue=None, hue_style=None, add_colorbar=None, add_legend=None, **kwargs): - from .dataset_plot import _infer_scatter_meta_data, scatter + from .dataset_plot import _infer_meta_data kwargs['add_legend'] = False kwargs['add_colorbar'] = False - meta_data = _infer_scatter_meta_data(self.data, x, y, hue, hue_style, - add_legend, add_colorbar) + meta_data = _infer_meta_data(self.data, x, y, hue, hue_style, + add_legend, add_colorbar) kwargs['_meta_data'] = meta_data if hue and meta_data['hue_style'] == 'continuous': - cmap_params, cbar_kwargs = self._process_cmap(scatter, kwargs, - self.data[hue]) + cmap_params, cbar_kwargs = self._process_cmap( + func, kwargs, self.data[hue]) kwargs['_meta_data']['cmap_params'] = cmap_params kwargs['_meta_data']['cbar_kwargs'] = cbar_kwargs diff --git a/xarray/tests/test_plot.py b/xarray/tests/test_plot.py index f16282fbe6e..ef761d92830 100644 --- a/xarray/tests/test_plot.py +++ b/xarray/tests/test_plot.py @@ -12,7 +12,6 @@ from xarray import DataArray, Dataset from xarray.coding.times import _import_cftime from xarray.plot.plot import _infer_interval_breaks -from xarray.plot.dataset_plot import _infer_scatter_meta_data from xarray.plot.utils import ( _build_discrete_cmap, _color_palette, _determine_cmap_params, import_seaborn, label_from_attrs) From a12378c59979c3e24199f8d9b93acad8bea4d70a Mon Sep 17 00:00:00 2001 From: dcherian Date: Sun, 13 Jan 2019 23:10:48 -0800 Subject: [PATCH 043/101] Add hist as test of generalization. --- xarray/plot/dataset_plot.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/xarray/plot/dataset_plot.py b/xarray/plot/dataset_plot.py index 147fe4cef4d..d4b88d5df9d 100644 --- a/xarray/plot/dataset_plot.py +++ b/xarray/plot/dataset_plot.py @@ -346,4 +346,14 @@ def scatter(ax, ds, x, y, hue, hue_style, **kwargs): return primitive +@_dsplot +def hist(ds, x, y, hue, hue_style, ax, **kwargs): + + cmap_params = kwargs.pop('cmap_params') + + xplt, yplt = broadcast(ds[x], ds[y]) + _, _, _, primitive = ax.hist2d(ds[x].values.ravel(), + ds[y].values.ravel(), + **cmap_params, **kwargs) + return primitive From 1d939afd9db9098581326038bb3e5b545a8be432 Mon Sep 17 00:00:00 2001 From: dcherian Date: Sun, 13 Jan 2019 23:48:59 -0800 Subject: [PATCH 044/101] Get facetgrid working again --- xarray/plot/dataset_plot.py | 52 ++++++++++--------------------------- xarray/plot/facetgrid.py | 4 ++- 2 files changed, 16 insertions(+), 40 deletions(-) diff --git a/xarray/plot/dataset_plot.py b/xarray/plot/dataset_plot.py index d4b88d5df9d..8ee4c2e18eb 100644 --- a/xarray/plot/dataset_plot.py +++ b/xarray/plot/dataset_plot.py @@ -5,10 +5,10 @@ import numpy as np from ..core.alignment import broadcast -from .facetgrid import FacetGrid +from .facetgrid import _easy_facetgrid from .utils import ( - _add_colorbar, _determine_cmap_params, _ensure_numeric, - _valid_other_type, get_axis, label_from_attrs) + _add_colorbar, _determine_cmap_params, + _ensure_numeric, _valid_other_type, get_axis, label_from_attrs) def _infer_meta_data(ds, x, y, hue, hue_style, add_colorbar, @@ -79,32 +79,6 @@ def _infer_meta_data(ds, x, y, hue, hue_style, add_colorbar, 'hue_values': hue_values} -def _easy_facetgrid(ds, plotfunc, x, y, row=None, col=None, - col_wrap=None, sharex=True, sharey=True, aspect=None, - size=None, subplot_kws=None, **kwargs): - """ - Convenience method to call xarray.plot.FacetGrid from 2d plotting methods - - kwargs are the arguments to 2d plotting method - """ - ax = kwargs.pop('ax', None) - figsize = kwargs.pop('figsize', None) - if ax is not None: - raise ValueError("Can't use axes when making faceted plots.") - if aspect is None: - aspect = 1 - if size is None: - size = 3 - elif figsize is not None: - raise ValueError('cannot provide both `figsize` and `size` arguments') - - g = FacetGrid(data=ds, col=col, row=row, col_wrap=col_wrap, - sharex=sharex, sharey=sharey, figsize=figsize, - aspect=aspect, size=size, subplot_kws=subplot_kws) - - return g.map_dataset(plotfunc, x, y, **kwargs) - - def _infer_scatter_data(ds, x, y, hue): data = {'x': ds[x].values.flatten(), @@ -231,18 +205,17 @@ def newplotfunc(ds, x=None, y=None, hue=None, hue_style=None, if col or row: allargs = locals().copy() allargs['plotfunc'] = globals()[plotfunc.__name__] - + allargs['data'] = ds # TODO dcherian: why do I need to remove kwargs? - for arg in ['meta_data', 'kwargs']: + for arg in ['meta_data', 'kwargs', 'ds']: del allargs[arg] - return _easy_facetgrid(**allargs) + return _easy_facetgrid(kind='dataset', **allargs) figsize = kwargs.pop('figsize', None) - ax = kwargs.pop('ax', None) ax = get_axis(figsize, size, aspect, ax) - - kwargs = kwargs.copy() + # TODO dcherian: _meta_data should not be needed + # I'm trying to avoid calling _determine_cmap_params multiple times _meta_data = kwargs.pop('_meta_data', None) if hue_style == 'continuous' and hue is not None: @@ -271,8 +244,10 @@ def newplotfunc(ds, x=None, y=None, hue=None, hue_style=None, else: cmap_params_subset = {} - primitive = plotfunc(ax, ds, x, y, hue, hue_style, - cmap_params=cmap_params_subset, **kwargs) + # TODO dcherian: hue, hue_style shouldn't be needed for all methods + # update signatures + primitive = plotfunc(ds=ds, x=x, y=y, hue=hue, hue_style=hue_style, + ax=ax, cmap_params=cmap_params_subset, **kwargs) if _meta_data: # if this was called from Facetgrid.map_dataset, return primitive # finish here. Else, make labels @@ -325,9 +300,8 @@ def plotmethod(_PlotMethods_obj, x=None, y=None, hue=None, @_dsplot -def scatter(ax, ds, x, y, hue, hue_style, **kwargs): +def scatter(ds, x, y, hue, hue_style, ax, **kwargs): """ Scatter Dataset data variables against each other. """ - cmap_params = kwargs.pop('cmap_params') if hue_style == 'discrete': diff --git a/xarray/plot/facetgrid.py b/xarray/plot/facetgrid.py index 7f94328d530..fb67a1f9a33 100644 --- a/xarray/plot/facetgrid.py +++ b/xarray/plot/facetgrid.py @@ -341,7 +341,8 @@ def map_dataset(self, func, x=None, y=None, hue=None, hue_style=None, # None is the sentinel value if d is not None: subset = self.data.loc[d] - maybe_mappable = func(subset, x=x, y=y, hue=hue, + maybe_mappable = func(ds=subset, x=x, y=y, + hue=hue, hue_style=hue_style, ax=ax, **kwargs) # TODO: this is needed to get legends to work. # but maybe_mappable is a list in that case :/ @@ -597,4 +598,5 @@ def _easy_facetgrid(data, plotfunc, x=None, y=None, kind=None, row=None, col=Non return g.map_dataarray(plotfunc, x, y, **kwargs) elif kind == 'array line': return g.map_dataarray_line(hue=kwargs.pop('hue'), **kwargs) + elif kind == 'dataset': return g.map_dataset(plotfunc, x, y, **kwargs) From 361f7a84c16712e20f3248312ad8d6b37eff15f9 Mon Sep 17 00:00:00 2001 From: dcherian Date: Mon, 14 Jan 2019 00:48:49 -0800 Subject: [PATCH 045/101] Refactor out utility functions. --- xarray/plot/plot.py | 235 ---------------------------------------- xarray/plot/utils.py | 248 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 248 insertions(+), 235 deletions(-) diff --git a/xarray/plot/plot.py b/xarray/plot/plot.py index 1f7b8d8587a..78501033cc1 100644 --- a/xarray/plot/plot.py +++ b/xarray/plot/plot.py @@ -9,7 +9,6 @@ import functools import warnings -from datetime import datetime import numpy as np import pandas as pd @@ -19,39 +18,6 @@ from .facetgrid import FacetGrid from .utils import ( - ROBUST_PERCENTILE, _determine_cmap_params, _infer_xy_labels, - _interval_to_double_bound_points, _interval_to_mid_points, - _resolve_intervals_2dplot, _valid_other_type, get_axis, - import_matplotlib_pyplot, label_from_attrs) - - -def _valid_numpy_subdtype(x, numpy_types): - """ - Is any dtype from numpy_types superior to the dtype of x? - """ - # If any of the types given in numpy_types is understood as numpy.generic, - # all possible x will be considered valid. This is probably unwanted. - for t in numpy_types: - assert not np.issubdtype(np.generic, t) - - return any(np.issubdtype(x.dtype, t) for t in numpy_types) - - -def _ensure_plottable(*args): - """ - Raise exception if there is anything in args that can't be plotted on an - axis by matplotlib. - """ - numpy_types = [np.floating, np.integer, np.timedelta64, np.datetime64] - other_types = [datetime] - - for x in args: - if not (_valid_numpy_subdtype(np.array(x), numpy_types) - or _valid_other_type(np.array(x), other_types)): - raise TypeError('Plotting requires coordinates to be numeric ' - 'or dates of type np.datetime64 or ' - 'datetime.datetime or pd.Interval.') - def _easy_facetgrid(darray, plotfunc, x, y, row=None, col=None, col_wrap=None, sharex=True, sharey=True, aspect=None, @@ -187,79 +153,6 @@ def plot(darray, row=None, col=None, col_wrap=None, ax=None, hue=None, return plotfunc(darray, **kwargs) -def _infer_line_data(darray, x, y, hue): - 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.') - - 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] - yplt = darray - - else: - yplt = darray[dim] - xplt = darray - - else: - 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 - - # This function signature should not change so that it can use # matplotlib format strings def line(darray, *args, **kwargs): @@ -495,48 +388,6 @@ def hist(darray, figsize=None, size=None, aspect=None, ax=None, **kwargs): return primitive -def _update_axes(ax, xincrease, yincrease, - xscale=None, yscale=None, - xticks=None, yticks=None, - xlim=None, ylim=None): - """ - Update axes with provided parameters - """ - if xincrease is None: - pass - elif xincrease and ax.xaxis_inverted(): - ax.invert_xaxis() - elif not xincrease and not ax.xaxis_inverted(): - ax.invert_xaxis() - - if yincrease is None: - pass - elif yincrease and ax.yaxis_inverted(): - ax.invert_yaxis() - elif not yincrease and not ax.yaxis_inverted(): - ax.invert_yaxis() - - # The default xscale, yscale needs to be None. - # If we set a scale it resets the axes formatters, - # This means that set_xscale('linear') on a datetime axis - # will remove the date labels. So only set the scale when explicitly - # asked to. https://github.com/matplotlib/matplotlib/issues/8740 - if xscale is not None: - ax.set_xscale(xscale) - if yscale is not None: - ax.set_yscale(yscale) - - if xticks is not None: - ax.set_xticks(xticks) - if yticks is not None: - ax.set_yticks(yticks) - - if xlim is not None: - ax.set_xlim(xlim) - if ylim is not None: - ax.set_ylim(ylim) - - # MUST run before any 2d plotting functions are defined since # _plot2d decorator adds them as methods here. class _PlotMethods(object): @@ -564,44 +415,6 @@ def step(self, *args, **kwargs): return step(self._da, *args, **kwargs) -def _rescale_imshow_rgb(darray, vmin, vmax, robust): - assert robust or vmin is not None or vmax is not None - # TODO: remove when min numpy version is bumped to 1.13 - # There's a cyclic dependency via DataArray, so we can't import from - # xarray.ufuncs in global scope. - from xarray.ufuncs import maximum, minimum - - # Calculate vmin and vmax automatically for `robust=True` - if robust: - if vmax is None: - vmax = np.nanpercentile(darray, 100 - ROBUST_PERCENTILE) - if vmin is None: - vmin = np.nanpercentile(darray, ROBUST_PERCENTILE) - # If not robust and one bound is None, calculate the default other bound - # and check that an interval between them exists. - elif vmax is None: - vmax = 255 if np.issubdtype(darray.dtype, np.integer) else 1 - if vmax < vmin: - raise ValueError( - 'vmin=%r is less than the default vmax (%r) - you must supply ' - 'a vmax > vmin in this case.' % (vmin, vmax)) - elif vmin is None: - vmin = 0 - if vmin > vmax: - raise ValueError( - 'vmax=%r is less than the default vmin (0) - you must supply ' - 'a vmin < vmax in this case.' % vmax) - # Scale interval [vmin .. vmax] to [0 .. 1], with darray as 64-bit float - # to avoid precision loss, integer over/underflow, etc with extreme inputs. - # After scaling, downcast to 32-bit float. This substantially reduces - # memory usage after we hand `darray` off to matplotlib. - darray = ((darray.astype('f8') - vmin) / (vmax - vmin)).astype('f4') - with warnings.catch_warnings(): - warnings.filterwarnings('ignore', 'xarray.ufuncs', - PendingDeprecationWarning) - return minimum(maximum(darray, 0), 1) - - def _plot2d(plotfunc): """ Decorator for common 2d plotting logic @@ -1019,54 +832,6 @@ def contourf(x, y, z, ax, **kwargs): return primitive -def _is_monotonic(coord, axis=0): - """ - >>> _is_monotonic(np.array([0, 1, 2])) - True - >>> _is_monotonic(np.array([2, 1, 0])) - True - >>> _is_monotonic(np.array([0, 2, 1])) - False - """ - if coord.shape[axis] < 3: - return True - else: - n = coord.shape[axis] - delta_pos = (coord.take(np.arange(1, n), axis=axis) >= - coord.take(np.arange(0, n - 1), axis=axis)) - delta_neg = (coord.take(np.arange(1, n), axis=axis) <= - coord.take(np.arange(0, n - 1), axis=axis)) - return np.all(delta_pos) or np.all(delta_neg) - - -def _infer_interval_breaks(coord, axis=0, check_monotonic=False): - """ - >>> _infer_interval_breaks(np.arange(5)) - array([-0.5, 0.5, 1.5, 2.5, 3.5, 4.5]) - >>> _infer_interval_breaks([[0, 1], [3, 4]], axis=1) - array([[-0.5, 0.5, 1.5], - [ 2.5, 3.5, 4.5]]) - """ - coord = np.asarray(coord) - - if check_monotonic and not _is_monotonic(coord, axis=axis): - raise ValueError("The input coordinate is not sorted in increasing " - "order along axis %d. This can lead to unexpected " - "results. Consider calling the `sortby` method on " - "the input DataArray. To plot data with categorical " - "axes, consider using the `heatmap` function from " - "the `seaborn` statistical plotting library." % axis) - - deltas = 0.5 * np.diff(coord, axis=axis) - if deltas.size == 0: - deltas = np.array(0.0) - first = np.take(coord, [0], axis=axis) - np.take(deltas, [0], axis=axis) - last = np.take(coord, [-1], axis=axis) + np.take(deltas, [-1], axis=axis) - trim_last = tuple(slice(None, -1) if n == axis else slice(None) - for n in range(coord.ndim)) - return np.concatenate([first, coord[trim_last] + deltas, last], axis=axis) - - @_plot2d def pcolormesh(x, y, z, ax, infer_intervals=None, **kwargs): """ diff --git a/xarray/plot/utils.py b/xarray/plot/utils.py index 41f61554739..c811b5b4427 100644 --- a/xarray/plot/utils.py +++ b/xarray/plot/utils.py @@ -3,6 +3,7 @@ import itertools import textwrap import warnings +from datetime import datetime import numpy as np import pandas as pd @@ -340,6 +341,79 @@ def _infer_xy_labels(darray, x, y, imshow=False, rgb=None): return x, y +def _infer_line_data(darray, x, y, hue): + 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.') + + 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] + yplt = darray + + else: + yplt = darray[dim] + xplt = darray + + else: + 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 get_axis(figsize, size, aspect, ax): import matplotlib as mpl import matplotlib.pyplot as plt @@ -450,3 +524,177 @@ def _valid_other_type(x, types): Do all elements of x have a type from types? """ return all(any(isinstance(el, t) for t in types) for el in np.ravel(x)) + + +def _valid_numpy_subdtype(x, numpy_types): + """ + Is any dtype from numpy_types superior to the dtype of x? + """ + # If any of the types given in numpy_types is understood as numpy.generic, + # all possible x will be considered valid. This is probably unwanted. + for t in numpy_types: + assert not np.issubdtype(np.generic, t) + + return any(np.issubdtype(x.dtype, t) for t in numpy_types) + + +def _ensure_plottable(*args): + """ + Raise exception if there is anything in args that can't be plotted on an + axis by matplotlib. + """ + numpy_types = [np.floating, np.integer, np.timedelta64, np.datetime64] + other_types = [datetime] + + for x in args: + if not (_valid_numpy_subdtype(np.array(x), numpy_types) + or _valid_other_type(np.array(x), other_types)): + raise TypeError('Plotting requires coordinates to be numeric ' + 'or dates of type np.datetime64 or ' + 'datetime.datetime or pd.Interval.') + + +def _ensure_numeric(arr): + numpy_types = [np.floating, np.integer] + return _valid_numpy_subdtype(arr, numpy_types) + + +def _add_colorbar(primitive, ax, cbar_ax, cbar_kwargs, cmap_params): + plt = import_matplotlib_pyplot() + cbar_kwargs.setdefault('extend', cmap_params['extend']) + if cbar_ax is None: + cbar_kwargs.setdefault('ax', ax) + else: + cbar_kwargs.setdefault('cax', cbar_ax) + + cbar = plt.colorbar(primitive, **cbar_kwargs) + + return cbar + + +def _rescale_imshow_rgb(darray, vmin, vmax, robust): + assert robust or vmin is not None or vmax is not None + # TODO: remove when min numpy version is bumped to 1.13 + # There's a cyclic dependency via DataArray, so we can't import from + # xarray.ufuncs in global scope. + from xarray.ufuncs import maximum, minimum + + # Calculate vmin and vmax automatically for `robust=True` + if robust: + if vmax is None: + vmax = np.nanpercentile(darray, 100 - ROBUST_PERCENTILE) + if vmin is None: + vmin = np.nanpercentile(darray, ROBUST_PERCENTILE) + # If not robust and one bound is None, calculate the default other bound + # and check that an interval between them exists. + elif vmax is None: + vmax = 255 if np.issubdtype(darray.dtype, np.integer) else 1 + if vmax < vmin: + raise ValueError( + 'vmin=%r is less than the default vmax (%r) - you must supply ' + 'a vmax > vmin in this case.' % (vmin, vmax)) + elif vmin is None: + vmin = 0 + if vmin > vmax: + raise ValueError( + 'vmax=%r is less than the default vmin (0) - you must supply ' + 'a vmin < vmax in this case.' % vmax) + # Scale interval [vmin .. vmax] to [0 .. 1], with darray as 64-bit float + # to avoid precision loss, integer over/underflow, etc with extreme inputs. + # After scaling, downcast to 32-bit float. This substantially reduces + # memory usage after we hand `darray` off to matplotlib. + darray = ((darray.astype('f8') - vmin) / (vmax - vmin)).astype('f4') + with warnings.catch_warnings(): + warnings.filterwarnings('ignore', 'xarray.ufuncs', + PendingDeprecationWarning) + return minimum(maximum(darray, 0), 1) + + +def _update_axes(ax, xincrease, yincrease, + xscale=None, yscale=None, + xticks=None, yticks=None, + xlim=None, ylim=None): + """ + Update axes with provided parameters + """ + if xincrease is None: + pass + elif xincrease and ax.xaxis_inverted(): + ax.invert_xaxis() + elif not xincrease and not ax.xaxis_inverted(): + ax.invert_xaxis() + + if yincrease is None: + pass + elif yincrease and ax.yaxis_inverted(): + ax.invert_yaxis() + elif not yincrease and not ax.yaxis_inverted(): + ax.invert_yaxis() + + # The default xscale, yscale needs to be None. + # If we set a scale it resets the axes formatters, + # This means that set_xscale('linear') on a datetime axis + # will remove the date labels. So only set the scale when explicitly + # asked to. https://github.com/matplotlib/matplotlib/issues/8740 + if xscale is not None: + ax.set_xscale(xscale) + if yscale is not None: + ax.set_yscale(yscale) + + if xticks is not None: + ax.set_xticks(xticks) + if yticks is not None: + ax.set_yticks(yticks) + + if xlim is not None: + ax.set_xlim(xlim) + if ylim is not None: + ax.set_ylim(ylim) + + +def _is_monotonic(coord, axis=0): + """ + >>> _is_monotonic(np.array([0, 1, 2])) + True + >>> _is_monotonic(np.array([2, 1, 0])) + True + >>> _is_monotonic(np.array([0, 2, 1])) + False + """ + if coord.shape[axis] < 3: + return True + else: + n = coord.shape[axis] + delta_pos = (coord.take(np.arange(1, n), axis=axis) >= + coord.take(np.arange(0, n - 1), axis=axis)) + delta_neg = (coord.take(np.arange(1, n), axis=axis) <= + coord.take(np.arange(0, n - 1), axis=axis)) + return np.all(delta_pos) or np.all(delta_neg) + + +def _infer_interval_breaks(coord, axis=0, check_monotonic=False): + """ + >>> _infer_interval_breaks(np.arange(5)) + array([-0.5, 0.5, 1.5, 2.5, 3.5, 4.5]) + >>> _infer_interval_breaks([[0, 1], [3, 4]], axis=1) + array([[-0.5, 0.5, 1.5], + [ 2.5, 3.5, 4.5]]) + """ + coord = np.asarray(coord) + + if check_monotonic and not _is_monotonic(coord, axis=axis): + raise ValueError("The input coordinate is not sorted in increasing " + "order along axis %d. This can lead to unexpected " + "results. Consider calling the `sortby` method on " + "the input DataArray. To plot data with categorical " + "axes, consider using the `heatmap` function from " + "the `seaborn` statistical plotting library." % axis) + + deltas = 0.5 * np.diff(coord, axis=axis) + if deltas.size == 0: + deltas = np.array(0.0) + first = np.take(coord, [0], axis=axis) - np.take(deltas, [0], axis=axis) + last = np.take(coord, [-1], axis=axis) + np.take(deltas, [-1], axis=axis) + trim_last = tuple(slice(None, -1) if n == axis else slice(None) + for n in range(coord.ndim)) + return np.concatenate([first, coord[trim_last] + deltas, last], axis=axis) From f0f1480fbae31493eab32979b2b0a6da976ac496 Mon Sep 17 00:00:00 2001 From: dcherian Date: Mon, 14 Jan 2019 00:49:11 -0800 Subject: [PATCH 046/101] facetgrid refactor 1. refactor out _easy_facetgrid 2. Combine map_dataarray_line with map_dataarray --- xarray/plot/facetgrid.py | 151 ++++++++++++++++++++------------------- xarray/plot/plot.py | 64 +++-------------- 2 files changed, 89 insertions(+), 126 deletions(-) diff --git a/xarray/plot/facetgrid.py b/xarray/plot/facetgrid.py index f133e7806a3..d982b2690b3 100644 --- a/xarray/plot/facetgrid.py +++ b/xarray/plot/facetgrid.py @@ -9,8 +9,8 @@ from ..core.formatting import format_item from ..core.pycompat import getargspec from .utils import ( - _determine_cmap_params, _infer_xy_labels, import_matplotlib_pyplot, - label_from_attrs) + _determine_cmap_params, _infer_line_data, + _infer_xy_labels, import_matplotlib_pyplot, label_from_attrs) # Overrides axes.labelsize, xtick.major.size, ytick.major.size # from mpl.rcParams @@ -221,95 +221,74 @@ def map_dataarray(self, func, x, y, **kwargs): """ - cmapkw = kwargs.get('cmap') - colorskw = kwargs.get('colors') - cbar_kwargs = kwargs.pop('cbar_kwargs', {}) - cbar_kwargs = {} if cbar_kwargs is None else dict(cbar_kwargs) + if func.__name__ == 'line': + add_legend = kwargs.pop('add_legend', True) + kwargs['add_legend'] = False + func_kwargs = kwargs.copy() + func_kwargs['_labels'] = False - if kwargs.get('cbar_ax', None) is not None: - raise ValueError('cbar_ax not supported by FacetGrid.') + else: + cmapkw = kwargs.get('cmap') + colorskw = kwargs.get('colors') + cbar_kwargs = kwargs.pop('cbar_kwargs', {}) + cbar_kwargs = {} if cbar_kwargs is None else dict(cbar_kwargs) + + if kwargs.get('cbar_ax', None) is not None: + raise ValueError('cbar_ax not supported by FacetGrid.') - # colors is mutually exclusive with cmap - if cmapkw and colorskw: - raise ValueError("Can't specify both cmap and colors.") + # colors is mutually exclusive with cmap + if cmapkw and colorskw: + raise ValueError("Can't specify both cmap and colors.") - # These should be consistent with xarray.plot._plot2d - cmap_kwargs = {'plot_data': self.data.values, - # MPL default - 'levels': 7 if 'contour' in func.__name__ else None, - 'filled': func.__name__ != 'contour', - } + # These should be consistent with xarray.plot._plot2d + cmap_kwargs = {'plot_data': self.data.values, + # MPL default + 'levels': 7 if 'contour' in func.__name__ else None, + 'filled': func.__name__ != 'contour', + } - cmap_args = getargspec(_determine_cmap_params).args - cmap_kwargs.update((a, kwargs[a]) for a in cmap_args if a in kwargs) + cmap_args = getargspec(_determine_cmap_params).args + cmap_kwargs.update((a, kwargs[a]) for a in cmap_args if a in kwargs) - cmap_params = _determine_cmap_params(**cmap_kwargs) + cmap_params = _determine_cmap_params(**cmap_kwargs) - if colorskw is not None: - cmap_params['cmap'] = None + if colorskw is not None: + cmap_params['cmap'] = None - # Order is important - func_kwargs = kwargs.copy() - func_kwargs.update(cmap_params) - func_kwargs.update({'add_colorbar': False, 'add_labels': False}) + # Order is important + func_kwargs = kwargs.copy() + func_kwargs.update(cmap_params) + func_kwargs.update({'add_colorbar': False, 'add_labels': False}) - # Get x, y labels for the first subplot - x, y = _infer_xy_labels( - darray=self.data.loc[self.name_dicts.flat[0]], x=x, y=y, - imshow=func.__name__ == 'imshow', rgb=kwargs.get('rgb', None)) + # Get x, y labels for the first subplot + x, y = _infer_xy_labels( + darray=self.data.loc[self.name_dicts.flat[0]], x=x, y=y, + imshow=func.__name__ == 'imshow', rgb=kwargs.get('rgb', None)) for d, ax in zip(self.name_dicts.flat, self.axes.flat): # None is the sentinel value if d is not None: subset = self.data.loc[d] - mappable = func(subset, x, y, ax=ax, **func_kwargs) + mappable = func(subset, x=x, y=y, ax=ax, **func_kwargs) self._mappables.append(mappable) - self._cmap_extend = cmap_params.get('extend') - self._finalize_grid(x, y) - - if kwargs.get('add_colorbar', True): - self.add_colorbar(**cbar_kwargs) - - return self - - def map_dataarray_line(self, x=None, y=None, hue=None, **kwargs): - """ - Apply a line plot to a 2d facet subset of the data. - - Parameters - ---------- - x, y, hue: string - dimension names for the axes and hues of each facet - - Returns - ------- - self : FacetGrid object - - """ - from .plot import line, _infer_line_data - - add_legend = kwargs.pop('add_legend', True) - kwargs['add_legend'] = False + if func.__name__ == 'line': + _, _, hueplt, xlabel, ylabel, huelabel = _infer_line_data( + darray=self.data.loc[self.name_dicts.flat[0]], + x=x, y=y, hue=func_kwargs['hue']) - for d, ax in zip(self.name_dicts.flat, self.axes.flat): - # None is the sentinel value - if d is not None: - subset = self.data.loc[d] - mappable = line(subset, x=x, y=y, hue=hue, - ax=ax, _labels=False, - **kwargs) - self._mappables.append(mappable) - _, _, hueplt, xlabel, ylabel, huelabel = _infer_line_data( - darray=self.data.loc[self.name_dicts.flat[0]], - x=x, y=y, hue=hue) + self._hue_var = hueplt + self._hue_label = huelabel + self._finalize_grid(xlabel, ylabel) - self._hue_var = hueplt - self._hue_label = huelabel - self._finalize_grid(xlabel, ylabel) + if add_legend and hueplt is not None and huelabel is not None: + self.add_legend() + else: + self._cmap_extend = cmap_params.get('extend') + self._finalize_grid(x, y) - if add_legend and hueplt is not None and huelabel is not None: - self.add_legend() + if kwargs.get('add_colorbar', True): + self.add_colorbar(**cbar_kwargs) return self @@ -522,3 +501,29 @@ def map(self, func, *args, **kwargs): self._finalize_grid(*args[:2]) return self + + +def _easy_facetgrid(data, plotfunc, x=None, y=None, row=None, + col=None, col_wrap=None, sharex=True, sharey=True, + aspect=None, size=None, subplot_kws=None, **kwargs): + """ + Convenience method to call xarray.plot.FacetGrid from 2d plotting methods + + kwargs are the arguments to 2d plotting method + """ + ax = kwargs.pop('ax', None) + figsize = kwargs.pop('figsize', None) + if ax is not None: + raise ValueError("Can't use axes when making faceted plots.") + if aspect is None: + aspect = 1 + if size is None: + size = 3 + elif figsize is not None: + raise ValueError('cannot provide both `figsize` and `size` arguments') + + g = FacetGrid(data=data, col=col, row=row, col_wrap=col_wrap, + sharex=sharex, sharey=sharey, figsize=figsize, + aspect=aspect, size=size, subplot_kws=subplot_kws) + + return g.map_dataarray(plotfunc, x, y, **kwargs) diff --git a/xarray/plot/plot.py b/xarray/plot/plot.py index 78501033cc1..9839667c2d9 100644 --- a/xarray/plot/plot.py +++ b/xarray/plot/plot.py @@ -16,56 +16,14 @@ from xarray.core.common import contains_cftime_datetimes from xarray.core.pycompat import basestring -from .facetgrid import FacetGrid +from .facetgrid import FacetGrid, _easy_facetgrid from .utils import ( - -def _easy_facetgrid(darray, plotfunc, x, y, row=None, col=None, - col_wrap=None, sharex=True, sharey=True, aspect=None, - size=None, subplot_kws=None, **kwargs): - """ - Convenience method to call xarray.plot.FacetGrid from 2d plotting methods - - kwargs are the arguments to 2d plotting method - """ - ax = kwargs.pop('ax', None) - figsize = kwargs.pop('figsize', None) - if ax is not None: - raise ValueError("Can't use axes when making faceted plots.") - if aspect is None: - aspect = 1 - if size is None: - size = 3 - elif figsize is not None: - raise ValueError('cannot provide both `figsize` and `size` arguments') - - g = FacetGrid(data=darray, col=col, row=row, col_wrap=col_wrap, - sharex=sharex, sharey=sharey, figsize=figsize, - aspect=aspect, size=size, subplot_kws=subplot_kws) - return g.map_dataarray(plotfunc, x, y, **kwargs) - - -def _line_facetgrid(darray, row=None, col=None, hue=None, - col_wrap=None, sharex=True, sharey=True, aspect=None, - size=None, subplot_kws=None, **kwargs): - """ - Convenience method to call xarray.plot.FacetGrid for line plots - kwargs are the arguments to pyplot.plot() - """ - ax = kwargs.pop('ax', None) - figsize = kwargs.pop('figsize', None) - if ax is not None: - raise ValueError("Can't use axes when making faceted plots.") - if aspect is None: - aspect = 1 - if size is None: - size = 3 - elif figsize is not None: - raise ValueError('cannot provide both `figsize` and `size` arguments') - - g = FacetGrid(data=darray, col=col, row=row, col_wrap=col_wrap, - sharex=sharex, sharey=sharey, figsize=figsize, - aspect=aspect, size=size, subplot_kws=subplot_kws) - return g.map_dataarray_line(hue=hue, **kwargs) + _add_colorbar, _determine_cmap_params, + _ensure_plottable, _infer_interval_breaks, _infer_line_data, + _infer_xy_labels, _interval_to_double_bound_points, + _interval_to_mid_points, _is_monotonic, _rescale_imshow_rgb, + _resolve_intervals_2dplot, _update_axes, _valid_other_type, + get_axis, import_matplotlib_pyplot, label_from_attrs) def plot(darray, row=None, col=None, col_wrap=None, ax=None, hue=None, @@ -208,7 +166,8 @@ def line(darray, *args, **kwargs): if row or col: allargs = locals().copy() allargs.update(allargs.pop('kwargs')) - return _line_facetgrid(**allargs) + allargs.pop('darray') + return _easy_facetgrid(darray, line, **allargs) ndims = len(darray.dims) if ndims > 2: @@ -557,11 +516,10 @@ def newplotfunc(darray, x=None, y=None, figsize=None, size=None, allargs = locals().copy() allargs.pop('imshow_rgb') allargs.update(allargs.pop('kwargs')) - + allargs.pop('darray') # Need the decorated plotting function allargs['plotfunc'] = globals()[plotfunc.__name__] - - return _easy_facetgrid(**allargs) + return _easy_facetgrid(darray, **allargs) plt = import_matplotlib_pyplot() From a998cfc79b238f306b3e312077cabfbd052734c4 Mon Sep 17 00:00:00 2001 From: dcherian Date: Mon, 14 Jan 2019 00:52:43 -0800 Subject: [PATCH 047/101] flake8 --- xarray/plot/facetgrid.py | 7 ++++--- xarray/plot/plot.py | 11 +++++------ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/xarray/plot/facetgrid.py b/xarray/plot/facetgrid.py index d982b2690b3..3b9f3f2664d 100644 --- a/xarray/plot/facetgrid.py +++ b/xarray/plot/facetgrid.py @@ -9,8 +9,8 @@ from ..core.formatting import format_item from ..core.pycompat import getargspec from .utils import ( - _determine_cmap_params, _infer_line_data, - _infer_xy_labels, import_matplotlib_pyplot, label_from_attrs) + _determine_cmap_params, _infer_line_data, _infer_xy_labels, + import_matplotlib_pyplot, label_from_attrs) # Overrides axes.labelsize, xtick.major.size, ytick.major.size # from mpl.rcParams @@ -248,7 +248,8 @@ def map_dataarray(self, func, x, y, **kwargs): } cmap_args = getargspec(_determine_cmap_params).args - cmap_kwargs.update((a, kwargs[a]) for a in cmap_args if a in kwargs) + cmap_kwargs.update((a, kwargs[a]) + for a in cmap_args if a in kwargs) cmap_params = _determine_cmap_params(**cmap_kwargs) diff --git a/xarray/plot/plot.py b/xarray/plot/plot.py index 9839667c2d9..2cef5b7d453 100644 --- a/xarray/plot/plot.py +++ b/xarray/plot/plot.py @@ -18,12 +18,11 @@ from .facetgrid import FacetGrid, _easy_facetgrid from .utils import ( - _add_colorbar, _determine_cmap_params, - _ensure_plottable, _infer_interval_breaks, _infer_line_data, - _infer_xy_labels, _interval_to_double_bound_points, - _interval_to_mid_points, _is_monotonic, _rescale_imshow_rgb, - _resolve_intervals_2dplot, _update_axes, _valid_other_type, - get_axis, import_matplotlib_pyplot, label_from_attrs) + _add_colorbar, _determine_cmap_params, _ensure_plottable, + _infer_interval_breaks, _infer_line_data, _infer_xy_labels, + _interval_to_double_bound_points, _interval_to_mid_points, _is_monotonic, + _rescale_imshow_rgb, _resolve_intervals_2dplot, _update_axes, + _valid_other_type, get_axis, import_matplotlib_pyplot, label_from_attrs) def plot(darray, row=None, col=None, col_wrap=None, ax=None, hue=None, From ce9e2aeafa89bd35497045b9d2e914c503a278c1 Mon Sep 17 00:00:00 2001 From: dcherian Date: Sat, 15 Dec 2018 15:38:30 -0800 Subject: [PATCH 048/101] Refactor out colorbar making to plot.utils._add_colorbar --- xarray/plot/plot.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/xarray/plot/plot.py b/xarray/plot/plot.py index 2cef5b7d453..92df56ca653 100644 --- a/xarray/plot/plot.py +++ b/xarray/plot/plot.py @@ -631,14 +631,11 @@ def newplotfunc(darray, x=None, y=None, figsize=None, size=None, if add_colorbar: cbar_kwargs = {} if cbar_kwargs is None else dict(cbar_kwargs) - cbar_kwargs.setdefault('extend', cmap_params['extend']) - if cbar_ax is None: - cbar_kwargs.setdefault('ax', ax) - else: - cbar_kwargs.setdefault('cax', cbar_ax) - cbar = plt.colorbar(primitive, **cbar_kwargs) if add_labels and 'label' not in cbar_kwargs: - cbar.set_label(label_from_attrs(darray)) + cbar_kwargs['label'] = label_from_attrs(darray) + cbar = _add_colorbar(primitive, ax, cbar_ax, cbar_kwargs, + cmap_params) + elif cbar_ax is not None or cbar_kwargs is not None: # inform the user about keywords which aren't used raise ValueError("cbar_ax and cbar_kwargs can't be used with " From 159bb25f06eb7955ac8cf1419cb9164e3d85782f Mon Sep 17 00:00:00 2001 From: dcherian Date: Wed, 19 Dec 2018 09:39:43 -0700 Subject: [PATCH 049/101] Refactor out cmap_params, cbar_kwargs processing --- xarray/plot/facetgrid.py | 29 +++----------------- xarray/plot/plot.py | 55 +++++++++----------------------------- xarray/plot/utils.py | 57 +++++++++++++++++++++++++++++++++++++++- 3 files changed, 73 insertions(+), 68 deletions(-) diff --git a/xarray/plot/facetgrid.py b/xarray/plot/facetgrid.py index 3b9f3f2664d..bfb629a078e 100644 --- a/xarray/plot/facetgrid.py +++ b/xarray/plot/facetgrid.py @@ -7,9 +7,8 @@ import numpy as np from ..core.formatting import format_item -from ..core.pycompat import getargspec from .utils import ( - _determine_cmap_params, _infer_line_data, _infer_xy_labels, + _infer_line_data, _infer_xy_labels, _process_cbar_cmap_kwargs, import_matplotlib_pyplot, label_from_attrs) # Overrides axes.labelsize, xtick.major.size, ytick.major.size @@ -228,33 +227,13 @@ def map_dataarray(self, func, x, y, **kwargs): func_kwargs['_labels'] = False else: - cmapkw = kwargs.get('cmap') - colorskw = kwargs.get('colors') - cbar_kwargs = kwargs.pop('cbar_kwargs', {}) - cbar_kwargs = {} if cbar_kwargs is None else dict(cbar_kwargs) - if kwargs.get('cbar_ax', None) is not None: raise ValueError('cbar_ax not supported by FacetGrid.') - # colors is mutually exclusive with cmap - if cmapkw and colorskw: - raise ValueError("Can't specify both cmap and colors.") - - # These should be consistent with xarray.plot._plot2d - cmap_kwargs = {'plot_data': self.data.values, - # MPL default - 'levels': 7 if 'contour' in func.__name__ else None, - 'filled': func.__name__ != 'contour', - } - - cmap_args = getargspec(_determine_cmap_params).args - cmap_kwargs.update((a, kwargs[a]) - for a in cmap_args if a in kwargs) + cmap_params, cbar_kwargs = _process_cbar_cmap_kwargs( + func, kwargs, self.data.values) - cmap_params = _determine_cmap_params(**cmap_kwargs) - - if colorskw is not None: - cmap_params['cmap'] = None + self._cmap_extend = cmap_params.get('extend') # Order is important func_kwargs = kwargs.copy() diff --git a/xarray/plot/plot.py b/xarray/plot/plot.py index 92df56ca653..28bebb10934 100644 --- a/xarray/plot/plot.py +++ b/xarray/plot/plot.py @@ -8,7 +8,6 @@ from __future__ import absolute_import, division, print_function import functools -import warnings import numpy as np import pandas as pd @@ -16,13 +15,13 @@ from xarray.core.common import contains_cftime_datetimes from xarray.core.pycompat import basestring -from .facetgrid import FacetGrid, _easy_facetgrid +from .facetgrid import _easy_facetgrid from .utils import ( - _add_colorbar, _determine_cmap_params, _ensure_plottable, - _infer_interval_breaks, _infer_line_data, _infer_xy_labels, - _interval_to_double_bound_points, _interval_to_mid_points, _is_monotonic, - _rescale_imshow_rgb, _resolve_intervals_2dplot, _update_axes, - _valid_other_type, get_axis, import_matplotlib_pyplot, label_from_attrs) + _add_colorbar, _ensure_plottable, _infer_interval_breaks, _infer_line_data, + _infer_xy_labels, _interval_to_double_bound_points, + _interval_to_mid_points, _process_cbar_cmap_kwargs, _rescale_imshow_rgb, + _resolve_intervals_2dplot, _update_axes, _valid_other_type, get_axis, + import_matplotlib_pyplot, label_from_attrs) def plot(darray, row=None, col=None, col_wrap=None, ax=None, hue=None, @@ -522,30 +521,16 @@ def newplotfunc(darray, x=None, y=None, figsize=None, size=None, plt = import_matplotlib_pyplot() - # colors is mutually exclusive with cmap - if cmap and colors: - raise ValueError("Can't specify both cmap and colors.") - # colors is only valid when levels is supplied or the plot is of type - # contour or contourf - if colors and (('contour' not in plotfunc.__name__) and (not levels)): - raise ValueError("Can only specify colors with contour or levels") - # we should not be getting a list of colors in cmap anymore - # is there a better way to do this test? - if isinstance(cmap, (list, tuple)): - warnings.warn("Specifying a list of colors in cmap is deprecated. " - "Use colors keyword instead.", - DeprecationWarning, stacklevel=3) - rgb = kwargs.pop('rgb', None) - xlab, ylab = _infer_xy_labels( - darray=darray, x=x, y=y, imshow=imshow_rgb, rgb=rgb) - if rgb is not None and plotfunc.__name__ != 'imshow': raise ValueError('The "rgb" keyword is only valid for imshow()') elif rgb is not None and not imshow_rgb: raise ValueError('The "rgb" keyword is only valid for imshow()' 'with a three-dimensional array (per facet)') + xlab, ylab = _infer_xy_labels( + darray=darray, x=x, y=y, imshow=imshow_rgb, rgb=rgb) + # better to pass the ndarrays directly to plotting functions xval = darray[xlab].values yval = darray[ylab].values @@ -579,22 +564,8 @@ def newplotfunc(darray, x=None, y=None, figsize=None, size=None, _ensure_plottable(xplt, yplt) - if 'contour' in plotfunc.__name__ and levels is None: - levels = 7 # this is the matplotlib default - - cmap_kwargs = {'plot_data': zval.data, - 'vmin': vmin, - 'vmax': vmax, - 'cmap': colors if colors else cmap, - 'center': center, - 'robust': robust, - 'extend': extend, - 'levels': levels, - 'filled': plotfunc.__name__ != 'contour', - 'norm': norm, - } - - cmap_params = _determine_cmap_params(**cmap_kwargs) + cmap_params, cbar_kwargs = _process_cbar_cmap_kwargs( + plotfunc, locals(), zval.data) if 'contour' in plotfunc.__name__: # extend is a keyword argument only for contour and contourf, but @@ -630,13 +601,13 @@ def newplotfunc(darray, x=None, y=None, figsize=None, size=None, ax.set_title(darray._title_for_slice()) if add_colorbar: - cbar_kwargs = {} if cbar_kwargs is None else dict(cbar_kwargs) if add_labels and 'label' not in cbar_kwargs: cbar_kwargs['label'] = label_from_attrs(darray) cbar = _add_colorbar(primitive, ax, cbar_ax, cbar_kwargs, cmap_params) - elif cbar_ax is not None or cbar_kwargs is not None: + elif (cbar_ax is not None + or (cbar_kwargs is not None and cbar_kwargs != {})): # inform the user about keywords which aren't used raise ValueError("cbar_ax and cbar_kwargs can't be used with " "add_colorbar=False.") diff --git a/xarray/plot/utils.py b/xarray/plot/utils.py index c811b5b4427..a24fec849d6 100644 --- a/xarray/plot/utils.py +++ b/xarray/plot/utils.py @@ -9,7 +9,7 @@ import pandas as pd from ..core.options import OPTIONS -from ..core.pycompat import basestring +from ..core.pycompat import basestring, getargspec from ..core.utils import is_scalar ROBUST_PERCENTILE = 2.0 @@ -698,3 +698,58 @@ def _infer_interval_breaks(coord, axis=0, check_monotonic=False): trim_last = tuple(slice(None, -1) if n == axis else slice(None) for n in range(coord.ndim)) return np.concatenate([first, coord[trim_last] + deltas, last], axis=axis) + + +def _process_cbar_cmap_kwargs(func, kwargs, data): + """ + Parameters + ========== + func : plotting function + kwargs : dict, + Dictionary with arguments that need to be parsed + data : ndarray, + Data values + + Returns + ======= + cmap_params + + cbar_kwargs + """ + + cmap = kwargs.pop('cmap', None) + colors = kwargs.pop('colors', None) + + cbar_kwargs = kwargs.pop('cbar_kwargs', {}) + cbar_kwargs = {} if cbar_kwargs is None else dict(cbar_kwargs) + + levels = kwargs.pop('levels', None) + if 'contour' in func.__name__ and levels is None: + levels = 7 # this is the matplotlib default + + # colors is mutually exclusive with cmap + if cmap and colors: + raise ValueError("Can't specify both cmap and colors.") + + # colors is only valid when levels is supplied or the plot is of type + # contour or contourf + if colors and (('contour' not in func.__name__) and (not levels)): + raise ValueError("Can only specify colors with contour or levels") + + # we should not be getting a list of colors in cmap anymore + # is there a better way to do this test? + if isinstance(cmap, (list, tuple)): + warnings.warn("Specifying a list of colors in cmap is deprecated. " + "Use colors keyword instead.", + DeprecationWarning, stacklevel=3) + + cmap_kwargs = {'plot_data': data, + 'levels': levels, + 'cmap': colors if colors else cmap, + 'filled': func.__name__ != 'contour'} + + cmap_args = getargspec(_determine_cmap_params).args + cmap_kwargs.update((a, kwargs[a]) for a in cmap_args if a in kwargs) + cmap_params = _determine_cmap_params(**cmap_kwargs) + + return cmap_params, cbar_kwargs From 3b4e4a096d475f00d165573b700618a169aa45ac Mon Sep 17 00:00:00 2001 From: dcherian Date: Mon, 14 Jan 2019 22:10:09 -0800 Subject: [PATCH 050/101] Back to map_dataarray_line --- xarray/plot/facetgrid.py | 84 +++++++++++++++++++++++----------------- xarray/plot/plot.py | 4 +- 2 files changed, 50 insertions(+), 38 deletions(-) diff --git a/xarray/plot/facetgrid.py b/xarray/plot/facetgrid.py index bfb629a078e..3d52ccbd024 100644 --- a/xarray/plot/facetgrid.py +++ b/xarray/plot/facetgrid.py @@ -220,30 +220,23 @@ def map_dataarray(self, func, x, y, **kwargs): """ - if func.__name__ == 'line': - add_legend = kwargs.pop('add_legend', True) - kwargs['add_legend'] = False - func_kwargs = kwargs.copy() - func_kwargs['_labels'] = False + if kwargs.get('cbar_ax', None) is not None: + raise ValueError('cbar_ax not supported by FacetGrid.') - else: - if kwargs.get('cbar_ax', None) is not None: - raise ValueError('cbar_ax not supported by FacetGrid.') - - cmap_params, cbar_kwargs = _process_cbar_cmap_kwargs( - func, kwargs, self.data.values) + cmap_params, cbar_kwargs = _process_cbar_cmap_kwargs( + func, kwargs, self.data.values) - self._cmap_extend = cmap_params.get('extend') + self._cmap_extend = cmap_params.get('extend') - # Order is important - func_kwargs = kwargs.copy() - func_kwargs.update(cmap_params) - func_kwargs.update({'add_colorbar': False, 'add_labels': False}) + # Order is important + func_kwargs = kwargs.copy() + func_kwargs.update(cmap_params) + func_kwargs.update({'add_colorbar': False, 'add_labels': False}) - # Get x, y labels for the first subplot - x, y = _infer_xy_labels( - darray=self.data.loc[self.name_dicts.flat[0]], x=x, y=y, - imshow=func.__name__ == 'imshow', rgb=kwargs.get('rgb', None)) + # Get x, y labels for the first subplot + x, y = _infer_xy_labels( + darray=self.data.loc[self.name_dicts.flat[0]], x=x, y=y, + imshow=func.__name__ == 'imshow', rgb=kwargs.get('rgb', None)) for d, ax in zip(self.name_dicts.flat, self.axes.flat): # None is the sentinel value @@ -252,23 +245,38 @@ def map_dataarray(self, func, x, y, **kwargs): mappable = func(subset, x=x, y=y, ax=ax, **func_kwargs) self._mappables.append(mappable) - if func.__name__ == 'line': - _, _, hueplt, xlabel, ylabel, huelabel = _infer_line_data( - darray=self.data.loc[self.name_dicts.flat[0]], - x=x, y=y, hue=func_kwargs['hue']) + self._cmap_extend = cmap_params.get('extend') + self._finalize_grid(x, y) - self._hue_var = hueplt - self._hue_label = huelabel - self._finalize_grid(xlabel, ylabel) + if kwargs.get('add_colorbar', True): + self.add_colorbar(**cbar_kwargs) - if add_legend and hueplt is not None and huelabel is not None: - self.add_legend() - else: - self._cmap_extend = cmap_params.get('extend') - self._finalize_grid(x, y) + return self + + def map_dataarray_line(self, func, x, y, **kwargs): + + add_legend = kwargs.pop('add_legend', True) + kwargs['add_legend'] = False + func_kwargs = kwargs.copy() + func_kwargs['_labels'] = False + + for d, ax in zip(self.name_dicts.flat, self.axes.flat): + # None is the sentinel value + if d is not None: + subset = self.data.loc[d] + mappable = func(subset, x=x, y=y, ax=ax, **func_kwargs) + self._mappables.append(mappable) + + _, _, hueplt, xlabel, ylabel, huelabel = _infer_line_data( + darray=self.data.loc[self.name_dicts.flat[0]], + x=x, y=y, hue=func_kwargs['hue']) - if kwargs.get('add_colorbar', True): - self.add_colorbar(**cbar_kwargs) + self._hue_var = hueplt + self._hue_label = huelabel + self._finalize_grid(xlabel, ylabel) + + if add_legend and hueplt is not None and huelabel is not None: + self.add_legend() return self @@ -483,7 +491,7 @@ def map(self, func, *args, **kwargs): return self -def _easy_facetgrid(data, plotfunc, x=None, y=None, row=None, +def _easy_facetgrid(data, plotfunc, kind, x=None, y=None, row=None, col=None, col_wrap=None, sharex=True, sharey=True, aspect=None, size=None, subplot_kws=None, **kwargs): """ @@ -506,4 +514,8 @@ def _easy_facetgrid(data, plotfunc, x=None, y=None, row=None, sharex=sharex, sharey=sharey, figsize=figsize, aspect=aspect, size=size, subplot_kws=subplot_kws) - return g.map_dataarray(plotfunc, x, y, **kwargs) + if kind == 'line': + return g.map_dataarray_line(plotfunc, x, y, **kwargs) + + if kind == 'dataarray': + return g.map_dataarray(plotfunc, x, y, **kwargs) diff --git a/xarray/plot/plot.py b/xarray/plot/plot.py index 28bebb10934..6c3b1a8d9a4 100644 --- a/xarray/plot/plot.py +++ b/xarray/plot/plot.py @@ -165,7 +165,7 @@ def line(darray, *args, **kwargs): allargs = locals().copy() allargs.update(allargs.pop('kwargs')) allargs.pop('darray') - return _easy_facetgrid(darray, line, **allargs) + return _easy_facetgrid(darray, line, kind='line',**allargs) ndims = len(darray.dims) if ndims > 2: @@ -517,7 +517,7 @@ def newplotfunc(darray, x=None, y=None, figsize=None, size=None, allargs.pop('darray') # Need the decorated plotting function allargs['plotfunc'] = globals()[plotfunc.__name__] - return _easy_facetgrid(darray, **allargs) + return _easy_facetgrid(darray, kind='dataarray', **allargs) plt = import_matplotlib_pyplot() From 1217ab14b84b2319d5895266e22e3dfae2569cda Mon Sep 17 00:00:00 2001 From: dcherian Date: Mon, 14 Jan 2019 22:17:47 -0800 Subject: [PATCH 051/101] lint --- xarray/plot/plot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/xarray/plot/plot.py b/xarray/plot/plot.py index 6c3b1a8d9a4..89d812afd53 100644 --- a/xarray/plot/plot.py +++ b/xarray/plot/plot.py @@ -165,7 +165,7 @@ def line(darray, *args, **kwargs): allargs = locals().copy() allargs.update(allargs.pop('kwargs')) allargs.pop('darray') - return _easy_facetgrid(darray, line, kind='line',**allargs) + return _easy_facetgrid(darray, line, kind='line', **allargs) ndims = len(darray.dims) if ndims > 2: From 792291ce50e995099e9ee9e7646f89892f773168 Mon Sep 17 00:00:00 2001 From: dcherian Date: Wed, 23 Jan 2019 21:14:39 -0800 Subject: [PATCH 052/101] small rename --- xarray/plot/facetgrid.py | 4 ++-- xarray/plot/plot.py | 4 ++-- xarray/plot/utils.py | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/xarray/plot/facetgrid.py b/xarray/plot/facetgrid.py index 3d52ccbd024..ada5d7de955 100644 --- a/xarray/plot/facetgrid.py +++ b/xarray/plot/facetgrid.py @@ -8,7 +8,7 @@ from ..core.formatting import format_item from .utils import ( - _infer_line_data, _infer_xy_labels, _process_cbar_cmap_kwargs, + _infer_line_data, _infer_xy_labels, _process_cmap_cbar_kwargs, import_matplotlib_pyplot, label_from_attrs) # Overrides axes.labelsize, xtick.major.size, ytick.major.size @@ -223,7 +223,7 @@ def map_dataarray(self, func, x, y, **kwargs): if kwargs.get('cbar_ax', None) is not None: raise ValueError('cbar_ax not supported by FacetGrid.') - cmap_params, cbar_kwargs = _process_cbar_cmap_kwargs( + cmap_params, cbar_kwargs = _process_cmap_cbar_kwargs( func, kwargs, self.data.values) self._cmap_extend = cmap_params.get('extend') diff --git a/xarray/plot/plot.py b/xarray/plot/plot.py index 89d812afd53..f0cf356fc5d 100644 --- a/xarray/plot/plot.py +++ b/xarray/plot/plot.py @@ -19,7 +19,7 @@ from .utils import ( _add_colorbar, _ensure_plottable, _infer_interval_breaks, _infer_line_data, _infer_xy_labels, _interval_to_double_bound_points, - _interval_to_mid_points, _process_cbar_cmap_kwargs, _rescale_imshow_rgb, + _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) @@ -564,7 +564,7 @@ def newplotfunc(darray, x=None, y=None, figsize=None, size=None, _ensure_plottable(xplt, yplt) - cmap_params, cbar_kwargs = _process_cbar_cmap_kwargs( + cmap_params, cbar_kwargs = _process_cmap_cbar_kwargs( plotfunc, locals(), zval.data) if 'contour' in plotfunc.__name__: diff --git a/xarray/plot/utils.py b/xarray/plot/utils.py index a24fec849d6..4d7a45a17c0 100644 --- a/xarray/plot/utils.py +++ b/xarray/plot/utils.py @@ -700,7 +700,7 @@ def _infer_interval_breaks(coord, axis=0, check_monotonic=False): return np.concatenate([first, coord[trim_last] + deltas, last], axis=axis) -def _process_cbar_cmap_kwargs(func, kwargs, data): +def _process_cmap_cbar_kwargs(func, kwargs, data): """ Parameters ========== From 351a46691cfdcd9513c2e853331e729f92884cb5 Mon Sep 17 00:00:00 2001 From: dcherian Date: Wed, 23 Jan 2019 21:18:05 -0800 Subject: [PATCH 053/101] review comment. --- xarray/plot/plot.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/xarray/plot/plot.py b/xarray/plot/plot.py index f0cf356fc5d..5911a24e29a 100644 --- a/xarray/plot/plot.py +++ b/xarray/plot/plot.py @@ -606,8 +606,7 @@ def newplotfunc(darray, x=None, y=None, figsize=None, size=None, cbar = _add_colorbar(primitive, ax, cbar_ax, cbar_kwargs, cmap_params) - elif (cbar_ax is not None - or (cbar_kwargs is not None and cbar_kwargs != {})): + elif (cbar_ax is not None or cbar_kwargs): # inform the user about keywords which aren't used raise ValueError("cbar_ax and cbar_kwargs can't be used with " "add_colorbar=False.") From 62679d980c9f5a7314d5a9b93765d9d8a5f51f5d Mon Sep 17 00:00:00 2001 From: dcherian Date: Wed, 23 Jan 2019 21:38:13 -0800 Subject: [PATCH 054/101] Bugfix merge --- xarray/plot/facetgrid.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/xarray/plot/facetgrid.py b/xarray/plot/facetgrid.py index 2f5be1b33a8..8da42ab975f 100644 --- a/xarray/plot/facetgrid.py +++ b/xarray/plot/facetgrid.py @@ -291,7 +291,7 @@ def map_dataset(self, func, x=None, y=None, hue=None, hue_style=None, kwargs['_meta_data'] = meta_data if hue and meta_data['hue_style'] == 'continuous': - cmap_params, cbar_kwargs = self._process_cmap( + cmap_params, cbar_kwargs = _process_cmap_cbar_kwargs( func, kwargs, self.data[hue]) kwargs['_meta_data']['cmap_params'] = cmap_params kwargs['_meta_data']['cbar_kwargs'] = cbar_kwargs @@ -558,3 +558,6 @@ def _easy_facetgrid(data, plotfunc, kind, x=None, y=None, row=None, if kind == 'dataarray': return g.map_dataarray(plotfunc, x, y, **kwargs) + + if kind == 'dataset': + return g.map_dataset(plotfunc, x, y, **kwargs) From afa92a3ab1fb58b01dabec97dc3aa3a51bb1d761 Mon Sep 17 00:00:00 2001 From: dcherian Date: Wed, 23 Jan 2019 21:50:42 -0800 Subject: [PATCH 055/101] hue, hue_style aren't needed for all functions. --- xarray/plot/dataset_plot.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/xarray/plot/dataset_plot.py b/xarray/plot/dataset_plot.py index 8ee4c2e18eb..cf5e749886b 100644 --- a/xarray/plot/dataset_plot.py +++ b/xarray/plot/dataset_plot.py @@ -300,9 +300,11 @@ def plotmethod(_PlotMethods_obj, x=None, y=None, hue=None, @_dsplot -def scatter(ds, x, y, hue, hue_style, ax, **kwargs): +def scatter(ds, x, y, ax, **kwargs): """ Scatter Dataset data variables against each other. """ cmap_params = kwargs.pop('cmap_params') + hue = kwargs.pop('hue') + hue_style = kwargs.pop('hue_style') if hue_style == 'discrete': primitive = [] @@ -321,7 +323,7 @@ def scatter(ds, x, y, hue, hue_style, ax, **kwargs): @_dsplot -def hist(ds, x, y, hue, hue_style, ax, **kwargs): +def hist(ds, x, y, ax, **kwargs): cmap_params = kwargs.pop('cmap_params') From 18199cf148ac7f727bb0494a2578274dfb6f44ec Mon Sep 17 00:00:00 2001 From: dcherian Date: Wed, 23 Jan 2019 21:52:54 -0800 Subject: [PATCH 056/101] lint --- xarray/plot/dataset_plot.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/xarray/plot/dataset_plot.py b/xarray/plot/dataset_plot.py index cf5e749886b..e837563a11b 100644 --- a/xarray/plot/dataset_plot.py +++ b/xarray/plot/dataset_plot.py @@ -7,8 +7,8 @@ from ..core.alignment import broadcast from .facetgrid import _easy_facetgrid from .utils import ( - _add_colorbar, _determine_cmap_params, - _ensure_numeric, _valid_other_type, get_axis, label_from_attrs) + _add_colorbar, _determine_cmap_params, _ensure_numeric, _valid_other_type, + get_axis, label_from_attrs) def _infer_meta_data(ds, x, y, hue, hue_style, add_colorbar, @@ -244,8 +244,6 @@ def newplotfunc(ds, x=None, y=None, hue=None, hue_style=None, else: cmap_params_subset = {} - # TODO dcherian: hue, hue_style shouldn't be needed for all methods - # update signatures primitive = plotfunc(ds=ds, x=x, y=y, hue=hue, hue_style=hue_style, ax=ax, cmap_params=cmap_params_subset, **kwargs) From 8e47189861574d89c3f0d390a528e17d7179fd84 Mon Sep 17 00:00:00 2001 From: dcherian Date: Wed, 23 Jan 2019 21:56:55 -0800 Subject: [PATCH 057/101] Use _process_cmap_cbar_kwargs. --- xarray/plot/dataset_plot.py | 18 ++++-------------- 1 file changed, 4 insertions(+), 14 deletions(-) diff --git a/xarray/plot/dataset_plot.py b/xarray/plot/dataset_plot.py index e837563a11b..d5370e099eb 100644 --- a/xarray/plot/dataset_plot.py +++ b/xarray/plot/dataset_plot.py @@ -7,8 +7,8 @@ from ..core.alignment import broadcast from .facetgrid import _easy_facetgrid from .utils import ( - _add_colorbar, _determine_cmap_params, _ensure_numeric, _valid_other_type, - get_axis, label_from_attrs) + _add_colorbar, _ensure_numeric, _process_cmap_cbar_kwargs, + _valid_other_type, get_axis, label_from_attrs) def _infer_meta_data(ds, x, y, hue, hue_style, add_colorbar, @@ -223,18 +223,8 @@ def newplotfunc(ds, x=None, y=None, hue=None, hue_style=None, cbar_kwargs = _meta_data['cbar_kwargs'] cmap_params = _meta_data['cmap_params'] else: - cmap_kwargs = {'plot_data': ds[hue], - 'vmin': vmin, - 'vmax': vmax, - 'cmap': colors if colors else cmap, - 'center': center, - 'robust': robust, - 'extend': extend, - 'levels': levels, - 'filled': None, - 'norm': norm} - - cmap_params = _determine_cmap_params(**cmap_kwargs) + cmap_params, cbar_kwargs = _process_cmap_cbar_kwargs( + plotfunc, locals(), ds[hue]) # subset that can be passed to scatter, hist2d cmap_params_subset = dict( From b25ad6b7c5911bc1d498ac2aa89bda361d097249 Mon Sep 17 00:00:00 2001 From: dcherian Date: Wed, 23 Jan 2019 21:59:12 -0800 Subject: [PATCH 058/101] Update whats-new --- doc/whats-new.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/whats-new.rst b/doc/whats-new.rst index 97e475bc368..6676a13b70b 100644 --- a/doc/whats-new.rst +++ b/doc/whats-new.rst @@ -43,6 +43,8 @@ Enhancements report showing what exactly differs between the two objects (dimensions / coordinates / variables / attributes) (:issue:`1507`). By `Benoit Bovy `_. +- Dataset plotting API! Currently only :py:meth:`Dataset.plot.scatter` is implemented. + By `Yohai Bar Sinai `_ and `Deepak Cherian `_ Bug fixes ~~~~~~~~~ @@ -116,8 +118,6 @@ Enhancements recommend enabling it in your test suites if you use xarray for IO. By `Stephan Hoyer `_ - Support Dask ``HighLevelGraphs`` by `Matthew Rocklin `_. -- Dataset plotting API! Currently only :py:meth:`Dataset.plot.scatter` is implemented. - By `Yohai Bar Sinai `_ and `Deepak Cherian `_ - :py:meth:`DataArray.resample` and :py:meth:`Dataset.resample` now supports the ``loffset`` kwarg just like Pandas. By `Deepak Cherian `_ From 072d83d98623b4aaacff633768cba10f01397ef5 Mon Sep 17 00:00:00 2001 From: dcherian Date: Thu, 24 Jan 2019 12:36:47 -0800 Subject: [PATCH 059/101] Some doc fixes. --- doc/plotting.rst | 4 ++-- xarray/plot/dataset_plot.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/doc/plotting.rst b/doc/plotting.rst index f4146051012..4f8a75a406d 100644 --- a/doc/plotting.rst +++ b/doc/plotting.rst @@ -630,14 +630,14 @@ Consider this dataset ds.B.attrs['units'] = 'Bunits' -Suppose we want to scatter the ``airtemps.fake`` against ``airtemps.air`` +Suppose we want to scatter ``A`` against ``B`` .. ipython:: python @savefig ds_simple_scatter.png ds.plot.scatter(x='A', y='B') -You can also set color using the ``hue`` kwarg +The ``hue`` kwarg lets you vary the color by variable value .. ipython:: python diff --git a/xarray/plot/dataset_plot.py b/xarray/plot/dataset_plot.py index d5370e099eb..0700f9880a0 100644 --- a/xarray/plot/dataset_plot.py +++ b/xarray/plot/dataset_plot.py @@ -106,8 +106,8 @@ def __call__(self, *args, **kwargs): def _dsplot(plotfunc): commondoc = """ - Input - ----- + Parameters + ---------- ds : Dataset x, y : string From 3fe8557af6a363060682e6e7de54aa9c450704bc Mon Sep 17 00:00:00 2001 From: dcherian Date: Thu, 24 Jan 2019 14:40:49 -0800 Subject: [PATCH 060/101] Fix tests? --- xarray/tests/test_plot.py | 1 + 1 file changed, 1 insertion(+) diff --git a/xarray/tests/test_plot.py b/xarray/tests/test_plot.py index ef761d92830..c1325285e91 100644 --- a/xarray/tests/test_plot.py +++ b/xarray/tests/test_plot.py @@ -1791,6 +1791,7 @@ def test_wrong_num_of_dimensions(self): self.darray.plot.line(row='row', hue='hue') +@requires_matplotlib class TestDatasetScatterPlots(PlotTestCase): @pytest.fixture(autouse=True) def setUp(self): From fab84a95276ec38ef8da6e30f765085e8b9925c4 Mon Sep 17 00:00:00 2001 From: dcherian Date: Fri, 25 Jan 2019 08:57:59 -0800 Subject: [PATCH 061/101] another attempt to fix tests. --- xarray/tests/test_plot.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/xarray/tests/test_plot.py b/xarray/tests/test_plot.py index c1325285e91..66afb878ff9 100644 --- a/xarray/tests/test_plot.py +++ b/xarray/tests/test_plot.py @@ -1861,15 +1861,15 @@ def test_datetime_hue(self, hue_style): ds2['hue'] = pd.timedelta_range('-1D', periods=4, freq='D') ds2.plot.scatter(x='A', y='B', hue='hue', hue_style=hue_style) - @pytest.mark.parametrize('hue_style, map_type', - (['discrete', list], - ['continuous', mpl.collections.PathCollection])) - def test_facetgrid_hue_style(self, hue_style, map_type): - g = self.ds.plot.scatter(x='A', y='B', row='row', col='col', hue='hue', - hue_style=hue_style) - # for 'discrete' a list is appended to _mappables - # for 'continuous', should be single PathCollection - assert type(g._mappables[-1]) == map_type + def test_facetgrid_hue_style(self): + # Can't move this to pytest.mark.parametrize because it can't find mpl? + for hue_style, map_type in zip(['discrete', 'continuous'], + [list, mpl.collections.PathCollection]): + g = self.ds.plot.scatter(x='A', y='B', row='row', col='col', + hue='hue', hue_style=hue_style) + # for 'discrete' a list is appended to _mappables + # for 'continuous', should be single PathCollection + assert isinstance(g._mappables[-1], map_type) def test_non_numeric_legend(self): ds2 = self.ds.copy() From 3309d2a6e52b3b0745a94dd5fc3dfb3fb878a9f0 Mon Sep 17 00:00:00 2001 From: dcherian Date: Mon, 28 Jan 2019 10:33:50 -0800 Subject: [PATCH 062/101] small --- xarray/tests/test_plot.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/xarray/tests/test_plot.py b/xarray/tests/test_plot.py index 66afb878ff9..5d64d124b47 100644 --- a/xarray/tests/test_plot.py +++ b/xarray/tests/test_plot.py @@ -1862,7 +1862,8 @@ def test_datetime_hue(self, hue_style): ds2.plot.scatter(x='A', y='B', hue='hue', hue_style=hue_style) def test_facetgrid_hue_style(self): - # Can't move this to pytest.mark.parametrize because it can't find mpl? + # Can't move this to pytest.mark.parametrize because py35-min + # doesn't have mpl. for hue_style, map_type in zip(['discrete', 'continuous'], [list, mpl.collections.PathCollection]): g = self.ds.plot.scatter(x='A', y='B', row='row', col='col', From ecc8b3ced92a1f300e180cc11a61abf11c6d3499 Mon Sep 17 00:00:00 2001 From: dcherian Date: Wed, 30 Jan 2019 09:46:20 -0800 Subject: [PATCH 063/101] remove py2 line --- xarray/plot/dataset_plot.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/xarray/plot/dataset_plot.py b/xarray/plot/dataset_plot.py index 0700f9880a0..c1dca5ac32a 100644 --- a/xarray/plot/dataset_plot.py +++ b/xarray/plot/dataset_plot.py @@ -1,5 +1,3 @@ -from __future__ import absolute_import, division, print_function - import functools import numpy as np From 09d067fe1b4b223b48e5e206b7f70c40053bf1d1 Mon Sep 17 00:00:00 2001 From: dcherian Date: Wed, 30 Jan 2019 09:47:18 -0800 Subject: [PATCH 064/101] remove extra _infer_line_data --- xarray/plot/utils.py | 73 -------------------------------------------- 1 file changed, 73 deletions(-) diff --git a/xarray/plot/utils.py b/xarray/plot/utils.py index ad618ea92ca..89249275838 100644 --- a/xarray/plot/utils.py +++ b/xarray/plot/utils.py @@ -347,79 +347,6 @@ def _infer_xy_labels(darray, x, y, imshow=False, rgb=None): return x, y -def _infer_line_data(darray, x, y, hue): - 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.') - - 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] - yplt = darray - - else: - yplt = darray[dim] - xplt = darray - - else: - 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 get_axis(figsize, size, aspect, ax): import matplotlib as mpl import matplotlib.pyplot as plt From c64fbba5db01aa8d7419ab550cb0ad7cbaf8ee93 Mon Sep 17 00:00:00 2001 From: dcherian Date: Mon, 4 Feb 2019 08:34:54 -0700 Subject: [PATCH 065/101] Use _is_facetgrid flag. --- xarray/plot/dataset_plot.py | 16 +++++++--------- xarray/plot/facetgrid.py | 1 + 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/xarray/plot/dataset_plot.py b/xarray/plot/dataset_plot.py index c1dca5ac32a..9cce22b4ae7 100644 --- a/xarray/plot/dataset_plot.py +++ b/xarray/plot/dataset_plot.py @@ -189,8 +189,9 @@ def newplotfunc(ds, x=None, y=None, hue=None, hue_style=None, robust=None, colors=None, extend=None, cmap=None, **kwargs): - if kwargs.get('_meta_data', None): # facetgrid call - meta_data = kwargs['_meta_data'] + _is_facetgrid = kwargs.pop('_is_facetgrid', False) + if _is_facetgrid: # facetgrid call + meta_data = kwargs.pop('_meta_data') else: meta_data = _infer_meta_data(ds, x, y, hue, hue_style, add_colorbar, add_legend) @@ -212,14 +213,11 @@ def newplotfunc(ds, x=None, y=None, hue=None, hue_style=None, figsize = kwargs.pop('figsize', None) ax = get_axis(figsize, size, aspect, ax) - # TODO dcherian: _meta_data should not be needed - # I'm trying to avoid calling _determine_cmap_params multiple times - _meta_data = kwargs.pop('_meta_data', None) if hue_style == 'continuous' and hue is not None: - if _meta_data: - cbar_kwargs = _meta_data['cbar_kwargs'] - cmap_params = _meta_data['cmap_params'] + if _is_facetgrid: + cbar_kwargs = meta_data['cbar_kwargs'] + cmap_params = meta_data['cmap_params'] else: cmap_params, cbar_kwargs = _process_cmap_cbar_kwargs( plotfunc, locals(), ds[hue]) @@ -235,7 +233,7 @@ def newplotfunc(ds, x=None, y=None, hue=None, hue_style=None, primitive = plotfunc(ds=ds, x=x, y=y, hue=hue, hue_style=hue_style, ax=ax, cmap_params=cmap_params_subset, **kwargs) - if _meta_data: # if this was called from Facetgrid.map_dataset, + if _is_facetgrid: # if this was called from Facetgrid.map_dataset, return primitive # finish here. Else, make labels if meta_data.get('xlabel', None): diff --git a/xarray/plot/facetgrid.py b/xarray/plot/facetgrid.py index 8559a9a466e..95ea657348c 100644 --- a/xarray/plot/facetgrid.py +++ b/xarray/plot/facetgrid.py @@ -285,6 +285,7 @@ def map_dataset(self, func, x=None, y=None, hue=None, hue_style=None, kwargs['add_legend'] = False kwargs['add_colorbar'] = False + kwargs['_is_facetgrid'] = True meta_data = _infer_meta_data(self.data, x, y, hue, hue_style, add_legend, add_colorbar) From 7a65d28ad6ace543658de137548b6ac1c5bc2c5d Mon Sep 17 00:00:00 2001 From: dcherian Date: Mon, 4 Feb 2019 08:41:26 -0700 Subject: [PATCH 066/101] Revert "_determine_cmap_params supports datetime64" This reverts commit 0a01e7c9ec9d1d22bf8fb210bcbcb9f670f40c98. --- xarray/plot/utils.py | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/xarray/plot/utils.py b/xarray/plot/utils.py index 89249275838..6d812fbc2bc 100644 --- a/xarray/plot/utils.py +++ b/xarray/plot/utils.py @@ -148,23 +148,16 @@ def _determine_cmap_params(plot_data, vmin=None, vmax=None, cmap=None, """ import matplotlib as mpl - if np.issubdtype(plot_data.dtype, np.datetime64): - calc_data = np.ravel(plot_data[~np.isnat(plot_data)]) - possibly_divergent = False - elif np.issubdtype(plot_data.dtype, np.timedelta64): - calc_data = np.ravel(plot_data[~np.isnat(plot_data)]).astype('float64') - # Setting center=False prevents a divergent cmap - possibly_divergent = center is not False - else: - calc_data = np.ravel(plot_data[np.isfinite(plot_data)]) - # Setting center=False prevents a divergent cmap - possibly_divergent = center is not False + calc_data = np.ravel(plot_data[np.isfinite(plot_data)]) # Handle all-NaN input data gracefully if calc_data.size == 0: # Arbitrary default for when all values are NaN calc_data = np.array(0.0) + # Setting center=False prevents a divergent cmap + possibly_divergent = center is not False + # Set center to 0 so math below makes sense but remember its state center_is_none = False if center is None: From 6e8c92cebeabd7091b35f4b7652af59ad1361f95 Mon Sep 17 00:00:00 2001 From: dcherian Date: Mon, 4 Feb 2019 08:46:39 -0700 Subject: [PATCH 067/101] Remove datetime/timedelta hue support --- xarray/plot/dataset_plot.py | 4 +--- xarray/tests/test_plot.py | 2 ++ 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/xarray/plot/dataset_plot.py b/xarray/plot/dataset_plot.py index 9cce22b4ae7..e2862ff0d32 100644 --- a/xarray/plot/dataset_plot.py +++ b/xarray/plot/dataset_plot.py @@ -27,9 +27,7 @@ def _infer_meta_data(ds, x, y, hue, hue_style, add_colorbar, 'instead.'.format(', '.join(dims_coords)), hue) if hue: - hue_is_numeric = (_ensure_numeric(ds[hue].values) - or _valid_other_type(ds[hue], [np.datetime64, - np.timedelta64])) + hue_is_numeric = _ensure_numeric(ds[hue].values) if hue_style is None: hue_style = 'continuous' if hue_is_numeric else 'discrete' diff --git a/xarray/tests/test_plot.py b/xarray/tests/test_plot.py index 705c27ba75e..44ba48577dd 100644 --- a/xarray/tests/test_plot.py +++ b/xarray/tests/test_plot.py @@ -1865,6 +1865,8 @@ def test_bad_args(self, x, y, hue_style, add_legend, add_colorbar): add_legend=add_legend, add_colorbar=add_colorbar) + @pytest.mark.xfail(reason=['datetime, timedelta64 hue variable' + ' not supported yet.']) @pytest.mark.parametrize('hue_style', ['discrete', 'continuous']) def test_datetime_hue(self, hue_style): ds2 = self.ds.copy() From 4c82009fa41ceb257897174ac70ef717c1b7e9c7 Mon Sep 17 00:00:00 2001 From: dcherian Date: Mon, 4 Feb 2019 09:01:01 -0700 Subject: [PATCH 068/101] =?UTF-8?q?=5Fmeta=5Fdata=20=E2=86=92=20meta=5Fdat?= =?UTF-8?q?a.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- xarray/plot/dataset_plot.py | 2 +- xarray/plot/facetgrid.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/xarray/plot/dataset_plot.py b/xarray/plot/dataset_plot.py index e2862ff0d32..563a10a84ba 100644 --- a/xarray/plot/dataset_plot.py +++ b/xarray/plot/dataset_plot.py @@ -189,7 +189,7 @@ def newplotfunc(ds, x=None, y=None, hue=None, hue_style=None, _is_facetgrid = kwargs.pop('_is_facetgrid', False) if _is_facetgrid: # facetgrid call - meta_data = kwargs.pop('_meta_data') + meta_data = kwargs.pop('meta_data') else: meta_data = _infer_meta_data(ds, x, y, hue, hue_style, add_colorbar, add_legend) diff --git a/xarray/plot/facetgrid.py b/xarray/plot/facetgrid.py index 95ea657348c..1e5078efab3 100644 --- a/xarray/plot/facetgrid.py +++ b/xarray/plot/facetgrid.py @@ -289,13 +289,13 @@ def map_dataset(self, func, x=None, y=None, hue=None, hue_style=None, meta_data = _infer_meta_data(self.data, x, y, hue, hue_style, add_legend, add_colorbar) - kwargs['_meta_data'] = meta_data + kwargs['meta_data'] = meta_data if hue and meta_data['hue_style'] == 'continuous': cmap_params, cbar_kwargs = _process_cmap_cbar_kwargs( func, kwargs, self.data[hue]) - kwargs['_meta_data']['cmap_params'] = cmap_params - kwargs['_meta_data']['cbar_kwargs'] = cbar_kwargs + kwargs['meta_data']['cmap_params'] = cmap_params + kwargs['meta_data']['cbar_kwargs'] = cbar_kwargs for d, ax in zip(self.name_dicts.flat, self.axes.flat): # None is the sentinel value From 7392c81b740bb0117a1b24d7363fa3916ba16a2c Mon Sep 17 00:00:00 2001 From: dcherian Date: Mon, 4 Feb 2019 09:01:14 -0700 Subject: [PATCH 069/101] isort --- xarray/plot/dataset_plot.py | 6 ++---- xarray/plot/facetgrid.py | 4 ++-- xarray/plot/utils.py | 3 +-- 3 files changed, 5 insertions(+), 8 deletions(-) diff --git a/xarray/plot/dataset_plot.py b/xarray/plot/dataset_plot.py index 563a10a84ba..bc70fd3178f 100644 --- a/xarray/plot/dataset_plot.py +++ b/xarray/plot/dataset_plot.py @@ -1,12 +1,10 @@ import functools -import numpy as np - from ..core.alignment import broadcast from .facetgrid import _easy_facetgrid from .utils import ( - _add_colorbar, _ensure_numeric, _process_cmap_cbar_kwargs, - _valid_other_type, get_axis, label_from_attrs) + _add_colorbar, _ensure_numeric, _process_cmap_cbar_kwargs, get_axis, + label_from_attrs) def _infer_meta_data(ds, x, y, hue, hue_style, add_colorbar, diff --git a/xarray/plot/facetgrid.py b/xarray/plot/facetgrid.py index 1e5078efab3..d8c2c414c7e 100644 --- a/xarray/plot/facetgrid.py +++ b/xarray/plot/facetgrid.py @@ -7,8 +7,8 @@ from ..core.formatting import format_item from .utils import ( - _infer_xy_labels, _process_cmap_cbar_kwargs, - import_matplotlib_pyplot, label_from_attrs) + _infer_xy_labels, _process_cmap_cbar_kwargs, import_matplotlib_pyplot, + label_from_attrs) # Overrides axes.labelsize, xtick.major.size, ytick.major.size # from mpl.rcParams diff --git a/xarray/plot/utils.py b/xarray/plot/utils.py index 6d812fbc2bc..6f39531eb5d 100644 --- a/xarray/plot/utils.py +++ b/xarray/plot/utils.py @@ -2,12 +2,11 @@ import textwrap import warnings from datetime import datetime +from inspect import getfullargspec import numpy as np import pandas as pd -from inspect import getfullargspec - from ..core.options import OPTIONS from ..core.utils import is_scalar From f755cb8eb7d48b9062e18e95161374ce76ee4116 Mon Sep 17 00:00:00 2001 From: dcherian Date: Tue, 5 Feb 2019 16:26:11 -0700 Subject: [PATCH 070/101] Add doc line --- doc/plotting.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/doc/plotting.rst b/doc/plotting.rst index 4f8a75a406d..5a8af04430e 100644 --- a/doc/plotting.rst +++ b/doc/plotting.rst @@ -663,6 +663,9 @@ Faceting is also possible ds.plot.scatter(x='A', y='B', col='x', row='z', hue='w', hue_style='discrete') +For more advanced scatter plots, we recommend converting the relevant data variables to a pandas DataFrame and using the extensive plotting capabilities of ``seaborn``. + + .. _plot-maps: Maps From 13a411b5f0cd1587ba9e9910aa3ddb5a72e0ddc2 Mon Sep 17 00:00:00 2001 From: dcherian Date: Thu, 14 Feb 2019 10:25:27 -0800 Subject: [PATCH 071/101] Switch to add_guide. --- xarray/plot/dataset_plot.py | 43 +++++++++++++++---------------------- xarray/plot/facetgrid.py | 11 +++++----- xarray/tests/test_plot.py | 17 +++++++-------- 3 files changed, 30 insertions(+), 41 deletions(-) diff --git a/xarray/plot/dataset_plot.py b/xarray/plot/dataset_plot.py index bc70fd3178f..0f4b7fa6be4 100644 --- a/xarray/plot/dataset_plot.py +++ b/xarray/plot/dataset_plot.py @@ -7,8 +7,7 @@ label_from_attrs) -def _infer_meta_data(ds, x, y, hue, hue_style, add_colorbar, - add_legend): +def _infer_meta_data(ds, x, y, hue, hue_style, add_guide): dvars = set(ds.data_vars.keys()) error_msg = (' must be either one of ({0:s})' .format(', '.join(dvars))) @@ -34,17 +33,13 @@ def _infer_meta_data(ds, x, y, hue, hue_style, add_colorbar, raise ValueError('Cannot create a colorbar for a non numeric' ' coordinate: ' + hue) - if add_colorbar is None: + if add_guide is None: add_colorbar = True if hue_style == 'continuous' else False - - if add_legend is None: add_legend = True if hue_style == 'discrete' else False else: - if add_legend is True: - raise ValueError('Cannot set add_legend when hue is None.') - if add_colorbar is True: - raise ValueError('Cannot set add_colorbar when hue is None.') + if add_guide is True: + raise ValueError('Cannot set add_guide when hue is None.') add_legend = False add_colorbar = False @@ -109,12 +104,12 @@ def _dsplot(plotfunc): hue: str, optional Variable by which to color scattered points hue_style: str, optional - Hue style. - - "discrete" builds a legend. This is the default for non-numeric - `hue` variables. - - "continuous" builds a colorbar - add_legend, add_colorbar: bool, optional - Turn the legend or colorbar on/off. + Hue style. Can be either 'discrete' or 'continuous'. + add_guide: bool, optional + Add a guide that depends on hue_style + - for "discrete", build a legend. + This is the default for non-numeric `hue` variables. + - for "continuous", build a colorbar row : string, optional If passed, make row faceted plots on this dimension name col : string, optional @@ -179,8 +174,8 @@ def _dsplot(plotfunc): def newplotfunc(ds, x=None, y=None, hue=None, hue_style=None, col=None, row=None, ax=None, figsize=None, size=None, col_wrap=None, sharex=True, sharey=True, aspect=None, - subplot_kws=None, add_colorbar=None, cbar_kwargs=None, - add_legend=None, cbar_ax=None, vmin=None, vmax=None, + subplot_kws=None, add_guide=None, cbar_kwargs=None, + cbar_ax=None, vmin=None, vmax=None, norm=None, infer_intervals=None, center=None, levels=None, robust=None, colors=None, extend=None, cmap=None, **kwargs): @@ -189,12 +184,9 @@ def newplotfunc(ds, x=None, y=None, hue=None, hue_style=None, if _is_facetgrid: # facetgrid call meta_data = kwargs.pop('meta_data') else: - meta_data = _infer_meta_data(ds, x, y, hue, hue_style, - add_colorbar, add_legend) + meta_data = _infer_meta_data(ds, x, y, hue, hue_style, add_guide) hue_style = meta_data['hue_style'] - add_legend = meta_data['add_legend'] - add_colorbar = meta_data['add_colorbar'] # handle facetgrids first if col or row: @@ -237,11 +229,11 @@ def newplotfunc(ds, x=None, y=None, hue=None, hue_style=None, if meta_data.get('ylabel', None): ax.set_ylabel(meta_data.get('ylabel')) - if add_legend: + if meta_data['add_legend']: ax.legend(handles=primitive, labels=list(meta_data['hue_values'].values), title=meta_data.get('hue_label', None)) - if add_colorbar: + if meta_data['add_colorbar']: cbar_kwargs = {} if cbar_kwargs is None else cbar_kwargs if 'label' not in cbar_kwargs: cbar_kwargs['label'] = meta_data.get('hue_label', None) @@ -254,9 +246,8 @@ def plotmethod(_PlotMethods_obj, x=None, y=None, hue=None, hue_style=None, col=None, row=None, ax=None, figsize=None, col_wrap=None, sharex=True, sharey=True, aspect=None, - size=None, subplot_kws=None, add_colorbar=None, - cbar_kwargs=None, - add_legend=None, cbar_ax=None, vmin=None, vmax=None, + size=None, subplot_kws=None, add_guide=None, + cbar_kwargs=None, cbar_ax=None, vmin=None, vmax=None, norm=None, infer_intervals=None, center=None, levels=None, robust=None, colors=None, extend=None, cmap=None, **kwargs): diff --git a/xarray/plot/facetgrid.py b/xarray/plot/facetgrid.py index d8c2c414c7e..c733640a0e8 100644 --- a/xarray/plot/facetgrid.py +++ b/xarray/plot/facetgrid.py @@ -280,15 +280,14 @@ def map_dataarray_line(self, func, x, y, **kwargs): return self def map_dataset(self, func, x=None, y=None, hue=None, hue_style=None, - add_colorbar=None, add_legend=None, **kwargs): + add_guide=None, **kwargs): from .dataset_plot import _infer_meta_data - kwargs['add_legend'] = False - kwargs['add_colorbar'] = False + kwargs['add_guide'] = False kwargs['_is_facetgrid'] = True meta_data = _infer_meta_data(self.data, x, y, hue, hue_style, - add_legend, add_colorbar) + add_guide) kwargs['meta_data'] = meta_data if hue and meta_data['hue_style'] == 'continuous': @@ -312,10 +311,10 @@ def map_dataset(self, func, x=None, y=None, hue=None, hue_style=None, if hue: self._hue_label = meta_data.pop('hue_label', None) - if add_legend: + if meta_data['add_legend']: self._hue_var = meta_data['hue_values'] self.add_legend() - elif add_colorbar: + elif meta_data['add_colorbar']: self.add_colorbar(label=self._hue_label, **cbar_kwargs) return self diff --git a/xarray/tests/test_plot.py b/xarray/tests/test_plot.py index 44ba48577dd..87befa0e427 100644 --- a/xarray/tests/test_plot.py +++ b/xarray/tests/test_plot.py @@ -1852,18 +1852,17 @@ def test_figsize_and_size(self): with pytest.raises(ValueError): self.ds.plot.scatter(x='A', y='B', row='row', size=3, figsize=4) - @pytest.mark.parametrize('x, y, hue_style, add_legend, add_colorbar', [ - ('A', 'B', 'something', True, False), - ('A', 'B', 'discrete', True, False), - ('A', 'B', None, True, False), ('A', 'B', None, False, True), - ('A', 'The Spanish Inquisition', None, None, None), - ('The Spanish Inquisition', 'B', None, None, None)]) - def test_bad_args(self, x, y, hue_style, add_legend, add_colorbar): + @pytest.mark.parametrize('x, y, hue_style, add_guide', [ + ('A', 'B', 'something', True), + ('A', 'B', 'discrete', True), + ('A', 'B', None, True), + ('A', 'The Spanish Inquisition', None, None), + ('The Spanish Inquisition', 'B', None, True)]) + def test_bad_args(self, x, y, hue_style, add_guide): with pytest.raises(ValueError): self.ds.plot.scatter(x, y, hue_style=hue_style, - add_legend=add_legend, - add_colorbar=add_colorbar) + add_guide=add_guide) @pytest.mark.xfail(reason=['datetime, timedelta64 hue variable' ' not supported yet.']) From d7e9a0f8e1c824a24d54d1f6a59bc285a693a22b Mon Sep 17 00:00:00 2001 From: dcherian Date: Thu, 14 Feb 2019 10:34:36 -0800 Subject: [PATCH 072/101] Save hist for a future PR. --- xarray/plot/dataset_plot.py | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/xarray/plot/dataset_plot.py b/xarray/plot/dataset_plot.py index 0f4b7fa6be4..d89f81b9f32 100644 --- a/xarray/plot/dataset_plot.py +++ b/xarray/plot/dataset_plot.py @@ -291,16 +291,3 @@ def scatter(ds, x, y, ax, **kwargs): **cmap_params, **kwargs) return primitive - - -@_dsplot -def hist(ds, x, y, ax, **kwargs): - - cmap_params = kwargs.pop('cmap_params') - - xplt, yplt = broadcast(ds[x], ds[y]) - _, _, _, primitive = ax.hist2d(ds[x].values.ravel(), - ds[y].values.ravel(), - **cmap_params, **kwargs) - - return primitive From ce41d4e0bcefb349c6827a2094c7d334db5c97a9 Mon Sep 17 00:00:00 2001 From: dcherian Date: Thu, 14 Feb 2019 10:39:29 -0800 Subject: [PATCH 073/101] rename _numeric to _is_numeric. --- xarray/plot/dataset_plot.py | 4 ++-- xarray/plot/utils.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/xarray/plot/dataset_plot.py b/xarray/plot/dataset_plot.py index d89f81b9f32..28c62ce03ed 100644 --- a/xarray/plot/dataset_plot.py +++ b/xarray/plot/dataset_plot.py @@ -3,7 +3,7 @@ from ..core.alignment import broadcast from .facetgrid import _easy_facetgrid from .utils import ( - _add_colorbar, _ensure_numeric, _process_cmap_cbar_kwargs, get_axis, + _add_colorbar, _is_numeric, _process_cmap_cbar_kwargs, get_axis, label_from_attrs) @@ -24,7 +24,7 @@ def _infer_meta_data(ds, x, y, hue, hue_style, add_guide): 'instead.'.format(', '.join(dims_coords)), hue) if hue: - hue_is_numeric = _ensure_numeric(ds[hue].values) + hue_is_numeric = _is_numeric(ds[hue].values) if hue_style is None: hue_style = 'continuous' if hue_is_numeric else 'discrete' diff --git a/xarray/plot/utils.py b/xarray/plot/utils.py index 594dc88c8de..d346c694e71 100644 --- a/xarray/plot/utils.py +++ b/xarray/plot/utils.py @@ -502,7 +502,7 @@ def _ensure_plottable(*args): 'package.') -def _numeric(arr): +def _is_numeric(arr): numpy_types = [np.floating, np.integer] return _valid_numpy_subdtype(arr, numpy_types) From 4b59672b34129a67434c590a0341cacd833e0215 Mon Sep 17 00:00:00 2001 From: dcherian Date: Thu, 14 Feb 2019 10:47:03 -0800 Subject: [PATCH 074/101] Raise error if add_colorbar or add_legend are passed to scatter. --- xarray/plot/dataset_plot.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/xarray/plot/dataset_plot.py b/xarray/plot/dataset_plot.py index 28c62ce03ed..edc232d32bd 100644 --- a/xarray/plot/dataset_plot.py +++ b/xarray/plot/dataset_plot.py @@ -273,6 +273,12 @@ def plotmethod(_PlotMethods_obj, x=None, y=None, hue=None, @_dsplot def scatter(ds, x, y, ax, **kwargs): """ Scatter Dataset data variables against each other. """ + + if 'add_colorbar' in kwargs or 'add_legend' in kwargs: + raise ValueError('Dataset.plot.scatter does not accept ' + + '\'add_colorbar\' or \'add_legend\'. ' + + 'Use \'add_guide\' instead.') + cmap_params = kwargs.pop('cmap_params') hue = kwargs.pop('hue') hue_style = kwargs.pop('hue_style') From 50468da7bbe8c771594f8881bebc30c9bae93f5e Mon Sep 17 00:00:00 2001 From: dcherian Date: Thu, 14 Feb 2019 18:58:19 -0800 Subject: [PATCH 075/101] Add scatter_example_dataset to tutorial.py --- doc/plotting.rst | 18 ++---------------- xarray/tutorial.py | 26 ++++++++++++++++++++++++++ 2 files changed, 28 insertions(+), 16 deletions(-) diff --git a/doc/plotting.rst b/doc/plotting.rst index 129aaec7239..78b78e7b30a 100644 --- a/doc/plotting.rst +++ b/doc/plotting.rst @@ -617,22 +617,8 @@ Consider this dataset .. ipython:: python - A = xr.DataArray(np.zeros([3, 11, 4, 4]), dims=[ 'x', 'y', 'z', 'w'], - coords=[np.arange(3), np.linspace(0,1,11), np.arange(4), - 0.1*np.random.randn(4)]) - # fake some data - B = 0.1*A.x**2 + A.y**2.5 + 0.1*A.z*A.w - A = -0.1*A.x + A.y/(5+A.z) + A.w - - ds = xr.Dataset({'A':A, 'B':B}) - ds['w'] = ['one', 'two', 'three', 'five'] - - # add some attributes to showcase automatic labelling - ds.x.attrs['units'] = 'xunits' - ds.y.attrs['units'] = 'yunits' - ds.z.attrs['units'] = 'zunits' - ds.A.attrs['units'] = 'Aunits' - ds.B.attrs['units'] = 'Bunits' + ds = xr.tutorial.scatter_example_dataset() + ds Suppose we want to scatter ``A`` against ``B`` diff --git a/xarray/tutorial.py b/xarray/tutorial.py index 3f92bd9a400..c670760a6d7 100644 --- a/xarray/tutorial.py +++ b/xarray/tutorial.py @@ -10,7 +10,11 @@ import warnings from urllib.request import urlretrieve +import numpy as np + from .backends.api import open_dataset as _open_dataset +from .core.dataarray import DataArray +from .core.dataset import Dataset _default_cache_dir = _os.sep.join(('~', '.xarray_tutorial_data')) @@ -104,3 +108,25 @@ def load_dataset(*args, **kwargs): "`tutorial.open_dataset(...).load()`.", DeprecationWarning, stacklevel=2) return open_dataset(*args, **kwargs).load() + + +def scatter_example_dataset(): + A = DataArray(np.zeros([3, 11, 4, 4]), dims=['x', 'y', 'z', 'w'], + coords=[np.arange(3), + np.linspace(0, 1, 11), + np.arange(4), + 0.1*np.random.randn(4)]) + B = 0.1*A.x**2+A.y**2.5+0.1*A.z*A.w + A = -0.1*A.x+A.y/(5+A.z)+A.w + ds = Dataset({'A': A, 'B': B}) + ds['w'] = ['one', 'two', 'three', 'five'] + + ds.x.attrs['units'] = 'xunits' + ds.y.attrs['units'] = 'yunits' + ds.z.attrs['units'] = 'zunits' + ds.w.attrs['units'] = 'wunits' + + ds.A.attrs['units'] = 'Aunits' + ds.B.attrs['units'] = 'Bunits' + + return ds From 68906e25f34b495dd9fba93ac680da9c1f806661 Mon Sep 17 00:00:00 2001 From: dcherian Date: Thu, 14 Feb 2019 19:35:34 -0800 Subject: [PATCH 076/101] Support scattering against coordinates, dimensions or data vars --- xarray/plot/dataset_plot.py | 62 ++++++++++++++++++++++++------------- xarray/plot/facetgrid.py | 4 +-- xarray/tests/test_plot.py | 13 ++++---- 3 files changed, 49 insertions(+), 30 deletions(-) diff --git a/xarray/plot/dataset_plot.py b/xarray/plot/dataset_plot.py index edc232d32bd..f94ae4de1c8 100644 --- a/xarray/plot/dataset_plot.py +++ b/xarray/plot/dataset_plot.py @@ -8,20 +8,18 @@ def _infer_meta_data(ds, x, y, hue, hue_style, add_guide): - dvars = set(ds.data_vars.keys()) - error_msg = (' must be either one of ({0:s})' + dvars = set(ds.variables.keys()) + error_msg = (' must be one of ({0:s})' .format(', '.join(dvars))) if x not in dvars: - raise ValueError(x + error_msg) + raise ValueError('x' + error_msg) if y not in dvars: - raise ValueError(y + error_msg) + raise ValueError('y' + error_msg) - dims_coords = set(list(ds.coords) + list(ds.dims)) - if hue is not None and hue not in dims_coords: - raise ValueError('hue must be one of ({0:s}) but is {hue}' - 'instead.'.format(', '.join(dims_coords)), hue) + if hue is not None and hue not in dvars: + raise ValueError('hue' + error_msg) if hue: hue_is_numeric = _is_numeric(ds[hue].values) @@ -47,14 +45,9 @@ def _infer_meta_data(ds, x, y, hue, hue_style, add_guide): raise ValueError('hue_style must be either None, \'discrete\' ' 'or \'continuous\'.') - dims = ds[x].dims - if ds[y].dims != dims: - raise ValueError('{} and {} must have the same dimensions.' - ''.format(x, y)) - if hue: - hue_label = label_from_attrs(ds.coords[hue]) - hue_values = ds[x].coords[hue] + hue_label = label_from_attrs(ds[hue]) + hue_values = ds[hue].values else: hue_label = None hue_values = None @@ -70,12 +63,37 @@ def _infer_meta_data(ds, x, y, hue, hue_style, add_guide): def _infer_scatter_data(ds, x, y, hue): - data = {'x': ds[x].values.flatten(), - 'y': ds[y].values.flatten(), - 'color': None} + broadcast_keys = ['x', 'y'] + to_broadcast = [ds[x], ds[y]] + if hue: + to_broadcast.append(ds[hue]) + broadcast_keys.append('hue') + if scatter_size: + to_broadcast.append(ds[scatter_size]) + broadcast_keys.append('size') + + broadcasted = dict(zip(broadcast_keys, broadcast(*to_broadcast))) + + data = {'x': broadcasted['x'].values.flatten(), + 'y': broadcasted['y'].values.flatten(), + 'color': None, + 'sizes': None} + if hue: - data['color'] = (broadcast(ds.coords[hue], ds[x])[0] - .values.flatten()) + data['color'] = broadcasted['hue'].values.flatten() + + if scatter_size: + ss = (broadcast(ds[scatter_size], xx)[0] + .values.flatten()) + + size_mapping = _parse_size(ss, size_norm) + + if _is_numeric(ss): + data['sizes'] = np.array(size_mapping.values())[ + np.digitize(ss, np.array(size_mapping.keys()))] + else: + data['sizes'] = np.array([size_mapping.get(s0) for s0 in ss]) + return data @@ -208,7 +226,7 @@ def newplotfunc(ds, x=None, y=None, hue=None, hue_style=None, cmap_params = meta_data['cmap_params'] else: cmap_params, cbar_kwargs = _process_cmap_cbar_kwargs( - plotfunc, locals(), ds[hue]) + plotfunc, locals(), ds[hue].values) # subset that can be passed to scatter, hist2d cmap_params_subset = dict( @@ -231,7 +249,7 @@ def newplotfunc(ds, x=None, y=None, hue=None, hue_style=None, if meta_data['add_legend']: ax.legend(handles=primitive, - labels=list(meta_data['hue_values'].values), + labels=list(meta_data['hue_values']), title=meta_data.get('hue_label', None)) if meta_data['add_colorbar']: cbar_kwargs = {} if cbar_kwargs is None else cbar_kwargs diff --git a/xarray/plot/facetgrid.py b/xarray/plot/facetgrid.py index c733640a0e8..407660b4c33 100644 --- a/xarray/plot/facetgrid.py +++ b/xarray/plot/facetgrid.py @@ -292,7 +292,7 @@ def map_dataset(self, func, x=None, y=None, hue=None, hue_style=None, if hue and meta_data['hue_style'] == 'continuous': cmap_params, cbar_kwargs = _process_cmap_cbar_kwargs( - func, kwargs, self.data[hue]) + func, kwargs, self.data[hue].values) kwargs['meta_data']['cmap_params'] = cmap_params kwargs['meta_data']['cbar_kwargs'] = cbar_kwargs @@ -335,7 +335,7 @@ def _finalize_grid(self, *axlabels): def add_legend(self, **kwargs): figlegend = self.fig.legend( handles=self._mappables[-1], - labels=list(self._hue_var.values), + labels=list(self._hue_var), title=self._hue_label, loc="center right", **kwargs) diff --git a/xarray/tests/test_plot.py b/xarray/tests/test_plot.py index 4afbb49d1bd..2a88c7ffc23 100644 --- a/xarray/tests/test_plot.py +++ b/xarray/tests/test_plot.py @@ -1854,10 +1854,10 @@ def test_figsize_and_size(self): with pytest.raises(ValueError): self.ds.plot.scatter(x='A', y='B', row='row', size=3, figsize=4) - ('A', 'B', None, False, True), @pytest.mark.parametrize('x, y, hue_style, add_guide', [ ('A', 'B', 'something', True), ('A', 'B', 'discrete', True), + ('x', 'y', 'A', None), ('A', 'B', None, True), ('A', 'The Spanish Inquisition', None, None), ('The Spanish Inquisition', 'B', None, True)]) @@ -1888,6 +1888,12 @@ def test_facetgrid_hue_style(self): # for 'continuous', should be single PathCollection assert isinstance(g._mappables[-1], map_type) + @pytest.mark.parametrize('x, y, hue, scatter_size', [ + ('A', 'B', 'x', 'col'), + ('x', 'row', 'A', 'B')]) + def test_scatter(self, x, y, hue, scatter_size): + self.ds.plot.scatter(x, y, hue=hue, scatter_size=scatter_size) + def test_non_numeric_legend(self): ds2 = self.ds.copy() ds2['hue'] = ['a', 'b', 'c', 'd'] @@ -1903,11 +1909,6 @@ def test_add_legend_by_default(self): sc = self.ds.plot.scatter(x='A', y='B', hue='hue') assert len(sc.figure.axes) == 2 - def test_not_same_dimensions(self): - self.ds['A'] = self.ds['A'].isel(x=0) - with pytest.raises(ValueError): - self.ds.plot.scatter(x='A', y='B') - class TestDatetimePlot(PlotTestCase): @pytest.fixture(autouse=True) From 4b6a4ef4813bc955e10bc43fc7afbd7acc1306b3 Mon Sep 17 00:00:00 2001 From: dcherian Date: Thu, 14 Feb 2019 19:19:56 -0800 Subject: [PATCH 077/101] Support 'scatter_size' kwarg --- doc/plotting.rst | 6 +++ xarray/plot/dataset_plot.py | 91 +++++++++++++++++++++++++++++++------ xarray/plot/facetgrid.py | 3 ++ xarray/tests/test_plot.py | 1 - 4 files changed, 87 insertions(+), 14 deletions(-) diff --git a/doc/plotting.rst b/doc/plotting.rst index 78b78e7b30a..cb49c3ccdf1 100644 --- a/doc/plotting.rst +++ b/doc/plotting.rst @@ -645,6 +645,12 @@ You can force a legend instead of a colorbar by setting ``hue_style='discrete'`` @savefig ds_discrete_legend_hue_scatter.png ds.plot.scatter(x='A', y='B', hue='w', hue_style='discrete') +The ``scatter_size`` kwarg lets you vary the point's size by variable value. You can additionally pass ``size_norm`` to control how the variable's values are mapped to point sizes. + +.. ipython:: python + + @savefig ds_hue_size_scatter.png + ds.plot.scatter(x='A', y='B', hue='z', hue_style='discrete', scatter_size='z') Faceting is also possible diff --git a/xarray/plot/dataset_plot.py b/xarray/plot/dataset_plot.py index f94ae4de1c8..5cf0816e550 100644 --- a/xarray/plot/dataset_plot.py +++ b/xarray/plot/dataset_plot.py @@ -1,11 +1,18 @@ import functools +import matplotlib as mpl +import numpy as np + from ..core.alignment import broadcast from .facetgrid import _easy_facetgrid from .utils import ( _add_colorbar, _is_numeric, _process_cmap_cbar_kwargs, get_axis, label_from_attrs) +# copied from seaborn +_SCATTER_SIZE_RANGE = (np.r_[.5, 2] + * np.square(mpl.rcParams["lines.markersize"])) + def _infer_meta_data(ds, x, y, hue, hue_style, add_guide): dvars = set(ds.variables.keys()) @@ -61,7 +68,7 @@ def _infer_meta_data(ds, x, y, hue, hue_style, add_guide): 'hue_values': hue_values} -def _infer_scatter_data(ds, x, y, hue): +def _infer_scatter_data(ds, x, y, hue, scatter_size, size_norm): broadcast_keys = ['x', 'y'] to_broadcast = [ds[x], ds[y]] @@ -83,20 +90,59 @@ def _infer_scatter_data(ds, x, y, hue): data['color'] = broadcasted['hue'].values.flatten() if scatter_size: - ss = (broadcast(ds[scatter_size], xx)[0] - .values.flatten()) + ss = broadcasted['size'].values.flatten() size_mapping = _parse_size(ss, size_norm) if _is_numeric(ss): - data['sizes'] = np.array(size_mapping.values())[ - np.digitize(ss, np.array(size_mapping.keys()))] + # TODO : is there a better way of doing this? + map_keys = np.array(list(size_mapping.keys())) + map_vals = np.array(list(size_mapping.values())) + data['sizes'] = map_vals[np.digitize(ss, map_keys) - 1] else: data['sizes'] = np.array([size_mapping.get(s0) for s0 in ss]) return data +# copied from seaborn +def _parse_size(data, norm): + + if data is None: + return None + + if not _is_numeric(data): + levels = np.unique(data) + numbers = np.arange(1, 1+len(levels))[::-1] + else: + levels = numbers = np.sort(np.unique(data)) + + min_width, max_width = _SCATTER_SIZE_RANGE + # width_range = min_width, max_width + + if norm is None: + norm = mpl.colors.Normalize() + elif isinstance(norm, tuple): + norm = mpl.colors.Normalize(*norm) + elif not isinstance(norm, mpl.colors.Normalize): + err = ("``size_norm`` must be None, tuple, " + "or Normalize object.") + raise ValueError(err) + + norm.clip = True + if not norm.scaled(): + norm(np.asarray(numbers)) + # limits = norm.vmin, norm.vmax + + scl = norm(numbers) + widths = np.asarray(min_width + scl * (max_width - min_width)) + if scl.mask.any(): + widths[scl.mask] = 0 + sizes = dict(zip(levels, widths)) + + return sizes + + class _Dataset_PlotMethods(object): """ Enables use of xarray.plot functions as attributes on a Dataset. @@ -123,6 +169,10 @@ def _dsplot(plotfunc): Variable by which to color scattered points hue_style: str, optional Hue style. Can be either 'discrete' or 'continuous'. + scatter_size: str, optional (scatter only) + Variably by which to vary size of scattered points + size_norm: optional + Either None or Norm instance to normalize the 'scatter_size' variable. add_guide: bool, optional Add a guide that depends on hue_style - for "discrete", build a legend. @@ -215,7 +265,7 @@ def newplotfunc(ds, x=None, y=None, hue=None, hue_style=None, for arg in ['meta_data', 'kwargs', 'ds']: del allargs[arg] - return _easy_facetgrid(kind='dataset', **allargs) + return _easy_facetgrid(kind='dataset', **allargs, **kwargs) figsize = kwargs.pop('figsize', None) ax = get_axis(figsize, size, aspect, ax) @@ -290,7 +340,9 @@ def plotmethod(_PlotMethods_obj, x=None, y=None, hue=None, @_dsplot def scatter(ds, x, y, ax, **kwargs): - """ Scatter Dataset data variables against each other. """ + """ + Scatter Dataset data variables against each other. + """ if 'add_colorbar' in kwargs or 'add_legend' in kwargs: raise ValueError('Dataset.plot.scatter does not accept ' @@ -300,18 +352,31 @@ def scatter(ds, x, y, ax, **kwargs): cmap_params = kwargs.pop('cmap_params') hue = kwargs.pop('hue') hue_style = kwargs.pop('hue_style') + scatter_size = kwargs.pop('scatter_size', None) + size_norm = kwargs.pop('size_norm', None) + + # need to infer size_mapping with full dataset + data = _infer_scatter_data(ds, x, y, hue, scatter_size, size_norm) if hue_style == 'discrete': primitive = [] - for label, grp in ds.groupby(ds[hue]): - data = _infer_scatter_data(grp, x, y, hue=None) - primitive.append(ax.scatter(data['x'], data['y'], label=label, + for label in np.unique(data['color']): + # is there a clever way to avoid this? + # data = _infer_scatter_data(grp, x, y, None, scatter_size, size_norm) + mask = data['color'] == label + if data['sizes'] is not None: + kwargs.update(s=data['sizes'][mask]) + + primitive.append(ax.scatter(data['x'][mask], + data['y'][mask], + label=label, **kwargs)) elif hue is None or hue_style == 'continuous': - data = _infer_scatter_data(ds, x, y, hue) - - primitive = ax.scatter(data['x'], data['y'], c=data['color'], + primitive = ax.scatter(data['x'], + data['y'], + c=data['color'], + s=data['sizes'], **cmap_params, **kwargs) return primitive diff --git a/xarray/plot/facetgrid.py b/xarray/plot/facetgrid.py index 407660b4c33..acc1b13b53a 100644 --- a/xarray/plot/facetgrid.py +++ b/xarray/plot/facetgrid.py @@ -286,6 +286,9 @@ def map_dataset(self, func, x=None, y=None, hue=None, hue_style=None, kwargs['add_guide'] = False kwargs['_is_facetgrid'] = True + if kwargs.get('scatter_size'): + raise NotImplementedError('Cannot facet with scatter_size specified.') + meta_data = _infer_meta_data(self.data, x, y, hue, hue_style, add_guide) kwargs['meta_data'] = meta_data diff --git a/xarray/tests/test_plot.py b/xarray/tests/test_plot.py index 2a88c7ffc23..191191bd46d 100644 --- a/xarray/tests/test_plot.py +++ b/xarray/tests/test_plot.py @@ -1857,7 +1857,6 @@ def test_figsize_and_size(self): @pytest.mark.parametrize('x, y, hue_style, add_guide', [ ('A', 'B', 'something', True), ('A', 'B', 'discrete', True), - ('x', 'y', 'A', None), ('A', 'B', None, True), ('A', 'The Spanish Inquisition', None, None), ('The Spanish Inquisition', 'B', None, True)]) From ccd9c4260615245b8915faa803e070c987bc0553 Mon Sep 17 00:00:00 2001 From: dcherian Date: Fri, 15 Feb 2019 00:11:03 -0800 Subject: [PATCH 078/101] =?UTF-8?q?color=20=E2=86=92=20hue=20and=20other?= =?UTF-8?q?=20changes.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- xarray/plot/dataset_plot.py | 53 +++++++++++++++++++++---------------- 1 file changed, 30 insertions(+), 23 deletions(-) diff --git a/xarray/plot/dataset_plot.py b/xarray/plot/dataset_plot.py index 5cf0816e550..0eebc3c3aa7 100644 --- a/xarray/plot/dataset_plot.py +++ b/xarray/plot/dataset_plot.py @@ -81,26 +81,29 @@ def _infer_scatter_data(ds, x, y, hue, scatter_size, size_norm): broadcasted = dict(zip(broadcast_keys, broadcast(*to_broadcast))) - data = {'x': broadcasted['x'].values.flatten(), - 'y': broadcasted['y'].values.flatten(), - 'color': None, + data = {'x': broadcasted['x'], + 'y': broadcasted['y'], + 'hue': None, 'sizes': None} if hue: - data['color'] = broadcasted['hue'].values.flatten() + data['hue'] = broadcasted['hue'] if scatter_size: - ss = broadcasted['size'].values.flatten() + size = broadcasted['size'] - size_mapping = _parse_size(ss, size_norm) + size_mapping = _parse_size(size, size_norm) - if _is_numeric(ss): - # TODO : is there a better way of doing this? + if _is_numeric(size): + # TODO : is there a better vectorized way of doing + # data['sizes'] = np.array([size_mapping.get(s0) for s0 in ss]) map_keys = np.array(list(size_mapping.keys())) map_vals = np.array(list(size_mapping.values())) - data['sizes'] = map_vals[np.digitize(ss, map_keys) - 1] + data['sizes'] = size.copy( + data=map_vals[np.digitize(size, map_keys) - 1]) else: - data['sizes'] = np.array([size_mapping.get(s0) for s0 in ss]) + data['sizes'] = size.copy( + data=np.array([size_mapping.get(s0) for s0 in size])) return data @@ -111,6 +114,8 @@ def _parse_size(data, norm): if data is None: return None + data = data.values.flatten() + if not _is_numeric(data): levels = np.unique(data) numbers = np.arange(1, 1+len(levels))[::-1] @@ -360,23 +365,25 @@ def scatter(ds, x, y, ax, **kwargs): if hue_style == 'discrete': primitive = [] - for label in np.unique(data['color']): - # is there a clever way to avoid this? - # data = _infer_scatter_data(grp, x, y, None, scatter_size, size_norm) - mask = data['color'] == label + for label in np.unique(data['hue'].values): + mask = data['hue'] == label if data['sizes'] is not None: - kwargs.update(s=data['sizes'][mask]) + kwargs.update( + s=data['sizes'].where(mask, drop=True).values.flatten()) - primitive.append(ax.scatter(data['x'][mask], - data['y'][mask], - label=label, - **kwargs)) + primitive.append( + ax.scatter(data['x'].where(mask, drop=True).values.flatten(), + data['y'].where(mask, drop=True).values.flatten(), + label=label, **kwargs)) elif hue is None or hue_style == 'continuous': - primitive = ax.scatter(data['x'], - data['y'], - c=data['color'], - s=data['sizes'], + if data['sizes'] is not None: + kwargs.update(s=data['sizes'].values.ravel()) + if data['hue'] is not None: + kwargs.update(c=data['hue'].values.ravel()) + + primitive = ax.scatter(data['x'].values.ravel(), + data['y'].values.ravel(), **cmap_params, **kwargs) return primitive From 4006531acf68f3ed0f51c6366253a0c567247179 Mon Sep 17 00:00:00 2001 From: dcherian Date: Fri, 15 Feb 2019 00:20:24 -0800 Subject: [PATCH 079/101] Facetgrid support for scatter_size. --- xarray/plot/dataset_plot.py | 10 +++++++--- xarray/plot/facetgrid.py | 8 +++++--- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/xarray/plot/dataset_plot.py b/xarray/plot/dataset_plot.py index 0eebc3c3aa7..6f8bffbabaa 100644 --- a/xarray/plot/dataset_plot.py +++ b/xarray/plot/dataset_plot.py @@ -68,7 +68,8 @@ def _infer_meta_data(ds, x, y, hue, hue_style, add_guide): 'hue_values': hue_values} -def _infer_scatter_data(ds, x, y, hue, scatter_size, size_norm): +def _infer_scatter_data(ds, x, y, hue, scatter_size, size_norm, + size_mapping=None): broadcast_keys = ['x', 'y'] to_broadcast = [ds[x], ds[y]] @@ -92,7 +93,8 @@ def _infer_scatter_data(ds, x, y, hue, scatter_size, size_norm): if scatter_size: size = broadcasted['size'] - size_mapping = _parse_size(size, size_norm) + if size_mapping is None: + size_mapping = _parse_size(size, size_norm) if _is_numeric(size): # TODO : is there a better vectorized way of doing @@ -359,9 +361,11 @@ def scatter(ds, x, y, ax, **kwargs): hue_style = kwargs.pop('hue_style') scatter_size = kwargs.pop('scatter_size', None) size_norm = kwargs.pop('size_norm', None) + size_mapping = kwargs.pop('size_mapping', None) # set by facetgrid # need to infer size_mapping with full dataset - data = _infer_scatter_data(ds, x, y, hue, scatter_size, size_norm) + data = _infer_scatter_data(ds, x, y, hue, + scatter_size, size_norm, size_mapping) if hue_style == 'discrete': primitive = [] diff --git a/xarray/plot/facetgrid.py b/xarray/plot/facetgrid.py index acc1b13b53a..4e03ff07491 100644 --- a/xarray/plot/facetgrid.py +++ b/xarray/plot/facetgrid.py @@ -281,13 +281,15 @@ def map_dataarray_line(self, func, x, y, **kwargs): def map_dataset(self, func, x=None, y=None, hue=None, hue_style=None, add_guide=None, **kwargs): - from .dataset_plot import _infer_meta_data + from .dataset_plot import _infer_meta_data, _parse_size kwargs['add_guide'] = False kwargs['_is_facetgrid'] = True - if kwargs.get('scatter_size'): - raise NotImplementedError('Cannot facet with scatter_size specified.') + if kwargs.get('scatter_size', None): + kwargs['size_mapping'] = _parse_size( + self.data[kwargs['scatter_size']], + kwargs.pop('size_norm', None)) meta_data = _infer_meta_data(self.data, x, y, hue, hue_style, add_guide) From 7f46f034006d6e37bc149b505d469e260ac4efe6 Mon Sep 17 00:00:00 2001 From: dcherian Date: Sun, 17 Feb 2019 18:51:06 +0530 Subject: [PATCH 080/101] add_guide in docs. --- doc/plotting.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/plotting.rst b/doc/plotting.rst index cb49c3ccdf1..631d292c5eb 100644 --- a/doc/plotting.rst +++ b/doc/plotting.rst @@ -638,6 +638,7 @@ The ``hue`` kwarg lets you vary the color by variable value When ``hue`` is specified, a colorbar is added for numeric ``hue`` DataArrays by default and a legend is added for non-numeric ``hue`` DataArrays (as above). You can force a legend instead of a colorbar by setting ``hue_style='discrete'``. +Additionally, the boolean kwarg ``add_guide`` can be used to prevent the display of a legend or colorbar (as appropriate). .. ipython:: python From 194ff8521e9b0274faccd2ef38efb4cba35021fb Mon Sep 17 00:00:00 2001 From: dcherian Date: Sun, 3 Mar 2019 04:42:21 -0800 Subject: [PATCH 081/101] Avoid top-level matplotlib import --- xarray/plot/dataset_plot.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/xarray/plot/dataset_plot.py b/xarray/plot/dataset_plot.py index 6f8bffbabaa..a347dde8616 100644 --- a/xarray/plot/dataset_plot.py +++ b/xarray/plot/dataset_plot.py @@ -1,6 +1,5 @@ import functools -import matplotlib as mpl import numpy as np from ..core.alignment import broadcast @@ -10,8 +9,7 @@ label_from_attrs) # copied from seaborn -_SCATTER_SIZE_RANGE = (np.r_[.5, 2] - * np.square(mpl.rcParams["lines.markersize"])) +_SCATTER_SIZE_RANGE = np.array([18.0, 72.0]) def _infer_meta_data(ds, x, y, hue, hue_style, add_guide): @@ -113,6 +111,8 @@ def _infer_scatter_data(ds, x, y, hue, scatter_size, size_norm, # copied from seaborn def _parse_size(data, norm): + import matplotlib as mpl + if data is None: return None From 1e66a3e7d1f025e60a1da9cddee3a84cf9322fb0 Mon Sep 17 00:00:00 2001 From: dcherian Date: Sun, 3 Mar 2019 23:15:37 -0800 Subject: [PATCH 082/101] Fix lint errors. --- xarray/plot/dataset_plot.py | 2 +- xarray/tutorial.py | 9 +++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/xarray/plot/dataset_plot.py b/xarray/plot/dataset_plot.py index a347dde8616..4b6e1a7c523 100644 --- a/xarray/plot/dataset_plot.py +++ b/xarray/plot/dataset_plot.py @@ -120,7 +120,7 @@ def _parse_size(data, norm): if not _is_numeric(data): levels = np.unique(data) - numbers = np.arange(1, 1+len(levels))[::-1] + numbers = np.arange(1, 1 + len(levels))[::-1] else: levels = numbers = np.sort(np.unique(data)) diff --git a/xarray/tutorial.py b/xarray/tutorial.py index c670760a6d7..0eaf95b75ea 100644 --- a/xarray/tutorial.py +++ b/xarray/tutorial.py @@ -111,13 +111,14 @@ def load_dataset(*args, **kwargs): def scatter_example_dataset(): - A = DataArray(np.zeros([3, 11, 4, 4]), dims=['x', 'y', 'z', 'w'], + A = DataArray(np.zeros([3, 11, 4, 4]), + dims=['x', 'y', 'z', 'w'], coords=[np.arange(3), np.linspace(0, 1, 11), np.arange(4), - 0.1*np.random.randn(4)]) - B = 0.1*A.x**2+A.y**2.5+0.1*A.z*A.w - A = -0.1*A.x+A.y/(5+A.z)+A.w + 0.1 * np.random.randn(4)]) + B = 0.1 * A.x**2 + A.y**2.5 + 0.1 * A.z * A.w + A = -0.1 * A.x + A.y / (5 + A.z) + A.w ds = Dataset({'A': A, 'B': B}) ds['w'] = ['one', 'two', 'three', 'five'] From d5151df324099dca3edbe7980a15d3b50f255cf8 Mon Sep 17 00:00:00 2001 From: dcherian Date: Wed, 6 Mar 2019 05:26:24 -0800 Subject: [PATCH 083/101] Follow shoyer's suggestions. --- xarray/plot/dataset_plot.py | 22 ++++++++-------------- 1 file changed, 8 insertions(+), 14 deletions(-) diff --git a/xarray/plot/dataset_plot.py b/xarray/plot/dataset_plot.py index 4b6e1a7c523..a4400064c33 100644 --- a/xarray/plot/dataset_plot.py +++ b/xarray/plot/dataset_plot.py @@ -1,6 +1,7 @@ import functools import numpy as np +import pandas as pd from ..core.alignment import broadcast from .facetgrid import _easy_facetgrid @@ -94,16 +95,9 @@ def _infer_scatter_data(ds, x, y, hue, scatter_size, size_norm, if size_mapping is None: size_mapping = _parse_size(size, size_norm) - if _is_numeric(size): - # TODO : is there a better vectorized way of doing - # data['sizes'] = np.array([size_mapping.get(s0) for s0 in ss]) - map_keys = np.array(list(size_mapping.keys())) - map_vals = np.array(list(size_mapping.values())) - data['sizes'] = size.copy( - data=map_vals[np.digitize(size, map_keys) - 1]) - else: - data['sizes'] = size.copy( - data=np.array([size_mapping.get(s0) for s0 in size])) + data['sizes'] = size.copy( + data=np.reshape(size_mapping.loc[size.values.ravel()].values, + size.shape)) return data @@ -147,7 +141,7 @@ def _parse_size(data, norm): widths[scl.mask] = 0 sizes = dict(zip(levels, widths)) - return sizes + return pd.Series(sizes) class _Dataset_PlotMethods(object): @@ -352,9 +346,9 @@ def scatter(ds, x, y, ax, **kwargs): """ if 'add_colorbar' in kwargs or 'add_legend' in kwargs: - raise ValueError('Dataset.plot.scatter does not accept ' - + '\'add_colorbar\' or \'add_legend\'. ' - + 'Use \'add_guide\' instead.') + raise ValueError("Dataset.plot.scatter does not accept " + "'add_colorbar' or 'add_legend'. " + "Use 'add_guide' instead.") cmap_params = kwargs.pop('cmap_params') hue = kwargs.pop('hue') From cffaf4437a85eaa4993dec66c39905d05d3ae901 Mon Sep 17 00:00:00 2001 From: dcherian Date: Wed, 6 Mar 2019 05:29:44 -0800 Subject: [PATCH 084/101] =?UTF-8?q?scatter=5Fsize=20=E2=86=92=20markersize?= =?UTF-8?q?.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- doc/plotting.rst | 4 ++-- xarray/plot/dataset_plot.py | 20 ++++++++++---------- xarray/tests/test_plot.py | 6 +++--- 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/doc/plotting.rst b/doc/plotting.rst index 631d292c5eb..9e372d7234e 100644 --- a/doc/plotting.rst +++ b/doc/plotting.rst @@ -646,12 +646,12 @@ Additionally, the boolean kwarg ``add_guide`` can be used to prevent the display @savefig ds_discrete_legend_hue_scatter.png ds.plot.scatter(x='A', y='B', hue='w', hue_style='discrete') -The ``scatter_size`` kwarg lets you vary the point's size by variable value. You can additionally pass ``size_norm`` to control how the variable's values are mapped to point sizes. +The ``markersize`` kwarg lets you vary the point's size by variable value. You can additionally pass ``size_norm`` to control how the variable's values are mapped to point sizes. .. ipython:: python @savefig ds_hue_size_scatter.png - ds.plot.scatter(x='A', y='B', hue='z', hue_style='discrete', scatter_size='z') + ds.plot.scatter(x='A', y='B', hue='z', hue_style='discrete', markersize='z') Faceting is also possible diff --git a/xarray/plot/dataset_plot.py b/xarray/plot/dataset_plot.py index a4400064c33..9b2aab6203e 100644 --- a/xarray/plot/dataset_plot.py +++ b/xarray/plot/dataset_plot.py @@ -10,7 +10,7 @@ label_from_attrs) # copied from seaborn -_SCATTER_SIZE_RANGE = np.array([18.0, 72.0]) +_MARKERSIZE_RANGE = np.array([18.0, 72.0]) def _infer_meta_data(ds, x, y, hue, hue_style, add_guide): @@ -67,7 +67,7 @@ def _infer_meta_data(ds, x, y, hue, hue_style, add_guide): 'hue_values': hue_values} -def _infer_scatter_data(ds, x, y, hue, scatter_size, size_norm, +def _infer_scatter_data(ds, x, y, hue, markersize, size_norm, size_mapping=None): broadcast_keys = ['x', 'y'] @@ -75,8 +75,8 @@ def _infer_scatter_data(ds, x, y, hue, scatter_size, size_norm, if hue: to_broadcast.append(ds[hue]) broadcast_keys.append('hue') - if scatter_size: - to_broadcast.append(ds[scatter_size]) + if markersize: + to_broadcast.append(ds[markersize]) broadcast_keys.append('size') broadcasted = dict(zip(broadcast_keys, broadcast(*to_broadcast))) @@ -89,7 +89,7 @@ def _infer_scatter_data(ds, x, y, hue, scatter_size, size_norm, if hue: data['hue'] = broadcasted['hue'] - if scatter_size: + if markersize: size = broadcasted['size'] if size_mapping is None: @@ -118,7 +118,7 @@ def _parse_size(data, norm): else: levels = numbers = np.sort(np.unique(data)) - min_width, max_width = _SCATTER_SIZE_RANGE + min_width, max_width = _MARKERSIZE_RANGE # width_range = min_width, max_width if norm is None: @@ -170,10 +170,10 @@ def _dsplot(plotfunc): Variable by which to color scattered points hue_style: str, optional Hue style. Can be either 'discrete' or 'continuous'. - scatter_size: str, optional (scatter only) + markersize: str, optional (scatter only) Variably by which to vary size of scattered points size_norm: optional - Either None or Norm instance to normalize the 'scatter_size' variable. + Either None or Norm instance to normalize the 'markersize' variable. add_guide: bool, optional Add a guide that depends on hue_style - for "discrete", build a legend. @@ -353,13 +353,13 @@ def scatter(ds, x, y, ax, **kwargs): cmap_params = kwargs.pop('cmap_params') hue = kwargs.pop('hue') hue_style = kwargs.pop('hue_style') - scatter_size = kwargs.pop('scatter_size', None) + markersize = kwargs.pop('markersize', None) size_norm = kwargs.pop('size_norm', None) size_mapping = kwargs.pop('size_mapping', None) # set by facetgrid # need to infer size_mapping with full dataset data = _infer_scatter_data(ds, x, y, hue, - scatter_size, size_norm, size_mapping) + markersize, size_norm, size_mapping) if hue_style == 'discrete': primitive = [] diff --git a/xarray/tests/test_plot.py b/xarray/tests/test_plot.py index 191191bd46d..58bd4b7778b 100644 --- a/xarray/tests/test_plot.py +++ b/xarray/tests/test_plot.py @@ -1887,11 +1887,11 @@ def test_facetgrid_hue_style(self): # for 'continuous', should be single PathCollection assert isinstance(g._mappables[-1], map_type) - @pytest.mark.parametrize('x, y, hue, scatter_size', [ + @pytest.mark.parametrize('x, y, hue, markersize', [ ('A', 'B', 'x', 'col'), ('x', 'row', 'A', 'B')]) - def test_scatter(self, x, y, hue, scatter_size): - self.ds.plot.scatter(x, y, hue=hue, scatter_size=scatter_size) + def test_scatter(self, x, y, hue, markersize): + self.ds.plot.scatter(x, y, hue=hue, markersize=markersize) def test_non_numeric_legend(self): ds2 = self.ds.copy() From 8cd8722eb0e8d2fb50a1f8b8574d1643a561e87b Mon Sep 17 00:00:00 2001 From: dcherian Date: Wed, 6 Mar 2019 10:06:57 -0800 Subject: [PATCH 085/101] Update more error messages. --- xarray/plot/dataset_plot.py | 4 ++-- xarray/plot/plot.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/xarray/plot/dataset_plot.py b/xarray/plot/dataset_plot.py index 9b2aab6203e..bdc800f7c59 100644 --- a/xarray/plot/dataset_plot.py +++ b/xarray/plot/dataset_plot.py @@ -48,8 +48,8 @@ def _infer_meta_data(ds, x, y, hue, hue_style, add_guide): add_colorbar = False if hue_style is not None and hue_style not in ['discrete', 'continuous']: - raise ValueError('hue_style must be either None, \'discrete\' ' - 'or \'continuous\'.') + raise ValueError("hue_style must be either None, 'discrete' " + "or 'continuous'.") if hue: hue_label = label_from_attrs(ds[hue]) diff --git a/xarray/plot/plot.py b/xarray/plot/plot.py index 846a20760bc..48292e2be13 100644 --- a/xarray/plot/plot.py +++ b/xarray/plot/plot.py @@ -69,7 +69,7 @@ def _infer_line_data(darray, x, y, hue): xplt = xplt.transpose(otherdim, huename) else: raise ValueError('For 2D inputs, hue must be a dimension' - + ' i.e. one of ' + repr(darray.dims)) + ' i.e. one of ' + repr(darray.dims)) else: yplt = darray.transpose(xname, huename) @@ -83,7 +83,7 @@ def _infer_line_data(darray, x, y, hue): xplt = darray.transpose(otherdim, huename) else: raise ValueError('For 2D inputs, hue must be a dimension' - + ' i.e. one of ' + repr(darray.dims)) + ' i.e. one of ' + repr(darray.dims)) else: xplt = darray.transpose(yname, huename) From 6af02635938966a621c46bad87bc90cd8cd83c35 Mon Sep 17 00:00:00 2001 From: Yohai Bar Sinai <6164157+yohai@users.noreply.github.com> Date: Thu, 20 Jun 2019 13:07:27 -0400 Subject: [PATCH 086/101] lint errors --- xarray/tutorial.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/xarray/tutorial.py b/xarray/tutorial.py index 2ee02da6d64..e1ce5fa68a7 100644 --- a/xarray/tutorial.py +++ b/xarray/tutorial.py @@ -104,7 +104,7 @@ def load_dataset(*args, **kwargs): with open_dataset(*args, **kwargs) as ds: return ds.load() - + def scatter_example_dataset(): A = DataArray(np.zeros([3, 11, 4, 4]), dims=['x', 'y', 'z', 'w'], @@ -126,4 +126,5 @@ def scatter_example_dataset(): ds.B.attrs['units'] = 'Bunits' return ds + \ No newline at end of file From 9abca607160b8826edb79d31c8db35a3da01d14b Mon Sep 17 00:00:00 2001 From: Yohai Bar Sinai <6164157+yohai@users.noreply.github.com> Date: Thu, 20 Jun 2019 13:46:58 -0400 Subject: [PATCH 087/101] lint errors again --- xarray/tutorial.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/xarray/tutorial.py b/xarray/tutorial.py index e1ce5fa68a7..5e19f5816e6 100644 --- a/xarray/tutorial.py +++ b/xarray/tutorial.py @@ -104,7 +104,7 @@ def load_dataset(*args, **kwargs): with open_dataset(*args, **kwargs) as ds: return ds.load() - + def scatter_example_dataset(): A = DataArray(np.zeros([3, 11, 4, 4]), dims=['x', 'y', 'z', 'w'], @@ -126,5 +126,4 @@ def scatter_example_dataset(): ds.B.attrs['units'] = 'Bunits' return ds - - \ No newline at end of file + From f3de227c6e7911a9ab9db0647d683c442b7aa60d Mon Sep 17 00:00:00 2001 From: Yohai Bar Sinai <6164157+yohai@users.noreply.github.com> Date: Thu, 20 Jun 2019 23:02:08 -0400 Subject: [PATCH 088/101] some more lints --- xarray/tutorial.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/xarray/tutorial.py b/xarray/tutorial.py index 5e19f5816e6..96f548a8766 100644 --- a/xarray/tutorial.py +++ b/xarray/tutorial.py @@ -126,4 +126,4 @@ def scatter_example_dataset(): ds.B.attrs['units'] = 'Bunits' return ds - + From 5b453a7532763e4003cb3b78f65a3022b84a2544 Mon Sep 17 00:00:00 2001 From: Yohai Bar Sinai <6164157+yohai@users.noreply.github.com> Date: Thu, 20 Jun 2019 23:18:00 -0400 Subject: [PATCH 089/101] docstrings --- xarray/plot/dataset_plot.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/xarray/plot/dataset_plot.py b/xarray/plot/dataset_plot.py index bdc800f7c59..3a9b975e49b 100644 --- a/xarray/plot/dataset_plot.py +++ b/xarray/plot/dataset_plot.py @@ -169,11 +169,11 @@ def _dsplot(plotfunc): hue: str, optional Variable by which to color scattered points hue_style: str, optional - Hue style. Can be either 'discrete' or 'continuous'. + Can be either 'discrete' (legend) or 'continuous' (color bar). markersize: str, optional (scatter only) Variably by which to vary size of scattered points size_norm: optional - Either None or Norm instance to normalize the 'markersize' variable. + Either None or 'Norm' instance to normalize the 'markersize' variable. add_guide: bool, optional Add a guide that depends on hue_style - for "discrete", build a legend. From 7116020e24f405ad741e06476c054b73443755cf Mon Sep 17 00:00:00 2001 From: Yohai Bar Sinai <6164157+yohai@users.noreply.github.com> Date: Fri, 21 Jun 2019 00:02:26 -0400 Subject: [PATCH 090/101] fix legend bug in line plots --- xarray/plot/facetgrid.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/xarray/plot/facetgrid.py b/xarray/plot/facetgrid.py index dc1e8fbf120..af76c0b22c7 100644 --- a/xarray/plot/facetgrid.py +++ b/xarray/plot/facetgrid.py @@ -339,7 +339,7 @@ def _finalize_grid(self, *axlabels): def add_legend(self, **kwargs): figlegend = self.fig.legend( handles=self._mappables[-1], - labels=list(self._hue_var), + labels=list(self._hue_var.values), title=self._hue_label, loc="center right", **kwargs) From fa37607a0fda5176b2c37e3cdeaf7b785a5db716 Mon Sep 17 00:00:00 2001 From: Yohai Bar Sinai <6164157+yohai@users.noreply.github.com> Date: Fri, 21 Jun 2019 00:31:16 -0400 Subject: [PATCH 091/101] unittest for legend in lineplot --- xarray/plot/facetgrid.py | 1 + xarray/tests/test_plot.py | 13 +++++++++++++ 2 files changed, 14 insertions(+) diff --git a/xarray/plot/facetgrid.py b/xarray/plot/facetgrid.py index af76c0b22c7..f244e865263 100644 --- a/xarray/plot/facetgrid.py +++ b/xarray/plot/facetgrid.py @@ -343,6 +343,7 @@ def add_legend(self, **kwargs): title=self._hue_label, loc="center right", **kwargs) + self.figlegend = figlegend # Draw the plot to set the bounding boxes correctly self.fig.draw(self.fig.canvas.get_renderer()) diff --git a/xarray/tests/test_plot.py b/xarray/tests/test_plot.py index c864c00e475..8af401f1550 100644 --- a/xarray/tests/test_plot.py +++ b/xarray/tests/test_plot.py @@ -1749,6 +1749,19 @@ def test_default_labels(self): assert substring_in_axes(label, ax) +@pytest.mark.filterwarnings('ignore:tight_layout cannot') +class TestFacetedLinePlotsLegend(PlotTestCase): + @pytest.fixture(autouse=True) + def setUp(self): + self.darray = xr.tutorial.scatter_example_dataset() + + def test_legend_labels(self): + fg = self.darray.A.plot.line(col='x', row='w', hue='z') + all_legend_labels = [t.get_text() for t in fg.figlegend.texts] + # labels in legend should be ['0', '1', '2', '3'] + assert sorted(all_legend_labels) == ['0', '1', '2', '3'] + + @pytest.mark.filterwarnings('ignore:tight_layout cannot') class TestFacetedLinePlots(PlotTestCase): @pytest.fixture(autouse=True) From 2bc6107b6e81a20d9dd8e93054b8b4142eba229e Mon Sep 17 00:00:00 2001 From: Yohai Bar Sinai <6164157+yohai@users.noreply.github.com> Date: Fri, 21 Jun 2019 00:50:49 -0400 Subject: [PATCH 092/101] bug fix --- xarray/plot/facetgrid.py | 8 ++++++-- xarray/tutorial.py | 1 - 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/xarray/plot/facetgrid.py b/xarray/plot/facetgrid.py index f244e865263..5bceba14bec 100644 --- a/xarray/plot/facetgrid.py +++ b/xarray/plot/facetgrid.py @@ -8,7 +8,7 @@ from .utils import ( _infer_xy_labels, _process_cmap_cbar_kwargs, import_matplotlib_pyplot, label_from_attrs) - +import xarray as xr # Overrides axes.labelsize, xtick.major.size, ytick.major.size # from mpl.rcParams _FONTSIZE = 'small' @@ -337,9 +337,13 @@ def _finalize_grid(self, *axlabels): self._finalized = True def add_legend(self, **kwargs): + if isinstance(self._hue_var, xr.DataArray): + labels = self._hue_var.values + else: + labels = self._hue_var figlegend = self.fig.legend( handles=self._mappables[-1], - labels=list(self._hue_var.values), + labels=list(labels), title=self._hue_label, loc="center right", **kwargs) diff --git a/xarray/tutorial.py b/xarray/tutorial.py index 96f548a8766..608f5c5270d 100644 --- a/xarray/tutorial.py +++ b/xarray/tutorial.py @@ -126,4 +126,3 @@ def scatter_example_dataset(): ds.B.attrs['units'] = 'Bunits' return ds - From 436f7af3e43d84218edf194a181e37fa8823c244 Mon Sep 17 00:00:00 2001 From: Yohai Bar Sinai <6164157+yohai@users.noreply.github.com> Date: Wed, 26 Jun 2019 12:35:10 -0400 Subject: [PATCH 093/101] add figlegend to __init__ --- xarray/plot/facetgrid.py | 1 + 1 file changed, 1 insertion(+) diff --git a/xarray/plot/facetgrid.py b/xarray/plot/facetgrid.py index 5bceba14bec..cdff78650de 100644 --- a/xarray/plot/facetgrid.py +++ b/xarray/plot/facetgrid.py @@ -174,6 +174,7 @@ def __init__(self, data, col=None, row=None, col_wrap=None, self.axes = axes self.row_names = row_names self.col_names = col_names + self.figlegend = None # Next the private variables self._single_group = single_group From 21353888a38e777bf59c397a7dcaa5a20c132668 Mon Sep 17 00:00:00 2001 From: Yohai Bar Sinai <6164157+yohai@users.noreply.github.com> Date: Fri, 28 Jun 2019 09:52:02 -0400 Subject: [PATCH 094/101] remove import from facetgrid.py --- xarray/plot/facetgrid.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/xarray/plot/facetgrid.py b/xarray/plot/facetgrid.py index cdff78650de..061fb014d36 100644 --- a/xarray/plot/facetgrid.py +++ b/xarray/plot/facetgrid.py @@ -8,7 +8,6 @@ from .utils import ( _infer_xy_labels, _process_cmap_cbar_kwargs, import_matplotlib_pyplot, label_from_attrs) -import xarray as xr # Overrides axes.labelsize, xtick.major.size, ytick.major.size # from mpl.rcParams _FONTSIZE = 'small' @@ -338,7 +337,7 @@ def _finalize_grid(self, *axlabels): self._finalized = True def add_legend(self, **kwargs): - if isinstance(self._hue_var, xr.DataArray): + if hasattr(self._hue_var, 'values'): labels = self._hue_var.values else: labels = self._hue_var From 3db1610a9937166c8bd00d0e56a79cba2fec3719 Mon Sep 17 00:00:00 2001 From: dcherian Date: Sat, 3 Aug 2019 16:34:13 -0600 Subject: [PATCH 095/101] Remove xr.plot.scatter. --- doc/api.rst | 1 - xarray/plot/__init__.py | 2 -- 2 files changed, 3 deletions(-) diff --git a/doc/api.rst b/doc/api.rst index 69065874097..872e7786e1b 100644 --- a/doc/api.rst +++ b/doc/api.rst @@ -610,7 +610,6 @@ Plotting plot.hist plot.imshow plot.line - plot.scatter plot.pcolormesh plot.FacetGrid diff --git a/xarray/plot/__init__.py b/xarray/plot/__init__.py index c9e74158a7d..adda541c21d 100644 --- a/xarray/plot/__init__.py +++ b/xarray/plot/__init__.py @@ -1,6 +1,5 @@ from .facetgrid import FacetGrid from .plot import contour, contourf, hist, imshow, line, pcolormesh, plot, step -from .dataset_plot import scatter __all__ = [ 'plot', @@ -12,5 +11,4 @@ 'imshow', 'pcolormesh', 'FacetGrid', - 'scatter', ] From fc7fc96741462abe43ab32da0e24cca1b1848cc7 Mon Sep 17 00:00:00 2001 From: dcherian Date: Sat, 3 Aug 2019 16:56:14 -0600 Subject: [PATCH 096/101] facetgrid._hue_var is always a DataArray. --- xarray/plot/dataset_plot.py | 8 ++++---- xarray/plot/facetgrid.py | 8 ++------ 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/xarray/plot/dataset_plot.py b/xarray/plot/dataset_plot.py index 3a9b975e49b..1a57657f0cf 100644 --- a/xarray/plot/dataset_plot.py +++ b/xarray/plot/dataset_plot.py @@ -53,10 +53,10 @@ def _infer_meta_data(ds, x, y, hue, hue_style, add_guide): if hue: hue_label = label_from_attrs(ds[hue]) - hue_values = ds[hue].values + hue = ds[hue] else: hue_label = None - hue_values = None + hue = None return {'add_colorbar': add_colorbar, 'add_legend': add_legend, @@ -64,7 +64,7 @@ def _infer_meta_data(ds, x, y, hue, hue_style, add_guide): 'hue_style': hue_style, 'xlabel': label_from_attrs(ds[x]), 'ylabel': label_from_attrs(ds[y]), - 'hue_values': hue_values} + 'hue': hue} def _infer_scatter_data(ds, x, y, hue, markersize, size_norm, @@ -300,7 +300,7 @@ def newplotfunc(ds, x=None, y=None, hue=None, hue_style=None, if meta_data['add_legend']: ax.legend(handles=primitive, - labels=list(meta_data['hue_values']), + labels=list(meta_data['hue'].values), title=meta_data.get('hue_label', None)) if meta_data['add_colorbar']: cbar_kwargs = {} if cbar_kwargs is None else cbar_kwargs diff --git a/xarray/plot/facetgrid.py b/xarray/plot/facetgrid.py index fff177b5df3..7ae05c25548 100644 --- a/xarray/plot/facetgrid.py +++ b/xarray/plot/facetgrid.py @@ -316,7 +316,7 @@ def map_dataset(self, func, x=None, y=None, hue=None, hue_style=None, if hue: self._hue_label = meta_data.pop('hue_label', None) if meta_data['add_legend']: - self._hue_var = meta_data['hue_values'] + self._hue_var = meta_data['hue'] self.add_legend() elif meta_data['add_colorbar']: self.add_colorbar(label=self._hue_label, **cbar_kwargs) @@ -337,13 +337,9 @@ def _finalize_grid(self, *axlabels): self._finalized = True def add_legend(self, **kwargs): - if hasattr(self._hue_var, 'values'): - labels = self._hue_var.values - else: - labels = self._hue_var figlegend = self.fig.legend( handles=self._mappables[-1], - labels=list(labels), + labels=list(self._hue_var.values), title=self._hue_label, loc="center right", **kwargs) From 2825c354969f65e7af720d2f5ca5b6ad2efe0304 Mon Sep 17 00:00:00 2001 From: dcherian Date: Sat, 3 Aug 2019 17:17:26 -0600 Subject: [PATCH 097/101] scatter_size bugfix. --- xarray/plot/facetgrid.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/xarray/plot/facetgrid.py b/xarray/plot/facetgrid.py index 7ae05c25548..9038e091903 100644 --- a/xarray/plot/facetgrid.py +++ b/xarray/plot/facetgrid.py @@ -285,9 +285,9 @@ def map_dataset(self, func, x=None, y=None, hue=None, hue_style=None, kwargs['add_guide'] = False kwargs['_is_facetgrid'] = True - if kwargs.get('scatter_size', None): + if kwargs.get('markersize', None): kwargs['size_mapping'] = _parse_size( - self.data[kwargs['scatter_size']], + self.data[kwargs['markersize']], kwargs.pop('size_norm', None)) meta_data = _infer_meta_data(self.data, x, y, hue, hue_style, From d4844bcb3b3d904188b53fbeb1e938b4317094eb Mon Sep 17 00:00:00 2001 From: dcherian Date: Sat, 3 Aug 2019 17:26:00 -0600 Subject: [PATCH 098/101] Update for latest _process_cmap_params_cbar_kwargs --- xarray/plot/dataset_plot.py | 2 +- xarray/plot/facetgrid.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/xarray/plot/dataset_plot.py b/xarray/plot/dataset_plot.py index 1a57657f0cf..853dbf9b0a7 100644 --- a/xarray/plot/dataset_plot.py +++ b/xarray/plot/dataset_plot.py @@ -277,7 +277,7 @@ def newplotfunc(ds, x=None, y=None, hue=None, hue_style=None, cmap_params = meta_data['cmap_params'] else: cmap_params, cbar_kwargs = _process_cmap_cbar_kwargs( - plotfunc, locals(), ds[hue].values) + plotfunc, ds[hue].values, **locals()) # subset that can be passed to scatter, hist2d cmap_params_subset = dict( diff --git a/xarray/plot/facetgrid.py b/xarray/plot/facetgrid.py index 9038e091903..a28be7ce187 100644 --- a/xarray/plot/facetgrid.py +++ b/xarray/plot/facetgrid.py @@ -296,7 +296,7 @@ def map_dataset(self, func, x=None, y=None, hue=None, hue_style=None, if hue and meta_data['hue_style'] == 'continuous': cmap_params, cbar_kwargs = _process_cmap_cbar_kwargs( - func, kwargs, self.data[hue].values) + func, self.data[hue].values, **kwargs) kwargs['meta_data']['cmap_params'] = cmap_params kwargs['meta_data']['cbar_kwargs'] = cbar_kwargs From dbe09b7281f60193bcd1f928ec3c0e6b20a46100 Mon Sep 17 00:00:00 2001 From: dcherian Date: Sat, 3 Aug 2019 17:31:19 -0600 Subject: [PATCH 099/101] Fix whats-new --- doc/whats-new.rst | 5 ----- 1 file changed, 5 deletions(-) diff --git a/doc/whats-new.rst b/doc/whats-new.rst index 90a393624e3..8a33587a257 100644 --- a/doc/whats-new.rst +++ b/doc/whats-new.rst @@ -361,11 +361,6 @@ Other enhancements report showing what exactly differs between the two objects (dimensions / coordinates / variables / attributes) (:issue:`1507`). By `Benoit Bovy `_. - -- Resampling of standard and non-standard calendars indexed by - :py:class:`~xarray.CFTimeIndex` is now possible. (:issue:`2191`). - By `Jwen Fai Low `_ and - `Spencer Clark `_. - Add ``tolerance`` option to ``resample()`` methods ``bfill``, ``pad``, ``nearest``. (:issue:`2695`) By `Hauke Schulz `_. From a805c59b565ef37bf9f94a17ad838144dd1bd242 Mon Sep 17 00:00:00 2001 From: dcherian Date: Sun, 4 Aug 2019 19:56:57 -0600 Subject: [PATCH 100/101] Fix tests. --- xarray/tests/test_plot.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/xarray/tests/test_plot.py b/xarray/tests/test_plot.py index 6605163b6d9..34852693b11 100644 --- a/xarray/tests/test_plot.py +++ b/xarray/tests/test_plot.py @@ -1879,8 +1879,7 @@ def test_bad_args(self, x, y, hue_style, add_guide): self.ds.plot.scatter(x, y, hue_style=hue_style, add_guide=add_guide) - @pytest.mark.xfail(reason=['datetime, timedelta64 hue variable' - ' not supported yet.']) + @pytest.mark.xfail(reason='datetime,timedelta hue variable not supported.') @pytest.mark.parametrize('hue_style', ['discrete', 'continuous']) def test_datetime_hue(self, hue_style): ds2 = self.ds.copy() From d56f7d13c9b82afbbe63734448e3594bfd06c940 Mon Sep 17 00:00:00 2001 From: dcherian Date: Wed, 7 Aug 2019 12:38:38 -0600 Subject: [PATCH 101/101] Make add_guide=False work. --- xarray/plot/dataset_plot.py | 6 ++++-- xarray/tests/test_plot.py | 18 ++++++++++++++++++ 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/xarray/plot/dataset_plot.py b/xarray/plot/dataset_plot.py index 853dbf9b0a7..aa31780a983 100644 --- a/xarray/plot/dataset_plot.py +++ b/xarray/plot/dataset_plot.py @@ -37,10 +37,12 @@ def _infer_meta_data(ds, x, y, hue, hue_style, add_guide): raise ValueError('Cannot create a colorbar for a non numeric' ' coordinate: ' + hue) - if add_guide is None: + if add_guide is None or add_guide is True: add_colorbar = True if hue_style == 'continuous' else False add_legend = True if hue_style == 'discrete' else False - + else: + add_colorbar = False + add_legend = False else: if add_guide is True: raise ValueError('Cannot set add_guide when hue is None.') diff --git a/xarray/tests/test_plot.py b/xarray/tests/test_plot.py index 1d3452be71d..172b6025b74 100644 --- a/xarray/tests/test_plot.py +++ b/xarray/tests/test_plot.py @@ -9,6 +9,7 @@ import xarray.plot as xplt from xarray import DataArray, Dataset from xarray.coding.times import _import_cftime +from xarray.plot.dataset_plot import _infer_meta_data from xarray.plot.plot import _infer_interval_breaks from xarray.plot.utils import ( _build_discrete_cmap, _color_palette, _determine_cmap_params, @@ -1838,6 +1839,23 @@ def setUp(self): ds.B.attrs['units'] = 'Bunits' self.ds = ds + @pytest.mark.parametrize( + 'add_guide, hue_style, legend, colorbar', [ + (None, None, False, True), + (False, None, False, False), + (True, None, False, True), + (True, "continuous", False, True), + (False, "discrete", False, False), + (True, "discrete", True, False)] + ) + def test_add_guide(self, add_guide, hue_style, legend, colorbar): + + meta_data = _infer_meta_data(self.ds, x='A', y='B', hue='hue', + hue_style=hue_style, + add_guide=add_guide) + assert meta_data['add_legend'] is legend + assert meta_data['add_colorbar'] is colorbar + def test_facetgrid_shape(self): g = self.ds.plot.scatter(x='A', y='B', row='row', col='col') assert g.axes.shape == (len(self.ds.row), len(self.ds.col))