From 486198f44af40226872f5af48c58390a9bdb8ae9 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Tue, 25 Aug 2020 16:04:51 +0200 Subject: [PATCH 01/98] Implement cnorm option --- holoviews/plotting/bokeh/element.py | 19 ++++++-- holoviews/plotting/mpl/element.py | 11 +++-- holoviews/plotting/mpl/util.py | 69 ++++++++++++++++++++++++++++- 3 files changed, 91 insertions(+), 8 deletions(-) diff --git a/holoviews/plotting/bokeh/element.py b/holoviews/plotting/bokeh/element.py index 3027418463..b35248ab7b 100644 --- a/holoviews/plotting/bokeh/element.py +++ b/holoviews/plotting/bokeh/element.py @@ -1692,13 +1692,17 @@ class ColorbarPlot(ElementPlot): Number of discrete colors to use when colormapping or a set of color intervals defining the range of values to map each color to.""") + cformatter = param.ClassSelector( + default=None, class_=(util.basestring, TickFormatter, FunctionType), doc=""" + Formatter for ticks along the colorbar axis.""") + clabel = param.String(default=None, doc=""" An explicit override of the color bar label, if set takes precedence over the title key in colorbar_opts.""") clim = param.Tuple(default=(np.nan, np.nan), length=2, doc=""" - User-specified colorbar axis range limits for the plot, as a tuple (low,high). - If specified, takes precedence over data and dimension ranges.""") + User-specified colorbar axis range limits for the plot, as a tuple (low,high). + If specified, takes precedence over data and dimension ranges.""") clim_percentile = param.ClassSelector(default=False, class_=(int, float, bool), doc=""" Percentile value to compute colorscale robust to outliers. If @@ -1709,6 +1713,9 @@ class ColorbarPlot(ElementPlot): default=None, class_=(util.basestring, TickFormatter, FunctionType), doc=""" Formatter for ticks along the colorbar axis.""") + cnorm = param.ObjectSelector(default='linear', objects=['linear', 'log', 'eqhist'], doc=""" + Color normalization applied during colormapping.""") + colorbar = param.Boolean(default=False, doc=""" Whether to display a colorbar.""") @@ -1931,8 +1938,9 @@ def _get_color_data(self, element, ranges, style, name='color', factors=None, co def _get_cmapper_opts(self, low, high, factors, colors): if factors is None: - colormapper = LinearColorMapper - if self.logz: + if self.cnorm == 'linear': + colormapper = LinearColorMapper + if self.cnorm == 'log' or self.logz: colormapper = LogColorMapper if util.is_int(low) and util.is_int(high) and low == 0: low = 1 @@ -1946,6 +1954,9 @@ def _get_cmapper_opts(self, low, high, factors, colors): "lower bound on the color dimension or using " "the `clim` option." ) + elif self.cnorm == 'eqhist': + from bokeh.models import EqHistColorMapper + colormapper = EqHistColorMapper if isinstance(low, (bool, np.bool_)): low = int(low) if isinstance(high, (bool, np.bool_)): high = int(high) # Pad zero-range to avoid breaking colorbar (as of bokeh 1.0.4) diff --git a/holoviews/plotting/mpl/element.py b/holoviews/plotting/mpl/element.py index 2696093710..9d92984d97 100644 --- a/holoviews/plotting/mpl/element.py +++ b/holoviews/plotting/mpl/element.py @@ -22,7 +22,7 @@ from ..plot import GenericElementPlot, GenericOverlayPlot from ..util import process_cmap, color_intervals, dim_range_key from .plot import MPLPlot, mpl_rc_context -from .util import mpl_version, validate, wrap_formatter +from .util import EqHistNormalize, mpl_version, validate, wrap_formatter class ElementPlot(GenericElementPlot, MPLPlot): @@ -689,6 +689,9 @@ class ColorbarPlot(ElementPlot): Number of discrete colors to use when colormapping or a set of color intervals defining the range of values to map each color to.""") + cnorm = param.ObjectSelector(default='linear', objects=['linear', 'log', 'eqhist'], doc=""" + Color normalization applied during colormapping.""") + clipping_colors = param.Dict(default={}, doc=""" Dictionary to specify colors for clipped values, allows setting color for NaN values and for values above and below @@ -894,7 +897,10 @@ def _norm_kwargs(self, element, ranges, opts, vdim, values=None, prefix=''): else: categorical = values.dtype.kind not in 'uif' - if self.logz: + if self.cnorm == 'eqhist': + opts[prefix+'norm'] = EqHistNormalize( + vmin=clim[0], vmax=clim[1]) + if self.cnorm == 'log' or self.logz: if self.symmetric: norm = mpl_colors.SymLogNorm(vmin=clim[0], vmax=clim[1], linthresh=clim[1]/np.e) @@ -973,7 +979,6 @@ def _norm_kwargs(self, element, ranges, opts, vdim, values=None, prefix=''): opts[prefix+'cmap'] = cmap - class LegendPlot(ElementPlot): show_legend = param.Boolean(default=True, doc=""" diff --git a/holoviews/plotting/mpl/util.py b/holoviews/plotting/mpl/util.py index 5441dacb3c..0e6ee34851 100644 --- a/holoviews/plotting/mpl/util.py +++ b/holoviews/plotting/mpl/util.py @@ -7,7 +7,7 @@ import matplotlib from matplotlib import units as munits from matplotlib import ticker -from matplotlib.colors import cnames +from matplotlib.colors import Normalize, cnames from matplotlib.lines import Line2D from matplotlib.markers import MarkerStyle from matplotlib.patches import Path, PathPatch @@ -396,5 +396,72 @@ def convert(cls, value, unit, axis): return super(CFTimeConverter, cls).convert(value, unit, axis) +class EqHistNormalize(Normalize): + + def __init__(self, vmin=None, vmax=None, clip=False, nbins=256**2, ncolors=256): + super(EqHistNormalize, self).__init__(vmin, vmax, clip) + self._nbins = nbins + self._bin_edges = None + self._ncolors = ncolors + self._color_bins = np.linspace(0, 1, ncolors) + + def binning(self, data, n=256): + low = data.min() if self.vmin is None else self.vmin + high = data.max() if self.vmax is None else self.vmax + nbins = self._nbins + eq_bin_edges = np.linspace(low, high, nbins+1) + hist, _ = np.histogram(data, eq_bin_edges) + + eq_bin_centers = np.convolve(eq_bin_edges, [0.5, 0.5], mode='valid') + cdf = np.cumsum(hist) + cdf_max = cdf[-1] + norm_cdf = cdf/cdf_max + + # Iteratively find as many finite bins as there are colors + finite_bins = n-1 + binning = [] + iterations = 0 + guess = n*2 + while ((finite_bins != n) and (iterations < 4) and (finite_bins != 0)): + ratio = guess/finite_bins + if (ratio > 1000): + #Abort if distribution is extremely skewed + break + guess = np.round(max(n*ratio, n)) + + # Interpolate + palette_edges = np.arange(0, guess) + palette_cdf = norm_cdf*(guess-1) + binning = np.interp(palette_edges, palette_cdf, eq_bin_centers) + + # Evaluate binning + uniq_bins = np.unique(binning) + finite_bins = len(uniq_bins)-1 + iterations += 1 + if (finite_bins == 0): + binning = [low]+[high]*(n-1) + else: + binning = binning[-n:] + if (finite_bins != n): + warnings.warn("EqHistColorMapper warning: Histogram equalization did not converge.") + return binning + + def __call__(self, data, clip=None): + return self.process_value(data)[0] + + def process_value(self, data): + if isinstance(data, np.ndarray): + self._bin_edges = self.binning(data, self._ncolors) + isscalar = np.isscalar(data) + data = np.array([data]) if isscalar else data + interped = np.interp(data, self._bin_edges, self._color_bins) + return np.ma.array(interped), isscalar + + def inverse(self, value): + if self._bin_edges is None: + raise ValueError("Not invertible until eqhist has been computed") + return np.interp([value], self._color_bins, self._bin_edges)[0] + + for cft in cftime_types: munits.registry[cft] = CFTimeConverter() From ef999c9d341607d28466666dedc182465ad6716f Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Tue, 25 Aug 2020 22:34:51 +0200 Subject: [PATCH 02/98] Add minimum to count aggregations --- holoviews/operation/datashader.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/holoviews/operation/datashader.py b/holoviews/operation/datashader.py index fad789fc39..2abdc41fc7 100644 --- a/holoviews/operation/datashader.py +++ b/holoviews/operation/datashader.py @@ -306,9 +306,9 @@ def _get_agg_params(self, element, x, y, agg_fn, bounds): name = '%s Count' % column if isinstance(agg_fn, ds.count_cat) else column vdims = [dims[0].clone(name)] elif category: - vdims = Dimension('%s Count' % category) + vdims = Dimension('%s Count' % category, range=(1, None)) else: - vdims = Dimension('Count') + vdims = Dimension('Count', range=(1, None)) params['vdims'] = vdims return params From 40f8899cf52deaca4675f2469dda16907de92f8e Mon Sep 17 00:00:00 2001 From: jlstevens Date: Fri, 11 Sep 2020 14:34:04 +0200 Subject: [PATCH 03/98] Applied transparent clipping colors to Image min and max --- holoviews/plotting/bokeh/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/holoviews/plotting/bokeh/__init__.py b/holoviews/plotting/bokeh/__init__.py index 3e6cbe840f..532fc326b5 100644 --- a/holoviews/plotting/bokeh/__init__.py +++ b/holoviews/plotting/bokeh/__init__.py @@ -194,6 +194,8 @@ def colormap_generator(palette): # Rasters options.Image = Options('style', cmap=dflt_cmap) +options.Image = Options('plot', clipping_colors={'min': 'transparent', + 'max': 'transparent'}) options.Raster = Options('style', cmap=dflt_cmap) options.QuadMesh = Options('style', cmap=dflt_cmap, line_alpha=0) options.HeatMap = Options('style', cmap='RdYlBu_r', annular_line_alpha=0, From 61ffe6aadd7f741940b8e527381fb9417b9ed652 Mon Sep 17 00:00:00 2001 From: jlstevens Date: Fri, 11 Sep 2020 15:13:43 +0200 Subject: [PATCH 04/98] Updated test_aggregate_curve unit test --- holoviews/tests/operation/testdatashader.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/holoviews/tests/operation/testdatashader.py b/holoviews/tests/operation/testdatashader.py index ab32a1c1ea..93dd20595e 100644 --- a/holoviews/tests/operation/testdatashader.py +++ b/holoviews/tests/operation/testdatashader.py @@ -109,9 +109,9 @@ def test_aggregate_points_categorical_zero_range(self): self.assertEqual(img, expected) def test_aggregate_curve(self): - curve = Curve([(0.2, 0.3), (0.4, 0.7), (0.8, 0.99)]) + curve = Curve([(0.2, 0.3), (0.4, 0.7), (0.8, 0.99)]). expected = Image(([0.25, 0.75], [0.25, 0.75], [[1, 0], [1, 1]]), - vdims=['Count']) + vdims=['Count']).redim.range(z=(1,None)) img = aggregate(curve, dynamic=False, x_range=(0, 1), y_range=(0, 1), width=2, height=2) self.assertEqual(img, expected) From 3d3c771c84d15d1c7c6219b20b80bfa49233efe9 Mon Sep 17 00:00:00 2001 From: jlstevens Date: Fri, 11 Sep 2020 15:26:10 +0200 Subject: [PATCH 05/98] Fixed typo --- holoviews/tests/operation/testdatashader.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/holoviews/tests/operation/testdatashader.py b/holoviews/tests/operation/testdatashader.py index 93dd20595e..eeae2303b5 100644 --- a/holoviews/tests/operation/testdatashader.py +++ b/holoviews/tests/operation/testdatashader.py @@ -109,7 +109,7 @@ def test_aggregate_points_categorical_zero_range(self): self.assertEqual(img, expected) def test_aggregate_curve(self): - curve = Curve([(0.2, 0.3), (0.4, 0.7), (0.8, 0.99)]). + curve = Curve([(0.2, 0.3), (0.4, 0.7), (0.8, 0.99)]) expected = Image(([0.25, 0.75], [0.25, 0.75], [[1, 0], [1, 1]]), vdims=['Count']).redim.range(z=(1,None)) img = aggregate(curve, dynamic=False, x_range=(0, 1), y_range=(0, 1), From 374ffcf403f41b74e2f9e4e4849b60a1e3baf034 Mon Sep 17 00:00:00 2001 From: jlstevens Date: Fri, 11 Sep 2020 17:07:19 +0200 Subject: [PATCH 06/98] Updated more unit tests --- holoviews/tests/operation/testdatashader.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/holoviews/tests/operation/testdatashader.py b/holoviews/tests/operation/testdatashader.py index eeae2303b5..9f2dd5eaeb 100644 --- a/holoviews/tests/operation/testdatashader.py +++ b/holoviews/tests/operation/testdatashader.py @@ -125,7 +125,8 @@ def test_aggregate_curve_datetimes(self): dates = [np.datetime64('2016-01-01T12:00:00.000000000'), np.datetime64('2016-01-02T12:00:00.000000000')] expected = Image((dates, [1.5, 2.5], [[1, 0], [0, 2]]), - datatype=['xarray'], bounds=bounds, vdims='Count') + datatype=['xarray'], bounds=bounds, + vdims='Count').redim.range(z=(1,None)) self.assertEqual(img, expected) def test_aggregate_curve_datetimes_dask(self): @@ -141,7 +142,8 @@ def test_aggregate_curve_datetimes_dask(self): dates = [np.datetime64('2019-01-01T04:09:45.000000000'), np.datetime64('2019-01-01T12:29:15.000000000')] expected = Image((dates, [166.5, 499.5, 832.5], [[332, 0], [167, 166], [0, 334]]), - ['index', 'a'], 'Count', datatype=['xarray'], bounds=bounds) + ['index', 'a'], 'Count', datatype=['xarray'], + bounds=bounds).redim.range(z=(1,None)) self.assertEqual(img, expected) def test_aggregate_curve_datetimes_microsecond_timebase(self): @@ -155,7 +157,8 @@ def test_aggregate_curve_datetimes_microsecond_timebase(self): dates = [np.datetime64('2016-01-01T11:59:59.861759000',), np.datetime64('2016-01-02T12:00:00.138241000')] expected = Image((dates, [1.5, 2.5], [[1, 0], [0, 2]]), - datatype=['xarray'], bounds=bounds, vdims='Count') + datatype=['xarray'], bounds=bounds, + vdims='Count').redim.range(z=(1,None)) self.assertEqual(img, expected) def test_aggregate_ndoverlay_count_cat_datetimes_microsecond_timebase(self): From e829935f3aad0726a5c4b0e92e46f59380ff2027 Mon Sep 17 00:00:00 2001 From: jlstevens Date: Fri, 11 Sep 2020 17:27:10 +0200 Subject: [PATCH 07/98] Updated redimming range on appropriate vdim --- holoviews/tests/operation/testdatashader.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/holoviews/tests/operation/testdatashader.py b/holoviews/tests/operation/testdatashader.py index 9f2dd5eaeb..b26be8f187 100644 --- a/holoviews/tests/operation/testdatashader.py +++ b/holoviews/tests/operation/testdatashader.py @@ -111,7 +111,7 @@ def test_aggregate_points_categorical_zero_range(self): def test_aggregate_curve(self): curve = Curve([(0.2, 0.3), (0.4, 0.7), (0.8, 0.99)]) expected = Image(([0.25, 0.75], [0.25, 0.75], [[1, 0], [1, 1]]), - vdims=['Count']).redim.range(z=(1,None)) + vdims=['Count']).redim.range(Count==(1,None)) img = aggregate(curve, dynamic=False, x_range=(0, 1), y_range=(0, 1), width=2, height=2) self.assertEqual(img, expected) @@ -126,7 +126,7 @@ def test_aggregate_curve_datetimes(self): np.datetime64('2016-01-02T12:00:00.000000000')] expected = Image((dates, [1.5, 2.5], [[1, 0], [0, 2]]), datatype=['xarray'], bounds=bounds, - vdims='Count').redim.range(z=(1,None)) + vdims='Count').redim.range(Count==(1,None)) self.assertEqual(img, expected) def test_aggregate_curve_datetimes_dask(self): @@ -143,7 +143,7 @@ def test_aggregate_curve_datetimes_dask(self): np.datetime64('2019-01-01T12:29:15.000000000')] expected = Image((dates, [166.5, 499.5, 832.5], [[332, 0], [167, 166], [0, 334]]), ['index', 'a'], 'Count', datatype=['xarray'], - bounds=bounds).redim.range(z=(1,None)) + bounds=bounds).redim.range(Count==(1,None)) self.assertEqual(img, expected) def test_aggregate_curve_datetimes_microsecond_timebase(self): @@ -158,7 +158,7 @@ def test_aggregate_curve_datetimes_microsecond_timebase(self): np.datetime64('2016-01-02T12:00:00.138241000')] expected = Image((dates, [1.5, 2.5], [[1, 0], [0, 2]]), datatype=['xarray'], bounds=bounds, - vdims='Count').redim.range(z=(1,None)) + vdims='Count').redim.range(Count=(1,None)) self.assertEqual(img, expected) def test_aggregate_ndoverlay_count_cat_datetimes_microsecond_timebase(self): From baa236263b5711e46eb4346d52be7725f5bc35c7 Mon Sep 17 00:00:00 2001 From: jlstevens Date: Fri, 11 Sep 2020 17:36:11 +0200 Subject: [PATCH 08/98] Fixed typos --- holoviews/tests/operation/testdatashader.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/holoviews/tests/operation/testdatashader.py b/holoviews/tests/operation/testdatashader.py index b26be8f187..796d2d0752 100644 --- a/holoviews/tests/operation/testdatashader.py +++ b/holoviews/tests/operation/testdatashader.py @@ -111,7 +111,7 @@ def test_aggregate_points_categorical_zero_range(self): def test_aggregate_curve(self): curve = Curve([(0.2, 0.3), (0.4, 0.7), (0.8, 0.99)]) expected = Image(([0.25, 0.75], [0.25, 0.75], [[1, 0], [1, 1]]), - vdims=['Count']).redim.range(Count==(1,None)) + vdims=['Count']).redim.range(Count=(1,None)) img = aggregate(curve, dynamic=False, x_range=(0, 1), y_range=(0, 1), width=2, height=2) self.assertEqual(img, expected) @@ -126,7 +126,7 @@ def test_aggregate_curve_datetimes(self): np.datetime64('2016-01-02T12:00:00.000000000')] expected = Image((dates, [1.5, 2.5], [[1, 0], [0, 2]]), datatype=['xarray'], bounds=bounds, - vdims='Count').redim.range(Count==(1,None)) + vdims='Count').redim.range(Count=(1,None)) self.assertEqual(img, expected) def test_aggregate_curve_datetimes_dask(self): @@ -143,7 +143,7 @@ def test_aggregate_curve_datetimes_dask(self): np.datetime64('2019-01-01T12:29:15.000000000')] expected = Image((dates, [166.5, 499.5, 832.5], [[332, 0], [167, 166], [0, 334]]), ['index', 'a'], 'Count', datatype=['xarray'], - bounds=bounds).redim.range(Count==(1,None)) + bounds=bounds).redim.range(Count=(1,None)) self.assertEqual(img, expected) def test_aggregate_curve_datetimes_microsecond_timebase(self): From bd534fe8423daf2533f93ea30b65b5d1e9317782 Mon Sep 17 00:00:00 2001 From: jlstevens Date: Mon, 28 Sep 2020 11:40:00 +0200 Subject: [PATCH 09/98] First cut at new nodata option for Rasters and QuadMeshes --- holoviews/plotting/bokeh/raster.py | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/holoviews/plotting/bokeh/raster.py b/holoviews/plotting/bokeh/raster.py index 9eb25fe830..6468d8cbf9 100644 --- a/holoviews/plotting/bokeh/raster.py +++ b/holoviews/plotting/bokeh/raster.py @@ -15,6 +15,9 @@ class RasterPlot(ColorbarPlot): + nodata = param.Integer(default=None, + doc="Missing data (NaN) value for integer data", allow_None=True) + clipping_colors = param.Dict(default={'NaN': 'transparent'}) padding = param.ClassSelector(default=0, class_=(int, float, tuple)) @@ -101,6 +104,7 @@ def get_data(self, element, ranges, style): b, t = t, b data = dict(x=[l], y=[b], dw=[dw], dh=[dh]) + plot_opts = element.opts.get('plot', 'bokeh') for i, vdim in enumerate(element.vdims, 2): if i > 2 and 'hover' not in self.handles: break @@ -117,6 +121,11 @@ def get_data(self, element, ranges, style): if self.invert_yaxis: img = img[::-1] key = 'image' if i == 2 else dimension_sanitizer(vdim.name) + nodata = plot_opts.kwargs.get('nodata') + if nodata is not None: + img = img if (img.dtype.kind == 'f') else img.astype(np.float64) + img[img == nodata] = np.NaN + data[key] = [img] return (data, mapping, style) @@ -175,7 +184,7 @@ def get_data(self, element, ranges, style): if self.invert_axes: img = img.T l, b, r, t = b, l, t, r - + dh, dw = t-b, r-l if self.invert_xaxis: l, r = r, l @@ -201,6 +210,9 @@ def get_data(self, element, ranges, style): class QuadMeshPlot(ColorbarPlot): + nodata = param.Integer(default=None, + doc="Missing data (NaN) value for integer data", allow_None=True) + clipping_colors = param.Dict(default={'NaN': 'transparent'}) padding = param.ClassSelector(default=0, class_=(int, float, tuple)) @@ -238,6 +250,12 @@ def get_data(self, element, ranges, style): x, y = dimension_sanitizer(x.name), dimension_sanitizer(y.name) zdata = element.dimension_values(z, flat=False) + plot_opts = element.opts.get('plot', 'bokeh') + nodata = plot_opts.kwargs.get('nodata') + if nodata is not None: + zdata = zdata if (zdata.dtype.kind == 'f') else zdata.astype(np.float64) + zdata[zdata == nodata] = np.NaN + if irregular: dims = element.kdims if self.invert_axes: dims = dims[::-1] From 52d6edfe11e7c1b5eb355c0a3cf0cb752b3c4033 Mon Sep 17 00:00:00 2001 From: jlstevens Date: Mon, 28 Sep 2020 14:30:53 +0200 Subject: [PATCH 10/98] Added nodata support for Matplotlib rasters and quadmeshes --- holoviews/plotting/mpl/raster.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/holoviews/plotting/mpl/raster.py b/holoviews/plotting/mpl/raster.py index a2e2ea7777..aff3d0dd03 100644 --- a/holoviews/plotting/mpl/raster.py +++ b/holoviews/plotting/mpl/raster.py @@ -14,6 +14,9 @@ class RasterBasePlot(ElementPlot): + nodata = param.Integer(default=None, + doc="Missing data (NaN) value for integer data", allow_None=True) + aspect = param.Parameter(default='equal', doc=""" Raster elements respect the aspect ratio of the Images by default but may be set to an explicit @@ -82,6 +85,12 @@ def get_data(self, element, ranges, style): style['extent'] = [l, r, b, t] style['origin'] = 'upper' + plot_opts = element.opts.get('plot', 'matplotlib') + nodata = plot_opts.kwargs.get('nodata') + if nodata is not None: + data = data if (data.dtype.kind == 'f') else data.astype(np.float64) + data[data == nodata] = np.NaN + return [data], style, {'xticks': xticks, 'yticks': yticks} def update_handles(self, key, axis, element, ranges, style): @@ -126,6 +135,9 @@ def update_handles(self, key, axis, element, ranges, style): class QuadMeshPlot(ColorbarPlot): + nodata = param.Integer(default=None, + doc="Missing data (NaN) value for integer data", allow_None=True) + clipping_colors = param.Dict(default={'NaN': 'transparent'}) padding = param.ClassSelector(default=0, class_=(int, float, tuple)) @@ -141,6 +153,13 @@ class QuadMeshPlot(ColorbarPlot): def get_data(self, element, ranges, style): zdata = element.dimension_values(2, flat=False) data = np.ma.array(zdata, mask=np.logical_not(np.isfinite(zdata))) + + plot_opts = element.opts.get('plot', 'matplotlib') + nodata = plot_opts.kwargs.get('nodata') + if nodata is not None: + data = data if (data.dtype.kind == 'f') else data.astype(np.float64) + data[data == nodata] = np.NaN + expanded = element.interface.irregular(element, element.kdims[0]) edges = style.get('shading') != 'gouraud' coords = [element.interface.coords(element, d, ordered=True, From d327a2bd3e280620c855bbe5d08966bd12ce67b8 Mon Sep 17 00:00:00 2001 From: jlstevens Date: Mon, 28 Sep 2020 14:44:40 +0200 Subject: [PATCH 11/98] Added nodata support for Plotly rasters and quadmeshes --- holoviews/plotting/plotly/raster.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/holoviews/plotting/plotly/raster.py b/holoviews/plotting/plotly/raster.py index dd361c2138..6550675562 100644 --- a/holoviews/plotting/plotly/raster.py +++ b/holoviews/plotting/plotly/raster.py @@ -13,6 +13,9 @@ class RasterPlot(ColorbarPlot): padding = param.ClassSelector(default=0, class_=(int, float, tuple)) + nodata = param.Integer(default=None, + doc="Missing data (NaN) value for integer data", allow_None=True) + style_opts = ['visible', 'cmap', 'alpha'] @classmethod @@ -41,6 +44,13 @@ def get_data(self, element, ranges, style, **kwargs): if self.invert_axes: x0, y0, dx, dy = y0, x0, dy, dx array = array.T + + plot_opts = element.opts.get('plot', 'plotly') + nodata = plot_opts.kwargs.get('nodata') + if nodata is not None: + array = array if (array.dtype.kind == 'f') else array.astype(np.float64) + array[array == nodata] = np.NaN + return [dict(x0=x0, y0=y0, dx=dx, dy=dy, z=array)] @@ -101,6 +111,9 @@ def get_data(self, element, ranges, style, **kwargs): class QuadMeshPlot(RasterPlot): + nodata = param.Integer(default=None, allow_None=True, doc=""" + Missing data (NaN) value for integer data""") + def get_data(self, element, ranges, style, **kwargs): x, y, z = element.dimensions()[:3] irregular = element.interface.irregular(element, x) @@ -113,4 +126,11 @@ def get_data(self, element, ranges, style, **kwargs): if self.invert_axes: y, x = 'x', 'y' zdata = zdata.T + + plot_opts = element.opts.get('plot', 'plotly') + nodata = plot_opts.kwargs.get('nodata') + if nodata is not None: + zdata = zdata if (zdata.dtype.kind == 'f') else zdata.astype(np.float64) + zdata[zdata == nodata] = np.NaN + return [{x: xc, y: yc, 'z': zdata}] From cbfc411edc0396d61b909214aacb9339cf39670a Mon Sep 17 00:00:00 2001 From: Jean-Luc Stevens Date: Mon, 28 Sep 2020 09:12:44 -0500 Subject: [PATCH 12/98] Update nodata docstring Co-authored-by: James A. Bednar --- holoviews/plotting/mpl/raster.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/holoviews/plotting/mpl/raster.py b/holoviews/plotting/mpl/raster.py index aff3d0dd03..ca251ee4b6 100644 --- a/holoviews/plotting/mpl/raster.py +++ b/holoviews/plotting/mpl/raster.py @@ -14,8 +14,8 @@ class RasterBasePlot(ElementPlot): - nodata = param.Integer(default=None, - doc="Missing data (NaN) value for integer data", allow_None=True) + nodata = param.Integer(default=None, doc=""" + Missing data (NaN) value for integer data""") aspect = param.Parameter(default='equal', doc=""" Raster elements respect the aspect ratio of the From c4684fd7bc80dabb7e155ef9c1f900fa6f9d7307 Mon Sep 17 00:00:00 2001 From: jlstevens Date: Mon, 28 Sep 2020 16:17:29 +0200 Subject: [PATCH 13/98] Updated nodata parameter definition throughout --- holoviews/plotting/bokeh/raster.py | 8 ++++---- holoviews/plotting/mpl/raster.py | 4 ++-- holoviews/plotting/plotly/raster.py | 6 +++--- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/holoviews/plotting/bokeh/raster.py b/holoviews/plotting/bokeh/raster.py index 6468d8cbf9..d993e1c1f6 100644 --- a/holoviews/plotting/bokeh/raster.py +++ b/holoviews/plotting/bokeh/raster.py @@ -15,8 +15,8 @@ class RasterPlot(ColorbarPlot): - nodata = param.Integer(default=None, - doc="Missing data (NaN) value for integer data", allow_None=True) + nodata = param.Integer(default=None, doc=""" + Missing data (NaN) value for integer data""") clipping_colors = param.Dict(default={'NaN': 'transparent'}) @@ -210,8 +210,8 @@ def get_data(self, element, ranges, style): class QuadMeshPlot(ColorbarPlot): - nodata = param.Integer(default=None, - doc="Missing data (NaN) value for integer data", allow_None=True) + nodata = param.Integer(default=None, doc=""" + Missing data (NaN) value for integer data""") clipping_colors = param.Dict(default={'NaN': 'transparent'}) diff --git a/holoviews/plotting/mpl/raster.py b/holoviews/plotting/mpl/raster.py index ca251ee4b6..c4e2e0485c 100644 --- a/holoviews/plotting/mpl/raster.py +++ b/holoviews/plotting/mpl/raster.py @@ -135,8 +135,8 @@ def update_handles(self, key, axis, element, ranges, style): class QuadMeshPlot(ColorbarPlot): - nodata = param.Integer(default=None, - doc="Missing data (NaN) value for integer data", allow_None=True) + nodata = param.Integer(default=None, doc=""" + Missing data (NaN) value for integer data""") clipping_colors = param.Dict(default={'NaN': 'transparent'}) diff --git a/holoviews/plotting/plotly/raster.py b/holoviews/plotting/plotly/raster.py index 6550675562..202cbfd6f5 100644 --- a/holoviews/plotting/plotly/raster.py +++ b/holoviews/plotting/plotly/raster.py @@ -13,8 +13,8 @@ class RasterPlot(ColorbarPlot): padding = param.ClassSelector(default=0, class_=(int, float, tuple)) - nodata = param.Integer(default=None, - doc="Missing data (NaN) value for integer data", allow_None=True) + nodata = param.Integer(default=None, doc=""" + Missing data (NaN) value for integer data""") style_opts = ['visible', 'cmap', 'alpha'] @@ -111,7 +111,7 @@ def get_data(self, element, ranges, style, **kwargs): class QuadMeshPlot(RasterPlot): - nodata = param.Integer(default=None, allow_None=True, doc=""" + nodata = param.Integer(default=None, doc=""" Missing data (NaN) value for integer data""") def get_data(self, element, ranges, style, **kwargs): From c98572249f2e66e3085de79edb60b6842a65ffc1 Mon Sep 17 00:00:00 2001 From: jlstevens Date: Mon, 28 Sep 2020 16:28:21 +0200 Subject: [PATCH 14/98] Raising a ValueError when using nodata with float data --- holoviews/plotting/bokeh/raster.py | 8 ++++++-- holoviews/plotting/mpl/raster.py | 8 ++++++-- holoviews/plotting/plotly/raster.py | 8 ++++++-- 3 files changed, 18 insertions(+), 6 deletions(-) diff --git a/holoviews/plotting/bokeh/raster.py b/holoviews/plotting/bokeh/raster.py index d993e1c1f6..5f24d7974c 100644 --- a/holoviews/plotting/bokeh/raster.py +++ b/holoviews/plotting/bokeh/raster.py @@ -123,7 +123,9 @@ def get_data(self, element, ranges, style): key = 'image' if i == 2 else dimension_sanitizer(vdim.name) nodata = plot_opts.kwargs.get('nodata') if nodata is not None: - img = img if (img.dtype.kind == 'f') else img.astype(np.float64) + if img.dtype.kind == 'f': + raise ValueError('The nodata plot option can only be applied to integer raster data.') + img = img.astype(np.float64) img[img == nodata] = np.NaN data[key] = [img] @@ -253,7 +255,9 @@ def get_data(self, element, ranges, style): plot_opts = element.opts.get('plot', 'bokeh') nodata = plot_opts.kwargs.get('nodata') if nodata is not None: - zdata = zdata if (zdata.dtype.kind == 'f') else zdata.astype(np.float64) + if zdata.dtype.kind == 'f': + raise ValueError('The nodata plot option can only be applied to integer raster data.') + zdata = zdata.astype(np.float64) zdata[zdata == nodata] = np.NaN if irregular: diff --git a/holoviews/plotting/mpl/raster.py b/holoviews/plotting/mpl/raster.py index c4e2e0485c..d98cbe524c 100644 --- a/holoviews/plotting/mpl/raster.py +++ b/holoviews/plotting/mpl/raster.py @@ -88,7 +88,9 @@ def get_data(self, element, ranges, style): plot_opts = element.opts.get('plot', 'matplotlib') nodata = plot_opts.kwargs.get('nodata') if nodata is not None: - data = data if (data.dtype.kind == 'f') else data.astype(np.float64) + if data.dtype.kind == 'f': + raise ValueError('The nodata plot option can only be applied to integer raster data.') + data = data.astype(np.float64) data[data == nodata] = np.NaN return [data], style, {'xticks': xticks, 'yticks': yticks} @@ -157,7 +159,9 @@ def get_data(self, element, ranges, style): plot_opts = element.opts.get('plot', 'matplotlib') nodata = plot_opts.kwargs.get('nodata') if nodata is not None: - data = data if (data.dtype.kind == 'f') else data.astype(np.float64) + if data.dtype.kind == 'f': + raise ValueError('The nodata plot option can only be applied to integer raster data.') + data = data.astype(np.float64) data[data == nodata] = np.NaN expanded = element.interface.irregular(element, element.kdims[0]) diff --git a/holoviews/plotting/plotly/raster.py b/holoviews/plotting/plotly/raster.py index 202cbfd6f5..94f94e353f 100644 --- a/holoviews/plotting/plotly/raster.py +++ b/holoviews/plotting/plotly/raster.py @@ -48,7 +48,9 @@ def get_data(self, element, ranges, style, **kwargs): plot_opts = element.opts.get('plot', 'plotly') nodata = plot_opts.kwargs.get('nodata') if nodata is not None: - array = array if (array.dtype.kind == 'f') else array.astype(np.float64) + if array.dtype.kind == 'f': + raise ValueError('The nodata plot option can only be applied to integer raster data.') + array = array.astype(np.float64) array[array == nodata] = np.NaN return [dict(x0=x0, y0=y0, dx=dx, dy=dy, z=array)] @@ -130,7 +132,9 @@ def get_data(self, element, ranges, style, **kwargs): plot_opts = element.opts.get('plot', 'plotly') nodata = plot_opts.kwargs.get('nodata') if nodata is not None: - zdata = zdata if (zdata.dtype.kind == 'f') else zdata.astype(np.float64) + if zdata.dtype.kind == 'f': + raise ValueError('The nodata plot option can only be applied to integer raster data.') + zdata = zdata.astype(np.float64) zdata[zdata == nodata] = np.NaN return [{x: xc, y: yc, 'z': zdata}] From 04d9c8c6ad99a5e9be8e665b1d250f5b8424585f Mon Sep 17 00:00:00 2001 From: jlstevens Date: Mon, 28 Sep 2020 16:35:24 +0200 Subject: [PATCH 15/98] Ignoring nodata option for float type data --- holoviews/plotting/bokeh/raster.py | 8 ++------ holoviews/plotting/mpl/raster.py | 8 ++------ holoviews/plotting/plotly/raster.py | 8 ++------ 3 files changed, 6 insertions(+), 18 deletions(-) diff --git a/holoviews/plotting/bokeh/raster.py b/holoviews/plotting/bokeh/raster.py index 5f24d7974c..b83dc6c754 100644 --- a/holoviews/plotting/bokeh/raster.py +++ b/holoviews/plotting/bokeh/raster.py @@ -122,9 +122,7 @@ def get_data(self, element, ranges, style): img = img[::-1] key = 'image' if i == 2 else dimension_sanitizer(vdim.name) nodata = plot_opts.kwargs.get('nodata') - if nodata is not None: - if img.dtype.kind == 'f': - raise ValueError('The nodata plot option can only be applied to integer raster data.') + if nodata is not None and (img.dtype.kind == 'i'): img = img.astype(np.float64) img[img == nodata] = np.NaN @@ -254,9 +252,7 @@ def get_data(self, element, ranges, style): zdata = element.dimension_values(z, flat=False) plot_opts = element.opts.get('plot', 'bokeh') nodata = plot_opts.kwargs.get('nodata') - if nodata is not None: - if zdata.dtype.kind == 'f': - raise ValueError('The nodata plot option can only be applied to integer raster data.') + if nodata is not None and (zdata.dtype.kind == 'i'): zdata = zdata.astype(np.float64) zdata[zdata == nodata] = np.NaN diff --git a/holoviews/plotting/mpl/raster.py b/holoviews/plotting/mpl/raster.py index d98cbe524c..0f4981d78e 100644 --- a/holoviews/plotting/mpl/raster.py +++ b/holoviews/plotting/mpl/raster.py @@ -87,9 +87,7 @@ def get_data(self, element, ranges, style): plot_opts = element.opts.get('plot', 'matplotlib') nodata = plot_opts.kwargs.get('nodata') - if nodata is not None: - if data.dtype.kind == 'f': - raise ValueError('The nodata plot option can only be applied to integer raster data.') + if nodata is not None and (data.dtype.kind == 'i'): data = data.astype(np.float64) data[data == nodata] = np.NaN @@ -158,9 +156,7 @@ def get_data(self, element, ranges, style): plot_opts = element.opts.get('plot', 'matplotlib') nodata = plot_opts.kwargs.get('nodata') - if nodata is not None: - if data.dtype.kind == 'f': - raise ValueError('The nodata plot option can only be applied to integer raster data.') + if nodata is not None and (data.dtype.kind == 'i'): data = data.astype(np.float64) data[data == nodata] = np.NaN diff --git a/holoviews/plotting/plotly/raster.py b/holoviews/plotting/plotly/raster.py index 94f94e353f..dca2f6c215 100644 --- a/holoviews/plotting/plotly/raster.py +++ b/holoviews/plotting/plotly/raster.py @@ -47,9 +47,7 @@ def get_data(self, element, ranges, style, **kwargs): plot_opts = element.opts.get('plot', 'plotly') nodata = plot_opts.kwargs.get('nodata') - if nodata is not None: - if array.dtype.kind == 'f': - raise ValueError('The nodata plot option can only be applied to integer raster data.') + if nodata is not None and (array.dtype.kind == 'i'): array = array.astype(np.float64) array[array == nodata] = np.NaN @@ -131,9 +129,7 @@ def get_data(self, element, ranges, style, **kwargs): plot_opts = element.opts.get('plot', 'plotly') nodata = plot_opts.kwargs.get('nodata') - if nodata is not None: - if zdata.dtype.kind == 'f': - raise ValueError('The nodata plot option can only be applied to integer raster data.') + if nodata is not None and (zdata.dtype.kind == 'i'): zdata = zdata.astype(np.float64) zdata[zdata == nodata] = np.NaN From ba23bc8b914fa9237373b698a745657d214dd99b Mon Sep 17 00:00:00 2001 From: jlstevens Date: Mon, 28 Sep 2020 17:41:35 +0200 Subject: [PATCH 16/98] Removed count range declaration to examine tests --- holoviews/operation/datashader.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/holoviews/operation/datashader.py b/holoviews/operation/datashader.py index 2abdc41fc7..fad789fc39 100644 --- a/holoviews/operation/datashader.py +++ b/holoviews/operation/datashader.py @@ -306,9 +306,9 @@ def _get_agg_params(self, element, x, y, agg_fn, bounds): name = '%s Count' % column if isinstance(agg_fn, ds.count_cat) else column vdims = [dims[0].clone(name)] elif category: - vdims = Dimension('%s Count' % category, range=(1, None)) + vdims = Dimension('%s Count' % category) else: - vdims = Dimension('Count', range=(1, None)) + vdims = Dimension('Count') params['vdims'] = vdims return params From 2e7bdf57b08501759c575824a23cd9f29b92ff85 Mon Sep 17 00:00:00 2001 From: jlstevens Date: Tue, 29 Sep 2020 10:40:01 +0200 Subject: [PATCH 17/98] Reverted dimension range changes in unit tests --- holoviews/tests/operation/testdatashader.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/holoviews/tests/operation/testdatashader.py b/holoviews/tests/operation/testdatashader.py index 796d2d0752..995e0cbe32 100644 --- a/holoviews/tests/operation/testdatashader.py +++ b/holoviews/tests/operation/testdatashader.py @@ -111,7 +111,7 @@ def test_aggregate_points_categorical_zero_range(self): def test_aggregate_curve(self): curve = Curve([(0.2, 0.3), (0.4, 0.7), (0.8, 0.99)]) expected = Image(([0.25, 0.75], [0.25, 0.75], [[1, 0], [1, 1]]), - vdims=['Count']).redim.range(Count=(1,None)) + vdims=['Count']) img = aggregate(curve, dynamic=False, x_range=(0, 1), y_range=(0, 1), width=2, height=2) self.assertEqual(img, expected) @@ -126,7 +126,7 @@ def test_aggregate_curve_datetimes(self): np.datetime64('2016-01-02T12:00:00.000000000')] expected = Image((dates, [1.5, 2.5], [[1, 0], [0, 2]]), datatype=['xarray'], bounds=bounds, - vdims='Count').redim.range(Count=(1,None)) + vdims='Count') self.assertEqual(img, expected) def test_aggregate_curve_datetimes_dask(self): @@ -143,7 +143,7 @@ def test_aggregate_curve_datetimes_dask(self): np.datetime64('2019-01-01T12:29:15.000000000')] expected = Image((dates, [166.5, 499.5, 832.5], [[332, 0], [167, 166], [0, 334]]), ['index', 'a'], 'Count', datatype=['xarray'], - bounds=bounds).redim.range(Count=(1,None)) + bounds=bounds) self.assertEqual(img, expected) def test_aggregate_curve_datetimes_microsecond_timebase(self): @@ -158,7 +158,7 @@ def test_aggregate_curve_datetimes_microsecond_timebase(self): np.datetime64('2016-01-02T12:00:00.138241000')] expected = Image((dates, [1.5, 2.5], [[1, 0], [0, 2]]), datatype=['xarray'], bounds=bounds, - vdims='Count').redim.range(Count=(1,None)) + vdims='Count') self.assertEqual(img, expected) def test_aggregate_ndoverlay_count_cat_datetimes_microsecond_timebase(self): From 15566f6fecea69bd4d1639ab9a58273e05e7579c Mon Sep 17 00:00:00 2001 From: jlstevens Date: Tue, 29 Sep 2020 16:47:49 +0200 Subject: [PATCH 18/98] Removed unnecessary clipping_colors setting from options.Image --- holoviews/plotting/bokeh/__init__.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/holoviews/plotting/bokeh/__init__.py b/holoviews/plotting/bokeh/__init__.py index 532fc326b5..8ddbff4958 100644 --- a/holoviews/plotting/bokeh/__init__.py +++ b/holoviews/plotting/bokeh/__init__.py @@ -194,8 +194,7 @@ def colormap_generator(palette): # Rasters options.Image = Options('style', cmap=dflt_cmap) -options.Image = Options('plot', clipping_colors={'min': 'transparent', - 'max': 'transparent'}) + options.Raster = Options('style', cmap=dflt_cmap) options.QuadMesh = Options('style', cmap=dflt_cmap, line_alpha=0) options.HeatMap = Options('style', cmap='RdYlBu_r', annular_line_alpha=0, From 6d3ab407b984568faef1b730a0bfd7b8c092a283 Mon Sep 17 00:00:00 2001 From: jlstevens Date: Tue, 29 Sep 2020 16:50:30 +0200 Subject: [PATCH 19/98] Formatting fixes in testdatashader.py --- holoviews/tests/operation/testdatashader.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/holoviews/tests/operation/testdatashader.py b/holoviews/tests/operation/testdatashader.py index 995e0cbe32..ab32a1c1ea 100644 --- a/holoviews/tests/operation/testdatashader.py +++ b/holoviews/tests/operation/testdatashader.py @@ -125,8 +125,7 @@ def test_aggregate_curve_datetimes(self): dates = [np.datetime64('2016-01-01T12:00:00.000000000'), np.datetime64('2016-01-02T12:00:00.000000000')] expected = Image((dates, [1.5, 2.5], [[1, 0], [0, 2]]), - datatype=['xarray'], bounds=bounds, - vdims='Count') + datatype=['xarray'], bounds=bounds, vdims='Count') self.assertEqual(img, expected) def test_aggregate_curve_datetimes_dask(self): @@ -142,8 +141,7 @@ def test_aggregate_curve_datetimes_dask(self): dates = [np.datetime64('2019-01-01T04:09:45.000000000'), np.datetime64('2019-01-01T12:29:15.000000000')] expected = Image((dates, [166.5, 499.5, 832.5], [[332, 0], [167, 166], [0, 334]]), - ['index', 'a'], 'Count', datatype=['xarray'], - bounds=bounds) + ['index', 'a'], 'Count', datatype=['xarray'], bounds=bounds) self.assertEqual(img, expected) def test_aggregate_curve_datetimes_microsecond_timebase(self): @@ -157,8 +155,7 @@ def test_aggregate_curve_datetimes_microsecond_timebase(self): dates = [np.datetime64('2016-01-01T11:59:59.861759000',), np.datetime64('2016-01-02T12:00:00.138241000')] expected = Image((dates, [1.5, 2.5], [[1, 0], [0, 2]]), - datatype=['xarray'], bounds=bounds, - vdims='Count') + datatype=['xarray'], bounds=bounds, vdims='Count') self.assertEqual(img, expected) def test_aggregate_ndoverlay_count_cat_datetimes_microsecond_timebase(self): From ed16d1e5e369bcc1bdf79a7133eb370d438d054e Mon Sep 17 00:00:00 2001 From: jlstevens Date: Tue, 29 Sep 2020 16:51:43 +0200 Subject: [PATCH 20/98] Removed unnecessary newline --- holoviews/plotting/bokeh/__init__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/holoviews/plotting/bokeh/__init__.py b/holoviews/plotting/bokeh/__init__.py index 8ddbff4958..3e6cbe840f 100644 --- a/holoviews/plotting/bokeh/__init__.py +++ b/holoviews/plotting/bokeh/__init__.py @@ -194,7 +194,6 @@ def colormap_generator(palette): # Rasters options.Image = Options('style', cmap=dflt_cmap) - options.Raster = Options('style', cmap=dflt_cmap) options.QuadMesh = Options('style', cmap=dflt_cmap, line_alpha=0) options.HeatMap = Options('style', cmap='RdYlBu_r', annular_line_alpha=0, From 3570fb1d8e17d0255b1df676d8c362737ba6f962 Mon Sep 17 00:00:00 2001 From: Jean-Luc Stevens Date: Tue, 29 Sep 2020 10:28:18 -0500 Subject: [PATCH 21/98] Applied docstring suggestions Co-authored-by: James A. Bednar --- holoviews/plotting/bokeh/element.py | 4 ++-- holoviews/plotting/bokeh/raster.py | 3 ++- holoviews/plotting/mpl/element.py | 2 +- holoviews/plotting/mpl/raster.py | 6 ++++-- holoviews/plotting/plotly/raster.py | 3 ++- 5 files changed, 11 insertions(+), 7 deletions(-) diff --git a/holoviews/plotting/bokeh/element.py b/holoviews/plotting/bokeh/element.py index b35248ab7b..a222499018 100644 --- a/holoviews/plotting/bokeh/element.py +++ b/holoviews/plotting/bokeh/element.py @@ -1697,7 +1697,7 @@ class ColorbarPlot(ElementPlot): Formatter for ticks along the colorbar axis.""") clabel = param.String(default=None, doc=""" - An explicit override of the color bar label, if set takes precedence + An explicit override of the color bar label. If set, takes precedence over the title key in colorbar_opts.""") clim = param.Tuple(default=(np.nan, np.nan), length=2, doc=""" @@ -1714,7 +1714,7 @@ class ColorbarPlot(ElementPlot): Formatter for ticks along the colorbar axis.""") cnorm = param.ObjectSelector(default='linear', objects=['linear', 'log', 'eqhist'], doc=""" - Color normalization applied during colormapping.""") + Color normalization to be applied during colormapping.""") colorbar = param.Boolean(default=False, doc=""" Whether to display a colorbar.""") diff --git a/holoviews/plotting/bokeh/raster.py b/holoviews/plotting/bokeh/raster.py index b83dc6c754..c9bda7518d 100644 --- a/holoviews/plotting/bokeh/raster.py +++ b/holoviews/plotting/bokeh/raster.py @@ -16,7 +16,8 @@ class RasterPlot(ColorbarPlot): nodata = param.Integer(default=None, doc=""" - Missing data (NaN) value for integer data""") + Optional missing-data value for integer data. + If non-None, data with this value will be replaced with NaN so that it is transparent when plotted.""") clipping_colors = param.Dict(default={'NaN': 'transparent'}) diff --git a/holoviews/plotting/mpl/element.py b/holoviews/plotting/mpl/element.py index 9d92984d97..0c987e78ca 100644 --- a/holoviews/plotting/mpl/element.py +++ b/holoviews/plotting/mpl/element.py @@ -690,7 +690,7 @@ class ColorbarPlot(ElementPlot): intervals defining the range of values to map each color to.""") cnorm = param.ObjectSelector(default='linear', objects=['linear', 'log', 'eqhist'], doc=""" - Color normalization applied during colormapping.""") + Color normalization to be applied during colormapping.""") clipping_colors = param.Dict(default={}, doc=""" Dictionary to specify colors for clipped values, allows diff --git a/holoviews/plotting/mpl/raster.py b/holoviews/plotting/mpl/raster.py index 0f4981d78e..9195cd51a9 100644 --- a/holoviews/plotting/mpl/raster.py +++ b/holoviews/plotting/mpl/raster.py @@ -15,7 +15,8 @@ class RasterBasePlot(ElementPlot): nodata = param.Integer(default=None, doc=""" - Missing data (NaN) value for integer data""") + Optional missing-data value for integer data. + If non-None, data with this value will be replaced with NaN so that it is transparent when plotted.""") aspect = param.Parameter(default='equal', doc=""" Raster elements respect the aspect ratio of the @@ -136,7 +137,8 @@ def update_handles(self, key, axis, element, ranges, style): class QuadMeshPlot(ColorbarPlot): nodata = param.Integer(default=None, doc=""" - Missing data (NaN) value for integer data""") + Optional missing-data value for integer data. + If non-None, data with this value will be replaced with NaN so that it is transparent when plotted.""") clipping_colors = param.Dict(default={'NaN': 'transparent'}) diff --git a/holoviews/plotting/plotly/raster.py b/holoviews/plotting/plotly/raster.py index dca2f6c215..bacbeee4c0 100644 --- a/holoviews/plotting/plotly/raster.py +++ b/holoviews/plotting/plotly/raster.py @@ -112,7 +112,8 @@ def get_data(self, element, ranges, style, **kwargs): class QuadMeshPlot(RasterPlot): nodata = param.Integer(default=None, doc=""" - Missing data (NaN) value for integer data""") + Optional missing-data value for integer data. + If non-None, data with this value will be replaced with NaN so that it is transparent when plotted.""") def get_data(self, element, ranges, style, **kwargs): x, y, z = element.dimensions()[:3] From d435197cfbb402b35ae6418421b2f62381113542 Mon Sep 17 00:00:00 2001 From: Jean-Luc Stevens Date: Tue, 29 Sep 2020 10:33:59 -0500 Subject: [PATCH 22/98] Updated docstring with suggestion Co-authored-by: James A. Bednar --- holoviews/plotting/plotly/raster.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/holoviews/plotting/plotly/raster.py b/holoviews/plotting/plotly/raster.py index bacbeee4c0..7a966d9971 100644 --- a/holoviews/plotting/plotly/raster.py +++ b/holoviews/plotting/plotly/raster.py @@ -14,7 +14,8 @@ class RasterPlot(ColorbarPlot): padding = param.ClassSelector(default=0, class_=(int, float, tuple)) nodata = param.Integer(default=None, doc=""" - Missing data (NaN) value for integer data""") + Optional missing-data value for integer data. + If non-None, data with this value will be replaced with NaN so that it is transparent when plotted.""") style_opts = ['visible', 'cmap', 'alpha'] From aaa785a76d2148b2f103ccf1ec3a38de5d6c3023 Mon Sep 17 00:00:00 2001 From: jlstevens Date: Wed, 30 Sep 2020 10:33:28 +0200 Subject: [PATCH 23/98] Factored out an apply_nodata utility --- holoviews/plotting/bokeh/raster.py | 14 +++----------- holoviews/plotting/mpl/raster.py | 15 +++------------ holoviews/plotting/plotly/raster.py | 15 +++------------ holoviews/plotting/util.py | 9 +++++++++ 4 files changed, 18 insertions(+), 35 deletions(-) diff --git a/holoviews/plotting/bokeh/raster.py b/holoviews/plotting/bokeh/raster.py index c9bda7518d..a41118e6f5 100644 --- a/holoviews/plotting/bokeh/raster.py +++ b/holoviews/plotting/bokeh/raster.py @@ -7,6 +7,7 @@ from ...core.util import cartesian_product, dimension_sanitizer, isfinite from ...element import Raster +from ..util import apply_nodata from .element import ElementPlot, ColorbarPlot from .selection import BokehOverlaySelectionDisplay from .styles import base_properties, fill_properties, line_properties, mpl_to_bokeh @@ -105,7 +106,6 @@ def get_data(self, element, ranges, style): b, t = t, b data = dict(x=[l], y=[b], dw=[dw], dh=[dh]) - plot_opts = element.opts.get('plot', 'bokeh') for i, vdim in enumerate(element.vdims, 2): if i > 2 and 'hover' not in self.handles: break @@ -122,11 +122,7 @@ def get_data(self, element, ranges, style): if self.invert_yaxis: img = img[::-1] key = 'image' if i == 2 else dimension_sanitizer(vdim.name) - nodata = plot_opts.kwargs.get('nodata') - if nodata is not None and (img.dtype.kind == 'i'): - img = img.astype(np.float64) - img[img == nodata] = np.NaN - + img = apply_nodata(element.opts.get('plot', 'bokeh'), img) data[key] = [img] return (data, mapping, style) @@ -251,11 +247,7 @@ def get_data(self, element, ranges, style): x, y = dimension_sanitizer(x.name), dimension_sanitizer(y.name) zdata = element.dimension_values(z, flat=False) - plot_opts = element.opts.get('plot', 'bokeh') - nodata = plot_opts.kwargs.get('nodata') - if nodata is not None and (zdata.dtype.kind == 'i'): - zdata = zdata.astype(np.float64) - zdata[zdata == nodata] = np.NaN + zdata = apply_nodata(element.opts.get('plot', 'bokeh'), zdata) if irregular: dims = element.kdims diff --git a/holoviews/plotting/mpl/raster.py b/holoviews/plotting/mpl/raster.py index 9195cd51a9..858e6aa337 100644 --- a/holoviews/plotting/mpl/raster.py +++ b/holoviews/plotting/mpl/raster.py @@ -7,6 +7,7 @@ from ...core import traversal from ...core.util import match_spec, max_range, unique_iterator from ...element.raster import Image, Raster, RGB +from ..util import apply_nodata from .element import ElementPlot, ColorbarPlot, OverlayPlot from .plot import MPLPlot, GridPlot, mpl_rc_context from .util import get_raster_array, mpl_version @@ -86,12 +87,7 @@ def get_data(self, element, ranges, style): style['extent'] = [l, r, b, t] style['origin'] = 'upper' - plot_opts = element.opts.get('plot', 'matplotlib') - nodata = plot_opts.kwargs.get('nodata') - if nodata is not None and (data.dtype.kind == 'i'): - data = data.astype(np.float64) - data[data == nodata] = np.NaN - + data = apply_nodata(element.opts.get('plot', 'matplotlib'), data) return [data], style, {'xticks': xticks, 'yticks': yticks} def update_handles(self, key, axis, element, ranges, style): @@ -156,12 +152,7 @@ def get_data(self, element, ranges, style): zdata = element.dimension_values(2, flat=False) data = np.ma.array(zdata, mask=np.logical_not(np.isfinite(zdata))) - plot_opts = element.opts.get('plot', 'matplotlib') - nodata = plot_opts.kwargs.get('nodata') - if nodata is not None and (data.dtype.kind == 'i'): - data = data.astype(np.float64) - data[data == nodata] = np.NaN - + data = apply_nodata(element.opts.get('plot', 'matplotlib'), data) expanded = element.interface.irregular(element, element.kdims[0]) edges = style.get('shading') != 'gouraud' coords = [element.interface.coords(element, d, ordered=True, diff --git a/holoviews/plotting/plotly/raster.py b/holoviews/plotting/plotly/raster.py index 7a966d9971..5271949eaa 100644 --- a/holoviews/plotting/plotly/raster.py +++ b/holoviews/plotting/plotly/raster.py @@ -6,6 +6,7 @@ from ...core.options import SkipRendering from ...element import Image, Raster from ..mixins import HeatMapMixin +from ..util import apply_nodata from .element import ColorbarPlot @@ -46,12 +47,7 @@ def get_data(self, element, ranges, style, **kwargs): x0, y0, dx, dy = y0, x0, dy, dx array = array.T - plot_opts = element.opts.get('plot', 'plotly') - nodata = plot_opts.kwargs.get('nodata') - if nodata is not None and (array.dtype.kind == 'i'): - array = array.astype(np.float64) - array[array == nodata] = np.NaN - + array = apply_nodata(element.opts.get('plot', 'plotly'), array) return [dict(x0=x0, y0=y0, dx=dx, dy=dy, z=array)] @@ -129,10 +125,5 @@ def get_data(self, element, ranges, style, **kwargs): y, x = 'x', 'y' zdata = zdata.T - plot_opts = element.opts.get('plot', 'plotly') - nodata = plot_opts.kwargs.get('nodata') - if nodata is not None and (zdata.dtype.kind == 'i'): - zdata = zdata.astype(np.float64) - zdata[zdata == nodata] = np.NaN - + zdata = apply_nodata(element.opts.get('plot', 'plotly'), zdata) return [{x: xc, y: yc, 'z': zdata}] diff --git a/holoviews/plotting/util.py b/holoviews/plotting/util.py index 52c9a71c64..0269f7de1d 100644 --- a/holoviews/plotting/util.py +++ b/holoviews/plotting/util.py @@ -1095,6 +1095,15 @@ def hex2rgb(hex): # Pass 16 to the integer function for change of base return [int(hex[i:i+2], 16) for i in range(1,6,2)] +def apply_nodata(opts, data): + "Replace `nodata` value in data with NaN, if specified in opts" + nodata = opts.kwargs.get('nodata') + if nodata is not None and (data.dtype.kind == 'i'): + data = data.astype(np.float64) + data[data == nodata] = np.NaN + return data + return data + RGB_HEX_REGEX = re.compile(r'^#(?:[0-9a-fA-F]{3}){1,2}$') From e55534626c0224d33cd1cf9b1d26ce4bc9a79a89 Mon Sep 17 00:00:00 2001 From: jlstevens Date: Wed, 30 Sep 2020 10:38:47 +0200 Subject: [PATCH 24/98] Updated docstring to make it clear than transparent NaNs is a default --- holoviews/plotting/bokeh/raster.py | 7 +++++-- holoviews/plotting/mpl/raster.py | 6 ++++-- holoviews/plotting/plotly/raster.py | 6 ++++-- 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/holoviews/plotting/bokeh/raster.py b/holoviews/plotting/bokeh/raster.py index a41118e6f5..d258d5024c 100644 --- a/holoviews/plotting/bokeh/raster.py +++ b/holoviews/plotting/bokeh/raster.py @@ -18,7 +18,8 @@ class RasterPlot(ColorbarPlot): nodata = param.Integer(default=None, doc=""" Optional missing-data value for integer data. - If non-None, data with this value will be replaced with NaN so that it is transparent when plotted.""") + If non-None, data with this value will be replaced with NaN so + that it is transparent (by default) when plotted.""") clipping_colors = param.Dict(default={'NaN': 'transparent'}) @@ -208,7 +209,9 @@ def get_data(self, element, ranges, style): class QuadMeshPlot(ColorbarPlot): nodata = param.Integer(default=None, doc=""" - Missing data (NaN) value for integer data""") + Optional missing-data value for integer data. + If non-None, data with this value will be replaced with NaN so + that it is transparent (by default) when plotted.""") clipping_colors = param.Dict(default={'NaN': 'transparent'}) diff --git a/holoviews/plotting/mpl/raster.py b/holoviews/plotting/mpl/raster.py index 858e6aa337..2a9594f27f 100644 --- a/holoviews/plotting/mpl/raster.py +++ b/holoviews/plotting/mpl/raster.py @@ -17,7 +17,8 @@ class RasterBasePlot(ElementPlot): nodata = param.Integer(default=None, doc=""" Optional missing-data value for integer data. - If non-None, data with this value will be replaced with NaN so that it is transparent when plotted.""") + If non-None, data with this value will be replaced with NaN so + that it is transparent (by default) when plotted.""") aspect = param.Parameter(default='equal', doc=""" Raster elements respect the aspect ratio of the @@ -134,7 +135,8 @@ class QuadMeshPlot(ColorbarPlot): nodata = param.Integer(default=None, doc=""" Optional missing-data value for integer data. - If non-None, data with this value will be replaced with NaN so that it is transparent when plotted.""") + If non-None, data with this value will be replaced with NaN so + that it is transparent (by default) when plotted.""") clipping_colors = param.Dict(default={'NaN': 'transparent'}) diff --git a/holoviews/plotting/plotly/raster.py b/holoviews/plotting/plotly/raster.py index 5271949eaa..26e2701611 100644 --- a/holoviews/plotting/plotly/raster.py +++ b/holoviews/plotting/plotly/raster.py @@ -16,7 +16,8 @@ class RasterPlot(ColorbarPlot): nodata = param.Integer(default=None, doc=""" Optional missing-data value for integer data. - If non-None, data with this value will be replaced with NaN so that it is transparent when plotted.""") + If non-None, data with this value will be replaced with NaN so + that it is transparent (by default) when plotted.""") style_opts = ['visible', 'cmap', 'alpha'] @@ -110,7 +111,8 @@ class QuadMeshPlot(RasterPlot): nodata = param.Integer(default=None, doc=""" Optional missing-data value for integer data. - If non-None, data with this value will be replaced with NaN so that it is transparent when plotted.""") + If non-None, data with this value will be replaced with NaN so + that it is transparent (by default) when plotted.""") def get_data(self, element, ranges, style, **kwargs): x, y, z = element.dimensions()[:3] From acd00bdc2636afb7a4794cb76ba2d2d4132b2c9f Mon Sep 17 00:00:00 2001 From: jlstevens Date: Wed, 30 Sep 2020 10:41:56 +0200 Subject: [PATCH 25/98] Renamed 'eqhist' to 'eq_hist' in cnorm parameter --- holoviews/plotting/bokeh/element.py | 4 ++-- holoviews/plotting/mpl/element.py | 4 ++-- holoviews/plotting/mpl/util.py | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/holoviews/plotting/bokeh/element.py b/holoviews/plotting/bokeh/element.py index a222499018..7fdecfce39 100644 --- a/holoviews/plotting/bokeh/element.py +++ b/holoviews/plotting/bokeh/element.py @@ -1713,7 +1713,7 @@ class ColorbarPlot(ElementPlot): default=None, class_=(util.basestring, TickFormatter, FunctionType), doc=""" Formatter for ticks along the colorbar axis.""") - cnorm = param.ObjectSelector(default='linear', objects=['linear', 'log', 'eqhist'], doc=""" + cnorm = param.ObjectSelector(default='linear', objects=['linear', 'log', 'eq_hist'], doc=""" Color normalization to be applied during colormapping.""") colorbar = param.Boolean(default=False, doc=""" @@ -1954,7 +1954,7 @@ def _get_cmapper_opts(self, low, high, factors, colors): "lower bound on the color dimension or using " "the `clim` option." ) - elif self.cnorm == 'eqhist': + elif self.cnorm == 'eq_hist': from bokeh.models import EqHistColorMapper colormapper = EqHistColorMapper if isinstance(low, (bool, np.bool_)): low = int(low) diff --git a/holoviews/plotting/mpl/element.py b/holoviews/plotting/mpl/element.py index 0c987e78ca..8e79440782 100644 --- a/holoviews/plotting/mpl/element.py +++ b/holoviews/plotting/mpl/element.py @@ -689,7 +689,7 @@ class ColorbarPlot(ElementPlot): Number of discrete colors to use when colormapping or a set of color intervals defining the range of values to map each color to.""") - cnorm = param.ObjectSelector(default='linear', objects=['linear', 'log', 'eqhist'], doc=""" + cnorm = param.ObjectSelector(default='linear', objects=['linear', 'log', 'eq_hist'], doc=""" Color normalization to be applied during colormapping.""") clipping_colors = param.Dict(default={}, doc=""" @@ -897,7 +897,7 @@ def _norm_kwargs(self, element, ranges, opts, vdim, values=None, prefix=''): else: categorical = values.dtype.kind not in 'uif' - if self.cnorm == 'eqhist': + if self.cnorm == 'eq_hist': opts[prefix+'norm'] = EqHistNormalize( vmin=clim[0], vmax=clim[1]) if self.cnorm == 'log' or self.logz: diff --git a/holoviews/plotting/mpl/util.py b/holoviews/plotting/mpl/util.py index 0e6ee34851..c698ff8f14 100644 --- a/holoviews/plotting/mpl/util.py +++ b/holoviews/plotting/mpl/util.py @@ -459,7 +459,7 @@ def process_value(self, data): def inverse(self, value): if self._bin_edges is None: - raise ValueError("Not invertible until eqhist has been computed") + raise ValueError("Not invertible until eq_hist has been computed") return np.interp([value], self._color_bins, self._bin_edges)[0] From feb98b65fc331fb82484b7048515e1ee0bc86bbe Mon Sep 17 00:00:00 2001 From: jlstevens Date: Thu, 19 Nov 2020 13:55:07 +0100 Subject: [PATCH 26/98] Added nodata unit tests for Image and Quadmesh across all backends --- holoviews/tests/plotting/bokeh/testquadmeshplot.py | 10 ++++++++++ holoviews/tests/plotting/bokeh/testrasterplot.py | 10 ++++++++++ .../tests/plotting/matplotlib/testquadmeshplot.py | 8 ++++++++ holoviews/tests/plotting/matplotlib/testrasterplot.py | 11 +++++++++++ holoviews/tests/plotting/plotly/testimageplot.py | 6 ++++++ holoviews/tests/plotting/plotly/testquadmeshplot.py | 7 +++++++ 6 files changed, 52 insertions(+) diff --git a/holoviews/tests/plotting/bokeh/testquadmeshplot.py b/holoviews/tests/plotting/bokeh/testquadmeshplot.py index 60015ffb14..78464e1daa 100644 --- a/holoviews/tests/plotting/bokeh/testquadmeshplot.py +++ b/holoviews/tests/plotting/bokeh/testquadmeshplot.py @@ -48,3 +48,13 @@ def test_quadmesh_inverted_coords(self): self.assertEqual(source.data['right'], np.array([0.5, 0.5, 0.5, 1.5, 1.5, 1.5, 2.5, 2.5, 2.5])) self.assertEqual(source.data['top'], np.array([0.5, 1.5, 2.5, 0.5, 1.5, 2.5, 0.5, 1.5, 2.5])) self.assertEqual(source.data['bottom'], np.array([-0.5, 0.5, 1.5, -0.5, 0.5, 1.5, -0.5, 0.5, 1.5])) + + def test_quadmesh_nodata(self): + xs = [0, 1, 2] + ys = [2, 1, 0] + data = np.array([[0,1,2], [3,4,5], [6,7,8]]) + flattened = np.array([6, 3, np.NaN, 7, 4, 1, 8, 5, 2]) + qmesh = QuadMesh((xs, ys, data)).opts(nodata=0) + plot = bokeh_renderer.get_plot(qmesh) + source = plot.handles['source'] + self.assertEqual(source.data['z'], flattened) \ No newline at end of file diff --git a/holoviews/tests/plotting/bokeh/testrasterplot.py b/holoviews/tests/plotting/bokeh/testrasterplot.py index b04a624ffc..be2636b636 100644 --- a/holoviews/tests/plotting/bokeh/testrasterplot.py +++ b/holoviews/tests/plotting/bokeh/testrasterplot.py @@ -21,6 +21,16 @@ def test_image_boolean_array(self): self.assertEqual(source.data['image'][0], np.array([[0, 1], [1, 0]])) + def test_nodata_array(self): + img = Image(np.array([[0, 1], [2, 0]])).opts(nodata=0) + plot = bokeh_renderer.get_plot(img) + cmapper = plot.handles['color_mapper'] + source = plot.handles['source'] + self.assertEqual(cmapper.low, 0) + self.assertEqual(cmapper.high, 2) + self.assertEqual(source.data['image'][0], + np.array([[2, np.NaN], [np.NaN, 1]])) + def test_raster_invert_axes(self): arr = np.array([[0, 1, 2], [3, 4, 5]]) raster = Raster(arr).opts(plot=dict(invert_axes=True)) diff --git a/holoviews/tests/plotting/matplotlib/testquadmeshplot.py b/holoviews/tests/plotting/matplotlib/testquadmeshplot.py index b93cd466f7..102e709551 100644 --- a/holoviews/tests/plotting/matplotlib/testquadmeshplot.py +++ b/holoviews/tests/plotting/matplotlib/testquadmeshplot.py @@ -14,6 +14,14 @@ def test_quadmesh_invert_axes(self): artist = plot.handles['artist'] self.assertEqual(artist.get_array().data, arr.T[:, ::-1].flatten()) + def test_quadmesh_nodata(self): + arr = np.array([[0, 1, 2], [3, 4, 5]]) + qmesh = QuadMesh(Image(arr)).opts(nodata=0) + plot = mpl_renderer.get_plot(qmesh) + artist = plot.handles['artist'] + self.assertEqual(artist.get_array().data, + np.array([3, 4, 5, np.NaN, 1, 2])) + def test_quadmesh_update_cbar(self): xs = ys = np.linspace(0, 6, 10) zs = np.linspace(1, 2, 5) diff --git a/holoviews/tests/plotting/matplotlib/testrasterplot.py b/holoviews/tests/plotting/matplotlib/testrasterplot.py index f7882465b9..bf05eaa8fa 100644 --- a/holoviews/tests/plotting/matplotlib/testrasterplot.py +++ b/holoviews/tests/plotting/matplotlib/testrasterplot.py @@ -20,6 +20,17 @@ def test_raster_invert_axes(self): self.assertEqual(artist.get_array().data, arr.T[::-1]) self.assertEqual(artist.get_extent(), [0, 2, 0, 3]) + def test_raster_nodata(self): + arr = np.array([[0, 1, 2], [3, 4, 5]]) + expected = np.array([[3, 4, 5], + [np.NaN, 1, 2]]) + + raster = Raster(arr).opts(nodata=0) + plot = mpl_renderer.get_plot(raster) + artist = plot.handles['artist'] + self.assertEqual(artist.get_array().data, expected) + + def test_image_invert_axes(self): arr = np.array([[0, 1, 2], [3, 4, 5]]) raster = Image(arr).opts(invert_axes=True) diff --git a/holoviews/tests/plotting/plotly/testimageplot.py b/holoviews/tests/plotting/plotly/testimageplot.py index 729987231d..97d71876d5 100644 --- a/holoviews/tests/plotting/plotly/testimageplot.py +++ b/holoviews/tests/plotting/plotly/testimageplot.py @@ -21,6 +21,12 @@ def test_image_state(self): self.assertEqual(state['layout']['xaxis']['range'], [0.5, 3.5]) self.assertEqual(state['layout']['yaxis']['range'], [-0.5, 1.5]) + def test_image_nodata(self): + img = Image(([1, 2, 3], [0, 1], np.array([[0, 1, 2], [2, 3, 4]]))).opts(nodata=0) + state = self._get_plot_state(img) + self.assertEqual(state['data'][0]['type'], 'heatmap') + self.assertEqual(state['data'][0]['z'], np.array([[np.NaN, 1, 2], [2, 3, 4]])) + def test_image_state_inverted(self): img = Image(([1, 2, 3], [0, 1], np.array([[0, 1, 2], [2, 3, 4]]))).options( invert_axes=True) diff --git a/holoviews/tests/plotting/plotly/testquadmeshplot.py b/holoviews/tests/plotting/plotly/testquadmeshplot.py index 3cdf70cbcf..8413b8cf82 100644 --- a/holoviews/tests/plotting/plotly/testquadmeshplot.py +++ b/holoviews/tests/plotting/plotly/testquadmeshplot.py @@ -19,6 +19,13 @@ def test_quadmesh_state(self): self.assertEqual(state['layout']['xaxis']['range'], [0.5, 5]) self.assertEqual(state['layout']['yaxis']['range'], [-0.5, 1.5]) + def test_quadmesh_nodata(self): + img = QuadMesh(([1, 2, 4], [0, 1], + np.array([[0, 1, 2], [2, 3, 4]]))).opts(nodata=0) + state = self._get_plot_state(img) + self.assertEqual(state['data'][0]['type'], 'heatmap') + self.assertEqual(state['data'][0]['z'], np.array([[np.NaN, 1, 2], [2, 3, 4]])) + def test_quadmesh_state_inverted(self): img = QuadMesh(([1, 2, 4], [0, 1], np.array([[0, 1, 2], [2, 3, 4]]))).options( invert_axes=True) From 388cdf236e25517593607630effe1a4ca4daba70 Mon Sep 17 00:00:00 2001 From: jlstevens Date: Thu, 19 Nov 2020 14:57:58 +0100 Subject: [PATCH 27/98] Added unit tests for the cnorm plot option --- .../tests/plotting/bokeh/testelementplot.py | 26 ++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/holoviews/tests/plotting/bokeh/testelementplot.py b/holoviews/tests/plotting/bokeh/testelementplot.py index b273f9976a..4952f8c62a 100644 --- a/holoviews/tests/plotting/bokeh/testelementplot.py +++ b/holoviews/tests/plotting/bokeh/testelementplot.py @@ -16,7 +16,8 @@ from bokeh.document import Document from bokeh.models import tools from bokeh.models import (FuncTickFormatter, PrintfTickFormatter, - NumeralTickFormatter, LogTicker) + NumeralTickFormatter, LogTicker, + LinearColorMapper, LogColorMapper) from holoviews.plotting.bokeh.util import bokeh_version except: pass @@ -810,6 +811,29 @@ def test_colormapper_transparent_nan(self): cmapper = plot.handles['color_mapper'] self.assertEqual(cmapper.nan_color, 'rgba(0, 0, 0, 0)') + def test_colormapper_cnorm_linear(self): + img = Image(np.array([[0, 1], [2, 3]])).options(cnorm='linear') + plot = bokeh_renderer.get_plot(img) + cmapper = plot.handles['color_mapper'] + self.assertTrue(cmapper, LinearColorMapper) + + def test_colormapper_cnorm_log(self): + img = Image(np.array([[0, 1], [2, 3]])).options(cnorm='log') + plot = bokeh_renderer.get_plot(img) + cmapper = plot.handles['color_mapper'] + self.assertTrue(cmapper, LogColorMapper) + + def test_colormapper_cnorm_eqhist(self): + try: + from bokeh.models import EqHistColorMapper + except: + raise SkipTest("Option cnorm='eq_hist' requires EqHistColorMapper") + img = Image(np.array([[0, 1], [2, 3]])).options(cnorm='eq_hist') + plot = bokeh_renderer.get_plot(img) + cmapper = plot.handles['color_mapper'] + self.assertTrue(cmapper, EqHistColorMapper) + + def test_colormapper_min_max_colors(self): img = Image(np.array([[0, 1], [2, 3]])).options(clipping_colors={'min': 'red', 'max': 'blue'}) plot = bokeh_renderer.get_plot(img) From 00dae036dc6af181e616b353ea2f3bf59957619e Mon Sep 17 00:00:00 2001 From: jlstevens Date: Fri, 20 Nov 2020 02:57:23 +0100 Subject: [PATCH 28/98] Added description of cnorm option to style mapping user guide --- examples/user_guide/04-Style_Mapping.ipynb | 51 ++++++++++++++++++++-- 1 file changed, 48 insertions(+), 3 deletions(-) diff --git a/examples/user_guide/04-Style_Mapping.ipynb b/examples/user_guide/04-Style_Mapping.ipynb index 0dc2e73184..e7e8aa0db7 100644 --- a/examples/user_guide/04-Style_Mapping.ipynb +++ b/examples/user_guide/04-Style_Mapping.ipynb @@ -521,12 +521,57 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "By default (left plot above), the min and max values in the array map to the first color (white) and last color (dark blue) in the colormap, and NaNs are ``'transparent'`` (an RGBA tuple of (0, 0, 0, 0)), revealing the underlying plot background. When the specified `clipping_colors` are supplied (middle plot above), NaN values are now colored gray, but the plot is otherwise the same because the autoranging still ensures that no value is mapped outside the available color range. Finally, when the `z` range is reduced (right plot above), the color range is mapped from a different range of numerical `z` values, and some values now fall outside the range and are thus clipped to red or green as specified.\n", - " \n", - "#### Other options\n", + "By default (left plot above), the min and max values in the array map to the first color (white) and last color (dark blue) in the colormap, and NaNs are ``'transparent'`` (an RGBA tuple of (0, 0, 0, 0)), revealing the underlying plot background. When the specified `clipping_colors` are supplied (middle plot above), NaN values are now colored gray, but the plot is otherwise the same because the autoranging still ensures that no value is mapped outside the available color range. Finally, when the `z` range is reduced (right plot above), the color range is mapped from a different range of numerical `z` values, and some values now fall outside the range and are thus clipped to red or green as specified.\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Normalization modes\n", + "\n", + "There are three available color normalization or `cnorm` options:\n", + "\n", + "* `'linear'`: This corresponds to the simple linear mapping (used by default)\n", + "* `log`: This mode applies logarithmic colormapping.\n", + "* `eq_hist`: This mode applies histogram equalization\n", + "\n", + "The following cell defines an `Image` containing random samples drawn from a normal distribution (mean of 3) with a square of constant value 50 in the middle shown with the three `cnorm` modes.\n", + "\n", + "In the `'linear'` mode, the Gaussian noise is hard to see as the square outlier means the random values only use a small portion at the bottom of the colormap, resulting in a background that is almost uniformly yellow.\n", + "\n", + "In the `'log'` mode, the random values are a little easier to see but these samples still use a small portion of the colormap. Logarithmic colormaps are most useful when you know that you are plotting data with an approximately logarithmic distribution.\n", + "\n", + "In the `'eq_hist'` mode the colormap is distorted so that an approximately equal number of samples use a roughly equal portion of the colormap. In this mode the structure of the low amplitude noise is very clear though the non-linear distortion makes this mode more difficult to express in simple terms than the linear and log modes." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "np.random.seed(42)\n", + "data = np.random.normal(loc=3, scale=0.3, size=(100,100))\n", + "print(\"Mean values of random samples is {mean:.3f} \".format(mean=np.mean(data))\n", + " + \"which is much lower than the black square in the center (value 50)\")\n", + "data[45:55,45:55] = 50\n", + "\n", + "pattern = hv.Image(data)\n", + "(pattern.options(cnorm='linear', title='linear', colorbar=True) \n", + " + pattern.options(cnorm='log', title='log', colorbar=True)\n", + " + pattern.options(cnorm='eq_hist', title='eq_hist', colorbar=True))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Other colormapping options\n", "\n", "* ``clim_percentile``: Percentile value to compute colorscale robust to outliers. If `True` uses 2nd and 98th percentile, otherwise uses the specified percentile value. \n", "* ``cnorm``: Color normalization to be applied during colormapping. Allows switching between 'linear', 'log' and 'eqhist'.\n", + "* ``logz``: Enable logarithmic color scale (Will be deprecated in future in favor of `cnorm='log'`)\n", "* ``symmetric``: Ensures that the color scale is centered on zero (e.g. ``symmetric=True``)" ] }, From 9d64c7777a870682efdb5a332490912707722107 Mon Sep 17 00:00:00 2001 From: jlstevens Date: Fri, 20 Nov 2020 13:05:45 +0100 Subject: [PATCH 29/98] Raising more helpful import error when EqHistColorMapper unavailable --- holoviews/plotting/bokeh/element.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/holoviews/plotting/bokeh/element.py b/holoviews/plotting/bokeh/element.py index 7fdecfce39..b86154a589 100644 --- a/holoviews/plotting/bokeh/element.py +++ b/holoviews/plotting/bokeh/element.py @@ -1955,7 +1955,12 @@ def _get_cmapper_opts(self, low, high, factors, colors): "the `clim` option." ) elif self.cnorm == 'eq_hist': - from bokeh.models import EqHistColorMapper + try: + from bokeh.models import EqHistColorMapper + except ImportError: + raise ImportError("Could not import bokeh.models.EqHistColorMapper. " + "Note that the option cnorm='eq_hist' requires " + "bokeh 2.2.3 or higher.") colormapper = EqHistColorMapper if isinstance(low, (bool, np.bool_)): low = int(low) if isinstance(high, (bool, np.bool_)): high = int(high) From 84cb40850bd56c1c3e509d310635f8bbf2a4a5bc Mon Sep 17 00:00:00 2001 From: jlstevens Date: Fri, 20 Nov 2020 13:06:09 +0100 Subject: [PATCH 30/98] Fixed apply_nodata utility to handle unsigned ints --- holoviews/plotting/util.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/holoviews/plotting/util.py b/holoviews/plotting/util.py index 0269f7de1d..3c38be131c 100644 --- a/holoviews/plotting/util.py +++ b/holoviews/plotting/util.py @@ -1098,7 +1098,7 @@ def hex2rgb(hex): def apply_nodata(opts, data): "Replace `nodata` value in data with NaN, if specified in opts" nodata = opts.kwargs.get('nodata') - if nodata is not None and (data.dtype.kind == 'i'): + if nodata is not None and (data.dtype.kind in ['i', 'u']): data = data.astype(np.float64) data[data == nodata] = np.NaN return data From 5dd266d82423def85906d3870f00859ede551bdc Mon Sep 17 00:00:00 2001 From: jlstevens Date: Fri, 20 Nov 2020 14:36:28 +0100 Subject: [PATCH 31/98] Updated the Large Data user guide with cnorm and nodata example --- examples/user_guide/15-Large_Data.ipynb | 39 +++++++++++++------------ 1 file changed, 21 insertions(+), 18 deletions(-) diff --git a/examples/user_guide/15-Large_Data.ipynb b/examples/user_guide/15-Large_Data.ipynb index 9422bc4e13..731ef62761 100644 --- a/examples/user_guide/15-Large_Data.ipynb +++ b/examples/user_guide/15-Large_Data.ipynb @@ -390,15 +390,15 @@ "Note that the above plot will look blocky in a static export (such as on anaconda.org), because the exported version is generated without taking the size of the actual plot (using default height and width for Datashader) into account, whereas the live notebook automatically regenerates the plot to match the visible area on the page. The result of all these operations can be laid out, overlaid, selected, and sampled just like any other HoloViews element, letting you work naturally with even very large datasets.\n", "\n", "\n", - "# Hover info\n", + "# Hover info and colorbars\n", "\n", - "As you can see in the examples above, converting the data to an image using Datashader makes it feasible to work with even very large datasets interactively. One unfortunate side effect is that the original datapoints and line segments can no longer be used to support \"tooltips\" or \"hover\" information directly for RGB images generated with `datashade`; that data simply is not present at the browser level, and so the browser cannot unambiguously report information about any specific datapoint. \n", + "Prior to HoloViews 1.14 there was a tradeoff between using `datashade` and `rasterize`:\n", "\n", - "If you do need hover information, there are two good options available:\n", + "* `datashade` would apply histogram equalization by default and map missing values to zero alpha pixels (i.e turn missing data transparent). The downside was that the RGB output could not support colorbars or Bokeh hover.\n", "\n", - "1) Use the ``rasterize`` operation without `shade`, which will let the plotting code handle the conversion to colors while still having the actual aggregated data to support hovering\n", + "* `rasterize` could support client-side color mapping including colorbars and hover information displaying the true aggregated values but could not support histogram equalization or easily map missing values to transparency.\n", "\n", - "2) Overlay a separate layer as a ``QuadMesh`` or ``Image`` containing the hover information" + "As of version HoloViews 1.14, `rasterize` can be made strictly superior to `datashade` by setting a few options namely `cnorm` and `nodata`:" ] }, { @@ -407,26 +407,29 @@ "metadata": {}, "outputs": [], "source": [ - "from holoviews.streams import RangeXY\n", + "import matplotlib as mpl\n", + "from matplotlib import cm\n", + "shade_cmap = mpl.colors.LinearSegmentedColormap.from_list(\"shade_cmap\", [\"lightblue\", \"darkblue\"])\n", "\n", - "rasterized = rasterize(points, width=400, height=400)\n", - "\n", - "fixed_hover = (datashade(points, width=400, height=400) * \n", - " hv.QuadMesh(rasterize(points, width=10, height=10, dynamic=False)))\n", - "\n", - "dynamic_hover = (datashade(points, width=400, height=400) * \n", - " rasterize(points, width=10, height=10, streams=[RangeXY]).apply(hv.QuadMesh))\n", - "\n", - "(rasterized + fixed_hover + dynamic_hover).opts(\n", - " opts.QuadMesh(tools=['hover'], alpha=0, hover_alpha=0.2), \n", - " opts.Image(tools=['hover']))" + "rasterized = rasterize(points, dynamic=False).opts(cnorm='eq_hist', nodata=0, cmap=shade_cmap, \n", + " colorbar=True, tools=['hover'], width=400)\n", + "datashaded = datashade(points, dynamic=False).opts(width=350)\n", + "datashaded.opts(title='datashade') + rasterized.opts(title='rasterized equivalent')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "In the above examples, the plot on the left provides hover information directly on the aggregated ``Image``. The middle plot displays hover information as a ``QuadMesh`` at a fixed spatial scale, while the one on the right reports on an area that scales with the zoom level so that arbitrarily small regions of data space can be examined, which is generally more useful (but requires a live Python server)." + "Now `rasterize` version is better than the `datashade` equivalent in every respect:\n", + "\n", + "* The Bokeh hover tool and colorbars are now supported.\n", + "* The data in the `Image` elements returned by `rasterize` are now useful aggregates ('Counts' by default) instead of meaningless RGB values.\n", + "* Although slightly longer to specify, the `rasterize` options make the histogram equalization and transparent values explicit.\n", + "\n", + "The `nodata` and `cnorm` options works across all backends except for the Plotly backend where `cnorm` is currently not supported. To use `cnorm='eq_hist'` as in the above example, you will need Bokeh version 2.2.3 or greater.\n", + "\n", + "If you can satisfy these version requirements, the use of the `rasterize` operation is now encouraged over `datashade` operation in all cases." ] }, { From be749925e5435d180a1b592bd8834652dbe5adcc Mon Sep 17 00:00:00 2001 From: jlstevens Date: Fri, 20 Nov 2020 14:47:16 +0100 Subject: [PATCH 32/98] Added unit tests for nodata when data is unsigned int --- holoviews/tests/plotting/bokeh/testquadmeshplot.py | 10 ++++++++++ holoviews/tests/plotting/bokeh/testrasterplot.py | 10 ++++++++++ .../tests/plotting/matplotlib/testquadmeshplot.py | 8 ++++++++ holoviews/tests/plotting/matplotlib/testrasterplot.py | 10 ++++++++++ holoviews/tests/plotting/plotly/testimageplot.py | 7 +++++++ holoviews/tests/plotting/plotly/testquadmeshplot.py | 7 +++++++ 6 files changed, 52 insertions(+) diff --git a/holoviews/tests/plotting/bokeh/testquadmeshplot.py b/holoviews/tests/plotting/bokeh/testquadmeshplot.py index 78464e1daa..2e23ec1799 100644 --- a/holoviews/tests/plotting/bokeh/testquadmeshplot.py +++ b/holoviews/tests/plotting/bokeh/testquadmeshplot.py @@ -57,4 +57,14 @@ def test_quadmesh_nodata(self): qmesh = QuadMesh((xs, ys, data)).opts(nodata=0) plot = bokeh_renderer.get_plot(qmesh) source = plot.handles['source'] + self.assertEqual(source.data['z'], flattened) + + def test_quadmesh_nodata_uint(self): + xs = [0, 1, 2] + ys = [2, 1, 0] + data = np.array([[0,1,2], [3,4,5], [6,7,8]], dtype='uint32') + flattened = np.array([6, 3, np.NaN, 7, 4, 1, 8, 5, 2]) + qmesh = QuadMesh((xs, ys, data)).opts(nodata=0) + plot = bokeh_renderer.get_plot(qmesh) + source = plot.handles['source'] self.assertEqual(source.data['z'], flattened) \ No newline at end of file diff --git a/holoviews/tests/plotting/bokeh/testrasterplot.py b/holoviews/tests/plotting/bokeh/testrasterplot.py index be2636b636..a948451dc9 100644 --- a/holoviews/tests/plotting/bokeh/testrasterplot.py +++ b/holoviews/tests/plotting/bokeh/testrasterplot.py @@ -31,6 +31,16 @@ def test_nodata_array(self): self.assertEqual(source.data['image'][0], np.array([[2, np.NaN], [np.NaN, 1]])) + def test_nodata_array_uint(self): + img = Image(np.array([[0, 1], [2, 0]], dtype='uint32')).opts(nodata=0) + plot = bokeh_renderer.get_plot(img) + cmapper = plot.handles['color_mapper'] + source = plot.handles['source'] + self.assertEqual(cmapper.low, 0) + self.assertEqual(cmapper.high, 2) + self.assertEqual(source.data['image'][0], + np.array([[2, np.NaN], [np.NaN, 1]])) + def test_raster_invert_axes(self): arr = np.array([[0, 1, 2], [3, 4, 5]]) raster = Raster(arr).opts(plot=dict(invert_axes=True)) diff --git a/holoviews/tests/plotting/matplotlib/testquadmeshplot.py b/holoviews/tests/plotting/matplotlib/testquadmeshplot.py index 102e709551..f2f20986e9 100644 --- a/holoviews/tests/plotting/matplotlib/testquadmeshplot.py +++ b/holoviews/tests/plotting/matplotlib/testquadmeshplot.py @@ -22,6 +22,14 @@ def test_quadmesh_nodata(self): self.assertEqual(artist.get_array().data, np.array([3, 4, 5, np.NaN, 1, 2])) + def test_quadmesh_nodata_uint(self): + arr = np.array([[0, 1, 2], [3, 4, 5]], dtype='uint32') + qmesh = QuadMesh(Image(arr)).opts(nodata=0) + plot = mpl_renderer.get_plot(qmesh) + artist = plot.handles['artist'] + self.assertEqual(artist.get_array().data, + np.array([3, 4, 5, np.NaN, 1, 2])) + def test_quadmesh_update_cbar(self): xs = ys = np.linspace(0, 6, 10) zs = np.linspace(1, 2, 5) diff --git a/holoviews/tests/plotting/matplotlib/testrasterplot.py b/holoviews/tests/plotting/matplotlib/testrasterplot.py index bf05eaa8fa..65fd66a655 100644 --- a/holoviews/tests/plotting/matplotlib/testrasterplot.py +++ b/holoviews/tests/plotting/matplotlib/testrasterplot.py @@ -30,6 +30,16 @@ def test_raster_nodata(self): artist = plot.handles['artist'] self.assertEqual(artist.get_array().data, expected) + def test_raster_nodata_uint(self): + arr = np.array([[0, 1, 2], [3, 4, 5]], dtype='uint32') + expected = np.array([[3, 4, 5], + [np.NaN, 1, 2]]) + + raster = Raster(arr).opts(nodata=0) + plot = mpl_renderer.get_plot(raster) + artist = plot.handles['artist'] + self.assertEqual(artist.get_array().data, expected) + def test_image_invert_axes(self): arr = np.array([[0, 1, 2], [3, 4, 5]]) diff --git a/holoviews/tests/plotting/plotly/testimageplot.py b/holoviews/tests/plotting/plotly/testimageplot.py index 97d71876d5..83f42388cb 100644 --- a/holoviews/tests/plotting/plotly/testimageplot.py +++ b/holoviews/tests/plotting/plotly/testimageplot.py @@ -27,6 +27,13 @@ def test_image_nodata(self): self.assertEqual(state['data'][0]['type'], 'heatmap') self.assertEqual(state['data'][0]['z'], np.array([[np.NaN, 1, 2], [2, 3, 4]])) + def test_image_nodata_unint(self): + img = Image(([1, 2, 3], [0, 1], np.array([[0, 1, 2], [2, 3, 4]], + dtype='uint32'))).opts(nodata=0) + state = self._get_plot_state(img) + self.assertEqual(state['data'][0]['type'], 'heatmap') + self.assertEqual(state['data'][0]['z'], np.array([[np.NaN, 1, 2], [2, 3, 4]])) + def test_image_state_inverted(self): img = Image(([1, 2, 3], [0, 1], np.array([[0, 1, 2], [2, 3, 4]]))).options( invert_axes=True) diff --git a/holoviews/tests/plotting/plotly/testquadmeshplot.py b/holoviews/tests/plotting/plotly/testquadmeshplot.py index 8413b8cf82..a841341e27 100644 --- a/holoviews/tests/plotting/plotly/testquadmeshplot.py +++ b/holoviews/tests/plotting/plotly/testquadmeshplot.py @@ -26,6 +26,13 @@ def test_quadmesh_nodata(self): self.assertEqual(state['data'][0]['type'], 'heatmap') self.assertEqual(state['data'][0]['z'], np.array([[np.NaN, 1, 2], [2, 3, 4]])) + def test_quadmesh_nodata_uint(self): + img = QuadMesh(([1, 2, 4], [0, 1], + np.array([[0, 1, 2], [2, 3, 4]], dtype='uint32'))).opts(nodata=0) + state = self._get_plot_state(img) + self.assertEqual(state['data'][0]['type'], 'heatmap') + self.assertEqual(state['data'][0]['z'], np.array([[np.NaN, 1, 2], [2, 3, 4]])) + def test_quadmesh_state_inverted(self): img = QuadMesh(([1, 2, 4], [0, 1], np.array([[0, 1, 2], [2, 3, 4]]))).options( invert_axes=True) From 969aa172c9badd2d3ce0d17022eb908c817b8fcf Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Fri, 20 Nov 2020 19:31:35 +0100 Subject: [PATCH 33/98] Apply nodata in Compositor --- holoviews/plotting/__init__.py | 19 ++++++++++++++-- holoviews/plotting/bokeh/raster.py | 11 ++++----- holoviews/plotting/mpl/raster.py | 17 ++++++-------- holoviews/plotting/plotly/raster.py | 7 ++---- holoviews/plotting/util.py | 35 ++++++++++++++++++++++++----- 5 files changed, 59 insertions(+), 30 deletions(-) diff --git a/holoviews/plotting/__init__.py b/holoviews/plotting/__init__.py index bde2521601..e561197972 100644 --- a/holoviews/plotting/__init__.py +++ b/holoviews/plotting/__init__.py @@ -8,13 +8,28 @@ from __future__ import absolute_import from ..core.options import Cycle, Compositor -from ..element import Area, Polygons +from ..element import Area, Image, QuadMesh, Polygons, Raster from ..element.sankey import _layout_sankey, Sankey from .plot import Plot from .renderer import Renderer, HTML_TAGS # noqa (API import) -from .util import list_cmaps # noqa (API import) +from .util import apply_nodata, list_cmaps # noqa (API import) from ..operation.stats import univariate_kde, bivariate_kde +Compositor.register(Compositor("Image", apply_nodata, None, + 'data', transfer_options=True, + transfer_parameters=True, + output_type=Image, + backends=['bokeh', 'matplotlib', 'plotly'])) +Compositor.register(Compositor("Raster", apply_nodata, None, + 'data', transfer_options=True, + transfer_parameters=True, + output_type=Raster, + backends=['bokeh', 'matplotlib', 'plotly'])) +Compositor.register(Compositor("QuadMesh", apply_nodata, None, + 'data', transfer_options=True, + transfer_parameters=True, + output_type=QuadMesh, + backends=['bokeh', 'matplotlib', 'plotly'])) Compositor.register(Compositor("Distribution", univariate_kde, None, 'data', transfer_options=True, transfer_parameters=True, diff --git a/holoviews/plotting/bokeh/raster.py b/holoviews/plotting/bokeh/raster.py index d258d5024c..fc9d857046 100644 --- a/holoviews/plotting/bokeh/raster.py +++ b/holoviews/plotting/bokeh/raster.py @@ -7,7 +7,6 @@ from ...core.util import cartesian_product, dimension_sanitizer, isfinite from ...element import Raster -from ..util import apply_nodata from .element import ElementPlot, ColorbarPlot from .selection import BokehOverlaySelectionDisplay from .styles import base_properties, fill_properties, line_properties, mpl_to_bokeh @@ -16,13 +15,13 @@ class RasterPlot(ColorbarPlot): + clipping_colors = param.Dict(default={'NaN': 'transparent'}) + nodata = param.Integer(default=None, doc=""" Optional missing-data value for integer data. If non-None, data with this value will be replaced with NaN so that it is transparent (by default) when plotted.""") - clipping_colors = param.Dict(default={'NaN': 'transparent'}) - padding = param.ClassSelector(default=0, class_=(int, float, tuple)) show_legend = param.Boolean(default=False, doc=""" @@ -123,7 +122,6 @@ def get_data(self, element, ranges, style): if self.invert_yaxis: img = img[::-1] key = 'image' if i == 2 else dimension_sanitizer(vdim.name) - img = apply_nodata(element.opts.get('plot', 'bokeh'), img) data[key] = [img] return (data, mapping, style) @@ -208,13 +206,13 @@ def get_data(self, element, ranges, style): class QuadMeshPlot(ColorbarPlot): + clipping_colors = param.Dict(default={'NaN': 'transparent'}) + nodata = param.Integer(default=None, doc=""" Optional missing-data value for integer data. If non-None, data with this value will be replaced with NaN so that it is transparent (by default) when plotted.""") - clipping_colors = param.Dict(default={'NaN': 'transparent'}) - padding = param.ClassSelector(default=0, class_=(int, float, tuple)) show_legend = param.Boolean(default=False, doc=""" @@ -250,7 +248,6 @@ def get_data(self, element, ranges, style): x, y = dimension_sanitizer(x.name), dimension_sanitizer(y.name) zdata = element.dimension_values(z, flat=False) - zdata = apply_nodata(element.opts.get('plot', 'bokeh'), zdata) if irregular: dims = element.kdims diff --git a/holoviews/plotting/mpl/raster.py b/holoviews/plotting/mpl/raster.py index 2a9594f27f..d080bf735a 100644 --- a/holoviews/plotting/mpl/raster.py +++ b/holoviews/plotting/mpl/raster.py @@ -7,7 +7,6 @@ from ...core import traversal from ...core.util import match_spec, max_range, unique_iterator from ...element.raster import Image, Raster, RGB -from ..util import apply_nodata from .element import ElementPlot, ColorbarPlot, OverlayPlot from .plot import MPLPlot, GridPlot, mpl_rc_context from .util import get_raster_array, mpl_version @@ -15,16 +14,16 @@ class RasterBasePlot(ElementPlot): - nodata = param.Integer(default=None, doc=""" - Optional missing-data value for integer data. - If non-None, data with this value will be replaced with NaN so - that it is transparent (by default) when plotted.""") - aspect = param.Parameter(default='equal', doc=""" Raster elements respect the aspect ratio of the Images by default but may be set to an explicit aspect ratio or to 'square'.""") + nodata = param.Integer(default=None, doc=""" + Optional missing-data value for integer data. + If non-None, data with this value will be replaced with NaN so + that it is transparent (by default) when plotted.""") + padding = param.ClassSelector(default=0, class_=(int, float, tuple)) show_legend = param.Boolean(default=False, doc=""" @@ -88,7 +87,6 @@ def get_data(self, element, ranges, style): style['extent'] = [l, r, b, t] style['origin'] = 'upper' - data = apply_nodata(element.opts.get('plot', 'matplotlib'), data) return [data], style, {'xticks': xticks, 'yticks': yticks} def update_handles(self, key, axis, element, ranges, style): @@ -133,13 +131,13 @@ def update_handles(self, key, axis, element, ranges, style): class QuadMeshPlot(ColorbarPlot): + clipping_colors = param.Dict(default={'NaN': 'transparent'}) + nodata = param.Integer(default=None, doc=""" Optional missing-data value for integer data. If non-None, data with this value will be replaced with NaN so that it is transparent (by default) when plotted.""") - clipping_colors = param.Dict(default={'NaN': 'transparent'}) - padding = param.ClassSelector(default=0, class_=(int, float, tuple)) show_legend = param.Boolean(default=False, doc=""" @@ -154,7 +152,6 @@ def get_data(self, element, ranges, style): zdata = element.dimension_values(2, flat=False) data = np.ma.array(zdata, mask=np.logical_not(np.isfinite(zdata))) - data = apply_nodata(element.opts.get('plot', 'matplotlib'), data) expanded = element.interface.irregular(element, element.kdims[0]) edges = style.get('shading') != 'gouraud' coords = [element.interface.coords(element, d, ordered=True, diff --git a/holoviews/plotting/plotly/raster.py b/holoviews/plotting/plotly/raster.py index 26e2701611..3e150abe44 100644 --- a/holoviews/plotting/plotly/raster.py +++ b/holoviews/plotting/plotly/raster.py @@ -6,19 +6,18 @@ from ...core.options import SkipRendering from ...element import Image, Raster from ..mixins import HeatMapMixin -from ..util import apply_nodata from .element import ColorbarPlot class RasterPlot(ColorbarPlot): - padding = param.ClassSelector(default=0, class_=(int, float, tuple)) - nodata = param.Integer(default=None, doc=""" Optional missing-data value for integer data. If non-None, data with this value will be replaced with NaN so that it is transparent (by default) when plotted.""") + padding = param.ClassSelector(default=0, class_=(int, float, tuple)) + style_opts = ['visible', 'cmap', 'alpha'] @classmethod @@ -48,7 +47,6 @@ def get_data(self, element, ranges, style, **kwargs): x0, y0, dx, dy = y0, x0, dy, dx array = array.T - array = apply_nodata(element.opts.get('plot', 'plotly'), array) return [dict(x0=x0, y0=y0, dx=dx, dy=dy, z=array)] @@ -127,5 +125,4 @@ def get_data(self, element, ranges, style, **kwargs): y, x = 'x', 'y' zdata = zdata.T - zdata = apply_nodata(element.opts.get('plot', 'plotly'), zdata) return [{x: xc, y: yc, 'z': zdata}] diff --git a/holoviews/plotting/util.py b/holoviews/plotting/util.py index 3c38be131c..4f446bfc01 100644 --- a/holoviews/plotting/util.py +++ b/holoviews/plotting/util.py @@ -13,6 +13,7 @@ from ..core import (HoloMap, DynamicMap, CompositeOverlay, Layout, Overlay, GridSpace, NdLayout, NdOverlay, AdjointLayout) from ..core.options import CallbackError, Cycle +from ..core.operation import Operation from ..core.ndmapping import item_check from ..core.spaces import get_nested_streams from ..core.util import (match_spec, wrap_tuple, basestring, get_overlay_spec, @@ -1095,16 +1096,38 @@ def hex2rgb(hex): # Pass 16 to the integer function for change of base return [int(hex[i:i+2], 16) for i in range(1,6,2)] -def apply_nodata(opts, data): + +def replace_value(data, value): "Replace `nodata` value in data with NaN, if specified in opts" - nodata = opts.kwargs.get('nodata') - if nodata is not None and (data.dtype.kind in ['i', 'u']): - data = data.astype(np.float64) - data[data == nodata] = np.NaN - return data + data[data == value] = np.NaN return data +class apply_nodata(Operation): + + nodata = param.Integer(default=None, doc=""" + Optional missing-data value for integer data. + If non-None, data with this value will be replaced with NaN so + that it is transparent (by default) when plotted.""") + + def _process(self, element, key=None): + if self.p.nodata is None: + return element + if hasattr(element, 'interface'): + vdim = element.vdims[0] + dtype = element.interface.dtype(element, vdim) + if dtype.kind not in 'iu': + return element + transform = dim(dim(vdim).astype('float64'), replace_value, self.p.nodata) + return element.transform(**{vdim.name: transform}) + else: + array = element.dimension_values(2) + if array.dtype.kind not in 'iu': + return element + array = array.astype('float64') + return element.clone(replace_value(array, self.p.nodata)) + + RGB_HEX_REGEX = re.compile(r'^#(?:[0-9a-fA-F]{3}){1,2}$') COLOR_ALIASES = { From f0b64c9bd63704a0c683a4b5e06e64902274d8d3 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Fri, 20 Nov 2020 19:46:11 +0100 Subject: [PATCH 34/98] Fix for xarray --- holoviews/plotting/util.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/holoviews/plotting/util.py b/holoviews/plotting/util.py index 4f446bfc01..f0bc51950d 100644 --- a/holoviews/plotting/util.py +++ b/holoviews/plotting/util.py @@ -1097,12 +1097,6 @@ def hex2rgb(hex): return [int(hex[i:i+2], 16) for i in range(1,6,2)] -def replace_value(data, value): - "Replace `nodata` value in data with NaN, if specified in opts" - data[data == value] = np.NaN - return data - - class apply_nodata(Operation): nodata = param.Integer(default=None, doc=""" @@ -1110,6 +1104,13 @@ class apply_nodata(Operation): If non-None, data with this value will be replaced with NaN so that it is transparent (by default) when plotted.""") + def _replace_value(self, data): + "Replace `nodata` value in data with NaN, if specified in opts" + data = data.astype('float64') + if hasattr(data, 'where'): + return data.where(data!=self.p.nodata, np.NaN) + return np.where(data==self.p.nodata, data, np.NaN) + def _process(self, element, key=None): if self.p.nodata is None: return element @@ -1118,7 +1119,7 @@ def _process(self, element, key=None): dtype = element.interface.dtype(element, vdim) if dtype.kind not in 'iu': return element - transform = dim(dim(vdim).astype('float64'), replace_value, self.p.nodata) + transform = dim(vdim, self._replace_value) return element.transform(**{vdim.name: transform}) else: array = element.dimension_values(2) From cdc7b8c6f6392a26e84a1a4ff731d7e89b29ff62 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Fri, 20 Nov 2020 19:51:58 +0100 Subject: [PATCH 35/98] Fix for Raster --- holoviews/plotting/util.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/holoviews/plotting/util.py b/holoviews/plotting/util.py index f0bc51950d..15915cb7af 100644 --- a/holoviews/plotting/util.py +++ b/holoviews/plotting/util.py @@ -1126,7 +1126,7 @@ def _process(self, element, key=None): if array.dtype.kind not in 'iu': return element array = array.astype('float64') - return element.clone(replace_value(array, self.p.nodata)) + return element.clone(self._replace_value(array)) RGB_HEX_REGEX = re.compile(r'^#(?:[0-9a-fA-F]{3}){1,2}$') From 03c6977c6c0cd37674e89d494bc66196aaa33069 Mon Sep 17 00:00:00 2001 From: jlstevens Date: Fri, 20 Nov 2020 21:57:06 +0100 Subject: [PATCH 36/98] Switched new rasterize example in large data to viridis --- examples/user_guide/15-Large_Data.ipynb | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/examples/user_guide/15-Large_Data.ipynb b/examples/user_guide/15-Large_Data.ipynb index 731ef62761..4775e8d9e5 100644 --- a/examples/user_guide/15-Large_Data.ipynb +++ b/examples/user_guide/15-Large_Data.ipynb @@ -407,13 +407,10 @@ "metadata": {}, "outputs": [], "source": [ - "import matplotlib as mpl\n", - "from matplotlib import cm\n", - "shade_cmap = mpl.colors.LinearSegmentedColormap.from_list(\"shade_cmap\", [\"lightblue\", \"darkblue\"])\n", - "\n", - "rasterized = rasterize(points, dynamic=False).opts(cnorm='eq_hist', nodata=0, cmap=shade_cmap, \n", - " colorbar=True, tools=['hover'], width=400)\n", - "datashaded = datashade(points, dynamic=False).opts(width=350)\n", + "palette = hv.plotting.util.mplcmap_to_palette('viridis', 256)\n", + "rasterized = rasterize(points, dynamic=False).opts(cnorm='eq_hist', nodata=0,\n", + " colorbar=True, tools=['hover'], width=400, cmap='viridis')\n", + "datashaded = datashade(points, dynamic=False, normalization='eq_hist', cmap=palette).opts(width=350)\n", "datashaded.opts(title='datashade') + rasterized.opts(title='rasterized equivalent')" ] }, From bca3321a61cd6a7bef14a57828791a4ecd4f4e2a Mon Sep 17 00:00:00 2001 From: "James A. Bednar" Date: Fri, 20 Nov 2020 16:35:52 -0600 Subject: [PATCH 37/98] Fixed error message formatting --- holoviews/element/graphs.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/holoviews/element/graphs.py b/holoviews/element/graphs.py index fe6f3ccb9f..537f08595d 100644 --- a/holoviews/element/graphs.py +++ b/holoviews/element/graphs.py @@ -569,7 +569,7 @@ def from_vertices(cls, data): try: from scipy.spatial import Delaunay except: - raise ImportError("Generating triangles from points requires, " + raise ImportError("Generating triangles from points requires " "SciPy to be installed.") if not isinstance(data, Points): data = Points(data) From c5e4de3acbb85ba69d03cceea6300d84f43949de Mon Sep 17 00:00:00 2001 From: "James A. Bednar" Date: Fri, 20 Nov 2020 17:03:33 -0600 Subject: [PATCH 38/98] Updated user guide --- examples/user_guide/04-Style_Mapping.ipynb | 43 +++++++++++++--------- examples/user_guide/15-Large_Data.ipynb | 6 +-- 2 files changed, 29 insertions(+), 20 deletions(-) diff --git a/examples/user_guide/04-Style_Mapping.ipynb b/examples/user_guide/04-Style_Mapping.ipynb index e7e8aa0db7..7d8d16c922 100644 --- a/examples/user_guide/04-Style_Mapping.ipynb +++ b/examples/user_guide/04-Style_Mapping.ipynb @@ -530,19 +530,13 @@ "source": [ "#### Normalization modes\n", "\n", - "There are three available color normalization or `cnorm` options:\n", + "There are three available color normalization or `cnorm` options to determine how numerical values are mapped to the range of colors in the colorbar:\n", "\n", - "* `'linear'`: This corresponds to the simple linear mapping (used by default)\n", - "* `log`: This mode applies logarithmic colormapping.\n", - "* `eq_hist`: This mode applies histogram equalization\n", + "* `'linear'`: Simple linear mapping (used by default)\n", + "* `log`: Logarithmic mapping\n", + "* `eq_hist`: Histogram-equalized mapping\n", "\n", - "The following cell defines an `Image` containing random samples drawn from a normal distribution (mean of 3) with a square of constant value 50 in the middle shown with the three `cnorm` modes.\n", - "\n", - "In the `'linear'` mode, the Gaussian noise is hard to see as the square outlier means the random values only use a small portion at the bottom of the colormap, resulting in a background that is almost uniformly yellow.\n", - "\n", - "In the `'log'` mode, the random values are a little easier to see but these samples still use a small portion of the colormap. Logarithmic colormaps are most useful when you know that you are plotting data with an approximately logarithmic distribution.\n", - "\n", - "In the `'eq_hist'` mode the colormap is distorted so that an approximately equal number of samples use a roughly equal portion of the colormap. In this mode the structure of the low amplitude noise is very clear though the non-linear distortion makes this mode more difficult to express in simple terms than the linear and log modes." + "The following cell defines an `Image` containing random samples drawn from a normal distribution (mean of 3) with a square of constant value 100 in the middle, shown with the three `cnorm` modes:" ] }, { @@ -553,14 +547,29 @@ "source": [ "np.random.seed(42)\n", "data = np.random.normal(loc=3, scale=0.3, size=(100,100))\n", - "print(\"Mean values of random samples is {mean:.3f} \".format(mean=np.mean(data))\n", - " + \"which is much lower than the black square in the center (value 50)\")\n", - "data[45:55,45:55] = 50\n", + "print(\"Mean value of random samples is {mean:.3f}, \".format(mean=np.mean(data))\n", + " + \"which is much lower\\nthan the black square in the center (value 100).\")\n", + "data[45:55,45:55] = 100\n", "\n", + "opts=dict(colorbar=True, xaxis='bare', yaxis='bare', height=160, width=200)\n", "pattern = hv.Image(data)\n", - "(pattern.options(cnorm='linear', title='linear', colorbar=True) \n", - " + pattern.options(cnorm='log', title='log', colorbar=True)\n", - " + pattern.options(cnorm='eq_hist', title='eq_hist', colorbar=True))" + "\n", + "( pattern.options(cnorm='linear', title='linear', **opts) \n", + " + pattern.options(cnorm='log', title='log', **opts)\n", + " + pattern.options(cnorm='eq_hist', title='eq_hist', **opts))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The `'linear'` mode is very easy to interpret numerically, with colors mapped to numerical values linearly as indicated. However, as you can see in this case, high-value outliers like the square here can make it difficult to see any structure in the remaining values. The Gaussian noise values all map to the first few colors at the bottom of the colormap, resulting in a background that is almost uniformly yellow even though we know the data includes a variety of different values in the background area.\n", + "\n", + "In the `'log'` mode, the random values are a little easier to see but these samples still use a small portion of the colormap. Logarithmic colormaps are most useful when you know that you are plotting data with an approximately logarithmic distribution.\n", + "\n", + "In the `'eq_hist'` mode colors are nonlinearly mapped according to the distribution of values in the plot, such that each color in the colormap represents an approximately equal number of values in the plot (with few or no colors reserved for the nearly empty range between 3 and 100). In this mode both the outliers and the overall low-amplitude noise can be seen clearly, but the non-linear distortion can make the colorbar more difficult to interpret.\n", + "\n", + "In practice, it is often a good idea to try all three of these modes with your data, using `eq_hist` to be sure that you are seeing all of the patterns in the data, then either `log` or `linear` (depending on which one is a better match to your distribution) with the values clipped to the range of values you want to show." ] }, { diff --git a/examples/user_guide/15-Large_Data.ipynb b/examples/user_guide/15-Large_Data.ipynb index 4775e8d9e5..44f4e34789 100644 --- a/examples/user_guide/15-Large_Data.ipynb +++ b/examples/user_guide/15-Large_Data.ipynb @@ -10,7 +10,7 @@ "\n", "Luckily, a visualization of even the largest dataset will be constrained by the resolution of your display device, and so one approach to handling such data is to pre-render or rasterize the data into a fixed-size array or image *before* sending it to the backend. The [Datashader](https://github.com/bokeh/datashader) library provides a high-performance big-data server-side rasterization pipeline that works seamlessly with HoloViews to support datasets that are orders of magnitude larger than those supported natively by the plotting-library backends, including millions or billions of points even on ordinary laptops.\n", "\n", - "Here, we will see how and when to use Datashader with HoloViews Elements and Containers. For simplicity in this discussion we'll focus on simple synthetic datasets, but the [Datashader docs](http://datashader.org/topics) include a wide variety of real datasets that give a much better idea of the power of using Datashader with HoloViews, and [PyViz.org](http://pyviz.org) shows how to install and work with HoloViews and Datashader together.\n", + "Here, we will see how and when to use Datashader with HoloViews Elements and Containers. For simplicity in this discussion we'll focus on simple synthetic datasets, but the [Datashader docs](http://datashader.org/topics) include a wide variety of real datasets that give a much better idea of the power of using Datashader with HoloViews, and [HoloViz.org](http://holoviz.org) shows how to install and work with HoloViews and Datashader together.\n", "\n", "" ] @@ -418,7 +418,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Now `rasterize` version is better than the `datashade` equivalent in every respect:\n", + "Now the `rasterize` version is better than the `datashade` equivalent in nearly every respect:\n", "\n", "* The Bokeh hover tool and colorbars are now supported.\n", "* The data in the `Image` elements returned by `rasterize` are now useful aggregates ('Counts' by default) instead of meaningless RGB values.\n", @@ -426,7 +426,7 @@ "\n", "The `nodata` and `cnorm` options works across all backends except for the Plotly backend where `cnorm` is currently not supported. To use `cnorm='eq_hist'` as in the above example, you will need Bokeh version 2.2.3 or greater.\n", "\n", - "If you can satisfy these version requirements, the use of the `rasterize` operation is now encouraged over `datashade` operation in all cases." + "If you can satisfy these version requirements, the `rasterize` operation is now encouraged over the `datashade` operation in all supported cases. Cases not yet supported include Datashader's rarely used `cbrt` (cube root) colormapping option, along with its [categorical color mixing](https://datashader.org/getting_started/Pipeline.html#Transformation) support (for which `shade()` or `datashade()` is still needed, and thus hover and colorbars will not be supported)." ] }, { From 8e5f7ce1cbd570a70b13187c41ffbdd78e4afbd9 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Mon, 23 Nov 2020 11:48:54 +0100 Subject: [PATCH 39/98] Fix inverted mask --- holoviews/plotting/util.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/holoviews/plotting/util.py b/holoviews/plotting/util.py index 15915cb7af..caaaca3b03 100644 --- a/holoviews/plotting/util.py +++ b/holoviews/plotting/util.py @@ -1107,9 +1107,10 @@ class apply_nodata(Operation): def _replace_value(self, data): "Replace `nodata` value in data with NaN, if specified in opts" data = data.astype('float64') + mask = data!=self.p.nodata if hasattr(data, 'where'): - return data.where(data!=self.p.nodata, np.NaN) - return np.where(data==self.p.nodata, data, np.NaN) + return data.where(mask, np.NaN) + return np.where(mask, data, np.NaN) def _process(self, element, key=None): if self.p.nodata is None: From 96dc926d2cabae38b42321b992971b01db507b7a Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Mon, 23 Nov 2020 13:07:11 +0100 Subject: [PATCH 40/98] Fixed unpacking on single element compositor --- holoviews/core/options.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/holoviews/core/options.py b/holoviews/core/options.py index 8dde88f9d9..f953e4fc81 100644 --- a/holoviews/core/options.py +++ b/holoviews/core/options.py @@ -928,6 +928,8 @@ def collapse_element(cls, overlay, ranges=None, mode='data', backend=None): sliced = overlay.clone(values[start:stop]) items = sliced.traverse(lambda x: x, [Element]) if applicable_op and all(el in processed[applicable_op] for el in items): + if unpack and len(overlay) == 1: + return overlay.values()[0] return overlay result = applicable_op.apply(sliced, ranges, backend) if applicable_op.group: From 28623b9c72f71b10febb344b876e61438141ac80 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Mon, 23 Nov 2020 13:13:48 +0100 Subject: [PATCH 41/98] Update tests --- .../tests/plotting/bokeh/testhextilesplot.py | 24 +++++++++---------- .../tests/plotting/bokeh/testrasterplot.py | 4 ++-- holoviews/tests/plotting/bokeh/testsankey.py | 4 ++-- 3 files changed, 16 insertions(+), 16 deletions(-) diff --git a/holoviews/tests/plotting/bokeh/testhextilesplot.py b/holoviews/tests/plotting/bokeh/testhextilesplot.py index a569148e77..82d2098524 100644 --- a/holoviews/tests/plotting/bokeh/testhextilesplot.py +++ b/holoviews/tests/plotting/bokeh/testhextilesplot.py @@ -47,57 +47,57 @@ def setUp(self): def test_hex_tiles_empty(self): tiles = HexTiles([]) - plot = list(bokeh_renderer.get_plot(tiles).subplots.values())[0] + plot = bokeh_renderer.get_plot(tiles) self.assertEqual(plot.handles['source'].data, {'q': [], 'r': []}) def test_hex_tiles_only_nans(self): tiles = HexTiles([(np.NaN, 0), (1, np.NaN)]) - plot = list(bokeh_renderer.get_plot(tiles).subplots.values())[0] + plot = bokeh_renderer.get_plot(tiles) self.assertEqual(plot.handles['source'].data, {'q': [], 'r': []}) def test_hex_tiles_zero_min_count(self): tiles = HexTiles([(0, 0), (0.5, 0.5), (-0.5, -0.5), (-0.4, -0.4)]).options(min_count=0) - plot = list(bokeh_renderer.get_plot(tiles).subplots.values())[0] + plot = bokeh_renderer.get_plot(tiles) cmapper = plot.handles['color_mapper'] self.assertEqual(cmapper.low, 0) self.assertEqual(plot.state.background_fill_color, cmapper.palette[0]) def test_hex_tiles_gridsize_tuple(self): tiles = HexTiles([(0, 0), (0.5, 0.5), (-0.5, -0.5), (-0.4, -0.4)]).options(gridsize=(5, 10)) - plot = list(bokeh_renderer.get_plot(tiles).subplots.values())[0] + plot = bokeh_renderer.get_plot(tiles) glyph = plot.handles['glyph'] self.assertEqual(glyph.size, 0.066666666666666666) self.assertEqual(glyph.aspect_scale, 0.5) def test_hex_tiles_gridsize_tuple_flat_orientation(self): tiles = HexTiles([(0, 0), (0.5, 0.5), (-0.5, -0.5), (-0.4, -0.4)]).options(gridsize=(5, 10), orientation='flat') - plot = list(bokeh_renderer.get_plot(tiles).subplots.values())[0] + plot = bokeh_renderer.get_plot(tiles) glyph = plot.handles['glyph'] self.assertEqual(glyph.size, 0.13333333333333333) self.assertEqual(glyph.aspect_scale, 0.5) def test_hex_tiles_scale(self): tiles = HexTiles([(0, 0), (0.5, 0.5), (-0.5, -0.5), (-0.4, -0.4)]).options(size_index=2, gridsize=3) - plot = list(bokeh_renderer.get_plot(tiles).subplots.values())[0] + plot = bokeh_renderer.get_plot(tiles) source = plot.handles['source'] self.assertEqual(source.data['scale'], np.array([0.45, 0.45, 0.9])) def test_hex_tiles_scale_all_equal(self): tiles = HexTiles([(0, 0), (0.5, 0.5), (-0.5, -0.5), (-0.4, -0.4)]).options(size_index=2) - plot = list(bokeh_renderer.get_plot(tiles).subplots.values())[0] + plot = bokeh_renderer.get_plot(tiles) source = plot.handles['source'] self.assertEqual(source.data['scale'], np.array([0.9, 0.9, 0.9, 0.9])) def test_hex_tiles_hover_count(self): tiles = HexTiles([(0, 0), (0.5, 0.5), (-0.5, -0.5), (-0.4, -0.4)]).options(tools=['hover']) - plot = list(bokeh_renderer.get_plot(tiles).subplots.values())[0] + plot = bokeh_renderer.get_plot(tiles) dims, opts = plot._hover_opts(tiles) self.assertEqual(dims, [Dimension('Count')]) self.assertEqual(opts, {}) def test_hex_tiles_hover_weighted(self): tiles = HexTiles([(0, 0, 0.1), (0.5, 0.5, 0.2), (-0.5, -0.5, 0.3)], vdims='z').options(aggregator=np.mean) - plot = list(bokeh_renderer.get_plot(tiles).subplots.values())[0] + plot = bokeh_renderer.get_plot(tiles) dims, opts = plot._hover_opts(tiles) self.assertEqual(dims, [Dimension('z')]) self.assertEqual(opts, {}) @@ -108,18 +108,18 @@ def test_hex_tiles_hover_weighted(self): def test_hex_tile_line_width_op(self): hextiles = HexTiles(np.random.randn(1000, 2)).options(line_width='Count') - plot = list(bokeh_renderer.get_plot(hextiles).subplots.values())[0] + plot = bokeh_renderer.get_plot(hextiles) glyph = plot.handles['glyph'] self.assertEqual(glyph.line_width, {'field': 'line_width'}) def test_hex_tile_alpha_op(self): hextiles = HexTiles(np.random.randn(1000, 2)).options(alpha='Count') - plot = list(bokeh_renderer.get_plot(hextiles).subplots.values())[0] + plot = bokeh_renderer.get_plot(hextiles) glyph = plot.handles['glyph'] self.assertEqual(glyph.fill_alpha, {'field': 'alpha'}) def test_hex_tile_scale_op(self): hextiles = HexTiles(np.random.randn(1000, 2)).options(scale='Count') - plot = list(bokeh_renderer.get_plot(hextiles).subplots.values())[0] + plot = bokeh_renderer.get_plot(hextiles) glyph = plot.handles['glyph'] self.assertEqual(glyph.scale, {'field': 'scale'}) diff --git a/holoviews/tests/plotting/bokeh/testrasterplot.py b/holoviews/tests/plotting/bokeh/testrasterplot.py index a948451dc9..de2eb8bb31 100644 --- a/holoviews/tests/plotting/bokeh/testrasterplot.py +++ b/holoviews/tests/plotting/bokeh/testrasterplot.py @@ -26,7 +26,7 @@ def test_nodata_array(self): plot = bokeh_renderer.get_plot(img) cmapper = plot.handles['color_mapper'] source = plot.handles['source'] - self.assertEqual(cmapper.low, 0) + self.assertEqual(cmapper.low, 1) self.assertEqual(cmapper.high, 2) self.assertEqual(source.data['image'][0], np.array([[2, np.NaN], [np.NaN, 1]])) @@ -36,7 +36,7 @@ def test_nodata_array_uint(self): plot = bokeh_renderer.get_plot(img) cmapper = plot.handles['color_mapper'] source = plot.handles['source'] - self.assertEqual(cmapper.low, 0) + self.assertEqual(cmapper.low, 1) self.assertEqual(cmapper.high, 2) self.assertEqual(source.data['image'][0], np.array([[2, np.NaN], [np.NaN, 1]])) diff --git a/holoviews/tests/plotting/bokeh/testsankey.py b/holoviews/tests/plotting/bokeh/testsankey.py index 68b863ec51..29ecd4e7d4 100644 --- a/holoviews/tests/plotting/bokeh/testsankey.py +++ b/holoviews/tests/plotting/bokeh/testsankey.py @@ -14,7 +14,7 @@ def test_sankey_simple(self): ('A', 'X', 5), ('A', 'Y', 7), ('A', 'Z', 6), ('B', 'X', 2), ('B', 'Y', 9), ('B', 'Z', 4)] ) - plot = list(bokeh_renderer.get_plot(sankey).subplots.values())[0] + plot = bokeh_renderer.get_plot(sankey) scatter_source = plot.handles['scatter_1_source'] quad_source = plot.handles['quad_1_source'] text_source = plot.handles['text_1_source'] @@ -56,7 +56,7 @@ def test_sankey_label_index(self): (1, 2, 2), (1, 3, 9), (1, 4, 4)], Dataset(enumerate('ABXYZ'), 'index', 'label')) ).options(label_index='label', tools=['hover']) - plot = list(bokeh_renderer.get_plot(sankey).subplots.values())[0] + plot = bokeh_renderer.get_plot(sankey) scatter_source = plot.handles['scatter_1_source'] text_source = plot.handles['text_1_source'] From 715a3995f119107891f7dc4d69d84b100e018eb5 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Mon, 23 Nov 2020 13:29:34 +0100 Subject: [PATCH 42/98] Further test fixes --- holoviews/plotting/util.py | 2 +- holoviews/tests/plotting/matplotlib/testsankey.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/holoviews/plotting/util.py b/holoviews/plotting/util.py index caaaca3b03..4abbcc34cd 100644 --- a/holoviews/plotting/util.py +++ b/holoviews/plotting/util.py @@ -1123,7 +1123,7 @@ def _process(self, element, key=None): transform = dim(vdim, self._replace_value) return element.transform(**{vdim.name: transform}) else: - array = element.dimension_values(2) + array = element.dimension_values(2, flat=False) if array.dtype.kind not in 'iu': return element array = array.astype('float64') diff --git a/holoviews/tests/plotting/matplotlib/testsankey.py b/holoviews/tests/plotting/matplotlib/testsankey.py index 894f4a6bf2..c75211cc63 100644 --- a/holoviews/tests/plotting/matplotlib/testsankey.py +++ b/holoviews/tests/plotting/matplotlib/testsankey.py @@ -13,7 +13,7 @@ def test_sankey_simple(self): ('A', 'X', 5), ('A', 'Y', 7), ('A', 'Z', 6), ('B', 'X', 2), ('B', 'Y', 9), ('B', 'Z', 4)] ) - plot = list(mpl_renderer.get_plot(sankey).subplots.values())[0] + plot = mpl_renderer.get_plot(sankey) rects = plot.handles['rects'] labels = plot.handles['labels'] @@ -45,7 +45,7 @@ def test_sankey_label_index(self): (1, 2, 2), (1, 3, 9), (1, 4, 4)], Dataset(enumerate('ABXYZ'), 'index', 'label')) ).options(label_index='label') - plot = list(mpl_renderer.get_plot(sankey).subplots.values())[0] + plot = mpl_renderer.get_plot(sankey) labels = plot.handles['labels'] text_data = {'x': np.array([18.75, 18.75, 1003.75, 1003.75, 1003.75]), From b7b5cc58e36b378e84a613232d3b999f6a3e74e1 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Mon, 23 Nov 2020 13:32:12 +0100 Subject: [PATCH 43/98] Further test fixes --- holoviews/core/options.py | 2 +- holoviews/plotting/util.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/holoviews/core/options.py b/holoviews/core/options.py index f953e4fc81..b707cbf8e6 100644 --- a/holoviews/core/options.py +++ b/holoviews/core/options.py @@ -1098,7 +1098,7 @@ def apply(self, value, input_ranges, backend=None): if k in self.operation.param}) transformed = self.operation(value, input_ranges=input_ranges, **kwargs) - if self.transfer_options: + if self.transfer_options and value is not transformed: Store.transfer_options(value, transformed, backend) return transformed diff --git a/holoviews/plotting/util.py b/holoviews/plotting/util.py index 4abbcc34cd..63f271327f 100644 --- a/holoviews/plotting/util.py +++ b/holoviews/plotting/util.py @@ -1123,7 +1123,7 @@ def _process(self, element, key=None): transform = dim(vdim, self._replace_value) return element.transform(**{vdim.name: transform}) else: - array = element.dimension_values(2, flat=False) + array = element.dimension_values(2, flat=False).T if array.dtype.kind not in 'iu': return element array = array.astype('float64') From caaee00e6bb9adfc23463098346efae96974e918 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Wed, 25 Nov 2020 14:47:23 +0100 Subject: [PATCH 44/98] Implement Dimension.nodata support --- holoviews/core/data/__init__.py | 6 +- holoviews/core/data/xarray.py | 8 +- holoviews/core/dimension.py | 26 ++-- holoviews/operation/datashader.py | 44 +++---- holoviews/tests/operation/testdatashader.py | 133 ++++++++++---------- 5 files changed, 114 insertions(+), 103 deletions(-) diff --git a/holoviews/core/data/__init__.py b/holoviews/core/data/__init__.py index 1f1600cd87..97207a5eff 100644 --- a/holoviews/core/data/__init__.py +++ b/holoviews/core/data/__init__.py @@ -1067,8 +1067,10 @@ def dimension_values(self, dimension, expanded=True, flat=True): NumPy array of values along the requested dimension """ dim = self.get_dimension(dimension, strict=True) - return self.interface.values(self, dim, expanded, flat) - + values = self.interface.values(self, dim, expanded, flat) + if dim.nodata is not None: + values = np.where(values==dim.nodata, np.NaN, values) + return values def get_dimension_type(self, dim): """Get the type of the requested dimension. diff --git a/holoviews/core/data/xarray.py b/holoviews/core/data/xarray.py index de69df791f..70c593f0f2 100644 --- a/holoviews/core/data/xarray.py +++ b/holoviews/core/data/xarray.py @@ -19,7 +19,7 @@ def is_cupy(array): if 'cupy' not in sys.modules: return False from cupy import ndarray - return isinstance(array, ndarray) + return isinstance(array, ndarray) class XArrayInterface(GridInterface): @@ -86,7 +86,8 @@ def retrieve_unit_and_label(dim): spec = (dim.name, coord.attrs['long_name']) else: spec = (dim.name, dim.label) - return dim.clone(spec, unit=unit) + nodata = coord.attrs.get('NODATA') + return dim.clone(spec, unit=unit, nodata=nodata) packed = False if isinstance(data, xr.DataArray): @@ -99,8 +100,9 @@ def retrieve_unit_and_label(dim): elif data.name: vdim = Dimension(data.name) vdim.unit = data.attrs.get('units') + vdim.nodata = data.attrs.get('NODATA') label = data.attrs.get('long_name') - if label is not None: + if 'long_name' in data.attrs: vdim.label = label elif len(vdim_param.default) == 1: vdim = vdim_param.default[0] diff --git a/holoviews/core/dimension.py b/holoviews/core/dimension.py index db4d95c104..afd0c979c4 100644 --- a/holoviews/core/dimension.py +++ b/holoviews/core/dimension.py @@ -191,8 +191,13 @@ class Dimension(param.Parameterized): maximum allowed value (defined by the range parameter) is continuous with the minimum allowed value.""") - value_format = param.Callable(default=None, doc=""" - Formatting function applied to each value before display.""") + default = param.Parameter(default=None, doc=""" + Default value of the Dimension which may be useful for widget + or other situations that require an initial or default value.""") + + nodata = param.Integer(default=None, doc=""" + Optional missing-data value for integer data. + If non-None, data with this value will be replaced with NaN.""") range = param.Tuple(default=(None, None), doc=""" Specifies the minimum and maximum allowed values for a @@ -202,25 +207,24 @@ class Dimension(param.Parameterized): Specifies a minimum and maximum reference value, which may be overridden by the data.""") - type = param.Parameter(default=None, doc=""" - Optional type associated with the Dimension values. The type - may be an inbuilt constructor (such as int, str, float) or a - custom class object.""") - - default = param.Parameter(default=None, doc=""" - Default value of the Dimension which may be useful for widget - or other situations that require an initial or default value.""") - step = param.Number(default=None, doc=""" Optional floating point step specifying how frequently the underlying space should be sampled. May be used to define a discrete sampling over the range.""") + type = param.Parameter(default=None, doc=""" + Optional type associated with the Dimension values. The type + may be an inbuilt constructor (such as int, str, float) or a + custom class object.""") + unit = param.String(default=None, allow_None=True, doc=""" Optional unit string associated with the Dimension. For instance, the string 'm' may be used represent units of meters and 's' to represent units of seconds.""") + value_format = param.Callable(default=None, doc=""" + Formatting function applied to each value before display.""") + values = param.List(default=[], doc=""" Optional specification of the allowed value set for the dimension that may also be used to retain a categorical diff --git a/holoviews/operation/datashader.py b/holoviews/operation/datashader.py index fad789fc39..dfce57f4f1 100644 --- a/holoviews/operation/datashader.py +++ b/holoviews/operation/datashader.py @@ -303,12 +303,17 @@ def _get_agg_params(self, element, x, y, agg_fn, bounds): raise ValueError("Aggregation column '%s' not found on '%s' element. " "Ensure the aggregator references an existing " "dimension." % (column,element)) - name = '%s Count' % column if isinstance(agg_fn, ds.count_cat) else column - vdims = [dims[0].clone(name)] + if isinstance(agg_fn, ds.count_cat): + vdims = dims[0].clone('%s Count' % column, nodata=0) + else: + vdims = dims[0].clone(column) elif category: - vdims = Dimension('%s Count' % category) + agg_name = type(agg_fn).__name__.title() + vdims = Dimension('%s %s' % (category, agg_name)) + if agg_name == 'Count': + vdims.nodata = 0 else: - vdims = Dimension('Count') + vdims = Dimension('Count', nodata=0) params['vdims'] = vdims return params @@ -615,8 +620,7 @@ def _process(self, element, key=None): cvs = ds.Canvas(plot_width=width, plot_height=height, x_range=x_range, y_range=y_range) - params = dict(get_param_values(element), kdims=[x, y], vdims=vdim, - datatype=['xarray'], bounds=(x0, y0, x1, y1)) + params = self._get_agg_params(element, x, y, agg_fn, (x0, y0, x1, y1)) if width == 0 or height == 0: return self._empty_agg(element, x, y, width, height, xs, ys, agg_fn, **params) @@ -705,13 +709,7 @@ def _process(self, element, key=None): if xtype == 'datetime': df[x.name] = df[x.name].astype('datetime64[us]').astype('int64') - if isinstance(agg_fn, (ds.count, ds.any)): - vdim = type(agg_fn).__name__ - else: - vdim = element.get_dimension(agg_fn.column) - - params = dict(get_param_values(element), kdims=[x, y], vdims=vdim, - datatype=['xarray'], bounds=(x0, y0, x1, y1)) + params = self._get_agg_params(element, x, y, agg_fn, (x0, y0, x1, y1)) if width == 0 or height == 0: return self._empty_agg(element, x, y, width, height, xs, ys, agg_fn, **params) @@ -752,18 +750,10 @@ def _process(self, element, key=None): df[y0d.name] = df[y0d.name].astype('datetime64[us]').astype('int64') df[y1d.name] = df[y1d.name].astype('datetime64[us]').astype('int64') - if isinstance(agg_fn, (ds.count, ds.any)): - vdim = type(agg_fn).__name__ - elif isinstance(agg_fn, ds.count_cat): - vdim = '%s Count' % agg_fn.column - else: - vdim = element.get_dimension(agg_fn.column) - if isinstance(agg_fn, ds.count_cat): df[agg_fn.column] = df[agg_fn.column].astype('category') - params = dict(get_param_values(element), kdims=[x0d, y0d], vdims=vdim, - datatype=['xarray'], bounds=(x0, y0, x1, y1)) + params = self._get_agg_params(element, x0d, y0d, agg_fn, (x0, y0, x1, y1)) if width == 0 or height == 0: return self._empty_agg(element, x0d, y0d, width, height, xs, ys, agg_fn, **params) @@ -1243,8 +1233,14 @@ def to_xarray(cls, element): return element data = tuple(element.dimension_values(kd, expanded=False) for kd in element.kdims) - data += tuple(element.dimension_values(vd, flat=False) - for vd in element.vdims) + vdims = list(element.vdims) + # Override nodata temporarily + element.vdims[:] = [vd.clone(nodata=None) for vd in element.vdims] + try: + data += tuple(element.dimension_values(vd, flat=False) + for vd in element.vdims) + finally: + element.vdims[:] = vdims dtypes = [dt for dt in element.datatype if dt != 'xarray'] return element.clone(data, datatype=['xarray']+dtypes, bounds=element.bounds, diff --git a/holoviews/tests/operation/testdatashader.py b/holoviews/tests/operation/testdatashader.py index ab32a1c1ea..eb04e8bc68 100644 --- a/holoviews/tests/operation/testdatashader.py +++ b/holoviews/tests/operation/testdatashader.py @@ -37,6 +37,11 @@ cudf_skip = skipIf(cudf is None, "cuDF not available") +import logging + +numba_logger = logging.getLogger('numba') +numba_logger.setLevel(logging.WARNING) + class DatashaderAggregateTests(ComparisonTestCase): """ @@ -48,7 +53,7 @@ def test_aggregate_points(self): img = aggregate(points, dynamic=False, x_range=(0, 1), y_range=(0, 1), width=2, height=2) expected = Image(([0.25, 0.75], [0.25, 0.75], [[1, 0], [2, 0]]), - vdims=['Count']) + vdims=[Dimension('Count', nodata=0)]) self.assertEqual(img, expected) @cudf_skip @@ -58,7 +63,7 @@ def test_aggregate_points_cudf(self): img = aggregate(points, dynamic=False, x_range=(0, 1), y_range=(0, 1), width=2, height=2) expected = Image(([0.25, 0.75], [0.25, 0.75], [[1, 0], [2, 0]]), - vdims=['Count']) + vdims=[Dimension('Count', nodata=0)]) self.assertIsInstance(img.data.Count.data, cupy.ndarray) self.assertEqual(img, expected) @@ -67,20 +72,20 @@ def test_aggregate_zero_range_points(self): agg = rasterize(p, x_range=(0, 0), y_range=(0, 1), expand=False, dynamic=False, width=2, height=2) img = Image(([], [0.25, 0.75], np.zeros((2, 0))), bounds=(0, 0, 0, 1), - xdensity=1, vdims=['Count']) + xdensity=1, vdims=[Dimension('Count', nodata=0)]) self.assertEqual(agg, img) def test_aggregate_points_target(self): points = Points([(0.2, 0.3), (0.4, 0.7), (0, 0.99)]) expected = Image(([0.25, 0.75], [0.25, 0.75], [[1, 0], [2, 0]]), - vdims=['Count']) + vdims=[Dimension('Count', nodata=0)]) img = aggregate(points, dynamic=False, target=expected) self.assertEqual(img, expected) def test_aggregate_points_sampling(self): points = Points([(0.2, 0.3), (0.4, 0.7), (0, 0.99)]) expected = Image(([0.25, 0.75], [0.25, 0.75], [[1, 0], [2, 0]]), - vdims=['Count']) + vdims=[Dimension('Count', nodata=0)]) img = aggregate(points, dynamic=False, x_range=(0, 1), y_range=(0, 1), x_sampling=0.5, y_sampling=0.5) self.assertEqual(img, expected) @@ -90,9 +95,9 @@ def test_aggregate_points_categorical(self): img = aggregate(points, dynamic=False, x_range=(0, 1), y_range=(0, 1), width=2, height=2, aggregator=ds.count_cat('z')) xs, ys = [0.25, 0.75], [0.25, 0.75] - expected = NdOverlay({'A': Image((xs, ys, [[1, 0], [0, 0]]), vdims='z Count'), - 'B': Image((xs, ys, [[0, 0], [1, 0]]), vdims='z Count'), - 'C': Image((xs, ys, [[0, 0], [1, 0]]), vdims='z Count')}, + expected = NdOverlay({'A': Image((xs, ys, [[1, 0], [0, 0]]), vdims=Dimension('z Count', nodata=0)), + 'B': Image((xs, ys, [[0, 0], [1, 0]]), vdims=Dimension('z Count', nodata=0)), + 'C': Image((xs, ys, [[0, 0], [1, 0]]), vdims=Dimension('z Count', nodata=0))}, kdims=['z']) self.assertEqual(img, expected) @@ -102,16 +107,16 @@ def test_aggregate_points_categorical_zero_range(self): aggregator=ds.count_cat('z'), height=2) xs, ys = [], [0.25, 0.75] params = dict(bounds=(0, 0, 0, 1), xdensity=1) - expected = NdOverlay({'A': Image((xs, ys, np.zeros((2, 0))), vdims='z Count', **params), - 'B': Image((xs, ys, np.zeros((2, 0))), vdims='z Count', **params), - 'C': Image((xs, ys, np.zeros((2, 0))), vdims='z Count', **params)}, + expected = NdOverlay({'A': Image((xs, ys, np.zeros((2, 0))), vdims=Dimension('z Count', nodata=0), **params), + 'B': Image((xs, ys, np.zeros((2, 0))), vdims=Dimension('z Count', nodata=0), **params), + 'C': Image((xs, ys, np.zeros((2, 0))), vdims=Dimension('z Count', nodata=0), **params)}, kdims=['z']) self.assertEqual(img, expected) def test_aggregate_curve(self): curve = Curve([(0.2, 0.3), (0.4, 0.7), (0.8, 0.99)]) expected = Image(([0.25, 0.75], [0.25, 0.75], [[1, 0], [1, 1]]), - vdims=['Count']) + vdims=[Dimension('Count', nodata=0)]) img = aggregate(curve, dynamic=False, x_range=(0, 1), y_range=(0, 1), width=2, height=2) self.assertEqual(img, expected) @@ -125,7 +130,7 @@ def test_aggregate_curve_datetimes(self): dates = [np.datetime64('2016-01-01T12:00:00.000000000'), np.datetime64('2016-01-02T12:00:00.000000000')] expected = Image((dates, [1.5, 2.5], [[1, 0], [0, 2]]), - datatype=['xarray'], bounds=bounds, vdims='Count') + datatype=['xarray'], bounds=bounds, vdims=Dimension('Count', nodata=0)) self.assertEqual(img, expected) def test_aggregate_curve_datetimes_dask(self): @@ -141,7 +146,8 @@ def test_aggregate_curve_datetimes_dask(self): dates = [np.datetime64('2019-01-01T04:09:45.000000000'), np.datetime64('2019-01-01T12:29:15.000000000')] expected = Image((dates, [166.5, 499.5, 832.5], [[332, 0], [167, 166], [0, 334]]), - ['index', 'a'], 'Count', datatype=['xarray'], bounds=bounds) + kdims=['index', 'a'], vdims=Dimension('Count', nodata=0), + datatype=['xarray'], bounds=bounds) self.assertEqual(img, expected) def test_aggregate_curve_datetimes_microsecond_timebase(self): @@ -155,7 +161,7 @@ def test_aggregate_curve_datetimes_microsecond_timebase(self): dates = [np.datetime64('2016-01-01T11:59:59.861759000',), np.datetime64('2016-01-02T12:00:00.138241000')] expected = Image((dates, [1.5, 2.5], [[1, 0], [0, 2]]), - datatype=['xarray'], bounds=bounds, vdims='Count') + datatype=['xarray'], bounds=bounds, vdims=Dimension('Count', nodata=0)) self.assertEqual(img, expected) def test_aggregate_ndoverlay_count_cat_datetimes_microsecond_timebase(self): @@ -172,9 +178,9 @@ def test_aggregate_ndoverlay_count_cat_datetimes_microsecond_timebase(self): dates = [np.datetime64('2016-01-01T11:59:59.861759000',), np.datetime64('2016-01-02T12:00:00.138241000')] expected = Image((dates, [1.5, 2.5], [[1, 0], [0, 2]]), - datatype=['xarray'], bounds=bounds, vdims='Count') + datatype=['xarray'], bounds=bounds, vdims=Dimension('Count', nodata=0)) expected2 = Image((dates, [1.5, 2.5], [[0, 1], [1, 1]]), - datatype=['xarray'], bounds=bounds, vdims='Count') + datatype=['xarray'], bounds=bounds, vdims=Dimension('Count', nodata=0)) self.assertEqual(imgs[0], expected) self.assertEqual(imgs[1], expected2) @@ -186,15 +192,16 @@ def test_aggregate_dt_xaxis_constant_yaxis(self): ys = np.array([]) bounds = (np.datetime64('1980-01-01T00:00:00.000000'), 1.0, np.datetime64('1980-01-01T01:39:00.000000'), 1.0) - expected = Image((xs, ys, np.empty((0, 3))), ['index', 'y'], 'Count', - xdensity=1, ydensity=1, bounds=bounds) + expected = Image((xs, ys, np.empty((0, 3))), ['index', 'y'], + vdims=Dimension('Count', nodata=0), xdensity=1, + ydensity=1, bounds=bounds) self.assertEqual(img, expected) def test_aggregate_ndoverlay(self): ds = Dataset([(0.2, 0.3, 0), (0.4, 0.7, 1), (0, 0.99, 2)], kdims=['x', 'y', 'z']) ndoverlay = ds.to(Points, ['x', 'y'], [], 'z').overlay() expected = Image(([0.25, 0.75], [0.25, 0.75], [[1, 0], [2, 0]]), - vdims=['Count']) + vdims=[Dimension('Count', nodata=0)]) img = aggregate(ndoverlay, dynamic=False, x_range=(0, 1), y_range=(0, 1), width=2, height=2) self.assertEqual(img, expected) @@ -202,7 +209,7 @@ def test_aggregate_ndoverlay(self): def test_aggregate_path(self): path = Path([[(0.2, 0.3), (0.4, 0.7)], [(0.4, 0.7), (0.8, 0.99)]]) expected = Image(([0.25, 0.75], [0.25, 0.75], [[1, 0], [2, 1]]), - vdims=['Count']) + vdims=[Dimension('Count', nodata=0)]) img = aggregate(path, dynamic=False, x_range=(0, 1), y_range=(0, 1), width=2, height=2) self.assertEqual(img, expected) @@ -215,12 +222,12 @@ def test_aggregate_contours_with_vdim(self): def test_aggregate_contours_without_vdim(self): contours = Contours([[(0.2, 0.3), (0.4, 0.7)], [(0.4, 0.7), (0.8, 0.99)]]) img = rasterize(contours, dynamic=False) - self.assertEqual(img.vdims, ['Count']) + self.assertEqual(img.vdims, [Dimension('Count', nodata=0)]) def test_aggregate_dframe_nan_path(self): path = Path([Path([[(0.2, 0.3), (0.4, 0.7)], [(0.4, 0.7), (0.8, 0.99)]]).dframe()]) expected = Image(([0.25, 0.75], [0.25, 0.75], [[1, 0], [2, 1]]), - vdims=['Count']) + vdims=[Dimension('Count', nodata=0)]) img = aggregate(path, dynamic=False, x_range=(0, 1), y_range=(0, 1), width=2, height=2) self.assertEqual(img, expected) @@ -228,14 +235,14 @@ def test_aggregate_dframe_nan_path(self): def test_spikes_aggregate_count(self): spikes = Spikes([1, 2, 3]) agg = rasterize(spikes, width=5, dynamic=False, expand=False) - expected = Image(np.array([[1, 0, 1, 0, 1]]), vdims='count', + expected = Image(np.array([[1, 0, 1, 0, 1]]), vdims=Dimension('Count', nodata=0), xdensity=2.5, ydensity=1, bounds=(1, 0, 3, 0.5)) self.assertEqual(agg, expected) def test_spikes_aggregate_count_dask(self): spikes = Spikes([1, 2, 3], datatype=['dask']) agg = rasterize(spikes, width=5, dynamic=False, expand=False) - expected = Image(np.array([[1, 0, 1, 0, 1]]), vdims='count', + expected = Image(np.array([[1, 0, 1, 0, 1]]), vdims=Dimension('Count', nodata=0), xdensity=2.5, ydensity=1, bounds=(1, 0, 3, 0.5)) self.assertEqual(agg, expected) @@ -244,7 +251,7 @@ def test_spikes_aggregate_dt_count(self): agg = rasterize(spikes, width=5, dynamic=False, expand=False) bounds = (np.datetime64('2016-01-01T00:00:00.000000'), 0, np.datetime64('2016-01-03T00:00:00.000000'), 0.5) - expected = Image(np.array([[1, 0, 1, 0, 1]]), vdims='count', bounds=bounds) + expected = Image(np.array([[1, 0, 1, 0, 1]]), vdims=Dimension('Count', nodata=0), bounds=bounds) self.assertEqual(agg, expected) def test_spikes_aggregate_dt_count_dask(self): @@ -253,13 +260,13 @@ def test_spikes_aggregate_dt_count_dask(self): agg = rasterize(spikes, width=5, dynamic=False, expand=False) bounds = (np.datetime64('2016-01-01T00:00:00.000000'), 0, np.datetime64('2016-01-03T00:00:00.000000'), 0.5) - expected = Image(np.array([[1, 0, 1, 0, 1]]), vdims='count', bounds=bounds) + expected = Image(np.array([[1, 0, 1, 0, 1]]), vdims=Dimension('Count', nodata=0), bounds=bounds) self.assertEqual(agg, expected) def test_spikes_aggregate_spike_length(self): spikes = Spikes([1, 2, 3]) agg = rasterize(spikes, width=5, dynamic=False, expand=False, spike_length=7) - expected = Image(np.array([[1, 0, 1, 0, 1]]), vdims='count', + expected = Image(np.array([[1, 0, 1, 0, 1]]), vdims=Dimension('Count', nodata=0), xdensity=2.5, ydensity=1, bounds=(1, 0, 3, 7.0)) self.assertEqual(agg, expected) @@ -275,7 +282,7 @@ def test_spikes_aggregate_with_height_count(self): [0, 0, 1, 0, 0], [0, 0, 1, 0, 0] ]) - expected = Image((xs, ys, arr), vdims='count') + expected = Image((xs, ys, arr), vdims=Dimension('Count', nodata=0)) self.assertEqual(agg, expected) def test_spikes_aggregate_with_height_count_override(self): @@ -289,7 +296,7 @@ def test_spikes_aggregate_with_height_count_override(self): [0, 0, 0, 0, 0], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0]]) - expected = Image((xs, ys, arr), vdims='count') + expected = Image((xs, ys, arr), vdims=Dimension('Count', nodata=0)) self.assertEqual(agg, expected) def test_rasterize_regrid_and_spikes_overlay(self): @@ -307,7 +314,7 @@ def test_rasterize_regrid_and_spikes_overlay(self): [0, 0, 0, 0], [0, 0, 0, 0]]) expected_spikes = Image(([0.25, 0.75, 1.25, 1.75], - [0.25, 0.75, 1.25, 1.75], spikes_arr), vdims='count') + [0.25, 0.75, 1.25, 1.75], spikes_arr), vdims=Dimension('Count', nodata=0)) overlay = img * spikes agg = rasterize(overlay, width=4, height=4, x_range=(0, 2), y_range=(0, 2), spike_length=0.5, upsample=True, dynamic=False) @@ -327,7 +334,7 @@ def test_spikes_aggregate_with_height_count_dask(self): [0, 0, 1, 0, 0], [0, 0, 1, 0, 0] ]) - expected = Image((xs, ys, arr), vdims='count') + expected = Image((xs, ys, arr), vdims=Dimension('Count', nodata=0)) self.assertEqual(agg, expected) def test_spikes_aggregate_with_negative_height_count(self): @@ -342,7 +349,7 @@ def test_spikes_aggregate_with_negative_height_count(self): [0, 0, 1, 0, 1], [1, 0, 1, 0, 1] ]) - expected = Image((xs, ys, arr), vdims='count') + expected = Image((xs, ys, arr), vdims=Dimension('Count', nodata=0)) self.assertEqual(agg, expected) def test_spikes_aggregate_with_positive_and_negative_height_count(self): @@ -357,7 +364,7 @@ def test_spikes_aggregate_with_positive_and_negative_height_count(self): [0, 0, 1, 0, 0], [0, 0, 1, 0, 0] ]) - expected = Image((xs, ys, arr), vdims='count') + expected = Image((xs, ys, arr), vdims=Dimension('Count', nodata=0)) self.assertEqual(agg, expected) def test_rectangles_aggregate_count(self): @@ -371,7 +378,7 @@ def test_rectangles_aggregate_count(self): [1, 2, 1, 1], [0, 0, 0, 0] ]) - expected = Image((xs, ys, arr), vdims='count') + expected = Image((xs, ys, arr), vdims=Dimension('Count', nodata=0)) self.assertEqual(agg, expected) def test_rectangles_aggregate_count_cat(self): @@ -392,8 +399,8 @@ def test_rectangles_aggregate_count_cat(self): [0, 1, 1, 1], [0, 0, 0, 0] ]) - expected1 = Image((xs, ys, arr1), vdims='cat Count') - expected2 = Image((xs, ys, arr2), vdims='cat Count') + expected1 = Image((xs, ys, arr1), vdims=Dimension('cat Count', nodata=0)) + expected2 = Image((xs, ys, arr2), vdims=Dimension('cat Count', nodata=0)) expected = NdOverlay({'A': expected1, 'B': expected2}, kdims=['cat']) self.assertEqual(agg, expected) @@ -431,7 +438,7 @@ def test_rectangles_aggregate_dt_count(self): ]) bounds = (0.0, np.datetime64('2016-01-01T00:00:00'), 4.0, np.datetime64('2016-01-05T00:00:00')) - expected = Image((xs, ys, arr), bounds=bounds, vdims='count') + expected = Image((xs, ys, arr), bounds=bounds, vdims=Dimension('Count', nodata=0)) self.assertEqual(agg, expected) def test_segments_aggregate_count(self): @@ -445,7 +452,7 @@ def test_segments_aggregate_count(self): [0, 1, 0, 0], [0, 1, 0, 0] ]) - expected = Image((xs, ys, arr), vdims='count') + expected = Image((xs, ys, arr), vdims=Dimension('Count', nodata=0)) self.assertEqual(agg, expected) def test_segments_aggregate_sum(self, instance=False): @@ -492,7 +499,7 @@ def test_segments_aggregate_dt_count(self): ]) bounds = (0.0, np.datetime64('2016-01-01T00:00:00'), 4.0, np.datetime64('2016-01-05T00:00:00')) - expected = Image((xs, ys, arr), bounds=bounds, vdims='count') + expected = Image((xs, ys, arr), bounds=bounds, vdims=Dimension('Count', nodata=0)) self.assertEqual(agg, expected) def test_area_aggregate_simple_count(self): @@ -506,7 +513,7 @@ def test_area_aggregate_simple_count(self): [0, 1, 1, 0], [0, 0, 0, 0] ]) - expected = Image((xs, ys, arr), vdims='count') + expected = Image((xs, ys, arr), vdims=Dimension('Count', nodata=0)) self.assertEqual(agg, expected) def test_area_aggregate_negative_count(self): @@ -520,7 +527,7 @@ def test_area_aggregate_negative_count(self): [1, 1, 1, 1], [1, 1, 1, 1] ]) - expected = Image((xs, ys, arr), vdims='count') + expected = Image((xs, ys, arr), vdims=Dimension('Count', nodata=0)) self.assertEqual(agg, expected) def test_area_aggregate_crossover_count(self): @@ -534,7 +541,7 @@ def test_area_aggregate_crossover_count(self): [1, 1, 1, 1], [0, 0, 1, 1] ]) - expected = Image((xs, ys, arr), vdims='count') + expected = Image((xs, ys, arr), vdims=Dimension('Count', nodata=0)) self.assertEqual(agg, expected) def test_spread_aggregate_symmetric_count(self): @@ -548,7 +555,7 @@ def test_spread_aggregate_symmetric_count(self): [0, 1, 1, 0], [0, 0, 0, 1] ]) - expected = Image((xs, ys, arr), vdims='count') + expected = Image((xs, ys, arr), vdims=Dimension('Count', nodata=0)) self.assertEqual(agg, expected) def test_spread_aggregate_assymmetric_count(self): @@ -563,7 +570,7 @@ def test_spread_aggregate_assymmetric_count(self): [0, 1, 1, 0], [0, 0, 1, 1] ]) - expected = Image((xs, ys, arr), vdims='count') + expected = Image((xs, ys, arr), vdims=Dimension('Count', nodata=0)) self.assertEqual(agg, expected) def test_rgb_regrid_packed(self): @@ -607,7 +614,7 @@ def test_line_rasterize(self): [1, 1, 1, 0], [1, 0, 1, 0] ]) - expected = Image((xs, ys, arr), vdims='Count') + expected = Image((xs, ys, arr), vdims=Dimension('Count', nodata=0)) self.assertEqual(agg, expected) @spatialpandas_skip @@ -623,7 +630,7 @@ def test_multi_line_rasterize(self): [1, 1, 1, 0], [1, 0, 1, 0] ]) - expected = Image((xs, ys, arr), vdims='Count') + expected = Image((xs, ys, arr), vdims=Dimension('Count', nodata=0)) self.assertEqual(agg, expected) @spatialpandas_skip @@ -638,7 +645,7 @@ def test_ring_rasterize(self): [0, 1, 1, 0], [0, 0, 1, 0] ]) - expected = Image((xs, ys, arr), vdims='Count') + expected = Image((xs, ys, arr), vdims=Dimension('Count', nodata=0)) self.assertEqual(agg, expected) @spatialpandas_skip @@ -658,7 +665,7 @@ def test_polygon_rasterize(self): [0, 0, 1, 1, 0, 0], [0, 0, 0, 0, 0, 0] ]) - expected = Image((xs, ys, arr), vdims='Count') + expected = Image((xs, ys, arr), vdims=Dimension('Count', nodata=0)) self.assertEqual(agg, expected) @spatialpandas_skip @@ -692,7 +699,7 @@ def test_multi_poly_rasterize(self): [1, 1, 1, 0], [1, 1, 0, 0] ]) - expected = Image((xs, ys, arr), vdims='Count') + expected = Image((xs, ys, arr), vdims=Dimension('Count', nodata=0)) self.assertEqual(agg, expected) @@ -708,9 +715,9 @@ def test_aggregate_points_categorical(self): img = aggregate(points, dynamic=False, x_range=(0, 1), y_range=(0, 1), width=2, height=2, aggregator=ds.by('z', ds.count())) xs, ys = [0.25, 0.75], [0.25, 0.75] - expected = NdOverlay({'A': Image((xs, ys, [[1, 0], [0, 0]]), vdims='z Count'), - 'B': Image((xs, ys, [[0, 0], [1, 0]]), vdims='z Count'), - 'C': Image((xs, ys, [[0, 0], [1, 0]]), vdims='z Count')}, + expected = NdOverlay({'A': Image((xs, ys, [[1, 0], [0, 0]]), vdims=Dimension('z Count', nodata=0)), + 'B': Image((xs, ys, [[0, 0], [1, 0]]), vdims=Dimension('z Count', nodata=0)), + 'C': Image((xs, ys, [[0, 0], [1, 0]]), vdims=Dimension('z Count', nodata=0))}, kdims=['z']) self.assertEqual(img, expected) @@ -733,11 +740,11 @@ class DatashaderShadeTests(ComparisonTestCase): def test_shade_categorical_images_xarray(self): xs, ys = [0.25, 0.75], [0.25, 0.75] data = NdOverlay({'A': Image((xs, ys, np.array([[1, 0], [0, 0]], dtype='u4')), - datatype=['xarray'], vdims='z Count'), + datatype=['xarray'], vdims=Dimension('z Count', nodata=0)), 'B': Image((xs, ys, np.array([[0, 0], [1, 0]], dtype='u4')), - datatype=['xarray'], vdims='z Count'), + datatype=['xarray'], vdims=Dimension('z Count', nodata=0)), 'C': Image((xs, ys, np.array([[0, 0], [1, 0]], dtype='u4')), - datatype=['xarray'], vdims='z Count')}, + datatype=['xarray'], vdims=Dimension('z Count', nodata=0))}, kdims=['z']) shaded = shade(data) r = [[228, 120], [66, 120]] @@ -751,11 +758,11 @@ def test_shade_categorical_images_xarray(self): def test_shade_categorical_images_grid(self): xs, ys = [0.25, 0.75], [0.25, 0.75] data = NdOverlay({'A': Image((xs, ys, np.array([[1, 0], [0, 0]], dtype='u4')), - datatype=['grid'], vdims='z Count'), + datatype=['grid'], vdims=Dimension('z Count', nodata=0)), 'B': Image((xs, ys, np.array([[0, 0], [1, 0]], dtype='u4')), - datatype=['grid'], vdims='z Count'), + datatype=['grid'], vdims=Dimension('z Count', nodata=0)), 'C': Image((xs, ys, np.array([[0, 0], [1, 0]], dtype='u4')), - datatype=['grid'], vdims='z Count')}, + datatype=['grid'], vdims=Dimension('z Count', nodata=0))}, kdims=['z']) shaded = shade(data) r = [[228, 120], [66, 120]] @@ -978,13 +985,13 @@ def test_rasterize_points(self): img = rasterize(points, dynamic=False, x_range=(0, 1), y_range=(0, 1), width=2, height=2) expected = Image(([0.25, 0.75], [0.25, 0.75], [[1, 0], [2, 0]]), - vdims=['Count']) + vdims=[Dimension('Count', nodata=0)]) self.assertEqual(img, expected) def test_rasterize_curve(self): curve = Curve([(0.2, 0.3), (0.4, 0.7), (0.8, 0.99)]) expected = Image(([0.25, 0.75], [0.25, 0.75], [[1, 0], [1, 1]]), - vdims=['Count']) + vdims=[Dimension('Count', nodata=0)]) img = rasterize(curve, dynamic=False, x_range=(0, 1), y_range=(0, 1), width=2, height=2) self.assertEqual(img, expected) @@ -993,7 +1000,7 @@ def test_rasterize_ndoverlay(self): ds = Dataset([(0.2, 0.3, 0), (0.4, 0.7, 1), (0, 0.99, 2)], kdims=['x', 'y', 'z']) ndoverlay = ds.to(Points, ['x', 'y'], [], 'z').overlay() expected = Image(([0.25, 0.75], [0.25, 0.75], [[1, 0], [2, 0]]), - vdims=['Count']) + vdims=[Dimension('Count', nodata=0)]) img = rasterize(ndoverlay, dynamic=False, x_range=(0, 1), y_range=(0, 1), width=2, height=2) self.assertEqual(img, expected) @@ -1001,7 +1008,7 @@ def test_rasterize_ndoverlay(self): def test_rasterize_path(self): path = Path([[(0.2, 0.3), (0.4, 0.7)], [(0.4, 0.7), (0.8, 0.99)]]) expected = Image(([0.25, 0.75], [0.25, 0.75], [[1, 0], [2, 1]]), - vdims=['Count']) + vdims=[Dimension('Count', nodata=0)]) img = rasterize(path, dynamic=False, x_range=(0, 1), y_range=(0, 1), width=2, height=2) self.assertEqual(img, expected) From 063a72c761a414e64a8f464f487674027f5ad1af Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Wed, 25 Nov 2020 15:02:34 +0100 Subject: [PATCH 45/98] Fixed flake --- holoviews/operation/datashader.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/holoviews/operation/datashader.py b/holoviews/operation/datashader.py index dfce57f4f1..2464d3d7bd 100644 --- a/holoviews/operation/datashader.py +++ b/holoviews/operation/datashader.py @@ -612,11 +612,6 @@ def _process(self, element, key=None): df = PandasInterface.as_dframe(element) - if isinstance(agg_fn, (ds.count, ds.any)): - vdim = type(agg_fn).__name__ - else: - vdim = element.get_dimension(agg_fn.column) - cvs = ds.Canvas(plot_width=width, plot_height=height, x_range=x_range, y_range=y_range) From 9c3b61fa0718b9133d64d2cd3c3761d1bb7895c6 Mon Sep 17 00:00:00 2001 From: "James A. Bednar" Date: Thu, 26 Nov 2020 07:21:57 -0600 Subject: [PATCH 46/98] Cleaned up wording --- examples/user_guide/04-Style_Mapping.ipynb | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/examples/user_guide/04-Style_Mapping.ipynb b/examples/user_guide/04-Style_Mapping.ipynb index 7d8d16c922..2b55179c0b 100644 --- a/examples/user_guide/04-Style_Mapping.ipynb +++ b/examples/user_guide/04-Style_Mapping.ipynb @@ -386,7 +386,7 @@ "source": [ "### Explicit color mapping\n", "\n", - "Some elements work through implicit colormapping the prime example being the ``Image`` type, however other elements can be colormapped using style mapping instead, by setting the color to an existing dimension." + "Some elements work through implicit colormapping, the prime example being the ``Image`` type. However, other elements can be colormapped using style mapping instead, by setting the color to an existing dimension." ] }, { @@ -530,7 +530,7 @@ "source": [ "#### Normalization modes\n", "\n", - "There are three available color normalization or `cnorm` options to determine how numerical values are mapped to the range of colors in the colorbar:\n", + "When using a colormap, there are three available color normalization or `cnorm` options to determine how numerical values are mapped to the range of colors in the colorbar:\n", "\n", "* `'linear'`: Simple linear mapping (used by default)\n", "* `log`: Logarithmic mapping\n", @@ -567,9 +567,9 @@ "\n", "In the `'log'` mode, the random values are a little easier to see but these samples still use a small portion of the colormap. Logarithmic colormaps are most useful when you know that you are plotting data with an approximately logarithmic distribution.\n", "\n", - "In the `'eq_hist'` mode colors are nonlinearly mapped according to the distribution of values in the plot, such that each color in the colormap represents an approximately equal number of values in the plot (with few or no colors reserved for the nearly empty range between 3 and 100). In this mode both the outliers and the overall low-amplitude noise can be seen clearly, but the non-linear distortion can make the colorbar more difficult to interpret.\n", + "In the `'eq_hist'` mode, colors are nonlinearly mapped according to the actual distribution of values in the plot, such that each color in the colormap represents an approximately equal number of values in the plot (here with few or no colors reserved for the nearly empty range between 10 and 100). In this mode both the outliers and the overall low-amplitude noise can be seen clearly, but the non-linear distortion can make the colors more difficult to interpret as numerical values.\n", "\n", - "In practice, it is often a good idea to try all three of these modes with your data, using `eq_hist` to be sure that you are seeing all of the patterns in the data, then either `log` or `linear` (depending on which one is a better match to your distribution) with the values clipped to the range of values you want to show." + "When working with unknown data distributions, it is often a good idea to try all three of these modes, using `eq_hist` to be sure that you are seeing all of the patterns in the data, then either `log` or `linear` (depending on which one is a better match to your distribution) with the values clipped to the range of values you want to show." ] }, { @@ -580,7 +580,7 @@ "\n", "* ``clim_percentile``: Percentile value to compute colorscale robust to outliers. If `True` uses 2nd and 98th percentile, otherwise uses the specified percentile value. \n", "* ``cnorm``: Color normalization to be applied during colormapping. Allows switching between 'linear', 'log' and 'eqhist'.\n", - "* ``logz``: Enable logarithmic color scale (Will be deprecated in future in favor of `cnorm='log'`)\n", + "* ``logz``: Enable logarithmic color scale (same as `cnorm='log'`; to be deprecated at some point)\n", "* ``symmetric``: Ensures that the color scale is centered on zero (e.g. ``symmetric=True``)" ] }, From 27a12e82cb0082e114214df012e8aca1609782d8 Mon Sep 17 00:00:00 2001 From: "James A. Bednar" Date: Thu, 26 Nov 2020 07:34:25 -0600 Subject: [PATCH 47/98] Set default colormap to match shade. Use separate xy/uv and count dimensions for paths to avoid shared axes. --- examples/user_guide/15-Large_Data.ipynb | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/examples/user_guide/15-Large_Data.ipynb b/examples/user_guide/15-Large_Data.ipynb index 44f4e34789..36b1100804 100644 --- a/examples/user_guide/15-Large_Data.ipynb +++ b/examples/user_guide/15-Large_Data.ipynb @@ -6,11 +6,11 @@ "source": [ "# Working with large data using Datashader\n", "\n", - "The various plotting-library backends supported by HoloViews, such as Matplotlib and Bokeh, each have a variety of limitations on the amount of data that is practical to work with. Bokeh in particular mirrors your data directly into an HTML page viewable in your browser, which can cause problems when data sizes approach the limited memory available for each web page in current browsers.\n", + "The various plotting-library backends supported by HoloViews, such as Matplotlib, Bokeh, and Plotly, each have a variety of limitations on the amount of data that is practical to work with. Bokeh and Plotly in particular mirror your data directly into an HTML page viewable in your browser, which can cause problems when data sizes approach the limited memory available for each web page in current browsers.\n", "\n", - "Luckily, a visualization of even the largest dataset will be constrained by the resolution of your display device, and so one approach to handling such data is to pre-render or rasterize the data into a fixed-size array or image *before* sending it to the backend. The [Datashader](https://github.com/bokeh/datashader) library provides a high-performance big-data server-side rasterization pipeline that works seamlessly with HoloViews to support datasets that are orders of magnitude larger than those supported natively by the plotting-library backends, including millions or billions of points even on ordinary laptops.\n", + "Luckily, a visualization of even the largest dataset will be constrained by the resolution of your display device, and so one approach to handling such data is to pre-render or rasterize the data into a fixed-size array or image *before* sending it to the backend plotting library and to your local web browser. The [Datashader](https://github.com/bokeh/datashader) library provides a high-performance big-data server-side rasterization pipeline that works seamlessly with HoloViews to support datasets that are orders of magnitude larger than those supported natively by the plotting-library backends, including millions or billions of points even on ordinary laptops.\n", "\n", - "Here, we will see how and when to use Datashader with HoloViews Elements and Containers. For simplicity in this discussion we'll focus on simple synthetic datasets, but the [Datashader docs](http://datashader.org/topics) include a wide variety of real datasets that give a much better idea of the power of using Datashader with HoloViews, and [HoloViz.org](http://holoviz.org) shows how to install and work with HoloViews and Datashader together.\n", + "Here, we will see how and when to use Datashader with HoloViews Elements and Containers. For simplicity in this discussion we'll focus on simple synthetic datasets, but [Datashader's examples](http://datashader.org/topics) include a wide variety of real datasets that give a much better idea of the power of using Datashader with HoloViews, and [HoloViz.org](http://holoviz.org) shows how to install and work with HoloViews and Datashader together.\n", "\n", "" ] @@ -75,9 +75,9 @@ "source": [ "# Principles of datashading\n", "\n", - "Because HoloViews elements are fundamentally data containers, not visualizations, you can very quickly declare elements such as ``Points`` or ``Path`` containing datasets that may be as large as the full memory available on your machine (or even larger if using Dask dataframes). So even for very large datasets, you can easily specify a data structure that you can work with for making selections, sampling, aggregations, and so on. However, as soon as you try to visualize it directly with either the matplotlib or bokeh plotting extensions, the rendering process may be prohibitively expensive.\n", + "Because HoloViews elements are fundamentally data containers, not visualizations, you can very quickly declare elements such as ``Points`` or ``Path`` containing datasets that may be as large as the full memory available on your machine (or even larger if using Dask dataframes). So even for very large datasets, you can easily specify a data structure that you can work with for making selections, sampling, aggregations, and so on. However, as soon as you try to visualize it directly with either the Matplotlib or Bokeh plotting extensions, the rendering process may be prohibitively expensive.\n", "\n", - "Let's start with a simple example we can visualize as normal:" + "Let's start with a simple example that's easy to visualize in any plotting library:" ] }, { @@ -88,7 +88,7 @@ "source": [ "np.random.seed(1)\n", "points = hv.Points(np.random.multivariate_normal((0,0), [[0.1, 0.1], [0.1, 1.0]], (1000,)),label=\"Points\")\n", - "paths = hv.Path([random_walk(2000,30)], label=\"Paths\")\n", + "paths = hv.Path([random_walk(2000,30)], kdims=[\"u\",\"v\"], label=\"Paths\")\n", "\n", "points + paths" ] @@ -101,7 +101,7 @@ "\n", "Because all of the data in these plots gets transferred directly into the web browser, the interactive functionality will be available even on a static export of this figure as a web page. Note that even though the visualization above is not computationally expensive, even with just 1000 points as in the scatterplot above, the plot already suffers from [overplotting](https://anaconda.org/jbednar/plotting_pitfalls), with later points obscuring previously plotted points. \n", "\n", - "With much larger datasets, these issues will quickly make it impossible to see the true structure of the data. We can easily declare 50X or 1000X larger versions of the same plots above, but if we tried to visualize them they would be nearly unusable even if the browser did not crash:" + "With much larger datasets, these issues will quickly make it impossible to see the true structure of the data. We can easily declare 50X or 1000X larger versions of the same plots above, but if we tried to visualize them directly they would be unusably slow even if the browser did not crash:" ] }, { @@ -112,7 +112,7 @@ "source": [ "np.random.seed(1)\n", "points = hv.Points(np.random.multivariate_normal((0,0), [[0.1, 0.1], [0.1, 1.0]], (1000000,)),label=\"Points\")\n", - "paths = hv.Path([0.15*random_walk(100000) for i in range(10)],label=\"Paths\")\n", + "paths = hv.Path([0.15*random_walk(100000) for i in range(10)], kdims=[\"u\",\"v\"], label=\"Paths\")\n", "\n", "#points + paths ## Danger! Browsers can't handle 1 million points!" ] @@ -130,7 +130,7 @@ "metadata": {}, "outputs": [], "source": [ - "decimate(points) + datashade(points) + datashade(paths)" + "decimate(points) + rasterize(points) + rasterize(paths).redim(Count=\"pcount\")" ] }, { From 514d42eff6c08b069bc2e427c7b217409c669c27 Mon Sep 17 00:00:00 2001 From: jlstevens Date: Mon, 30 Nov 2020 16:47:19 +0100 Subject: [PATCH 48/98] Added vdim_suffix parameter to AggregationOperation --- holoviews/operation/datashader.py | 15 +++++++++++---- holoviews/tests/operation/testdatashader.py | 3 ++- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/holoviews/operation/datashader.py b/holoviews/operation/datashader.py index 2464d3d7bd..d467b45569 100644 --- a/holoviews/operation/datashader.py +++ b/holoviews/operation/datashader.py @@ -228,6 +228,10 @@ class AggregationOperation(ResamplingOperation): no column is defined the first value dimension of the element will be used. May also be defined as a string.""") + vdim_suffix = param.String(default=' over {kdims}', doc=""" + Suffix to add to value dimension name where {kdims} templates + in the names of the input element key dimensions""") + _agg_methods = { 'any': rd.any, 'count': rd.count, @@ -292,6 +296,9 @@ def _get_agg_params(self, element, x, y, agg_fn, bounds): params = dict(get_param_values(element), kdims=[x, y], datatype=['xarray'], bounds=bounds) + kdim_list = ', '.join(str(kd) for kd in params['kdims']) + vdim_suffix = self.vdim_suffix.format(kdims=kdim_list) + category = None if hasattr(agg_fn, 'reduction'): category = agg_fn.cat_column @@ -304,16 +311,16 @@ def _get_agg_params(self, element, x, y, agg_fn, bounds): "Ensure the aggregator references an existing " "dimension." % (column,element)) if isinstance(agg_fn, ds.count_cat): - vdims = dims[0].clone('%s Count' % column, nodata=0) + vdims = dims[0].clone('%s Count%s' % (column, vdim_suffix), nodata=0) else: - vdims = dims[0].clone(column) + vdims = dims[0].clone(column + vdim_suffix) elif category: agg_name = type(agg_fn).__name__.title() - vdims = Dimension('%s %s' % (category, agg_name)) + vdims = Dimension('%s %s%s' % (category, agg_name, vdim_suffix)) if agg_name == 'Count': vdims.nodata = 0 else: - vdims = Dimension('Count', nodata=0) + vdims = Dimension('Count%s' % vdim_suffix, nodata=0) params['vdims'] = vdims return params diff --git a/holoviews/tests/operation/testdatashader.py b/holoviews/tests/operation/testdatashader.py index eb04e8bc68..72f6d11b0a 100644 --- a/holoviews/tests/operation/testdatashader.py +++ b/holoviews/tests/operation/testdatashader.py @@ -17,7 +17,7 @@ from holoviews.core.util import pd from holoviews.operation.datashader import ( aggregate, regrid, ds_version, stack, directly_connect_edges, - shade, spread, rasterize + shade, spread, rasterize, AggregationOperation ) except: raise SkipTest('Datashader not available') @@ -42,6 +42,7 @@ numba_logger = logging.getLogger('numba') numba_logger.setLevel(logging.WARNING) +AggregationOperation.vdim_suffix = '' class DatashaderAggregateTests(ComparisonTestCase): """ From e762da7a73935a10580f262d681b6e3c05d9158f Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Mon, 30 Nov 2020 17:26:24 +0100 Subject: [PATCH 49/98] Unify handling of trimesh rasterize parameters --- holoviews/operation/datashader.py | 27 ++++++--------------- holoviews/tests/operation/testdatashader.py | 8 +++--- 2 files changed, 11 insertions(+), 24 deletions(-) diff --git a/holoviews/operation/datashader.py b/holoviews/operation/datashader.py index d467b45569..4352b7c36a 100644 --- a/holoviews/operation/datashader.py +++ b/holoviews/operation/datashader.py @@ -317,10 +317,11 @@ def _get_agg_params(self, element, x, y, agg_fn, bounds): elif category: agg_name = type(agg_fn).__name__.title() vdims = Dimension('%s %s%s' % (category, agg_name, vdim_suffix)) - if agg_name == 'Count': + if agg_name in ('Count', 'Any'): vdims.nodata = 0 else: - vdims = Dimension('Count%s' % vdim_suffix, nodata=0) + agg_name = type(agg_fn).__name__.title() + vdims = Dimension('%s%s' % (agg_name, vdim_suffix), nodata=0) params['vdims'] = vdims return params @@ -1024,20 +1025,7 @@ def _process(self, element, key=None): wireframe = True precompute = False # TriMesh itself caches wireframe agg = self._get_aggregator(element) if isinstance(agg, (ds.any, ds.count)) else ds.any() - vdim = 'Count' if isinstance(agg, ds.count) else 'Any' - elif getattr(agg, 'column', None): - if agg.column in element.vdims: - vdim = element.get_dimension(agg.column) - elif isinstance(element, TriMesh) and agg.column in element.nodes.vdims: - vdim = element.nodes.get_dimension(agg.column) - else: - raise ValueError("Aggregation column %s not found on TriMesh element." - % agg.column) - else: - if isinstance(element, TriMesh) and element.nodes.vdims: - vdim = element.nodes.vdims[0] - else: - vdim = element.vdims[0] + elif getattr(agg, 'column', None) is None: agg = self._get_aggregator(element) if element._plot_id in self._precomputed: @@ -1047,14 +1035,13 @@ def _process(self, element, key=None): else: precomputed = self._precompute(element, agg) - params = dict(get_param_values(element), kdims=[x, y], - datatype=['xarray'], vdims=[vdim]) + bounds = (x_range[0], y_range[0], x_range[1], y_range[1]) + params = self._get_agg_params(element, x, y, agg, bounds) if width == 0 or height == 0: if width == 0: params['xdensity'] = 1 if height == 0: params['ydensity'] = 1 - bounds = (x_range[0], y_range[0], x_range[1], y_range[1]) - return Image((xs, ys, np.zeros((height, width))), bounds=bounds, **params) + return Image((xs, ys, np.zeros((height, width))), **params) if wireframe: segments = precomputed['segments'] diff --git a/holoviews/tests/operation/testdatashader.py b/holoviews/tests/operation/testdatashader.py index 72f6d11b0a..03caa03814 100644 --- a/holoviews/tests/operation/testdatashader.py +++ b/holoviews/tests/operation/testdatashader.py @@ -223,7 +223,7 @@ def test_aggregate_contours_with_vdim(self): def test_aggregate_contours_without_vdim(self): contours = Contours([[(0.2, 0.3), (0.4, 0.7)], [(0.4, 0.7), (0.8, 0.99)]]) img = rasterize(contours, dynamic=False) - self.assertEqual(img.vdims, [Dimension('Count', nodata=0)]) + self.assertEqual(img.vdims, [Dimension('Any', nodata=0)]) def test_aggregate_dframe_nan_path(self): path = Path([Path([[(0.2, 0.3), (0.4, 0.7)], [(0.4, 0.7), (0.8, 0.99)]]).dframe()]) @@ -883,7 +883,7 @@ def test_rasterize_trimesh_no_vdims(self): trimesh = TriMesh((simplices, vertices)) img = rasterize(trimesh, width=3, height=3, dynamic=False) image = Image(np.array([[True, True, True], [True, True, True], [True, True, True]]), - bounds=(0, 0, 1, 1), vdims='Any') + bounds=(0, 0, 1, 1), vdims=Dimension('Any', nodata=0)) self.assertEqual(img, image) def test_rasterize_trimesh_no_vdims_zero_range(self): @@ -892,7 +892,7 @@ def test_rasterize_trimesh_no_vdims_zero_range(self): trimesh = TriMesh((simplices, vertices)) img = rasterize(trimesh, height=2, x_range=(0, 0), dynamic=False) image = Image(([], [0.25, 0.75], np.zeros((2, 0))), - bounds=(0, 0, 0, 1), xdensity=1, vdims='Any') + bounds=(0, 0, 0, 1), xdensity=1, vdims=Dimension('Any', nodata=0)) self.assertEqual(img, image) def test_rasterize_trimesh_with_vdims_as_wireframe(self): @@ -901,7 +901,7 @@ def test_rasterize_trimesh_with_vdims_as_wireframe(self): trimesh = TriMesh((simplices, vertices), vdims=['z']) img = rasterize(trimesh, width=3, height=3, aggregator='any', interpolation=None, dynamic=False) image = Image(np.array([[True, True, True], [True, True, True], [True, True, True]]), - bounds=(0, 0, 1, 1), vdims='Any') + bounds=(0, 0, 1, 1), vdims=Dimension('Any', nodata=0)) self.assertEqual(img, image) def test_rasterize_trimesh(self): From 3b37725a3c60c6ea3b8983f8fc66cc722594af9a Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Mon, 30 Nov 2020 18:48:31 +0100 Subject: [PATCH 50/98] Keep Dimension.label unchanged --- holoviews/operation/datashader.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/holoviews/operation/datashader.py b/holoviews/operation/datashader.py index 4352b7c36a..4630c3400e 100644 --- a/holoviews/operation/datashader.py +++ b/holoviews/operation/datashader.py @@ -296,7 +296,7 @@ def _get_agg_params(self, element, x, y, agg_fn, bounds): params = dict(get_param_values(element), kdims=[x, y], datatype=['xarray'], bounds=bounds) - kdim_list = ', '.join(str(kd) for kd in params['kdims']) + kdim_list = '_'.join(str(kd) for kd in params['kdims']) vdim_suffix = self.vdim_suffix.format(kdims=kdim_list) category = None @@ -316,12 +316,12 @@ def _get_agg_params(self, element, x, y, agg_fn, bounds): vdims = dims[0].clone(column + vdim_suffix) elif category: agg_name = type(agg_fn).__name__.title() - vdims = Dimension('%s %s%s' % (category, agg_name, vdim_suffix)) + vdims = Dimension('%s %s%s' % (category, agg_name, vdim_suffix), label=agg_name) if agg_name in ('Count', 'Any'): vdims.nodata = 0 else: agg_name = type(agg_fn).__name__.title() - vdims = Dimension('%s%s' % (agg_name, vdim_suffix), nodata=0) + vdims = Dimension('%s%s' % (agg_name, vdim_suffix), label=agg_name, nodata=0) params['vdims'] = vdims return params From 9680d28d415c6ec8141435796449d9dcaa3e7622 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Mon, 30 Nov 2020 18:54:56 +0100 Subject: [PATCH 51/98] Switche default cmap for Charts --- holoviews/plotting/bokeh/__init__.py | 5 +++-- holoviews/plotting/mpl/__init__.py | 6 ++++-- holoviews/plotting/plotly/__init__.py | 5 +++-- setup.py | 1 + 4 files changed, 11 insertions(+), 6 deletions(-) diff --git a/holoviews/plotting/bokeh/__init__.py b/holoviews/plotting/bokeh/__init__.py index 3e6cbe840f..f0cbccebd0 100644 --- a/holoviews/plotting/bokeh/__init__.py +++ b/holoviews/plotting/bokeh/__init__.py @@ -159,6 +159,7 @@ def colormap_generator(palette): if max(p.keys()) < 256}) dflt_cmap = 'fire' +dflt_chart_cmap = 'kbc_r' all_palettes['fire'] = {len(fire): fire} options = Store.options(backend='bokeh') @@ -167,8 +168,8 @@ def colormap_generator(palette): options.Curve = Options('style', color=Cycle(), line_width=2) options.BoxWhisker = Options('style', box_fill_color=Cycle(), whisker_color='black', box_line_color='black', outlier_color='black') -options.Scatter = Options('style', color=Cycle(), size=point_size, cmap=dflt_cmap) -options.Points = Options('style', color=Cycle(), size=point_size, cmap=dflt_cmap) +options.Scatter = Options('style', color=Cycle(), size=point_size, cmap=dflt_chart_cmap) +options.Points = Options('style', color=Cycle(), size=point_size, cmap=dflt_chart_cmap) options.Points = Options('plot', show_frame=True) options.Histogram = Options('style', line_color='black', color=Cycle(), muted_alpha=0.2) options.ErrorBars = Options('style', color='black') diff --git a/holoviews/plotting/mpl/__init__.py b/holoviews/plotting/mpl/__init__.py index 6a49ea4e1c..92cb246d15 100644 --- a/holoviews/plotting/mpl/__init__.py +++ b/holoviews/plotting/mpl/__init__.py @@ -221,18 +221,20 @@ def grid_selector(grid): options = Store.options(backend='matplotlib') dflt_cmap = 'fire' +dflt_chart_cmap = 'kbc_r' + # Default option definitions # Note: *No*short aliases here! e.g use 'facecolor' instead of 'fc' # Charts options.Curve = Options('style', color=Cycle(), linewidth=2) -options.Scatter = Options('style', color=Cycle(), marker='o', cmap=dflt_cmap) +options.Scatter = Options('style', color=Cycle(), marker='o', cmap=dflt_chart_cmap) options.Points = Options('plot', show_frame=True) options.ErrorBars = Options('style', edgecolor='k') options.Spread = Options('style', facecolor=Cycle(), alpha=0.6, edgecolor='k', linewidth=0.5) options.Bars = Options('style', edgecolor='k', color=Cycle()) options.Histogram = Options('style', edgecolor='k', facecolor=Cycle()) -options.Points = Options('style', color=Cycle(), marker='o', cmap=dflt_cmap) +options.Points = Options('style', color=Cycle(), marker='o', cmap=dflt_chart_cmap) options.Scatter3D = Options('style', c=Cycle(), marker='o') options.Scatter3D = Options('plot', fig_size=150) options.Path3D = Options('plot', fig_size=150) diff --git a/holoviews/plotting/plotly/__init__.py b/holoviews/plotting/plotly/__init__.py index af055e45d4..a725251c05 100644 --- a/holoviews/plotting/plotly/__init__.py +++ b/holoviews/plotting/plotly/__init__.py @@ -104,6 +104,7 @@ plot.padding = 0 dflt_cmap = 'fire' +dflt_chart_cmap = 'kbc_r' dflt_shape_line_color = '#2a3f5f' # Line color of default plotly template point_size = np.sqrt(6) # Matches matplotlib default @@ -113,8 +114,8 @@ # Charts options.Curve = Options('style', color=Cycle(), line_width=2) options.ErrorBars = Options('style', color='black') -options.Scatter = Options('style', color=Cycle()) -options.Points = Options('style', color=Cycle()) +options.Scatter = Options('style', color=Cycle(), cmap=dflt_chart_cmap) +options.Points = Options('style', color=Cycle(), cmap=dflt_chart_cmap) options.Area = Options('style', color=Cycle(), line_width=2) options.Spread = Options('style', color=Cycle(), line_width=2) options.TriSurface = Options('style', cmap='viridis') diff --git a/setup.py b/setup.py index d58103dfb3..d0780fabb3 100644 --- a/setup.py +++ b/setup.py @@ -15,6 +15,7 @@ "numpy >=1.0", "pyviz_comms >=0.7.3", "panel >=0.8.0", + "colorcet", "pandas", ] From 646753e90db07117813e69946ae5654cac8584a3 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Mon, 30 Nov 2020 18:55:20 +0100 Subject: [PATCH 52/98] Enhance Store.transfer_options --- holoviews/core/options.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/holoviews/core/options.py b/holoviews/core/options.py index b707cbf8e6..5cdb56ad4e 100644 --- a/holoviews/core/options.py +++ b/holoviews/core/options.py @@ -1280,11 +1280,13 @@ def lookup(cls, backend, obj): @classmethod - def transfer_options(cls, obj, new_obj, backend=None): + def transfer_options(cls, obj, new_obj, backend=None, names=None): """ Transfers options for all backends from one object to another. Drops any options defined in the supplied drop list. """ + if obj is new_obj: + return backend = cls.current_backend if backend is None else backend type_name = type(new_obj).__name__ group = type_name if obj.group == type(obj).__name__ else obj.group @@ -1292,7 +1294,10 @@ def transfer_options(cls, obj, new_obj, backend=None): options = [] for group in Options._option_groups: opts = cls.lookup_options(backend, obj, group) - if opts and opts.kwargs: options.append(Options(group, **opts.kwargs)) + new_opts = cls.lookup_options(backend, new_obj, group, defaults=False) + filtered = {k: v for k, v in opts.kwargs.items() + if (names is None or k in names) and k not in new_opts.kwargs} + if opts and filtered: options.append(Options(group, **filtered)) if options: StoreOptions.set_options(new_obj, {spec: options}, backend) From 77fe29c3e3cf407f85b4dbb0d63aff6efe843cc6 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Mon, 30 Nov 2020 19:25:00 +0100 Subject: [PATCH 53/98] Transfer cmap on rasterize --- holoviews/core/operation.py | 12 +++++++++++- holoviews/core/options.py | 4 ++-- holoviews/operation/datashader.py | 2 ++ 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/holoviews/core/operation.py b/holoviews/core/operation.py index 9622b83024..b4e9252fc7 100644 --- a/holoviews/core/operation.py +++ b/holoviews/core/operation.py @@ -3,9 +3,11 @@ the purposes of analysis or visualization. """ import param + from .dimension import ViewableElement from .element import Element from .layout import Layout +from .options import Store from .overlay import NdOverlay, Overlay from .spaces import Callable, HoloMap from . import util, Dataset @@ -77,6 +79,9 @@ class Operation(param.ParameterizedFunction): # the input of the operation to the result _propagate_dataset = True + # Options to transfer from the input element to the transformed element + _transfer_options = [] + @classmethod def search(cls, element, pattern): """ @@ -127,7 +132,6 @@ def _apply(self, element, key=None): for hook in self._preprocess_hooks: kwargs.update(hook(self, element)) - element_pipeline = getattr(element, '_pipeline', None) if hasattr(element, '_in_method'): @@ -138,6 +142,12 @@ def _apply(self, element, key=None): if hasattr(element, '_in_method') and not in_method: element._in_method = in_method + if self._transfer_options: + for backend in Store.loaded_backends(): + Store.transfer_options( + element, ret, backend, self._transfer_options, level=1 + ) + for hook in self._postprocess_hooks: ret = hook(self, ret, **kwargs) diff --git a/holoviews/core/options.py b/holoviews/core/options.py index 5cdb56ad4e..6071489e5b 100644 --- a/holoviews/core/options.py +++ b/holoviews/core/options.py @@ -1280,7 +1280,7 @@ def lookup(cls, backend, obj): @classmethod - def transfer_options(cls, obj, new_obj, backend=None, names=None): + def transfer_options(cls, obj, new_obj, backend=None, names=None, level=3): """ Transfers options for all backends from one object to another. Drops any options defined in the supplied drop list. @@ -1290,7 +1290,7 @@ def transfer_options(cls, obj, new_obj, backend=None, names=None): backend = cls.current_backend if backend is None else backend type_name = type(new_obj).__name__ group = type_name if obj.group == type(obj).__name__ else obj.group - spec = '.'.join([s for s in (type_name, group, obj.label) if s]) + spec = '.'.join([s for s in (type_name, group, obj.label)[:level] if s]) options = [] for group in Options._option_groups: opts = cls.lookup_options(backend, obj, group) diff --git a/holoviews/operation/datashader.py b/holoviews/operation/datashader.py index 4630c3400e..2b05173145 100644 --- a/holoviews/operation/datashader.py +++ b/holoviews/operation/datashader.py @@ -113,6 +113,8 @@ class ResamplingOperation(LinkableOperation): used to represent this internal state is not freed between calls.""") + _transfer_options = ['cmap'] + @bothmethod def instance(self_or_cls,**params): filtered = {k:v for k,v in params.items() if k in self_or_cls.param} From 1f25411c31b7e9ee942958f83550d3a0dc861718 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Mon, 30 Nov 2020 19:26:44 +0100 Subject: [PATCH 54/98] Updated datashader docs --- examples/user_guide/15-Large_Data.ipynb | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/examples/user_guide/15-Large_Data.ipynb b/examples/user_guide/15-Large_Data.ipynb index 36b1100804..db24653668 100644 --- a/examples/user_guide/15-Large_Data.ipynb +++ b/examples/user_guide/15-Large_Data.ipynb @@ -130,7 +130,7 @@ "metadata": {}, "outputs": [], "source": [ - "decimate(points) + rasterize(points) + rasterize(paths).redim(Count=\"pcount\")" + "decimate(points) + rasterize(points) + rasterize(paths)" ] }, { @@ -356,7 +356,7 @@ "\n", "dates = pd.date_range(start=\"2014-01-01\", end=\"2016-01-01\", freq='1D') # or '1min'\n", "curve = hv.Curve((dates, time_series(N=len(dates), sigma = 1)))\n", - "datashade(curve, cmap=[\"blue\"], width=800).opts(width=800)" + "rasterize(curve, width=800).opts(width=800, cmap=['blue'])" ] }, { @@ -376,7 +376,7 @@ "smoothed = rolling(curve, rolling_window=50)\n", "outliers = rolling_outlier_std(curve, rolling_window=50, sigma=2)\n", "\n", - "ds_curve = datashade(curve, cmap=[\"blue\"])\n", + "ds_curve = rasterize(curve).opts(cmap=[\"blue\"])\n", "spread = dynspread(datashade(smoothed, cmap=[\"red\"], width=800),max_px=1) \n", "\n", "(ds_curve * spread * outliers).opts(\n", @@ -408,9 +408,12 @@ "outputs": [], "source": [ "palette = hv.plotting.util.mplcmap_to_palette('viridis', 256)\n", - "rasterized = rasterize(points, dynamic=False).opts(cnorm='eq_hist', nodata=0,\n", - " colorbar=True, tools=['hover'], width=400, cmap='viridis')\n", - "datashaded = datashade(points, dynamic=False, normalization='eq_hist', cmap=palette).opts(width=350)\n", + "\n", + "rasterized = rasterize(points, dynamic=False).opts(\n", + " cnorm='eq_hist', colorbar=True, tools=['hover'], frame_width=350, cmap=palette\n", + ")\n", + "datashaded = datashade(points, dynamic=False, normalization='eq_hist', cmap=palette).opts(frame_width=350)\n", + "\n", "datashaded.opts(title='datashade') + rasterized.opts(title='rasterized equivalent')" ] }, From 5139d5ebde5fd5abb9ca8e85ed5bf3fce02a4e83 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Mon, 30 Nov 2020 19:30:13 +0100 Subject: [PATCH 55/98] Fix categorical label --- holoviews/operation/datashader.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/holoviews/operation/datashader.py b/holoviews/operation/datashader.py index 2b05173145..d51bef287c 100644 --- a/holoviews/operation/datashader.py +++ b/holoviews/operation/datashader.py @@ -318,7 +318,8 @@ def _get_agg_params(self, element, x, y, agg_fn, bounds): vdims = dims[0].clone(column + vdim_suffix) elif category: agg_name = type(agg_fn).__name__.title() - vdims = Dimension('%s %s%s' % (category, agg_name, vdim_suffix), label=agg_name) + agg_label = '%s %s' % (category, agg_name) + vdims = Dimension('%s%s' % (agg_label, vdim_suffix), label=agg_label) if agg_name in ('Count', 'Any'): vdims.nodata = 0 else: From e474ff0cd80d52bb37dadd9c6ac07e9e108b88a2 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Mon, 30 Nov 2020 19:35:35 +0100 Subject: [PATCH 56/98] Fixed boolean handling --- holoviews/core/data/__init__.py | 3 +++ holoviews/tests/element/test_selection.py | 8 ++++---- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/holoviews/core/data/__init__.py b/holoviews/core/data/__init__.py index 97207a5eff..3d67aa6490 100644 --- a/holoviews/core/data/__init__.py +++ b/holoviews/core/data/__init__.py @@ -1069,6 +1069,9 @@ def dimension_values(self, dimension, expanded=True, flat=True): dim = self.get_dimension(dimension, strict=True) values = self.interface.values(self, dim, expanded, flat) if dim.nodata is not None: + # Ensure nodata applies to boolean data in py2m + if sys.version.version_info.major == 2 and values.dtype.kind == 'b': + values = values.astype('int') values = np.where(values==dim.nodata, np.NaN, values) return values diff --git a/holoviews/tests/element/test_selection.py b/holoviews/tests/element/test_selection.py index cf7d2095e6..1ab0ceb4d5 100644 --- a/holoviews/tests/element/test_selection.py +++ b/holoviews/tests/element/test_selection.py @@ -350,10 +350,10 @@ def test_img_selection_geom(self): self.assertEqual(bbox, {'x': np.array([-0.4, 0.6, 0.4, -0.1]), 'y': np.array([-0.1, -0.1, 1.7, 1.7])}) self.assertEqual(expr.apply(img, expanded=True, flat=False), np.array([ - [ True, False, False], - [ True, False, False], - [ False, False, False], - [False, False, False] + [ 1., np.nan, np.nan], + [ 1., np.nan, np.nan], + [np.nan, np.nan, np.nan], + [np.nan, np.nan, np.nan] ])) self.assertEqual(region, Rectangles([]) * Path([list(geom)+[(-0.4, -0.1)]])) From e4dd3e459f5bfc2dbbadaa0697d970943dde76c6 Mon Sep 17 00:00:00 2001 From: jlstevens Date: Mon, 30 Nov 2020 19:45:57 +0100 Subject: [PATCH 57/98] Fixed missing import --- holoviews/core/data/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/holoviews/core/data/__init__.py b/holoviews/core/data/__init__.py index 3d67aa6490..24c4338935 100644 --- a/holoviews/core/data/__init__.py +++ b/holoviews/core/data/__init__.py @@ -5,6 +5,7 @@ except ImportError: pass +import sys import types import copy From 00274bdd12976f0bd872396b49a9c4d2a41a3a7a Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Mon, 30 Nov 2020 19:46:09 +0100 Subject: [PATCH 58/98] Fix transfer_opts --- holoviews/core/options.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/holoviews/core/options.py b/holoviews/core/options.py index 6071489e5b..d8f0a31fc8 100644 --- a/holoviews/core/options.py +++ b/holoviews/core/options.py @@ -1294,10 +1294,14 @@ def transfer_options(cls, obj, new_obj, backend=None, names=None, level=3): options = [] for group in Options._option_groups: opts = cls.lookup_options(backend, obj, group) + if not opts: + continue new_opts = cls.lookup_options(backend, new_obj, group, defaults=False) + existing = new_opts.kwargs if new_opts else {} filtered = {k: v for k, v in opts.kwargs.items() - if (names is None or k in names) and k not in new_opts.kwargs} - if opts and filtered: options.append(Options(group, **filtered)) + if (names is None or k in names) and k not in existing} + if filtered: + options.append(Options(group, **filtered)) if options: StoreOptions.set_options(new_obj, {spec: options}, backend) From 3e8519228ad53f203cc6f9f7a6b5bdce1f91ec4c Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Mon, 30 Nov 2020 19:59:24 +0100 Subject: [PATCH 59/98] Fix version check --- holoviews/core/data/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/holoviews/core/data/__init__.py b/holoviews/core/data/__init__.py index 24c4338935..1443189288 100644 --- a/holoviews/core/data/__init__.py +++ b/holoviews/core/data/__init__.py @@ -1070,8 +1070,8 @@ def dimension_values(self, dimension, expanded=True, flat=True): dim = self.get_dimension(dimension, strict=True) values = self.interface.values(self, dim, expanded, flat) if dim.nodata is not None: - # Ensure nodata applies to boolean data in py2m - if sys.version.version_info.major == 2 and values.dtype.kind == 'b': + # Ensure nodata applies to boolean data in py2 + if sys.version_info.major == 2 and values.dtype.kind == 'b': values = values.astype('int') values = np.where(values==dim.nodata, np.NaN, values) return values From 6d489e579ed19750b37231a9502a0867046be003 Mon Sep 17 00:00:00 2001 From: jlstevens Date: Mon, 30 Nov 2020 21:05:07 +0100 Subject: [PATCH 60/98] Skipping Polygon rasterization if spatialpandas unavailable --- examples/user_guide/15-Large_Data.ipynb | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/examples/user_guide/15-Large_Data.ipynb b/examples/user_guide/15-Large_Data.ipynb index db24653668..788034d55e 100644 --- a/examples/user_guide/15-Large_Data.ipynb +++ b/examples/user_guide/15-Large_Data.ipynb @@ -533,8 +533,12 @@ "shadeable += [hv.Graph(((np.zeros(N), np.arange(N)),))]\n", "shadeable += [tri.edgepaths]\n", "shadeable += [tri]\n", - "shadeable += [hv.Polygons([county for county in counties.values() if county['state'] == 'tx'], ['lons', 'lats'], ['name'])]\n", "shadeable += [hv.operation.contours(hv.Image((x,y,z)), levels=10)]\n", + "try:\n", + " import spatialpandas # Needed for datashader polygon support\n", + " shadeable += [hv.Polygons([county for county in counties.values() \n", + " if county['state'] == 'tx'], ['lons', 'lats'], ['name'])]\n", + "except: pass\n", "\n", "rasterizable = [hv.RGB(np.dstack([r,g,b])), hv.HSV(np.dstack([g,b,r]))]\n", "\n", From 44d8591cb7b8821c41a281922a4acfcfa0f8061a Mon Sep 17 00:00:00 2001 From: "James A. Bednar" Date: Mon, 30 Nov 2020 19:11:51 -0600 Subject: [PATCH 61/98] Fixed clim_percentile and eq_hist docs --- examples/user_guide/04-Style_Mapping.ipynb | 4 ++-- holoviews/plotting/bokeh/element.py | 4 ++-- holoviews/plotting/mpl/element.py | 4 ++-- holoviews/plotting/plotly/element.py | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/examples/user_guide/04-Style_Mapping.ipynb b/examples/user_guide/04-Style_Mapping.ipynb index 2b55179c0b..8a03443b36 100644 --- a/examples/user_guide/04-Style_Mapping.ipynb +++ b/examples/user_guide/04-Style_Mapping.ipynb @@ -578,8 +578,8 @@ "source": [ "## Other colormapping options\n", "\n", - "* ``clim_percentile``: Percentile value to compute colorscale robust to outliers. If `True` uses 2nd and 98th percentile, otherwise uses the specified percentile value. \n", - "* ``cnorm``: Color normalization to be applied during colormapping. Allows switching between 'linear', 'log' and 'eqhist'.\n", + "* ``clim_percentile``: Percentile value to compute colorscale robust to outliers. If `True`, uses 2nd and 98th percentile; otherwise uses the specified percentile value. \n", + "* ``cnorm``: Color normalization to be applied during colormapping. Allows switching between 'linear', 'log', and 'eq_hist'.\n", "* ``logz``: Enable logarithmic color scale (same as `cnorm='log'`; to be deprecated at some point)\n", "* ``symmetric``: Ensures that the color scale is centered on zero (e.g. ``symmetric=True``)" ] diff --git a/holoviews/plotting/bokeh/element.py b/holoviews/plotting/bokeh/element.py index b86154a589..421ebd07bb 100644 --- a/holoviews/plotting/bokeh/element.py +++ b/holoviews/plotting/bokeh/element.py @@ -1706,8 +1706,8 @@ class ColorbarPlot(ElementPlot): clim_percentile = param.ClassSelector(default=False, class_=(int, float, bool), doc=""" Percentile value to compute colorscale robust to outliers. If - True uses 2nd and 98th percentile, otherwise uses the specified - percentile value.""") + True, uses 2nd and 98th percentile; otherwise uses the specified + numerical percentile value.""") cformatter = param.ClassSelector( default=None, class_=(util.basestring, TickFormatter, FunctionType), doc=""" diff --git a/holoviews/plotting/mpl/element.py b/holoviews/plotting/mpl/element.py index 8e79440782..41185ba20a 100644 --- a/holoviews/plotting/mpl/element.py +++ b/holoviews/plotting/mpl/element.py @@ -672,8 +672,8 @@ class ColorbarPlot(ElementPlot): clim_percentile = param.ClassSelector(default=False, class_=(int, float, bool), doc=""" Percentile value to compute colorscale robust to outliers. If - True uses 2nd and 98th percentile, otherwise uses the specified - percentile value.""") + True, uses 2nd and 98th percentile; otherwise uses the specified + numerical percentile value.""") cformatter = param.ClassSelector( default=None, class_=(util.basestring, ticker.Formatter, FunctionType), doc=""" diff --git a/holoviews/plotting/plotly/element.py b/holoviews/plotting/plotly/element.py index 7e302c8ef4..da58d08bc7 100644 --- a/holoviews/plotting/plotly/element.py +++ b/holoviews/plotting/plotly/element.py @@ -578,8 +578,8 @@ class ColorbarPlot(ElementPlot): clim_percentile = param.ClassSelector(default=False, class_=(int, float, bool), doc=""" Percentile value to compute colorscale robust to outliers. If - True uses 2nd and 98th percentile, otherwise uses the specified - percentile value.""") + True, uses 2nd and 98th percentile; otherwise uses the specified + numerical percentile value.""") colorbar = param.Boolean(default=False, doc=""" Whether to display a colorbar.""") From 27af0b37ae6362f0ef2246d3d1b3f1f1dc89e9e6 Mon Sep 17 00:00:00 2001 From: "James A. Bednar" Date: Mon, 30 Nov 2020 21:21:07 -0600 Subject: [PATCH 62/98] Updated 14-Large_Data.ipynb to use rasterize throughout --- examples/user_guide/15-Large_Data.ipynb | 87 +++++++++++++++---------- 1 file changed, 53 insertions(+), 34 deletions(-) diff --git a/examples/user_guide/15-Large_Data.ipynb b/examples/user_guide/15-Large_Data.ipynb index 788034d55e..0b071cb6f2 100644 --- a/examples/user_guide/15-Large_Data.ipynb +++ b/examples/user_guide/15-Large_Data.ipynb @@ -6,9 +6,9 @@ "source": [ "# Working with large data using Datashader\n", "\n", - "The various plotting-library backends supported by HoloViews, such as Matplotlib, Bokeh, and Plotly, each have a variety of limitations on the amount of data that is practical to work with. Bokeh and Plotly in particular mirror your data directly into an HTML page viewable in your browser, which can cause problems when data sizes approach the limited memory available for each web page in current browsers.\n", + "The various plotting-library backends supported by HoloViews, such as Matplotlib, Bokeh, and Plotly, each have limitations on the amount of data that is practical to work with. Bokeh and Plotly in particular mirror your data directly into an HTML page viewable in your browser, which can cause problems when data sizes approach the limited memory available for each web page in current browsers.\n", "\n", - "Luckily, a visualization of even the largest dataset will be constrained by the resolution of your display device, and so one approach to handling such data is to pre-render or rasterize the data into a fixed-size array or image *before* sending it to the backend plotting library and to your local web browser. The [Datashader](https://github.com/bokeh/datashader) library provides a high-performance big-data server-side rasterization pipeline that works seamlessly with HoloViews to support datasets that are orders of magnitude larger than those supported natively by the plotting-library backends, including millions or billions of points even on ordinary laptops.\n", + "Luckily, a visualization of even the largest dataset will be constrained by the resolution of your display device, and so one approach to handling such data is to pre-render or rasterize the data into a fixed-size array or image *before* sending it to the backend plotting library and thus to your local web browser. The [Datashader](https://github.com/bokeh/datashader) library provides a high-performance big-data server-side rasterization pipeline that works seamlessly with HoloViews to support datasets that are orders of magnitude larger than those supported natively by the plotting-library backends, including millions or billions of points even on ordinary laptops.\n", "\n", "Here, we will see how and when to use Datashader with HoloViews Elements and Containers. For simplicity in this discussion we'll focus on simple synthetic datasets, but [Datashader's examples](http://datashader.org/topics) include a wide variety of real datasets that give a much better idea of the power of using Datashader with HoloViews, and [HoloViz.org](http://holoviz.org) shows how to install and work with HoloViews and Datashader together.\n", "\n", @@ -137,13 +137,22 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Decimating a plot in this way can be useful, but it discards most of the data, yet still suffers from overplotting. If you have Datashader installed, you can instead use the `datashade()` operation to create a dynamic Datashader-based Bokeh plot. The middle plot above shows the result of using `datashade()` to create a dynamic Datashader-based plot out of an Element with arbitrarily large data. In the Datashader version, a new image is regenerated automatically on every zoom or pan event, revealing all the data available at that zoom level and avoiding issues with overplotting by dynamically rescaling the colors used. The same process is used for the line-based data in the Paths plot.\n", + "Decimating a plot in this way can be useful, but it discards most of the data, yet still suffers from overplotting. If you have Datashader installed, you can instead use Datashader operations like `rasterize()` to create a dynamic Datashader-based Bokeh plot. The middle plot above shows the result of using `rasterize()` to create a dynamic Datashader-based plot out of an Element with arbitrarily large data. In the rasterized version, the data is binned into a fixed-size 2D array automatically on every zoom or pan event, revealing all the data available at that zoom level and avoiding issues with overplotting by dynamically rescaling the colors used. Each pixel is colored by how many datapoints fall in that pixel, faithfully revealing the data's distribution in a easy-to-display plot. The same process is used for the line-based data in the Paths plot.\n", "\n", - "These two Datashader-based plots are similar to the native Bokeh plots above, but instead of making a static Bokeh plot that embeds points or line segments directly into the browser, HoloViews sets up a Bokeh plot with dynamic callbacks that render the data as an RGB image using Datashader instead. The dynamic re-rendering provides an interactive user experience even though the data itself is never provided directly to the browser. Of course, because the full data is not in the browser, a static export of this page (e.g. on holoviews.org or on anaconda.org) will only show the initially rendered version, and will not update with new images when zooming as it will when there is a live Python process available.\n", + "These two Datashader-based plots are similar to the native Bokeh plots above, but instead of making a static Bokeh plot that embeds points or line segments directly into the browser, HoloViews sets up a Bokeh plot with dynamic callbacks instructing Datashader to rasterize the data into a fixed-size array (effectivey a 2D histogram) instead. The dynamic re-rendering provides an interactive user experience, even though the data itself is never provided directly to the browser. Of course, because the full data is not in the browser, a static export of this page (e.g. on holoviews.org or on anaconda.org) will only show the initially rendered version, and will not update with new rasterized arrays when zooming as it will when there is a live Python process available.\n", "\n", - "Though you can no longer have a completely interactive exported file, with the Datashader version on a live server you can now change the number of data points from 1000000 to 10000000 or more to see how well your machine will handle larger datasets. It will get a bit slower, but if you have enough memory, it should still be very usable, and should never crash your browser as transferring the whole dataset into your browser would. If you don't have enough memory, you can instead set up a [Dask](http://dask.pydata.org) dataframe as shown in other Datashader examples, which will provide out-of-core and/or distributed processing to handle even the largest datasets.\n", + "Though you can no longer have a completely interactive exported file, with the Datashader version on a live server you can now change the number of data points from 1000000 to 10000000 or more to see how well your machine will handle larger datasets. It will get a bit slower, but if you have enough memory, it should still be very usable, and should never crash your browser as transferring the whole dataset into your browser would. If you don't have enough memory, you can instead set up a [Dask](http://dask.pydata.org) dataframe as shown in other Datashader examples, which will provide out-of-core and/or distributed processing to handle even the largest datasets if you have enough computational power and memory or are willing to wait for out-of-core computation." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# `rasterize()`, `shade()`, and `datashade()`\n", + "\n", + "`rasterize()` uses Datashader to render the data into what is by default a 2D histogram, where every array cell counts the data points falling into that pixel. Bokeh then colormaps that array, turning each cell into a pixel in an image. \n", "\n", - "The `datashade()` operation is actually a \"macro\" or shortcut that combines the two main computations done by datashader, namely `shade()` and `rasterize()`:" + "Instead of having Bokeh do the colormapping, you can instruct Datashader to do so, by wrapping the output of `rasterize()` in a call to `shade()`, where `shade()` is Datashader's colormapping function. The `datashade()` operation is also provided as a simple macro, where `datashade(x)` is equivalent to `shade(rasterize(x))`:" ] }, { @@ -152,16 +161,18 @@ "metadata": {}, "outputs": [], "source": [ - "rasterize(points).hist() + shade(rasterize(points)) + datashade(points)" + "rasterize(points).opts(width=350, cmap=kbc_r, colorbar=True, tools=[\"hover\"]).hist() + \\\n", + "shade(rasterize(points), cmap=kbc_r, normalization=\"linear\") + \\\n", + "datashade(points, cmap=kbc_r, normalization=\"linear\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "In all three of the above plots, `rasterize()` is being called to aggregate the data (a large set of x,y locations) into a rectangular grid, with each grid cell counting up the number of points that fall into it. In the plot on the left, only `rasterize()` is done, and the resulting numeric array of counts is passed to Bokeh for colormapping. Bokeh can then use dynamic (client-side, browser-based) operations in JavaScript, allowing users to have dynamic control over even static HTML plots. For instance, in this case, users can use the Box Select tool and select a range of the histogram shown, dynamically remapping the colors used in the plot to cover the selected range.\n", + "In all three of the above plots, `rasterize()` is being called to aggregate the data (a large set of x,y locations) into a rectangular grid, with each grid cell counting up the number of points that fall into it. In the plot on the left, only `rasterize()` is done, and the resulting numeric array of counts is passed to Bokeh for colormapping. That way hover and colorbars can be supported (as shown), and Bokeh can then provide dynamic (client-side, browser-based) colormapping tools in JavaScript, allowing users to have dynamic control over even static HTML plots. For instance, in this case, users can use the Box Select tool and select a range of the histogram shown, dynamically remapping the colors used in the plot to cover the selected range.\n", "\n", - "The other two plots should be identical. In both cases, the numerical array output of `rasterize()` is mapped into RGB colors by Datashader itself, in Python (\"server-side\"), which allows special Datashader computations like the histogram-equalization in the above plots and the \"spreading\" discussed below. The `shade()` and `datashade()` operations accept a `cmap` argument that lets you control the colormap used, which can be selected to match the HoloViews/Bokeh `cmap` option but is strictly independent of it. See ``hv.help(rasterize)``, ``hv.help(shade)``, and ``hv.help(datashade)`` for options that can be selected, and the [Datashader web site](http://datashader.org) for all the details. The lower-level `aggregate()` and `regrid()` give more control over how the data is aggregated.\n", + "The other two plots should be identical in appearance, but with the numerical array output of `rasterize()` mapped into RGB colors by Datashader itself, in Python (\"server-side\"), which allows some special Datashader computations described below. Here we've instructed Datashader to use the same colormap used by bokeh, so that the plots look similar, but as you can see the `rasterize()` colormap is determined by a HoloViews plot option, while the `shade` and `datashade` colormap is determined by an argument to those operations. See ``hv.help(rasterize)``, ``hv.help(shade)``, and ``hv.help(datashade)`` for options that can be selected, and the [Datashader web site](http://datashader.org) for all the details. HoloViews also provides lower-level `aggregate()` and `regrid()` operations that implement `rasterize()` and give more control over how the data is aggregated, but these are not needed for typical usage.\n", "\n", "Since datashader only sends the data currently in view to the plotting backend, the default behavior is to rescale colormap to the range of the visible data as the zoom level changes. This behavior may not be desirable when working with images; to instead use a fixed colormap range, the `clim` parameter can be passed to the `bokeh` backend via the `opts()` method. Note that this approach works with `rasterize()` where the colormapping is done by the `bokeh` backend. With `datashade()`, the colormapping is done with the `shade()` function which takes a `clims` parameter directly instead of passing additional parameters to the backend via `opts()`." ] @@ -207,14 +218,14 @@ "\n", "The Datashader examples above treat points and lines as infinitesimal in width, such that a given point or small bit of line segment appears in at most one pixel. This approach ensures that the overall distribution of the points will be mathematically well founded -- each pixel will scale in value directly by the number of points that fall into it, or by the lines that cross it.\n", "\n", - "However, many monitors are sufficiently high resolution that the resulting point or line can be difficult to see---a single pixel may not actually be visible on its own, and its color may likely be very difficult to make out. To compensate for this, HoloViews provides access to Datashader's image-based \"spreading\", which makes isolated pixels \"spread\" into adjacent ones for visibility. There are two varieties of spreading supported:\n", + "However, many monitors are sufficiently high resolution that the resulting point or line can be difficult to see---a single pixel may not actually be visible on its own, and its color may likely be very difficult to make out. To compensate for this, HoloViews provides access to Datashader's raster-based \"spreading\", which makes isolated nonzero cells \"spread\" into adjacent ones for visibility. There are two varieties of spreading supported:\n", "\n", - "1. ``spread``: fixed spreading of a certain number of pixels, which is useful if you want to be sure how much spreading is done regardless of the properties of the data.\n", - "2. ``dynspread``: spreads up to a maximum size as long as it does not exceed a specified fraction of adjacency between pixels. \n", + "1. ``spread``: fixed spreading of a certain number of cells (pixels), which is useful if you want to be sure how much spreading is done regardless of the properties of the data.\n", + "2. ``dynspread``: spreads up to a maximum size as long as it does not exceed a specified fraction of adjacency between cells (pixels).\n", "\n", - "Dynamic spreading is typically more useful, because it adjusts depending on how close the datapoints are to each other on screen. Both types of spreading require Datashader to do the colormapping (applying `shade`), because they operate on RGB pixels, not data arrays.\n", + "Dynamic spreading is typically more useful for interactive plotting, because it adjusts depending on how close the datapoints are to each other on screen. As of Datashader 0.11.2, both types of spreading are supported for both `rasterize()` and `shade()`, but previous Datashader versions only support spreading on the RGB output of `shade()`.\n", "\n", - "You can compare the results in the two plots below after zooming in:" + "You can compare the results in the two zoomed-in plots below, then zoom out to see that the plots are the same when points are clustered together to form a distribution:" ] }, { @@ -223,7 +234,8 @@ "metadata": {}, "outputs": [], "source": [ - "datashade(points) + dynspread(datashade(points))" + "(rasterize(points) + dynspread(rasterize(points)))\\\n", + ".opts(opts.Image(cnorm='eq_hist', xlim=(0.1,0.2), ylim=(0.1,0.2)))" ] }, { @@ -241,7 +253,7 @@ "metadata": {}, "outputs": [], "source": [ - "datashade(paths) + dynspread(datashade(paths))" + "rasterize(paths) + dynspread(rasterize(paths), threshold=0.6)" ] }, { @@ -286,7 +298,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Above you can see that (as of Datashader 0.11) categorical aggregates can take any reduction function, either `count`ing the datapoints (left) or reporting some other statistic (e.g. the mean value of a column, right).\n", + "Above you can see that (as of Datashader 0.11) categorical aggregates can take any reduction function, either `count`ing the datapoints (left) or reporting some other statistic (e.g. the mean value of a column, right). This type of categorical mixing is currently only supported by `shade()` and `datashade()`, not `rasterize()` alone, because it depends on Datashader's custom color mixing code.\n", "\n", "Categorical aggregates are one way to allow separate lines or other shapes to be visually distinctive from one another while avoiding obscuring data due to overplotting:" ] @@ -387,18 +399,19 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Note that the above plot will look blocky in a static export (such as on anaconda.org), because the exported version is generated without taking the size of the actual plot (using default height and width for Datashader) into account, whereas the live notebook automatically regenerates the plot to match the visible area on the page. The result of all these operations can be laid out, overlaid, selected, and sampled just like any other HoloViews element, letting you work naturally with even very large datasets.\n", + "The result of all these operations can be laid out, overlaid, selected, and sampled just like any other HoloViews element, letting you work naturally with even very large datasets.\n", "\n", + "Note that the above plot will look blocky in a static export (such as on anaconda.org), because the exported version is generated without taking the size of the actual plot (using default height and width for Datashader) into account, whereas the live notebook automatically regenerates the plot to match the visible area on the page. \n", "\n", "# Hover info and colorbars\n", "\n", "Prior to HoloViews 1.14 there was a tradeoff between using `datashade` and `rasterize`:\n", "\n", - "* `datashade` would apply histogram equalization by default and map missing values to zero alpha pixels (i.e turn missing data transparent). The downside was that the RGB output could not support colorbars or Bokeh hover.\n", + "* `datashade` would apply histogram equalization by default and map missing values to zero alpha pixels (i.e. turn missing data transparent). The downside was that the RGB output could not support colorbars or Bokeh hover.\n", "\n", "* `rasterize` could support client-side color mapping including colorbars and hover information displaying the true aggregated values but could not support histogram equalization or easily map missing values to transparency.\n", "\n", - "As of version HoloViews 1.14, `rasterize` can be made strictly superior to `datashade` by setting a few options namely `cnorm` and `nodata`:" + "As of version HoloViews 1.14, for non-categorical data `rasterize` can be made strictly superior to `datashade` by setting a few options, namely `cnorm` and `nodata`:" ] }, { @@ -427,7 +440,7 @@ "* The data in the `Image` elements returned by `rasterize` are now useful aggregates ('Counts' by default) instead of meaningless RGB values.\n", "* Although slightly longer to specify, the `rasterize` options make the histogram equalization and transparent values explicit.\n", "\n", - "The `nodata` and `cnorm` options works across all backends except for the Plotly backend where `cnorm` is currently not supported. To use `cnorm='eq_hist'` as in the above example, you will need Bokeh version 2.2.3 or greater.\n", + "The `nodata` and `cnorm` options works across all backends except for the Plotly backend, where `cnorm` is currently not supported. To use `cnorm='eq_hist'` as in the above example, you will need Bokeh version 2.2.3 or greater.\n", "\n", "If you can satisfy these version requirements, the `rasterize` operation is now encouraged over the `datashade` operation in all supported cases. Cases not yet supported include Datashader's rarely used `cbrt` (cube root) colormapping option, along with its [categorical color mixing](https://datashader.org/getting_started/Pipeline.html#Transformation) support (for which `shade()` or `datashade()` is still needed, and thus hover and colorbars will not be supported)." ] @@ -454,7 +467,7 @@ "\n", "- **points**: [`hv.Nodes`](../reference/elements/bokeh/Graph.ipynb), [`hv.Points`](../reference/elements/bokeh/Points.ipynb), [`hv.Scatter`](../reference/elements/bokeh/Scatter.ipynb)\n", "- **line**: [`hv.Contours`](../reference/elements/bokeh/Contours.ipynb), [`hv.Curve`](../reference/elements/bokeh/Curve.ipynb), [`hv.Path`](../reference/elements/bokeh/Path.ipynb), [`hv.Graph`](../reference/elements/bokeh/Graph.ipynb), [`hv.EdgePaths`](../reference/elements/bokeh/Graph.ipynb), [`hv.Spikes`](../reference/elements/bokeh/Spikes.ipynb), [`hv.Segments`](../reference/elements/bokeh/Segments.ipynb)\n", - "- **area**: [`hv.Area`](../reference/elements/bokeh/Area.ipynb), [`hv.Spread`](../reference/elements/bokeh/Spread.ipynb)\n", + "- **area**: [`hv.Area`](../reference/elements/bokeh/Area.ipynb), [`hv.Rectangles`](../reference/elements/bokeh/Rectangles.ipynb), [`hv.Spread`](../reference/elements/bokeh/Spread.ipynb)\n", "- **raster**: [`hv.Image`](../reference/elements/bokeh/Image.ipynb), [`hv.HSV`](../reference/elements/bokeh/HSV.ipynb), [`hv.RGB`](../reference/elements/bokeh/RGB.ipynb)\n", "- **trimesh**: [`hv.TriMesh`](../reference/elements/bokeh/TriMesh.ipynb)\n", "- **quadmesh**: [`hv.QuadMesh`](../reference/elements/bokeh/QuadMesh.ipynb)\n", @@ -490,11 +503,8 @@ "\n", "from bokeh.sampledata.us_counties import data as counties\n", "\n", - "opts.defaults(\n", - " opts.Image(aspect=1, axiswise=True, xaxis='bare', yaxis='bare'),\n", - " opts.RGB(aspect=1, axiswise=True, xaxis='bare', yaxis='bare'),\n", - " opts.HSV(aspect=1, axiswise=True, xaxis='bare', yaxis='bare'),\n", - " opts.Layout(vspace=0.1, hspace=0.1, sublabel_format=\"\", fig_size=80))\n", + "opts.defaults(opts.Layout(vspace=0.1, hspace=0.1, sublabel_format=\"\", fig_size=48))\n", + "el_opts = dict(aspect=1, axiswise=True, xaxis='bare', yaxis='bare')\n", "\n", "np.random.seed(12)\n", "N=100\n", @@ -536,7 +546,7 @@ "shadeable += [hv.operation.contours(hv.Image((x,y,z)), levels=10)]\n", "try:\n", " import spatialpandas # Needed for datashader polygon support\n", - " shadeable += [hv.Polygons([county for county in counties.values() \n", + " shadeable += [hv.Polygons([county for county in counties.values()\n", " if county['state'] == 'tx'], ['lons', 'lats'], ['name'])]\n", "except: pass\n", "\n", @@ -549,8 +559,18 @@ " hv.Segments: dict(aggregator='any')\n", "}\n", "\n", - "hv.Layout([dynspread(datashade(e.relabel(e.__class__.name), **ds_opts.get(e.__class__, {}))) for e in shadeable] + \n", - " [ rasterize(e.relabel(e.__class__.name)) for e in rasterizable]).opts(shared_axes=False).cols(6)" + "hv.Layout([dynspread(datashade(e.relabel(e.__class__.name), **ds_opts.get(e.__class__, {}))).opts(**el_opts) for e in shadeable] + \n", + " [ rasterize(e.relabel(e.__class__.name)).opts(**el_opts) for e in rasterizable]).opts(shared_axes=False).cols(6)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "hv.Layout([dynspread(rasterize(e.relabel(e.__class__.name), dynamic=False, **ds_opts.get(e.__class__, {}))).opts(**el_opts) for e in shadeable] + \n", + " [ rasterize(e.relabel(e.__class__.name), dynamic=False).opts(**el_opts) for e in rasterizable]).opts(shared_axes=False).cols(6)" ] }, { @@ -568,7 +588,6 @@ "metadata": {}, "outputs": [], "source": [ - "el_opts = dict(aspect=1, axiswise=True, xaxis='bare', yaxis='bare')\n", "hv.Layout([e.relabel(e.__class__.name).opts(**el_opts) for e in shadeable + rasterizable]).cols(6)" ] }, @@ -595,7 +614,7 @@ "curves = {'+':hv.Curve(pts), '-':hv.Curve([(x, -1.0*y) for x, y in pts])}\n", "\n", "supported = [hv.HoloMap(curves,'sign'), hv.Overlay(list(curves.values())), hv.NdOverlay(curves), hv.GridSpace(hv.NdOverlay(curves))]\n", - "hv.Layout([datashade(e.relabel(e.__class__.name)) for e in supported]).cols(4)" + "hv.Layout([rasterize(e.relabel(e.__class__.name)) for e in supported]).cols(4)" ] }, { @@ -604,7 +623,7 @@ "metadata": {}, "outputs": [], "source": [ - "dynspread(datashade(hv.NdLayout(curves,'sign')))" + "dynspread(rasterize(hv.NdLayout(curves,'sign')))" ] }, { @@ -613,7 +632,7 @@ "metadata": {}, "outputs": [], "source": [ - "hv.output(backend='bokeh')" + "hv.output(backend='bokeh') # restore bokeh backend in case cells will run out of order" ] }, { From 104e349ed58a1fdff61d625f4cd783d36d4e4707 Mon Sep 17 00:00:00 2001 From: "James A. Bednar" Date: Mon, 30 Nov 2020 22:35:30 -0600 Subject: [PATCH 63/98] Fixed 15-Large_Data regressions --- examples/user_guide/15-Large_Data.ipynb | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/examples/user_guide/15-Large_Data.ipynb b/examples/user_guide/15-Large_Data.ipynb index 0b071cb6f2..044388cf20 100644 --- a/examples/user_guide/15-Large_Data.ipynb +++ b/examples/user_guide/15-Large_Data.ipynb @@ -30,6 +30,9 @@ "from holoviews.operation.datashader import datashade, shade, dynspread, rasterize\n", "from holoviews.operation import decimate\n", "\n", + "from colorcet import kbc\n", + "kbc_r=kbc[::-1]\n", + "\n", "hv.extension('bokeh','matplotlib')\n", "\n", "decimate.max_samples=1000\n", @@ -503,7 +506,11 @@ "\n", "from bokeh.sampledata.us_counties import data as counties\n", "\n", - "opts.defaults(opts.Layout(vspace=0.1, hspace=0.1, sublabel_format=\"\", fig_size=48))\n", + "opts.defaults(opts.Image(aspect=1, axiswise=True, xaxis='bare', yaxis='bare'),\n", + " opts.RGB(aspect=1, axiswise=True, xaxis='bare', yaxis='bare'),\n", + " opts.HSV(aspect=1, axiswise=True, xaxis='bare', yaxis='bare'),\n", + " opts.Layout(vspace=0.1, hspace=0.1, sublabel_format='', fig_size=48))\n", + "\n", "el_opts = dict(aspect=1, axiswise=True, xaxis='bare', yaxis='bare')\n", "\n", "np.random.seed(12)\n", From 47c8a388185d0858b7f86be46342d1498cfcad9e Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Tue, 1 Dec 2020 11:28:31 +0100 Subject: [PATCH 64/98] Implement nodata range --- holoviews/core/data/cudf.py | 5 ++++- holoviews/core/data/dask.py | 5 ++++- holoviews/core/data/dictionary.py | 2 +- holoviews/core/data/grid.py | 6 +++++- holoviews/core/data/ibis.py | 5 ++++- holoviews/core/data/image.py | 3 +++ holoviews/core/data/interface.py | 12 +++++++++++- holoviews/core/data/pandas.py | 5 ++++- holoviews/core/data/xarray.py | 6 +++++- holoviews/tests/core/data/base.py | 8 ++++++++ 10 files changed, 49 insertions(+), 8 deletions(-) diff --git a/holoviews/core/data/cudf.py b/holoviews/core/data/cudf.py index 3bcaa85317..27a0dd0a9e 100644 --- a/holoviews/core/data/cudf.py +++ b/holoviews/core/data/cudf.py @@ -123,7 +123,10 @@ def init(cls, eltype, data, kdims, vdims): @classmethod def range(cls, dataset, dimension): - column = dataset.data[dataset.get_dimension(dimension, strict=True).name] + dimension = dataset.get_dimension(dimension, strict=True) + column = dataset.data[dimension.name] + if dimension.nodata is not None: + column = cls.replace_value(column, dimension.nodata) if column.dtype.kind == 'O': return np.NaN, np.NaN else: diff --git a/holoviews/core/data/dask.py b/holoviews/core/data/dask.py index 656c255554..841f98e417 100644 --- a/holoviews/core/data/dask.py +++ b/holoviews/core/data/dask.py @@ -85,11 +85,14 @@ def shape(cls, dataset): @classmethod def range(cls, dataset, dimension): import dask.dataframe as dd - column = dataset.data[dataset.get_dimension(dimension).name] + dimension = dataset.get_dimension(dimension, strict=True) + column = dataset.data[dimension.name] if column.dtype.kind == 'O': column = np.sort(column[column.notnull()].compute()) return (column[0], column[-1]) if len(column) else (None, None) else: + if dimension.nodata is not None: + column = cls.replace_value(column, dimension.nodata) return dd.compute(column.min(), column.max()) @classmethod diff --git a/holoviews/core/data/dictionary.py b/holoviews/core/data/dictionary.py index 9085175417..df2e0cd687 100644 --- a/holoviews/core/data/dictionary.py +++ b/holoviews/core/data/dictionary.py @@ -248,7 +248,7 @@ def sort(cls, dataset, by=[], reverse=False): @classmethod def range(cls, dataset, dimension): - dim = dataset.get_dimension(dimension) + dim = dataset.get_dimension(dimension, strict=True) column = dataset.data[dim.name] if isscalar(column): return column, column diff --git a/holoviews/core/data/grid.py b/holoviews/core/data/grid.py index 2a33197f17..9af70d78d7 100644 --- a/holoviews/core/data/grid.py +++ b/holoviews/core/data/grid.py @@ -787,12 +787,16 @@ def iloc(cls, dataset, index): @classmethod def range(cls, dataset, dimension): + dimension = dataset.get_dimension(dimension, strict=True) if dataset._binned and dimension in dataset.kdims: expanded = cls.irregular(dataset, dimension) column = cls.coords(dataset, dimension, expanded=expanded, edges=True) else: column = cls.values(dataset, dimension, expanded=False, flat=False) + if dimension.nodata is not None: + column = cls.replace_value(column, dimension.nodata) + da = dask_array_module() if column.dtype.kind == 'M': dmin, dmax = column.min(), column.max() @@ -824,6 +828,6 @@ def assign(cls, dataset, new_data): data[k] = cls.canonicalize(dataset, v) return data - + Interface.register(GridInterface) diff --git a/holoviews/core/data/ibis.py b/holoviews/core/data/ibis.py index 583bff870a..5873a2c13b 100644 --- a/holoviews/core/data/ibis.py +++ b/holoviews/core/data/ibis.py @@ -98,9 +98,12 @@ def nonzero(cls, dataset): @classmethod @cached def range(cls, dataset, dimension): + dimension = dataset.get_dimension(dimension, strict=True) if cls.dtype(dataset, dimension).kind in 'SUO': return None, None - column = dataset.data[dataset.get_dimension(dimension, strict=True).name] + if dimension.nodata is not None: + return Interface.range(dataset, dimension) + column = dataset.data[dimension.name] return tuple( dataset.data.aggregate([column.min(), column.max()]).execute().values[0, :] ) diff --git a/holoviews/core/data/image.py b/holoviews/core/data/image.py index 6640fdcbad..5c37fd62c8 100644 --- a/holoviews/core/data/image.py +++ b/holoviews/core/data/image.py @@ -135,6 +135,7 @@ def coords(cls, dataset, dim, ordered=False, expanded=False, edges=False): @classmethod def range(cls, obj, dim): + dim = obj.get_dimension(dim, strict=True) dim_idx = obj.get_dimension_index(dim) if dim_idx in [0, 1] and obj.bounds: l, b, r, t = obj.bounds.lbrt() @@ -151,6 +152,8 @@ def range(cls, obj, dim): elif 1 < dim_idx < len(obj.vdims) + 2: dim_idx -= 2 data = np.atleast_3d(obj.data)[:, :, dim_idx] + if dim.nodata is not None: + data = cls.replace_value(data, dim.nodata) drange = (np.nanmin(data), np.nanmax(data)) else: drange = (None, None) diff --git a/holoviews/core/data/interface.py b/holoviews/core/data/interface.py index 957587f298..ceebccaa2e 100644 --- a/holoviews/core/data/interface.py +++ b/holoviews/core/data/interface.py @@ -359,7 +359,17 @@ def dtype(cls, dataset, dimension): else: return data.dtype - + @classmethod + def replace_value(cls, data, nodata): + """ + Replace `nodata` value in data with NaN + """ + data = data.astype('float64') + mask = data != nodata + if hasattr(data, 'where'): + return data.where(mask, np.NaN) + return np.where(mask, data, np.NaN) + @classmethod def select_mask(cls, dataset, selection): """ diff --git a/holoviews/core/data/pandas.py b/holoviews/core/data/pandas.py index 80bcca6c7a..e80f4b7380 100644 --- a/holoviews/core/data/pandas.py +++ b/holoviews/core/data/pandas.py @@ -159,7 +159,8 @@ def validate(cls, dataset, vdims=True): @classmethod def range(cls, dataset, dimension): - column = dataset.data[dataset.get_dimension(dimension, strict=True).name] + dimension = dataset.get_dimension(dimension, strict=True) + column = dataset.data[dimension.name] if column.dtype.kind == 'O': if (not isinstance(dataset.data, pd.DataFrame) or util.LooseVersion(pd.__version__) < '0.17.0'): @@ -174,6 +175,8 @@ def range(cls, dataset, dimension): return np.NaN, np.NaN return column.iloc[0], column.iloc[-1] else: + if dimension.nodata is not None: + column = cls.replace_value(column, dimension.nodata) cmin, cmax = column.min(), column.max() if column.dtype.kind == 'M' and getattr(column.dtype, 'tz', None): return (cmin.to_pydatetime().replace(tzinfo=None), diff --git a/holoviews/core/data/xarray.py b/holoviews/core/data/xarray.py index 70c593f0f2..7330481762 100644 --- a/holoviews/core/data/xarray.py +++ b/holoviews/core/data/xarray.py @@ -248,7 +248,8 @@ def persist(cls, dataset): @classmethod def range(cls, dataset, dimension): - dim = dataset.get_dimension(dimension, strict=True).name + dimension = dataset.get_dimension(dimension, strict=True) + dim = dimension.name if dataset._binned and dimension in dataset.kdims: data = cls.coords(dataset, dim, edges=True) if data.dtype.kind == 'M': @@ -260,6 +261,9 @@ def range(cls, dataset, dimension): data = dataset.data.values[..., dataset.vdims.index(dim)] else: data = dataset.data[dim] + if dimension.nodata is not None: + data = cls.replace_value(data, dimension.nodata) + if len(data): dmin, dmax = data.min().data, data.max().data else: diff --git a/holoviews/tests/core/data/base.py b/holoviews/tests/core/data/base.py index e77ce2ba0a..bd33862623 100644 --- a/holoviews/tests/core/data/base.py +++ b/holoviews/tests/core/data/base.py @@ -530,6 +530,10 @@ def test_dataset_mixed_type_range(self): ds = Dataset((['A', 'B', 'C', None],), 'A') self.assertEqual(ds.range(0), ('A', 'C')) + def test_dataset_nodata_range(self): + table = self.table.clone(vdims=[Dimension('Weight', nodata=10), 'Height']) + self.assertEqual(table.range('Weight'), (15, 18)) + def test_dataset_sort_vdim_ht(self): dataset = Dataset({'x':self.xs, 'y':-self.ys}, kdims=['x'], vdims=['y']) @@ -983,6 +987,10 @@ def test_select_tuple(self): ) self.assertEqual(self.dataset_grid.select(y=(0, 0.25)), ds) + def test_nodata_range(self): + ds = self.dataset_grid.clone(vdims=[Dimension('z', nodata=0)]) + self.assertEqual(ds.range('z'), (1, 5)) + def test_dataset_ndloc_index(self): xs, ys = np.linspace(0.12, 0.81, 10), np.linspace(0.12, 0.391, 5) arr = np.arange(10)*np.arange(5)[np.newaxis].T From 1117d566b1f1e57cb2efaa06125e5dd0f27b8142 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Tue, 1 Dec 2020 11:50:41 +0100 Subject: [PATCH 65/98] Add Rectangles example --- examples/user_guide/15-Large_Data.ipynb | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/examples/user_guide/15-Large_Data.ipynb b/examples/user_guide/15-Large_Data.ipynb index 044388cf20..24fd3af833 100644 --- a/examples/user_guide/15-Large_Data.ipynb +++ b/examples/user_guide/15-Large_Data.ipynb @@ -414,7 +414,7 @@ "\n", "* `rasterize` could support client-side color mapping including colorbars and hover information displaying the true aggregated values but could not support histogram equalization or easily map missing values to transparency.\n", "\n", - "As of version HoloViews 1.14, for non-categorical data `rasterize` can be made strictly superior to `datashade` by setting a few options, namely `cnorm` and `nodata`:" + "As of version HoloViews 1.14, for non-categorical data `rasterize` can be made strictly superior to `datashade` by setting `cnorm`:" ] }, { @@ -423,7 +423,7 @@ "metadata": {}, "outputs": [], "source": [ - "palette = hv.plotting.util.mplcmap_to_palette('viridis', 256)\n", + "palette = hv.plotting.util.process_cmap('viridis', 256)\n", "\n", "rasterized = rasterize(points, dynamic=False).opts(\n", " cnorm='eq_hist', colorbar=True, tools=['hover'], frame_width=350, cmap=palette\n", @@ -543,6 +543,7 @@ "shadeable += [hv.Path(counties[(1, 1)], ['lons', 'lats']), hv.Points(counties[(1, 1)], ['lons', 'lats'])]\n", "shadeable += [hv.Spikes(np.random.randn(10000))]\n", "shadeable += [hv.Segments((np.arange(100), s, np.arange(100), e))]\n", + "shadeable += [hv.Rectangles((np.arange(100)-0.25, s, np.arange(100)+0.25, e, s>e), vdims='sign')]\n", "shadeable += [hv.Area(np.random.randn(10000).cumsum())]\n", "shadeable += [hv.Spread((np.arange(10000), np.random.randn(10000).cumsum(), np.random.randn(10000)*10))]\n", "shadeable += [hv.Image((x,y,z))]\n", @@ -563,7 +564,8 @@ " hv.Path: dict(aggregator='any'),\n", " hv.Points: dict(aggregator='any'),\n", " hv.Polygons: dict(aggregator=ds.count_cat('name'), color_key=hv.plotting.util.process_cmap('glasbey')),\n", - " hv.Segments: dict(aggregator='any')\n", + " hv.Segments: dict(aggregator='any'),\n", + " hv.Rectangles: dict(aggregator=ds.count_cat('sign'), color_key={True: 'red', False: 'green'})\n", "}\n", "\n", "hv.Layout([dynspread(datashade(e.relabel(e.__class__.name), **ds_opts.get(e.__class__, {}))).opts(**el_opts) for e in shadeable] + \n", From 9b6a9c53bc7e6bd58022caeca9bec3fa74dcda5f Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Tue, 1 Dec 2020 11:57:48 +0100 Subject: [PATCH 66/98] Polygons and Rectangles coloring --- examples/user_guide/15-Large_Data.ipynb | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/examples/user_guide/15-Large_Data.ipynb b/examples/user_guide/15-Large_Data.ipynb index 24fd3af833..480935d2dc 100644 --- a/examples/user_guide/15-Large_Data.ipynb +++ b/examples/user_guide/15-Large_Data.ipynb @@ -532,6 +532,8 @@ "Qy = np.sin(Y) + np.sin(X)\n", "Z = np.sqrt(X**2 + Y**2)\n", "\n", + "rect_colors = {True: 'red', False: 'green'}\n", + "rect_opts = opts.Rectangles(lw=0, color=hv.dim('sign').categorize(rect_colors))\n", "s = np.random.randn(100).cumsum()\n", "e = s + np.random.randn(100)\n", "\n", @@ -543,7 +545,7 @@ "shadeable += [hv.Path(counties[(1, 1)], ['lons', 'lats']), hv.Points(counties[(1, 1)], ['lons', 'lats'])]\n", "shadeable += [hv.Spikes(np.random.randn(10000))]\n", "shadeable += [hv.Segments((np.arange(100), s, np.arange(100), e))]\n", - "shadeable += [hv.Rectangles((np.arange(100)-0.25, s, np.arange(100)+0.25, e, s>e), vdims='sign')]\n", + "shadeable += [hv.Rectangles((np.arange(100)-0.4, s, np.arange(100)+0.4, e, s>e), vdims='sign').opts(rect_opts)]\n", "shadeable += [hv.Area(np.random.randn(10000).cumsum())]\n", "shadeable += [hv.Spread((np.arange(10000), np.random.randn(10000).cumsum(), np.random.randn(10000)*10))]\n", "shadeable += [hv.Image((x,y,z))]\n", @@ -555,7 +557,7 @@ "try:\n", " import spatialpandas # Needed for datashader polygon support\n", " shadeable += [hv.Polygons([county for county in counties.values()\n", - " if county['state'] == 'tx'], ['lons', 'lats'], ['name'])]\n", + " if county['state'] == 'tx'], ['lons', 'lats'], ['name']).opts(color='name', cmap='glasbey')]\n", "except: pass\n", "\n", "rasterizable = [hv.RGB(np.dstack([r,g,b])), hv.HSV(np.dstack([g,b,r]))]\n", @@ -565,7 +567,7 @@ " hv.Points: dict(aggregator='any'),\n", " hv.Polygons: dict(aggregator=ds.count_cat('name'), color_key=hv.plotting.util.process_cmap('glasbey')),\n", " hv.Segments: dict(aggregator='any'),\n", - " hv.Rectangles: dict(aggregator=ds.count_cat('sign'), color_key={True: 'red', False: 'green'})\n", + " hv.Rectangles: dict(aggregator=ds.count_cat('sign'), color_key=rect_colors)\n", "}\n", "\n", "hv.Layout([dynspread(datashade(e.relabel(e.__class__.name), **ds_opts.get(e.__class__, {}))).opts(**el_opts) for e in shadeable] + \n", From dc7296d5de796943cf3694dec8e8b590b720a2f8 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Tue, 1 Dec 2020 12:04:33 +0100 Subject: [PATCH 67/98] Switch to vdim_prefix --- holoviews/operation/datashader.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/holoviews/operation/datashader.py b/holoviews/operation/datashader.py index d51bef287c..df71b6e2c8 100644 --- a/holoviews/operation/datashader.py +++ b/holoviews/operation/datashader.py @@ -230,9 +230,9 @@ class AggregationOperation(ResamplingOperation): no column is defined the first value dimension of the element will be used. May also be defined as a string.""") - vdim_suffix = param.String(default=' over {kdims}', doc=""" - Suffix to add to value dimension name where {kdims} templates - in the names of the input element key dimensions""") + vdim_prefix = param.String(default='{kdims} ', doc=""" + Prefix to prepend to value dimension name where {kdims} + templates in the names of the input element key dimensions.""") _agg_methods = { 'any': rd.any, @@ -299,7 +299,7 @@ def _get_agg_params(self, element, x, y, agg_fn, bounds): datatype=['xarray'], bounds=bounds) kdim_list = '_'.join(str(kd) for kd in params['kdims']) - vdim_suffix = self.vdim_suffix.format(kdims=kdim_list) + vdim_prefix = self.vdim_prefix.format(kdims=kdim_list) category = None if hasattr(agg_fn, 'reduction'): @@ -319,12 +319,12 @@ def _get_agg_params(self, element, x, y, agg_fn, bounds): elif category: agg_name = type(agg_fn).__name__.title() agg_label = '%s %s' % (category, agg_name) - vdims = Dimension('%s%s' % (agg_label, vdim_suffix), label=agg_label) + vdims = Dimension('%s%s' % (vdim_prefix, agg_label), label=agg_label) if agg_name in ('Count', 'Any'): vdims.nodata = 0 else: agg_name = type(agg_fn).__name__.title() - vdims = Dimension('%s%s' % (agg_name, vdim_suffix), label=agg_name, nodata=0) + vdims = Dimension('%s%s' % (vdim_prefix, agg_name), label=agg_name, nodata=0) params['vdims'] = vdims return params From e88aa5622dbb5511c38057a85523da465ab99690 Mon Sep 17 00:00:00 2001 From: jlstevens Date: Tue, 1 Dec 2020 12:24:08 +0100 Subject: [PATCH 68/98] Fixed lingering vdim_suffix references --- holoviews/operation/datashader.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/holoviews/operation/datashader.py b/holoviews/operation/datashader.py index df71b6e2c8..a933ef122a 100644 --- a/holoviews/operation/datashader.py +++ b/holoviews/operation/datashader.py @@ -313,9 +313,9 @@ def _get_agg_params(self, element, x, y, agg_fn, bounds): "Ensure the aggregator references an existing " "dimension." % (column,element)) if isinstance(agg_fn, ds.count_cat): - vdims = dims[0].clone('%s Count%s' % (column, vdim_suffix), nodata=0) + vdims = dims[0].clone('%s %s Count' % (vdim_prefix, column), nodata=0) else: - vdims = dims[0].clone(column + vdim_suffix) + vdims = dims[0].clone(vdim_prefix + column) elif category: agg_name = type(agg_fn).__name__.title() agg_label = '%s %s' % (category, agg_name) From 60c2e7bf2c223a3a8836dccba7355b0233837e1e Mon Sep 17 00:00:00 2001 From: jlstevens Date: Tue, 1 Dec 2020 12:37:52 +0100 Subject: [PATCH 69/98] Improved error message in spreading operation for wrong input types --- holoviews/operation/datashader.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/holoviews/operation/datashader.py b/holoviews/operation/datashader.py index a933ef122a..89d25b4a31 100644 --- a/holoviews/operation/datashader.py +++ b/holoviews/operation/datashader.py @@ -1586,7 +1586,8 @@ def _process(self, element, key=None): elif isinstance(element, Image): data = element.clone(datatype=['xarray']).data[element.vdims[0].name] else: - raise ValueError('spreading can only be applied to Image or RGB Elements.') + raise ValueError('spreading can only be applied to Image or RGB Elements. ' + 'Received object of type %s' % str(type(element))) kwargs = {} array = self._apply_spreading(data) From ff852df5758081482724276c41e6261bc16255b7 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Tue, 1 Dec 2020 12:57:56 +0100 Subject: [PATCH 70/98] Implement per_element for dynamic Operations --- holoviews/core/operation.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/holoviews/core/operation.py b/holoviews/core/operation.py index b4e9252fc7..cbeea9c054 100644 --- a/holoviews/core/operation.py +++ b/holoviews/core/operation.py @@ -178,6 +178,9 @@ def process_element(self, element, key, **params): The process_element method allows a single element to be operated on given an externally supplied key. """ + if self._per_element and not isinstance(element, Element): + return element.clone({k: self.process_element(el, key, **params) + for k, el in element.items()}) if hasattr(self, 'p'): if self._allow_extra_keywords: extras = self.p._extract_extra_keywords(params) From d8c49c5cf20ffe9040d0a159a39854ee31990a14 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Tue, 1 Dec 2020 12:58:20 +0100 Subject: [PATCH 71/98] Support match_spec on numpy booleans --- holoviews/core/util.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/holoviews/core/util.py b/holoviews/core/util.py index 5968e3d198..469c1a8ba8 100644 --- a/holoviews/core/util.py +++ b/holoviews/core/util.py @@ -2208,7 +2208,9 @@ def closest_match(match, specs, depth=0): match_length = max(i for i in range(len(match[0])) if match[0].startswith(spec[0][:i])) elif is_number(match[0]) and is_number(spec[0]): - match_length = -abs(match[0]-spec[0]) + m = bool(match[0]) if isinstance(match[0], np.bool_) else match[0] + s = bool(spec[0]) if isinstance(spec[0], np.bool_) else spec[0] + match_length = -abs(m-s) else: match_length = 0 match_lengths.append((i, match_length, spec[0])) From 4570f7a4d2709943a380fe590aa05b56a68e9ac3 Mon Sep 17 00:00:00 2001 From: jlstevens Date: Tue, 1 Dec 2020 13:15:12 +0100 Subject: [PATCH 72/98] Removed unnecessary uses of dynamic=False --- examples/user_guide/15-Large_Data.ipynb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/examples/user_guide/15-Large_Data.ipynb b/examples/user_guide/15-Large_Data.ipynb index 480935d2dc..99d6bfa63d 100644 --- a/examples/user_guide/15-Large_Data.ipynb +++ b/examples/user_guide/15-Large_Data.ipynb @@ -425,10 +425,10 @@ "source": [ "palette = hv.plotting.util.process_cmap('viridis', 256)\n", "\n", - "rasterized = rasterize(points, dynamic=False).opts(\n", + "rasterized = rasterize(points).opts(\n", " cnorm='eq_hist', colorbar=True, tools=['hover'], frame_width=350, cmap=palette\n", ")\n", - "datashaded = datashade(points, dynamic=False, normalization='eq_hist', cmap=palette).opts(frame_width=350)\n", + "datashaded = datashade(points, normalization='eq_hist', cmap=palette).opts(frame_width=350)\n", "\n", "datashaded.opts(title='datashade') + rasterized.opts(title='rasterized equivalent')" ] @@ -580,8 +580,8 @@ "metadata": {}, "outputs": [], "source": [ - "hv.Layout([dynspread(rasterize(e.relabel(e.__class__.name), dynamic=False, **ds_opts.get(e.__class__, {}))).opts(**el_opts) for e in shadeable] + \n", - " [ rasterize(e.relabel(e.__class__.name), dynamic=False).opts(**el_opts) for e in rasterizable]).opts(shared_axes=False).cols(6)" + "hv.Layout([dynspread(rasterize(e.relabel(e.__class__.name), **ds_opts.get(e.__class__, {}))).opts(**el_opts) for e in shadeable] + \n", + " [ rasterize(e.relabel(e.__class__.name)).opts(**el_opts) for e in rasterizable]).opts(shared_axes=False).cols(6)" ] }, { From 0a80c0591e7c5fc27d969d56ef54b4a81b61fcb9 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Tue, 1 Dec 2020 13:42:46 +0100 Subject: [PATCH 73/98] Update vdim_prefix in tests --- holoviews/tests/operation/testdatashader.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/holoviews/tests/operation/testdatashader.py b/holoviews/tests/operation/testdatashader.py index 03caa03814..22c24e6d5f 100644 --- a/holoviews/tests/operation/testdatashader.py +++ b/holoviews/tests/operation/testdatashader.py @@ -42,7 +42,7 @@ numba_logger = logging.getLogger('numba') numba_logger.setLevel(logging.WARNING) -AggregationOperation.vdim_suffix = '' +AggregationOperation.vdim_prefix = '' class DatashaderAggregateTests(ComparisonTestCase): """ From c6cc3ebf64fae3d2b60ed13f9e9b547e95b98a4d Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Tue, 1 Dec 2020 13:52:00 +0100 Subject: [PATCH 74/98] Update default colormap for Path and geometry types --- holoviews/plotting/bokeh/__init__.py | 8 +++++--- holoviews/plotting/mpl/__init__.py | 8 +++++--- holoviews/plotting/plotly/__init__.py | 8 ++++---- 3 files changed, 14 insertions(+), 10 deletions(-) diff --git a/holoviews/plotting/bokeh/__init__.py b/holoviews/plotting/bokeh/__init__.py index f0cbccebd0..047835126f 100644 --- a/holoviews/plotting/bokeh/__init__.py +++ b/holoviews/plotting/bokeh/__init__.py @@ -182,13 +182,15 @@ def colormap_generator(palette): # Paths options.Contours = Options('plot', show_legend=True) -options.Contours = Options('style', color=Cycle(), cmap='viridis') -options.Path = Options('style', color=Cycle(), cmap='viridis') +options.Contours = Options('style', color=Cycle(), cmap=dflt_chart_cmap) +options.Path = Options('style', color=Cycle(), cmap=dflt_chart_cmap) options.Box = Options('style', color='black') options.Bounds = Options('style', color='black') options.Ellipse = Options('style', color='black') options.Polygons = Options('style', color=Cycle(), line_color='black', - cmap='viridis') + cmap=dflt_chart_cmap) +options.Rectangles = Options('style', cmap=dflt_chart_cmap) +options.Segments = Options('style', cmap=dflt_chart_cmap) # Geometries options.Rectangles = Options('style', line_color='black') diff --git a/holoviews/plotting/mpl/__init__.py b/holoviews/plotting/mpl/__init__.py index 92cb246d15..bd61c47dd1 100644 --- a/holoviews/plotting/mpl/__init__.py +++ b/holoviews/plotting/mpl/__init__.py @@ -274,11 +274,13 @@ def grid_selector(grid): options.Arrow = Options('style', color='k', linewidth=2, fontsize=13) # Paths -options.Contours = Options('style', color=Cycle(), cmap='viridis') +options.Contours = Options('style', color=Cycle(), cmap=dflt_chart_cmap) options.Contours = Options('plot', show_legend=True) -options.Path = Options('style', color=Cycle(), cmap='viridis') +options.Path = Options('style', color=Cycle(), cmap=dflt_chart_cmap) options.Polygons = Options('style', facecolor=Cycle(), edgecolor='black', - cmap='viridis') + cmap=dflt_chart_cmap) +options.Rectangles = Options('style', cmap=dflt_chart_cmap) +options.Segments = Options('style', cmap=dflt_chart_cmap) options.Box = Options('style', color='black') options.Bounds = Options('style', color='black') options.Ellipse = Options('style', color='black') diff --git a/holoviews/plotting/plotly/__init__.py b/holoviews/plotting/plotly/__init__.py index a725251c05..f36a263a1c 100644 --- a/holoviews/plotting/plotly/__init__.py +++ b/holoviews/plotting/plotly/__init__.py @@ -140,8 +140,8 @@ options.HSpan = Options('style', fillcolor=Cycle(), opacity=0.5) # Shapes -options.Rectangles = Options('style', line_color=dflt_shape_line_color) -options.Bounds = Options('style', line_color=dflt_shape_line_color) -options.Path = Options('style', line_color=dflt_shape_line_color) -options.Segments = Options('style', line_color=dflt_shape_line_color) +options.Rectangles = Options('style', line_color=dflt_shape_line_color, cmap=dflt_chart_cmap) +options.Bounds = Options('style', line_color=dflt_shape_line_color, cmap=dflt_chart_cmap) +options.Path = Options('style', line_color=dflt_shape_line_color, cmap=dflt_chart_cmap) +options.Segments = Options('style', line_color=dflt_shape_line_color, cmap=dflt_chart_cmap) options.Box = Options('style', line_color=dflt_shape_line_color) From de604320e8ac76992963b5d05b3d07903d40c9fc Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Tue, 1 Dec 2020 14:09:42 +0100 Subject: [PATCH 75/98] Update default cmap for Spikes --- holoviews/plotting/bokeh/__init__.py | 2 +- holoviews/plotting/mpl/__init__.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/holoviews/plotting/bokeh/__init__.py b/holoviews/plotting/bokeh/__init__.py index 047835126f..062f176881 100644 --- a/holoviews/plotting/bokeh/__init__.py +++ b/holoviews/plotting/bokeh/__init__.py @@ -176,7 +176,7 @@ def colormap_generator(palette): options.Spread = Options('style', color=Cycle(), alpha=0.6, line_color='black', muted_alpha=0.2) options.Bars = Options('style', color=Cycle(), line_color='black', bar_width=0.8, muted_alpha=0.2) -options.Spikes = Options('style', color='black', cmap='fire', muted_alpha=0.2) +options.Spikes = Options('style', color='black', cmap=dflt_chart_cmap, muted_alpha=0.2) options.Area = Options('style', color=Cycle(), alpha=1, line_color='black', muted_alpha=0.2) options.VectorField = Options('style', color='black', muted_alpha=0.2) diff --git a/holoviews/plotting/mpl/__init__.py b/holoviews/plotting/mpl/__init__.py index bd61c47dd1..909540b7c2 100644 --- a/holoviews/plotting/mpl/__init__.py +++ b/holoviews/plotting/mpl/__init__.py @@ -240,7 +240,7 @@ def grid_selector(grid): options.Path3D = Options('plot', fig_size=150) options.Surface = Options('plot', fig_size=150) options.Surface = Options('style', cmap='fire') -options.Spikes = Options('style', color='black', cmap='fire') +options.Spikes = Options('style', color='black', cmap=dflt_chart_cmap) options.Area = Options('style', facecolor=Cycle(), edgecolor='black') options.BoxWhisker = Options('style', boxprops=dict(color='k', linewidth=1.5), whiskerprops=dict(color='k', linewidth=1.5)) From 6693d4bee46513791f9c5d400c087e5ab8315f13 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Tue, 1 Dec 2020 16:57:20 +0100 Subject: [PATCH 76/98] Enable BinnedTicker --- holoviews/plotting/bokeh/element.py | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/holoviews/plotting/bokeh/element.py b/holoviews/plotting/bokeh/element.py index 421ebd07bb..4bfdd70f45 100644 --- a/holoviews/plotting/bokeh/element.py +++ b/holoviews/plotting/bokeh/element.py @@ -50,6 +50,16 @@ compute_layout_properties, wrap_formatter, match_ax_type, remove_legend ) +try: + from bokeh.models import EqHistColorMapper +except ImportError: + EqHistColorMapper = None + +try: + from bokeh.models import BinnedTicker +except ImportError: + BinnedTicker = None + if bokeh_version >= '2.0.1': try: TOOLS_MAP = Tool._known_aliases @@ -1755,7 +1765,9 @@ class ColorbarPlot(ElementPlot): def _draw_colorbar(self, plot, color_mapper, prefix=''): if CategoricalColorMapper and isinstance(color_mapper, CategoricalColorMapper): return - if LogColorMapper and isinstance(color_mapper, LogColorMapper) and color_mapper.low > 0: + if EqHistColorMapper and isinstance(color_mapper, EqHistColorMapper) and BinnedTicker: + ticker = BinnedTicker(mapper=color_mapper) + elif isinstance(color_mapper, LogColorMapper) and color_mapper.low > 0: ticker = LogTicker() else: ticker = BasicTicker() @@ -1955,9 +1967,7 @@ def _get_cmapper_opts(self, low, high, factors, colors): "the `clim` option." ) elif self.cnorm == 'eq_hist': - try: - from bokeh.models import EqHistColorMapper - except ImportError: + if EqHistColorMapper is None: raise ImportError("Could not import bokeh.models.EqHistColorMapper. " "Note that the option cnorm='eq_hist' requires " "bokeh 2.2.3 or higher.") From 311771c784f9778627164a09e2f05c4ea57598b9 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Tue, 1 Dec 2020 17:01:02 +0100 Subject: [PATCH 77/98] Add redim.nodata --- holoviews/core/accessors.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/holoviews/core/accessors.py b/holoviews/core/accessors.py index 3359bfa2ad..f516b8107c 100644 --- a/holoviews/core/accessors.py +++ b/holoviews/core/accessors.py @@ -474,6 +474,9 @@ def soft_range(self, specs=None, **values): def type(self, specs=None, **values): return self._redim('type', specs, **values) + def nodata(self, specs=None, **values): + return self._redim('nodata', specs, **values) + def step(self, specs=None, **values): return self._redim('step', specs, **values) From cf9205db6133f386b626c892df92a0e6cb60f428 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Tue, 1 Dec 2020 17:21:32 +0100 Subject: [PATCH 78/98] Fix cmap handling on matplotlib collections --- holoviews/plotting/mpl/chart.py | 2 ++ holoviews/plotting/mpl/geometry.py | 4 ++++ holoviews/plotting/mpl/path.py | 33 +++++++++++++----------------- 3 files changed, 20 insertions(+), 19 deletions(-) diff --git a/holoviews/plotting/mpl/chart.py b/holoviews/plotting/mpl/chart.py index 813da303a1..7f99766500 100644 --- a/holoviews/plotting/mpl/chart.py +++ b/holoviews/plotting/mpl/chart.py @@ -1052,6 +1052,8 @@ def init_artists(self, ax, plot_args, plot_kwargs): plot_kwargs['array'] = plot_kwargs.pop('c') if 'vmin' in plot_kwargs and 'vmax' in plot_kwargs: plot_kwargs['clim'] = plot_kwargs.pop('vmin'), plot_kwargs.pop('vmax') + if not 'array' in plot_kwargs and 'cmap' in plot_kwargs: + del plot_kwargs['cmap'] line_segments = LineCollection(*plot_args, **plot_kwargs) ax.add_collection(line_segments) return {'artist': line_segments} diff --git a/holoviews/plotting/mpl/geometry.py b/holoviews/plotting/mpl/geometry.py index 83e02c94d5..9bf1c8cf3c 100644 --- a/holoviews/plotting/mpl/geometry.py +++ b/holoviews/plotting/mpl/geometry.py @@ -27,6 +27,8 @@ def init_artists(self, ax, plot_args, plot_kwargs): plot_kwargs['array'] = plot_kwargs.pop('c') if 'vmin' in plot_kwargs and 'vmax' in plot_kwargs: plot_kwargs['clim'] = plot_kwargs.pop('vmin'), plot_kwargs.pop('vmax') + if not 'array' in plot_kwargs and 'cmap' in plot_kwargs: + del plot_kwargs['cmap'] line_segments = LineCollection(*plot_args, **plot_kwargs) ax.add_collection(line_segments) return {'artist': line_segments} @@ -57,6 +59,8 @@ def init_artists(self, ax, plot_args, plot_kwargs): plot_kwargs['array'] = plot_kwargs.pop('c') if 'vmin' in plot_kwargs and 'vmax' in plot_kwargs: plot_kwargs['clim'] = plot_kwargs.pop('vmin'), plot_kwargs.pop('vmax') + if not 'array' in plot_kwargs and 'cmap' in plot_kwargs: + del plot_kwargs['cmap'] line_segments = PatchCollection(*plot_args, **plot_kwargs) ax.add_collection(line_segments) return {'artist': line_segments} diff --git a/holoviews/plotting/mpl/path.py b/holoviews/plotting/mpl/path.py index d145a9934f..bf3fe4b6ef 100644 --- a/holoviews/plotting/mpl/path.py +++ b/holoviews/plotting/mpl/path.py @@ -29,6 +29,19 @@ class PathPlot(ColorbarPlot): style_opts = ['alpha', 'color', 'linestyle', 'linewidth', 'visible', 'cmap'] + _collection = LineCollection + + def init_artists(self, ax, plot_args, plot_kwargs): + if 'c' in plot_kwargs: + plot_kwargs['array'] = plot_kwargs.pop('c') + if 'vmin' in plot_kwargs and 'vmax' in plot_kwargs: + plot_kwargs['clim'] = plot_kwargs.pop('vmin'), plot_kwargs.pop('vmax') + if not 'array' in plot_kwargs and 'cmap' in plot_kwargs: + del plot_kwargs['cmap'] + collection = self._collection(*plot_args, **plot_kwargs) + ax.add_collection(collection) + return {'artist': collection} + def get_data(self, element, ranges, style): cdim = element.get_dimension(self.color_index) @@ -71,17 +84,8 @@ def get_data(self, element, ranges, style): if cdim: self._norm_kwargs(element, ranges, style, cdim) style['array'] = np.array(cvals) - if 'c' in style: - style['array'] = style.pop('c') - if 'vmin' in style: - style['clim'] = style.pop('vmin', None), style.pop('vmax', None) return (paths,), style, {'dimensions': dims} - def init_artists(self, ax, plot_args, plot_kwargs): - line_segments = LineCollection(*plot_args, **plot_kwargs) - ax.add_collection(line_segments) - return {'artist': line_segments} - def update_handles(self, key, axis, element, ranges, style): artist = self.handles['artist'] data, style, axis_kwargs = self.get_data(element, ranges, style) @@ -107,11 +111,6 @@ class ContourPlot(PathPlot): allow_None=True, doc=""" Index of the dimension from which the color will the drawn""") - def init_artists(self, ax, plot_args, plot_kwargs): - line_segments = LineCollection(*plot_args, **plot_kwargs) - ax.add_collection(line_segments) - return {'artist': line_segments} - def get_data(self, element, ranges, style): if isinstance(element, Polygons): color_prop = 'facecolors' @@ -162,7 +161,6 @@ def get_data(self, element, ranges, style): array = util.search_indices(array, util.unique_array(array)) style['array'] = array self._norm_kwargs(element, ranges, style, cdim) - style['clim'] = style.pop('vmin'), style.pop('vmax') return (paths,), style, {} @@ -182,7 +180,4 @@ class PolygonPlot(ContourPlot): 'hatch', 'linestyle', 'joinstyle', 'fill', 'capstyle', 'color'] - def init_artists(self, ax, plot_args, plot_kwargs): - polys = PatchCollection(*plot_args, **plot_kwargs) - ax.add_collection(polys) - return {'artist': polys} + _collection = PatchCollection From 63da62ecef0d12f112cbf6b7291543e1eef3d81b Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Tue, 1 Dec 2020 17:22:29 +0100 Subject: [PATCH 79/98] Switch to numerical colormapping on Polygons --- examples/user_guide/15-Large_Data.ipynb | 16 +++------------- 1 file changed, 3 insertions(+), 13 deletions(-) diff --git a/examples/user_guide/15-Large_Data.ipynb b/examples/user_guide/15-Large_Data.ipynb index 99d6bfa63d..cb2c0f960a 100644 --- a/examples/user_guide/15-Large_Data.ipynb +++ b/examples/user_guide/15-Large_Data.ipynb @@ -504,6 +504,7 @@ "source": [ "hv.output(backend='matplotlib')\n", "\n", + "from bokeh.sampledata.unemployment import data as unemployment\n", "from bokeh.sampledata.us_counties import data as counties\n", "\n", "opts.defaults(opts.Image(aspect=1, axiswise=True, xaxis='bare', yaxis='bare'),\n", @@ -556,8 +557,8 @@ "shadeable += [hv.operation.contours(hv.Image((x,y,z)), levels=10)]\n", "try:\n", " import spatialpandas # Needed for datashader polygon support\n", - " shadeable += [hv.Polygons([county for county in counties.values()\n", - " if county['state'] == 'tx'], ['lons', 'lats'], ['name']).opts(color='name', cmap='glasbey')]\n", + " shadeable += [hv.Polygons([dict(county, unemployment=unemployment[k]) for k, county in counties.items()\n", + " if county['state'] == 'tx'], ['lons', 'lats'], ['unemployment']).opts(color='unemployment')]\n", "except: pass\n", "\n", "rasterizable = [hv.RGB(np.dstack([r,g,b])), hv.HSV(np.dstack([g,b,r]))]\n", @@ -565,7 +566,6 @@ "ds_opts = {\n", " hv.Path: dict(aggregator='any'),\n", " hv.Points: dict(aggregator='any'),\n", - " hv.Polygons: dict(aggregator=ds.count_cat('name'), color_key=hv.plotting.util.process_cmap('glasbey')),\n", " hv.Segments: dict(aggregator='any'),\n", " hv.Rectangles: dict(aggregator=ds.count_cat('sign'), color_key=rect_colors)\n", "}\n", @@ -574,16 +574,6 @@ " [ rasterize(e.relabel(e.__class__.name)).opts(**el_opts) for e in rasterizable]).opts(shared_axes=False).cols(6)" ] }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "hv.Layout([dynspread(rasterize(e.relabel(e.__class__.name), **ds_opts.get(e.__class__, {}))).opts(**el_opts) for e in shadeable] + \n", - " [ rasterize(e.relabel(e.__class__.name)).opts(**el_opts) for e in rasterizable]).opts(shared_axes=False).cols(6)" - ] - }, { "cell_type": "markdown", "metadata": {}, From 00ac8975bbd76062ecf4f3597e154f680739b14f Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Tue, 1 Dec 2020 17:40:36 +0100 Subject: [PATCH 80/98] Various bug fixes --- holoviews/plotting/mpl/path.py | 2 +- holoviews/plotting/plotly/__init__.py | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/holoviews/plotting/mpl/path.py b/holoviews/plotting/mpl/path.py index bf3fe4b6ef..983cc51766 100644 --- a/holoviews/plotting/mpl/path.py +++ b/holoviews/plotting/mpl/path.py @@ -92,7 +92,7 @@ def update_handles(self, key, axis, element, ranges, style): artist.set_paths(data[0]) if 'array' in style: artist.set_array(style['array']) - artist.set_clim(style['clim']) + artist.set_clim((style['vmin'], style['vmin'])) if 'norm' in style: artist.set_norm(style['norm']) artist.set_visible(style.get('visible', True)) diff --git a/holoviews/plotting/plotly/__init__.py b/holoviews/plotting/plotly/__init__.py index f36a263a1c..a725251c05 100644 --- a/holoviews/plotting/plotly/__init__.py +++ b/holoviews/plotting/plotly/__init__.py @@ -140,8 +140,8 @@ options.HSpan = Options('style', fillcolor=Cycle(), opacity=0.5) # Shapes -options.Rectangles = Options('style', line_color=dflt_shape_line_color, cmap=dflt_chart_cmap) -options.Bounds = Options('style', line_color=dflt_shape_line_color, cmap=dflt_chart_cmap) -options.Path = Options('style', line_color=dflt_shape_line_color, cmap=dflt_chart_cmap) -options.Segments = Options('style', line_color=dflt_shape_line_color, cmap=dflt_chart_cmap) +options.Rectangles = Options('style', line_color=dflt_shape_line_color) +options.Bounds = Options('style', line_color=dflt_shape_line_color) +options.Path = Options('style', line_color=dflt_shape_line_color) +options.Segments = Options('style', line_color=dflt_shape_line_color) options.Box = Options('style', line_color=dflt_shape_line_color) From 6b8e2c2699d3d625c9a5b7171e00bf3d5edc1773 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Tue, 1 Dec 2020 18:01:01 +0100 Subject: [PATCH 81/98] Handle updates to vmin/vmax/clim --- holoviews/plotting/mpl/path.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/holoviews/plotting/mpl/path.py b/holoviews/plotting/mpl/path.py index 983cc51766..3f5663126d 100644 --- a/holoviews/plotting/mpl/path.py +++ b/holoviews/plotting/mpl/path.py @@ -92,7 +92,10 @@ def update_handles(self, key, axis, element, ranges, style): artist.set_paths(data[0]) if 'array' in style: artist.set_array(style['array']) - artist.set_clim((style['vmin'], style['vmin'])) + if 'vmin' in style and 'vmax' in style: + artist.set_clim((style['vmin'], style['vmax'])) + if 'clim' in style: + artist.set_clim(style['clim']) if 'norm' in style: artist.set_norm(style['norm']) artist.set_visible(style.get('visible', True)) From 3c27e92fb87f572b3365374ddade9ccdfe077086 Mon Sep 17 00:00:00 2001 From: jlstevens Date: Tue, 1 Dec 2020 18:31:23 +0100 Subject: [PATCH 82/98] Deprecated normalization parameter in favor of cnorm --- holoviews/operation/datashader.py | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/holoviews/operation/datashader.py b/holoviews/operation/datashader.py index 89d25b4a31..482c655fea 100644 --- a/holoviews/operation/datashader.py +++ b/holoviews/operation/datashader.py @@ -1155,14 +1155,19 @@ class shade(LinkableOperation): Callable type must allow mapping colors for supplied values between 0 and 1.""") - normalization = param.ClassSelector(default='eq_hist', - class_=(basestring, Callable), - doc=""" + cnorm = param.ClassSelector(default='eq_hist', + class_=(basestring, Callable), + doc=""" The normalization operation applied before colormapping. Valid options include 'linear', 'log', 'eq_hist', 'cbrt', and any valid transfer function that accepts data, mask, nbins arguments.""") + normalization = param.ClassSelector(default='eq_hist', + precedence=-1, + class_=(basestring, Callable), + doc="Deprecated parameter (use cnorm instead)") + clims = param.NumericTuple(default=None, length=2, doc=""" Min and max data values to use for colormap interpolation, when wishing to override autoranging. @@ -1258,9 +1263,17 @@ def _process(self, element, key=None): array = element.data[vdim] kdims = element.kdims + overrides = dict(self.p.items()) + if 'normalization' in overrides: + self.param.warning("Shading 'normalization' parameter deprecated, " + "use 'cnorm' parameter instead'") + cnorm = overrides.get('cnorm', overrides['normalization']) + else: + cnorm = self.p.cnorm + # Compute shading options depending on whether # it is a categorical or regular aggregate - shade_opts = dict(how=self.p.normalization, + shade_opts = dict(how=cnorm, min_alpha=self.p.min_alpha, alpha=self.p.alpha) if element.ndims > 2: @@ -1292,7 +1305,7 @@ def _process(self, element, key=None): if self.p.clims: shade_opts['span'] = self.p.clims - elif ds_version > '0.5.0' and self.p.normalization != 'eq_hist': + elif ds_version > '0.5.0' and cnorm != 'eq_hist': shade_opts['span'] = element.range(vdim) params = dict(get_param_values(element), kdims=kdims, From 87f1b7e7e6e94a6c0921a28206711606d5e0760d Mon Sep 17 00:00:00 2001 From: jlstevens Date: Tue, 1 Dec 2020 19:02:46 +0100 Subject: [PATCH 83/98] Removed dflt_chart_cmap and setting dflt_cmap via hv.config.default_cmap --- holoviews/core/util.py | 2 ++ holoviews/plotting/bokeh/__init__.py | 18 +++++++----------- holoviews/plotting/mpl/__init__.py | 19 +++++++------------ holoviews/plotting/plotly/__init__.py | 7 +++---- 4 files changed, 19 insertions(+), 27 deletions(-) diff --git a/holoviews/core/util.py b/holoviews/core/util.py index 469c1a8ba8..fe60916c96 100644 --- a/holoviews/core/util.py +++ b/holoviews/core/util.py @@ -145,6 +145,8 @@ class Config(param.ParameterizedFunction): recommended that users switch this on to update any uses of __call__ as it will be deprecated in future.""") + default_cmap = param.String(default='fire', doc="Used to be 'fire' ") + def __call__(self, **params): self.param.set_param(**params) return self diff --git a/holoviews/plotting/bokeh/__init__.py b/holoviews/plotting/bokeh/__init__.py index 062f176881..fa5343b6ee 100644 --- a/holoviews/plotting/bokeh/__init__.py +++ b/holoviews/plotting/bokeh/__init__.py @@ -158,8 +158,7 @@ def colormap_generator(palette): Cycle.default_cycles.update({name: p[max(p.keys())] for name, p in all_palettes.items() if max(p.keys()) < 256}) -dflt_cmap = 'fire' -dflt_chart_cmap = 'kbc_r' +dflt_cmap = config.default_cmap all_palettes['fire'] = {len(fire): fire} options = Store.options(backend='bokeh') @@ -168,29 +167,26 @@ def colormap_generator(palette): options.Curve = Options('style', color=Cycle(), line_width=2) options.BoxWhisker = Options('style', box_fill_color=Cycle(), whisker_color='black', box_line_color='black', outlier_color='black') -options.Scatter = Options('style', color=Cycle(), size=point_size, cmap=dflt_chart_cmap) -options.Points = Options('style', color=Cycle(), size=point_size, cmap=dflt_chart_cmap) +options.Scatter = Options('style', color=Cycle(), size=point_size) +options.Points = Options('style', color=Cycle(), size=point_size) options.Points = Options('plot', show_frame=True) options.Histogram = Options('style', line_color='black', color=Cycle(), muted_alpha=0.2) options.ErrorBars = Options('style', color='black') options.Spread = Options('style', color=Cycle(), alpha=0.6, line_color='black', muted_alpha=0.2) options.Bars = Options('style', color=Cycle(), line_color='black', bar_width=0.8, muted_alpha=0.2) -options.Spikes = Options('style', color='black', cmap=dflt_chart_cmap, muted_alpha=0.2) +options.Spikes = Options('style', color='black', muted_alpha=0.2) options.Area = Options('style', color=Cycle(), alpha=1, line_color='black', muted_alpha=0.2) options.VectorField = Options('style', color='black', muted_alpha=0.2) # Paths options.Contours = Options('plot', show_legend=True) -options.Contours = Options('style', color=Cycle(), cmap=dflt_chart_cmap) -options.Path = Options('style', color=Cycle(), cmap=dflt_chart_cmap) +options.Contours = Options('style', color=Cycle()) +options.Path = Options('style', color=Cycle()) options.Box = Options('style', color='black') options.Bounds = Options('style', color='black') options.Ellipse = Options('style', color='black') -options.Polygons = Options('style', color=Cycle(), line_color='black', - cmap=dflt_chart_cmap) -options.Rectangles = Options('style', cmap=dflt_chart_cmap) -options.Segments = Options('style', cmap=dflt_chart_cmap) +options.Polygons = Options('style', color=Cycle(), line_color='black') # Geometries options.Rectangles = Options('style', line_color='black') diff --git a/holoviews/plotting/mpl/__init__.py b/holoviews/plotting/mpl/__init__.py index 909540b7c2..9aaba8701a 100644 --- a/holoviews/plotting/mpl/__init__.py +++ b/holoviews/plotting/mpl/__init__.py @@ -220,27 +220,25 @@ def grid_selector(grid): register_cmap("fire_r", cmap=fire_r_cmap) options = Store.options(backend='matplotlib') -dflt_cmap = 'fire' -dflt_chart_cmap = 'kbc_r' +dflt_cmap = config.default_cmap # Default option definitions # Note: *No*short aliases here! e.g use 'facecolor' instead of 'fc' # Charts options.Curve = Options('style', color=Cycle(), linewidth=2) -options.Scatter = Options('style', color=Cycle(), marker='o', cmap=dflt_chart_cmap) +options.Scatter = Options('style', color=Cycle(), marker='o') options.Points = Options('plot', show_frame=True) options.ErrorBars = Options('style', edgecolor='k') options.Spread = Options('style', facecolor=Cycle(), alpha=0.6, edgecolor='k', linewidth=0.5) options.Bars = Options('style', edgecolor='k', color=Cycle()) options.Histogram = Options('style', edgecolor='k', facecolor=Cycle()) -options.Points = Options('style', color=Cycle(), marker='o', cmap=dflt_chart_cmap) +options.Points = Options('style', color=Cycle(), marker='o') options.Scatter3D = Options('style', c=Cycle(), marker='o') options.Scatter3D = Options('plot', fig_size=150) options.Path3D = Options('plot', fig_size=150) options.Surface = Options('plot', fig_size=150) -options.Surface = Options('style', cmap='fire') -options.Spikes = Options('style', color='black', cmap=dflt_chart_cmap) +options.Spikes = Options('style', color='black') options.Area = Options('style', facecolor=Cycle(), edgecolor='black') options.BoxWhisker = Options('style', boxprops=dict(color='k', linewidth=1.5), whiskerprops=dict(color='k', linewidth=1.5)) @@ -274,13 +272,10 @@ def grid_selector(grid): options.Arrow = Options('style', color='k', linewidth=2, fontsize=13) # Paths -options.Contours = Options('style', color=Cycle(), cmap=dflt_chart_cmap) +options.Contours = Options('style', color=Cycle()) options.Contours = Options('plot', show_legend=True) -options.Path = Options('style', color=Cycle(), cmap=dflt_chart_cmap) -options.Polygons = Options('style', facecolor=Cycle(), edgecolor='black', - cmap=dflt_chart_cmap) -options.Rectangles = Options('style', cmap=dflt_chart_cmap) -options.Segments = Options('style', cmap=dflt_chart_cmap) +options.Path = Options('style', color=Cycle()) +options.Polygons = Options('style', facecolor=Cycle(), edgecolor='black') options.Box = Options('style', color='black') options.Bounds = Options('style', color='black') options.Ellipse = Options('style', color='black') diff --git a/holoviews/plotting/plotly/__init__.py b/holoviews/plotting/plotly/__init__.py index a725251c05..e7503eac90 100644 --- a/holoviews/plotting/plotly/__init__.py +++ b/holoviews/plotting/plotly/__init__.py @@ -103,8 +103,7 @@ for plot in concrete_descendents(ElementPlot).values(): plot.padding = 0 -dflt_cmap = 'fire' -dflt_chart_cmap = 'kbc_r' +dflt_cmap = config.default_cmap dflt_shape_line_color = '#2a3f5f' # Line color of default plotly template point_size = np.sqrt(6) # Matches matplotlib default @@ -114,8 +113,8 @@ # Charts options.Curve = Options('style', color=Cycle(), line_width=2) options.ErrorBars = Options('style', color='black') -options.Scatter = Options('style', color=Cycle(), cmap=dflt_chart_cmap) -options.Points = Options('style', color=Cycle(), cmap=dflt_chart_cmap) +options.Scatter = Options('style', color=Cycle()) +options.Points = Options('style', color=Cycle()) options.Area = Options('style', color=Cycle(), line_width=2) options.Spread = Options('style', color=Cycle(), line_width=2) options.TriSurface = Options('style', cmap='viridis') From 127e0bf7ecadb92516672864aaa8b7fc896bdbf7 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Tue, 1 Dec 2020 19:34:33 +0100 Subject: [PATCH 84/98] Fixed triggering mechanism and updates to Range streams --- holoviews/plotting/bokeh/element.py | 6 +--- holoviews/plotting/bokeh/tabular.py | 2 +- holoviews/plotting/plot.py | 49 +++++++++++++++++++--------- holoviews/plotting/plotly/element.py | 2 +- holoviews/plotting/renderer.py | 12 +++++++ 5 files changed, 48 insertions(+), 23 deletions(-) diff --git a/holoviews/plotting/bokeh/element.py b/holoviews/plotting/bokeh/element.py index 4bfdd70f45..a249963075 100644 --- a/holoviews/plotting/bokeh/element.py +++ b/holoviews/plotting/bokeh/element.py @@ -213,7 +213,7 @@ def __init__(self, element, plot=None, **params): super(ElementPlot, self).__init__(element, **params) self.handles = {} if plot is None else self.handles['plot'] self.static = len(self.hmap) == 1 and len(self.keys) == len(self.hmap) - self.callbacks = self._construct_callbacks() + self.callbacks, self.source_streams = self._construct_callbacks() self.static_source = False self.streaming = [s for s in self.streams if isinstance(s, Buffer)] self.geographic = bool(self.hmap.last.traverse(lambda x: x, Tiles)) @@ -1401,10 +1401,6 @@ def initialize_plot(self, ranges=None, plot=None, plots=None, source=None): self.drawn = True - trigger = self._trigger - self._trigger = [] - Stream.trigger(trigger) - return plot diff --git a/holoviews/plotting/bokeh/tabular.py b/holoviews/plotting/bokeh/tabular.py index daa98c11d6..28edf188fc 100644 --- a/holoviews/plotting/bokeh/tabular.py +++ b/holoviews/plotting/bokeh/tabular.py @@ -48,7 +48,7 @@ def __init__(self, element, plot=None, **params): self.handles = {} if plot is None else self.handles['plot'] element_ids = self.hmap.traverse(lambda x: id(x), [Dataset, ItemTable]) self.static = len(set(element_ids)) == 1 and len(self.keys) == len(self.hmap) - self.callbacks = self._construct_callbacks() + self.callbacks, self.source_streams = self._construct_callbacks() self.streaming = [s for s in self.streams if isinstance(s, Buffer)] self.static_source = False diff --git a/holoviews/plotting/plot.py b/holoviews/plotting/plot.py index 47fe97a2d6..be56f41980 100644 --- a/holoviews/plotting/plot.py +++ b/holoviews/plotting/plot.py @@ -629,7 +629,7 @@ def compute_ranges(self, obj, key, ranges): elif key is not None: # Traverse to get elements for each frame frame = self._get_frame(key) elements = [] if frame is None else frame.traverse(return_fn, [group]) - + # Only compute ranges if not axiswise on a composite plot # or not framewise on a Overlay or ElementPlot if (not (axiswise and not isinstance(obj, HoloMap)) or @@ -693,7 +693,7 @@ def _compute_group_range(cls, group, elements, ranges, framewise, robust, top_le # Iterate over all elements in a normalization group # and accumulate their ranges into the supplied dictionary. elements = [el for el in elements if el is not None] - + data_ranges = {} robust_ranges = {} categorical_dims = [] @@ -962,6 +962,7 @@ def _construct_callbacks(self): Initializes any callbacks for streams which have defined the plotted object as a source. """ + source_streams = [] cb_classes = set() registry = list(Stream.registry.items()) callbacks = Stream._callbacks[self.backend] @@ -977,8 +978,11 @@ def _construct_callbacks(self): sorted_cbs = sorted(cb_classes, key=lambda x: id(x[0])) for cb, group in groupby(sorted_cbs, lambda x: x[0]): cb_streams = [s for _, s in group] + for cb_stream in cb_streams: + if cb_stream not in source_streams: + source_streams.append(cb_stream) cbs.append(cb(self, cb_streams, source)) - return cbs + return cbs, source_streams @property def link_sources(self): @@ -1312,27 +1316,17 @@ def _get_range_extents(self, element, ranges, range_type, xdim, ydim, zdim): (y0, y1), ysrange, yhrange = get_range(element, ranges, ydim) (z0, z1), zsrange, zhrange = get_range(element, ranges, zdim) + trigger = False if not self.overlaid and not self.batched: xspan, yspan, zspan = (v/2. for v in get_axis_padding(self.default_span)) mx0, mx1 = get_minimum_span(x0, x1, xspan) - - # If auto-padding is enabled ensure RangeXY dependent plots - # are recomputed before initial render if x0 != mx0 or x1 != mx1: - for stream in self.streams: - if isinstance(stream, (RangeX, RangeXY)): - stream.update(x_range=(mx0, mx1)) - if stream not in self._trigger: - self._trigger.append(stream) x0, x1 = mx0, mx1 + trigger = True my0, my1 = get_minimum_span(y0, y1, yspan) if y0 != my0 or y1 != my1: - for stream in self.streams: - if isinstance(stream, (RangeY, RangeXY)): - stream.update(y_range=(my0, my1)) - if stream not in self._trigger: - self._trigger.append(stream) y0, y1 = my0, my1 + trigger = True mz0, mz1 = get_minimum_span(z0, z1, zspan) xpad, ypad, zpad = self.get_padding(element, (x0, y0, z0, x1, y1, z1)) @@ -1369,6 +1363,13 @@ def _get_range_extents(self, element, ranges, range_type, xdim, ydim, zdim): elif zdim is None: z0, z1 = np.NaN, np.NaN return (x0, y0, z0, x1, y1, z1) + + if not self.drawn: + for stream in getattr(self, 'source_streams', []): + if (isinstance(stream, (RangeX, RangeY, RangeXY)) and + trigger and stream not in self._trigger): + self._trigger.append(stream) + return (x0, y0, x1, y1) @@ -1431,6 +1432,22 @@ def get_extents(self, element, ranges, range_type='combined', xdim=None, ydim=No x0, x1 = util.dimension_range(x0, x1, self.xlim, (None, None)) y0, y1 = util.dimension_range(y0, y1, self.ylim, (None, None)) + + if not self.drawn: + x_range, y_range = ((y0, y1), (x0, x1)) if self.invert_axes else ((x0, x1), (y0, y1)) + for stream in getattr(self, 'source_streams', []): + if isinstance(stream, RangeX): + params = {'x_range': x_range} + elif isinstance(stream, RangeY): + params = {'y_range': y_range} + elif isinstance(stream, RangeXY): + params = {'x_range': x_range, 'y_range': y_range} + else: + continue + stream.update(**params) + if stream not in self._trigger and (self.xlim or self.ylim): + self._trigger.append(stream) + if self.projection == '3d': z0, z1 = util.dimension_range(z0, z1, self.zlim, (None, None)) return (x0, y0, z0, x1, y1, z1) diff --git a/holoviews/plotting/plotly/element.py b/holoviews/plotting/plotly/element.py index da58d08bc7..47b16554b8 100644 --- a/holoviews/plotting/plotly/element.py +++ b/holoviews/plotting/plotly/element.py @@ -112,7 +112,7 @@ def __init__(self, element, plot=None, **params): super(ElementPlot, self).__init__(element, **params) self.trace_uid = str(uuid.uuid4()) self.static = len(self.hmap) == 1 and len(self.keys) == len(self.hmap) - self.callbacks = self._construct_callbacks() + self.callbacks, self.source_streams = self._construct_callbacks() @classmethod def trace_kwargs(cls, **kwargs): diff --git a/holoviews/plotting/renderer.py b/holoviews/plotting/renderer.py index e87f3a3c1d..b2a9bfe2d4 100644 --- a/holoviews/plotting/renderer.py +++ b/holoviews/plotting/renderer.py @@ -241,6 +241,18 @@ def get_plot(self_or_cls, obj, doc=None, renderer=None, comm=None, **kwargs): else: plot = obj + # Trigger streams which were marked as requiring an update + triggers = [] + for p in plot.traverse(): + if not hasattr(p, '_trigger'): + continue + for trigger in p._trigger: + if trigger not in triggers: + triggers.append(trigger) + p._trigger = [] + for trigger in triggers: + Stream.trigger([trigger]) + if isinstance(self_or_cls, Renderer): self_or_cls.last_plot = plot From 95fe17625b8eb114b5a4e6fdfd3921e635581145 Mon Sep 17 00:00:00 2001 From: jlstevens Date: Tue, 1 Dec 2020 19:39:09 +0100 Subject: [PATCH 85/98] Put the normalization parameter warning behind future_deprecations --- holoviews/core/accessors.py | 2 +- holoviews/operation/datashader.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/holoviews/core/accessors.py b/holoviews/core/accessors.py index f516b8107c..24df612471 100644 --- a/holoviews/core/accessors.py +++ b/holoviews/core/accessors.py @@ -568,7 +568,7 @@ def __call__(self, *args, **kwargs): "group (i.e. separate plot, style and norm groups) is deprecated. " "Use the .options method converting to the simplified format " "instead or use hv.opts.apply_groups for backward compatibility.") - param.main.warning(msg) + param.main.param.warning(msg) return self._dispatch_opts( *args, **kwargs) diff --git a/holoviews/operation/datashader.py b/holoviews/operation/datashader.py index 482c655fea..886fa35797 100644 --- a/holoviews/operation/datashader.py +++ b/holoviews/operation/datashader.py @@ -28,7 +28,7 @@ from ..core.data import PandasInterface, XArrayInterface, DaskInterface, cuDFInterface from ..core.util import ( Iterable, LooseVersion, basestring, cftime_types, cftime_to_timestamp, - datetime_types, dt_to_int, isfinite, get_param_values, max_range) + datetime_types, dt_to_int, isfinite, get_param_values, max_range, config) from ..element import (Image, Path, Curve, RGB, Graph, TriMesh, QuadMesh, Contours, Spikes, Area, Rectangles, Spread, Segments, Scatter, Points, Polygons) @@ -1264,7 +1264,7 @@ def _process(self, element, key=None): kdims = element.kdims overrides = dict(self.p.items()) - if 'normalization' in overrides: + if 'normalization' in overrides and config.future_deprecations: self.param.warning("Shading 'normalization' parameter deprecated, " "use 'cnorm' parameter instead'") cnorm = overrides.get('cnorm', overrides['normalization']) From c3a55fc2ec4a3ad805518648ddfd3b963c113c84 Mon Sep 17 00:00:00 2001 From: jlstevens Date: Tue, 1 Dec 2020 19:50:31 +0100 Subject: [PATCH 86/98] Setting dflt_cmap where dflt_chart_cmap used to be set --- holoviews/plotting/bokeh/__init__.py | 15 +++++++++------ holoviews/plotting/mpl/__init__.py | 16 ++++++++++------ holoviews/plotting/plotly/__init__.py | 4 ++-- 3 files changed, 21 insertions(+), 14 deletions(-) diff --git a/holoviews/plotting/bokeh/__init__.py b/holoviews/plotting/bokeh/__init__.py index fa5343b6ee..330047cca2 100644 --- a/holoviews/plotting/bokeh/__init__.py +++ b/holoviews/plotting/bokeh/__init__.py @@ -167,26 +167,29 @@ def colormap_generator(palette): options.Curve = Options('style', color=Cycle(), line_width=2) options.BoxWhisker = Options('style', box_fill_color=Cycle(), whisker_color='black', box_line_color='black', outlier_color='black') -options.Scatter = Options('style', color=Cycle(), size=point_size) -options.Points = Options('style', color=Cycle(), size=point_size) +options.Scatter = Options('style', color=Cycle(), size=point_size, cmap=dflt_cmap) +options.Points = Options('style', color=Cycle(), size=point_size, cmap=dflt_cmap) options.Points = Options('plot', show_frame=True) options.Histogram = Options('style', line_color='black', color=Cycle(), muted_alpha=0.2) options.ErrorBars = Options('style', color='black') options.Spread = Options('style', color=Cycle(), alpha=0.6, line_color='black', muted_alpha=0.2) options.Bars = Options('style', color=Cycle(), line_color='black', bar_width=0.8, muted_alpha=0.2) -options.Spikes = Options('style', color='black', muted_alpha=0.2) +options.Spikes = Options('style', color='black', cmap=dflt_cmap, muted_alpha=0.2) options.Area = Options('style', color=Cycle(), alpha=1, line_color='black', muted_alpha=0.2) options.VectorField = Options('style', color='black', muted_alpha=0.2) # Paths options.Contours = Options('plot', show_legend=True) -options.Contours = Options('style', color=Cycle()) -options.Path = Options('style', color=Cycle()) +options.Contours = Options('style', color=Cycle(), cmap=dflt_cmap) +options.Path = Options('style', color=Cycle(), cmap=dflt_cmap) options.Box = Options('style', color='black') options.Bounds = Options('style', color='black') options.Ellipse = Options('style', color='black') -options.Polygons = Options('style', color=Cycle(), line_color='black') +options.Polygons = Options('style', color=Cycle(), line_color='black', + cmap=dflt_cmap) +options.Rectangles = Options('style', cmap=dflt_cmap) +options.Segments = Options('style', cmap=dflt_cmap) # Geometries options.Rectangles = Options('style', line_color='black') diff --git a/holoviews/plotting/mpl/__init__.py b/holoviews/plotting/mpl/__init__.py index 9aaba8701a..6f8b55ccb1 100644 --- a/holoviews/plotting/mpl/__init__.py +++ b/holoviews/plotting/mpl/__init__.py @@ -227,18 +227,19 @@ def grid_selector(grid): # Charts options.Curve = Options('style', color=Cycle(), linewidth=2) -options.Scatter = Options('style', color=Cycle(), marker='o') +options.Scatter = Options('style', color=Cycle(), marker='o', cmap=dflt_cmap) options.Points = Options('plot', show_frame=True) options.ErrorBars = Options('style', edgecolor='k') options.Spread = Options('style', facecolor=Cycle(), alpha=0.6, edgecolor='k', linewidth=0.5) options.Bars = Options('style', edgecolor='k', color=Cycle()) options.Histogram = Options('style', edgecolor='k', facecolor=Cycle()) -options.Points = Options('style', color=Cycle(), marker='o') +options.Points = Options('style', color=Cycle(), marker='o', cmap=dflt_cmap) options.Scatter3D = Options('style', c=Cycle(), marker='o') options.Scatter3D = Options('plot', fig_size=150) options.Path3D = Options('plot', fig_size=150) options.Surface = Options('plot', fig_size=150) -options.Spikes = Options('style', color='black') +options.Surface = Options('style', cmap='fire') +options.Spikes = Options('style', color='black', cmap=dflt_cmap) options.Area = Options('style', facecolor=Cycle(), edgecolor='black') options.BoxWhisker = Options('style', boxprops=dict(color='k', linewidth=1.5), whiskerprops=dict(color='k', linewidth=1.5)) @@ -272,10 +273,13 @@ def grid_selector(grid): options.Arrow = Options('style', color='k', linewidth=2, fontsize=13) # Paths -options.Contours = Options('style', color=Cycle()) +options.Contours = Options('style', color=Cycle(), cmap=dflt_cmap) options.Contours = Options('plot', show_legend=True) -options.Path = Options('style', color=Cycle()) -options.Polygons = Options('style', facecolor=Cycle(), edgecolor='black') +options.Path = Options('style', color=Cycle(), cmap=dflt_cmap) +options.Polygons = Options('style', facecolor=Cycle(), edgecolor='black', + cmap=dflt_cmap) +options.Rectangles = Options('style', cmap=dflt_cmap) +options.Segments = Options('style', cmap=dflt_cmap) options.Box = Options('style', color='black') options.Bounds = Options('style', color='black') options.Ellipse = Options('style', color='black') diff --git a/holoviews/plotting/plotly/__init__.py b/holoviews/plotting/plotly/__init__.py index e7503eac90..155a0e3bf7 100644 --- a/holoviews/plotting/plotly/__init__.py +++ b/holoviews/plotting/plotly/__init__.py @@ -113,8 +113,8 @@ # Charts options.Curve = Options('style', color=Cycle(), line_width=2) options.ErrorBars = Options('style', color='black') -options.Scatter = Options('style', color=Cycle()) -options.Points = Options('style', color=Cycle()) +options.Scatter = Options('style', color=Cycle(), cmap=dflt_cmap) +options.Points = Options('style', color=Cycle(), cmap=dflt_cmap) options.Area = Options('style', color=Cycle(), line_width=2) options.Spread = Options('style', color=Cycle(), line_width=2) options.TriSurface = Options('style', cmap='viridis') From 41d66104d790093b1c49c8692297e26a2fc2d3e6 Mon Sep 17 00:00:00 2001 From: jlstevens Date: Tue, 1 Dec 2020 19:57:50 +0100 Subject: [PATCH 87/98] Fixed flake --- holoviews/plotting/bokeh/element.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/holoviews/plotting/bokeh/element.py b/holoviews/plotting/bokeh/element.py index a249963075..496a240de5 100644 --- a/holoviews/plotting/bokeh/element.py +++ b/holoviews/plotting/bokeh/element.py @@ -32,7 +32,7 @@ from ...core.options import abbreviated_exception, SkipRendering from ...core import util from ...element import Annotation, Graph, VectorField, Path, Contours, Tiles -from ...streams import Stream, Buffer, RangeXY, PlotSize +from ...streams import Buffer, RangeXY, PlotSize from ...util.transform import dim from ..plot import GenericElementPlot, GenericOverlayPlot from ..util import process_cmap, color_intervals, dim_range_key From cb9a9236ad0e4c8836a11d2b6a72eb407ff6ff3b Mon Sep 17 00:00:00 2001 From: jlstevens Date: Tue, 1 Dec 2020 20:36:02 +0100 Subject: [PATCH 88/98] Registering kbc_r in matplotlib and setting it as the default --- holoviews/core/util.py | 4 +++- holoviews/plotting/mpl/__init__.py | 9 +++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/holoviews/core/util.py b/holoviews/core/util.py index fe60916c96..feeb095e2d 100644 --- a/holoviews/core/util.py +++ b/holoviews/core/util.py @@ -145,7 +145,9 @@ class Config(param.ParameterizedFunction): recommended that users switch this on to update any uses of __call__ as it will be deprecated in future.""") - default_cmap = param.String(default='fire', doc="Used to be 'fire' ") + default_cmap = param.String(default='kbc_r', doc=""" + Global default colormap. Prior to HoloViews 1.14.0, the default + value was 'fire' which can be set for backwards compatibility.""") def __call__(self, **params): self.param.set_param(**params) diff --git a/holoviews/plotting/mpl/__init__.py b/holoviews/plotting/mpl/__init__.py index 6f8b55ccb1..165a966583 100644 --- a/holoviews/plotting/mpl/__init__.py +++ b/holoviews/plotting/mpl/__init__.py @@ -6,6 +6,7 @@ from matplotlib.colors import ListedColormap, LinearSegmentedColormap from matplotlib.cm import register_cmap from param import concrete_descendents +from colorcet import kbc from ...core import Layout, Collator, GridMatrix, config from ...core.options import Cycle, Palette, Options @@ -219,6 +220,14 @@ def grid_selector(grid): register_cmap("fire", cmap=fire_cmap) register_cmap("fire_r", cmap=fire_r_cmap) +def mpl_cm(name,colorlist): + cm = LinearSegmentedColormap.from_list(name, colorlist, N=len(colorlist)) + register_cmap(name, cmap=cm) + +mpl_cm('kbc_r',list(reversed(kbc))) + + + options = Store.options(backend='matplotlib') dflt_cmap = config.default_cmap From 4a94fd2ec899426dd55a2b510ea190324afdb5f0 Mon Sep 17 00:00:00 2001 From: jlstevens Date: Tue, 1 Dec 2020 20:41:23 +0100 Subject: [PATCH 89/98] Set default cmap for bokeh and matplotlib TriMesh and plotly TriSurface --- holoviews/plotting/bokeh/__init__.py | 2 +- holoviews/plotting/mpl/__init__.py | 2 +- holoviews/plotting/plotly/__init__.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/holoviews/plotting/bokeh/__init__.py b/holoviews/plotting/bokeh/__init__.py index 330047cca2..a963f176de 100644 --- a/holoviews/plotting/bokeh/__init__.py +++ b/holoviews/plotting/bokeh/__init__.py @@ -225,7 +225,7 @@ def colormap_generator(palette): edge_line_color='black', node_hover_fill_color='limegreen', edge_line_width=1, edge_hover_line_color='limegreen', edge_nonselection_alpha=0.2, edge_nonselection_line_color='black', - node_nonselection_alpha=0.2, + node_nonselection_alpha=0.2, cmap=dflt_cmap ) options.TriMesh = Options('plot', tools=[]) options.Chord = Options('style', node_size=15, node_color=Cycle(), diff --git a/holoviews/plotting/mpl/__init__.py b/holoviews/plotting/mpl/__init__.py index 165a966583..0278c24487 100644 --- a/holoviews/plotting/mpl/__init__.py +++ b/holoviews/plotting/mpl/__init__.py @@ -300,7 +300,7 @@ def mpl_cm(name,colorlist): options.Graph = Options('style', node_edgecolors='black', node_facecolors=Cycle(), edge_color='black', node_size=15) options.TriMesh = Options('style', node_edgecolors='black', node_facecolors='white', - edge_color='black', node_size=5, edge_linewidth=1) + edge_color='black', node_size=5, edge_linewidth=1, cmap=dflt_cmap) options.Chord = Options('style', node_edgecolors='black', node_facecolors=Cycle(), edge_color='black', node_size=10, edge_linewidth=0.5) options.Chord = Options('plot', xaxis=None, yaxis=None) diff --git a/holoviews/plotting/plotly/__init__.py b/holoviews/plotting/plotly/__init__.py index 155a0e3bf7..6bacb3822f 100644 --- a/holoviews/plotting/plotly/__init__.py +++ b/holoviews/plotting/plotly/__init__.py @@ -117,7 +117,7 @@ options.Points = Options('style', color=Cycle(), cmap=dflt_cmap) options.Area = Options('style', color=Cycle(), line_width=2) options.Spread = Options('style', color=Cycle(), line_width=2) -options.TriSurface = Options('style', cmap='viridis') +options.TriSurface = Options('style', cmap=dflt_cmap) options.Histogram = Options('style', color=Cycle(), line_width=1, line_color='black') # Rasters From 19ce288af4c937675b0dc5c8d93618b73d887517 Mon Sep 17 00:00:00 2001 From: jlstevens Date: Tue, 1 Dec 2020 20:43:36 +0100 Subject: [PATCH 90/98] Removed 'cmap' from _transfer_options --- holoviews/operation/datashader.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/holoviews/operation/datashader.py b/holoviews/operation/datashader.py index 886fa35797..f3b6eaa255 100644 --- a/holoviews/operation/datashader.py +++ b/holoviews/operation/datashader.py @@ -113,7 +113,7 @@ class ResamplingOperation(LinkableOperation): used to represent this internal state is not freed between calls.""") - _transfer_options = ['cmap'] + _transfer_options = [] @bothmethod def instance(self_or_cls,**params): From b77d29f3c21e40ad07a2a477ecc895ec62d28f17 Mon Sep 17 00:00:00 2001 From: jlstevens Date: Tue, 1 Dec 2020 20:54:29 +0100 Subject: [PATCH 91/98] Added hv.config.default_gridded_cmap option --- holoviews/core/util.py | 6 ++++++ holoviews/plotting/bokeh/__init__.py | 6 +++--- holoviews/plotting/mpl/__init__.py | 6 +++--- holoviews/plotting/plotly/__init__.py | 8 ++++---- 4 files changed, 16 insertions(+), 10 deletions(-) diff --git a/holoviews/core/util.py b/holoviews/core/util.py index feeb095e2d..8987a43ed5 100644 --- a/holoviews/core/util.py +++ b/holoviews/core/util.py @@ -149,6 +149,12 @@ class Config(param.ParameterizedFunction): Global default colormap. Prior to HoloViews 1.14.0, the default value was 'fire' which can be set for backwards compatibility.""") + default_gridded_cmap = param.String(default='kbc_r', doc=""" Global + default colormap for gridded elements (i.e Image, Raster and + QuadMesh). Can be set to 'fire' to match raster defaults prior to + HoloViews 1.14.0 while allowing the default_cmap to be the value + of 'kbc_r' used in HoloViews >= 1.14.0""") + def __call__(self, **params): self.param.set_param(**params) return self diff --git a/holoviews/plotting/bokeh/__init__.py b/holoviews/plotting/bokeh/__init__.py index a963f176de..cfc239f6ad 100644 --- a/holoviews/plotting/bokeh/__init__.py +++ b/holoviews/plotting/bokeh/__init__.py @@ -195,9 +195,9 @@ def colormap_generator(palette): options.Rectangles = Options('style', line_color='black') # Rasters -options.Image = Options('style', cmap=dflt_cmap) -options.Raster = Options('style', cmap=dflt_cmap) -options.QuadMesh = Options('style', cmap=dflt_cmap, line_alpha=0) +options.Image = Options('style', cmap=config.default_gridded_cmap) +options.Raster = Options('style', cmap=config.default_gridded_cmap) +options.QuadMesh = Options('style', cmap=config.default_gridded_cmap, line_alpha=0) options.HeatMap = Options('style', cmap='RdYlBu_r', annular_line_alpha=0, xmarks_line_color="#FFFFFF", xmarks_line_width=3, ymarks_line_color="#FFFFFF", ymarks_line_width=3) diff --git a/holoviews/plotting/mpl/__init__.py b/holoviews/plotting/mpl/__init__.py index 0278c24487..580511e5f0 100644 --- a/holoviews/plotting/mpl/__init__.py +++ b/holoviews/plotting/mpl/__init__.py @@ -257,9 +257,9 @@ def mpl_cm(name,colorlist): options.Rectangles = Options('style', edgecolor='black') # Rasters -options.Image = Options('style', cmap=dflt_cmap, interpolation='nearest') -options.Raster = Options('style', cmap=dflt_cmap, interpolation='nearest') -options.QuadMesh = Options('style', cmap=dflt_cmap) +options.Image = Options('style', cmap=config.default_gridded_cmap, interpolation='nearest') +options.Raster = Options('style', cmap=config.default_gridded_cmap, interpolation='nearest') +options.QuadMesh = Options('style', cmap=config.default_gridded_cmap) options.HeatMap = Options('style', cmap='RdYlBu_r', edgecolors='white', annular_edgecolors='white', annular_linewidth=0.5, xmarks_edgecolor='white', xmarks_linewidth=3, diff --git a/holoviews/plotting/plotly/__init__.py b/holoviews/plotting/plotly/__init__.py index 6bacb3822f..3421bd3d95 100644 --- a/holoviews/plotting/plotly/__init__.py +++ b/holoviews/plotting/plotly/__init__.py @@ -121,10 +121,10 @@ options.Histogram = Options('style', color=Cycle(), line_width=1, line_color='black') # Rasters -options.Image = Options('style', cmap=dflt_cmap) -options.Raster = Options('style', cmap=dflt_cmap) -options.QuadMesh = Options('style', cmap=dflt_cmap) -options.HeatMap = Options('style', cmap='RdBu_r') +options.Image = Options('style', cmap=config.default_gridded_cmap) +options.Raster = Options('style', cmap=config.default_cmapconfig.default_gridded_cmap) +options.QuadMesh = Options('style', cmap=config.default_gridded_cmap) +options.HeatMap = Options('style', cmap='RdYlBu_r') # Disable padding for image-like elements options.Image = Options("plot", padding=0) From c72f83b0da71839d79d02508ac2a8adb9424a3dc Mon Sep 17 00:00:00 2001 From: jlstevens Date: Tue, 1 Dec 2020 21:10:07 +0100 Subject: [PATCH 92/98] Simplified registration of kbc_r colormap --- holoviews/plotting/mpl/__init__.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/holoviews/plotting/mpl/__init__.py b/holoviews/plotting/mpl/__init__.py index 580511e5f0..7684d3f02d 100644 --- a/holoviews/plotting/mpl/__init__.py +++ b/holoviews/plotting/mpl/__init__.py @@ -220,13 +220,9 @@ def grid_selector(grid): register_cmap("fire", cmap=fire_cmap) register_cmap("fire_r", cmap=fire_r_cmap) -def mpl_cm(name,colorlist): - cm = LinearSegmentedColormap.from_list(name, colorlist, N=len(colorlist)) - register_cmap(name, cmap=cm) - -mpl_cm('kbc_r',list(reversed(kbc))) - - +register_cmap('kbc_r', + cmap=LinearSegmentedColormap.from_list('kbc_r', + list(reversed(kbc)), N=len(kbc))) options = Store.options(backend='matplotlib') dflt_cmap = config.default_cmap From 85f3e5a2a02011d759818adf2f37849516da2ca6 Mon Sep 17 00:00:00 2001 From: "James A. Bednar" Date: Tue, 1 Dec 2020 14:44:02 -0600 Subject: [PATCH 93/98] Warn for duplicated cnorm/normalization args --- holoviews/operation/datashader.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/holoviews/operation/datashader.py b/holoviews/operation/datashader.py index f3b6eaa255..6a2601e097 100644 --- a/holoviews/operation/datashader.py +++ b/holoviews/operation/datashader.py @@ -1264,9 +1264,13 @@ def _process(self, element, key=None): kdims = element.kdims overrides = dict(self.p.items()) - if 'normalization' in overrides and config.future_deprecations: - self.param.warning("Shading 'normalization' parameter deprecated, " - "use 'cnorm' parameter instead'") + if 'normalization' in overrides: + if 'cnorm' in overrides: + self.param.warning("Cannot supply both 'normalization' and 'cnorm' for shading; " + "use 'cnorm' instead'") + elif config.future_deprecations: + self.param.warning("Shading 'normalization' parameter deprecated, " + "use 'cnorm' parameter instead'") cnorm = overrides.get('cnorm', overrides['normalization']) else: cnorm = self.p.cnorm From 0245401656f3f43862f98323335ab7eb7f83e8d2 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Tue, 1 Dec 2020 22:32:38 +0100 Subject: [PATCH 94/98] Fix copy/paste error --- holoviews/plotting/plotly/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/holoviews/plotting/plotly/__init__.py b/holoviews/plotting/plotly/__init__.py index 3421bd3d95..e48548e4d5 100644 --- a/holoviews/plotting/plotly/__init__.py +++ b/holoviews/plotting/plotly/__init__.py @@ -122,7 +122,7 @@ # Rasters options.Image = Options('style', cmap=config.default_gridded_cmap) -options.Raster = Options('style', cmap=config.default_cmapconfig.default_gridded_cmap) +options.Raster = Options('style', cmap=config.default_gridded_cmap) options.QuadMesh = Options('style', cmap=config.default_gridded_cmap) options.HeatMap = Options('style', cmap='RdYlBu_r') From 058543769f227413f3087692a2124f2320c8d738 Mon Sep 17 00:00:00 2001 From: "James A. Bednar" Date: Tue, 1 Dec 2020 16:54:03 -0600 Subject: [PATCH 95/98] Fixed typo --- examples/user_guide/04-Style_Mapping.ipynb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/user_guide/04-Style_Mapping.ipynb b/examples/user_guide/04-Style_Mapping.ipynb index 8a03443b36..4647b799f1 100644 --- a/examples/user_guide/04-Style_Mapping.ipynb +++ b/examples/user_guide/04-Style_Mapping.ipynb @@ -532,7 +532,7 @@ "\n", "When using a colormap, there are three available color normalization or `cnorm` options to determine how numerical values are mapped to the range of colors in the colorbar:\n", "\n", - "* `'linear'`: Simple linear mapping (used by default)\n", + "* `linear`: Simple linear mapping (used by default)\n", "* `log`: Logarithmic mapping\n", "* `eq_hist`: Histogram-equalized mapping\n", "\n", From d07f151283595ecb5c0a56cac7731fce8e3233f2 Mon Sep 17 00:00:00 2001 From: "James A. Bednar" Date: Tue, 1 Dec 2020 16:54:03 -0600 Subject: [PATCH 96/98] Fixed i.e./e.g. --- holoviews/core/util.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/holoviews/core/util.py b/holoviews/core/util.py index 8987a43ed5..51fcc5577f 100644 --- a/holoviews/core/util.py +++ b/holoviews/core/util.py @@ -150,7 +150,7 @@ class Config(param.ParameterizedFunction): value was 'fire' which can be set for backwards compatibility.""") default_gridded_cmap = param.String(default='kbc_r', doc=""" Global - default colormap for gridded elements (i.e Image, Raster and + default colormap for gridded elements (i.e. Image, Raster and QuadMesh). Can be set to 'fire' to match raster defaults prior to HoloViews 1.14.0 while allowing the default_cmap to be the value of 'kbc_r' used in HoloViews >= 1.14.0""") @@ -181,7 +181,7 @@ class HashableJSON(json.JSONEncoder): their id. One limitation of this approach is that dictionaries with composite - keys (e.g tuples) are not supported due to the JSON spec. + keys (e.g. tuples) are not supported due to the JSON spec. """ string_hashable = (dt.datetime,) repr_hashable = () @@ -412,7 +412,7 @@ def validate_dynamic_argspec(callback, kdims, streams): appropriate signature. If validation succeeds, returns a list of strings to be zipped with - the positional arguments i.e kdim values. The zipped values can then + the positional arguments, i.e. kdim values. The zipped values can then be merged with the stream values to pass everything to the Callable as keywords. @@ -511,11 +511,11 @@ def callable_name(callable_obj): def process_ellipses(obj, key, vdim_selection=False): """ Helper function to pad a __getitem__ key with the right number of - empty slices (i.e :) when the key contains an Ellipsis (...). + empty slices (i.e. :) when the key contains an Ellipsis (...). If the vdim_selection flag is true, check if the end of the key contains strings or Dimension objects in obj. If so, extra padding - will not be applied for the value dimensions (i.e the resulting key + will not be applied for the value dimensions (i.e. the resulting key will be exactly one longer than the number of kdims). Note: this flag should not be used for composite types. """ @@ -534,7 +534,7 @@ def process_ellipses(obj, key, vdim_selection=False): padlen = dim_count - (len(head) + len(tail)) if vdim_selection: - # If the end of the key (i.e the tail) is in vdims, pad to len(kdims)+1 + # If the end of the key (i.e. the tail) is in vdims, pad to len(kdims)+1 if wrapped_key[-1] in obj.vdims: padlen = (len(obj.kdims) +1 ) - len(head+tail) return head + ((slice(None),) * padlen) + tail @@ -1949,7 +1949,7 @@ def arglexsort(arrays): def dimensioned_streams(dmap): """ Given a DynamicMap return all streams that have any dimensioned - parameters i.e parameters also listed in the key dimensions. + parameters, i.e. parameters also listed in the key dimensions. """ dimensioned = [] for stream in dmap.streams: From 563a0723f5045f9f5aa4dc35fef47d7f469861d0 Mon Sep 17 00:00:00 2001 From: jlstevens Date: Wed, 2 Dec 2020 00:01:30 +0100 Subject: [PATCH 97/98] Updated clash warning when both cnorm and normalization supplied --- holoviews/operation/datashader.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/holoviews/operation/datashader.py b/holoviews/operation/datashader.py index 6a2601e097..ec5d17cc15 100644 --- a/holoviews/operation/datashader.py +++ b/holoviews/operation/datashader.py @@ -1266,8 +1266,9 @@ def _process(self, element, key=None): overrides = dict(self.p.items()) if 'normalization' in overrides: if 'cnorm' in overrides: - self.param.warning("Cannot supply both 'normalization' and 'cnorm' for shading; " - "use 'cnorm' instead'") + self.param.warning("Both the 'cnorm' and 'normalization' keywords" + "specified; 'cnorm' value taking precedence over " + "deprecated 'normalization' option") elif config.future_deprecations: self.param.warning("Shading 'normalization' parameter deprecated, " "use 'cnorm' parameter instead'") From b9680e3c06ec1f094d75fc1aa4947b03ca46788c Mon Sep 17 00:00:00 2001 From: jlstevens Date: Wed, 2 Dec 2020 00:06:25 +0100 Subject: [PATCH 98/98] Added hv.config.default_heatmap_cmap option --- holoviews/core/util.py | 14 +++++++++----- holoviews/plotting/bokeh/__init__.py | 2 +- holoviews/plotting/mpl/__init__.py | 2 +- holoviews/plotting/plotly/__init__.py | 2 +- 4 files changed, 12 insertions(+), 8 deletions(-) diff --git a/holoviews/core/util.py b/holoviews/core/util.py index 51fcc5577f..1d550c2d82 100644 --- a/holoviews/core/util.py +++ b/holoviews/core/util.py @@ -149,11 +149,15 @@ class Config(param.ParameterizedFunction): Global default colormap. Prior to HoloViews 1.14.0, the default value was 'fire' which can be set for backwards compatibility.""") - default_gridded_cmap = param.String(default='kbc_r', doc=""" Global - default colormap for gridded elements (i.e. Image, Raster and - QuadMesh). Can be set to 'fire' to match raster defaults prior to - HoloViews 1.14.0 while allowing the default_cmap to be the value - of 'kbc_r' used in HoloViews >= 1.14.0""") + default_gridded_cmap = param.String(default='kbc_r', doc=""" + Global default colormap for gridded elements (i.e. Image, Raster + and QuadMesh). Can be set to 'fire' to match raster defaults + prior to HoloViews 1.14.0 while allowing the default_cmap to be + the value of 'kbc_r' used in HoloViews >= 1.14.0""") + + default_heatmap_cmap = param.String(default='kbc_r', doc=""" + Global default colormap for HeatMap elements. Prior to HoloViews + 1.14.0, the default value was the 'RdYlBu_r' colormap.""") def __call__(self, **params): self.param.set_param(**params) diff --git a/holoviews/plotting/bokeh/__init__.py b/holoviews/plotting/bokeh/__init__.py index cfc239f6ad..e4a76514f7 100644 --- a/holoviews/plotting/bokeh/__init__.py +++ b/holoviews/plotting/bokeh/__init__.py @@ -198,7 +198,7 @@ def colormap_generator(palette): options.Image = Options('style', cmap=config.default_gridded_cmap) options.Raster = Options('style', cmap=config.default_gridded_cmap) options.QuadMesh = Options('style', cmap=config.default_gridded_cmap, line_alpha=0) -options.HeatMap = Options('style', cmap='RdYlBu_r', annular_line_alpha=0, +options.HeatMap = Options('style', cmap=config.default_heatmap_cmap, annular_line_alpha=0, xmarks_line_color="#FFFFFF", xmarks_line_width=3, ymarks_line_color="#FFFFFF", ymarks_line_width=3) diff --git a/holoviews/plotting/mpl/__init__.py b/holoviews/plotting/mpl/__init__.py index 7684d3f02d..019bf490e8 100644 --- a/holoviews/plotting/mpl/__init__.py +++ b/holoviews/plotting/mpl/__init__.py @@ -256,7 +256,7 @@ def grid_selector(grid): options.Image = Options('style', cmap=config.default_gridded_cmap, interpolation='nearest') options.Raster = Options('style', cmap=config.default_gridded_cmap, interpolation='nearest') options.QuadMesh = Options('style', cmap=config.default_gridded_cmap) -options.HeatMap = Options('style', cmap='RdYlBu_r', edgecolors='white', +options.HeatMap = Options('style', cmap=config.default_heatmap_cmap, edgecolors='white', annular_edgecolors='white', annular_linewidth=0.5, xmarks_edgecolor='white', xmarks_linewidth=3, ymarks_edgecolor='white', ymarks_linewidth=3, diff --git a/holoviews/plotting/plotly/__init__.py b/holoviews/plotting/plotly/__init__.py index e48548e4d5..06398a331d 100644 --- a/holoviews/plotting/plotly/__init__.py +++ b/holoviews/plotting/plotly/__init__.py @@ -124,7 +124,7 @@ options.Image = Options('style', cmap=config.default_gridded_cmap) options.Raster = Options('style', cmap=config.default_gridded_cmap) options.QuadMesh = Options('style', cmap=config.default_gridded_cmap) -options.HeatMap = Options('style', cmap='RdYlBu_r') +options.HeatMap = Options('style', cmap=config.default_heatmap_cmap) # Disable padding for image-like elements options.Image = Options("plot", padding=0)