From c6ac9a27bb42b46a1aa3111a817c11d656074b01 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Sat, 25 Nov 2017 22:02:09 +0000 Subject: [PATCH 001/117] Added ops --- holoviews/plotting/bokeh/element.py | 7 +++ holoviews/util/ops.py | 83 +++++++++++++++++++++++++++++ 2 files changed, 90 insertions(+) create mode 100644 holoviews/util/ops.py diff --git a/holoviews/plotting/bokeh/element.py b/holoviews/plotting/bokeh/element.py index 35597202aa..10810e9089 100644 --- a/holoviews/plotting/bokeh/element.py +++ b/holoviews/plotting/bokeh/element.py @@ -26,6 +26,7 @@ from ...core import util from ...element import Graph from ...streams import Buffer +from ...util.ops import op from ..plot import GenericElementPlot, GenericOverlayPlot from ..util import dynamic_update, process_cmap, color_intervals from .plot import BokehPlot, TOOLS @@ -664,6 +665,12 @@ def _init_glyph(self, plot, mapping, properties): def _glyph_properties(self, plot, element, source, ranges, style): + for k, v in style.items(): + if not isinstance(v, op): + continue + source.data[k] = v.eval(element) + style[k] = k + properties = dict(style, source=source) if self.show_legend: if self.overlay_dims: diff --git a/holoviews/util/ops.py b/holoviews/util/ops.py new file mode 100644 index 0000000000..d0375bfc00 --- /dev/null +++ b/holoviews/util/ops.py @@ -0,0 +1,83 @@ +import operator +import numpy as np + +from ..core.dimension import Dimension + +def norm_fn(values, min=None, max=None): + min = np.min(values) if min is None else min + max = np.max(values) if max is None else max + return (values - min) / (max-min) + +class op(object): + + _op_registry = {'norm': norm_fn} + + def __init__(self, obj, fn=None, other=None, reverse=False, **kwargs): + if isinstance(obj, (str, Dimension)): + self.dimension = obj + ops = [] + else: + self.dimension = obj.dimension + ops = obj.ops + if isinstance(fn, str): + fn = self._op_registry.get(fn) + if fn is None: + raise ValueError('Operation function %s not found' % fn) + if fn is not None: + ops = ops + [{'other': other, 'fn': fn, 'kwargs': kwargs, + 'reverse': reverse}] + self.ops = ops + + # Unary operators + def __abs__(self): return op(self, operator.abs) + def __neg__(self): return op(self, operator.neg) + def __pos__(self): return op(self, operator.pos) + + # Binary operators + def __add__(self, other): return op(self, operator.add, other) + def __div__(self, other): return op(self, operator.div, other) + def __floordiv__(self, other): return op(self, operator.floordiv, other) + def __pow__(self, other): return op(self, operator.pow, other) + def __mod__(self, other): return op(self, operator.mod, other) + def __mul__(self, other): return op(self, operator.mul, other) + def __sub__(self, other): return op(self, operator.sub, other) + def __truediv__(self, other): return op(self, operator.truediv, other) + + # Reverse binary operators + def __radd__(self, other): return op(self, operator.add, other, True) + def __rdiv__(self, other): return op(self, operator.div, other, True) + def __rfloordiv__(self, other): return op(self, operator.floordiv, other, True) + def __rmod__(self, other): return op(self, operator.mod, other, True) + def __rmul__(self, other): return op(self, operator.mul, other, True) + def __rsub__(self, other): return op(self, operator.sub, other, True) + def __rtruediv__(self, other): return op(self, operator.truediv, other, True) + + def eval(self, dataset): + expanded = not (dataset.interface.gridded and self.dimension in dataset.kdims) + data = dataset.dimension_values(self.dimension, expanded=expanded, flat=False) + for o in self.ops: + other = o['other'] + if other is not None: + if isinstance(other, op): + other = other.eval(dataset) + args = (other, data) if o['reverse'] else (data, other) + else: + args = (data,) + data = o['fn'](*args, **o['kwargs']) + return data + + def __repr__(self): + op_repr = "'%s'" % self.dimension + for o in self.ops: + arg = ', %r' % o['other'] if o['other'] else '' + kwargs = sorted(o['kwargs'].items(), key=operator.itemgetter(0)) + kwargs = ', %s' % ', '.join(['%s=%s' % item for item in kwargs]) if kwargs else '' + op_repr = '{fn}({repr}{arg}{kwargs})'.format(fn=o['fn'].__name__, repr=op_repr, + arg=arg, kwargs=kwargs) + return op_repr + + +class norm(op): + + def __init__(self, obj, **kwargs): + super(norm, self).__init__(obj, norm_fn, **kwargs) From bc6c1d3b5a008932e863b0bc8e0d6d926449583e Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Sat, 25 Nov 2017 23:23:16 +0000 Subject: [PATCH 002/117] Implement NumPy support for ops --- holoviews/util/ops.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/holoviews/util/ops.py b/holoviews/util/ops.py index d0375bfc00..85c01f8339 100644 --- a/holoviews/util/ops.py +++ b/holoviews/util/ops.py @@ -52,6 +52,19 @@ def __rmul__(self, other): return op(self, operator.mul, other, True) def __rsub__(self, other): return op(self, operator.sub, other, True) def __rtruediv__(self, other): return op(self, operator.truediv, other, True) + ## NumPy operations + def __array_ufunc__(self, *args, **kwargs): + ufunc = getattr(args[0], args[1]) + kwargs = {k: v for k, v in kwargs.items() if v is not None} + return op(self, ufunc, **kwargs) + def max(self, **kwargs): return op(self, np.max, **kwargs) + def mean(self, **kwargs): return op(self, np.mean, **kwargs) + def min(self, **kwargs): return op(self, np.min, **kwargs) + def sum(self, **kwargs): return op(self, np.sum, **kwargs) + def std(self, **kwargs): return op(self, np.std, **kwargs) + def std(self, **kwargs): return op(self, np.std, **kwargs) + def var(self, **kwargs): return op(self, np.var, **kwargs) + def eval(self, dataset): expanded = not (dataset.interface.gridded and self.dimension in dataset.kdims) data = dataset.dimension_values(self.dimension, expanded=expanded, flat=False) From 1a909df037df4960035102efa1b82180d2dd91bb Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Sat, 25 Nov 2017 23:26:51 +0000 Subject: [PATCH 003/117] Support scalar ops in plotting --- holoviews/plotting/bokeh/element.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/holoviews/plotting/bokeh/element.py b/holoviews/plotting/bokeh/element.py index 10810e9089..a6426cab65 100644 --- a/holoviews/plotting/bokeh/element.py +++ b/holoviews/plotting/bokeh/element.py @@ -668,8 +668,13 @@ def _glyph_properties(self, plot, element, source, ranges, style): for k, v in style.items(): if not isinstance(v, op): continue - source.data[k] = v.eval(element) - style[k] = k + val = v.eval(element) + if np.isscalar(val): + key = val + else: + key = k + source.data[k] = val + style[k] = key properties = dict(style, source=source) if self.show_legend: From c503afb7c8299b964f0bac763537313ab568a202 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Sat, 3 Mar 2018 01:30:39 +0000 Subject: [PATCH 004/117] Various improvements for ops --- holoviews/plotting/bokeh/element.py | 29 ++++++++++++++++++++++++----- holoviews/plotting/mpl/element.py | 23 ++++++++++++++++++++--- holoviews/util/ops.py | 21 +++++++++++++++------ holoviews/util/parser.py | 4 +++- 4 files changed, 62 insertions(+), 15 deletions(-) diff --git a/holoviews/plotting/bokeh/element.py b/holoviews/plotting/bokeh/element.py index a6426cab65..18f0a62444 100644 --- a/holoviews/plotting/bokeh/element.py +++ b/holoviews/plotting/bokeh/element.py @@ -664,19 +664,38 @@ def _init_glyph(self, plot, mapping, properties): return renderer, renderer.glyph - def _glyph_properties(self, plot, element, source, ranges, style): - for k, v in style.items(): + def _apply_ops(self, element, source, ranges, style): + new_style = dict(style) + for k, v in dict(style).items(): + if isinstance(v, util.basestring) and v in element: + v = op(v) if not isinstance(v, op): continue - val = v.eval(element) + dname = v.dimension.name + if dname not in element: + new_style.pop(k) + self.warning('Specified %s op %r could not be applied, %s dimension ' + 'could not be found' % (k, v, v.dimension)) + continue + vrange = ranges.get(dname) + val = v.eval(element, ranges) if np.isscalar(val): key = val else: key = k source.data[k] = val - style[k] = key + if ('color' in k and isinstance(val, np.ndarray) and + val.dtype.kind in 'if'): + cmapper = self._get_colormapper(v.dimension, element, ranges, + style, name=dname+'_color_mapper') + key = {'field': k, 'transform': cmapper} + new_style[k] = key + return new_style - properties = dict(style, source=source) + + def _glyph_properties(self, plot, element, source, ranges, style): + new_style = self._apply_ops(element, source, ranges, style) + properties = dict(new_style, source=source) if self.show_legend: if self.overlay_dims: legend = ', '.join([d.pprint_value(v) for d, v in diff --git a/holoviews/plotting/mpl/element.py b/holoviews/plotting/mpl/element.py index 88a6742edb..146019a2cf 100644 --- a/holoviews/plotting/mpl/element.py +++ b/holoviews/plotting/mpl/element.py @@ -13,6 +13,7 @@ CompositeOverlay, Element3D, Element) from ...core.options import abbreviated_exception from ...element import Graph +from ...util.ops import op from ..plot import GenericElementPlot, GenericOverlayPlot from ..util import dynamic_update, process_cmap, color_intervals from .plot import MPLPlot, mpl_rc_context @@ -476,6 +477,7 @@ def initialize_plot(self, ranges=None): if self.show_legend: style['label'] = element.label + style = self._apply_ops(element, ranges, style) plot_data, plot_kwargs, axis_kwargs = self.get_data(element, ranges, style) with abbreviated_exception(): @@ -503,13 +505,27 @@ def update_handles(self, key, axis, element, ranges, style): Update the elements of the plot. """ self.teardown_handles() - plot_data, plot_kwargs, axis_kwargs = self.get_data(element, ranges, style) + new_style = self._apply_ops(element, range, style) + plot_data, plot_kwargs, axis_kwargs = self.get_data(element, ranges, new_style) with abbreviated_exception(): handles = self.init_artists(axis, plot_data, plot_kwargs) self.handles.update(handles) return axis_kwargs + + def _apply_ops(self, element, ranges, style): + new_style = dict(style) + for k, v in style.items(): + if (isinstance(v, util.basestring) and v in element): + v = op(v) + if not isinstance(v, op): + continue + val = v.eval(element, ranges) + new_style[k] = val + return new_style + + def teardown_handles(self): """ If no custom update_handles method is supplied this method @@ -641,13 +657,14 @@ def _draw_colorbar(self, dim=None, redraw=True): ColorbarPlot._colorbars[id(axis)] = (ax_colorbars, (l, b, w, h)) - def _norm_kwargs(self, element, ranges, opts, vdim, prefix=''): + def _norm_kwargs(self, element, ranges, opts, vdim, values=None, prefix=''): """ Returns valid color normalization kwargs to be passed to matplotlib plot function. """ clim = opts.pop(prefix+'clims', None) - values = np.asarray(element.dimension_values(vdim)) + if values is None: + values = np.asarray(element.dimension_values(vdim)) if clim is None: if not len(values): clim = (0, 0) diff --git a/holoviews/util/ops.py b/holoviews/util/ops.py index 85c01f8339..6489d527f3 100644 --- a/holoviews/util/ops.py +++ b/holoviews/util/ops.py @@ -2,20 +2,24 @@ import numpy as np from ..core.dimension import Dimension +from ..core.util import basestring + def norm_fn(values, min=None, max=None): min = np.min(values) if min is None else min max = np.max(values) if max is None else max return (values - min) / (max-min) - + class op(object): _op_registry = {'norm': norm_fn} def __init__(self, obj, fn=None, other=None, reverse=False, **kwargs): - if isinstance(obj, (str, Dimension)): + ops = [] + if isinstance(obj, basestring): + self.dimension = Dimension(obj) + elif isinstance(obj, Dimension): self.dimension = obj - ops = [] else: self.dimension = obj.dimension ops = obj.ops @@ -57,6 +61,7 @@ def __array_ufunc__(self, *args, **kwargs): ufunc = getattr(args[0], args[1]) kwargs = {k: v for k, v in kwargs.items() if v is not None} return op(self, ufunc, **kwargs) + def max(self, **kwargs): return op(self, np.max, **kwargs) def mean(self, **kwargs): return op(self, np.mean, **kwargs) def min(self, **kwargs): return op(self, np.min, **kwargs) @@ -65,18 +70,22 @@ def std(self, **kwargs): return op(self, np.std, **kwargs) def std(self, **kwargs): return op(self, np.std, **kwargs) def var(self, **kwargs): return op(self, np.var, **kwargs) - def eval(self, dataset): + def eval(self, dataset, ranges={}): expanded = not (dataset.interface.gridded and self.dimension in dataset.kdims) data = dataset.dimension_values(self.dimension, expanded=expanded, flat=False) for o in self.ops: other = o['other'] if other is not None: if isinstance(other, op): - other = other.eval(dataset) + other = other.eval(dataset, ranges) args = (other, data) if o['reverse'] else (data, other) else: args = (data,) - data = o['fn'](*args, **o['kwargs']) + drange = ranges.get(self.dimension.name) + if o['fn'] == norm_fn and drange is not None: + data = o['fn'](data, *drange) + else: + data = o['fn'](*args, **o['kwargs']) return data def __repr__(self): diff --git a/holoviews/util/parser.py b/holoviews/util/parser.py index 591f2f96fd..eab98653c9 100644 --- a/holoviews/util/parser.py +++ b/holoviews/util/parser.py @@ -17,6 +17,7 @@ from ..core.options import Options, Cycle, Palette from ..core.util import merge_option_dicts from ..operation import Compositor +from .ops import op, norm ascii_uppercase = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' allowed = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!#$%&\()*+,-./:;<=>?@\\^_`{|}~' @@ -35,7 +36,8 @@ class Parser(object): """ # Static namespace set in __init__.py of the extension - namespace = {'np': np, 'Cycle': Cycle, 'Palette': Palette} + namespace = {'np': np, 'Cycle': Cycle, 'Palette': Palette, 'op': op, + 'norm': norm} # If True, raise SyntaxError on eval error otherwise warn abort_on_eval_failure = False From dc27124ac7724e72f63955989a4e0d68d7faf294 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Sat, 23 Jun 2018 15:34:56 +0100 Subject: [PATCH 005/117] Improved handling for op style mappings --- holoviews/plotting/bokeh/annotation.py | 3 ++- holoviews/plotting/bokeh/chart.py | 5 ++++- holoviews/plotting/bokeh/element.py | 4 ++++ holoviews/plotting/mpl/chart.py | 7 +++++-- holoviews/plotting/mpl/element.py | 10 ++++++++++ 5 files changed, 25 insertions(+), 4 deletions(-) diff --git a/holoviews/plotting/bokeh/annotation.py b/holoviews/plotting/bokeh/annotation.py index a94490b4ba..4f4f31436f 100644 --- a/holoviews/plotting/bokeh/annotation.py +++ b/holoviews/plotting/bokeh/annotation.py @@ -88,7 +88,8 @@ class LabelsPlot(ColorbarPlot, AnnotationPlot): def get_data(self, element, ranges, style): style = self.style[self.cyclic_index] - style['angle'] = np.deg2rad(style.get('angle', 0)) + if 'angle' in style and isinstance(style['angle'], (int, float)): + style['angle'] = np.deg2rad(style.get('angle', 0)) dims = element.dimensions() coords = (1, 0) if self.invert_axes else (0, 1) diff --git a/holoviews/plotting/bokeh/chart.py b/holoviews/plotting/bokeh/chart.py index a92b9cc28f..40568ec052 100644 --- a/holoviews/plotting/bokeh/chart.py +++ b/holoviews/plotting/bokeh/chart.py @@ -43,7 +43,7 @@ class PointPlot(LegendPlot, ColorbarPlot): Function applied to size values before applying scaling, to remove values lower than zero.""") - style_opts = (['cmap', 'palette', 'marker', 'size'] + + style_opts = (['cmap', 'palette', 'marker', 'size', 'angle'] + line_properties + fill_properties) _plot_methods = dict(single='scatter', batched='scatter') @@ -92,6 +92,9 @@ def get_data(self, element, ranges, style): data.update(sdata) mapping.update(smapping) + if 'angle' in style and isinstance(style['angle'], (int, float)): + style['angle'] = np.deg2rad(style['angle']) + if self.jitter: axrange = 'y_range' if self.invert_axes else 'x_range' mapping['x'] = jitter(dims[xidx], self.jitter, diff --git a/holoviews/plotting/bokeh/element.py b/holoviews/plotting/bokeh/element.py index 18f0a62444..15e352b6d7 100644 --- a/holoviews/plotting/bokeh/element.py +++ b/holoviews/plotting/bokeh/element.py @@ -679,6 +679,10 @@ def _apply_ops(self, element, source, ranges, style): continue vrange = ranges.get(dname) val = v.eval(element, ranges) + if len(np.unique(val)) == 1: + val = val if np.isscalar(val) else val[0] + if k == 'angle': + val = np.deg2rad(val) if np.isscalar(val): key = val else: diff --git a/holoviews/plotting/mpl/chart.py b/holoviews/plotting/mpl/chart.py index bd82f89d76..7807122850 100644 --- a/holoviews/plotting/mpl/chart.py +++ b/holoviews/plotting/mpl/chart.py @@ -576,8 +576,11 @@ def _compute_styles(self, element, ranges, style): ypos = np.searchsorted(categories, cs) style['c'] = xsorted[ypos] self._norm_kwargs(element, ranges, style, cdim) - elif color: - style['c'] = color + elif color is not None: + if np.isscalar(color): + style['c'] = color + else: + style['color'] = color style['edgecolors'] = style.pop('edgecolors', style.pop('edgecolor', 'none')) sdim = element.get_dimension(self.size_index) diff --git a/holoviews/plotting/mpl/element.py b/holoviews/plotting/mpl/element.py index 146019a2cf..8313deeb97 100644 --- a/holoviews/plotting/mpl/element.py +++ b/holoviews/plotting/mpl/element.py @@ -522,6 +522,16 @@ def _apply_ops(self, element, ranges, style): if not isinstance(v, op): continue val = v.eval(element, ranges) + if len(np.unique(val)) == 1: + val = val if np.isscalar(val) else val[0] + if k == 'alpha' and not np.isscalar(val): + self.warning('The matplotlib backend currently does not ' + 'support scaling the alpha by a dimension.') + continue + if k == 'marker': + self.warning('The matplotlib backend currently does not ' + 'support mapping a dimension to the marker type.') + continue new_style[k] = val return new_style From 073cbf407f92c06a5a828599c7f447df8f491a10 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Sat, 23 Jun 2018 17:43:06 +0100 Subject: [PATCH 006/117] Enabled op style mapping on Composite glyphs --- holoviews/plotting/bokeh/element.py | 14 ++++++++------ holoviews/plotting/bokeh/graphs.py | 3 ++- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/holoviews/plotting/bokeh/element.py b/holoviews/plotting/bokeh/element.py index 15e352b6d7..25ab688622 100644 --- a/holoviews/plotting/bokeh/element.py +++ b/holoviews/plotting/bokeh/element.py @@ -664,12 +664,12 @@ def _init_glyph(self, plot, mapping, properties): return renderer, renderer.glyph - def _apply_ops(self, element, source, ranges, style): + def _apply_ops(self, element, source, ranges, style, group=None): new_style = dict(style) for k, v in dict(style).items(): if isinstance(v, util.basestring) and v in element: v = op(v) - if not isinstance(v, op): + if not isinstance(v, op) or (group is not None and not k.startswith(group)): continue dname = v.dimension.name if dname not in element: @@ -697,8 +697,8 @@ def _apply_ops(self, element, source, ranges, style): return new_style - def _glyph_properties(self, plot, element, source, ranges, style): - new_style = self._apply_ops(element, source, ranges, style) + def _glyph_properties(self, plot, element, source, ranges, style, group=None): + new_style = self._apply_ops(element, source, ranges, style, group) properties = dict(new_style, source=source) if self.show_legend: if self.overlay_dims: @@ -1020,7 +1020,8 @@ def _init_glyphs(self, plot, element, ranges, source, data=None, mapping=None, s source = self._init_datasource(ds_data) source_cache[id(ds_data)] = source self.handles[key+'_source'] = source - properties = self._glyph_properties(plot, element, source, ranges, style) + style_group = self._style_groups.get('_'.join(key.split('_')[:-1])) + properties = self._glyph_properties(plot, element, source, ranges, style, style_group) properties = self._process_properties(key, properties, mapping.get(key, {})) with abbreviated_exception(): renderer, glyph = self._init_glyph(plot, mapping.get(key, {}), properties, key) @@ -1074,7 +1075,8 @@ def _update_glyphs(self, element, ranges): glyph = self.handles.get(key+'_glyph') if glyph: - properties = self._glyph_properties(plot, element, source, ranges, style) + style_group = self._style_groups.get('_'.join(key.split('_')[:-1])) + properties = self._glyph_properties(plot, element, source, ranges, style, style_group) properties = self._process_properties(key, properties, mapping[key]) renderer = self.handles.get(key+'_glyph_renderer') with abbreviated_exception(): diff --git a/holoviews/plotting/bokeh/graphs.py b/holoviews/plotting/bokeh/graphs.py index 133789a564..442a17caa0 100644 --- a/holoviews/plotting/bokeh/graphs.py +++ b/holoviews/plotting/bokeh/graphs.py @@ -246,7 +246,8 @@ def _init_glyphs(self, plot, element, ranges, source): continue source = self._init_datasource(data.pop(key, {})) self.handles[key+'_source'] = source - glyph_props = self._glyph_properties(plot, element, source, ranges, style) + style_group = self._style_groups.get('_'.join(key.split('_')[:-1])) + glyph_props = self._glyph_properties(plot, element, source, ranges, style, style_group) properties.update(glyph_props) mappings.update(mapping.pop(key, {})) properties = {p: v for p, v in properties.items() if p not in ('legend', 'source')} From 1f09d599908a754ef376ee1b0bd5bf68f25109f0 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Sun, 24 Jun 2018 11:27:36 +0100 Subject: [PATCH 007/117] Handle scalar values in ops mapping --- holoviews/plotting/bokeh/element.py | 6 ++++++ holoviews/util/ops.py | 3 ++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/holoviews/plotting/bokeh/element.py b/holoviews/plotting/bokeh/element.py index 25ab688622..bd8769bb3a 100644 --- a/holoviews/plotting/bokeh/element.py +++ b/holoviews/plotting/bokeh/element.py @@ -678,9 +678,15 @@ def _apply_ops(self, element, source, ranges, style, group=None): 'could not be found' % (k, v, v.dimension)) continue vrange = ranges.get(dname) + val = v.eval(element, ranges) + length = [len(v) for v in source.data.values()][0] if len(np.unique(val)) == 1: val = val if np.isscalar(val) else val[0] + + if not np.isscalar(val) and len(val) != length: + continue + if k == 'angle': val = np.deg2rad(val) if np.isscalar(val): diff --git a/holoviews/util/ops.py b/holoviews/util/ops.py index 6489d527f3..ea2cd1f23b 100644 --- a/holoviews/util/ops.py +++ b/holoviews/util/ops.py @@ -72,7 +72,8 @@ def var(self, **kwargs): return op(self, np.var, **kwargs) def eval(self, dataset, ranges={}): expanded = not (dataset.interface.gridded and self.dimension in dataset.kdims) - data = dataset.dimension_values(self.dimension, expanded=expanded, flat=False) + isscalar = dataset.interface.isscalar(dataset, self.dimension) + data = dataset.dimension_values(self.dimension, expanded=expanded and not isscalar, flat=False) for o in self.ops: other = o['other'] if other is not None: From ddd5a656a8cd2241e117be3f6a9bc904fd6e2b0d Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Mon, 25 Jun 2018 12:52:49 +0100 Subject: [PATCH 008/117] Fixed handling of multi-interface in ops --- holoviews/util/ops.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/holoviews/util/ops.py b/holoviews/util/ops.py index ea2cd1f23b..97d2db54b7 100644 --- a/holoviews/util/ops.py +++ b/holoviews/util/ops.py @@ -71,9 +71,9 @@ def std(self, **kwargs): return op(self, np.std, **kwargs) def var(self, **kwargs): return op(self, np.var, **kwargs) def eval(self, dataset, ranges={}): - expanded = not (dataset.interface.gridded and self.dimension in dataset.kdims) - isscalar = dataset.interface.isscalar(dataset, self.dimension) - data = dataset.dimension_values(self.dimension, expanded=expanded and not isscalar, flat=False) + expanded = not ((dataset.interface.gridded and self.dimension in dataset.kdims) or + (dataset.interface.multi and dataset.interface.isscalar(dataset, self.dimension))) + data = dataset.dimension_values(self.dimension, expanded=expanded, flat=False) for o in self.ops: other = o['other'] if other is not None: From a076722ba77b78b3d9ee608106e0b0fcb733f026 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Mon, 25 Jun 2018 12:53:28 +0100 Subject: [PATCH 009/117] Enabled ops mapping for mpl Labels --- holoviews/plotting/mpl/annotation.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/holoviews/plotting/mpl/annotation.py b/holoviews/plotting/mpl/annotation.py index 376c31ae1c..e3b9a7d041 100644 --- a/holoviews/plotting/mpl/annotation.py +++ b/holoviews/plotting/mpl/annotation.py @@ -99,7 +99,7 @@ class LabelsPlot(ColorbarPlot): Amount of offset to apply to labels along x-axis.""") style_opts = ['alpha', 'color', 'family', 'weight', 'size', 'visible', - 'horizontalalignment', 'verticalalignment', 'cmap'] + 'horizontalalignment', 'verticalalignment', 'cmap', 'rotation'] _plot_methods = dict(single='annotate') @@ -131,8 +131,10 @@ def init_artists(self, ax, plot_args, plot_kwargs): cmap = None plot_args = plot_args[:-1] + vectorized = {k: v for k, v in plot_kwargs.items() if isinstance(v, np.ndarray)} + texts = [] - for item in zip(*plot_args): + for i, item in enumerate(zip(*plot_args)): x, y, text = item[:3] if len(item) == 4 and cmap is not None: color = item[3] @@ -142,7 +144,8 @@ def init_artists(self, ax, plot_args, plot_kwargs): else: color = colors.index(color) if color in colors else np.NaN plot_kwargs['color'] = cmap(color) - texts.append(ax.text(x, y, text, **plot_kwargs)) + kwargs = dict(plot_kwargs, **{k: v[i] for k, v in vectorized.items()}) + texts.append(ax.text(x, y, text, **kwargs)) return {'artist': texts} def teardown_handles(self): From b2ec0220e941ce5135caf527fe969373d6dfa8ef Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Mon, 25 Jun 2018 12:53:55 +0100 Subject: [PATCH 010/117] Improved of unsupported matplotlib ops mapping --- holoviews/plotting/mpl/element.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/holoviews/plotting/mpl/element.py b/holoviews/plotting/mpl/element.py index 8313deeb97..3ebfb5acd7 100644 --- a/holoviews/plotting/mpl/element.py +++ b/holoviews/plotting/mpl/element.py @@ -524,13 +524,15 @@ def _apply_ops(self, element, ranges, style): val = v.eval(element, ranges) if len(np.unique(val)) == 1: val = val if np.isscalar(val) else val[0] - if k == 'alpha' and not np.isscalar(val): + if k == 'alpha' and not np.isscalar(val) and not self._plot_methods.get('single') == 'annotate': self.warning('The matplotlib backend currently does not ' 'support scaling the alpha by a dimension.') + new_style.pop('alpha') continue if k == 'marker': self.warning('The matplotlib backend currently does not ' 'support mapping a dimension to the marker type.') + new_style.pop('marker') continue new_style[k] = val return new_style From 93d1cbd023d8e06603b906f6d74f0c49d44061e9 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Sun, 23 Sep 2018 13:52:50 +0100 Subject: [PATCH 011/117] Add unit tests for bokeh PointPlot ops --- .../tests/plotting/bokeh/testpointplot.py | 94 ++++++++++++++++++- 1 file changed, 93 insertions(+), 1 deletion(-) diff --git a/holoviews/tests/plotting/bokeh/testpointplot.py b/holoviews/tests/plotting/bokeh/testpointplot.py index 8543bf7f38..6ba27f8ebf 100644 --- a/holoviews/tests/plotting/bokeh/testpointplot.py +++ b/holoviews/tests/plotting/bokeh/testpointplot.py @@ -4,7 +4,7 @@ import numpy as np from holoviews.core import NdOverlay -from holoviews.core.options import Cycle +from holoviews.core.options import Cycle, AbbreviatedException from holoviews.core.util import pd from holoviews.element import Points @@ -320,3 +320,95 @@ def test_points_datetime_hover(self): self.assertEqual(cds.data['date_dt_strings'], ['2017-01-01 00:00:00']) hover = plot.handles['hover'] self.assertEqual(hover.tooltips, [('x', '@{x}'), ('y', '@{y}'), ('date', '@{date_dt_strings}')]) + + def test_point_color_op(self): + points = Points([(0, 0, '#000'), (0, 1, '#F00'), (0, 2, '#0F0')], + vdims='color').options(color='color') + plot = bokeh_renderer.get_plot(points) + cds = plot.handles['cds'] + glyph = plot.handles['glyph'] + self.assertEqual(cds.data['color'], np.array(['#000', '#F00', '#0F0'])) + self.assertEqual(glyph.fill_color, 'color') + self.assertEqual(glyph.line_color, 'color') + + def test_point_line_color_op(self): + points = Points([(0, 0, '#000'), (0, 1, '#F00'), (0, 2, '#0F0')], + vdims='color').options(line_color='color') + plot = bokeh_renderer.get_plot(points) + cds = plot.handles['cds'] + glyph = plot.handles['glyph'] + self.assertEqual(cds.data['line_color'], np.array(['#000', '#F00', '#0F0'])) + self.assertNotEqual(glyph.fill_color, 'line_color') + self.assertEqual(glyph.line_color, 'line_color') + + def test_point_line_color_op(self): + points = Points([(0, 0, '#000'), (0, 1, '#F00'), (0, 2, '#0F0')], + vdims='color').options(fill_color='color') + plot = bokeh_renderer.get_plot(points) + cds = plot.handles['cds'] + glyph = plot.handles['glyph'] + self.assertEqual(cds.data['fill_color'], np.array(['#000', '#F00', '#0F0'])) + self.assertEqual(glyph.fill_color, 'fill_color') + self.assertNotEqual(glyph.line_color, 'fill_color') + + def test_point_angle_op(self): + points = Points([(0, 0, 0), (0, 1, 45), (0, 2, 90)], + vdims='angle').options(angle='angle') + plot = bokeh_renderer.get_plot(points) + cds = plot.handles['cds'] + glyph = plot.handles['glyph'] + self.assertEqual(cds.data['angle'], np.array([0, 0.785398, 1.570796])) + self.assertEqual(glyph.angle, 'angle') + + def test_point_alpha_op(self): + points = Points([(0, 0, 0), (0, 1, 0.2), (0, 2, 0.7)], + vdims='alpha').options(alpha='alpha') + plot = bokeh_renderer.get_plot(points) + cds = plot.handles['cds'] + glyph = plot.handles['glyph'] + self.assertEqual(cds.data['alpha'], np.array([0, 0.2, 0.7])) + self.assertEqual(glyph.fill_alpha, 'alpha') + + def test_point_line_alpha_op(self): + points = Points([(0, 0, 0), (0, 1, 0.2), (0, 2, 0.7)], + vdims='alpha').options(line_alpha='alpha') + plot = bokeh_renderer.get_plot(points) + cds = plot.handles['cds'] + glyph = plot.handles['glyph'] + self.assertEqual(cds.data['line_alpha'], np.array([0, 0.2, 0.7])) + self.assertEqual(glyph.line_alpha, 'line_alpha') + self.assertNotEqual(glyph.fill_alpha, 'line_alpha') + + def test_point_fill_alpha_op(self): + points = Points([(0, 0, 0), (0, 1, 0.2), (0, 2, 0.7)], + vdims='alpha').options(fill_alpha='alpha') + plot = bokeh_renderer.get_plot(points) + cds = plot.handles['cds'] + glyph = plot.handles['glyph'] + self.assertEqual(cds.data['fill_alpha'], np.array([0, 0.2, 0.7])) + self.assertNotEqual(glyph.line_alpha, 'fill_alpha') + self.assertEqual(glyph.fill_alpha, 'fill_alpha') + + def test_point_size_op(self): + points = Points([(0, 0, 1), (0, 1, 4), (0, 2, 8)], + vdims='size').options(size='size') + plot = bokeh_renderer.get_plot(points) + cds = plot.handles['cds'] + glyph = plot.handles['glyph'] + self.assertEqual(cds.data['size'], np.array([1, 4, 8])) + self.assertEqual(glyph.size, 'size') + + def test_point_line_width_op(self): + points = Points([(0, 0, 1), (0, 1, 4), (0, 2, 8)], + vdims='line_width').options(line_width='line_width') + plot = bokeh_renderer.get_plot(points) + cds = plot.handles['cds'] + glyph = plot.handles['glyph'] + self.assertEqual(cds.data['line_width'], np.array([1, 4, 8])) + self.assertEqual(glyph.line_width, 'line_width') + + def test_point_marker_op(self): + points = Points([(0, 0, 'circle'), (0, 1, 'triangle'), (0, 2, 'square')], + vdims='marker').options(marker='marker') + with self.assertRaises(AbbreviatedException): + plot = bokeh_renderer.get_plot(points) From a8d515a95dff87aa7d14737f584acf656b8e5c11 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Mon, 24 Sep 2018 04:00:01 +0100 Subject: [PATCH 012/117] Fixes and improvements for Point mappings --- holoviews/plotting/bokeh/element.py | 51 ++++++++++++++----- holoviews/plotting/bokeh/util.py | 18 +++++++ .../tests/plotting/bokeh/testpointplot.py | 40 +++++++++++++-- 3 files changed, 93 insertions(+), 16 deletions(-) diff --git a/holoviews/plotting/bokeh/element.py b/holoviews/plotting/bokeh/element.py index bd8769bb3a..5e63562f95 100644 --- a/holoviews/plotting/bokeh/element.py +++ b/holoviews/plotting/bokeh/element.py @@ -5,6 +5,7 @@ import numpy as np import bokeh import bokeh.plotting + from bokeh.core.properties import value from bokeh.document.events import ModelChangedEvent from bokeh.models import (HoverTool, Renderer, Range1d, DataRange1d, Title, @@ -30,10 +31,11 @@ from ..plot import GenericElementPlot, GenericOverlayPlot from ..util import dynamic_update, process_cmap, color_intervals from .plot import BokehPlot, TOOLS -from .util import (mpl_to_bokeh, get_tab_title, py2js_tickformatter, - rgba_tuple, recursive_model_update, glyph_order, - decode_bytes, bokeh_version, theme_attr_json, - cds_column_replace, hold_policy) +from .util import ( + mpl_to_bokeh, get_tab_title, py2js_tickformatter, rgba_tuple, + recursive_model_update, glyph_order, decode_bytes, bokeh_version, + theme_attr_json, is_color, cds_column_replace, hold_policy +) property_prefixes = ['selection', 'nonselection', 'muted', 'hover'] @@ -53,6 +55,8 @@ legend_dimensions = ['label_standoff', 'label_width', 'label_height', 'glyph_width', 'glyph_height', 'legend_padding', 'legend_spacing', 'click_policy'] +no_op_styles = ['cmap', 'palette', 'marker'] + class ElementPlot(BokehPlot, GenericElementPlot): @@ -667,25 +671,41 @@ def _init_glyph(self, plot, mapping, properties): def _apply_ops(self, element, source, ranges, style, group=None): new_style = dict(style) for k, v in dict(style).items(): - if isinstance(v, util.basestring) and v in element: - v = op(v) + if isinstance(v, util.basestring): + if v in element: + v = op(v) + elif any(d==v for d in self.overlay_dims): + v = op([d for d in self.overlay_dims if d==v][0]) if not isinstance(v, op) or (group is not None and not k.startswith(group)): continue dname = v.dimension.name - if dname not in element: + print(v.dimension) + if dname not in element and v.dimension not in self.overlay_dims: new_style.pop(k) self.warning('Specified %s op %r could not be applied, %s dimension ' 'could not be found' % (k, v, v.dimension)) continue vrange = ranges.get(dname) - val = v.eval(element, ranges) - length = [len(v) for v in source.data.values()][0] + if len(v.ops) == 0 and v.dimension in self.overlay_dims: + val = self.overlay_dims[v.dimension] + else: + val = v.eval(element, ranges) + if len(np.unique(val)) == 1: val = val if np.isscalar(val) else val[0] - if not np.isscalar(val) and len(val) != length: - continue + if not np.isscalar(val): + lengths = [len(v) for v in source.data.values()] + if k in no_op_styles: + raise ValueError('Mapping the a dimension to the "{style}" ' + 'style option is not supported. To ' + 'map the {dim} dimension to the {style} ' + 'use a groupby operation to overlay ' + 'your data along the dimension.'.format( + style=k, dim=v.dimension)) + elif source.data and len(val) != len(list(source.data.values())[0]): + continue if k == 'angle': val = np.deg2rad(val) @@ -694,10 +714,15 @@ def _apply_ops(self, element, source, ranges, style, group=None): else: key = k source.data[k] = val + + numeric = isinstance(val, np.ndarray) and val.dtype.kind in 'uifMm' if ('color' in k and isinstance(val, np.ndarray) and - val.dtype.kind in 'if'): + (numeric or not all(is_color(v) for v in val))): + kwargs = {} + if val.dtype.kind not in 'if': + kwargs['factors'] = np.unique(val) cmapper = self._get_colormapper(v.dimension, element, ranges, - style, name=dname+'_color_mapper') + style, name=dname+'_color_mapper', **kwargs) key = {'field': k, 'transform': cmapper} new_style[k] = key return new_style diff --git a/holoviews/plotting/bokeh/util.py b/holoviews/plotting/bokeh/util.py index f0e2768fec..298db80197 100644 --- a/holoviews/plotting/bokeh/util.py +++ b/holoviews/plotting/bokeh/util.py @@ -17,6 +17,7 @@ bokeh_version = LooseVersion(bokeh.__version__) # noqa +from bokeh.colors.named import __all__ as named_colors from bokeh.core.enums import Palette from bokeh.core.json_encoder import serialize_json # noqa (API import) from bokeh.core.properties import value @@ -57,6 +58,8 @@ '3': {'marker': 'triangle', 'angle': np.pi}, '4': {'marker': 'triangle', 'angle': np.pi/2}} +RGB_HEX_REGEX = re.compile(r'^#(?:[0-9a-fA-F]{3}){1,2}$') + def convert_timestamp(timestamp): """ @@ -76,6 +79,21 @@ def rgba_tuple(rgba): return COLOR_ALIASES.get(rgba, rgba) +def is_color(color): + """ + Checks if a color + """ + if not isinstance(color, basestring): + return False + elif RGB_HEX_REGEX.match(color): + return True + elif color in COLOR_ALIASES: + return True + elif color in named_colors: + return True + return False + + def decode_bytes(array): """ Decodes an array, list or tuple of bytestrings to avoid python 3 diff --git a/holoviews/tests/plotting/bokeh/testpointplot.py b/holoviews/tests/plotting/bokeh/testpointplot.py index 6ba27f8ebf..aeafb74ea5 100644 --- a/holoviews/tests/plotting/bokeh/testpointplot.py +++ b/holoviews/tests/plotting/bokeh/testpointplot.py @@ -4,7 +4,7 @@ import numpy as np from holoviews.core import NdOverlay -from holoviews.core.options import Cycle, AbbreviatedException +from holoviews.core.options import Cycle from holoviews.core.util import pd from holoviews.element import Points @@ -12,7 +12,8 @@ from ..utils import ParamLogStream try: - from bokeh.models import FactorRange, CategoricalColorMapper + from bokeh.models import FactorRange, LinearColorMapper, CategoricalColorMapper + from bokeh.models.glyphs import Circle, Triangle except: pass @@ -331,6 +332,33 @@ def test_point_color_op(self): self.assertEqual(glyph.fill_color, 'color') self.assertEqual(glyph.line_color, 'color') + def test_point_linear_color_op(self): + points = Points([(0, 0, 0), (0, 1, 1), (0, 2, 2)], + vdims='color').options(color='color') + plot = bokeh_renderer.get_plot(points) + cds = plot.handles['cds'] + glyph = plot.handles['glyph'] + cmapper = plot.handles['color_color_mapper'] + self.assertTrue(cmapper, LinearColorMapper) + self.assertEqual(cmapper.low, 0) + self.assertEqual(cmapper.high, 2) + self.assertEqual(cds.data['color'], np.array([0, 1, 2])) + self.assertEqual(glyph.fill_color, {'field': 'color', 'transform': cmapper}) + self.assertEqual(glyph.line_color, {'field': 'color', 'transform': cmapper}) + + def test_point_categorical_color_op(self): + points = Points([(0, 0, 'A'), (0, 1, 'B'), (0, 2, 'C')], + vdims='color').options(color='color') + plot = bokeh_renderer.get_plot(points) + cds = plot.handles['cds'] + glyph = plot.handles['glyph'] + cmapper = plot.handles['color_color_mapper'] + self.assertTrue(cmapper, CategoricalColorMapper) + self.assertEqual(cmapper.factors, np.array(['A', 'B', 'C'])) + self.assertEqual(cds.data['color'], np.array(['A', 'B', 'C'])) + self.assertEqual(glyph.fill_color, {'field': 'color', 'transform': cmapper}) + self.assertEqual(glyph.line_color, {'field': 'color', 'transform': cmapper}) + def test_point_line_color_op(self): points = Points([(0, 0, '#000'), (0, 1, '#F00'), (0, 2, '#0F0')], vdims='color').options(line_color='color') @@ -410,5 +438,11 @@ def test_point_line_width_op(self): def test_point_marker_op(self): points = Points([(0, 0, 'circle'), (0, 1, 'triangle'), (0, 2, 'square')], vdims='marker').options(marker='marker') - with self.assertRaises(AbbreviatedException): + with self.assertRaises(ValueError): plot = bokeh_renderer.get_plot(points) + + def test_op_ndoverlay_value(self): + overlay = NdOverlay({marker: Points(np.arange(i)) for i, marker in enumerate(['circle', 'triangle'])}, 'Marker').options('Points', marker='Marker') + plot = bokeh_renderer.get_plot(overlay) + for subplot, glyph_type in zip(plot.subplots.values(), [Circle, Triangle]): + self.assertIsInstance(subplot.handles['glyph'], glyph_type) From 5100e5e010951353b2f6626d9ca0454e0ee3c66b Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Mon, 24 Sep 2018 12:57:30 +0100 Subject: [PATCH 013/117] Fixed flakes and tests --- holoviews/plotting/bokeh/element.py | 3 -- holoviews/plotting/bokeh/stats.py | 5 ++- holoviews/plotting/mpl/element.py | 43 +++++++++++++------ .../tests/plotting/bokeh/testpointplot.py | 4 +- holoviews/util/ops.py | 1 - 5 files changed, 35 insertions(+), 21 deletions(-) diff --git a/holoviews/plotting/bokeh/element.py b/holoviews/plotting/bokeh/element.py index 5e63562f95..c7c2102274 100644 --- a/holoviews/plotting/bokeh/element.py +++ b/holoviews/plotting/bokeh/element.py @@ -679,13 +679,11 @@ def _apply_ops(self, element, source, ranges, style, group=None): if not isinstance(v, op) or (group is not None and not k.startswith(group)): continue dname = v.dimension.name - print(v.dimension) if dname not in element and v.dimension not in self.overlay_dims: new_style.pop(k) self.warning('Specified %s op %r could not be applied, %s dimension ' 'could not be found' % (k, v, v.dimension)) continue - vrange = ranges.get(dname) if len(v.ops) == 0 and v.dimension in self.overlay_dims: val = self.overlay_dims[v.dimension] @@ -696,7 +694,6 @@ def _apply_ops(self, element, source, ranges, style, group=None): val = val if np.isscalar(val) else val[0] if not np.isscalar(val): - lengths = [len(v) for v in source.data.values()] if k in no_op_styles: raise ValueError('Mapping the a dimension to the "{style}" ' 'style option is not supported. To ' diff --git a/holoviews/plotting/bokeh/stats.py b/holoviews/plotting/bokeh/stats.py index e44e7fe84e..033f36e5e8 100644 --- a/holoviews/plotting/bokeh/stats.py +++ b/holoviews/plotting/bokeh/stats.py @@ -93,8 +93,9 @@ def _get_axis_labels(self, *args, **kwargs): ylabel = element.vdims[0].pprint_label return xlabel, ylabel, None - def _glyph_properties(self, plot, element, source, ranges, style): - properties = dict(style, source=source) + def _glyph_properties(self, plot, element, source, ranges, style, group=None): + new_style = self._apply_ops(element, source, ranges, style, group) + properties = dict(new_style, source=source) if self.show_legend and not element.kdims: properties['legend'] = element.label return properties diff --git a/holoviews/plotting/mpl/element.py b/holoviews/plotting/mpl/element.py index 3ebfb5acd7..0c2f973302 100644 --- a/holoviews/plotting/mpl/element.py +++ b/holoviews/plotting/mpl/element.py @@ -20,6 +20,8 @@ from .util import wrap_formatter from distutils.version import LooseVersion +no_op_styles = ['marker', 'alpha', 'cmap', 'angle'] + class ElementPlot(GenericElementPlot, MPLPlot): @@ -517,23 +519,38 @@ def update_handles(self, key, axis, element, ranges, style): def _apply_ops(self, element, ranges, style): new_style = dict(style) for k, v in style.items(): - if (isinstance(v, util.basestring) and v in element): - v = op(v) + if isinstance(v, util.basestring): + if v in element: + v = op(v) + elif any(d==v for d in self.overlay_dims): + v = op([d for d in self.overlay_dims if d==v][0]) + if not isinstance(v, op): continue - val = v.eval(element, ranges) + + dname = v.dimension.name + if dname not in element and v.dimension not in self.overlay_dims: + new_style.pop(k) + self.warning('Specified %s op %r could not be applied, %s dimension ' + 'could not be found' % (k, v, v.dimension)) + continue + + if len(v.ops) == 0 and v.dimension in self.overlay_dims: + val = self.overlay_dims[v.dimension] + else: + val = v.eval(element, ranges) + if len(np.unique(val)) == 1: val = val if np.isscalar(val) else val[0] - if k == 'alpha' and not np.isscalar(val) and not self._plot_methods.get('single') == 'annotate': - self.warning('The matplotlib backend currently does not ' - 'support scaling the alpha by a dimension.') - new_style.pop('alpha') - continue - if k == 'marker': - self.warning('The matplotlib backend currently does not ' - 'support mapping a dimension to the marker type.') - new_style.pop('marker') - continue + + if not np.isscalar(val) and k in no_op_styles: + raise ValueError('Mapping the a dimension to the "{style}" ' + 'style option is not supported. To ' + 'map the {dim} dimension to the {style} ' + 'use a groupby operation to overlay ' + 'your data along the dimension.'.format( + style=k, dim=v.dimension)) + new_style[k] = val return new_style diff --git a/holoviews/tests/plotting/bokeh/testpointplot.py b/holoviews/tests/plotting/bokeh/testpointplot.py index aeafb74ea5..225671ddff 100644 --- a/holoviews/tests/plotting/bokeh/testpointplot.py +++ b/holoviews/tests/plotting/bokeh/testpointplot.py @@ -369,7 +369,7 @@ def test_point_line_color_op(self): self.assertNotEqual(glyph.fill_color, 'line_color') self.assertEqual(glyph.line_color, 'line_color') - def test_point_line_color_op(self): + def test_point_fill_color_op(self): points = Points([(0, 0, '#000'), (0, 1, '#F00'), (0, 2, '#0F0')], vdims='color').options(fill_color='color') plot = bokeh_renderer.get_plot(points) @@ -439,7 +439,7 @@ def test_point_marker_op(self): points = Points([(0, 0, 'circle'), (0, 1, 'triangle'), (0, 2, 'square')], vdims='marker').options(marker='marker') with self.assertRaises(ValueError): - plot = bokeh_renderer.get_plot(points) + bokeh_renderer.get_plot(points) def test_op_ndoverlay_value(self): overlay = NdOverlay({marker: Points(np.arange(i)) for i, marker in enumerate(['circle', 'triangle'])}, 'Marker').options('Points', marker='Marker') diff --git a/holoviews/util/ops.py b/holoviews/util/ops.py index 97d2db54b7..3ee4e81609 100644 --- a/holoviews/util/ops.py +++ b/holoviews/util/ops.py @@ -67,7 +67,6 @@ def mean(self, **kwargs): return op(self, np.mean, **kwargs) def min(self, **kwargs): return op(self, np.min, **kwargs) def sum(self, **kwargs): return op(self, np.sum, **kwargs) def std(self, **kwargs): return op(self, np.std, **kwargs) - def std(self, **kwargs): return op(self, np.std, **kwargs) def var(self, **kwargs): return op(self, np.var, **kwargs) def eval(self, dataset, ranges={}): From 09d6ab8c62931a7bbb52cc38d4bbae6c983db299 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Mon, 24 Sep 2018 14:03:16 +0100 Subject: [PATCH 014/117] Fixed op colormapping in matplotlib --- holoviews/plotting/bokeh/util.py | 6 ++---- holoviews/plotting/mpl/chart.py | 6 ++---- holoviews/plotting/mpl/element.py | 14 +++++++++++++- holoviews/plotting/mpl/util.py | 29 +++++++++++++++++++++++++++++ holoviews/plotting/util.py | 3 +++ 5 files changed, 49 insertions(+), 9 deletions(-) diff --git a/holoviews/plotting/bokeh/util.py b/holoviews/plotting/bokeh/util.py index 298db80197..0775647b05 100644 --- a/holoviews/plotting/bokeh/util.py +++ b/holoviews/plotting/bokeh/util.py @@ -43,7 +43,7 @@ dt64_to_dt, _getargspec) from ...core.spaces import get_nested_dmaps, DynamicMap -from ..util import dim_axis_label, rgb2hex, COLOR_ALIASES +from ..util import dim_axis_label, rgb2hex, COLOR_ALIASES, RGB_HEX_REGEX # Conversion between matplotlib and bokeh markers markers = {'s': {'marker': 'square'}, @@ -58,8 +58,6 @@ '3': {'marker': 'triangle', 'angle': np.pi}, '4': {'marker': 'triangle', 'angle': np.pi/2}} -RGB_HEX_REGEX = re.compile(r'^#(?:[0-9a-fA-F]{3}){1,2}$') - def convert_timestamp(timestamp): """ @@ -81,7 +79,7 @@ def rgba_tuple(rgba): def is_color(color): """ - Checks if a color + Checks if the supplied value is a valid color spec. """ if not isinstance(color, basestring): return False diff --git a/holoviews/plotting/mpl/chart.py b/holoviews/plotting/mpl/chart.py index 7807122850..cde40f53c9 100644 --- a/holoviews/plotting/mpl/chart.py +++ b/holoviews/plotting/mpl/chart.py @@ -25,6 +25,7 @@ from .element import ElementPlot, ColorbarPlot, LegendPlot from .path import PathPlot from .plot import AdjoinedPlot, mpl_rc_context +from .util import categorize_colors class ChartPlot(ElementPlot): @@ -571,10 +572,7 @@ def _compute_styles(self, element, ranges, style): if cs.dtype.kind in 'uif': style['c'] = cs else: - categories = np.unique(cs) - xsorted = np.argsort(categories) - ypos = np.searchsorted(categories, cs) - style['c'] = xsorted[ypos] + style['c'] = categorize_colors(cs) self._norm_kwargs(element, ranges, style, cdim) elif color is not None: if np.isscalar(color): diff --git a/holoviews/plotting/mpl/element.py b/holoviews/plotting/mpl/element.py index 0c2f973302..abd4f48abe 100644 --- a/holoviews/plotting/mpl/element.py +++ b/holoviews/plotting/mpl/element.py @@ -17,7 +17,7 @@ from ..plot import GenericElementPlot, GenericOverlayPlot from ..util import dynamic_update, process_cmap, color_intervals from .plot import MPLPlot, mpl_rc_context -from .util import wrap_formatter +from .util import wrap_formatter, is_color, categorize_colors from distutils.version import LooseVersion no_op_styles = ['marker', 'alpha', 'cmap', 'angle'] @@ -551,7 +551,19 @@ def _apply_ops(self, element, ranges, style): 'your data along the dimension.'.format( style=k, dim=v.dimension)) + if 'color' == k and (isinstance(val, np.ndarray) and all(not is_color(c) for c in val)): + new_style.pop(k) + self._norm_kwargs(element, ranges, new_style, v.dimension, val) + if val.dtype.kind in 'OSUM': + val = categorize_colors(val) + k = 'c' + + if k == 'facecolors': + # Color overrides facecolors if defined + new_style.pop('color') + new_style[k] = val + return new_style diff --git a/holoviews/plotting/mpl/util.py b/holoviews/plotting/mpl/util.py index e87b397391..9b5479d628 100644 --- a/holoviews/plotting/mpl/util.py +++ b/holoviews/plotting/mpl/util.py @@ -5,6 +5,7 @@ import numpy as np import matplotlib from matplotlib import ticker +from matplotlib.colors import cnames from matplotlib.patches import Path, PathPatch from matplotlib.transforms import Bbox, TransformedBbox, Affine2D @@ -12,6 +13,34 @@ from ...core.util import basestring, _getargspec from ...element import Raster, RGB, Polygons +from ...element import Raster, RGB +from ..util import COLOR_ALIASES, RGB_HEX_REGEX + + +def is_color(color): + """ + Checks if supplied object is a valid color spec. + """ + if not isinstance(color, basestring): + return False + elif RGB_HEX_REGEX.match(color): + return True + elif color in COLOR_ALIASES: + return True + elif color in cnames: + return True + return False + + +def categorize_colors(colors, categories=None): + """ + Takes a list of categorical values and turns them into integers + which can be colormapped. + """ + categories = np.unique(colors) if categories is None else categories + sorted_colors = np.argsort(categories) + positions = np.searchsorted(categories, colors) + return sorted_colors[positions] def wrap_formatter(formatter): diff --git a/holoviews/plotting/util.py b/holoviews/plotting/util.py index 3934128f7d..aae0f50f42 100644 --- a/holoviews/plotting/util.py +++ b/holoviews/plotting/util.py @@ -2,6 +2,7 @@ from collections import defaultdict, namedtuple +import re import traceback import warnings import bisect @@ -973,6 +974,8 @@ def hex2rgb(hex): return [int(hex[i:i+2], 16) for i in range(1,6,2)] +RGB_HEX_REGEX = re.compile(r'^#(?:[0-9a-fA-F]{3}){1,2}$') + COLOR_ALIASES = { 'b': (0, 0, 1), 'c': (0, 0.75, 0.75), From bc2e1cdba38eac55b8ef47df3ad84d07ff7dc48c Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Mon, 24 Sep 2018 20:02:50 +0100 Subject: [PATCH 015/117] Added unit tests for matplotlib PointPlot op mapping --- .../plotting/matplotlib/testpointplot.py | 78 +++++++++++++++++++ 1 file changed, 78 insertions(+) diff --git a/holoviews/tests/plotting/matplotlib/testpointplot.py b/holoviews/tests/plotting/matplotlib/testpointplot.py index 4984a7ac00..af7438de36 100644 --- a/holoviews/tests/plotting/matplotlib/testpointplot.py +++ b/holoviews/tests/plotting/matplotlib/testpointplot.py @@ -1,5 +1,6 @@ import numpy as np +from holoviews.core.overlay import NdOverlay from holoviews.element import Points from .testplot import TestMPLPlot, mpl_renderer @@ -150,3 +151,80 @@ def test_points_padding_datetime_nonsquare(self): self.assertEqual(x_range[1], 736057.09999999998) self.assertEqual(y_range[0], 0.8) self.assertEqual(y_range[1], 3.2) + + def test_point_color_op(self): + points = Points([(0, 0, '#000000'), (0, 1, '#FF0000'), (0, 2, '#00FF00')], + vdims='color').options(color='color') + plot = mpl_renderer.get_plot(points) + artist = plot.handles['artist'] + self.assertEqual(artist.get_facecolors(), + np.array([[0, 0, 0, 1], [1, 0, 0, 1], [0, 1, 0, 1]])) + + def test_point_line_color_op(self): + points = Points([(0, 0, '#000000'), (0, 1, '#FF0000'), (0, 2, '#00FF00')], + vdims='color').options(edgecolors='color') + plot = mpl_renderer.get_plot(points) + artist = plot.handles['artist'] + self.assertEqual(artist.get_edgecolors(), + np.array([[0, 0, 0, 1], [1, 0, 0, 1], [0, 1, 0, 1]])) + + def test_point_fill_color_op(self): + points = Points([(0, 0, '#000000'), (0, 1, '#FF0000'), (0, 2, '#00FF00')], + vdims='color').options(facecolors='color') + plot = mpl_renderer.get_plot(points) + artist = plot.handles['artist'] + self.assertEqual(artist.get_facecolors(), + np.array([[0, 0, 0, 1], [1, 0, 0, 1], [0, 1, 0, 1]])) + + def test_point_linear_color_op(self): + points = Points([(0, 0, 0), (0, 1, 1), (0, 2, 2)], + vdims='color').options(color='color') + plot = mpl_renderer.get_plot(points) + artist = plot.handles['artist'] + self.assertEqual(artist.get_array(), np.array([0, 1, 2])) + self.assertEqual(artist.get_clim(), (0, 2)) + + def test_point_categorical_color_op(self): + points = Points([(0, 0, 'A'), (0, 1, 'B'), (0, 2, 'A')], + vdims='color').options(color='color') + plot = mpl_renderer.get_plot(points) + artist = plot.handles['artist'] + self.assertEqual(artist.get_array(), np.array([0, 1, 0])) + self.assertEqual(artist.get_clim(), (0, 1)) + + def test_point_size_op(self): + points = Points([(0, 0, 1), (0, 1, 4), (0, 2, 8)], + vdims='size').options(s='size') + plot = mpl_renderer.get_plot(points) + artist = plot.handles['artist'] + self.assertEqual(artist.get_sizes(), np.array([1, 4, 8])) + + def test_point_line_width_op(self): + points = Points([(0, 0, 1), (0, 1, 4), (0, 2, 8)], + vdims='line_width').options(linewidth='line_width') + plot = mpl_renderer.get_plot(points) + artist = plot.handles['artist'] + self.assertEqual(artist.get_linewidths(), [1, 4, 8]) + + def test_point_marker_op(self): + points = Points([(0, 0, 'circle'), (0, 1, 'triangle'), (0, 2, 'square')], + vdims='marker').options(marker='marker') + with self.assertRaises(ValueError): + mpl_renderer.get_plot(points) + + def test_point_alpha_op(self): + points = Points([(0, 0, 0), (0, 1, 0.2), (0, 2, 0.7)], + vdims='alpha').options(alpha='alpha') + with self.assertRaises(ValueError): + mpl_renderer.get_plot(points) + + def test_op_ndoverlay_value(self): + markers = ['d', 's'] + overlay = NdOverlay({marker: Points(np.arange(i)) + for i, marker in enumerate(markers)}, + 'Marker').options('Points', marker='Marker') + plot = mpl_renderer.get_plot(overlay) + for subplot, marker in zip(plot.subplots.values(), markers): + style = dict(subplot.style[subplot.cyclic_index]) + style = subplot._apply_ops(subplot.current_frame, {}, style) + self.assertEqual(style['marker'], marker) From b8fdc0bd96a1b8510a0aab105e040ef34a28e740 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Mon, 29 Oct 2018 00:02:53 +0000 Subject: [PATCH 016/117] Created new module for bokeh styles and utilities --- holoviews/plotting/bokeh/annotation.py | 4 +- holoviews/plotting/bokeh/chart.py | 7 +- holoviews/plotting/bokeh/element.py | 31 +-- holoviews/plotting/bokeh/graphs.py | 7 +- holoviews/plotting/bokeh/heatmap.py | 6 +- holoviews/plotting/bokeh/hex_tiles.py | 3 +- holoviews/plotting/bokeh/path.py | 5 +- holoviews/plotting/bokeh/raster.py | 5 +- holoviews/plotting/bokeh/stats.py | 6 +- holoviews/plotting/bokeh/styles.py | 182 ++++++++++++++++++ holoviews/plotting/bokeh/util.py | 127 +----------- .../tests/plotting/bokeh/testpointplot.py | 15 +- 12 files changed, 224 insertions(+), 174 deletions(-) create mode 100644 holoviews/plotting/bokeh/styles.py diff --git a/holoviews/plotting/bokeh/annotation.py b/holoviews/plotting/bokeh/annotation.py index 4f4f31436f..7972a61abf 100644 --- a/holoviews/plotting/bokeh/annotation.py +++ b/holoviews/plotting/bokeh/annotation.py @@ -17,8 +17,8 @@ from ...core.util import dimension_sanitizer, basestring from ...element import HLine from ..plot import GenericElementPlot -from .element import (AnnotationPlot, CompositeElementPlot, ColorbarPlot, - ElementPlot, text_properties, line_properties) +from .element import AnnotationPlot, ElementPlot, CompositeElementPlot, ColorbarPlot +from .styles import text_properties, line_properties from .plot import BokehPlot diff --git a/holoviews/plotting/bokeh/chart.py b/holoviews/plotting/bokeh/chart.py index 40568ec052..7d4f6c6d36 100644 --- a/holoviews/plotting/bokeh/chart.py +++ b/holoviews/plotting/bokeh/chart.py @@ -11,9 +11,10 @@ from ...element import Bars from ...operation import interpolate_curve from ..util import compute_sizes, get_min_distance, dim_axis_label, get_axis_padding -from .element import (ElementPlot, ColorbarPlot, LegendPlot, line_properties, - fill_properties) -from .util import expand_batched_style, categorize_array, rgb2hex, mpl_to_bokeh +from .element import ElementPlot, ColorbarPlot, LegendPlot +from .styles import (expand_batched_style, line_properties, fill_properties, + mpl_to_bokeh, rgb2hex) +from .util import categorize_array class PointPlot(LegendPlot, ColorbarPlot): diff --git a/holoviews/plotting/bokeh/element.py b/holoviews/plotting/bokeh/element.py index c7c2102274..3a6075cf6a 100644 --- a/holoviews/plotting/bokeh/element.py +++ b/holoviews/plotting/bokeh/element.py @@ -31,31 +31,16 @@ from ..plot import GenericElementPlot, GenericOverlayPlot from ..util import dynamic_update, process_cmap, color_intervals from .plot import BokehPlot, TOOLS +from .styles import ( + legend_dimensions, line_properties, mpl_to_bokeh, no_op_styles, + rgba_tuple, text_properties, validate +) from .util import ( - mpl_to_bokeh, get_tab_title, py2js_tickformatter, rgba_tuple, - recursive_model_update, glyph_order, decode_bytes, bokeh_version, - theme_attr_json, is_color, cds_column_replace, hold_policy + bokeh_version, decode_bytes, get_tab_title, glyph_order, + py2js_tickformatter, recursive_model_update, theme_attr_json, + cds_column_replace, hold_policy ) -property_prefixes = ['selection', 'nonselection', 'muted', 'hover'] - -# Define shared style properties for bokeh plots -line_properties = ['line_color', 'line_alpha', 'color', 'alpha', 'line_width', - 'line_join', 'line_cap', 'line_dash'] -line_properties += ['_'.join([prefix, prop]) for prop in line_properties[:4] - for prefix in property_prefixes] - -fill_properties = ['fill_color', 'fill_alpha'] -fill_properties += ['_'.join([prefix, prop]) for prop in fill_properties - for prefix in property_prefixes] - -text_properties = ['text_font', 'text_font_size', 'text_font_style', 'text_color', - 'text_alpha', 'text_align', 'text_baseline'] - -legend_dimensions = ['label_standoff', 'label_width', 'label_height', 'glyph_width', - 'glyph_height', 'legend_padding', 'legend_spacing', 'click_policy'] - -no_op_styles = ['cmap', 'palette', 'marker'] class ElementPlot(BokehPlot, GenericElementPlot): @@ -714,7 +699,7 @@ def _apply_ops(self, element, source, ranges, style, group=None): numeric = isinstance(val, np.ndarray) and val.dtype.kind in 'uifMm' if ('color' in k and isinstance(val, np.ndarray) and - (numeric or not all(is_color(v) for v in val))): + (numeric or not validate('color', val))): kwargs = {} if val.dtype.kind not in 'if': kwargs['factors'] = np.unique(val) diff --git a/holoviews/plotting/bokeh/graphs.py b/holoviews/plotting/bokeh/graphs.py index 442a17caa0..7545d18cff 100644 --- a/holoviews/plotting/bokeh/graphs.py +++ b/holoviews/plotting/bokeh/graphs.py @@ -9,13 +9,14 @@ from ...core.util import (basestring, dimension_sanitizer, unique_array, max_range) from ...core.options import Cycle -from .chart import ColorbarPlot, PointPlot -from .element import (CompositeElementPlot, LegendPlot, line_properties, - fill_properties, text_properties) from ..util import process_cmap +from .chart import ColorbarPlot, PointPlot +from .element import CompositeElementPlot, LegendPlot +from .styles import line_properties, fill_properties, text_properties from .util import rgba_tuple + class GraphPlot(CompositeElementPlot, ColorbarPlot, LegendPlot): color_index = param.ClassSelector(default=None, class_=(basestring, int), diff --git a/holoviews/plotting/bokeh/heatmap.py b/holoviews/plotting/bokeh/heatmap.py index eb33542db1..762b550466 100644 --- a/holoviews/plotting/bokeh/heatmap.py +++ b/holoviews/plotting/bokeh/heatmap.py @@ -6,10 +6,8 @@ from ...core.util import is_nan, dimension_sanitizer from ...core.spaces import HoloMap -from .element import (ColorbarPlot, CompositeElementPlot, - line_properties, fill_properties, text_properties) -from .util import mpl_to_bokeh - +from .element import ColorbarPlot, CompositeElementPlot +from .styles import line_properties, fill_properties, mpl_to_bokeh, text_properties class HeatMapPlot(ColorbarPlot): diff --git a/holoviews/plotting/bokeh/hex_tiles.py b/holoviews/plotting/bokeh/hex_tiles.py index 3969a56ea8..7b90ce9223 100644 --- a/holoviews/plotting/bokeh/hex_tiles.py +++ b/holoviews/plotting/bokeh/hex_tiles.py @@ -12,7 +12,8 @@ from ...core.options import Compositor, SkipRendering from ...core.util import basestring, isfinite from ...element import HexTiles -from .element import ColorbarPlot, line_properties, fill_properties +from .element import ColorbarPlot +from .styles import line_properties, fill_properties from .util import bokeh_version diff --git a/holoviews/plotting/bokeh/path.py b/holoviews/plotting/bokeh/path.py index 335b28171f..c956a6dc5f 100644 --- a/holoviews/plotting/bokeh/path.py +++ b/holoviews/plotting/bokeh/path.py @@ -5,8 +5,9 @@ from ...core import util from ...element import Polygons -from .element import ColorbarPlot, LegendPlot, line_properties, fill_properties -from .util import expand_batched_style, mpl_to_bokeh, bokeh_version, multi_polygons_data +from .element import ColorbarPlot, LegendPlot +from .styles import expand_batched_style, line_properties, fill_properties, mpl_to_bokeh +from .util import bokeh_version, multi_polygons_data class PathPlot(ColorbarPlot): diff --git a/holoviews/plotting/bokeh/raster.py b/holoviews/plotting/bokeh/raster.py index f92936313d..d560c9185f 100644 --- a/holoviews/plotting/bokeh/raster.py +++ b/holoviews/plotting/bokeh/raster.py @@ -3,8 +3,9 @@ from ...core.util import cartesian_product, dimension_sanitizer, isfinite from ...element import Raster, RGB, HSV -from .element import ElementPlot, ColorbarPlot, line_properties, fill_properties -from .util import mpl_to_bokeh, colormesh, bokeh_version +from .element import ElementPlot, ColorbarPlot +from .styles import line_properties, fill_properties, mpl_to_bokeh +from .util import bokeh_version, colormesh class RasterPlot(ColorbarPlot): diff --git a/holoviews/plotting/bokeh/stats.py b/holoviews/plotting/bokeh/stats.py index 033f36e5e8..2733fb7543 100644 --- a/holoviews/plotting/bokeh/stats.py +++ b/holoviews/plotting/bokeh/stats.py @@ -12,10 +12,10 @@ unique_iterator, isfinite) from ...operation.stats import univariate_kde from .chart import AreaPlot -from .element import (CompositeElementPlot, ColorbarPlot, LegendPlot, - fill_properties, line_properties) +from .element import CompositeElementPlot, ColorbarPlot, LegendPlot from .path import PolygonPlot -from .util import rgb2hex, decode_bytes +from .styles import fill_properties, line_properties, rgb2hex +from .util import decode_bytes class DistributionPlot(AreaPlot): diff --git a/holoviews/plotting/bokeh/styles.py b/holoviews/plotting/bokeh/styles.py new file mode 100644 index 0000000000..5c3fc93cb6 --- /dev/null +++ b/holoviews/plotting/bokeh/styles.py @@ -0,0 +1,182 @@ +""" +Defines valid style options, validation and utilities +""" + +import numpy as np + +from bokeh.core.properties import ( + Angle, Color, DashPattern, FontSize, MarkerType, Size +) + +try: + from matplotlib import colors + import matplotlib.cm as cm +except ImportError: + cm, colors = None, None + +from ...core.options import abbreviated_exception +from ...core.util import basestring +from ..util import COLOR_ALIASES, RGB_HEX_REGEX, rgb2hex + +# Define shared style properties for bokeh plots + +property_prefixes = ['selection', 'nonselection', 'muted', 'hover'] + +line_properties = ['line_color', 'line_alpha', 'color', 'alpha', 'line_width', + 'line_join', 'line_cap', 'line_dash'] +line_properties += ['_'.join([prefix, prop]) for prop in line_properties[:4] + for prefix in property_prefixes] + +fill_properties = ['fill_color', 'fill_alpha'] +fill_properties += ['_'.join([prefix, prop]) for prop in fill_properties + for prefix in property_prefixes] + +text_properties = ['text_font', 'text_font_size', 'text_font_style', 'text_color', + 'text_alpha', 'text_align', 'text_baseline'] + +legend_dimensions = ['label_standoff', 'label_width', 'label_height', 'glyph_width', + 'glyph_height', 'legend_padding', 'legend_spacing', 'click_policy'] + +no_op_styles = ['cmap', 'palette'] + +# Conversion between matplotlib and bokeh markers + +markers = { + 's': {'marker': 'square'}, + 'd': {'marker': 'diamond'}, + '^': {'marker': 'triangle', 'angle': 0}, + '>': {'marker': 'triangle', 'angle': -np.pi/2}, + 'v': {'marker': 'triangle', 'angle': np.pi}, + '<': {'marker': 'triangle', 'angle': np.pi/2}, + '1': {'marker': 'triangle', 'angle': 0}, + '2': {'marker': 'triangle', 'angle': -np.pi/2}, + '3': {'marker': 'triangle', 'angle': np.pi}, + '4': {'marker': 'triangle', 'angle': np.pi/2} +} + + +def mpl_to_bokeh(properties): + """ + Utility to process style properties converting any + matplotlib specific options to their nearest bokeh + equivalent. + """ + new_properties = {} + for k, v in properties.items(): + if k == 's': + new_properties['size'] = v + elif k == 'marker': + new_properties.update(markers.get(v, {'marker': v})) + elif (k == 'color' or k.endswith('_color')) and not isinstance(v, dict): + with abbreviated_exception(): + v = COLOR_ALIASES.get(v, v) + if isinstance(v, tuple): + with abbreviated_exception(): + v = rgb2hex(v) + new_properties[k] = v + else: + new_properties[k] = v + new_properties.pop('cmap', None) + return new_properties + +# Validation + +angle = Angle() +color = Color() +dash_pattern = DashPattern() +font_size = FontSize() +marker = MarkerType() +size = Size() + +validators = { + 'angle' : angle.is_valid, + 'color' : lambda x: ( + color.is_valid(x) or (isinstance(x, basestring) and RGB_HEX_REGEX.match(x)) + ), + 'font_size' : font_size.is_valid, + 'line_dash' : dash_pattern.is_valid, + 'marker' : marker.is_valid, + 'size' : size.is_valid, +} + +def get_validator(style): + for k, v in validators.items(): + if style.endswith(k): + return v + + +def validate(style, value): + """ + Validates a style and associated value. + + Arguments + --------- + style: str + The style to validate (e.g. 'color', 'size' or 'marker') + value: + The style value to validate + + Returns + ------- + valid: boolean or None + If validation is supported returns boolean, otherwise None + """ + validator = get_validator(style) + if validator is None: + return None + if isinstance(value, (np.ndarray, list)): + return all(validator(v) for v in value) + return validator(value) + +# Utilities + +def rgba_tuple(rgba): + """ + Ensures RGB(A) tuples in the range 0-1 are scaled to 0-255. + """ + if isinstance(rgba, tuple): + return tuple(int(c*255) if i<3 else c for i, c in enumerate(rgba)) + else: + return COLOR_ALIASES.get(rgba, rgba) + + +def expand_batched_style(style, opts, mapping, nvals): + """ + Computes styles applied to a batched plot by iterating over the + supplied list of style options and expanding any options found in + the supplied style dictionary returning a data and mapping defining + the data that should be added to the ColumnDataSource. + """ + opts = sorted(opts, key=lambda x: x in ['color', 'alpha']) + applied_styles = set(mapping) + style_data, style_mapping = {}, {} + for opt in opts: + if 'color' in opt: + alias = 'color' + elif 'alpha' in opt: + alias = 'alpha' + else: + alias = None + if opt not in style or opt in mapping: + continue + elif opt == alias: + if alias in applied_styles: + continue + elif 'line_'+alias in applied_styles: + if 'fill_'+alias not in opts: + continue + opt = 'fill_'+alias + val = style[alias] + elif 'fill_'+alias in applied_styles: + opt = 'line_'+alias + val = style[alias] + else: + val = style[alias] + else: + val = style[opt] + style_mapping[opt] = {'field': opt} + applied_styles.add(opt) + if 'color' in opt and isinstance(val, tuple): + val = rgb2hex(val) + style_data[opt] = [val]*nvals + return style_data, style_mapping diff --git a/holoviews/plotting/bokeh/util.py b/holoviews/plotting/bokeh/util.py index 0775647b05..299f0f83ee 100644 --- a/holoviews/plotting/bokeh/util.py +++ b/holoviews/plotting/bokeh/util.py @@ -6,19 +6,11 @@ import numpy as np -try: - from matplotlib import colors - import matplotlib.cm as cm -except ImportError: - cm, colors = None, None - import param import bokeh bokeh_version = LooseVersion(bokeh.__version__) # noqa -from bokeh.colors.named import __all__ as named_colors -from bokeh.core.enums import Palette from bokeh.core.json_encoder import serialize_json # noqa (API import) from bokeh.core.properties import value from bokeh.layouts import WidgetBox, Row, Column @@ -37,26 +29,12 @@ except: Chart = type(None) # Create stub for isinstance check -from ...core.options import abbreviated_exception from ...core.overlay import Overlay from ...core.util import (basestring, unique_array, callable_name, pd, dt64_to_dt, _getargspec) from ...core.spaces import get_nested_dmaps, DynamicMap +from ..util import dim_axis_label -from ..util import dim_axis_label, rgb2hex, COLOR_ALIASES, RGB_HEX_REGEX - -# Conversion between matplotlib and bokeh markers -markers = {'s': {'marker': 'square'}, - 'd': {'marker': 'diamond'}, - '+': {'marker': 'cross'}, - '^': {'marker': 'triangle', 'angle': 0}, - '>': {'marker': 'triangle', 'angle': -np.pi/2}, - 'v': {'marker': 'triangle', 'angle': np.pi}, - '<': {'marker': 'triangle', 'angle': np.pi/2}, - '1': {'marker': 'triangle', 'angle': 0}, - '2': {'marker': 'triangle', 'angle': -np.pi/2}, - '3': {'marker': 'triangle', 'angle': np.pi}, - '4': {'marker': 'triangle', 'angle': np.pi/2}} def convert_timestamp(timestamp): @@ -67,31 +45,6 @@ def convert_timestamp(timestamp): return np.datetime64(datetime.replace(tzinfo=None)) -def rgba_tuple(rgba): - """ - Ensures RGB(A) tuples in the range 0-1 are scaled to 0-255. - """ - if isinstance(rgba, tuple): - return tuple(int(c*255) if i<3 else c for i, c in enumerate(rgba)) - else: - return COLOR_ALIASES.get(rgba, rgba) - - -def is_color(color): - """ - Checks if the supplied value is a valid color spec. - """ - if not isinstance(color, basestring): - return False - elif RGB_HEX_REGEX.match(color): - return True - elif color in COLOR_ALIASES: - return True - elif color in named_colors: - return True - return False - - def decode_bytes(array): """ Decodes an array, list or tuple of bytestrings to avoid python 3 @@ -108,43 +61,6 @@ def decode_bytes(array): return decoded -def get_cmap(cmap): - """ - Returns matplotlib cmap generated from bokeh palette or - directly accessed from matplotlib. - """ - with abbreviated_exception(): - rgb_vals = getattr(Palette, cmap, None) - if rgb_vals: - return colors.ListedColormap(rgb_vals, name=cmap) - return cm.get_cmap(cmap) - - -def mpl_to_bokeh(properties): - """ - Utility to process style properties converting any - matplotlib specific options to their nearest bokeh - equivalent. - """ - new_properties = {} - for k, v in properties.items(): - if k == 's': - new_properties['size'] = v - elif k == 'marker': - new_properties.update(markers.get(v, {'marker': v})) - elif (k == 'color' or k.endswith('_color')) and not isinstance(v, dict): - with abbreviated_exception(): - v = COLOR_ALIASES.get(v, v) - if isinstance(v, tuple): - with abbreviated_exception(): - v = rgb2hex(v) - new_properties[k] = v - else: - new_properties[k] = v - new_properties.pop('cmap', None) - return new_properties - - def layout_padding(plots, renderer): """ Pads Nones in a list of lists of plots with empty plots. @@ -434,47 +350,6 @@ def get_tab_title(key, frame, overlay): return title -def expand_batched_style(style, opts, mapping, nvals): - """ - Computes styles applied to a batched plot by iterating over the - supplied list of style options and expanding any options found in - the supplied style dictionary returning a data and mapping defining - the data that should be added to the ColumnDataSource. - """ - opts = sorted(opts, key=lambda x: x in ['color', 'alpha']) - applied_styles = set(mapping) - style_data, style_mapping = {}, {} - for opt in opts: - if 'color' in opt: - alias = 'color' - elif 'alpha' in opt: - alias = 'alpha' - else: - alias = None - if opt not in style or opt in mapping: - continue - elif opt == alias: - if alias in applied_styles: - continue - elif 'line_'+alias in applied_styles: - if 'fill_'+alias not in opts: - continue - opt = 'fill_'+alias - val = style[alias] - elif 'fill_'+alias in applied_styles: - opt = 'line_'+alias - val = style[alias] - else: - val = style[alias] - else: - val = style[opt] - style_mapping[opt] = {'field': opt} - applied_styles.add(opt) - if 'color' in opt and isinstance(val, tuple): - val = rgb2hex(val) - style_data[opt] = [val]*nvals - return style_data, style_mapping - def filter_batched_data(data, mapping): """ diff --git a/holoviews/tests/plotting/bokeh/testpointplot.py b/holoviews/tests/plotting/bokeh/testpointplot.py index 225671ddff..8820ab0b97 100644 --- a/holoviews/tests/plotting/bokeh/testpointplot.py +++ b/holoviews/tests/plotting/bokeh/testpointplot.py @@ -13,7 +13,7 @@ try: from bokeh.models import FactorRange, LinearColorMapper, CategoricalColorMapper - from bokeh.models.glyphs import Circle, Triangle + from bokeh.models import Scatter except: pass @@ -438,11 +438,16 @@ def test_point_line_width_op(self): def test_point_marker_op(self): points = Points([(0, 0, 'circle'), (0, 1, 'triangle'), (0, 2, 'square')], vdims='marker').options(marker='marker') - with self.assertRaises(ValueError): - bokeh_renderer.get_plot(points) + plot = bokeh_renderer.get_plot(points) + cds = plot.handles['cds'] + glyph = plot.handles['glyph'] + self.assertEqual(cds.data['marker'], np.array(['circle', 'triangle', 'square'])) + self.assertEqual(glyph.marker, 'marker') def test_op_ndoverlay_value(self): - overlay = NdOverlay({marker: Points(np.arange(i)) for i, marker in enumerate(['circle', 'triangle'])}, 'Marker').options('Points', marker='Marker') + markers = ['circle', 'triangle'] + overlay = NdOverlay({marker: Points(np.arange(i)) for i, marker in enumerate(markers)}, 'Marker').options('Points', marker='Marker') plot = bokeh_renderer.get_plot(overlay) - for subplot, glyph_type in zip(plot.subplots.values(), [Circle, Triangle]): + for subplot, glyph_type, marker in zip(plot.subplots.values(), [Scatter, Scatter], markers): self.assertIsInstance(subplot.handles['glyph'], glyph_type) + self.assertEqual(subplot.handles['glyph'].marker, marker) From b5c01911c7e0834d59b8ff8e21162285877f7220 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Fri, 9 Nov 2018 04:18:57 +0000 Subject: [PATCH 017/117] Improved validation of op mappings --- holoviews/plotting/bokeh/__init__.py | 2 +- holoviews/plotting/bokeh/chart.py | 11 ++++++++++ holoviews/plotting/bokeh/element.py | 33 ++++++++++++++++++---------- holoviews/plotting/bokeh/graphs.py | 5 +++-- holoviews/plotting/bokeh/stats.py | 4 ++++ holoviews/plotting/bokeh/styles.py | 2 -- holoviews/plotting/mpl/annotation.py | 2 ++ holoviews/plotting/mpl/element.py | 30 +++++++++++++++---------- holoviews/plotting/mpl/graphs.py | 4 ++++ holoviews/plotting/mpl/stats.py | 6 +++++ holoviews/util/ops.py | 3 +++ 11 files changed, 74 insertions(+), 28 deletions(-) diff --git a/holoviews/plotting/bokeh/__init__.py b/holoviews/plotting/bokeh/__init__.py index 2c11930a6b..13fd9aa3ad 100644 --- a/holoviews/plotting/bokeh/__init__.py +++ b/holoviews/plotting/bokeh/__init__.py @@ -170,7 +170,7 @@ def colormap_generator(palette): options.Histogram = Options('style', line_color='black', fill_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', width=0.8, 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.Area = Options('style', color=Cycle(), alpha=1, line_color='black', muted_alpha=0.2) diff --git a/holoviews/plotting/bokeh/chart.py b/holoviews/plotting/bokeh/chart.py index 7d4f6c6d36..08899336a3 100644 --- a/holoviews/plotting/bokeh/chart.py +++ b/holoviews/plotting/bokeh/chart.py @@ -166,6 +166,9 @@ class VectorFieldPlot(ColorbarPlot): smallest non-zero distance between two vectors.""") style_opts = line_properties + ['scale', 'cmap'] + + _no_op_styles = ['scale', 'cmap'] + _plot_methods = dict(single='segment') def _get_lengths(self, element, ranges): @@ -263,6 +266,8 @@ class CurvePlot(ElementPlot): 'steps-pre' and 'steps-post'.""") style_opts = line_properties + _no_op_styles = line_properties + _plot_methods = dict(single='line', batched='multi_line') _batched_style_opts = line_properties @@ -486,6 +491,8 @@ def _init_glyph(self, plot, mapping, properties): class SpreadPlot(ElementPlot): style_opts = line_properties + fill_properties + _no_op_style = style_opts + _plot_methods = dict(single='patch') _stream_data = False # Plot does not support streaming data @@ -697,6 +704,8 @@ class BarPlot(ColorbarPlot, LegendPlot): style_opts = line_properties + fill_properties + ['width', 'bar_width', 'cmap'] + _no_op_styles = ['bar_width', 'cmap', 'width'] + _plot_methods = dict(single=('vbar', 'hbar')) # Declare that y-range should auto-range if not bounded @@ -862,6 +871,8 @@ def get_data(self, element, ranges, style): # Define style information width = style.get('bar_width', style.get('width', 1)) + if 'width' in style: + self.warning("BarPlot width option is deprecated use 'bar_width' instead.") cmap = style.get('cmap') hover = 'hover' in self.handles diff --git a/holoviews/plotting/bokeh/element.py b/holoviews/plotting/bokeh/element.py index 3a6075cf6a..2d1c723c33 100644 --- a/holoviews/plotting/bokeh/element.py +++ b/holoviews/plotting/bokeh/element.py @@ -32,8 +32,8 @@ from ..util import dynamic_update, process_cmap, color_intervals from .plot import BokehPlot, TOOLS from .styles import ( - legend_dimensions, line_properties, mpl_to_bokeh, no_op_styles, - rgba_tuple, text_properties, validate + legend_dimensions, line_properties, mpl_to_bokeh, rgba_tuple, + text_properties, validate ) from .util import ( bokeh_version, decode_bytes, get_tab_title, glyph_order, @@ -116,6 +116,9 @@ class ElementPlot(BokehPlot, GenericElementPlot): _categorical = False + # Declare which styles cannot be mapped to a non-scalar dimension + _no_op_styles = [] + # Declares the default types for continuous x- and y-axes _x_range_type = Range1d _y_range_type = Range1d @@ -664,7 +667,8 @@ def _apply_ops(self, element, source, ranges, style, group=None): if not isinstance(v, op) or (group is not None and not k.startswith(group)): continue dname = v.dimension.name - if dname not in element and v.dimension not in self.overlay_dims: + if (dname not in element and v.dimension not in self.overlay_dims and + not (isinstance(element, Graph) and v.dimension in element.nodes)): new_style.pop(k) self.warning('Specified %s op %r could not be applied, %s dimension ' 'could not be found' % (k, v, v.dimension)) @@ -679,13 +683,17 @@ def _apply_ops(self, element, source, ranges, style, group=None): val = val if np.isscalar(val) else val[0] if not np.isscalar(val): - if k in no_op_styles: - raise ValueError('Mapping the a dimension to the "{style}" ' - 'style option is not supported. To ' - 'map the {dim} dimension to the {style} ' - 'use a groupby operation to overlay ' - 'your data along the dimension.'.format( - style=k, dim=v.dimension)) + if k in self._no_op_styles: + raise ValueError('Mapping a dimension to the "{style}" ' + 'style option is not supported by the ' + '{backend} backend. To map the {dim} ' + 'dimension to the {style} use a ' + 'groupby operation to overlay your ' + 'data along the dimension.'.format( + style=k, dim=v.dimension, + backend=self.renderer.backend + ) + ) elif source.data and len(val) != len(list(source.data.values())[0]): continue @@ -711,7 +719,8 @@ def _apply_ops(self, element, source, ranges, style, group=None): def _glyph_properties(self, plot, element, source, ranges, style, group=None): - new_style = self._apply_ops(element, source, ranges, style, group) + with abbreviated_exception(): + new_style = self._apply_ops(element, source, ranges, style, group) properties = dict(new_style, source=source) if self.show_legend: if self.overlay_dims: @@ -1178,6 +1187,8 @@ class ColorbarPlot(ElementPlot): _default_nan = '#8b8b8b' + _no_op_styles = ['cmap', 'palette'] + def _draw_colorbar(self, plot, color_mapper): if CategoricalColorMapper and isinstance(color_mapper, CategoricalColorMapper): return diff --git a/holoviews/plotting/bokeh/graphs.py b/holoviews/plotting/bokeh/graphs.py index 7545d18cff..d6093b0a51 100644 --- a/holoviews/plotting/bokeh/graphs.py +++ b/holoviews/plotting/bokeh/graphs.py @@ -12,8 +12,7 @@ from ..util import process_cmap from .chart import ColorbarPlot, PointPlot from .element import CompositeElementPlot, LegendPlot -from .styles import line_properties, fill_properties, text_properties -from .util import rgba_tuple +from .styles import line_properties, fill_properties, text_properties, rgba_tuple @@ -45,6 +44,8 @@ class GraphPlot(CompositeElementPlot, ColorbarPlot, LegendPlot): ['node_'+p for p in fill_properties+line_properties] + ['node_size', 'cmap', 'edge_cmap']) + _no_op_styles = ['cmap', 'edge_cmap'] + # Filled is only supported for subclasses filled = False diff --git a/holoviews/plotting/bokeh/stats.py b/holoviews/plotting/bokeh/stats.py index 2733fb7543..c92972ab76 100644 --- a/holoviews/plotting/bokeh/stats.py +++ b/holoviews/plotting/bokeh/stats.py @@ -77,6 +77,8 @@ class BoxWhiskerPlot(CompositeElementPlot, ColorbarPlot, LegendPlot): ['outlier_'+p for p in fill_properties+line_properties] + ['width', 'box_width', 'cmap']) + _no_op_styles = ['box_width', 'width', 'cmap'] + _stream_data = False # Plot does not support streaming data def get_extents(self, element, ranges, range_type='combined'): @@ -134,6 +136,8 @@ def get_data(self, element, ranges, style): # Define glyph-data mapping width = style.get('box_width', style.get('width', 0.7)) + if 'width' in style: + self.warning("BoxWhisker width option is deprecated use 'box_width' instead.") if self.invert_axes: vbar_map = {'y': 'index', 'left': 'top', 'right': 'bottom', 'height': width} seg_map = {'y0': 'x0', 'y1': 'x1', 'x0': 'y0', 'x1': 'y1'} diff --git a/holoviews/plotting/bokeh/styles.py b/holoviews/plotting/bokeh/styles.py index 5c3fc93cb6..12b7627c81 100644 --- a/holoviews/plotting/bokeh/styles.py +++ b/holoviews/plotting/bokeh/styles.py @@ -37,8 +37,6 @@ legend_dimensions = ['label_standoff', 'label_width', 'label_height', 'glyph_width', 'glyph_height', 'legend_padding', 'legend_spacing', 'click_policy'] -no_op_styles = ['cmap', 'palette'] - # Conversion between matplotlib and bokeh markers markers = { diff --git a/holoviews/plotting/mpl/annotation.py b/holoviews/plotting/mpl/annotation.py index e3b9a7d041..328a26516a 100644 --- a/holoviews/plotting/mpl/annotation.py +++ b/holoviews/plotting/mpl/annotation.py @@ -101,6 +101,8 @@ class LabelsPlot(ColorbarPlot): style_opts = ['alpha', 'color', 'family', 'weight', 'size', 'visible', 'horizontalalignment', 'verticalalignment', 'cmap', 'rotation'] + _no_op_styles = [] + _plot_methods = dict(single='annotate') def get_data(self, element, ranges, style): diff --git a/holoviews/plotting/mpl/element.py b/holoviews/plotting/mpl/element.py index abd4f48abe..55035f41ac 100644 --- a/holoviews/plotting/mpl/element.py +++ b/holoviews/plotting/mpl/element.py @@ -20,8 +20,6 @@ from .util import wrap_formatter, is_color, categorize_colors from distutils.version import LooseVersion -no_op_styles = ['marker', 'alpha', 'cmap', 'angle'] - class ElementPlot(GenericElementPlot, MPLPlot): @@ -76,6 +74,9 @@ class ElementPlot(GenericElementPlot, MPLPlot): # Element Plots should declare the valid style options for matplotlib call style_opts = [] + # Declare which styles cannot be mapped to a non-scalar dimension + _no_op_styles = ['marker', 'alpha', 'cmap', 'angle'] + # Whether plot has axes, disables setting axis limits, labels and ticks _has_axes = True @@ -479,7 +480,8 @@ def initialize_plot(self, ranges=None): if self.show_legend: style['label'] = element.label - style = self._apply_ops(element, ranges, style) + with abbreviated_exception(): + style = self._apply_ops(element, ranges, style) plot_data, plot_kwargs, axis_kwargs = self.get_data(element, ranges, style) with abbreviated_exception(): @@ -507,7 +509,8 @@ def update_handles(self, key, axis, element, ranges, style): Update the elements of the plot. """ self.teardown_handles() - new_style = self._apply_ops(element, range, style) + with abbreviated_exception(): + new_style = self._apply_ops(element, range, style) plot_data, plot_kwargs, axis_kwargs = self.get_data(element, ranges, new_style) with abbreviated_exception(): @@ -543,14 +546,17 @@ def _apply_ops(self, element, ranges, style): if len(np.unique(val)) == 1: val = val if np.isscalar(val) else val[0] - if not np.isscalar(val) and k in no_op_styles: - raise ValueError('Mapping the a dimension to the "{style}" ' - 'style option is not supported. To ' - 'map the {dim} dimension to the {style} ' - 'use a groupby operation to overlay ' - 'your data along the dimension.'.format( - style=k, dim=v.dimension)) - + if not np.isscalar(val) and k in self._no_op_styles: + raise ValueError('Mapping a dimension to the "{style}" ' + 'style option is not supported by the ' + '{backend} backend. To map the "{dim}" ' + 'dimension to the {style} use a ' + 'groupby operation to overlay your ' + 'data along the dimension.'.format( + style=k, dim=v.dimension, + backend=self.renderer.backend + ) + ) if 'color' == k and (isinstance(val, np.ndarray) and all(not is_color(c) for c in val)): new_style.pop(k) self._norm_kwargs(element, ranges, new_style, v.dimension, val) diff --git a/holoviews/plotting/mpl/graphs.py b/holoviews/plotting/mpl/graphs.py index 92e7242a3d..3af764db21 100644 --- a/holoviews/plotting/mpl/graphs.py +++ b/holoviews/plotting/mpl/graphs.py @@ -27,6 +27,9 @@ class GraphPlot(ColorbarPlot): _style_groups = ['node', 'edge'] + _no_op_styles = ['edge_alpha', 'edge_linestyle', 'edge_cmap', 'cmap', + 'visible', 'node_marker'] + filled = False def _compute_styles(self, element, ranges, style): @@ -124,6 +127,7 @@ def init_artists(self, ax, plot_args, plot_kwargs): edge_opts['facecolors'] = edge_opts.pop('colors') else: coll = LineCollection + print(edge_opts) edges = coll(paths, **edge_opts) ax.add_collection(edges) diff --git a/holoviews/plotting/mpl/stats.py b/holoviews/plotting/mpl/stats.py index 25ba56c1c1..449621bc2f 100644 --- a/holoviews/plotting/mpl/stats.py +++ b/holoviews/plotting/mpl/stats.py @@ -56,6 +56,9 @@ class BoxPlot(ChartPlot): 'whiskerprops', 'capprops', 'flierprops', 'medianprops', 'meanprops', 'meanline'] + _no_op_styles = [s for s in style_opts + if s not in ('conf_intervals', 'widths')] + _plot_methods = dict(single='boxplot') def get_extents(self, element, ranges, range_type='combined'): @@ -150,6 +153,9 @@ class ViolinPlot(BoxPlot): style_opts = ['showmeans', 'facecolors', 'showextrema', 'bw_method', 'widths', 'stats_color', 'box_color', 'alpha', 'edgecolors'] + _no_op_styles = [s for s in style_opts + if s not in ('facecolors', 'edgecolors', 'widths')] + def init_artists(self, ax, plot_args, plot_kwargs): box_color = plot_kwargs.pop('box_color', 'black') stats_color = plot_kwargs.pop('stats_color', 'black') diff --git a/holoviews/util/ops.py b/holoviews/util/ops.py index 3ee4e81609..7e442b14b6 100644 --- a/holoviews/util/ops.py +++ b/holoviews/util/ops.py @@ -3,6 +3,7 @@ from ..core.dimension import Dimension from ..core.util import basestring +from ..element import Graph def norm_fn(values, min=None, max=None): @@ -72,6 +73,8 @@ def var(self, **kwargs): return op(self, np.var, **kwargs) def eval(self, dataset, ranges={}): expanded = not ((dataset.interface.gridded and self.dimension in dataset.kdims) or (dataset.interface.multi and dataset.interface.isscalar(dataset, self.dimension))) + if isinstance(dataset, Graph): + dataset = dataset if self.dimension in dataset else dataset.nodes data = dataset.dimension_values(self.dimension, expanded=expanded, flat=False) for o in self.ops: other = o['other'] From 8f6defa745ce4e30e744e7afab4e17b7f53013c6 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Fri, 9 Nov 2018 16:17:12 +0000 Subject: [PATCH 018/117] Improved ops validation and added tests --- holoviews/element/chart.py | 4 +- holoviews/plotting/bokeh/annotation.py | 2 + holoviews/plotting/bokeh/chart.py | 10 +- holoviews/plotting/bokeh/element.py | 28 +++-- holoviews/plotting/mpl/chart.py | 33 +++++- holoviews/plotting/mpl/element.py | 28 +++-- .../tests/plotting/bokeh/testcurveplot.py | 41 +++++++ .../tests/plotting/bokeh/testhistogramplot.py | 109 +++++++++++++++++- holoviews/tests/plotting/bokeh/testlabels.py | 71 ++++++++++++ .../tests/plotting/bokeh/testpointplot.py | 4 + .../plotting/matplotlib/testcurveplot.py | 41 +++++++ .../plotting/matplotlib/testhistogramplot.py | 61 ++++++++++ .../tests/plotting/matplotlib/testlabels.py | 47 ++++++++ .../plotting/matplotlib/testpointplot.py | 8 +- 14 files changed, 458 insertions(+), 29 deletions(-) diff --git a/holoviews/element/chart.py b/holoviews/element/chart.py index 7c59e7fbfc..e635bbacc6 100644 --- a/holoviews/element/chart.py +++ b/holoviews/element/chart.py @@ -88,7 +88,7 @@ class ErrorBars(Chart): the error bars.""") vdims = param.List(default=[Dimension('y'), Dimension('yerror')], - bounds=(1, 3), constant=True) + bounds=(1, None), constant=True) def range(self, dim, data_range=True, dimension_range=True): @@ -155,7 +155,7 @@ class Histogram(Chart): Dimensions on Element2Ds determine the number of indexable dimensions.""") - vdims = param.List(default=[Dimension('Frequency')], bounds=(1,1)) + vdims = param.List(default=[Dimension('Frequency')], bounds=(1, None)) _binned = True diff --git a/holoviews/plotting/bokeh/annotation.py b/holoviews/plotting/bokeh/annotation.py index 7972a61abf..d1ce62a5a1 100644 --- a/holoviews/plotting/bokeh/annotation.py +++ b/holoviews/plotting/bokeh/annotation.py @@ -83,6 +83,8 @@ class LabelsPlot(ColorbarPlot, AnnotationPlot): style_opts = text_properties + ['cmap', 'angle'] + _no_op_styles = ['cmap'] + _plot_methods = dict(single='text', batched='text') _batched_style_opts = text_properties diff --git a/holoviews/plotting/bokeh/chart.py b/holoviews/plotting/bokeh/chart.py index 08899336a3..72a067e846 100644 --- a/holoviews/plotting/bokeh/chart.py +++ b/holoviews/plotting/bokeh/chart.py @@ -330,9 +330,9 @@ def get_batched_data(self, overlay, ranges): -class HistogramPlot(ElementPlot): +class HistogramPlot(ColorbarPlot): - style_opts = line_properties + fill_properties + style_opts = line_properties + fill_properties + ['cmap'] _plot_methods = dict(single='quad') def get_data(self, element, ranges, style): @@ -360,7 +360,7 @@ def get_extents(self, element, ranges, range_type='combined'): -class SideHistogramPlot(ColorbarPlot, HistogramPlot): +class SideHistogramPlot(HistogramPlot): style_opts = HistogramPlot.style_opts + ['cmap'] @@ -434,10 +434,12 @@ def _init_glyph(self, plot, mapping, properties): -class ErrorPlot(ElementPlot): +class ErrorPlot(ColorbarPlot): style_opts = line_properties + ['lower_head', 'upper_head'] + _no_op_styles = ['line_dash'] + _mapping = dict(base="base", upper="upper", lower="lower") _plot_methods = dict(single=Whisker) diff --git a/holoviews/plotting/bokeh/element.py b/holoviews/plotting/bokeh/element.py index 2d1c723c33..caf8a1a65f 100644 --- a/holoviews/plotting/bokeh/element.py +++ b/holoviews/plotting/bokeh/element.py @@ -677,22 +677,23 @@ def _apply_ops(self, element, source, ranges, style, group=None): if len(v.ops) == 0 and v.dimension in self.overlay_dims: val = self.overlay_dims[v.dimension] else: - val = v.eval(element, ranges) + val = v.eval(element, ranges['combined']) if len(np.unique(val)) == 1: val = val if np.isscalar(val) else val[0] if not np.isscalar(val): if k in self._no_op_styles: + element = type(element).__name__ raise ValueError('Mapping a dimension to the "{style}" ' 'style option is not supported by the ' - '{backend} backend. To map the {dim} ' - 'dimension to the {style} use a ' - 'groupby operation to overlay your ' - 'data along the dimension.'.format( - style=k, dim=v.dimension, + '{element} element using the {backend} ' + 'backend. To map the "{dim}" dimension ' + 'to the {style} use a groupby operation ' + 'to overlay your data along the dimension.'.format( + style=k, dim=v.dimension, element=element, backend=self.renderer.backend - ) + ) ) elif source.data and len(val) != len(list(source.data.values())[0]): continue @@ -705,6 +706,7 @@ def _apply_ops(self, element, source, ranges, style, group=None): key = k source.data[k] = val + # If color is not valid colorspec add colormapper numeric = isinstance(val, np.ndarray) and val.dtype.kind in 'uifMm' if ('color' in k and isinstance(val, np.ndarray) and (numeric or not validate('color', val))): @@ -714,6 +716,18 @@ def _apply_ops(self, element, source, ranges, style, group=None): cmapper = self._get_colormapper(v.dimension, element, ranges, style, name=dname+'_color_mapper', **kwargs) key = {'field': k, 'transform': cmapper} + + # If mapped to color/alpha override static fill/line style + for s in ('alpha', 'color'): + if s != k or k not in source.data: + continue + fill_style = new_style.get('fill_'+s) + if fill_style and validate(s, fill_style): + new_style.pop('fill_'+s) + line_style = new_style.get('line_'+s) + if line_style and validate(s, line_style): + new_style.pop('line_'+s) + new_style[k] = key return new_style diff --git a/holoviews/plotting/mpl/chart.py b/holoviews/plotting/mpl/chart.py index cde40f53c9..f17679019e 100644 --- a/holoviews/plotting/mpl/chart.py +++ b/holoviews/plotting/mpl/chart.py @@ -13,7 +13,8 @@ import param -from ...core import OrderedDict, Dimension, Store +from ...core import OrderedDict, Dimension +from ...core.options import Store, abbreviated_exception from ...core.util import ( match_spec, unique_iterator, basestring, max_range, isfinite, datetime_types, dt_to_int, dt64_to_dt @@ -68,6 +69,8 @@ class CurvePlot(ChartPlot): style_opts = ['alpha', 'color', 'visible', 'linewidth', 'linestyle', 'marker', 'ms'] + _no_op_styles = style_opts + _plot_methods = dict(single='plot') def get_data(self, element, ranges, style): @@ -262,7 +265,7 @@ def get_extents(self, element, ranges, range_type='combined'): -class HistogramPlot(ChartPlot): +class HistogramPlot(ColorbarPlot): """ HistogramPlot can plot DataHistograms and ViewMaps of DataHistograms, which can be displayed as a single frame or @@ -273,6 +276,8 @@ class HistogramPlot(ChartPlot): 'edgecolor', 'log', 'capsize', 'error_kw', 'hatch', 'linewidth'] + _no_op_styles = ['alpha', 'log', 'error_kw', 'hatch', 'visible', 'align'] + def __init__(self, histograms, **params): self.center = False self.cyclic = False @@ -310,6 +315,18 @@ def initialize_plot(self, ranges=None): self.offset_linefn = self.handles['axis'].axhline self.plotfn = self.handles['axis'].bar + with abbreviated_exception(): + style = self._apply_ops(hist, ranges, style) + if 'vmin' in style: + raise ValueError('Mapping a continuous dimension to a ' + 'color on a HistogramPlot is not ' + 'supported by the {backend} backend. ' + 'To map a dimension to a color supply ' + 'an explicit list of rgba colors.'.format( + backend=self.renderer.backend + ) + ) + # Plot bars and make any adjustments legend = hist.label if self.show_legend else '' bars = self.plotfn(edges, hvals, widths, zorder=self.zorder, label=legend, align='edge', **style) @@ -553,6 +570,9 @@ class PointPlot(ChartPlot, ColorbarPlot): 'linewidth', 'marker', 'size', 'visible', 'cmap', 'vmin', 'vmax', 'norm'] + _no_ops_styles = ['alpha', 'marker', 'cmap', 'vmin', 'vmax', + 'norm', 'visible'] + _disabled_opts = ['size'] _plot_methods = dict(single='scatter') @@ -745,6 +765,8 @@ class BarPlot(LegendPlot): style_opts = ['alpha', 'color', 'align', 'visible', 'edgecolor', 'log', 'facecolor', 'capsize', 'error_kw', 'hatch'] + _no_op_styles = style_opts + legend_specs = dict(LegendPlot.legend_specs, **{ 'top': dict(bbox_to_anchor=(0., 1.02, 1., .102), ncol=3, loc=3, mode="expand", borderaxespad=0.), @@ -801,6 +823,7 @@ def _compute_styles(self, element, style_groups): wrapped_style = self.lookup_options(element, 'style').max_cycles(len(style_product)) color_groups = {k:tuple(wrapped_style[n][sopt] for sopt in sopts) for n,k in enumerate(style_product)} + return style, color_groups, sopts @@ -906,6 +929,8 @@ def _create_bars(self, axis, element): label = ', '.join(label_key) style = dict(style_opts, label='' if label in labels else label, **dict(zip(sopts, color_groups[tuple(style_key)]))) + with abbreviated_exception(): + style = self._apply_ops(element, {}, style) bar = axis.bar([xpos], [val], width=width, bottom=prev, **style) @@ -1000,10 +1025,10 @@ def get_data(self, element, ranges, style): pos = self.position if ndims > 1: - data = [[(x, pos), (x, pos+y)] for x, y in element.array()] + data = [[(x, pos), (x, pos+y)] for x, y in element.array([0, 1])] else: height = self.spike_length - data = [[(x[0], pos), (x[0], pos+height)] for x in element.array()] + data = [[(x[0], pos), (x[0], pos+height)] for x in element.array([0])] if self.invert_axes: data = [(line[0][::-1], line[1][::-1]) for line in data] diff --git a/holoviews/plotting/mpl/element.py b/holoviews/plotting/mpl/element.py index 55035f41ac..d8183905c1 100644 --- a/holoviews/plotting/mpl/element.py +++ b/holoviews/plotting/mpl/element.py @@ -510,7 +510,7 @@ def update_handles(self, key, axis, element, ranges, style): """ self.teardown_handles() with abbreviated_exception(): - new_style = self._apply_ops(element, range, style) + new_style = self._apply_ops(element, ranges, style) plot_data, plot_kwargs, axis_kwargs = self.get_data(element, ranges, new_style) with abbreviated_exception(): @@ -541,22 +541,24 @@ def _apply_ops(self, element, ranges, style): if len(v.ops) == 0 and v.dimension in self.overlay_dims: val = self.overlay_dims[v.dimension] else: - val = v.eval(element, ranges) + val = v.eval(element, ranges['combined']) if len(np.unique(val)) == 1: val = val if np.isscalar(val) else val[0] if not np.isscalar(val) and k in self._no_op_styles: + element = type(element).__name__ raise ValueError('Mapping a dimension to the "{style}" ' 'style option is not supported by the ' - '{backend} backend. To map the "{dim}" ' - 'dimension to the {style} use a ' - 'groupby operation to overlay your ' - 'data along the dimension.'.format( - style=k, dim=v.dimension, + '{element} element using the {backend} ' + 'backend. To map the "{dim}" dimension ' + 'to the {style} use a groupby operation ' + 'to overlay your data along the dimension.'.format( + style=k, dim=v.dimension, element=element, backend=self.renderer.backend ) ) + if 'color' == k and (isinstance(val, np.ndarray) and all(not is_color(c) for c in val)): new_style.pop(k) self._norm_kwargs(element, ranges, new_style, v.dimension, val) @@ -564,9 +566,17 @@ def _apply_ops(self, element, ranges, style): val = categorize_colors(val) k = 'c' - if k == 'facecolors': + # If mapped to color/alpha override static fill/line style + if k == 'c' or (k == 'color' and isinstance(val, np.ndarray)): + fill_style = new_style.get('facecolor') + if fill_style and is_color(fill_style): + new_style.pop('facecolor') + line_style = new_style.get('edgecolor') + if line_style and is_color(line_style): + new_style.pop('edgecolor') + elif k == 'facecolors': # Color overrides facecolors if defined - new_style.pop('color') + new_style.pop('color', None) new_style[k] = val diff --git a/holoviews/tests/plotting/bokeh/testcurveplot.py b/holoviews/tests/plotting/bokeh/testcurveplot.py index a50cc94457..eda7801b50 100644 --- a/holoviews/tests/plotting/bokeh/testcurveplot.py +++ b/holoviews/tests/plotting/bokeh/testcurveplot.py @@ -337,3 +337,44 @@ def test_curve_padding_datetime_nonsquare(self): self.assertEqual(x_range.end, np.datetime64('2016-04-03T02:24:00.000000000')) self.assertEqual(y_range.start, 0.8) self.assertEqual(y_range.end, 3.2) + + ########################### + # Styling mapping # + ########################### + + def test_curve_scalar_color_op(self): + curve = Curve([(0, 0, 'red'), (0, 1, 'red'), (0, 2, 'red')], + vdims=['y', 'color']).options(color='color') + plot = bokeh_renderer.get_plot(curve) + glyph = plot.handles['glyph'] + self.assertEqual(glyph.line_color, 'red') + + def test_op_ndoverlay_color_value(self): + colors = ['blue', 'red'] + overlay = NdOverlay({color: Curve(np.arange(i)) + for i, color in enumerate(colors)}, + 'color').options('Curve', color='color') + plot = bokeh_renderer.get_plot(overlay) + for subplot, color in zip(plot.subplots.values(), colors): + style = dict(subplot.style[subplot.cyclic_index]) + cds = subplot.handles['cds'] + style = subplot._apply_ops(subplot.current_frame, cds, {}, style) + self.assertEqual(style['color'], color) + + def test_curve_color_op(self): + curve = Curve([(0, 0, 'red'), (0, 1, 'blue'), (0, 2, 'red')], + vdims=['y', 'color']).options(color='color') + with self.assertRaises(Exception): + bokeh_renderer.get_plot(curve) + + def test_curve_alpha_op(self): + curve = Curve([(0, 0, 0.1), (0, 1, 0.3), (0, 2, 1)], + vdims=['y', 'alpha']).options(alpha='alpha') + with self.assertRaises(Exception): + bokeh_renderer.get_plot(curve) + + def test_curve_line_width_op(self): + curve = Curve([(0, 0, 0.1), (0, 1, 0.3), (0, 2, 1)], + vdims=['y', 'linewidth']).options(line_width='linewidth') + with self.assertRaises(Exception): + bokeh_renderer.get_plot(curve) diff --git a/holoviews/tests/plotting/bokeh/testhistogramplot.py b/holoviews/tests/plotting/bokeh/testhistogramplot.py index 4ef525d4b2..bd36451f1b 100644 --- a/holoviews/tests/plotting/bokeh/testhistogramplot.py +++ b/holoviews/tests/plotting/bokeh/testhistogramplot.py @@ -2,10 +2,11 @@ import numpy as np +from holoviews.core.overlay import NdOverlay from holoviews.element import Image, Points, Dataset, Histogram from holoviews.operation import histogram -from bokeh.models import DatetimeAxis +from bokeh.models import DatetimeAxis, CategoricalColorMapper, LinearColorMapper from .testplot import TestBokehPlot, bokeh_renderer @@ -148,3 +149,109 @@ def test_histogram_padding_datetime_nonsquare(self): self.assertEqual(x_range.end, np.datetime64('2016-04-03T15:36:00.000000000')) self.assertEqual(y_range.start, 0) self.assertEqual(y_range.end, 3.2) + + ########################### + # Styling mapping # + ########################### + + def test_histogram_color_op(self): + histogram = Histogram([(0, 0, '#000'), (0, 1, '#F00'), (0, 2, '#0F0')], + vdims=['y', 'color']).options(color='color') + plot = bokeh_renderer.get_plot(histogram) + cds = plot.handles['cds'] + glyph = plot.handles['glyph'] + self.assertEqual(cds.data['color'], np.array(['#000', '#F00', '#0F0'])) + self.assertEqual(glyph.fill_color, 'color') + self.assertEqual(glyph.line_color, 'color') + + def test_histogram_linear_color_op(self): + histogram = Histogram([(0, 0, 0), (0, 1, 1), (0, 2, 2)], + vdims=['y', 'color']).options(color='color') + plot = bokeh_renderer.get_plot(histogram) + cds = plot.handles['cds'] + glyph = plot.handles['glyph'] + cmapper = plot.handles['color_color_mapper'] + self.assertTrue(cmapper, LinearColorMapper) + self.assertEqual(cmapper.low, 0) + self.assertEqual(cmapper.high, 2) + self.assertEqual(cds.data['color'], np.array([0, 1, 2])) + self.assertEqual(glyph.fill_color, {'field': 'color', 'transform': cmapper}) + self.assertEqual(glyph.line_color, {'field': 'color', 'transform': cmapper}) + + def test_histogram_categorical_color_op(self): + histogram = Histogram([(0, 0, 'A'), (0, 1, 'B'), (0, 2, 'C')], + vdims=['y', 'color']).options(color='color') + plot = bokeh_renderer.get_plot(histogram) + cds = plot.handles['cds'] + glyph = plot.handles['glyph'] + cmapper = plot.handles['color_color_mapper'] + self.assertTrue(cmapper, CategoricalColorMapper) + self.assertEqual(cmapper.factors, np.array(['A', 'B', 'C'])) + self.assertEqual(cds.data['color'], np.array(['A', 'B', 'C'])) + self.assertEqual(glyph.fill_color, {'field': 'color', 'transform': cmapper}) + self.assertEqual(glyph.line_color, {'field': 'color', 'transform': cmapper}) + + def test_histogram_line_color_op(self): + histogram = Histogram([(0, 0, '#000'), (0, 1, '#F00'), (0, 2, '#0F0')], + vdims=['y', 'color']).options(line_color='color') + plot = bokeh_renderer.get_plot(histogram) + cds = plot.handles['cds'] + glyph = plot.handles['glyph'] + self.assertEqual(cds.data['line_color'], np.array(['#000', '#F00', '#0F0'])) + self.assertNotEqual(glyph.fill_color, 'line_color') + self.assertEqual(glyph.line_color, 'line_color') + + def test_histogram_fill_color_op(self): + histogram = Histogram([(0, 0, '#000'), (0, 1, '#F00'), (0, 2, '#0F0')], + vdims=['y', 'color']).options(fill_color='color') + plot = bokeh_renderer.get_plot(histogram) + cds = plot.handles['cds'] + glyph = plot.handles['glyph'] + self.assertEqual(cds.data['fill_color'], np.array(['#000', '#F00', '#0F0'])) + self.assertEqual(glyph.fill_color, 'fill_color') + self.assertNotEqual(glyph.line_color, 'fill_color') + + def test_histogram_alpha_op(self): + histogram = Histogram([(0, 0, 0), (0, 1, 0.2), (0, 2, 0.7)], + vdims=['y', 'alpha']).options(alpha='alpha') + plot = bokeh_renderer.get_plot(histogram) + cds = plot.handles['cds'] + glyph = plot.handles['glyph'] + self.assertEqual(cds.data['alpha'], np.array([0, 0.2, 0.7])) + self.assertEqual(glyph.fill_alpha, 'alpha') + + def test_histogram_line_alpha_op(self): + histogram = Histogram([(0, 0, 0), (0, 1, 0.2), (0, 2, 0.7)], + vdims=['y', 'alpha']).options(line_alpha='alpha') + plot = bokeh_renderer.get_plot(histogram) + cds = plot.handles['cds'] + glyph = plot.handles['glyph'] + self.assertEqual(cds.data['line_alpha'], np.array([0, 0.2, 0.7])) + self.assertEqual(glyph.line_alpha, 'line_alpha') + self.assertNotEqual(glyph.fill_alpha, 'line_alpha') + + def test_histogram_fill_alpha_op(self): + histogram = Histogram([(0, 0, 0), (0, 1, 0.2), (0, 2, 0.7)], + vdims=['y', 'alpha']).options(fill_alpha='alpha') + plot = bokeh_renderer.get_plot(histogram) + cds = plot.handles['cds'] + glyph = plot.handles['glyph'] + self.assertEqual(cds.data['fill_alpha'], np.array([0, 0.2, 0.7])) + self.assertNotEqual(glyph.line_alpha, 'fill_alpha') + self.assertEqual(glyph.fill_alpha, 'fill_alpha') + + def test_histogram_line_width_op(self): + histogram = Histogram([(0, 0, 1), (0, 1, 4), (0, 2, 8)], + vdims=['y', 'line_width']).options(line_width='line_width') + plot = bokeh_renderer.get_plot(histogram) + cds = plot.handles['cds'] + glyph = plot.handles['glyph'] + self.assertEqual(cds.data['line_width'], np.array([1, 4, 8])) + self.assertEqual(glyph.line_width, 'line_width') + + def test_op_ndoverlay_value(self): + colors = ['blue', 'red'] + overlay = NdOverlay({color: Histogram(np.arange(i+2)) for i, color in enumerate(colors)}, 'Color').options('Histogram', fill_color='Color') + plot = bokeh_renderer.get_plot(overlay) + for subplot, color in zip(plot.subplots.values(), colors): + self.assertEqual(subplot.handles['glyph'].fill_color, color) diff --git a/holoviews/tests/plotting/bokeh/testlabels.py b/holoviews/tests/plotting/bokeh/testlabels.py index a94577027e..224dd6ee99 100644 --- a/holoviews/tests/plotting/bokeh/testlabels.py +++ b/holoviews/tests/plotting/bokeh/testlabels.py @@ -3,6 +3,12 @@ from holoviews.core.dimension import Dimension from holoviews.element import Labels +try: + from bokeh.models import LinearColorMapper, CategoricalColorMapper +except: + pass + + from .testplot import TestBokehPlot, bokeh_renderer @@ -94,3 +100,68 @@ def test_labels_color_mapped(self): self.assertEqual(glyph.text_color, {'field': 'color', 'transform': cmapper}) self.assertEqual(cmapper.low, 1) self.assertEqual(cmapper.high, 2) + + ########################### + # Styling mapping # + ########################### + + def test_label_color_op(self): + labels = Labels([(0, 0, '#000'), (0, 1, '#F00'), (0, 2, '#0F0')], + vdims='color').options(text_color='color') + plot = bokeh_renderer.get_plot(labels) + cds = plot.handles['cds'] + glyph = plot.handles['glyph'] + self.assertEqual(cds.data['text_color'], np.array(['#000', '#F00', '#0F0'])) + self.assertEqual(glyph.text_color, 'text_color') + + def test_label_linear_color_op(self): + labels = Labels([(0, 0, 0), (0, 1, 1), (0, 2, 2)], + vdims='color').options(text_color='color') + plot = bokeh_renderer.get_plot(labels) + cds = plot.handles['cds'] + glyph = plot.handles['glyph'] + cmapper = plot.handles['color_color_mapper'] + self.assertTrue(cmapper, LinearColorMapper) + self.assertEqual(cmapper.low, 0) + self.assertEqual(cmapper.high, 2) + self.assertEqual(cds.data['text_color'], np.array([0, 1, 2])) + self.assertEqual(glyph.text_color, {'field': 'text_color', 'transform': cmapper}) + + def test_label_categorical_color_op(self): + labels = Labels([(0, 0, 'A'), (0, 1, 'B'), (0, 2, 'C')], + vdims='color').options(text_color='color') + plot = bokeh_renderer.get_plot(labels) + cds = plot.handles['cds'] + glyph = plot.handles['glyph'] + cmapper = plot.handles['color_color_mapper'] + self.assertTrue(cmapper, CategoricalColorMapper) + self.assertEqual(cmapper.factors, np.array(['A', 'B', 'C'])) + self.assertEqual(cds.data['text_color'], np.array(['A', 'B', 'C'])) + self.assertEqual(glyph.text_color, {'field': 'text_color', 'transform': cmapper}) + + def test_label_angle_op(self): + labels = Labels([(0, 0, 0), (0, 1, 45), (0, 2, 90)], + vdims='angle').options(angle='angle') + plot = bokeh_renderer.get_plot(labels) + cds = plot.handles['cds'] + glyph = plot.handles['glyph'] + self.assertEqual(cds.data['angle'], np.array([0, 0.785398, 1.570796])) + self.assertEqual(glyph.angle, 'angle') + + def test_label_alpha_op(self): + labels = Labels([(0, 0, 0), (0, 1, 0.2), (0, 2, 0.7)], + vdims='alpha').options(text_alpha='alpha') + plot = bokeh_renderer.get_plot(labels) + cds = plot.handles['cds'] + glyph = plot.handles['glyph'] + self.assertEqual(cds.data['text_alpha'], np.array([0, 0.2, 0.7])) + self.assertEqual(glyph.text_alpha, 'text_alpha') + + def test_label_size_op(self): + labels = Labels([(0, 0, '10pt'), (0, 1, '4pt'), (0, 2, '8pt')], + vdims='size').options(text_font_size='size') + plot = bokeh_renderer.get_plot(labels) + cds = plot.handles['cds'] + glyph = plot.handles['glyph'] + self.assertEqual(cds.data['size'], ['10pt', '4pt', '8pt']) + self.assertEqual(glyph.text_font_size, 'text_font_size') diff --git a/holoviews/tests/plotting/bokeh/testpointplot.py b/holoviews/tests/plotting/bokeh/testpointplot.py index 8820ab0b97..cc1ff2b5ae 100644 --- a/holoviews/tests/plotting/bokeh/testpointplot.py +++ b/holoviews/tests/plotting/bokeh/testpointplot.py @@ -322,6 +322,10 @@ def test_points_datetime_hover(self): hover = plot.handles['hover'] self.assertEqual(hover.tooltips, [('x', '@{x}'), ('y', '@{y}'), ('date', '@{date_dt_strings}')]) + ########################### + # Styling mapping # + ########################### + def test_point_color_op(self): points = Points([(0, 0, '#000'), (0, 1, '#F00'), (0, 2, '#0F0')], vdims='color').options(color='color') diff --git a/holoviews/tests/plotting/matplotlib/testcurveplot.py b/holoviews/tests/plotting/matplotlib/testcurveplot.py index 3b0100a1c6..1eba50e40f 100644 --- a/holoviews/tests/plotting/matplotlib/testcurveplot.py +++ b/holoviews/tests/plotting/matplotlib/testcurveplot.py @@ -3,6 +3,7 @@ import numpy as np +from holoviews.core.overlay import NdOverlay from holoviews.core.util import pd from holoviews.element import Curve @@ -144,3 +145,43 @@ def test_curve_padding_datetime_nonsquare(self): self.assertEqual(x_range[1], 736057.09999999998) self.assertEqual(y_range[0], 0.8) self.assertEqual(y_range[1], 3.2) + + ########################### + # Styling mapping # + ########################### + + def test_curve_scalar_color_op(self): + curve = Curve([(0, 0, 'red'), (0, 1, 'red'), (0, 2, 'red')], + vdims=['y', 'color']).options(color='color') + plot = mpl_renderer.get_plot(curve) + artist = plot.handles['artist'] + self.assertEqual(artist.get_color(), 'red') + + def test_op_ndoverlay_color_value(self): + colors = ['blue', 'red'] + overlay = NdOverlay({color: Curve(np.arange(i)) + for i, color in enumerate(colors)}, + 'color').options('Curve', color='color') + plot = mpl_renderer.get_plot(overlay) + for subplot, color in zip(plot.subplots.values(), colors): + style = dict(subplot.style[subplot.cyclic_index]) + style = subplot._apply_ops(subplot.current_frame, {}, style) + self.assertEqual(style['color'], color) + + def test_curve_color_op(self): + curve = Curve([(0, 0, 'red'), (0, 1, 'blue'), (0, 2, 'red')], + vdims=['y', 'color']).options(color='color') + with self.assertRaises(Exception): + mpl_renderer.get_plot(curve) + + def test_curve_alpha_op(self): + curve = Curve([(0, 0, 0.1), (0, 1, 0.3), (0, 2, 1)], + vdims=['y', 'alpha']).options(alpha='alpha') + with self.assertRaises(Exception): + mpl_renderer.get_plot(curve) + + def test_curve_linewidth_op(self): + curve = Curve([(0, 0, 0.1), (0, 1, 0.3), (0, 2, 1)], + vdims=['y', 'linewidth']).options(linewidth='linewidth') + with self.assertRaises(Exception): + mpl_renderer.get_plot(curve) diff --git a/holoviews/tests/plotting/matplotlib/testhistogramplot.py b/holoviews/tests/plotting/matplotlib/testhistogramplot.py index 63b8f3d652..f06ef118ef 100644 --- a/holoviews/tests/plotting/matplotlib/testhistogramplot.py +++ b/holoviews/tests/plotting/matplotlib/testhistogramplot.py @@ -2,8 +2,10 @@ import numpy as np +from holoviews.core.overlay import NdOverlay from holoviews.element import Dataset, Histogram from holoviews.operation import histogram +from holoviews.plotting.util import hex2rgb from .testplot import TestMPLPlot, mpl_renderer @@ -95,3 +97,62 @@ def test_histogram_padding_datetime_nonsquare(self): self.assertEqual(x_range[1], 736057.65000000002) self.assertEqual(y_range[0], 0) self.assertEqual(y_range[1], 3.2) + + ########################### + # Styling mapping # + ########################### + + def test_histogram_color_op(self): + histogram = Histogram([(0, 0, '#000000'), (0, 1, '#FF0000'), (0, 2, '#00FF00')], + vdims=['y', 'color']).options(color='color') + plot = mpl_renderer.get_plot(histogram) + artist = plot.handles['artist'] + children = artist.get_children() + for c, w in zip(children, ['#000000', '#FF0000', '#00FF00']): + self.assertEqual(c.get_facecolor(), tuple(c/255. for c in hex2rgb(w))+(1,)) + + def test_histogram_linear_color_op(self): + histogram = Histogram([(0, 0, 0), (0, 1, 1), (0, 2, 2)], + vdims=['y', 'color']).options(color='color') + with self.assertRaises(Exception): + mpl_renderer.get_plot(histogram) + + def test_histogram_categorical_color_op(self): + histogram = Histogram([(0, 0, 'A'), (0, 1, 'B'), (0, 2, 'C')], + vdims=['y', 'color']).options(color='color') + with self.assertRaises(Exception): + mpl_renderer.get_plot(histogram) + + def test_histogram_line_color_op(self): + histogram = Histogram([(0, 0, '#000'), (0, 1, '#F00'), (0, 2, '#0F0')], + vdims=['y', 'color']).options(edgecolor='color') + with self.assertRaises(Exception): + mpl_renderer.get_plot(histogram) + + def test_histogram_alpha_op(self): + histogram = Histogram([(0, 0, 0), (0, 1, 0.2), (0, 2, 0.7)], + vdims=['y', 'alpha']).options(alpha='alpha') + with self.assertRaises(Exception): + mpl_renderer.get_plot(histogram) + + def test_histogram_line_width_op(self): + histogram = Histogram([(0, 0, 1), (0, 1, 4), (0, 2, 8)], + vdims=['y', 'line_width']).options(linewidth='line_width') + plot = mpl_renderer.get_plot(histogram) + artist = plot.handles['artist'] + children = artist.get_children() + for c, w in zip(children, np.array([1, 4, 8])): + self.assertEqual(c.get_linewidth(), w) + + def test_op_ndoverlay_value(self): + colors = ['blue', 'red'] + overlay = NdOverlay({color: Histogram(np.arange(i+2)) + for i, color in enumerate(colors)}, 'Color').options( + 'Histogram', facecolor='Color' + ) + plot = mpl_renderer.get_plot(overlay) + colors = [(0, 0, 1, 1), (1, 0, 0, 1)] + for subplot, color in zip(plot.subplots.values(), colors): + children = subplot.handles['artist'].get_children() + for c in children: + self.assertEqual(c.get_facecolor(), color) diff --git a/holoviews/tests/plotting/matplotlib/testlabels.py b/holoviews/tests/plotting/matplotlib/testlabels.py index 676ac24877..d509a1ae77 100644 --- a/holoviews/tests/plotting/matplotlib/testlabels.py +++ b/holoviews/tests/plotting/matplotlib/testlabels.py @@ -60,3 +60,50 @@ def test_labels_color_mapped(self): self.assertEqual(text._y, expected['y'][i]) self.assertEqual(text.get_text(), expected['Label'][i]) self.assertEqual(text.get_color(), colors[i]) + + ########################### + # Styling mapping # + ########################### + + def test_label_color_op(self): + labels = Labels([(0, 0, '#000000'), (0, 1, '#FF0000'), (0, 2, '#00FF00')], + vdims='color').options(color='color') + plot = mpl_renderer.get_plot(labels) + artist = plot.handles['artist'] + self.assertEqual([a.get_color() for a in artist], + ['#000000', '#FF0000', '#00FF00']) + + def test_label_linear_color_op(self): + labels = Labels([(0, 0, 0), (0, 1, 1), (0, 2, 2)], + vdims='color').options(color='color') + with self.assertRaises(Exception): + mpl_renderer.get_plot(labels) + + def test_label_categorical_color_op(self): + labels = Labels([(0, 0, 'A'), (0, 1, 'B'), (0, 2, 'A')], + vdims='color').options(color='color') + with self.assertRaises(Exception): + mpl_renderer.get_plot(labels) + + def test_label_size_op(self): + labels = Labels([(0, 0, 8), (0, 1, 12), (0, 2, 6)], + vdims='size').options(size='size') + plot = mpl_renderer.get_plot(labels) + artist = plot.handles['artist'] + self.assertEqual([a.get_fontsize() for a in artist], [8, 12, 6]) + + def test_label_alpha_op(self): + labels = Labels([(0, 0, 0), (0, 1, 0.2), (0, 2, 0.7)], + vdims='alpha').options(alpha='alpha') + plot = mpl_renderer.get_plot(labels) + artist = plot.handles['artist'] + self.assertEqual([a.get_alpha() for a in artist], + [0, 0.2, 0.7]) + + def test_label_rotation_op(self): + labels = Labels([(0, 0, 90), (0, 1, 180), (0, 2, 270)], + vdims='rotation').options(rotation='rotation') + plot = mpl_renderer.get_plot(labels) + artist = plot.handles['artist'] + self.assertEqual([a.get_rotation() for a in artist], + [90, 180, 270]) diff --git a/holoviews/tests/plotting/matplotlib/testpointplot.py b/holoviews/tests/plotting/matplotlib/testpointplot.py index af7438de36..7c238b4519 100644 --- a/holoviews/tests/plotting/matplotlib/testpointplot.py +++ b/holoviews/tests/plotting/matplotlib/testpointplot.py @@ -152,6 +152,10 @@ def test_points_padding_datetime_nonsquare(self): self.assertEqual(y_range[0], 0.8) self.assertEqual(y_range[1], 3.2) + ########################### + # Styling mapping # + ########################### + def test_point_color_op(self): points = Points([(0, 0, '#000000'), (0, 1, '#FF0000'), (0, 2, '#00FF00')], vdims='color').options(color='color') @@ -209,13 +213,13 @@ def test_point_line_width_op(self): def test_point_marker_op(self): points = Points([(0, 0, 'circle'), (0, 1, 'triangle'), (0, 2, 'square')], vdims='marker').options(marker='marker') - with self.assertRaises(ValueError): + with self.assertRaises(Exception): mpl_renderer.get_plot(points) def test_point_alpha_op(self): points = Points([(0, 0, 0), (0, 1, 0.2), (0, 2, 0.7)], vdims='alpha').options(alpha='alpha') - with self.assertRaises(ValueError): + with self.assertRaises(Exception): mpl_renderer.get_plot(points) def test_op_ndoverlay_value(self): From df80c26452ff9b698ca20034cfae93d2709f7208 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Sat, 10 Nov 2018 17:13:14 +0000 Subject: [PATCH 019/117] Fixes for op ranges --- holoviews/plotting/bokeh/element.py | 2 +- holoviews/plotting/mpl/element.py | 2 +- holoviews/util/ops.py | 1 + 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/holoviews/plotting/bokeh/element.py b/holoviews/plotting/bokeh/element.py index caf8a1a65f..eb20ec7a31 100644 --- a/holoviews/plotting/bokeh/element.py +++ b/holoviews/plotting/bokeh/element.py @@ -677,7 +677,7 @@ def _apply_ops(self, element, source, ranges, style, group=None): if len(v.ops) == 0 and v.dimension in self.overlay_dims: val = self.overlay_dims[v.dimension] else: - val = v.eval(element, ranges['combined']) + val = v.eval(element, ranges) if len(np.unique(val)) == 1: val = val if np.isscalar(val) else val[0] diff --git a/holoviews/plotting/mpl/element.py b/holoviews/plotting/mpl/element.py index d8183905c1..3c31bb2f0d 100644 --- a/holoviews/plotting/mpl/element.py +++ b/holoviews/plotting/mpl/element.py @@ -541,7 +541,7 @@ def _apply_ops(self, element, ranges, style): if len(v.ops) == 0 and v.dimension in self.overlay_dims: val = self.overlay_dims[v.dimension] else: - val = v.eval(element, ranges['combined']) + val = v.eval(element, ranges) if len(np.unique(val)) == 1: val = val if np.isscalar(val) else val[0] diff --git a/holoviews/util/ops.py b/holoviews/util/ops.py index 7e442b14b6..16445b12e5 100644 --- a/holoviews/util/ops.py +++ b/holoviews/util/ops.py @@ -85,6 +85,7 @@ def eval(self, dataset, ranges={}): else: args = (data,) drange = ranges.get(self.dimension.name) + drange = drange.get('combined', drange) if o['fn'] == norm_fn and drange is not None: data = o['fn'](data, *drange) else: From a8276a677e5c0fd54551bd0e8a38e62a4794d204 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Sat, 10 Nov 2018 17:13:35 +0000 Subject: [PATCH 020/117] Add style mapping tests for SpikePlot --- .../tests/plotting/bokeh/testspikesplot.py | 83 +++++++++++++++++++ .../plotting/matplotlib/testspikeplot.py | 54 ++++++++++++ 2 files changed, 137 insertions(+) diff --git a/holoviews/tests/plotting/bokeh/testspikesplot.py b/holoviews/tests/plotting/bokeh/testspikesplot.py index a95ad02995..9929a37d0b 100644 --- a/holoviews/tests/plotting/bokeh/testspikesplot.py +++ b/holoviews/tests/plotting/bokeh/testspikesplot.py @@ -4,6 +4,8 @@ from holoviews.core import NdOverlay from holoviews.element import Spikes +from bokeh.models import CategoricalColorMapper, LinearColorMapper + from .testplot import TestBokehPlot, bokeh_renderer @@ -127,3 +129,84 @@ def test_spikes_datetime_kdim_hover(self): self.assertEqual(cds.data['x_dt_strings'], ['2017-01-01 00:00:00']) hover = plot.handles['hover'] self.assertEqual(hover.tooltips, [('x', '@{x_dt_strings}'), ('y', '@{y}')]) + + ########################### + # Styling mapping # + ########################### + + def test_spikes_color_op(self): + spikes = Spikes([(0, 0, '#000'), (0, 1, '#F00'), (0, 2, '#0F0')], + vdims=['y', 'color']).options(color='color') + plot = bokeh_renderer.get_plot(spikes) + cds = plot.handles['cds'] + glyph = plot.handles['glyph'] + self.assertEqual(cds.data['color'], np.array(['#000', '#F00', '#0F0'])) + self.assertEqual(glyph.line_color, 'color') + + def test_spikes_linear_color_op(self): + spikes = Spikes([(0, 0, 0), (0, 1, 1), (0, 2, 2)], + vdims=['y', 'color']).options(color='color') + plot = bokeh_renderer.get_plot(spikes) + cds = plot.handles['cds'] + glyph = plot.handles['glyph'] + cmapper = plot.handles['color_color_mapper'] + self.assertTrue(cmapper, LinearColorMapper) + self.assertEqual(cmapper.low, 0) + self.assertEqual(cmapper.high, 2) + self.assertEqual(cds.data['color'], np.array([0, 1, 2])) + self.assertEqual(glyph.line_color, {'field': 'color', 'transform': cmapper}) + + def test_spikes_categorical_color_op(self): + spikes = Spikes([(0, 0, 'A'), (0, 1, 'B'), (0, 2, 'C')], + vdims=['y', 'color']).options(color='color') + plot = bokeh_renderer.get_plot(spikes) + cds = plot.handles['cds'] + glyph = plot.handles['glyph'] + cmapper = plot.handles['color_color_mapper'] + self.assertTrue(cmapper, CategoricalColorMapper) + self.assertEqual(cmapper.factors, np.array(['A', 'B', 'C'])) + self.assertEqual(cds.data['color'], np.array(['A', 'B', 'C'])) + self.assertEqual(glyph.line_color, {'field': 'color', 'transform': cmapper}) + + def test_spikes_line_color_op(self): + spikes = Spikes([(0, 0, '#000'), (0, 1, '#F00'), (0, 2, '#0F0')], + vdims=['y', 'color']).options(line_color='color') + plot = bokeh_renderer.get_plot(spikes) + cds = plot.handles['cds'] + glyph = plot.handles['glyph'] + self.assertEqual(cds.data['line_color'], np.array(['#000', '#F00', '#0F0'])) + self.assertEqual(glyph.line_color, 'line_color') + + def test_spikes_alpha_op(self): + spikes = Spikes([(0, 0, 0), (0, 1, 0.2), (0, 2, 0.7)], + vdims=['y', 'alpha']).options(alpha='alpha') + plot = bokeh_renderer.get_plot(spikes) + cds = plot.handles['cds'] + glyph = plot.handles['glyph'] + self.assertEqual(cds.data['alpha'], np.array([0, 0.2, 0.7])) + self.assertEqual(glyph.line_alpha, 'alpha') + + def test_spikes_line_alpha_op(self): + spikes = Spikes([(0, 0, 0), (0, 1, 0.2), (0, 2, 0.7)], + vdims=['y', 'alpha']).options(line_alpha='alpha') + plot = bokeh_renderer.get_plot(spikes) + cds = plot.handles['cds'] + glyph = plot.handles['glyph'] + self.assertEqual(cds.data['line_alpha'], np.array([0, 0.2, 0.7])) + self.assertEqual(glyph.line_alpha, 'line_alpha') + + def test_spikes_line_width_op(self): + spikes = Spikes([(0, 0, 1), (0, 1, 4), (0, 2, 8)], + vdims=['y', 'line_width']).options(line_width='line_width') + plot = bokeh_renderer.get_plot(spikes) + cds = plot.handles['cds'] + glyph = plot.handles['glyph'] + self.assertEqual(cds.data['line_width'], np.array([1, 4, 8])) + self.assertEqual(glyph.line_width, 'line_width') + + def test_op_ndoverlay_value(self): + colors = ['blue', 'red'] + overlay = NdOverlay({color: Spikes(np.arange(i+2)) for i, color in enumerate(colors)}, 'Color').options('Spikes', color='Color') + plot = bokeh_renderer.get_plot(overlay) + for subplot, color in zip(plot.subplots.values(), colors): + self.assertEqual(subplot.handles['glyph'].line_color, color) diff --git a/holoviews/tests/plotting/matplotlib/testspikeplot.py b/holoviews/tests/plotting/matplotlib/testspikeplot.py index e14872a5f5..d72bafc6a9 100644 --- a/holoviews/tests/plotting/matplotlib/testspikeplot.py +++ b/holoviews/tests/plotting/matplotlib/testspikeplot.py @@ -1,5 +1,6 @@ import numpy as np +from holoviews.core.overlay import NdOverlay from holoviews.element import Spikes from .testplot import TestMPLPlot, mpl_renderer @@ -86,3 +87,56 @@ def test_spikes_padding_datetime_nonsquare(self): x_range = plot.handles['axis'].get_xlim() self.assertEqual(x_range[0], 736054.90000000002) self.assertEqual(x_range[1], 736057.09999999998) + + ########################### + # Styling mapping # + ########################### + + def test_spikes_color_op(self): + spikes = Spikes([(0, 0, '#000000'), (0, 1, '#FF0000'), (0, 2, '#00FF00')], + vdims=['y', 'color']).options(color='color') + plot = mpl_renderer.get_plot(spikes) + artist = plot.handles['artist'] + children = artist.get_children() + for c, w in zip(children, ['#000000', '#FF0000', '#00FF00']): + self.assertEqual(c.get_facecolor(), tuple(c/255. for c in hex2rgb(w))+(1,)) + + def test_spikes_linear_color_op(self): + spikes = Spikes([(0, 0, 0), (0, 1, 1), (0, 2, 2)], + vdims=['y', 'color']).options(color='color') + with self.assertRaises(Exception): + mpl_renderer.get_plot(spikes) + + def test_spikes_categorical_color_op(self): + spikes = Spikes([(0, 0, 'A'), (0, 1, 'B'), (0, 2, 'C')], + vdims=['y', 'color']).options(color='color') + with self.assertRaises(Exception): + mpl_renderer.get_plot(spikes) + + def test_spikes_alpha_op(self): + spikes = Spikes([(0, 0, 0), (0, 1, 0.2), (0, 2, 0.7)], + vdims=['y', 'alpha']).options(alpha='alpha') + with self.assertRaises(Exception): + mpl_renderer.get_plot(spikes) + + def test_spikes_line_width_op(self): + spikes = Spikes([(0, 0, 1), (0, 1, 4), (0, 2, 8)], + vdims=['y', 'line_width']).options(linewidth='line_width') + plot = mpl_renderer.get_plot(spikes) + artist = plot.handles['artist'] + children = artist.get_children() + for c, w in zip(children, np.array([1, 4, 8])): + self.assertEqual(c.get_linewidth(), w) + + def test_op_ndoverlay_value(self): + colors = ['blue', 'red'] + overlay = NdOverlay({color: Spikes(np.arange(i+2)) + for i, color in enumerate(colors)}, 'Color').options( + 'Spikes', color='Color' + ) + plot = mpl_renderer.get_plot(overlay) + colors = [(0, 0, 1, 1), (1, 0, 0, 1)] + for subplot, color in zip(plot.subplots.values(), colors): + children = subplot.handles['artist'].get_children() + for c in children: + self.assertEqual(c.get_facecolor(), color) From f96674f3de184c5667f82b3e9d8c7ce2f56607b0 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Sat, 10 Nov 2018 20:12:37 +0000 Subject: [PATCH 021/117] Various smaller fixes --- holoviews/__init__.py | 6 ++++-- holoviews/plotting/bokeh/element.py | 8 +++++--- holoviews/plotting/bokeh/path.py | 7 ++++++- holoviews/plotting/bokeh/styles.py | 2 ++ holoviews/plotting/mpl/chart.py | 2 ++ 5 files changed, 19 insertions(+), 6 deletions(-) diff --git a/holoviews/__init__.py b/holoviews/__init__.py index 4fbf4a7c4b..c789fc02b2 100644 --- a/holoviews/__init__.py +++ b/holoviews/__init__.py @@ -9,6 +9,7 @@ __version__ = str(param.version.Version(fpath=__file__, archive_commit="$Format:%h$", reponame="holoviews")) +from . import util # noqa (API import) from .core import archive, config # noqa (API import) from .core.boundingregion import BoundingBox # noqa (API import) from .core.dimension import OrderedDict, Dimension # noqa (API import) @@ -26,8 +27,9 @@ from .operation import ElementOperation # noqa (Deprecated API import) from .element import * # noqa (API import) from .element import __all__ as elements_list -from . import util # noqa (API import) -from .util import extension, renderer, output, opts, render, save # noqa (API import) +from .util import (extension, renderer, output, opts, # noqa (API import) + render, save) +from .util.ops import dim # noqa (API import) # Suppress warnings generated by NumPy in matplotlib # Expected to be fixed in next matplotlib release diff --git a/holoviews/plotting/bokeh/element.py b/holoviews/plotting/bokeh/element.py index eb20ec7a31..da427acd98 100644 --- a/holoviews/plotting/bokeh/element.py +++ b/holoviews/plotting/bokeh/element.py @@ -32,7 +32,7 @@ from ..util import dynamic_update, process_cmap, color_intervals from .plot import BokehPlot, TOOLS from .styles import ( - legend_dimensions, line_properties, mpl_to_bokeh, rgba_tuple, + legend_dimensions, line_properties, markers, mpl_to_bokeh, rgba_tuple, text_properties, validate ) from .util import ( @@ -667,7 +667,9 @@ def _apply_ops(self, element, source, ranges, style, group=None): if not isinstance(v, op) or (group is not None and not k.startswith(group)): continue dname = v.dimension.name - if (dname not in element and v.dimension not in self.overlay_dims and + if k == 'marker' and dname in markers: + continue + elif (dname not in element and v.dimension not in self.overlay_dims and not (isinstance(element, Graph) and v.dimension in element.nodes)): new_style.pop(k) self.warning('Specified %s op %r could not be applied, %s dimension ' @@ -1257,7 +1259,7 @@ def _get_colormapper(self, dim, element, ranges, style, factors=None, colors=Non cmap = colors or style.pop('cmap', 'viridis') nan_colors = {k: rgba_tuple(v) for k, v in self.clipping_colors.items()} if isinstance(cmap, dict): - if not factors: + if factors is None: factors = list(cmap) palette = [cmap.get(f, nan_colors.get('NaN', self._default_nan)) for f in factors] factors = [dim.pprint_value(f) for f in factors] diff --git a/holoviews/plotting/bokeh/path.py b/holoviews/plotting/bokeh/path.py index c956a6dc5f..39539c4f0b 100644 --- a/holoviews/plotting/bokeh/path.py +++ b/holoviews/plotting/bokeh/path.py @@ -49,7 +49,12 @@ def _get_hover_data(self, data, element): def get_data(self, element, ranges, style): - cdim = element.get_dimension(self.color_index) + color = style.get('color', None) + cdim = None + if isinstance(color, util.basestring): + cdim = element.get_dimension(color) + if cdim is None: + cdim = element.get_dimension(self.color_index) inds = (1, 0) if self.invert_axes else (0, 1) mapping = dict(self._mapping) if not cdim: diff --git a/holoviews/plotting/bokeh/styles.py b/holoviews/plotting/bokeh/styles.py index 12b7627c81..938b06f900 100644 --- a/holoviews/plotting/bokeh/styles.py +++ b/holoviews/plotting/bokeh/styles.py @@ -40,6 +40,8 @@ # Conversion between matplotlib and bokeh markers markers = { + '+': {'marker': 'cross'}, + 'x': {'marker': 'cross', 'angle': np.pi/4}, 's': {'marker': 'square'}, 'd': {'marker': 'diamond'}, '^': {'marker': 'triangle', 'angle': 0}, diff --git a/holoviews/plotting/mpl/chart.py b/holoviews/plotting/mpl/chart.py index f17679019e..82aa2a0b96 100644 --- a/holoviews/plotting/mpl/chart.py +++ b/holoviews/plotting/mpl/chart.py @@ -187,6 +187,8 @@ class AreaPlot(ChartPlot): 'hatch', 'linestyle', 'joinstyle', 'fill', 'capstyle', 'interpolate'] + _no_op_styles = style_opts + _plot_methods = dict(single='fill_between') def get_data(self, element, ranges, style): From d19ca7f4376b0fd1aa496caa49e6b57b2b2f8246 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Sat, 10 Nov 2018 20:14:35 +0000 Subject: [PATCH 022/117] Updated Styling_Plots user guide --- examples/user_guide/Styling_Plots.ipynb | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/examples/user_guide/Styling_Plots.ipynb b/examples/user_guide/Styling_Plots.ipynb index 5288963369..9a7add522e 100644 --- a/examples/user_guide/Styling_Plots.ipynb +++ b/examples/user_guide/Styling_Plots.ipynb @@ -179,8 +179,7 @@ "\n", "## Colormapping\n", "\n", - "Color cycles and styles are useful for categorical plots and when overlaying multiple subsets, but when we want to map data values to a color it is better to use HoloViews' facilities for color mapping. Certain image-like types will apply colormapping automatically; e.g. for ``Image``, ``QuadMesh`` or ``HeatMap`` types the first value dimension is automatically mapped to the color. In other cases the values to colormap can be declared through the ``color_index``, which may reference any dimension by name or by numerical index.\n", - "\n", + "Color cycles and styles are useful for categorical plots and when overlaying multiple subsets, but when we want to map data values to a color it is better to use HoloViews' facilities for color mapping. Certain image-like types will apply colormapping automatically; e.g. for ``Image``, ``QuadMesh`` or ``HeatMap`` types the first value dimension is automatically mapped to the color. In other cases the values to colormap can be declared by mapping a ``color`` style option to an existing dimension.\n", "\n", "#### Named colormaps\n", "\n", @@ -286,7 +285,7 @@ "]\n", "\n", "hv.Path([path], vdims='Wind Speed').options(\n", - " color_index='Wind Speed', color_levels=levels, cmap=colors, line_width=8, colorbar=True, width=450\n", + " color='Wind Speed', color_levels=levels, cmap=colors, line_width=8, colorbar=True, width=450\n", ")" ] }, @@ -335,13 +334,13 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "### Using color_index\n", + "### Colormapping\n", "\n", - "As mentioned above, when plotting elements that do not automatically map colors to certain dimensions, we can use the ``color_index`` option to do so explicitly. This allows colormapping both continuously valued and categorical values.\n", + "As mentioned above, when plotting elements that do not automatically map colors to certain dimensions (e.g. an Image), we can use ``color`` options to do so explicitly. This allows colormapping both continuously valued and categorical values.\n", "\n", "#### Continuous values\n", "\n", - "If we provide a continuous value for the ``color_index``, we have a continuous colormap and can enable a ``colorbar``:" + "If we provide a continuous value for the ``color`` style option along with a continuous colormap, we can also enable a ``colorbar``:" ] }, { @@ -352,7 +351,7 @@ "source": [ "polygons = hv.Polygons([{('x', 'y'): hv.Ellipse(0, 0, (i, i)).array(), 'z': i} for i in range(1, 10)[::-1]], vdims='z')\n", "\n", - "polygons.options(color_index='z', colorbar=True, width=380)" + "polygons.options(color='z', colorbar=True, width=380)" ] }, { @@ -379,7 +378,7 @@ " np.random.rand(100), \n", " np.random.choice(list('ABCD'), 100)), vdims='Category')\n", "\n", - "categorical_points.sort('Category').options(color_index='Category', cmap='Category20', size=5)" + "categorical_points.sort('Category').options(color='Category', cmap='Category20', size=5)" ] }, { @@ -399,7 +398,7 @@ "source": [ "explicit_mapping = {'A': 'blue', 'B': 'red', 'C': 'green', 'D': 'purple'}\n", "\n", - "categorical_points.sort('Category').options(color_index='Category', cmap=explicit_mapping, size=5)" + "categorical_points.sort('Category').options(color='Category', cmap=explicit_mapping, size=5)" ] }, { From 5db683a90e5cf01e72f16190cee5cb583f2501b3 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Sat, 10 Nov 2018 20:16:26 +0000 Subject: [PATCH 023/117] Updated Geometry Data notebook --- examples/user_guide/Geometry_Data.ipynb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/examples/user_guide/Geometry_Data.ipynb b/examples/user_guide/Geometry_Data.ipynb index a70ee7abea..ef200b99f5 100644 --- a/examples/user_guide/Geometry_Data.ipynb +++ b/examples/user_guide/Geometry_Data.ipynb @@ -55,7 +55,7 @@ "outputs": [], "source": [ "hv.Path([{'x': [1, 2, 3, 4, 5], 'y': [0, 0, 1, 1, 2], 'value': 0},\n", - " {'x': [5, 4, 3, 2, 1], 'y': [2, 2, 1, 1, 0], 'value': 1}], vdims='value').options(padding=0.1, color_index='value')" + " {'x': [5, 4, 3, 2, 1], 'y': [2, 2, 1, 1, 0], 'value': 1}], vdims='value').options(padding=0.1, color='value')" ] }, { @@ -80,7 +80,7 @@ "source": [ "hv.Path([{'x': [1, 2, 3, 4, 5, np.nan, 5, 4, 3, 2, 1],\n", " 'y': [0, 0, 1, 1, 2, np.nan, 2, 2, 1, 1, 0], 'value': 0}],\n", - " vdims='value').options(padding=0.1, color_index='value')" + " vdims='value').options(padding=0.1, color='value')" ] }, { @@ -107,7 +107,7 @@ "ys = np.sin(b * vs)\n", "\n", "hv.Path([{'x': xs, 'y': ys, 'value': vs}], vdims='value').options(\n", - " color_index='value', padding=0.1, cmap='hsv'\n", + " color='value', padding=0.1, cmap='hsv'\n", ")" ] }, @@ -125,7 +125,7 @@ "outputs": [], "source": [ "hv.Contours([{'x': xs, 'y': ys, 'value': np.ones(200)}], vdims='value').options(\n", - " color_index='value', padding=0.1\n", + " color='value', padding=0.1\n", ")" ] }, From 5459a168a91b280534e1d643c5f04fb3a1b17ee1 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Sat, 10 Nov 2018 20:22:43 +0000 Subject: [PATCH 024/117] Added bin and cat op functions --- holoviews/util/ops.py | 35 +++++++++++++++++++++++++++++++++-- 1 file changed, 33 insertions(+), 2 deletions(-) diff --git a/holoviews/util/ops.py b/holoviews/util/ops.py index 16445b12e5..4b9705b0bb 100644 --- a/holoviews/util/ops.py +++ b/holoviews/util/ops.py @@ -2,7 +2,7 @@ import numpy as np from ..core.dimension import Dimension -from ..core.util import basestring +from ..core.util import basestring, unique_iterator, isfinite from ..element import Graph @@ -11,9 +11,40 @@ def norm_fn(values, min=None, max=None): max = np.max(values) if max is None else max return (values - min) / (max-min) + +def bin_fn(values, bins, labels=None): + bins = np.asarray(bins) + if labels is None: + labels = (bins[:-1] + np.diff(bins)/2.) + else: + labels = np.asarray(labels) + dtype = 'float' if labels.dtype.kind == 'f' else 'O' + binned = np.full_like(values, (np.nan if dtype == 'f' else None), dtype=dtype) + for lower, upper, label in zip(bins[:-1], bins[1:], labels): + condition = (values > lower) & (values <= upper) + binned[np.where(condition)[0]] = label + return binned + + +def cat_fn(values, categories, empty=None): + uniq_cats = list(unique_iterator(values)) + cats = [] + for c in values: + if isinstance(categories, list): + cat_ind = uniq_cats.index(c) + if cat_ind < len(categories): + cat = categories[cat_ind] + else: + cat = empty + else: + cat = categories.get(c, empty) + cats.append(cat) + return np.asarray(cats) + + class op(object): - _op_registry = {'norm': norm_fn} + _op_registry = {'norm': norm_fn, 'bin': bin_fn, 'cat': cat_fn} def __init__(self, obj, fn=None, other=None, reverse=False, **kwargs): ops = [] From 2e9c2166667db5ad1ac1c23599f739312f5999d5 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Sat, 10 Nov 2018 20:22:57 +0000 Subject: [PATCH 025/117] Fixed op range resolution --- holoviews/util/ops.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/holoviews/util/ops.py b/holoviews/util/ops.py index 4b9705b0bb..e9c5cf385e 100644 --- a/holoviews/util/ops.py +++ b/holoviews/util/ops.py @@ -115,9 +115,9 @@ def eval(self, dataset, ranges={}): args = (other, data) if o['reverse'] else (data, other) else: args = (data,) - drange = ranges.get(self.dimension.name) + drange = ranges.get(self.dimension.name, {}) drange = drange.get('combined', drange) - if o['fn'] == norm_fn and drange is not None: + if o['fn'] == norm_fn and drange != {}: data = o['fn'](data, *drange) else: data = o['fn'](*args, **o['kwargs']) From 24873f6688dd340b55468ca081dd2282d3208604 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Sat, 10 Nov 2018 22:51:34 +0000 Subject: [PATCH 026/117] Fixes for CompositeElementPlot --- holoviews/plotting/bokeh/element.py | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/holoviews/plotting/bokeh/element.py b/holoviews/plotting/bokeh/element.py index da427acd98..c355c1a713 100644 --- a/holoviews/plotting/bokeh/element.py +++ b/holoviews/plotting/bokeh/element.py @@ -658,6 +658,7 @@ def _init_glyph(self, plot, mapping, properties): def _apply_ops(self, element, source, ranges, style, group=None): new_style = dict(style) + prefix = group+'_' if group else '' for k, v in dict(style).items(): if isinstance(v, util.basestring): if v in element: @@ -705,7 +706,7 @@ def _apply_ops(self, element, source, ranges, style, group=None): if np.isscalar(val): key = val else: - key = k + key = {'field': k} source.data[k] = val # If color is not valid colorspec add colormapper @@ -719,18 +720,19 @@ def _apply_ops(self, element, source, ranges, style, group=None): style, name=dname+'_color_mapper', **kwargs) key = {'field': k, 'transform': cmapper} + new_style[k] = key + + for style, value in list(new_style.items()): # If mapped to color/alpha override static fill/line style for s in ('alpha', 'color'): - if s != k or k not in source.data: + if prefix+s != style or style not in source.data: continue - fill_style = new_style.get('fill_'+s) + fill_style = new_style.get(prefix+'fill_'+s) if fill_style and validate(s, fill_style): - new_style.pop('fill_'+s) - line_style = new_style.get('line_'+s) + new_style.pop(prefix+'fill_'+s) + line_style = new_style.get(prefix+'line_'+s) if line_style and validate(s, line_style): - new_style.pop('line_'+s) - - new_style[k] = key + new_style.pop(prefix+'line_'+s) return new_style @@ -1061,6 +1063,7 @@ def _init_glyphs(self, plot, element, ranges, source, data=None, mapping=None, s style_group = self._style_groups.get('_'.join(key.split('_')[:-1])) properties = self._glyph_properties(plot, element, source, ranges, style, style_group) properties = self._process_properties(key, properties, mapping.get(key, {})) + with abbreviated_exception(): renderer, glyph = self._init_glyph(plot, mapping.get(key, {}), properties, key) self.handles[key+'_glyph'] = glyph @@ -1340,7 +1343,7 @@ def _get_cmapper_opts(self, low, high, factors, colors): else: colormapper = CategoricalColorMapper factors = decode_bytes(factors) - opts = dict(factors=factors) + opts = dict(factors=list(factors)) if 'NaN' in colors: opts['nan_color'] = colors['NaN'] return colormapper, opts From 3484fa72434976c621d46ae92fd32579f43071e4 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Sat, 10 Nov 2018 22:51:48 +0000 Subject: [PATCH 027/117] Disabled style mapping for Rasters --- holoviews/plotting/bokeh/raster.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/holoviews/plotting/bokeh/raster.py b/holoviews/plotting/bokeh/raster.py index d560c9185f..a9e94e57c4 100644 --- a/holoviews/plotting/bokeh/raster.py +++ b/holoviews/plotting/bokeh/raster.py @@ -16,6 +16,9 @@ class RasterPlot(ColorbarPlot): Whether to show legend for the plot.""") style_opts = ['cmap', 'alpha'] + + _no_op_styles = style_opts + _plot_methods = dict(single='image') def _hover_opts(self, element): @@ -74,6 +77,9 @@ def get_data(self, element, ranges, style): class RGBPlot(ElementPlot): style_opts = ['alpha'] + + _no_op_styles = style_opts + _plot_methods = dict(single='image_rgba') def get_data(self, element, ranges, style): @@ -130,9 +136,12 @@ class QuadMeshPlot(ColorbarPlot): show_legend = param.Boolean(default=False, doc=""" Whether to show legend for the plot.""") - _plot_methods = dict(single='quad') style_opts = ['cmap', 'color'] + line_properties + fill_properties + _no_op_styles = style_opts + + _plot_methods = dict(single='quad') + def get_data(self, element, ranges, style): x, y, z = element.dimensions()[:3] if self.invert_axes: x, y = y, x @@ -182,7 +191,7 @@ def get_data(self, element, ranges, style): else: xc, yc = (element.interface.coords(element, x, edges=True, ordered=True), element.interface.coords(element, y, edges=True, ordered=True)) - + x0, y0 = cartesian_product([xc[:-1], yc[:-1]], copy=True) x1, y1 = cartesian_product([xc[1:], yc[1:]], copy=True) zvals = zdata.flatten() if self.invert_axes else zdata.T.flatten() From f287c5ff0f2239981a93b96058bbc13dc706ac48 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Sat, 10 Nov 2018 22:52:30 +0000 Subject: [PATCH 028/117] Enabled style mapping for stats plots --- holoviews/plotting/bokeh/stats.py | 45 ++++++++++++------------------- 1 file changed, 17 insertions(+), 28 deletions(-) diff --git a/holoviews/plotting/bokeh/stats.py b/holoviews/plotting/bokeh/stats.py index c92972ab76..37674a5f6e 100644 --- a/holoviews/plotting/bokeh/stats.py +++ b/holoviews/plotting/bokeh/stats.py @@ -8,6 +8,7 @@ from ...core.dimension import Dimension from ...core.ndmapping import sorted_context +from ...core.options import abbreviated_exception from ...core.util import (basestring, dimension_sanitizer, wrap_tuple, unique_iterator, isfinite) from ...operation.stats import univariate_kde @@ -96,7 +97,9 @@ def _get_axis_labels(self, *args, **kwargs): return xlabel, ylabel, None def _glyph_properties(self, plot, element, source, ranges, style, group=None): - new_style = self._apply_ops(element, source, ranges, style, group) + element = element.aggregate(function=np.mean) + with abbreviated_exception(): + new_style = self._apply_ops(element, source, ranges, style, group) properties = dict(new_style, source=source) if self.show_legend and not element.kdims: properties['legend'] = element.label @@ -259,20 +262,6 @@ def get_data(self, element, ranges, style): r2_data[dimension_sanitizer(cdim.name)] = factors factors = list(unique_iterator(factors)) - # Get colors and define categorical colormapper - cname = dimension_sanitizer(cdim.name) - cmap = style.get('cmap') - if cmap is None: - cycle_style = self.lookup_options(element, 'style') - styles = cycle_style.max_cycles(len(factors)) - colors = [styles[i].get('box_color', styles[i]['box_fill_color']) - for i in range(len(factors))] - colors = [rgb2hex(c) if isinstance(c, tuple) else c for c in colors] - else: - colors = None - mapper = self._get_colormapper(cdim, element, ranges, style, factors, colors) - vbar_map['fill_color'] = {'field': cname, 'transform': mapper} - vbar2_map['fill_color'] = {'field': cname, 'transform': mapper} if self.show_legend: vbar_map['legend'] = cdim.name @@ -305,16 +294,17 @@ class ViolinPlot(BoxWhiskerPlot): Relative width of the violin""") # Map each glyph to a style group - _style_groups = {'patch': 'violin', 'segment': 'stats', 'vbar': 'box', + _style_groups = {'patches': 'violin', 'segment': 'stats', 'vbar': 'box', 'scatter': 'median', 'hbar': 'box'} - _draw_order = ['patch', 'segment', 'vbar', 'hbar', 'circle', 'scatter'] + _draw_order = ['patches', 'segment', 'vbar', 'hbar', 'circle', 'scatter'] style_opts = ([glyph+p for p in fill_properties+line_properties for glyph in ('violin_', 'box_')] + ['stats_'+p for p in line_properties] + ['_'.join([glyph, p]) for p in ('color', 'alpha') - for glyph in ('box', 'violin', 'stats', 'median')]) + for glyph in ('box', 'violin', 'stats', 'median')] + + ['cmap']) _stat_fns = [partial(np.percentile, q=q) for q in [25, 50, 75]] @@ -331,7 +321,7 @@ def _kde_data(self, el, key, **kwargs): ys = (ys/ys.max())*(self.violin_width/2.) if len(ys) else [] ys = [key+(sign*y,) for sign, vs in ((-1, ys), (1, ys[::-1])) for y in vs] xs = np.concatenate([xs, xs[::-1]]) - kde = {'x': xs, 'y': ys} + kde = {'ys': xs, 'xs': ys} bars, segments, scatter = defaultdict(list), defaultdict(list), {} values = el.dimension_values(vdim) @@ -382,7 +372,7 @@ def get_data(self, element, ranges, style): if self.invert_axes: bar_map = {'y': 'x', 'left': 'bottom', 'right': 'top', 'height': 0.1} - kde_map = {'x': 'x', 'y': 'y'} + kde_map = {'xs': 'ys', 'ys': 'xs'} if self.inner == 'box': seg_map = {'x0': 'y0', 'x1': 'y1', 'y0': 'x', 'y1': 'x'} else: @@ -392,7 +382,7 @@ def get_data(self, element, ranges, style): else: bar_map = {'x': 'x', 'bottom': 'bottom', 'top': 'top', 'width': 0.1} - kde_map = {'x': 'y', 'y': 'x'} + kde_map = {'xs': 'xs', 'ys': 'ys'} if self.inner == 'box': seg_map = {'x0': 'x', 'x1': 'x', 'y0': 'y0', 'y1': 'y1'} else: @@ -403,11 +393,10 @@ def get_data(self, element, ranges, style): elstyle = self.lookup_options(element, 'style') kwargs = {'bandwidth': self.bandwidth, 'cut': self.cut} - data, mapping = {}, {} - seg_data, bar_data, scatter_data = (defaultdict(list) for i in range(3)) + mapping, data = {}, {} + patches_data, seg_data, bar_data, scatter_data = (defaultdict(list) for i in range(4)) for i, (key, g) in enumerate(groups.items()): key = decode_bytes(key) - gkey = 'patch_%d'%i kde, segs, bars, scatter = self._kde_data(g, key, **kwargs) for k, v in segs.items(): seg_data[k] += v @@ -415,11 +404,11 @@ def get_data(self, element, ranges, style): bar_data[k] += v for k, v in scatter.items(): scatter_data[k].append(v) - data[gkey] = kde - patch_style = {k[7:]: v for k, v in elstyle[i].items() - if k.startswith('violin')} - mapping[gkey] = dict(kde_map, **patch_style) + for k, v in kde.items(): + patches_data[k].append(v) + data['patches_1'] = patches_data + mapping['patches_1'] = kde_map if seg_data: data['segment_1'] = {k: v if isinstance(v[0], tuple) else np.array(v) for k, v in seg_data.items()} From 4b6cc372b6b9a89a60fd8988fb9000deb487d30c Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Sat, 10 Nov 2018 22:57:41 +0000 Subject: [PATCH 029/117] Improved op transforms to allow construction from tuple --- holoviews/util/ops.py | 39 +++++++++++++++++++++++++++++++++++++-- 1 file changed, 37 insertions(+), 2 deletions(-) diff --git a/holoviews/util/ops.py b/holoviews/util/ops.py index e9c5cf385e..77fea3a519 100644 --- a/holoviews/util/ops.py +++ b/holoviews/util/ops.py @@ -1,4 +1,6 @@ import operator +from itertools import zip_longest + import numpy as np from ..core.dimension import Dimension @@ -42,9 +44,18 @@ def cat_fn(values, categories, empty=None): return np.asarray(cats) +def str_fn(values): + return np.asarray([str(v) for v in values]) + + +def int_fn(values): + return values.astype(int) + + class op(object): - _op_registry = {'norm': norm_fn, 'bin': bin_fn, 'cat': cat_fn} + _op_registry = {'norm': norm_fn, 'bin': bin_fn, 'cat': cat_fn, + str: str_fn, int: int_fn} def __init__(self, obj, fn=None, other=None, reverse=False, **kwargs): ops = [] @@ -55,7 +66,7 @@ def __init__(self, obj, fn=None, other=None, reverse=False, **kwargs): else: self.dimension = obj.dimension ops = obj.ops - if isinstance(fn, str): + if isinstance(fn, str) or fn in self._op_registry: fn = self._op_registry.get(fn) if fn is None: raise ValueError('Operation function %s not found' % fn) @@ -64,6 +75,30 @@ def __init__(self, obj, fn=None, other=None, reverse=False, **kwargs): 'reverse': reverse}] self.ops = ops + @classmethod + def resolve_spec(cls, op_spec): + """ + Converts an op spec, i.e. a string or tuple declaring an op + or a nested spec of ops into an op instance. + """ + if isinstance(op_spec, basestring): + return cls(op_spec) + elif isinstance(op_spec, tuple): + combined = zip_longest(op_spec, (None, None, None, {})) + obj, fn, other, kwargs = (o2 if o1 is None else o1 for o1, o2 in combined) + if isinstance(obj, tuple): + obj = cls.resolve_spec(obj) + return cls(obj, fn, other, **kwargs) + return op_spec + + @classmethod + def register(cls, key, function): + """ + Register a custom op transform function which can from then + on be referenced by the key. + """ + self._op_registry[name] = function + # Unary operators def __abs__(self): return op(self, operator.abs) def __neg__(self): return op(self, operator.neg) From 92927ea09d47a8b75259cdcd656ea7a9cb3eda0e Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Sun, 11 Nov 2018 15:11:03 +0000 Subject: [PATCH 030/117] Finished cleaning up bokeh Scatter/Point plots, tests, docs --- .../reference/elements/bokeh/Points.ipynb | 10 +-- .../reference/elements/bokeh/Scatter.ipynb | 10 +-- holoviews/plotting/bokeh/chart.py | 8 ++- holoviews/plotting/bokeh/element.py | 12 +++- holoviews/plotting/bokeh/styles.py | 4 +- .../tests/plotting/bokeh/testpointplot.py | 64 ++++++++++++++----- 6 files changed, 78 insertions(+), 30 deletions(-) diff --git a/examples/reference/elements/bokeh/Points.ipynb b/examples/reference/elements/bokeh/Points.ipynb index c6fc7da8e6..6b6037eba0 100644 --- a/examples/reference/elements/bokeh/Points.ipynb +++ b/examples/reference/elements/bokeh/Points.ipynb @@ -28,7 +28,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "The ``Points`` element visualizes as markers placed in a space of two independent variables, traditionally denoted *x* and *y*. In HoloViews, the names ``'x'`` and ``'y'`` are used as the default ``key_dimensions`` of the element. We can see this from the default axis labels when visualizing a simple ``Points`` element:" + "The ``Points`` element visualizes as markers placed in a space of two independent variables, traditionally denoted *x* and *y*. In HoloViews, the names ``'x'`` and ``'y'`` are used as the default key dimensions (``kdims``) of the element. We can see this from the default axis labels when visualizing a simple ``Points`` element:" ] }, { @@ -47,7 +47,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Here both the random *x* values and random *y* values are *both* considered to be the 'data' with no dependency between them (compare this to how [``Scatter``](./Scatter.ipynb) elements are defined). You can think of ``Points`` as simply marking positions in some two-dimensional space that can be sliced by specifying a 2D region-of-interest:" + "Here the random *x* values and random *y* values are *both* considered to be the 'data' with no dependency between them (compare this to how [``Scatter``](./Scatter.ipynb) elements are defined). You can think of ``Points`` as simply marking positions in some two-dimensional space that can be sliced by specifying a 2D region-of-interest:" ] }, { @@ -64,7 +64,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Although the simplest ``Points`` element simply mark positions in a two-dimensional space without any associated value this doesn't mean value dimensions aren't supported. Here is an example with two additional quantities for each point, declared as the ``value_dimension``s *z* and α visualized as the color and size of the dots, respectively:" + "Although the simplest ``Points`` element simply mark positions in a two-dimensional space without any associated value this doesn't mean value dimensions (``vdims``) aren't supported. Here is an example with two additional quantities for each point, declared as the ``vdims`` ``'z'`` and ``'size'`` visualized as the color and size of the dots, respectively:" ] }, { @@ -73,12 +73,12 @@ "metadata": {}, "outputs": [], "source": [ - "%%opts Points [color_index=2 size_index=3 scaling_factor=50]\n", + "%%opts Points (color='z' size=op('size')*20)\n", "np.random.seed(10)\n", "data = np.random.rand(100,4)\n", "\n", "points = hv.Points(data, vdims=['z', 'size'])\n", - "points + points[0.3:0.7, 0.3:0.7].hist()" + "points + points[0.3:0.7, 0.3:0.7].hist('z')" ] }, { diff --git a/examples/reference/elements/bokeh/Scatter.ipynb b/examples/reference/elements/bokeh/Scatter.ipynb index 5ae411083b..ccb9e2b129 100644 --- a/examples/reference/elements/bokeh/Scatter.ipynb +++ b/examples/reference/elements/bokeh/Scatter.ipynb @@ -28,7 +28,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "The ``Scatter`` element visualizes as markers placed in a space of one independent variable, traditionally denoted as *x*, against a dependent variable, traditonally denoted as *y*. In HoloViews, the name ``'x'`` is the default dimension name used in the ``key_dimensions`` and ``'y'`` is the default dimension name used in the ``value_dimensions``. We can see this from the default axis labels when visualizing a simple ``Scatter`` element:" + "The ``Scatter`` element visualizes as markers placed in a space of one independent variable, traditionally denoted as *x*, against a dependent variable, traditionally denoted as *y*. In HoloViews, the name ``'x'`` is the default dimension name used in the key dimensions (``kdims``) and ``'y'`` is the default dimension name used in the value dimensions (``vdims``). We can see this from the default axis labels when visualizing a simple ``Scatter`` element:" ] }, { @@ -64,7 +64,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "A ``Scatter`` element must always have at least one value dimension but that doesn't mean additional value dimensions aren't supported. Here is an example with two additional quantities for each point, declared as the ``value_dimension``s *z* and α visualized as the color and size of the dots, respectively:" + "A ``Scatter`` element must always have at least one value dimension but that doesn't mean additional value dimensions aren't supported. Here is an example with two additional quantities for each point, declared as the ``vdims`` ``'z'`` and ``'size'`` visualized as the color and size of the dots, respectively:" ] }, { @@ -73,19 +73,19 @@ "metadata": {}, "outputs": [], "source": [ - "%%opts Scatter [color_index=2 size_index=3 scaling_factor=50]\n", + "%%opts Scatter (color='z' size=op('size')*20)\n", "np.random.seed(10)\n", "data = np.random.rand(100,4)\n", "\n", "scatter = hv.Scatter(data, vdims=['y', 'z', 'size'])\n", - "scatter + scatter[0.3:0.7, 0.3:0.7].hist()" + "scatter + scatter[0.3:0.7, 0.3:0.7].hist('z')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "In the right subplot, the ``hist`` method is used to show the distribution of samples along our first value dimension, (*y*)." + "In the right subplot, the ``hist`` method is used to show the distribution of samples along our first value dimension, (``'y'``)." ] }, { diff --git a/holoviews/plotting/bokeh/chart.py b/holoviews/plotting/bokeh/chart.py index 72a067e846..0fcb6f6a4a 100644 --- a/holoviews/plotting/bokeh/chart.py +++ b/holoviews/plotting/bokeh/chart.py @@ -10,6 +10,7 @@ from ...core.util import max_range, basestring, dimension_sanitizer, isfinite, range_pad from ...element import Bars from ...operation import interpolate_curve +from ...util.ops import op from ..util import compute_sizes, get_min_distance, dim_axis_label, get_axis_padding from .element import ElementPlot, ColorbarPlot, LegendPlot from .styles import (expand_batched_style, line_properties, fill_properties, @@ -53,11 +54,16 @@ class PointPlot(LegendPlot, ColorbarPlot): def _get_size_data(self, element, ranges, style): data, mapping = {}, {} sdim = element.get_dimension(self.size_index) + ms = style.get('size', np.sqrt(6)) + if sdim and ((isinstance(ms, basestring) and ms in element) or isinstance(ms, op)): + self.warning("Cannot declare style mapping for 'size' option " + "and declare a size_index, ignoring the size_index.") + sdim = None if not sdim or self.static_source: return data, mapping map_key = 'size_' + sdim.name - ms = style.get('size', np.sqrt(6))**2 + ms = ms**2 sizes = element.dimension_values(self.size_index) sizes = compute_sizes(sizes, self.size_fn, self.scaling_factor, diff --git a/holoviews/plotting/bokeh/element.py b/holoviews/plotting/bokeh/element.py index c355c1a713..806f594410 100644 --- a/holoviews/plotting/bokeh/element.py +++ b/holoviews/plotting/bokeh/element.py @@ -719,7 +719,6 @@ def _apply_ops(self, element, source, ranges, style, group=None): cmapper = self._get_colormapper(v.dimension, element, ranges, style, name=dname+'_color_mapper', **kwargs) key = {'field': k, 'transform': cmapper} - new_style[k] = key for style, value in list(new_style.items()): @@ -1236,9 +1235,12 @@ def _get_colormapper(self, dim, element, ranges, style, factors=None, colors=Non # and then only updated if dim is None and colors is None: return None + + # Attempt to find matching colormapper on the adjoined plot if self.adjoined: + dim_name = dim.name+'_'+name cmappers = self.adjoined.traverse(lambda x: (x.handles.get('color_dim'), - x.handles.get(name))) + x.handles.get(name, x.handles.get(dim_name)))) cmappers = [cmap for cdim, cmap in cmappers if cdim == dim] if cmappers: cmapper = cmappers[0] @@ -1301,6 +1303,12 @@ def _get_color_data(self, element, ranges, style, name='color', factors=None, co int_categories=False): data, mapping = {}, {} cdim = element.get_dimension(self.color_index) + color = style.get(name, None) + if cdim and ((isinstance(color, util.basestring) and color in element) or isinstance(color, op)): + self.warning("Cannot declare style mapping for '%s' option " + "and declare a color_index, ignoring the color_index." + % name) + cdim = None if not cdim: return data, mapping diff --git a/holoviews/plotting/bokeh/styles.py b/holoviews/plotting/bokeh/styles.py index 938b06f900..a86a5c5552 100644 --- a/holoviews/plotting/bokeh/styles.py +++ b/holoviews/plotting/bokeh/styles.py @@ -63,7 +63,9 @@ def mpl_to_bokeh(properties): """ new_properties = {} for k, v in properties.items(): - if k == 's': + if isinstance(v, dict): + new_properties[k] = v + elif k == 's': new_properties['size'] = v elif k == 'marker': new_properties.update(markers.get(v, {'marker': v})) diff --git a/holoviews/tests/plotting/bokeh/testpointplot.py b/holoviews/tests/plotting/bokeh/testpointplot.py index cc1ff2b5ae..524ad8e3fa 100644 --- a/holoviews/tests/plotting/bokeh/testpointplot.py +++ b/holoviews/tests/plotting/bokeh/testpointplot.py @@ -333,8 +333,8 @@ def test_point_color_op(self): cds = plot.handles['cds'] glyph = plot.handles['glyph'] self.assertEqual(cds.data['color'], np.array(['#000', '#F00', '#0F0'])) - self.assertEqual(glyph.fill_color, 'color') - self.assertEqual(glyph.line_color, 'color') + self.assertEqual(glyph.fill_color, {'field': 'color'}) + self.assertEqual(glyph.line_color, {'field': 'color'}) def test_point_linear_color_op(self): points = Points([(0, 0, 0), (0, 1, 1), (0, 2, 2)], @@ -358,7 +358,7 @@ def test_point_categorical_color_op(self): glyph = plot.handles['glyph'] cmapper = plot.handles['color_color_mapper'] self.assertTrue(cmapper, CategoricalColorMapper) - self.assertEqual(cmapper.factors, np.array(['A', 'B', 'C'])) + self.assertEqual(cmapper.factors, ['A', 'B', 'C']) self.assertEqual(cds.data['color'], np.array(['A', 'B', 'C'])) self.assertEqual(glyph.fill_color, {'field': 'color', 'transform': cmapper}) self.assertEqual(glyph.line_color, {'field': 'color', 'transform': cmapper}) @@ -370,8 +370,8 @@ def test_point_line_color_op(self): cds = plot.handles['cds'] glyph = plot.handles['glyph'] self.assertEqual(cds.data['line_color'], np.array(['#000', '#F00', '#0F0'])) - self.assertNotEqual(glyph.fill_color, 'line_color') - self.assertEqual(glyph.line_color, 'line_color') + self.assertNotEqual(glyph.fill_color, {'field': 'line_color'}) + self.assertEqual(glyph.line_color, {'field': 'line_color'}) def test_point_fill_color_op(self): points = Points([(0, 0, '#000'), (0, 1, '#F00'), (0, 2, '#0F0')], @@ -380,8 +380,8 @@ def test_point_fill_color_op(self): cds = plot.handles['cds'] glyph = plot.handles['glyph'] self.assertEqual(cds.data['fill_color'], np.array(['#000', '#F00', '#0F0'])) - self.assertEqual(glyph.fill_color, 'fill_color') - self.assertNotEqual(glyph.line_color, 'fill_color') + self.assertEqual(glyph.fill_color, {'field': 'fill_color'}) + self.assertNotEqual(glyph.line_color, {'field': 'fill_color'}) def test_point_angle_op(self): points = Points([(0, 0, 0), (0, 1, 45), (0, 2, 90)], @@ -390,7 +390,7 @@ def test_point_angle_op(self): cds = plot.handles['cds'] glyph = plot.handles['glyph'] self.assertEqual(cds.data['angle'], np.array([0, 0.785398, 1.570796])) - self.assertEqual(glyph.angle, 'angle') + self.assertEqual(glyph.angle, {'field': 'angle'}) def test_point_alpha_op(self): points = Points([(0, 0, 0), (0, 1, 0.2), (0, 2, 0.7)], @@ -399,7 +399,7 @@ def test_point_alpha_op(self): cds = plot.handles['cds'] glyph = plot.handles['glyph'] self.assertEqual(cds.data['alpha'], np.array([0, 0.2, 0.7])) - self.assertEqual(glyph.fill_alpha, 'alpha') + self.assertEqual(glyph.fill_alpha, {'field': 'alpha'}) def test_point_line_alpha_op(self): points = Points([(0, 0, 0), (0, 1, 0.2), (0, 2, 0.7)], @@ -408,8 +408,8 @@ def test_point_line_alpha_op(self): cds = plot.handles['cds'] glyph = plot.handles['glyph'] self.assertEqual(cds.data['line_alpha'], np.array([0, 0.2, 0.7])) - self.assertEqual(glyph.line_alpha, 'line_alpha') - self.assertNotEqual(glyph.fill_alpha, 'line_alpha') + self.assertEqual(glyph.line_alpha, {'field': 'line_alpha'}) + self.assertNotEqual(glyph.fill_alpha, {'field': 'line_alpha'}) def test_point_fill_alpha_op(self): points = Points([(0, 0, 0), (0, 1, 0.2), (0, 2, 0.7)], @@ -418,8 +418,8 @@ def test_point_fill_alpha_op(self): cds = plot.handles['cds'] glyph = plot.handles['glyph'] self.assertEqual(cds.data['fill_alpha'], np.array([0, 0.2, 0.7])) - self.assertNotEqual(glyph.line_alpha, 'fill_alpha') - self.assertEqual(glyph.fill_alpha, 'fill_alpha') + self.assertNotEqual(glyph.line_alpha, {'field': 'fill_alpha'}) + self.assertEqual(glyph.fill_alpha, {'field': 'fill_alpha'}) def test_point_size_op(self): points = Points([(0, 0, 1), (0, 1, 4), (0, 2, 8)], @@ -428,7 +428,7 @@ def test_point_size_op(self): cds = plot.handles['cds'] glyph = plot.handles['glyph'] self.assertEqual(cds.data['size'], np.array([1, 4, 8])) - self.assertEqual(glyph.size, 'size') + self.assertEqual(glyph.size, {'field': 'size'}) def test_point_line_width_op(self): points = Points([(0, 0, 1), (0, 1, 4), (0, 2, 8)], @@ -437,7 +437,7 @@ def test_point_line_width_op(self): cds = plot.handles['cds'] glyph = plot.handles['glyph'] self.assertEqual(cds.data['line_width'], np.array([1, 4, 8])) - self.assertEqual(glyph.line_width, 'line_width') + self.assertEqual(glyph.line_width, {'field': 'line_width'}) def test_point_marker_op(self): points = Points([(0, 0, 'circle'), (0, 1, 'triangle'), (0, 2, 'square')], @@ -446,7 +446,7 @@ def test_point_marker_op(self): cds = plot.handles['cds'] glyph = plot.handles['glyph'] self.assertEqual(cds.data['marker'], np.array(['circle', 'triangle', 'square'])) - self.assertEqual(glyph.marker, 'marker') + self.assertEqual(glyph.marker, {'field': 'marker'}) def test_op_ndoverlay_value(self): markers = ['circle', 'triangle'] @@ -455,3 +455,35 @@ def test_op_ndoverlay_value(self): for subplot, glyph_type, marker in zip(plot.subplots.values(), [Scatter, Scatter], markers): self.assertIsInstance(subplot.handles['glyph'], glyph_type) self.assertEqual(subplot.handles['glyph'].marker, marker) + + def test_point_color_index_color_clash(self): + points = Points([(0, 0, 0), (0, 1, 1), (0, 2, 2)], + vdims='color').options(color='color', color_index='color') + with ParamLogStream() as log: + plot = bokeh_renderer.get_plot(points) + log_msg = log.stream.read() + warning = ("%s: Cannot declare style mapping for 'color' option " + "and declare a color_index, ignoring the color_index.\n" + % plot.name) + self.assertEqual(log_msg, warning) + + def test_point_color_index_color_no_clash(self): + points = Points([(0, 0, 0), (0, 1, 1), (0, 2, 2)], + vdims='color').options(fill_color='color', color_index='color') + plot = bokeh_renderer.get_plot(points) + glyph = plot.handles['glyph'] + cmapper = plot.handles['color_color_mapper'] + cmapper2 = plot.handles['color_mapper'] + self.assertEqual(glyph.fill_color, {'field': 'fill_color', 'transform': cmapper}) + self.assertEqual(glyph.line_color, {'field': 'color', 'transform': cmapper2}) + + def test_point_size_index_size_clash(self): + points = Points([(0, 0, 0), (0, 1, 1), (0, 2, 2)], + vdims='size').options(size='size', size_index='size') + with ParamLogStream() as log: + plot = bokeh_renderer.get_plot(points) + log_msg = log.stream.read() + warning = ("%s: Cannot declare style mapping for 'size' option " + "and declare a size_index, ignoring the size_index.\n" + % plot.name) + self.assertEqual(log_msg, warning) From 637b8e1c2a06f36608235d40950b94da35db98de Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Sun, 11 Nov 2018 15:32:05 +0000 Subject: [PATCH 031/117] Finished cleaning up bokeh Histogram plot, tests, doc --- .../reference/elements/bokeh/Histogram.ipynb | 18 ++++++++++++- .../elements/matplotlib/Histogram.ipynb | 16 ++++++++++++ .../tests/plotting/bokeh/testhistogramplot.py | 26 +++++++++---------- 3 files changed, 46 insertions(+), 14 deletions(-) diff --git a/examples/reference/elements/bokeh/Histogram.ipynb b/examples/reference/elements/bokeh/Histogram.ipynb index 8f2ff4f887..d612d9e5cc 100644 --- a/examples/reference/elements/bokeh/Histogram.ipynb +++ b/examples/reference/elements/bokeh/Histogram.ipynb @@ -48,7 +48,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "The ``Histogram`` Element will also expand evenly sampled bin centers, therefore we can easily cast between a linearly sampled Curve or Scatter and a Histogram." + "The ``Histogram`` Element will also expand evenly sampled bin centers, therefore we can easily cast between a linearly sampled ``Curve`` or ``Scatter`` and a ``Histogram``." ] }, { @@ -63,6 +63,22 @@ "curve + hv.Histogram(curve)" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Like most other elements a ``Histogram`` also supports using ``op`` transforms to map dimensions to visual attributes. To demonstrate this we will use the ``bin`` op to bin the 'y' values into positive and negative values and map those to a 'blue' and 'red' ``fill_color``:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "hv.Histogram(curve).options(fill_color=hv.op('y', 'bin', bins=[-1, 0, 1], labels=['red', 'blue']))" + ] + }, { "cell_type": "markdown", "metadata": {}, diff --git a/examples/reference/elements/matplotlib/Histogram.ipynb b/examples/reference/elements/matplotlib/Histogram.ipynb index e072e4748f..4d3271547f 100644 --- a/examples/reference/elements/matplotlib/Histogram.ipynb +++ b/examples/reference/elements/matplotlib/Histogram.ipynb @@ -63,6 +63,22 @@ "curve + hv.Histogram(curve)" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Like most other elements a ``Histogram`` also supports using ``op`` transforms to map dimensions to visual attributes. To demonstrate this we will use the ``bin`` op to bin the 'y' values into positive and negative values and map those to a 'blue' and 'red' ``fill_color``:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "hv.Histogram(curve).options(color=hv.op('y', 'bin', bins=[-1, 0, 1], labels=['red', 'blue']))" + ] + }, { "cell_type": "markdown", "metadata": {}, diff --git a/holoviews/tests/plotting/bokeh/testhistogramplot.py b/holoviews/tests/plotting/bokeh/testhistogramplot.py index bd36451f1b..655642c6e1 100644 --- a/holoviews/tests/plotting/bokeh/testhistogramplot.py +++ b/holoviews/tests/plotting/bokeh/testhistogramplot.py @@ -161,8 +161,8 @@ def test_histogram_color_op(self): cds = plot.handles['cds'] glyph = plot.handles['glyph'] self.assertEqual(cds.data['color'], np.array(['#000', '#F00', '#0F0'])) - self.assertEqual(glyph.fill_color, 'color') - self.assertEqual(glyph.line_color, 'color') + self.assertEqual(glyph.fill_color, {'field': 'color'}) + self.assertEqual(glyph.line_color, {'field': 'color'}) def test_histogram_linear_color_op(self): histogram = Histogram([(0, 0, 0), (0, 1, 1), (0, 2, 2)], @@ -186,7 +186,7 @@ def test_histogram_categorical_color_op(self): glyph = plot.handles['glyph'] cmapper = plot.handles['color_color_mapper'] self.assertTrue(cmapper, CategoricalColorMapper) - self.assertEqual(cmapper.factors, np.array(['A', 'B', 'C'])) + self.assertEqual(cmapper.factors, ['A', 'B', 'C']) self.assertEqual(cds.data['color'], np.array(['A', 'B', 'C'])) self.assertEqual(glyph.fill_color, {'field': 'color', 'transform': cmapper}) self.assertEqual(glyph.line_color, {'field': 'color', 'transform': cmapper}) @@ -198,8 +198,8 @@ def test_histogram_line_color_op(self): cds = plot.handles['cds'] glyph = plot.handles['glyph'] self.assertEqual(cds.data['line_color'], np.array(['#000', '#F00', '#0F0'])) - self.assertNotEqual(glyph.fill_color, 'line_color') - self.assertEqual(glyph.line_color, 'line_color') + self.assertNotEqual(glyph.fill_color, {'field': 'line_color'}) + self.assertEqual(glyph.line_color, {'field': 'line_color'}) def test_histogram_fill_color_op(self): histogram = Histogram([(0, 0, '#000'), (0, 1, '#F00'), (0, 2, '#0F0')], @@ -208,8 +208,8 @@ def test_histogram_fill_color_op(self): cds = plot.handles['cds'] glyph = plot.handles['glyph'] self.assertEqual(cds.data['fill_color'], np.array(['#000', '#F00', '#0F0'])) - self.assertEqual(glyph.fill_color, 'fill_color') - self.assertNotEqual(glyph.line_color, 'fill_color') + self.assertEqual(glyph.fill_color, {'field': 'fill_color'}) + self.assertNotEqual(glyph.line_color, {'field': 'fill_color'}) def test_histogram_alpha_op(self): histogram = Histogram([(0, 0, 0), (0, 1, 0.2), (0, 2, 0.7)], @@ -218,7 +218,7 @@ def test_histogram_alpha_op(self): cds = plot.handles['cds'] glyph = plot.handles['glyph'] self.assertEqual(cds.data['alpha'], np.array([0, 0.2, 0.7])) - self.assertEqual(glyph.fill_alpha, 'alpha') + self.assertEqual(glyph.fill_alpha, {'field': 'alpha'}) def test_histogram_line_alpha_op(self): histogram = Histogram([(0, 0, 0), (0, 1, 0.2), (0, 2, 0.7)], @@ -227,8 +227,8 @@ def test_histogram_line_alpha_op(self): cds = plot.handles['cds'] glyph = plot.handles['glyph'] self.assertEqual(cds.data['line_alpha'], np.array([0, 0.2, 0.7])) - self.assertEqual(glyph.line_alpha, 'line_alpha') - self.assertNotEqual(glyph.fill_alpha, 'line_alpha') + self.assertEqual(glyph.line_alpha, {'field': 'line_alpha'}) + self.assertNotEqual(glyph.fill_alpha, {'field': 'line_alpha'}) def test_histogram_fill_alpha_op(self): histogram = Histogram([(0, 0, 0), (0, 1, 0.2), (0, 2, 0.7)], @@ -237,8 +237,8 @@ def test_histogram_fill_alpha_op(self): cds = plot.handles['cds'] glyph = plot.handles['glyph'] self.assertEqual(cds.data['fill_alpha'], np.array([0, 0.2, 0.7])) - self.assertNotEqual(glyph.line_alpha, 'fill_alpha') - self.assertEqual(glyph.fill_alpha, 'fill_alpha') + self.assertNotEqual(glyph.line_alpha, {'field': 'fill_alpha'}) + self.assertEqual(glyph.fill_alpha, {'field': 'fill_alpha'}) def test_histogram_line_width_op(self): histogram = Histogram([(0, 0, 1), (0, 1, 4), (0, 2, 8)], @@ -247,7 +247,7 @@ def test_histogram_line_width_op(self): cds = plot.handles['cds'] glyph = plot.handles['glyph'] self.assertEqual(cds.data['line_width'], np.array([1, 4, 8])) - self.assertEqual(glyph.line_width, 'line_width') + self.assertEqual(glyph.line_width, {'field': 'line_width'}) def test_op_ndoverlay_value(self): colors = ['blue', 'red'] From a7055748da45df7c0f5ebe8f5fd9db1cb49668df Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Sun, 11 Nov 2018 16:07:12 +0000 Subject: [PATCH 032/117] Overhauled bokeh BarPlot code, tests, docs --- .../gallery/demos/bokeh/bars_economic.ipynb | 4 +- examples/reference/elements/bokeh/Bars.ipynb | 6 +- holoviews/plotting/bokeh/chart.py | 61 ++++---- holoviews/tests/plotting/bokeh/testbarplot.py | 130 ++++++++++++++++++ 4 files changed, 173 insertions(+), 28 deletions(-) diff --git a/examples/gallery/demos/bokeh/bars_economic.ipynb b/examples/gallery/demos/bokeh/bars_economic.ipynb index 6801997738..25a0fd2fef 100644 --- a/examples/gallery/demos/bokeh/bars_economic.ipynb +++ b/examples/gallery/demos/bokeh/bars_economic.ipynb @@ -53,9 +53,9 @@ "metadata": {}, "outputs": [], "source": [ - "%%opts Bars [stack_index=1 xrotation=90 width=600 show_legend=False tools=['hover']]\n", + "%%opts Bars [stacked=True xrotation=90 width=600 show_legend=False tools=['hover']]\n", "%%opts Bars (color=Cycle('Category20'))\n", - "macro.to.bars([ 'Year', 'Country'], 'Trade', [])" + "macro.to.bars(['Year', 'Country'], 'Trade', [])" ] } ], diff --git a/examples/reference/elements/bokeh/Bars.ipynb b/examples/reference/elements/bokeh/Bars.ipynb index 8ef60f0955..145bcf1c65 100644 --- a/examples/reference/elements/bokeh/Bars.ipynb +++ b/examples/reference/elements/bokeh/Bars.ipynb @@ -30,7 +30,7 @@ "source": [ "The ``Bars`` Element uses bars to show discrete, numerical comparisons across categories. One axis of the chart shows the specific categories being compared and the other axis represents a continuous value.\n", "\n", - "Bars may also be stacked by supplying a second key dimensions representing sub-categories. Therefore the ``Bars`` Element expects a tabular data format with one or two key dimensions and one value dimension. See the [Tabular Datasets](../../../user_guide/07-Tabular_Datasets.ipynb) user guide for supported data formats, which include arrays, pandas dataframes and dictionaries of arrays." + "Bars may also be grouped or stacked by supplying a second key dimension representing sub-categories. Therefore the ``Bars`` Element expects a tabular data format with one or two key dimensions (``kdims``) and one or more value dimensions (``vdims``). See the [Tabular Datasets](../../../user_guide/07-Tabular_Datasets.ipynb) user guide for supported data formats, which include arrays, pandas dataframes and dictionaries of arrays." ] }, { @@ -64,7 +64,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "``Bars`` support stacking just like the ``Area`` element as well as grouping by a second key dimension. To activate grouping and stacking set the ``group_index`` or ``stack_index`` to the dimension name or dimension index:" + "``Bars`` support nested categorical grouping as well as stacking if more than one key dimension is defined, to switch between the two set ``stacked=True/False``:" ] }, { @@ -73,7 +73,7 @@ "metadata": {}, "outputs": [], "source": [ - "%%opts Bars.Grouped [group_index='Group'] Bars.Stacked [stack_index='Group']\n", + "%%opts Bars.Stacked [stacked=True]\n", "from itertools import product\n", "np.random.seed(3)\n", "index, groups = ['A', 'B'], ['a', 'b']\n", diff --git a/holoviews/plotting/bokeh/chart.py b/holoviews/plotting/bokeh/chart.py index 0fcb6f6a4a..227ad7f04b 100644 --- a/holoviews/plotting/bokeh/chart.py +++ b/holoviews/plotting/bokeh/chart.py @@ -700,15 +700,16 @@ class BarPlot(ColorbarPlot, LegendPlot): allow_None=True, doc=""" Index of the dimension from which the color will the drawn""") + stacked = param.Boolean(default=False, doc=""" + Whether the bars should be stacked or grouped.""") + group_index = param.ClassSelector(default=1, class_=(basestring, int), allow_None=True, doc=""" - Index of the dimension in the supplied Bars - Element, which will be laid out into groups.""") + Deprecated; use stacked option instead.""") stack_index = param.ClassSelector(default=None, class_=(basestring, int), allow_None=True, doc=""" - Index of the dimension in the supplied Bars - Element, which will stacked.""") + Deprecated; use stacked option instead.""") style_opts = line_properties + fill_properties + ['width', 'bar_width', 'cmap'] @@ -732,13 +733,12 @@ def get_extents(self, element, ranges, range_type='combined'): for kd in overlay.kdims: ranges[kd.name]['combined'] = overlay.range(kd) - stacked = element.get_dimension(self.stack_index) extents = super(BarPlot, self).get_extents(element, ranges, range_type) xdim = element.kdims[0] ydim = element.vdims[0] # Compute stack heights - if stacked: + if self.stacked: ds = Dataset(element) pos_range = ds.select(**{ydim.name: (0, None)}).aggregate(xdim, function=np.sum).range(ydim) neg_range = ds.select(**{ydim.name: (None, 0)}).aggregate(xdim, function=np.sum).range(ydim) @@ -768,12 +768,14 @@ def _get_factors(self, element): """ Get factors for categorical axes. """ - gdim = element.get_dimension(self.group_index) - if gdim not in element.kdims: - gdim = None - sdim = element.get_dimension(self.stack_index) - if sdim not in element.kdims: - sdim = None + gdim = None + sdim = None + if element.ndims == 1: + pass + elif not self.stacked: + gdim = element.get_dimension(1) + else: + sdim = element.get_dimension(1) xdim, ydim = element.dimensions()[:2] xvals = element.dimension_values(0, False) @@ -797,7 +799,10 @@ def _get_axis_labels(self, *args, **kwargs): if self.batched: element = element.last xlabel = dim_axis_label(element.kdims[0]) - gdim = element.get_dimension(self.group_index) + if element.ndims > 1 and not self.stacked: + gdim = element.get_dimension(1) + else: + gdim = None if gdim and gdim in element.kdims: xlabel = ', '.join([xlabel, dim_axis_label(gdim)]) return (xlabel, dim_axis_label(element.vdims[0]), None) @@ -843,6 +848,10 @@ def _add_color_data(self, ds, ranges, style, cdim, data, mapping, factors, color isinstance(cmapper, CategoricalColorMapper)): mapping['legend'] = cdim.name + if not self.stacked and ds.ndims > 1: + cmapping.pop('legend', None) + mapping.pop('legend', None) + # Merge data and mappings mapping.update(cmapping) for k, cd in cdata.items(): @@ -855,19 +864,25 @@ def _add_color_data(self, ds, ranges, style, cdim, data, mapping, factors, color def get_data(self, element, ranges, style): + if self.stack_index is not None: + self.warning('Bars stack_index plot option is deprecated ' + 'and will be ignored, set stacked=True/False ' + 'instead.') + if self.group_index not in (None, 1): + self.warning('Bars group_index plot option is deprecated ' + 'and will be ignored, set stacked=True/False ' + 'instead.') + # Get x, y, group, stack and color dimensions - grouping = None - group_dim = element.get_dimension(self.group_index) - if group_dim not in element.kdims: - group_dim = None + group_dim, stack_dim = None, None + if element.ndims == 1: + grouping = None + elif self.stacked: + grouping = 'stacked' + stack_dim = element.get_dimension(1) else: grouping = 'grouped' - stack_dim = element.get_dimension(self.stack_index) - if stack_dim not in element.kdims: - stack_dim = None - else: - grouping = 'stacked' - group_dim = None + group_dim = element.get_dimension(1) xdim = element.get_dimension(0) ydim = element.vdims[0] diff --git a/holoviews/tests/plotting/bokeh/testbarplot.py b/holoviews/tests/plotting/bokeh/testbarplot.py index d1a9d60d88..d3ba4d35c9 100644 --- a/holoviews/tests/plotting/bokeh/testbarplot.py +++ b/holoviews/tests/plotting/bokeh/testbarplot.py @@ -1,7 +1,11 @@ import numpy as np +from holoviews.core.overlay import NdOverlay from holoviews.element import Bars +from bokeh.models import CategoricalColorMapper, LinearColorMapper + +from ..utils import ParamLogStream from .testplot import TestBokehPlot, bokeh_renderer @@ -122,3 +126,129 @@ def test_bars_padding_logy(self): self.assertEqual(y_range.start, 0.033483695221017122) self.assertEqual(y_range.end, 3.3483695221017129) + ########################### + # Styling mapping # + ########################### + + def test_bars_color_op(self): + bars = Bars([(0, 0, '#000'), (0, 1, '#F00'), (0, 2, '#0F0')], + vdims=['y', 'color']).options(color='color') + plot = bokeh_renderer.get_plot(bars) + cds = plot.handles['cds'] + glyph = plot.handles['glyph'] + self.assertEqual(cds.data['color'], np.array(['#000', '#F00', '#0F0'])) + self.assertEqual(glyph.fill_color, {'field': 'color'}) + self.assertEqual(glyph.line_color, {'field': 'color'}) + + def test_bars_linear_color_op(self): + bars = Bars([(0, 0, 0), (0, 1, 1), (0, 2, 2)], + vdims=['y', 'color']).options(color='color') + plot = bokeh_renderer.get_plot(bars) + cds = plot.handles['cds'] + glyph = plot.handles['glyph'] + cmapper = plot.handles['color_color_mapper'] + self.assertTrue(cmapper, LinearColorMapper) + self.assertEqual(cmapper.low, 0) + self.assertEqual(cmapper.high, 2) + self.assertEqual(cds.data['color'], np.array([0, 1, 2])) + self.assertEqual(glyph.fill_color, {'field': 'color', 'transform': cmapper}) + self.assertEqual(glyph.line_color, {'field': 'color', 'transform': cmapper}) + + def test_bars_categorical_color_op(self): + bars = Bars([(0, 0, 'A'), (0, 1, 'B'), (0, 2, 'C')], + vdims=['y', 'color']).options(color='color') + plot = bokeh_renderer.get_plot(bars) + cds = plot.handles['cds'] + glyph = plot.handles['glyph'] + cmapper = plot.handles['color_color_mapper'] + self.assertTrue(cmapper, CategoricalColorMapper) + self.assertEqual(cmapper.factors, ['A', 'B', 'C']) + self.assertEqual(cds.data['color'], np.array(['A', 'B', 'C'])) + self.assertEqual(glyph.fill_color, {'field': 'color', 'transform': cmapper}) + self.assertEqual(glyph.line_color, {'field': 'color', 'transform': cmapper}) + + def test_bars_line_color_op(self): + bars = Bars([(0, 0, '#000'), (0, 1, '#F00'), (0, 2, '#0F0')], + vdims=['y', 'color']).options(line_color='color') + plot = bokeh_renderer.get_plot(bars) + cds = plot.handles['cds'] + glyph = plot.handles['glyph'] + self.assertEqual(cds.data['line_color'], np.array(['#000', '#F00', '#0F0'])) + self.assertNotEqual(glyph.fill_color, {'field': 'line_color'}) + self.assertEqual(glyph.line_color, {'field': 'line_color'}) + + def test_bars_fill_color_op(self): + bars = Bars([(0, 0, '#000'), (0, 1, '#F00'), (0, 2, '#0F0')], + vdims=['y', 'color']).options(fill_color='color') + plot = bokeh_renderer.get_plot(bars) + cds = plot.handles['cds'] + glyph = plot.handles['glyph'] + self.assertEqual(cds.data['fill_color'], np.array(['#000', '#F00', '#0F0'])) + self.assertEqual(glyph.fill_color, {'field': 'fill_color'}) + self.assertNotEqual(glyph.line_color, {'field': 'fill_color'}) + + def test_bars_alpha_op(self): + bars = Bars([(0, 0, 0), (0, 1, 0.2), (0, 2, 0.7)], + vdims=['y', 'alpha']).options(alpha='alpha') + plot = bokeh_renderer.get_plot(bars) + cds = plot.handles['cds'] + glyph = plot.handles['glyph'] + self.assertEqual(cds.data['alpha'], np.array([0, 0.2, 0.7])) + self.assertEqual(glyph.fill_alpha, {'field': 'alpha'}) + + def test_bars_line_alpha_op(self): + bars = Bars([(0, 0, 0), (0, 1, 0.2), (0, 2, 0.7)], + vdims=['y', 'alpha']).options(line_alpha='alpha') + plot = bokeh_renderer.get_plot(bars) + cds = plot.handles['cds'] + glyph = plot.handles['glyph'] + self.assertEqual(cds.data['line_alpha'], np.array([0, 0.2, 0.7])) + self.assertEqual(glyph.line_alpha, {'field': 'line_alpha'}) + self.assertNotEqual(glyph.fill_alpha, {'field': 'line_alpha'}) + + def test_bars_fill_alpha_op(self): + bars = Bars([(0, 0, 0), (0, 1, 0.2), (0, 2, 0.7)], + vdims=['y', 'alpha']).options(fill_alpha='alpha') + plot = bokeh_renderer.get_plot(bars) + cds = plot.handles['cds'] + glyph = plot.handles['glyph'] + self.assertEqual(cds.data['fill_alpha'], np.array([0, 0.2, 0.7])) + self.assertNotEqual(glyph.line_alpha, {'field': 'fill_alpha'}) + self.assertEqual(glyph.fill_alpha, {'field': 'fill_alpha'}) + + def test_bars_line_width_op(self): + bars = Bars([(0, 0, 1), (0, 1, 4), (0, 2, 8)], + vdims=['y', 'line_width']).options(line_width='line_width') + plot = bokeh_renderer.get_plot(bars) + cds = plot.handles['cds'] + glyph = plot.handles['glyph'] + self.assertEqual(cds.data['line_width'], np.array([1, 4, 8])) + self.assertEqual(glyph.line_width, {'field': 'line_width'}) + + def test_op_ndoverlay_value(self): + colors = ['blue', 'red'] + overlay = NdOverlay({color: Bars(np.arange(i+2)) for i, color in enumerate(colors)}, 'Color').options('Bars', fill_color='Color') + plot = bokeh_renderer.get_plot(overlay) + for subplot, color in zip(plot.subplots.values(), colors): + self.assertEqual(subplot.handles['glyph'].fill_color, color) + + def test_bars_color_index_color_clash(self): + bars = Bars([(0, 0, 0), (0, 1, 1), (0, 2, 2)], + vdims=['y', 'color']).options(color='color', color_index='color') + with ParamLogStream() as log: + plot = bokeh_renderer.get_plot(bars) + log_msg = log.stream.read() + warning = ("%s: Cannot declare style mapping for 'color' option " + "and declare a color_index, ignoring the color_index.\n" + % plot.name) + self.assertEqual(log_msg, warning) + + def test_bars_color_index_color_no_clash(self): + bars = Bars([(0, 0, 0), (0, 1, 1), (0, 2, 2)], + vdims=['y', 'color']).options(fill_color='color', color_index='color') + plot = bokeh_renderer.get_plot(bars) + glyph = plot.handles['glyph'] + cmapper = plot.handles['color_color_mapper'] + cmapper2 = plot.handles['color_mapper'] + self.assertEqual(glyph.fill_color, {'field': 'fill_color', 'transform': cmapper}) + self.assertEqual(glyph.line_color, {'field': 'color', 'transform': cmapper2}) From 2c4709d80166d539f9f4589e2ea23dbd4ce88e63 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Sun, 11 Nov 2018 16:28:05 +0000 Subject: [PATCH 033/117] Finished off mpl PointPlot code, tests, docs --- holoviews/plotting/mpl/chart.py | 13 +++++++++- holoviews/plotting/mpl/element.py | 16 ++++++------- .../plotting/matplotlib/testpointplot.py | 24 ++++++++++++++++++- 3 files changed, 42 insertions(+), 11 deletions(-) diff --git a/holoviews/plotting/mpl/chart.py b/holoviews/plotting/mpl/chart.py index 82aa2a0b96..56f665b9b7 100644 --- a/holoviews/plotting/mpl/chart.py +++ b/holoviews/plotting/mpl/chart.py @@ -581,6 +581,8 @@ class PointPlot(ChartPlot, ColorbarPlot): def get_data(self, element, ranges, style): xs, ys = (element.dimension_values(i) for i in range(2)) self._compute_styles(element, ranges, style) + with abbreviated_exception(): + style = self._apply_ops(element, ranges, style) return (ys, xs) if self.invert_axes else (xs, ys), style, {} @@ -588,6 +590,11 @@ def _compute_styles(self, element, ranges, style): cdim = element.get_dimension(self.color_index) color = style.pop('color', None) cmap = style.get('cmap', None) + + if cdim and ((isinstance(color, basestring) and color in element) or isinstance(color, op)): + self.warning("Cannot declare style mapping for 'color' option " + "and declare a color_index, ignoring the color_index.") + cdim = None if cdim and cmap: cs = element.dimension_values(self.color_index) # Check if numeric otherwise treat as categorical @@ -603,10 +610,14 @@ def _compute_styles(self, element, ranges, style): style['color'] = color style['edgecolors'] = style.pop('edgecolors', style.pop('edgecolor', 'none')) + ms = style.get('s', mpl.rcParams['lines.markersize']) sdim = element.get_dimension(self.size_index) + if sdim and ((isinstance(ms, basestring) and ms in element) or isinstance(ms, op)): + self.warning("Cannot declare style mapping for 's' option " + "and declare a size_index, ignoring the size_index.") + sdim = None if sdim: sizes = element.dimension_values(self.size_index) - ms = style['s'] if 's' in style else mpl.rcParams['lines.markersize'] sizes = compute_sizes(sizes, self.size_fn, self.scaling_factor, self.scaling_method, ms) if sizes is None: diff --git a/holoviews/plotting/mpl/element.py b/holoviews/plotting/mpl/element.py index 3c31bb2f0d..96338dd28a 100644 --- a/holoviews/plotting/mpl/element.py +++ b/holoviews/plotting/mpl/element.py @@ -480,8 +480,6 @@ def initialize_plot(self, ranges=None): if self.show_legend: style['label'] = element.label - with abbreviated_exception(): - style = self._apply_ops(element, ranges, style) plot_data, plot_kwargs, axis_kwargs = self.get_data(element, ranges, style) with abbreviated_exception(): @@ -509,9 +507,7 @@ def update_handles(self, key, axis, element, ranges, style): Update the elements of the plot. """ self.teardown_handles() - with abbreviated_exception(): - new_style = self._apply_ops(element, ranges, style) - plot_data, plot_kwargs, axis_kwargs = self.get_data(element, ranges, new_style) + plot_data, plot_kwargs, axis_kwargs = self.get_data(element, ranges, style) with abbreviated_exception(): handles = self.init_artists(axis, plot_data, plot_kwargs) @@ -559,13 +555,16 @@ def _apply_ops(self, element, ranges, style): ) ) - if 'color' == k and (isinstance(val, np.ndarray) and all(not is_color(c) for c in val)): + if k in ('color', 'c') and isinstance(val, np.ndarray) and all(not is_color(c) for c in val): new_style.pop(k) self._norm_kwargs(element, ranges, new_style, v.dimension, val) if val.dtype.kind in 'OSUM': val = categorize_colors(val) k = 'c' + new_style[k] = val + + for k, val in list(new_style.items()): # If mapped to color/alpha override static fill/line style if k == 'c' or (k == 'color' and isinstance(val, np.ndarray)): fill_style = new_style.get('facecolor') @@ -574,11 +573,10 @@ def _apply_ops(self, element, ranges, style): line_style = new_style.get('edgecolor') if line_style and is_color(line_style): new_style.pop('edgecolor') - elif k == 'facecolors': + elif k == 'facecolors' and not isinstance(new_style.get('color', new_style.get('c')), np.ndarray): # Color overrides facecolors if defined new_style.pop('color', None) - - new_style[k] = val + new_style.pop('c', None) return new_style diff --git a/holoviews/tests/plotting/matplotlib/testpointplot.py b/holoviews/tests/plotting/matplotlib/testpointplot.py index 7c238b4519..93477ed971 100644 --- a/holoviews/tests/plotting/matplotlib/testpointplot.py +++ b/holoviews/tests/plotting/matplotlib/testpointplot.py @@ -57,7 +57,7 @@ def test_points_rcparams_used(self): ax = plot.state.axes[0] lines = ax.get_xgridlines() self.assertEqual(lines[0].get_color(), 'red') - + def test_points_padding_square(self): points = Points([1, 2, 3]).options(padding=0.1) plot = mpl_renderer.get_plot(points) @@ -232,3 +232,25 @@ def test_op_ndoverlay_value(self): style = dict(subplot.style[subplot.cyclic_index]) style = subplot._apply_ops(subplot.current_frame, {}, style) self.assertEqual(style['marker'], marker) + + def test_point_color_index_color_clash(self): + points = Points([(0, 0, 0), (0, 1, 1), (0, 2, 2)], + vdims='color').options(color='color', color_index='color') + with ParamLogStream() as log: + plot = mpl_renderer.get_plot(points) + log_msg = log.stream.read() + warning = ("%s: Cannot declare style mapping for 'color' option " + "and declare a color_index, ignoring the color_index.\n" + % plot.name) + self.assertEqual(log_msg, warning) + + def test_point_size_index_size_clash(self): + points = Points([(0, 0, 0), (0, 1, 1), (0, 2, 2)], + vdims='size').options(s='size', size_index='size') + with ParamLogStream() as log: + plot = mpl_renderer.get_plot(points) + log_msg = log.stream.read() + warning = ("%s: Cannot declare style mapping for 's' option " + "and declare a size_index, ignoring the size_index.\n" + % plot.name) + self.assertEqual(log_msg, warning) From 15b769bb515bcd8735f18cbf44ec0948da5e6868 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Sun, 11 Nov 2018 17:01:33 +0000 Subject: [PATCH 034/117] Finalized handling of SpikesPlot across backends --- holoviews/plotting/bokeh/chart.py | 11 +++---- holoviews/plotting/mpl/chart.py | 17 ++++++++-- holoviews/plotting/mpl/element.py | 6 ++-- .../tests/plotting/bokeh/testspikesplot.py | 24 ++++++++++---- .../plotting/matplotlib/testspikeplot.py | 31 ++++++++++++++----- 5 files changed, 65 insertions(+), 24 deletions(-) diff --git a/holoviews/plotting/bokeh/chart.py b/holoviews/plotting/bokeh/chart.py index 227ad7f04b..449db85e83 100644 --- a/holoviews/plotting/bokeh/chart.py +++ b/holoviews/plotting/bokeh/chart.py @@ -341,6 +341,8 @@ class HistogramPlot(ColorbarPlot): style_opts = line_properties + fill_properties + ['cmap'] _plot_methods = dict(single='quad') + _no_op_styles = ['line_dash'] + def get_data(self, element, ranges, style): if self.invert_axes: mapping = dict(top='right', bottom='left', left=0, right='top') @@ -650,13 +652,10 @@ def get_data(self, element, ranges, style): mapping = {'x0': 'y0', 'x1': 'y1', 'y0': 'x', 'y1': 'x'} else: mapping = {'x0': 'x', 'x1': 'x', 'y0': 'y0', 'y1': 'y1'} - cdim = element.get_dimension(self.color_index) - if cdim: - cmapper = self._get_colormapper(cdim, element, ranges, style) - data[cdim.name] = [] if self.static_source else element.dimension_values(cdim) - mapping['color'] = {'field': cdim.name, - 'transform': cmapper} + cdata, cmapping = self._get_color_data(element, ranges, dict(style)) + data.update(cdata) + mapping.update(cmapping) self._get_hover_data(data, element) return data, mapping, style diff --git a/holoviews/plotting/mpl/chart.py b/holoviews/plotting/mpl/chart.py index 56f665b9b7..dc76e1e7c5 100644 --- a/holoviews/plotting/mpl/chart.py +++ b/holoviews/plotting/mpl/chart.py @@ -1003,6 +1003,10 @@ class SpikesPlot(PathPlot, ColorbarPlot): style_opts = PathPlot.style_opts + ['cmap'] 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') line_segments = LineCollection(*plot_args, **plot_kwargs) ax.add_collection(line_segments) return {'artist': line_segments} @@ -1061,10 +1065,17 @@ def get_data(self, element, ranges, style): clean_spikes.append(np.column_stack(cols)) cdim = element.get_dimension(self.color_index) + color = style.get('color', None) + if cdim and ((isinstance(color, basestring) and color in element) or isinstance(color, op)): + self.warning("Cannot declare style mapping for 'color' option " + "and declare a color_index, ignoring the color_index.") + cdim = None if cdim: style['array'] = element.dimension_values(cdim) self._norm_kwargs(element, ranges, style, cdim) - style['clim'] = style.pop('vmin'), style.pop('vmax') + + with abbreviated_exception(): + style = self._apply_ops(element, ranges, style) return (clean_spikes,), style, {'dimensions': dims} @@ -1073,9 +1084,9 @@ def update_handles(self, key, axis, element, ranges, style): (data,), kwargs, axis_kwargs = self.get_data(element, ranges, style) artist.set_paths(data) artist.set_visible(style.get('visible', True)) - if 'array' in kwargs: + if 'array' in kwargs or 'c' in kwargs: artist.set_clim((kwargs['vmin'], kwargs['vmax'])) - artist.set_array(kwargs['array']) + artist.set_array(kwargs.get('array', kwargs.get('c'))) if 'norm' in kwargs: artist.norm = kwargs['norm'] return axis_kwargs diff --git a/holoviews/plotting/mpl/element.py b/holoviews/plotting/mpl/element.py index 96338dd28a..b3f5b9fedf 100644 --- a/holoviews/plotting/mpl/element.py +++ b/holoviews/plotting/mpl/element.py @@ -555,7 +555,7 @@ def _apply_ops(self, element, ranges, style): ) ) - if k in ('color', 'c') and isinstance(val, np.ndarray) and all(not is_color(c) for c in val): + if k in ('c', 'color') and isinstance(val, np.ndarray) and all(not is_color(c) for c in val): new_style.pop(k) self._norm_kwargs(element, ranges, new_style, v.dimension, val) if val.dtype.kind in 'OSUM': @@ -566,7 +566,9 @@ def _apply_ops(self, element, ranges, style): for k, val in list(new_style.items()): # If mapped to color/alpha override static fill/line style - if k == 'c' or (k == 'color' and isinstance(val, np.ndarray)): + if k == 'c': + new_style.pop('color', None) + if k in ('c', 'color') and isinstance(val, np.ndarray): fill_style = new_style.get('facecolor') if fill_style and is_color(fill_style): new_style.pop('facecolor') diff --git a/holoviews/tests/plotting/bokeh/testspikesplot.py b/holoviews/tests/plotting/bokeh/testspikesplot.py index 9929a37d0b..f794d4ecb2 100644 --- a/holoviews/tests/plotting/bokeh/testspikesplot.py +++ b/holoviews/tests/plotting/bokeh/testspikesplot.py @@ -6,6 +6,7 @@ from bokeh.models import CategoricalColorMapper, LinearColorMapper +from ..utils import ParamLogStream from .testplot import TestBokehPlot, bokeh_renderer @@ -141,7 +142,7 @@ def test_spikes_color_op(self): cds = plot.handles['cds'] glyph = plot.handles['glyph'] self.assertEqual(cds.data['color'], np.array(['#000', '#F00', '#0F0'])) - self.assertEqual(glyph.line_color, 'color') + self.assertEqual(glyph.line_color, {'field': 'color'}) def test_spikes_linear_color_op(self): spikes = Spikes([(0, 0, 0), (0, 1, 1), (0, 2, 2)], @@ -164,7 +165,7 @@ def test_spikes_categorical_color_op(self): glyph = plot.handles['glyph'] cmapper = plot.handles['color_color_mapper'] self.assertTrue(cmapper, CategoricalColorMapper) - self.assertEqual(cmapper.factors, np.array(['A', 'B', 'C'])) + self.assertEqual(cmapper.factors, ['A', 'B', 'C']) self.assertEqual(cds.data['color'], np.array(['A', 'B', 'C'])) self.assertEqual(glyph.line_color, {'field': 'color', 'transform': cmapper}) @@ -175,7 +176,7 @@ def test_spikes_line_color_op(self): cds = plot.handles['cds'] glyph = plot.handles['glyph'] self.assertEqual(cds.data['line_color'], np.array(['#000', '#F00', '#0F0'])) - self.assertEqual(glyph.line_color, 'line_color') + self.assertEqual(glyph.line_color, {'field': 'line_color'}) def test_spikes_alpha_op(self): spikes = Spikes([(0, 0, 0), (0, 1, 0.2), (0, 2, 0.7)], @@ -184,7 +185,7 @@ def test_spikes_alpha_op(self): cds = plot.handles['cds'] glyph = plot.handles['glyph'] self.assertEqual(cds.data['alpha'], np.array([0, 0.2, 0.7])) - self.assertEqual(glyph.line_alpha, 'alpha') + self.assertEqual(glyph.line_alpha, {'field': 'alpha'}) def test_spikes_line_alpha_op(self): spikes = Spikes([(0, 0, 0), (0, 1, 0.2), (0, 2, 0.7)], @@ -193,7 +194,7 @@ def test_spikes_line_alpha_op(self): cds = plot.handles['cds'] glyph = plot.handles['glyph'] self.assertEqual(cds.data['line_alpha'], np.array([0, 0.2, 0.7])) - self.assertEqual(glyph.line_alpha, 'line_alpha') + self.assertEqual(glyph.line_alpha, {'field': 'line_alpha'}) def test_spikes_line_width_op(self): spikes = Spikes([(0, 0, 1), (0, 1, 4), (0, 2, 8)], @@ -202,7 +203,7 @@ def test_spikes_line_width_op(self): cds = plot.handles['cds'] glyph = plot.handles['glyph'] self.assertEqual(cds.data['line_width'], np.array([1, 4, 8])) - self.assertEqual(glyph.line_width, 'line_width') + self.assertEqual(glyph.line_width, {'field': 'line_width'}) def test_op_ndoverlay_value(self): colors = ['blue', 'red'] @@ -210,3 +211,14 @@ def test_op_ndoverlay_value(self): plot = bokeh_renderer.get_plot(overlay) for subplot, color in zip(plot.subplots.values(), colors): self.assertEqual(subplot.handles['glyph'].line_color, color) + + def test_spikes_color_index_color_clash(self): + spikes = Spikes([(0, 0, 0), (0, 1, 1), (0, 2, 2)], + vdims=['y', 'color']).options(color='color', color_index='color') + with ParamLogStream() as log: + plot = bokeh_renderer.get_plot(spikes) + log_msg = log.stream.read() + warning = ("%s: Cannot declare style mapping for 'color' option " + "and declare a color_index, ignoring the color_index.\n" + % plot.name) + self.assertEqual(log_msg, warning) diff --git a/holoviews/tests/plotting/matplotlib/testspikeplot.py b/holoviews/tests/plotting/matplotlib/testspikeplot.py index d72bafc6a9..f0f45bd0eb 100644 --- a/holoviews/tests/plotting/matplotlib/testspikeplot.py +++ b/holoviews/tests/plotting/matplotlib/testspikeplot.py @@ -3,6 +3,7 @@ from holoviews.core.overlay import NdOverlay from holoviews.element import Spikes +from ..utils import ParamLogStream from .testplot import TestMPLPlot, mpl_renderer @@ -103,15 +104,19 @@ def test_spikes_color_op(self): def test_spikes_linear_color_op(self): spikes = Spikes([(0, 0, 0), (0, 1, 1), (0, 2, 2)], - vdims=['y', 'color']).options(color='color') - with self.assertRaises(Exception): - mpl_renderer.get_plot(spikes) + vdims=['y', 'color']).options(color='color') + plot = mpl_renderer.get_plot(spikes) + artist = plot.handles['artist'] + self.assertEqual(artist.get_array(), np.array([0, 1, 2])) + self.assertEqual(artist.get_clim(), (0, 2)) def test_spikes_categorical_color_op(self): - spikes = Spikes([(0, 0, 'A'), (0, 1, 'B'), (0, 2, 'C')], - vdims=['y', 'color']).options(color='color') - with self.assertRaises(Exception): - mpl_renderer.get_plot(spikes) + spikes = Spikes([(0, 0, 'A'), (0, 1, 'B'), (0, 2, 'A')], + vdims=['y', 'color']).options(color='color') + plot = mpl_renderer.get_plot(spikes) + artist = plot.handles['artist'] + self.assertEqual(artist.get_array(), np.array([0, 1, 0])) + self.assertEqual(artist.get_clim(), (0, 1)) def test_spikes_alpha_op(self): spikes = Spikes([(0, 0, 0), (0, 1, 0.2), (0, 2, 0.7)], @@ -140,3 +145,15 @@ def test_op_ndoverlay_value(self): children = subplot.handles['artist'].get_children() for c in children: self.assertEqual(c.get_facecolor(), color) + + def test_spikes_color_index_color_clash(self): + spikes = Spikes([(0, 0, 0), (0, 1, 1), (0, 2, 2)], + vdims=['y', 'color']).options(color='color', color_index='color') + with ParamLogStream() as log: + plot = mpl_renderer.get_plot(spikes) + log_msg = log.stream.read() + warning = ("%s: Cannot declare style mapping for 'color' option " + "and declare a color_index, ignoring the color_index.\n" + % plot.name) + self.assertEqual(log_msg, warning) + From dc3c5fd0b2542cc6a3a122dbb0b5e2dc7c1e7c66 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Sun, 11 Nov 2018 17:23:40 +0000 Subject: [PATCH 035/117] Finalized style mapping for Spikes and Labels --- .../reference/elements/bokeh/Labels.ipynb | 4 +-- .../reference/elements/bokeh/Spikes.ipynb | 6 ++-- .../elements/matplotlib/Spikes.ipynb | 6 ++-- holoviews/plotting/bokeh/annotation.py | 2 +- holoviews/plotting/bokeh/element.py | 8 ++++- holoviews/plotting/mpl/annotation.py | 5 ++- holoviews/tests/plotting/bokeh/testlabels.py | 36 ++++++++++++++----- holoviews/util/ops.py | 4 +-- 8 files changed, 50 insertions(+), 21 deletions(-) diff --git a/examples/reference/elements/bokeh/Labels.ipynb b/examples/reference/elements/bokeh/Labels.ipynb index 9b2fbdad8f..0a40fa75b0 100644 --- a/examples/reference/elements/bokeh/Labels.ipynb +++ b/examples/reference/elements/bokeh/Labels.ipynb @@ -72,7 +72,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "If the value dimension of the data is not already of string type it will be formatted using the applicable entry in ``Dimension.type_formatters`` or an explicit ``value_format`` defined on the Dimension. Additionally the ``color_index`` option allows us to colormap the text by a dimension.\n", + "If the value dimension of the data is not already of string type it will be formatted using the applicable entry in ``Dimension.type_formatters`` or an explicit ``value_format`` defined on the Dimension. Additionally the ``text_color`` option allows us to colormap the text by a dimension.\n", "\n", "Here we will create a 2D array of values, define a Dimension with a formatter and then colormap the text:" ] @@ -88,7 +88,7 @@ "zs = np.sin(xs**2)*np.sin(ys**2)[:, np.newaxis]\n", "\n", "hv.Labels((xs, ys, zs), vdims=value_dimension).options(\n", - " bgcolor='black', cmap='magma', color_index='Values', height=400, text_font_size='6pt', width=400\n", + " bgcolor='black', cmap='magma', text_color='Values', height=400, text_font_size='6pt', width=400\n", ")" ] } diff --git a/examples/reference/elements/bokeh/Spikes.ipynb b/examples/reference/elements/bokeh/Spikes.ipynb index 1674194d72..bf73094b72 100644 --- a/examples/reference/elements/bokeh/Spikes.ipynb +++ b/examples/reference/elements/bokeh/Spikes.ipynb @@ -66,7 +66,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "When supplying a second dimension to the ``Spikes`` element as a value dimension, these additional values will be mapped onto the line height. Optionally, you may also supply a colormap ``cmap`` and ``color_index`` to map the value dimensions to a suitable set of colors. This way we can, for example, plot a [mass spectrogram](https://en.wikipedia.org/wiki/Mass_spectrometry):" + "When supplying a second dimension to the ``Spikes`` element as a value dimension, these additional values will be mapped onto the line height. Optionally, it is also possible to map dimensions to style options. This way we can, for example, plot a [mass spectrogram](https://en.wikipedia.org/wiki/Mass_spectrometry):" ] }, { @@ -75,7 +75,7 @@ "metadata": {}, "outputs": [], "source": [ - "%%opts Spikes [color_index='Intensity'] (cmap='Reds')\n", + "%%opts Spikes (color='Intensity' cmap='Reds')\n", "hv.Spikes(np.random.rand(20, 2), 'Mass', 'Intensity')" ] }, @@ -83,7 +83,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Another possibility is to draw a number of spike trains representing the firing of neurons, of the sort that are commonly encountered in neuroscience. Here we generate 10 separate random spike trains and distribute them evenly across the space by setting their ``position``. By declaring some ``yticks``, each spike train can be labeled individually:" + "Another possibility is to draw a set of Spikes offset by a position, this can be useful for plotting neuron spike trains or other events. Here we generate 10 separate random spike trains and distribute them evenly across the space by setting their ``position``. By declaring some ``yticks``, each spike train can be labeled individually:" ] }, { diff --git a/examples/reference/elements/matplotlib/Spikes.ipynb b/examples/reference/elements/matplotlib/Spikes.ipynb index 9ef9fd6e8b..b47b1bfbad 100644 --- a/examples/reference/elements/matplotlib/Spikes.ipynb +++ b/examples/reference/elements/matplotlib/Spikes.ipynb @@ -66,7 +66,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "When supplying a second dimension to the ``Spikes`` element as a value dimensions, these additional values will be mapped onto the line height. Optionally, you may also supply a colormap ``cmap`` and ``color_index`` to map the value dimensions to a suitable set of colors. This way we can, for example, plot a [mass spectrogram](https://en.wikipedia.org/wiki/Mass_spectrometry):" + "When supplying a second dimension to the ``Spikes`` element as a value dimension, these additional values will be mapped onto the line height. Optionally, it is also possible to map dimensions to style options. This way we can, for example, plot a [mass spectrogram](https://en.wikipedia.org/wiki/Mass_spectrometry):" ] }, { @@ -75,7 +75,7 @@ "metadata": {}, "outputs": [], "source": [ - "%%opts Spikes [color_index='Intensity'] (cmap='Reds')\n", + "%%opts Spikes (color='Intensity' cmap='Reds')\n", "hv.Spikes(np.random.rand(20, 2), 'Mass', 'Intensity')" ] }, @@ -83,7 +83,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Another possibility is to draw a number of spike trains representing the firing of neurons, of the sort that are commonly encountered in neuroscience. Here we generate 10 separate random spike trains and distribute them evenly across the space by setting their ``position``. By declaring some ``yticks``, each spike train can be labeled individually:" + "Another possibility is to draw a set of Spikes offset by a position, this can be useful for plotting neuron spike trains or other events. Here we generate 10 separate random spike trains and distribute them evenly across the space by setting their ``position``. By declaring some ``yticks``, each spike train can be labeled individually:" ] }, { diff --git a/holoviews/plotting/bokeh/annotation.py b/holoviews/plotting/bokeh/annotation.py index d1ce62a5a1..f212151f37 100644 --- a/holoviews/plotting/bokeh/annotation.py +++ b/holoviews/plotting/bokeh/annotation.py @@ -110,7 +110,7 @@ def get_data(self, element, ranges, style): return data, mapping, style cdata, cmapping = self._get_color_data(element, ranges, style, name='text_color') - if dims[2] is cdim: + if dims[2] is cdim and cdata: # If color dim is same as text dim, rename color column data['text_color'] = cdata[tdim] mapping['text_color'] = dict(cmapping['text_color'], field='text_color') diff --git a/holoviews/plotting/bokeh/element.py b/holoviews/plotting/bokeh/element.py index 806f594410..b11b2f2d73 100644 --- a/holoviews/plotting/bokeh/element.py +++ b/holoviews/plotting/bokeh/element.py @@ -680,7 +680,7 @@ def _apply_ops(self, element, source, ranges, style, group=None): if len(v.ops) == 0 and v.dimension in self.overlay_dims: val = self.overlay_dims[v.dimension] else: - val = v.eval(element, ranges) + val = v.eval(element, ranges=ranges, flat=True) if len(np.unique(val)) == 1: val = val if np.isscalar(val) else val[0] @@ -703,6 +703,11 @@ def _apply_ops(self, element, source, ranges, style, group=None): if k == 'angle': val = np.deg2rad(val) + elif k.endswith('font_size'): + if np.isscalar(val) and isinstance(val, int): + val = str(v)+'pt' + elif isinstance(val, np.ndarray) and val.dtype.kind in 'ifu': + val = [str(int(v))+'pt' for v in val] if np.isscalar(val): key = val else: @@ -732,6 +737,7 @@ def _apply_ops(self, element, source, ranges, style, group=None): line_style = new_style.get(prefix+'line_'+s) if line_style and validate(s, line_style): new_style.pop(prefix+'line_'+s) + return new_style diff --git a/holoviews/plotting/mpl/annotation.py b/holoviews/plotting/mpl/annotation.py index 328a26516a..bc92e8600d 100644 --- a/holoviews/plotting/mpl/annotation.py +++ b/holoviews/plotting/mpl/annotation.py @@ -101,11 +101,14 @@ class LabelsPlot(ColorbarPlot): style_opts = ['alpha', 'color', 'family', 'weight', 'size', 'visible', 'horizontalalignment', 'verticalalignment', 'cmap', 'rotation'] - _no_op_styles = [] + _no_op_styles = ['cmap'] _plot_methods = dict(single='annotate') def get_data(self, element, ranges, style): + with abbreviated_exception(): + style = self._apply_ops(element, ranges, style) + xs, ys = (element.dimension_values(i) for i in range(2)) tdim = element.get_dimension(2) text = [tdim.pprint_value(v) for v in element.dimension_values(tdim)] diff --git a/holoviews/tests/plotting/bokeh/testlabels.py b/holoviews/tests/plotting/bokeh/testlabels.py index 224dd6ee99..20cda8c140 100644 --- a/holoviews/tests/plotting/bokeh/testlabels.py +++ b/holoviews/tests/plotting/bokeh/testlabels.py @@ -8,7 +8,7 @@ except: pass - +from ..utils import ParamLogStream from .testplot import TestBokehPlot, bokeh_renderer @@ -112,7 +112,7 @@ def test_label_color_op(self): cds = plot.handles['cds'] glyph = plot.handles['glyph'] self.assertEqual(cds.data['text_color'], np.array(['#000', '#F00', '#0F0'])) - self.assertEqual(glyph.text_color, 'text_color') + self.assertEqual(glyph.text_color, {'field': 'text_color'}) def test_label_linear_color_op(self): labels = Labels([(0, 0, 0), (0, 1, 1), (0, 2, 2)], @@ -135,7 +135,7 @@ def test_label_categorical_color_op(self): glyph = plot.handles['glyph'] cmapper = plot.handles['color_color_mapper'] self.assertTrue(cmapper, CategoricalColorMapper) - self.assertEqual(cmapper.factors, np.array(['A', 'B', 'C'])) + self.assertEqual(cmapper.factors, ['A', 'B', 'C']) self.assertEqual(cds.data['text_color'], np.array(['A', 'B', 'C'])) self.assertEqual(glyph.text_color, {'field': 'text_color', 'transform': cmapper}) @@ -146,7 +146,7 @@ def test_label_angle_op(self): cds = plot.handles['cds'] glyph = plot.handles['glyph'] self.assertEqual(cds.data['angle'], np.array([0, 0.785398, 1.570796])) - self.assertEqual(glyph.angle, 'angle') + self.assertEqual(glyph.angle, {'field': 'angle'}) def test_label_alpha_op(self): labels = Labels([(0, 0, 0), (0, 1, 0.2), (0, 2, 0.7)], @@ -155,13 +155,33 @@ def test_label_alpha_op(self): cds = plot.handles['cds'] glyph = plot.handles['glyph'] self.assertEqual(cds.data['text_alpha'], np.array([0, 0.2, 0.7])) - self.assertEqual(glyph.text_alpha, 'text_alpha') + self.assertEqual(glyph.text_alpha, {'field': 'text_alpha'}) - def test_label_size_op(self): + def test_label_font_size_op_strings(self): labels = Labels([(0, 0, '10pt'), (0, 1, '4pt'), (0, 2, '8pt')], vdims='size').options(text_font_size='size') plot = bokeh_renderer.get_plot(labels) cds = plot.handles['cds'] glyph = plot.handles['glyph'] - self.assertEqual(cds.data['size'], ['10pt', '4pt', '8pt']) - self.assertEqual(glyph.text_font_size, 'text_font_size') + self.assertEqual(cds.data['text_font_size'], np.array(['10pt', '4pt', '8pt'])) + self.assertEqual(glyph.text_font_size, {'field': 'text_font_size'}) + + def test_label_font_size_op_ints(self): + labels = Labels([(0, 0, 10), (0, 1, 4), (0, 2, 8)], + vdims='size').options(text_font_size='size') + plot = bokeh_renderer.get_plot(labels) + cds = plot.handles['cds'] + glyph = plot.handles['glyph'] + self.assertEqual(cds.data['text_font_size'], ['10pt', '4pt', '8pt']) + self.assertEqual(glyph.text_font_size, {'field': 'text_font_size'}) + + def test_labels_color_index_color_clash(self): + labels = Labels([(0, 0, 0), (0, 1, 1), (0, 2, 2)], + vdims='color').options(text_color='color', color_index='color') + with ParamLogStream() as log: + plot = bokeh_renderer.get_plot(labels) + log_msg = log.stream.read() + warning = ("%s: Cannot declare style mapping for 'text_color' option " + "and declare a color_index, ignoring the color_index.\n" + % plot.name) + self.assertEqual(log_msg, warning) diff --git a/holoviews/util/ops.py b/holoviews/util/ops.py index 77fea3a519..3c9432f4a2 100644 --- a/holoviews/util/ops.py +++ b/holoviews/util/ops.py @@ -136,12 +136,12 @@ def sum(self, **kwargs): return op(self, np.sum, **kwargs) def std(self, **kwargs): return op(self, np.std, **kwargs) def var(self, **kwargs): return op(self, np.var, **kwargs) - def eval(self, dataset, ranges={}): + def eval(self, dataset, flat=False, ranges={}): expanded = not ((dataset.interface.gridded and self.dimension in dataset.kdims) or (dataset.interface.multi and dataset.interface.isscalar(dataset, self.dimension))) if isinstance(dataset, Graph): dataset = dataset if self.dimension in dataset else dataset.nodes - data = dataset.dimension_values(self.dimension, expanded=expanded, flat=False) + data = dataset.dimension_values(self.dimension, expanded=expanded, flat=flat) for o in self.ops: other = o['other'] if other is not None: From 67d518a09cd5df5f8409b44de3ca9ccf812264c3 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Sun, 11 Nov 2018 18:34:06 +0000 Subject: [PATCH 036/117] Finalized style mapping support for VectorField --- .../gallery/demos/bokeh/quiver_demo.ipynb | 2 +- .../demos/matplotlib/quiver_demo.ipynb | 2 +- .../elements/bokeh/VectorField.ipynb | 6 +- .../elements/matplotlib/VectorField.ipynb | 6 +- holoviews/plotting/bokeh/chart.py | 6 +- holoviews/plotting/bokeh/element.py | 7 +- holoviews/plotting/mpl/chart.py | 27 ++++-- .../plotting/bokeh/testvectorfieldplot.py | 89 +++++++++++++++++++ .../matplotlib/testvectorfieldplot.py | 70 +++++++++++++++ holoviews/util/ops.py | 7 +- 10 files changed, 200 insertions(+), 22 deletions(-) create mode 100644 holoviews/tests/plotting/bokeh/testvectorfieldplot.py create mode 100644 holoviews/tests/plotting/matplotlib/testvectorfieldplot.py diff --git a/examples/gallery/demos/bokeh/quiver_demo.ipynb b/examples/gallery/demos/bokeh/quiver_demo.ipynb index 0c02b0a612..d1fd9164a8 100644 --- a/examples/gallery/demos/bokeh/quiver_demo.ipynb +++ b/examples/gallery/demos/bokeh/quiver_demo.ipynb @@ -77,7 +77,7 @@ "metadata": {}, "outputs": [], "source": [ - "%%opts VectorField [width=500 color_index=3 size_index=3 pivot='tip'] Points (color='black')\n", + "%%opts VectorField [width=500 size_index=3 pivot='tip'] (color='Magnitude') Points (color='black')\n", "hv.VectorField((xs, ys, angle, mag), label=\"pivot='tip'; scales with x view\") * hv.Points((X.flat, Y.flat))" ] } diff --git a/examples/gallery/demos/matplotlib/quiver_demo.ipynb b/examples/gallery/demos/matplotlib/quiver_demo.ipynb index 92dbdfc874..bce0418387 100644 --- a/examples/gallery/demos/matplotlib/quiver_demo.ipynb +++ b/examples/gallery/demos/matplotlib/quiver_demo.ipynb @@ -77,7 +77,7 @@ "metadata": {}, "outputs": [], "source": [ - "%%opts VectorField [aspect=1.5 fig_size=200 size_index=3 color_index=3] (pivot='tip') Points (color='black' s=5)\n", + "%%opts VectorField [aspect=1.5 fig_size=200 size_index=3] (color='Magnitude' pivot='tip') Points (color='black' s=5)\n", "hv.VectorField((xs, ys, angle, mag), label=\"pivot='tip'; scales with x view\") * hv.Points((X.flat, Y.flat))" ] } diff --git a/examples/reference/elements/bokeh/VectorField.ipynb b/examples/reference/elements/bokeh/VectorField.ipynb index 0648b2893f..fbe157b80a 100644 --- a/examples/reference/elements/bokeh/VectorField.ipynb +++ b/examples/reference/elements/bokeh/VectorField.ipynb @@ -61,7 +61,7 @@ "metadata": {}, "outputs": [], "source": [ - "%%opts VectorField [size_index=3] VectorField.A [color_index=2] VectorField.M [color_index=3]\n", + "%%opts VectorField [size_index=3] VectorField.A (color='Angle') VectorField.M (color='Magnitude')\n", "hv.VectorField(vector_data, group='A') + hv.VectorField(vector_data, group='M')" ] }, @@ -78,7 +78,7 @@ "metadata": {}, "outputs": [], "source": [ - "%%opts VectorField [color_index=2 size_index=3 rescale_lengths=False] (scale=4)\n", + "%%opts VectorField [size_index=3 rescale_lengths=False] (scale=4, color='Angle')\n", "hv.HoloMap({phase: hv.VectorField([x, y,(vector_data[2]+phase)%np.pi*2, vector_data[3]+np.abs(phase)])\n", " for phase in np.linspace(-np.pi,np.pi,5)}, kdims='Phase')" ] @@ -119,7 +119,7 @@ "metadata": {}, "outputs": [], "source": [ - "%%opts VectorField [width=500 color_index=3 size_index=3 pivot='tip'] (cmap='fire' scale=0.8) Points (color='black' size=1)\n", + "%%opts VectorField [width=500 size_index=3 pivot='tip'] (cmap='fire' scale=0.8 color='Magnitude') Points (color='black' size=1)\n", "hv.VectorField((xs, ys, angle, mag)) * hv.Points((X.flat, Y.flat))" ] }, diff --git a/examples/reference/elements/matplotlib/VectorField.ipynb b/examples/reference/elements/matplotlib/VectorField.ipynb index 89b0f76afd..3c74609d34 100644 --- a/examples/reference/elements/matplotlib/VectorField.ipynb +++ b/examples/reference/elements/matplotlib/VectorField.ipynb @@ -61,7 +61,7 @@ "metadata": {}, "outputs": [], "source": [ - "%%opts VectorField [size_index=3] VectorField.A [color_index=2] VectorField.M [color_index=3]\n", + "%%opts VectorField [size_index=3] VectorField.A (color='Angle') VectorField.M (color='Magnitude')\n", "hv.VectorField(vector_data, group='A') + hv.VectorField(vector_data, group='M')" ] }, @@ -78,7 +78,7 @@ "metadata": {}, "outputs": [], "source": [ - "%%opts VectorField [color_index=2 size_index=3 rescale_lengths=False] (scale=4)\n", + "%%opts VectorField [size_index=3 rescale_lengths=False] (scale=4 color='Angle')\n", "hv.HoloMap({phase: hv.VectorField([x, y,(vector_data[2]+phase)%np.pi*2, vector_data[3]+np.abs(phase)])\n", " for phase in np.linspace(-np.pi,np.pi,5)}, kdims='Phase')" ] @@ -119,7 +119,7 @@ "metadata": {}, "outputs": [], "source": [ - "%%opts VectorField [color_index=3 size_index=3 pivot='tip'] (cmap='fire' scale=0.8) Points (color='black' s=1)\n", + "%%opts VectorField [size_index=3 aspect=2 fig_size=300] (pivot='tip' cmap='fire' scale=0.8 color='Magnitude') Points (color='black' s=1)\n", "hv.VectorField((xs, ys, angle, mag)) * hv.Points((X.flat, Y.flat))" ] }, diff --git a/holoviews/plotting/bokeh/chart.py b/holoviews/plotting/bokeh/chart.py index 449db85e83..fc5097fe11 100644 --- a/holoviews/plotting/bokeh/chart.py +++ b/holoviews/plotting/bokeh/chart.py @@ -242,13 +242,13 @@ def get_data(self, element, ranges, style): ya1s = y0s - np.sin(rads+np.pi/4)*arrow_len xa2s = x0s - np.cos(rads-np.pi/4)*arrow_len ya2s = y0s - np.sin(rads-np.pi/4)*arrow_len - x0s = np.concatenate([x0s, x0s, x0s]) + x0s = np.tile(x0s, 3) x1s = np.concatenate([x1s, xa1s, xa2s]) - y0s = np.concatenate([y0s, y0s, y0s]) + y0s = np.tile(y0s, 3) y1s = np.concatenate([y1s, ya1s, ya2s]) if cdim: color = cdata.get(cdim.name) - color = np.concatenate([color, color, color]) + color = np.tile(color, 3) elif cdim: color = cdata.get(cdim.name) diff --git a/holoviews/plotting/bokeh/element.py b/holoviews/plotting/bokeh/element.py index b11b2f2d73..09be58fa9a 100644 --- a/holoviews/plotting/bokeh/element.py +++ b/holoviews/plotting/bokeh/element.py @@ -25,7 +25,7 @@ from ...core import DynamicMap, CompositeOverlay, Element, Dimension from ...core.options import abbreviated_exception, SkipRendering from ...core import util -from ...element import Graph +from ...element import Graph, VectorField from ...streams import Buffer from ...util.ops import op from ..plot import GenericElementPlot, GenericOverlayPlot @@ -699,7 +699,10 @@ def _apply_ops(self, element, source, ranges, style, group=None): ) ) elif source.data and len(val) != len(list(source.data.values())[0]): - continue + if isinstance(element, VectorField): + val = np.tile(val, 3) + else: + continue if k == 'angle': val = np.deg2rad(val) diff --git a/holoviews/plotting/mpl/chart.py b/holoviews/plotting/mpl/chart.py index dc76e1e7c5..f600f3c09f 100644 --- a/holoviews/plotting/mpl/chart.py +++ b/holoviews/plotting/mpl/chart.py @@ -21,6 +21,7 @@ ) from ...element import Raster, HeatMap from ...operation import interpolate_curve +from ...util.ops import op from ..plot import PlotSelector from ..util import compute_sizes, get_sideplot_ranges, get_min_distance from .element import ElementPlot, ColorbarPlot, LegendPlot @@ -688,11 +689,13 @@ class VectorFieldPlot(ColorbarPlot): style_opts = ['alpha', 'color', 'edgecolors', 'facecolors', 'linewidth', 'marker', 'visible', 'cmap', 'scale', 'headlength', 'headaxislength', 'pivot', - 'width','headwidth', 'norm'] - - _plot_methods = dict(single='quiver') + 'width', 'headwidth', 'norm'] + _no_op_styles = ['alpha', 'marker', 'cmap', 'visible', 'norm', + 'pivot', 'scale', 'headlength', 'headaxislength', + 'headwidth'] + _plot_methods = dict(single='quiver') def get_data(self, element, ranges, style): input_scale = style.pop('scale', 1.0) @@ -717,22 +720,34 @@ def get_data(self, element, ranges, style): magnitudes = np.ones(len(xs)) args = (xs, ys, magnitudes, [0.0] * len(element)) - if self.color_index: + cdim = element.get_dimension(self.color_index) + color = style.get('color', None) + if cdim and ((isinstance(color, basestring) and color in element) or isinstance(color, op)): + self.warning("Cannot declare style mapping for 'color' option " + "and declare a color_index, ignoring the color_index.") + cdim = None + if cdim: colors = element.dimension_values(self.color_index) args += (colors,) cdim = element.get_dimension(self.color_index) self._norm_kwargs(element, ranges, style, cdim) - style['clim'] = (style.pop('vmin'), style.pop('vmax')) style.pop('color', None) if 'pivot' not in style: style['pivot'] = 'mid' if not self.arrow_heads: style['headaxislength'] = 0 + + with abbreviated_exception(): + style = self._apply_ops(element, ranges, style) style.update(dict(scale=input_scale, angles=angles, units='x', scale_units='x')) - return args, style, {} + if 'vmin' in style: + style['clim'] = (style.pop('vmin'), style.pop('vmax')) + if 'c' in style: + style['array'] = style.pop('c') + return args, style, {} def update_handles(self, key, axis, element, ranges, style): args, style, axis_kwargs = self.get_data(element, ranges, style) diff --git a/holoviews/tests/plotting/bokeh/testvectorfieldplot.py b/holoviews/tests/plotting/bokeh/testvectorfieldplot.py new file mode 100644 index 0000000000..592239a391 --- /dev/null +++ b/holoviews/tests/plotting/bokeh/testvectorfieldplot.py @@ -0,0 +1,89 @@ +import datetime as dt +from unittest import SkipTest + +import numpy as np + +from holoviews.core import NdOverlay +from holoviews.core.options import Cycle +from holoviews.core.util import pd +from holoviews.element import VectorField + +from .testplot import TestBokehPlot, bokeh_renderer +from ..utils import ParamLogStream + +try: + from bokeh.models import FactorRange, LinearColorMapper, CategoricalColorMapper + from bokeh.models import Scatter +except: + pass + + +class TestVectorFieldPlot(TestBokehPlot): + + ########################### + # Styling mapping # + ########################### + + def test_vectorfield_color_op(self): + vectorfield = VectorField([(0, 0, 0, 1, '#000'), (0, 1, 0, 1,'#F00'), (0, 2, 0, 1,'#0F0')], + vdims=['A', 'M', 'color']).options(color='color') + plot = bokeh_renderer.get_plot(vectorfield) + cds = plot.handles['cds'] + glyph = plot.handles['glyph'] + self.assertEqual(cds.data['color'], np.array(['#000', '#F00', '#0F0', '#000', + '#F00', '#0F0', '#000', '#F00', '#0F0'])) + self.assertEqual(glyph.line_color, {'field': 'color'}) + + def test_vectorfield_linear_color_op(self): + vectorfield = VectorField([(0, 0, 0, 1, 0), (0, 1, 0, 1, 1), (0, 2, 0, 1, 2)], + vdims=['A', 'M', 'color']).options(color='color') + plot = bokeh_renderer.get_plot(vectorfield) + cds = plot.handles['cds'] + glyph = plot.handles['glyph'] + cmapper = plot.handles['color_color_mapper'] + self.assertTrue(cmapper, LinearColorMapper) + self.assertEqual(cmapper.low, 0) + self.assertEqual(cmapper.high, 2) + self.assertEqual(cds.data['color'], np.array([0, 1, 2, 0, 1, 2, 0, 1, 2])) + self.assertEqual(glyph.line_color, {'field': 'color', 'transform': cmapper}) + + def test_vectorfield_categorical_color_op(self): + vectorfield = VectorField([(0, 0, 0, 1, 'A'), (0, 1, 0, 1, 'B'), (0, 2, 0, 1, 'C')], + vdims=['A', 'M', 'color']).options(color='color') + plot = bokeh_renderer.get_plot(vectorfield) + cds = plot.handles['cds'] + glyph = plot.handles['glyph'] + cmapper = plot.handles['color_color_mapper'] + self.assertTrue(cmapper, CategoricalColorMapper) + self.assertEqual(cmapper.factors, ['A', 'B', 'C']) + self.assertEqual(cds.data['color'], np.array(['A', 'B', 'C', 'A', 'B', 'C', 'A', 'B', 'C'])) + self.assertEqual(glyph.line_color, {'field': 'color', 'transform': cmapper}) + + def test_vectorfield_alpha_op(self): + vectorfield = VectorField([(0, 0, 0, 1, 0), (0, 1, 0, 1, 0.2), (0, 2, 0, 1, 0.7)], + vdims=['A', 'M', 'alpha']).options(alpha='alpha') + plot = bokeh_renderer.get_plot(vectorfield) + cds = plot.handles['cds'] + glyph = plot.handles['glyph'] + self.assertEqual(cds.data['alpha'], np.array([0, 0.2, 0.7, 0, 0.2, 0.7, 0, 0.2, 0.7])) + self.assertEqual(glyph.line_alpha, {'field': 'alpha'}) + + def test_vectorfield_line_width_op(self): + vectorfield = VectorField([(0, 0, 0, 1, 1), (0, 1, 0, 1, 4), (0, 2, 0, 1, 8)], + vdims=['A', 'M', 'line_width']).options(line_width='line_width') + plot = bokeh_renderer.get_plot(vectorfield) + cds = plot.handles['cds'] + glyph = plot.handles['glyph'] + self.assertEqual(cds.data['line_width'], np.array([1, 4, 8, 1, 4, 8, 1, 4, 8])) + self.assertEqual(glyph.line_width, {'field': 'line_width'}) + + def test_vectorfield_color_index_color_clash(self): + vectorfield = VectorField([(0, 0, 0), (0, 1, 1), (0, 2, 2)], + vdims='color').options(line_color='color', color_index='color') + with ParamLogStream() as log: + plot = bokeh_renderer.get_plot(vectorfield) + log_msg = log.stream.read() + warning = ("%s: Cannot declare style mapping for 'line_color' option " + "and declare a color_index, ignoring the color_index.\n" + % plot.name) + self.assertEqual(log_msg, warning) diff --git a/holoviews/tests/plotting/matplotlib/testvectorfieldplot.py b/holoviews/tests/plotting/matplotlib/testvectorfieldplot.py new file mode 100644 index 0000000000..9bb160aa6e --- /dev/null +++ b/holoviews/tests/plotting/matplotlib/testvectorfieldplot.py @@ -0,0 +1,70 @@ +import numpy as np + +from holoviews.core.overlay import NdOverlay +from holoviews.element import VectorField + +from .testplot import TestMPLPlot, mpl_renderer +from ..utils import ParamLogStream + +try: + from matplotlib import pyplot +except: + pass + + +class TestVectorFieldPlot(TestMPLPlot): + + ########################### + # Styling mapping # + ########################### + + def test_vectorfield_color_op(self): + vectorfield = VectorField([(0, 0, 0, 1, '#000000'), (0, 1, 0, 1,'#FF0000'), (0, 2, 0, 1,'#00FF00')], + vdims=['A', 'M', 'color']).options(color='color') + plot = mpl_renderer.get_plot(vectorfield) + artist = plot.handles['artist'] + self.assertEqual(artist.get_facecolors(), np.array([ + [0, 0, 0, 1], + [1, 0, 0, 1], + [0, 1, 0, 1] + ])) + + def test_vectorfield_linear_color_op(self): + vectorfield = VectorField([(0, 0, 0, 1, 0), (0, 1, 0, 1, 1), (0, 2, 0, 1, 2)], + vdims=['A', 'M', 'color']).options(color='color') + plot = mpl_renderer.get_plot(vectorfield) + artist = plot.handles['artist'] + self.assertEqual(artist.get_array(), np.array([0, 1, 2])) + self.assertEqual(artist.get_clim(), (0, 2)) + + def test_vectorfield_categorical_color_op(self): + vectorfield = VectorField([(0, 0, 0, 1, 'A'), (0, 1, 0, 1, 'B'), (0, 2, 0, 1, 'C')], + vdims=['A', 'M', 'color']).options(color='color') + plot = mpl_renderer.get_plot(vectorfield) + artist = plot.handles['artist'] + self.assertEqual(artist.get_array(), np.array([0, 1, 2])) + self.assertEqual(artist.get_clim(), (0, 2)) + + def test_vectorfield_alpha_op(self): + vectorfield = VectorField([(0, 0, 0, 1, 0), (0, 1, 0, 1, 0.2), (0, 2, 0, 1, 0.7)], + vdims=['A', 'M', 'alpha']).options(alpha='alpha') + with self.assertRaises(Exception): + mpl_renderer.get_plot(vectorfield) + + def test_vectorfield_line_width_op(self): + vectorfield = VectorField([(0, 0, 0, 1, 1), (0, 1, 0, 1, 4), (0, 2, 0, 1, 8)], + vdims=['A', 'M', 'line_width']).options(linewidth='line_width') + plot = mpl_renderer.get_plot(vectorfield) + artist = plot.handles['artist'] + self.assertEqual(artist.get_linewidths(), [1, 4, 8]) + + def test_vectorfield_color_index_color_clash(self): + vectorfield = VectorField([(0, 0, 0, 1, 0), (0, 1, 0, 1, 1), (0, 2, 0, 1, 2)], + vdims=['A', 'M', 'color']).options(color='color', color_index='A') + with ParamLogStream() as log: + plot = mpl_renderer.get_plot(vectorfield) + log_msg = log.stream.read() + warning = ("%s: Cannot declare style mapping for 'color' option " + "and declare a color_index, ignoring the color_index.\n" + % plot.name) + self.assertEqual(log_msg, warning) diff --git a/holoviews/util/ops.py b/holoviews/util/ops.py index 3c9432f4a2..ebf1f27f25 100644 --- a/holoviews/util/ops.py +++ b/holoviews/util/ops.py @@ -136,9 +136,10 @@ def sum(self, **kwargs): return op(self, np.sum, **kwargs) def std(self, **kwargs): return op(self, np.std, **kwargs) def var(self, **kwargs): return op(self, np.var, **kwargs) - def eval(self, dataset, flat=False, ranges={}): - expanded = not ((dataset.interface.gridded and self.dimension in dataset.kdims) or - (dataset.interface.multi and dataset.interface.isscalar(dataset, self.dimension))) + def eval(self, dataset, flat=False, expanded=None, ranges={}): + if expanded is None: + expanded = not ((dataset.interface.gridded and self.dimension in dataset.kdims) or + (dataset.interface.multi and dataset.interface.isscalar(dataset, self.dimension))) if isinstance(dataset, Graph): dataset = dataset if self.dimension in dataset else dataset.nodes data = dataset.dimension_values(self.dimension, expanded=expanded, flat=flat) From 4f73fbda1842f41a54acd3c4b646d49aed192b5c Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Sun, 11 Nov 2018 21:01:09 +0000 Subject: [PATCH 037/117] Finalized style mapping for ErrorBars --- holoviews/plotting/mpl/chart.py | 23 +++++- .../tests/plotting/bokeh/testerrorbarplot.py | 80 +++++++++++++++++++ .../plotting/matplotlib/testerrorbarplot.py | 58 ++++++++++++++ 3 files changed, 159 insertions(+), 2 deletions(-) create mode 100644 holoviews/tests/plotting/matplotlib/testerrorbarplot.py diff --git a/holoviews/plotting/mpl/chart.py b/holoviews/plotting/mpl/chart.py index f600f3c09f..db7831ca33 100644 --- a/holoviews/plotting/mpl/chart.py +++ b/holoviews/plotting/mpl/chart.py @@ -104,7 +104,7 @@ def update_handles(self, key, axis, element, ranges, style): -class ErrorPlot(ChartPlot): +class ErrorPlot(ColorbarPlot): """ ErrorPlot plots the ErrorBar Element type and supporting both horizontal and vertical error bars via the 'horizontal' @@ -129,10 +129,27 @@ def init_artists(self, ax, plot_data, plot_kwargs): bottoms, tops = caps else: _, (bottoms, tops), verts = handles - return {'bottoms': bottoms, 'tops': tops, 'verts': verts[0]} + return {'bottoms': bottoms, 'tops': tops, 'verts': verts[0], 'artist': verts[0]} def get_data(self, element, ranges, style): + with abbreviated_exception(): + style = self._apply_ops(element, ranges, style) + color = style.get('color') + if isinstance(color, np.ndarray): + style['ecolor'] = color + c = style.get('c') + if isinstance(c, np.ndarray): + with abbreviated_exception(): + raise ValueError('Mapping a continuous or categorical ' + 'dimension to a color on a ErrorBarPlot ' + 'is not supported by the {backend} backend. ' + 'To map a dimension to a color supply ' + 'an explicit list of rgba colors.'.format( + backend=self.renderer.backend + ) + ) + style['fmt'] = 'none' dims = element.dimensions() xs, ys = (element.dimension_values(i) for i in range(2)) @@ -168,6 +185,8 @@ def update_handles(self, key, axis, element, ranges, style): new_arrays = [np.array([[xs[i], bys[i]], [xs[i], tys[i]]]) for i in range(samples)] verts.set_paths(new_arrays) + if 'ecolor' in style: + verts.set_edgecolors(style['ecolor']) if bottoms: bottoms.set_xdata(bxs) diff --git a/holoviews/tests/plotting/bokeh/testerrorbarplot.py b/holoviews/tests/plotting/bokeh/testerrorbarplot.py index 9c72991fd6..6f31511b30 100644 --- a/holoviews/tests/plotting/bokeh/testerrorbarplot.py +++ b/holoviews/tests/plotting/bokeh/testerrorbarplot.py @@ -1,5 +1,11 @@ +import numpy as np + +from bokeh.models import CategoricalColorMapper, LinearColorMapper + +from holoviews.core.overlay import NdOverlay from holoviews.element import ErrorBars +from ..utils import ParamLogStream from .testplot import TestBokehPlot, bokeh_renderer @@ -58,3 +64,77 @@ def test_errorbars_padding_logy(self): self.assertEqual(x_range.end, 3.2) self.assertEqual(y_range.start, 0.41158562699652224) self.assertEqual(y_range.end, 4.2518491541367327) + + ########################### + # Styling mapping # + ########################### + + def test_errorbars_color_op(self): + errorbars = ErrorBars([(0, 0, 0.1, 0.2, '#000'), (0, 1, 0.2, 0.4, '#F00'), (0, 2, 0.6, 1.2, '#0F0')], + vdims=['y', 'perr', 'nerr', 'color']).options(color='color') + plot = bokeh_renderer.get_plot(errorbars) + cds = plot.handles['cds'] + glyph = plot.handles['glyph'] + self.assertEqual(cds.data['color'], np.array(['#000', '#F00', '#0F0'])) + self.assertEqual(glyph.line_color, {'field': 'color'}) + + def test_errorbars_linear_color_op(self): + errorbars = ErrorBars([(0, 0, 0.1, 0.2, 0), (0, 1, 0.2, 0.4, 1), (0, 2, 0.6, 1.2, 2)], + vdims=['y', 'perr', 'nerr', 'color']).options(color='color') + plot = bokeh_renderer.get_plot(errorbars) + cds = plot.handles['cds'] + glyph = plot.handles['glyph'] + cmapper = plot.handles['color_color_mapper'] + self.assertTrue(cmapper, LinearColorMapper) + self.assertEqual(cmapper.low, 0) + self.assertEqual(cmapper.high, 2) + self.assertEqual(cds.data['color'], np.array([0, 1, 2])) + self.assertEqual(glyph.line_color, {'field': 'color', 'transform': cmapper}) + + def test_errorbars_categorical_color_op(self): + errorbars = ErrorBars([(0, 0, 0.1, 0.2, 'A'), (0, 1, 0.2, 0.4, 'B'), (0, 2, 0.6, 1.2, 'C')], + vdims=['y', 'perr', 'nerr', 'color']).options(color='color') + plot = bokeh_renderer.get_plot(errorbars) + cds = plot.handles['cds'] + glyph = plot.handles['glyph'] + cmapper = plot.handles['color_color_mapper'] + self.assertTrue(cmapper, CategoricalColorMapper) + self.assertEqual(cmapper.factors, ['A', 'B', 'C']) + self.assertEqual(cds.data['color'], np.array(['A', 'B', 'C'])) + self.assertEqual(glyph.line_color, {'field': 'color', 'transform': cmapper}) + + def test_errorbars_line_color_op(self): + errorbars = ErrorBars([(0, 0, 0.1, 0.2, '#000'), (0, 1, 0.2, 0.4, '#F00'), (0, 2, 0.6, 1.2, '#0F0')], + vdims=['y', 'perr', 'nerr', 'color']).options(line_color='color') + plot = bokeh_renderer.get_plot(errorbars) + cds = plot.handles['cds'] + glyph = plot.handles['glyph'] + self.assertEqual(cds.data['line_color'], np.array(['#000', '#F00', '#0F0'])) + self.assertEqual(glyph.line_color, {'field': 'line_color'}) + + def test_errorbars_alpha_op(self): + errorbars = ErrorBars([(0, 0, 0.1, 0.2, 0), (0, 1, 0.2, 0.4, 0.2), (0, 2, 0.6, 1.2, 0.7)], + vdims=['y', 'perr', 'nerr', 'alpha']).options(alpha='alpha') + plot = bokeh_renderer.get_plot(errorbars) + cds = plot.handles['cds'] + glyph = plot.handles['glyph'] + self.assertEqual(cds.data['alpha'], np.array([0, 0.2, 0.7])) + self.assertEqual(glyph.line_alpha, {'field': 'alpha'}) + + def test_errorbars_line_alpha_op(self): + errorbars = ErrorBars([(0, 0, 0.1, 0.2, 0), (0, 1, 0.2, 0.4, 0.2), (0, 2, 0.6, 1.2, 0.7)], + vdims=['y', 'perr', 'nerr', 'alpha']).options(line_alpha='alpha') + plot = bokeh_renderer.get_plot(errorbars) + cds = plot.handles['cds'] + glyph = plot.handles['glyph'] + self.assertEqual(cds.data['line_alpha'], np.array([0, 0.2, 0.7])) + self.assertEqual(glyph.line_alpha, {'field': 'line_alpha'}) + + def test_errorbars_line_width_op(self): + errorbars = ErrorBars([(0, 0, 0.1, 0.2, 1), (0, 1, 0.2, 0.4, 4), (0, 2, 0.6, 1.2, 8)], + vdims=['y', 'perr', 'nerr', 'line_width']).options(line_width='line_width') + plot = bokeh_renderer.get_plot(errorbars) + cds = plot.handles['cds'] + glyph = plot.handles['glyph'] + self.assertEqual(cds.data['line_width'], np.array([1, 4, 8])) + self.assertEqual(glyph.line_width, {'field': 'line_width'}) diff --git a/holoviews/tests/plotting/matplotlib/testerrorbarplot.py b/holoviews/tests/plotting/matplotlib/testerrorbarplot.py new file mode 100644 index 0000000000..fcd31b780c --- /dev/null +++ b/holoviews/tests/plotting/matplotlib/testerrorbarplot.py @@ -0,0 +1,58 @@ +import numpy as np + +from holoviews.element import ErrorBars + +from ..utils import ParamLogStream +from .testplot import TestMPLPlot, mpl_renderer + + +class TestErrorBarPlot(TestMPLPlot): + + + ########################### + # Styling mapping # + ########################### + + def test_errorbars_color_op(self): + errorbars = ErrorBars([(0, 0, 0.1, 0.2, '#000000'), (0, 1, 0.2, 0.4, '#FF0000'), (0, 2, 0.6, 1.2, '#00FF00')], + vdims=['y', 'perr', 'nerr', 'color']).options(color='color') + plot = mpl_renderer.get_plot(errorbars) + artist = plot.handles['artist'] + self.assertEqual(artist.get_edgecolors(), + np.array([[0, 0, 0, 1], [1, 0, 0, 1], [0, 1, 0, 1]])) + + def test_errorbars_linear_color_op(self): + errorbars = ErrorBars([(0, 0, 0.1, 0.2, 0), (0, 1, 0.2, 0.4, 1), (0, 2, 0.6, 1.2, 2)], + vdims=['y', 'perr', 'nerr', 'color']).options(color='color') + with self.assertRaises(Exception): + mpl_renderer.get_plot(errorbars) + + def test_errorbars_categorical_color_op(self): + errorbars = ErrorBars([(0, 0, 0.1, 0.2, 'A'), (0, 1, 0.2, 0.4, 'B'), (0, 2, 0.6, 1.2, 'C')], + vdims=['y', 'perr', 'nerr', 'color']).options(color='color') + with self.assertRaises(Exception): + mpl_renderer.get_plot(errorbars) + + def test_errorbars_line_color_op(self): + errorbars = ErrorBars([(0, 0, 0.1, 0.2, '#000000'), (0, 1, 0.2, 0.4, '#FF0000'), (0, 2, 0.6, 1.2, '#00FF00')], + vdims=['y', 'perr', 'nerr', 'color']).options(ecolor='color') + plot = mpl_renderer.get_plot(errorbars) + artist = plot.handles['artist'] + children = artist.get_children() + for c, w in zip(children, ['#000000', '#FF0000', '#00FF00']): + self.assertEqual(c.get_edgecolor(), tuple(c/255. for c in hex2rgb(w))+(1,)) + + def test_errorbars_alpha_op(self): + errorbars = ErrorBars([(0, 0, 0), (0, 1, 0.2), (0, 2, 0.7)], + vdims=['y', 'alpha']).options(alpha='alpha') + with self.assertRaises(Exception): + mpl_renderer.get_plot(errorbars) + + def test_errorbars_line_width_op(self): + errorbars = ErrorBars([(0, 0, 0.1, 0.2, 1), (0, 1, 0.2, 0.4, 4), (0, 2, 0.6, 1.2, 8)], + vdims=['y', 'perr', 'nerr', 'line_width']).options(linewidth='line_width') + plot = mpl_renderer.get_plot(errorbars) + artist = plot.handles['artist'] + children = artist.get_children() + for c, w in zip(children, np.array([1, 4, 8])): + self.assertEqual(c.get_linewidth(), w) From b36925162ca9d5f8db082ea7390dea9f3f1e9118 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Sun, 11 Nov 2018 21:03:13 +0000 Subject: [PATCH 038/117] Finalized CurvePlot and AreaPlot style mapping --- holoviews/plotting/mpl/chart.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/holoviews/plotting/mpl/chart.py b/holoviews/plotting/mpl/chart.py index db7831ca33..d447e51cf2 100644 --- a/holoviews/plotting/mpl/chart.py +++ b/holoviews/plotting/mpl/chart.py @@ -75,6 +75,9 @@ class CurvePlot(ChartPlot): _plot_methods = dict(single='plot') def get_data(self, element, ranges, style): + with abbreviated_exception(): + style = self._apply_ops(element, ranges, style) + if 'steps' in self.interpolation: element = interpolate_curve(element, interpolation=self.interpolation) xs = element.dimension_values(0) @@ -212,6 +215,9 @@ class AreaPlot(ChartPlot): _plot_methods = dict(single='fill_between') def get_data(self, element, ranges, style): + with abbreviated_exception(): + style = self._apply_ops(element, ranges, style) + xs = element.dimension_values(0) ys = [element.dimension_values(vdim) for vdim in element.vdims] return tuple([xs]+ys), style, {} From 0f8a81839d3ef1104533abb2ea60c3af321e3c2d Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Mon, 12 Nov 2018 01:04:58 +0000 Subject: [PATCH 039/117] Various updates to examples --- .../gallery/demos/bokeh/autompg_histogram.ipynb | 2 +- examples/gallery/demos/bokeh/autompg_violins.ipynb | 2 +- examples/gallery/demos/bokeh/boxplot_chart.ipynb | 2 +- .../gallery/demos/bokeh/choropleth_data_link.ipynb | 4 ++-- examples/gallery/demos/bokeh/dragon_curve.ipynb | 14 ++------------ .../gallery/demos/bokeh/dropdown_economic.ipynb | 2 +- .../demos/matplotlib/dropdown_economic.ipynb | 2 +- 7 files changed, 9 insertions(+), 19 deletions(-) diff --git a/examples/gallery/demos/bokeh/autompg_histogram.ipynb b/examples/gallery/demos/bokeh/autompg_histogram.ipynb index 6c209a2082..b9817400fe 100644 --- a/examples/gallery/demos/bokeh/autompg_histogram.ipynb +++ b/examples/gallery/demos/bokeh/autompg_histogram.ipynb @@ -53,7 +53,7 @@ "outputs": [], "source": [ "%%opts Histogram (alpha=0.9) [width=600]\n", - "autompg_ds.hist(dimension='mpg', groupby='cyl', adjoin=False)" + "autompg_ds.hist(dimension='mpg', groupby='cyl', bin_range=(9, 46), bins=40, adjoin=False)" ] } ], diff --git a/examples/gallery/demos/bokeh/autompg_violins.ipynb b/examples/gallery/demos/bokeh/autompg_violins.ipynb index 04fb690400..2a7f1735a5 100644 --- a/examples/gallery/demos/bokeh/autompg_violins.ipynb +++ b/examples/gallery/demos/bokeh/autompg_violins.ipynb @@ -50,7 +50,7 @@ "metadata": {}, "outputs": [], "source": [ - "violin.options(height=500, width=900)" + "violin.options(height=500, width=900, violin_fill_color=('Year', str), cmap='Set1')" ] } ], diff --git a/examples/gallery/demos/bokeh/boxplot_chart.ipynb b/examples/gallery/demos/bokeh/boxplot_chart.ipynb index c89dc74e7e..e75efb0506 100644 --- a/examples/gallery/demos/bokeh/boxplot_chart.ipynb +++ b/examples/gallery/demos/bokeh/boxplot_chart.ipynb @@ -53,7 +53,7 @@ "metadata": {}, "outputs": [], "source": [ - "boxwhisker.options(show_legend=False, width=400)" + "boxwhisker.options(show_legend=False, width=600, box_fill_color=('origin', str), cmap='Set1')" ] } ], diff --git a/examples/gallery/demos/bokeh/choropleth_data_link.ipynb b/examples/gallery/demos/bokeh/choropleth_data_link.ipynb index d296861299..5d67e4ff1a 100644 --- a/examples/gallery/demos/bokeh/choropleth_data_link.ipynb +++ b/examples/gallery/demos/bokeh/choropleth_data_link.ipynb @@ -70,8 +70,8 @@ "metadata": {}, "outputs": [], "source": [ - "choropleth.options(width=500, height=500, tools=['hover', 'tap'], xaxis=None, yaxis=None, color_index='Unemployment', clone=False)\n", - "table.options(height=428, clone=False)\n", + "choropleth = choropleth.options(width=500, height=500, tools=['hover', 'tap'], xaxis=None, yaxis=None, color_index='Unemployment')\n", + "table = table.options(height=428)\n", "\n", "# Link the choropleth and the table\n", "DataLink(choropleth, table)\n", diff --git a/examples/gallery/demos/bokeh/dragon_curve.ipynb b/examples/gallery/demos/bokeh/dragon_curve.ipynb index 63a341fc98..caec9b74af 100644 --- a/examples/gallery/demos/bokeh/dragon_curve.ipynb +++ b/examples/gallery/demos/bokeh/dragon_curve.ipynb @@ -107,21 +107,11 @@ "metadata": {}, "outputs": [], "source": [ - "%%opts Path {+framewise} [xaxis=None yaxis=None title_format=''] (color='black' line_width=1)\n", + "%%opts Path {+framewise} [xaxis=None yaxis=None title_format='' padding=0.1] (color='black' line_width=1)\n", "\n", - "def pad_extents(path):\n", - " \"Add 5% padding around the path\"\n", - " minx, maxx = path.range('x')\n", - " miny, maxy = path.range('y')\n", - " xpadding = ((maxx-minx) * 0.1)/2\n", - " ypadding = ((maxy-miny) * 0.1)/2\n", - " path.extents = (minx-xpadding, miny-ypadding, maxx+xpadding, maxy+ypadding)\n", - " return path\n", - " \n", "hmap = hv.HoloMap(kdims='Iteration')\n", "for i in range(7,17):\n", - " path = DragonCurve(-200, 0, i).path\n", - " hmap[i] = pad_extents(path)\n", + " hmap[i] = DragonCurve(-200, 0, i).path\n", "hmap" ] } diff --git a/examples/gallery/demos/bokeh/dropdown_economic.ipynb b/examples/gallery/demos/bokeh/dropdown_economic.ipynb index fb1347f812..91a8d8788f 100644 --- a/examples/gallery/demos/bokeh/dropdown_economic.ipynb +++ b/examples/gallery/demos/bokeh/dropdown_economic.ipynb @@ -54,7 +54,7 @@ "outputs": [], "source": [ "%%opts Overlay [width=700 height=400 show_frame=False]\n", - "%%opts Curve (color='k') Scatter [color_index=2 size_index=2 scaling_factor=1.4] (cmap='Blues' line_color='k')\n", + "%%opts Curve (color='k') Scatter (color='Unemployment' size=op('Unemployment')*1.5 cmap='Blues' line_color='k')\n", "%%opts VLine (color='k' line_width=1)\n", "%%opts Text (text_font_size='13px')\n", "gdp_curves = macro.to.curve('Year', 'GDP Growth')\n", diff --git a/examples/gallery/demos/matplotlib/dropdown_economic.ipynb b/examples/gallery/demos/matplotlib/dropdown_economic.ipynb index 9ce2a8b483..c93bb0944f 100644 --- a/examples/gallery/demos/matplotlib/dropdown_economic.ipynb +++ b/examples/gallery/demos/matplotlib/dropdown_economic.ipynb @@ -53,7 +53,7 @@ "metadata": {}, "outputs": [], "source": [ - "%%opts Curve (color='k') Scatter [color_index=2 size_index=2 scaling_factor=1.4] (cmap='Blues' edgecolors='k')\n", + "%%opts Curve (color='k') Scatter (color='Unemployment' s=op('Unemployment')*10 cmap='Blues' edgecolors='k')\n", "%%opts Overlay [show_frame=True aspect=2, fig_size=250, show_frame=False]\n", "gdp_curves = macro.to.curve('Year', 'GDP Growth')\n", "gdp_unem_scatter = macro.to.scatter('Year', ['GDP Growth', 'Unemployment'])\n", From 81602867bfce6bb08962e0fd973163c5bad88532 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Mon, 12 Nov 2018 01:07:07 +0000 Subject: [PATCH 040/117] Resolve tuple op specs --- holoviews/plotting/bokeh/element.py | 6 ++++++ holoviews/plotting/mpl/element.py | 5 +++++ 2 files changed, 11 insertions(+) diff --git a/holoviews/plotting/bokeh/element.py b/holoviews/plotting/bokeh/element.py index 09be58fa9a..ab8a73f06e 100644 --- a/holoviews/plotting/bokeh/element.py +++ b/holoviews/plotting/bokeh/element.py @@ -665,6 +665,12 @@ def _apply_ops(self, element, source, ranges, style, group=None): v = op(v) elif any(d==v for d in self.overlay_dims): v = op([d for d in self.overlay_dims if d==v][0]) + elif isinstance(v, tuple) and v and isinstance(v[0], (util.basestring, tuple, op)): + try: + v = op.resolve_spec(v) + except: + continue + if not isinstance(v, op) or (group is not None and not k.startswith(group)): continue dname = v.dimension.name diff --git a/holoviews/plotting/mpl/element.py b/holoviews/plotting/mpl/element.py index b3f5b9fedf..e0182c2ae0 100644 --- a/holoviews/plotting/mpl/element.py +++ b/holoviews/plotting/mpl/element.py @@ -523,6 +523,11 @@ def _apply_ops(self, element, ranges, style): v = op(v) elif any(d==v for d in self.overlay_dims): v = op([d for d in self.overlay_dims if d==v][0]) + elif isinstance(v, tuple) and v and isinstance(v[0], (util.basestring, tuple, op)): + try: + v = op.resolve_spec(v) + except: + continue if not isinstance(v, op): continue From 98218c8e134889995dcb52a52e2a38b3d66b2e2b Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Mon, 12 Nov 2018 01:07:25 +0000 Subject: [PATCH 041/117] Fix for CompositeElementPlot --- holoviews/plotting/bokeh/element.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/holoviews/plotting/bokeh/element.py b/holoviews/plotting/bokeh/element.py index ab8a73f06e..c0a60de2d8 100644 --- a/holoviews/plotting/bokeh/element.py +++ b/holoviews/plotting/bokeh/element.py @@ -1074,8 +1074,9 @@ def _init_glyphs(self, plot, element, ranges, source, data=None, mapping=None, s source = self._init_datasource(ds_data) source_cache[id(ds_data)] = source self.handles[key+'_source'] = source + group_style = dict(style) style_group = self._style_groups.get('_'.join(key.split('_')[:-1])) - properties = self._glyph_properties(plot, element, source, ranges, style, style_group) + properties = self._glyph_properties(plot, element, source, ranges, group_style, style_group) properties = self._process_properties(key, properties, mapping.get(key, {})) with abbreviated_exception(): @@ -1128,10 +1129,10 @@ def _update_glyphs(self, element, ranges): gdata = data.get(key) source = self.handles[key+'_source'] glyph = self.handles.get(key+'_glyph') - if glyph: + group_style = dict(style) style_group = self._style_groups.get('_'.join(key.split('_')[:-1])) - properties = self._glyph_properties(plot, element, source, ranges, style, style_group) + properties = self._glyph_properties(plot, element, source, ranges, group_style, style_group) properties = self._process_properties(key, properties, mapping[key]) renderer = self.handles.get(key+'_glyph_renderer') with abbreviated_exception(): From 4a6fa7a2403221124b2d6846e65c128e81665281 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Mon, 12 Nov 2018 01:07:45 +0000 Subject: [PATCH 042/117] Fixed bug in DataLinkCallback --- holoviews/plotting/bokeh/callbacks.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/holoviews/plotting/bokeh/callbacks.py b/holoviews/plotting/bokeh/callbacks.py index e32bbbd9a5..0156877114 100644 --- a/holoviews/plotting/bokeh/callbacks.py +++ b/holoviews/plotting/bokeh/callbacks.py @@ -1237,10 +1237,10 @@ def __init__(self, root_model, link, source_plot, target_plot): for k, v in tgt_cds.data.items(): if k not in src_cds.data: continue - col = src_cds.data[k] + col = np.asarray(src_cds.data[k]) if not ((isscalar(v) and v == col) or (v.dtype.kind not in 'iufc' and (v==col).all()) or - np.allclose(v, src_cds.data[k])): + np.allclose(v, np.asarray(src_cds.data[k]))): raise ValueError('DataLink can only be applied if overlapping ' 'dimension values are equal, %s column on source ' 'does not match target' % k) From 1d71f7965ab038e81bb74bd848e7905caf074e83 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Mon, 12 Nov 2018 01:08:30 +0000 Subject: [PATCH 043/117] Fixed BoxWhiskerPlot bug --- holoviews/plotting/bokeh/stats.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/holoviews/plotting/bokeh/stats.py b/holoviews/plotting/bokeh/stats.py index 37674a5f6e..1a96f0f946 100644 --- a/holoviews/plotting/bokeh/stats.py +++ b/holoviews/plotting/bokeh/stats.py @@ -110,12 +110,14 @@ def _get_factors(self, element): Get factors for categorical axes. """ if not element.kdims: - xfactors, yfactors = [element.label], [] + xfactors, yfactors = [element.label], [] else: factors = [tuple(d.pprint_value(v) for d, v in zip(element.kdims, key)) for key in element.groupby(element.kdims).data.keys()] factors = [f[0] if len(f) == 1 else f for f in factors] xfactors, yfactors = factors, [] + if element.ndims > 1: + xfactors = sorted(xfactors) return (yfactors, xfactors) if self.invert_axes else (xfactors, yfactors) def _postprocess_hover(self, renderer, source): @@ -390,9 +392,7 @@ def get_data(self, element, ranges, style): scatter_map = {'x': 'x', 'y': 'y'} bar_glyph = 'vbar' - elstyle = self.lookup_options(element, 'style') kwargs = {'bandwidth': self.bandwidth, 'cut': self.cut} - mapping, data = {}, {} patches_data, seg_data, bar_data, scatter_data = (defaultdict(list) for i in range(4)) for i, (key, g) in enumerate(groups.items()): From 232ac40f50aad2769420cd97fdb28f8e5d3e1b9c Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Thu, 15 Nov 2018 13:10:31 +0000 Subject: [PATCH 044/117] Added common function to ops --- holoviews/util/ops.py | 27 ++++++++++++++++++++++----- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/holoviews/util/ops.py b/holoviews/util/ops.py index ebf1f27f25..aa00c6c714 100644 --- a/holoviews/util/ops.py +++ b/holoviews/util/ops.py @@ -129,12 +129,29 @@ def __array_ufunc__(self, *args, **kwargs): kwargs = {k: v for k, v in kwargs.items() if v is not None} return op(self, ufunc, **kwargs) - def max(self, **kwargs): return op(self, np.max, **kwargs) + def max(self, **kwargs): return op(self, np.max, **kwargs) def mean(self, **kwargs): return op(self, np.mean, **kwargs) - def min(self, **kwargs): return op(self, np.min, **kwargs) - def sum(self, **kwargs): return op(self, np.sum, **kwargs) - def std(self, **kwargs): return op(self, np.std, **kwargs) - def var(self, **kwargs): return op(self, np.var, **kwargs) + def min(self, **kwargs): return op(self, np.min, **kwargs) + def sum(self, **kwargs): return op(self, np.sum, **kwargs) + def std(self, **kwargs): return op(self, np.std, **kwargs) + def var(self, **kwargs): return op(self, np.var, **kwargs) + def astype(self, dtype): return op(self, np.asarray, dtype=dtype) + + ## Custom functions + + def norm(self): + """ + Normalizes the data into the given range + """ + return op(self, norm_fn) + + def cat(self, categories, empty=None): + cat_op = op(self, cat_fn, categories=categories, empty=empty) + return cat_op + + def bin(self, bins, labels=None): + bin_op = op(self, bin_fn, categories=categories, empty=empty) + return bin_op def eval(self, dataset, flat=False, expanded=None, ranges={}): if expanded is None: From 87411d947aae6b515674a2c4fd106379760d30a2 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Sat, 17 Nov 2018 13:17:38 +0000 Subject: [PATCH 045/117] Various updates for Graph and stats elements --- holoviews/plotting/bokeh/chart.py | 6 +- holoviews/plotting/bokeh/element.py | 16 ++- holoviews/plotting/bokeh/graphs.py | 12 +- holoviews/plotting/bokeh/hex_tiles.py | 4 +- holoviews/plotting/bokeh/stats.py | 5 +- holoviews/plotting/mpl/element.py | 25 +++- holoviews/plotting/mpl/graphs.py | 15 +- holoviews/plotting/mpl/hex_tiles.py | 2 + holoviews/plotting/mpl/path.py | 7 + holoviews/plotting/mpl/stats.py | 14 +- holoviews/tests/plotting/bokeh/testbarplot.py | 2 +- .../tests/plotting/bokeh/testgraphplot.py | 130 +++++++++++++++++- holoviews/tests/plotting/bokeh/testlabels.py | 4 +- .../tests/plotting/bokeh/testpointplot.py | 2 +- .../tests/plotting/bokeh/testviolinplot.py | 12 +- .../plotting/matplotlib/testgraphplot.py | 83 ++++++++++- 16 files changed, 295 insertions(+), 44 deletions(-) diff --git a/holoviews/plotting/bokeh/chart.py b/holoviews/plotting/bokeh/chart.py index fc5097fe11..399f63cd25 100644 --- a/holoviews/plotting/bokeh/chart.py +++ b/holoviews/plotting/bokeh/chart.py @@ -783,12 +783,12 @@ def _get_factors(self, element): if gdim and not sdim: gvals = element.dimension_values(gdim, False) gvals = [g if gvals.dtype.kind in 'SU' else gdim.pprint_value(g) for g in gvals] - coords = ([(x, g) for x in xvals for g in gvals], []) - else: - coords = (xvals, []) + xvals = sorted([(x, g) for x in xvals for g in gvals]) + coords = xvals, [] if self.invert_axes: coords = coords[::-1] return coords + def _get_axis_labels(self, *args, **kwargs): """ Override axis mapping by setting the first key and value diff --git a/holoviews/plotting/bokeh/element.py b/holoviews/plotting/bokeh/element.py index c0a60de2d8..3c590a9a03 100644 --- a/holoviews/plotting/bokeh/element.py +++ b/holoviews/plotting/bokeh/element.py @@ -661,7 +661,7 @@ def _apply_ops(self, element, source, ranges, style, group=None): prefix = group+'_' if group else '' for k, v in dict(style).items(): if isinstance(v, util.basestring): - if v in element: + if v in element or (isinstance(element, Graph) and v in element.nodes): v = op(v) elif any(d==v for d in self.overlay_dims): v = op([d for d in self.overlay_dims if d==v][0]) @@ -674,6 +674,7 @@ def _apply_ops(self, element, source, ranges, style, group=None): if not isinstance(v, op) or (group is not None and not k.startswith(group)): continue dname = v.dimension.name + if k == 'marker' and dname in markers: continue elif (dname not in element and v.dimension not in self.overlay_dims and @@ -731,7 +732,8 @@ def _apply_ops(self, element, source, ranges, style, group=None): if val.dtype.kind not in 'if': kwargs['factors'] = np.unique(val) cmapper = self._get_colormapper(v.dimension, element, ranges, - style, name=dname+'_color_mapper', **kwargs) + style, name=k+'_color_mapper', + group=group, **kwargs) key = {'field': k, 'transform': cmapper} new_style[k] = key @@ -742,10 +744,10 @@ def _apply_ops(self, element, source, ranges, style, group=None): continue fill_style = new_style.get(prefix+'fill_'+s) if fill_style and validate(s, fill_style): - new_style.pop(prefix+'fill_'+s) + new_style[prefix+'fill_'+s] = value line_style = new_style.get(prefix+'line_'+s) if line_style and validate(s, line_style): - new_style.pop(prefix+'line_'+s) + new_style[prefix+'line_'+s] = value return new_style @@ -1061,6 +1063,7 @@ def _init_glyphs(self, plot, element, ranges, source, data=None, mapping=None, s style = self.style[self.cyclic_index] data, mapping, style = self.get_data(element, ranges, style) + keys = glyph_order(dict(data, **mapping), self._draw_order) source_cache = {} @@ -1246,7 +1249,7 @@ def _draw_colorbar(self, plot, color_mapper): def _get_colormapper(self, dim, element, ranges, style, factors=None, colors=None, - name='color_mapper'): + group=None, name='color_mapper'): # The initial colormapper instance is cached the first time # and then only updated if dim is None and colors is None: @@ -1277,7 +1280,8 @@ def _get_colormapper(self, dim, element, ranges, style, factors=None, colors=Non else: low, high = None, None - cmap = colors or style.pop('cmap', 'viridis') + prefix = '' if group is None else group+'_' + cmap = colors or style.get(prefix+'cmap', style.pop('cmap', 'viridis')) nan_colors = {k: rgba_tuple(v) for k, v in self.clipping_colors.items()} if isinstance(cmap, dict): if factors is None: diff --git a/holoviews/plotting/bokeh/graphs.py b/holoviews/plotting/bokeh/graphs.py index d6093b0a51..f054eaf74b 100644 --- a/holoviews/plotting/bokeh/graphs.py +++ b/holoviews/plotting/bokeh/graphs.py @@ -79,6 +79,7 @@ def _hover_opts(self, element): def get_extents(self, element, ranges, range_type='combined'): return super(GraphPlot, self).get_extents(element.nodes, ranges, range_type) + def _get_axis_labels(self, *args, **kwargs): """ Override axis labels to group all key dimensions together. @@ -127,7 +128,7 @@ def _get_edge_colors(self, element, ranges, edge_data, edge_mapping, style): edge_data[field] = cvals edge_style = dict(style, cmap=cmap) mapper = self._get_colormapper(cdim, element, ranges, edge_style, - factors, colors, 'edge_colormapper') + factors, colors, 'edge', 'edge_colormapper') transform = {'field': field, 'transform': mapper} color_type = 'fill_color' if self.filled else 'line_color' edge_mapping['edge_'+color_type] = transform @@ -248,9 +249,14 @@ def _init_glyphs(self, plot, element, ranges, source): continue source = self._init_datasource(data.pop(key, {})) self.handles[key+'_source'] = source + group_style = dict(style) style_group = self._style_groups.get('_'.join(key.split('_')[:-1])) - glyph_props = self._glyph_properties(plot, element, source, ranges, style, style_group) - properties.update(glyph_props) + others = [sg for sg in self._style_groups.values() if sg != style_group] + glyph_props = self._glyph_properties(plot, element, source, ranges, group_style, style_group) + for k, p in glyph_props.items(): + if any(k.startswith(o) for o in others): + continue + properties[k] = p mappings.update(mapping.pop(key, {})) properties = {p: v for p, v in properties.items() if p not in ('legend', 'source')} properties.update(mappings) diff --git a/holoviews/plotting/bokeh/hex_tiles.py b/holoviews/plotting/bokeh/hex_tiles.py index 7b90ce9223..51c1a08f3e 100644 --- a/holoviews/plotting/bokeh/hex_tiles.py +++ b/holoviews/plotting/bokeh/hex_tiles.py @@ -132,7 +132,9 @@ class HexTilesPlot(ColorbarPlot): _plot_methods = dict(single='hex_tile') - style_opts = ['cmap', 'color'] + line_properties + fill_properties + style_opts = ['cmap', 'color', 'scale'] + line_properties + fill_properties + + _no_op_styles = ['cmap', 'line_dash'] def _hover_opts(self, element): if self.aggregator is np.size: diff --git a/holoviews/plotting/bokeh/stats.py b/holoviews/plotting/bokeh/stats.py index 1a96f0f946..3d8f038f1c 100644 --- a/holoviews/plotting/bokeh/stats.py +++ b/holoviews/plotting/bokeh/stats.py @@ -97,7 +97,10 @@ def _get_axis_labels(self, *args, **kwargs): return xlabel, ylabel, None def _glyph_properties(self, plot, element, source, ranges, style, group=None): - element = element.aggregate(function=np.mean) + if element.ndims > 0: + element = element.aggregate(function=np.mean) + else: + element = element.clone([(element.aggregate(function=np.mean),)]) with abbreviated_exception(): new_style = self._apply_ops(element, source, ranges, style, group) properties = dict(new_style, source=source) diff --git a/holoviews/plotting/mpl/element.py b/holoviews/plotting/mpl/element.py index e0182c2ae0..1839bcdb4b 100644 --- a/holoviews/plotting/mpl/element.py +++ b/holoviews/plotting/mpl/element.py @@ -519,7 +519,7 @@ def _apply_ops(self, element, ranges, style): new_style = dict(style) for k, v in style.items(): if isinstance(v, util.basestring): - if v in element: + if v in element or (isinstance(element, Graph) and v in element.nodes): v = op(v) elif any(d==v for d in self.overlay_dims): v = op([d for d in self.overlay_dims if d==v][0]) @@ -529,11 +529,13 @@ def _apply_ops(self, element, ranges, style): except: continue + if not isinstance(v, op): continue dname = v.dimension.name - if dname not in element and v.dimension not in self.overlay_dims: + if (dname not in element and v.dimension not in self.overlay_dims and + not (isinstance(element, Graph) and v.dimension in element.nodes)): new_style.pop(k) self.warning('Specified %s op %r could not be applied, %s dimension ' 'could not be found' % (k, v, v.dimension)) @@ -560,12 +562,16 @@ def _apply_ops(self, element, ranges, style): ) ) - if k in ('c', 'color') and isinstance(val, np.ndarray) and all(not is_color(c) for c in val): + style_groups = getattr(self, '_style_groups', []) + groups = [sg for sg in style_groups if k.startswith(sg)] + group = groups[0] if groups else None + prefix = '' if group is None else group+'_' + if k in (prefix+'c', prefix+'color') and isinstance(val, np.ndarray) and all(not is_color(c) for c in val): new_style.pop(k) self._norm_kwargs(element, ranges, new_style, v.dimension, val) if val.dtype.kind in 'OSUM': val = categorize_colors(val) - k = 'c' + k = prefix+'c' new_style[k] = val @@ -573,11 +579,16 @@ def _apply_ops(self, element, ranges, style): # If mapped to color/alpha override static fill/line style if k == 'c': new_style.pop('color', None) - if k in ('c', 'color') and isinstance(val, np.ndarray): - fill_style = new_style.get('facecolor') + + style_groups = getattr(self, '_style_groups', []) + groups = [sg for sg in style_groups if k.startswith(sg)] + group = groups[0] if groups else None + prefix = '' if group is None else group+'_' + if k in (prefix+'c', prefix+'color') and isinstance(val, np.ndarray): + fill_style = new_style.get(prefix+'facecolor') if fill_style and is_color(fill_style): new_style.pop('facecolor') - line_style = new_style.get('edgecolor') + line_style = new_style.get(prefix+'edgecolor') if line_style and is_color(line_style): new_style.pop('edgecolor') elif k == 'facecolors' and not isinstance(new_style.get('color', new_style.get('c')), np.ndarray): diff --git a/holoviews/plotting/mpl/graphs.py b/holoviews/plotting/mpl/graphs.py index 3af764db21..707bb2777f 100644 --- a/holoviews/plotting/mpl/graphs.py +++ b/holoviews/plotting/mpl/graphs.py @@ -4,8 +4,8 @@ from matplotlib.collections import LineCollection, PolyCollection from ...core.data import Dataset -from ...core.options import Cycle -from ...core.util import basestring, unique_array, search_indices, max_range +from ...core.options import Cycle, abbreviated_exception +from ...core.util import basestring, unique_array, search_indices, max_range, is_number from ..util import process_cmap from .element import ColorbarPlot @@ -55,7 +55,7 @@ def _compute_styles(self, element, ranges, style): style.pop('node_color', None) if 'c' in style: self._norm_kwargs(element.nodes, ranges, style, cdim) - elif color: + elif color and 'node_color' in style: style['c'] = style.pop('node_color') style['node_edgecolors'] = style.pop('node_edgecolors', 'none') @@ -97,6 +97,9 @@ def _compute_styles(self, element, ranges, style): def get_data(self, element, ranges, style): + with abbreviated_exception(): + style = self._apply_ops(element, ranges, style) + xidx, yidx = (1, 0) if self.invert_axes else (0, 1) pxs, pys = (element.nodes.dimension_values(i) for i in range(2)) dims = element.nodes.dimensions() @@ -120,6 +123,8 @@ def init_artists(self, ax, plot_args, plot_kwargs): for k, v in plot_kwargs.items() if not any(k.startswith(p) for p in groups) and k not in color_opts} + if 'c' in edge_opts: + edge_opts['array'] = edge_opts.pop('c') paths = plot_args['edges'] if self.filled: coll = PolyCollection @@ -127,7 +132,6 @@ def init_artists(self, ax, plot_args, plot_kwargs): edge_opts['facecolors'] = edge_opts.pop('colors') else: coll = LineCollection - print(edge_opts) edges = coll(paths, **edge_opts) ax.add_collection(edges) @@ -137,7 +141,8 @@ def init_artists(self, ax, plot_args, plot_kwargs): node_opts = {k[5:] if 'node_' in k else k: v for k, v in plot_kwargs.items() if not any(k.startswith(p) for p in groups)} - if 'size' in node_opts: node_opts['s'] = node_opts.pop('size')**2 + if is_number(node_opts.get('size')): + node_opts['s'] = node_opts.pop('size')**2 nodes = ax.scatter(xs, ys, **node_opts) return {'nodes': nodes, 'edges': edges} diff --git a/holoviews/plotting/mpl/hex_tiles.py b/holoviews/plotting/mpl/hex_tiles.py index 80f70b769e..043b9eb595 100644 --- a/holoviews/plotting/mpl/hex_tiles.py +++ b/holoviews/plotting/mpl/hex_tiles.py @@ -29,6 +29,8 @@ class HexTilesPlot(ColorbarPlot): style_opts = ['edgecolors', 'alpha', 'linewidths', 'marginals'] + _no_op_styles = style_opts + _plot_methods = dict(single='hexbin') def get_data(self, element, ranges, style): diff --git a/holoviews/plotting/mpl/path.py b/holoviews/plotting/mpl/path.py index beabefba3d..cd4ebcc317 100644 --- a/holoviews/plotting/mpl/path.py +++ b/holoviews/plotting/mpl/path.py @@ -3,6 +3,7 @@ from matplotlib.collections import PatchCollection, LineCollection from ...core import util +from ...core.options import abbreviated_exception from ...element import Polygons from .element import ColorbarPlot from .util import polygons_to_path_patches @@ -28,6 +29,9 @@ def _finalize_artist(self, element): self._draw_colorbar(element.get_dimension(self.color_index)) def get_data(self, element, ranges, style): + with abbreviated_exception(): + style = self._apply_ops(element, ranges, style) + cdim = element.get_dimension(self.color_index) if cdim: cidx = element.get_dimension_index(cdim) if not cdim: @@ -79,6 +83,9 @@ def _finalize_artist(self, element): self._draw_colorbar(cdim) def get_data(self, element, ranges, style): + with abbreviated_exception(): + style = self._apply_ops(element, ranges, style) + if None not in [element.level, self.color_index]: cdim = element.vdims[0] else: diff --git a/holoviews/plotting/mpl/stats.py b/holoviews/plotting/mpl/stats.py index 449621bc2f..699e540462 100644 --- a/holoviews/plotting/mpl/stats.py +++ b/holoviews/plotting/mpl/stats.py @@ -1,4 +1,6 @@ import param +import numpy as np + from ...core.ndmapping import sorted_context from .chart import AreaPlot, ChartPlot @@ -56,8 +58,7 @@ class BoxPlot(ChartPlot): 'whiskerprops', 'capprops', 'flierprops', 'medianprops', 'meanprops', 'meanline'] - _no_op_styles = [s for s in style_opts - if s not in ('conf_intervals', 'widths')] + _no_op_styles = style_opts _plot_methods = dict(single='boxplot') @@ -154,7 +155,7 @@ class ViolinPlot(BoxPlot): 'widths', 'stats_color', 'box_color', 'alpha', 'edgecolors'] _no_op_styles = [s for s in style_opts - if s not in ('facecolors', 'edgecolors', 'widths')] + if s not in ('facecolors', 'edgecolors', 'widths')] def init_artists(self, ax, plot_args, plot_kwargs): box_color = plot_kwargs.pop('box_color', 'black') @@ -204,6 +205,13 @@ def get_data(self, element, ranges, style): style['positions'] = list(range(len(data))) style['labels'] = labels style['facecolors'] = colors + + if element.ndims > 0: + element = element.aggregate(function=np.mean) + else: + element = element.clone([(element.aggregate(function=np.mean),)]) + new_style = self._apply_ops(element, ranges, style) + style = {k: v for k, v in style.items() if k not in ['zorder', 'label']} style['vert'] = not self.invert_axes diff --git a/holoviews/tests/plotting/bokeh/testbarplot.py b/holoviews/tests/plotting/bokeh/testbarplot.py index d3ba4d35c9..b7bad415f1 100644 --- a/holoviews/tests/plotting/bokeh/testbarplot.py +++ b/holoviews/tests/plotting/bokeh/testbarplot.py @@ -248,7 +248,7 @@ def test_bars_color_index_color_no_clash(self): vdims=['y', 'color']).options(fill_color='color', color_index='color') plot = bokeh_renderer.get_plot(bars) glyph = plot.handles['glyph'] - cmapper = plot.handles['color_color_mapper'] + cmapper = plot.handles['fill_color_color_mapper'] cmapper2 = plot.handles['color_mapper'] self.assertEqual(glyph.fill_color, {'field': 'fill_color', 'transform': cmapper}) self.assertEqual(glyph.line_color, {'field': 'color', 'transform': cmapper2}) diff --git a/holoviews/tests/plotting/bokeh/testgraphplot.py b/holoviews/tests/plotting/bokeh/testgraphplot.py index f316119a3b..ba3b23de9e 100644 --- a/holoviews/tests/plotting/bokeh/testgraphplot.py +++ b/holoviews/tests/plotting/bokeh/testgraphplot.py @@ -4,7 +4,7 @@ import numpy as np from holoviews.core.data import Dataset -from holoviews.element import Graph, TriMesh, Chord, circular_layout +from holoviews.element import Graph, Nodes, TriMesh, Chord, circular_layout try: from bokeh.models import (NodesAndLinkedEdges, EdgesAndLinkedNodes, Patches) @@ -158,6 +158,127 @@ def test_graph_edges_numerically_colormapped(self): self.assertEqual(edge_source.data['Weight'], self.node_info2['Weight']) self.assertEqual(glyph.line_color, {'field': 'Weight', 'transform': cmapper}) + ########################### + # Styling mapping # + ########################### + + def test_graph_op_node_color(self): + edges = [(0, 1), (0, 2)] + nodes = Nodes([(0, 0, 0, 'red'), (0, 1, 1, 'green'), (1, 1, 2, 'blue')], + vdims='color') + graph = Graph((edges, nodes)).options(node_color='color') + plot = bokeh_renderer.get_plot(graph) + cds = plot.handles['scatter_1_source'] + glyph = plot.handles['scatter_1_glyph'] + self.assertEqual(glyph.fill_color, {'field': 'node_color'}) + self.assertEqual(glyph.line_color, {'field': 'node_color'}) + self.assertEqual(cds.data['node_color'], np.array(['red', 'green', 'blue'])) + + def test_graph_op_node_color_linear(self): + edges = [(0, 1), (0, 2)] + nodes = Nodes([(0, 0, 0, 0.5), (0, 1, 1, 1.5), (1, 1, 2, 2.5)], + vdims='color') + graph = Graph((edges, nodes)).options(node_color='color') + plot = bokeh_renderer.get_plot(graph) + cds = plot.handles['scatter_1_source'] + glyph = plot.handles['scatter_1_glyph'] + cmapper = plot.handles['node_color_color_mapper'] + self.assertEqual(glyph.fill_color, {'field': 'node_color', 'transform': cmapper}) + self.assertEqual(glyph.line_color, {'field': 'node_color', 'transform': cmapper}) + self.assertEqual(cds.data['node_color'], np.array([0.5, 1.5, 2.5])) + + def test_graph_op_node_color_categorical(self): + edges = [(0, 1), (0, 2)] + nodes = Nodes([(0, 0, 0, 'A'), (0, 1, 1, 'B'), (1, 1, 2, 'C')], + vdims='color') + graph = Graph((edges, nodes)).options(node_color='color') + plot = bokeh_renderer.get_plot(graph) + cds = plot.handles['scatter_1_source'] + glyph = plot.handles['scatter_1_glyph'] + cmapper = plot.handles['node_color_color_mapper'] + self.assertEqual(glyph.fill_color, {'field': 'node_color', 'transform': cmapper}) + self.assertEqual(glyph.line_color, {'field': 'node_color', 'transform': cmapper}) + self.assertEqual(cds.data['node_color'], np.array(['A', 'B', 'C'])) + + def test_graph_op_node_size(self): + edges = [(0, 1), (0, 2)] + nodes = Nodes([(0, 0, 0, 2), (0, 1, 1, 4), (1, 1, 2, 6)], + vdims='size') + graph = Graph((edges, nodes)).options(node_size='size') + plot = bokeh_renderer.get_plot(graph) + cds = plot.handles['scatter_1_source'] + glyph = plot.handles['scatter_1_glyph'] + self.assertEqual(glyph.size, {'field': 'node_size'}) + self.assertEqual(cds.data['node_size'], np.array([2, 4, 6])) + + def test_graph_op_node_alpha(self): + edges = [(0, 1), (0, 2)] + nodes = Nodes([(0, 0, 0, 0.2), (0, 1, 1, 0.6), (1, 1, 2, 1)], vdims='alpha') + graph = Graph((edges, nodes)).options(node_alpha='alpha') + plot = bokeh_renderer.get_plot(graph) + cds = plot.handles['scatter_1_source'] + glyph = plot.handles['scatter_1_glyph'] + self.assertEqual(glyph.fill_alpha, {'field': 'node_alpha'}) + self.assertEqual(glyph.line_alpha, {'field': 'node_alpha'}) + self.assertEqual(cds.data['node_alpha'], np.array([0.2, 0.6, 1])) + + def test_graph_op_node_line_width(self): + edges = [(0, 1), (0, 2)] + nodes = Nodes([(0, 0, 0, 2), (0, 1, 1, 4), (1, 1, 2, 6)], vdims='line_width') + graph = Graph((edges, nodes)).options(node_line_width='line_width') + plot = bokeh_renderer.get_plot(graph) + cds = plot.handles['scatter_1_source'] + glyph = plot.handles['scatter_1_glyph'] + self.assertEqual(glyph.line_width, {'field': 'node_line_width'}) + self.assertEqual(cds.data['node_line_width'], np.array([2, 4, 6])) + + def test_graph_op_edge_color(self): + edges = [(0, 1, 'red'), (0, 2, 'green'), (1, 3, 'blue')] + graph = Graph(edges, vdims='color').options(edge_color='color') + plot = bokeh_renderer.get_plot(graph) + cds = plot.handles['multi_line_1_source'] + glyph = plot.handles['multi_line_1_glyph'] + self.assertEqual(glyph.line_color, {'field': 'edge_color'}) + self.assertEqual(cds.data['edge_color'], np.array(['red', 'green', 'blue'])) + + def test_graph_op_edge_color_linear(self): + edges = [(0, 1, 2), (0, 2, 0.5), (1, 3, 3)] + graph = Graph(edges, vdims='color').options(edge_color='color') + plot = bokeh_renderer.get_plot(graph) + cds = plot.handles['multi_line_1_source'] + glyph = plot.handles['multi_line_1_glyph'] + cmapper = plot.handles['edge_color_color_mapper'] + self.assertEqual(glyph.line_color, {'field': 'edge_color', 'transform': cmapper}) + self.assertEqual(cds.data['edge_color'], np.array([2, 0.5, 3])) + + def test_graph_op_edge_color_categorical(self): + edges = [(0, 1, 'C'), (0, 2, 'B'), (1, 3, 'A')] + graph = Graph(edges, vdims='color').options(edge_color='color') + plot = bokeh_renderer.get_plot(graph) + cds = plot.handles['multi_line_1_source'] + glyph = plot.handles['multi_line_1_glyph'] + cmapper = plot.handles['edge_color_color_mapper'] + self.assertEqual(glyph.line_color, {'field': 'edge_color', 'transform': cmapper}) + self.assertEqual(cds.data['edge_color'], np.array(['C', 'B', 'A'])) + + def test_graph_op_edge_alpha(self): + edges = [(0, 1, 0.1), (0, 2, 0.5), (1, 3, 0.3)] + graph = Graph(edges, vdims='alpha').options(edge_alpha='alpha') + plot = bokeh_renderer.get_plot(graph) + cds = plot.handles['multi_line_1_source'] + glyph = plot.handles['multi_line_1_glyph'] + self.assertEqual(glyph.line_alpha, {'field': 'edge_alpha'}) + self.assertEqual(cds.data['edge_alpha'], np.array([0.1, 0.5, 0.3])) + + def test_graph_op_edge_line_width(self): + edges = [(0, 1, 2), (0, 2, 10), (1, 3, 6)] + graph = Graph(edges, vdims='line_width').options(edge_line_width='line_width') + plot = bokeh_renderer.get_plot(graph) + cds = plot.handles['multi_line_1_source'] + glyph = plot.handles['multi_line_1_glyph'] + self.assertEqual(glyph.line_width, {'field': 'edge_line_width'}) + self.assertEqual(cds.data['edge_line_width'], np.array([2, 10, 6])) + class TestBokehTriMeshPlot(TestBokehPlot): @@ -192,10 +313,11 @@ def test_plot_simple_trimesh_filled(self): layout = {str(int(z)): (x, y) for x, y, z in self.trimesh.nodes.array()} self.assertEqual(layout_source.graph_layout, layout) - def test_graph_edges_categorical_colormapped(self): + def test_trimesh_edges_categorical_colormapped(self): g = self.trimesh.opts(plot=dict(edge_color_index='node1'), - style=dict(edge_cmap=['#FFFFFF', '#000000'])) + style=dict(edge_cmap=['#FFFFFF', '#000000'])) plot = bokeh_renderer.get_plot(g) + print(plot.handles) cmapper = plot.handles['edge_colormapper'] edge_source = plot.handles['multi_line_1_source'] glyph = plot.handles['multi_line_1_glyph'] @@ -205,7 +327,7 @@ def test_graph_edges_categorical_colormapped(self): self.assertEqual(edge_source.data['node1_str__'], ['0', '1']) self.assertEqual(glyph.line_color, {'field': 'node1_str__', 'transform': cmapper}) - def test_graph_nodes_numerically_colormapped(self): + def test_trimesh_nodes_numerically_colormapped(self): g = self.trimesh_weighted.opts(plot=dict(edge_color_index='weight'), style=dict(edge_cmap=['#FFFFFF', '#000000'])) plot = bokeh_renderer.get_plot(g) diff --git a/holoviews/tests/plotting/bokeh/testlabels.py b/holoviews/tests/plotting/bokeh/testlabels.py index 20cda8c140..f997655580 100644 --- a/holoviews/tests/plotting/bokeh/testlabels.py +++ b/holoviews/tests/plotting/bokeh/testlabels.py @@ -120,7 +120,7 @@ def test_label_linear_color_op(self): plot = bokeh_renderer.get_plot(labels) cds = plot.handles['cds'] glyph = plot.handles['glyph'] - cmapper = plot.handles['color_color_mapper'] + cmapper = plot.handles['text_color_color_mapper'] self.assertTrue(cmapper, LinearColorMapper) self.assertEqual(cmapper.low, 0) self.assertEqual(cmapper.high, 2) @@ -133,7 +133,7 @@ def test_label_categorical_color_op(self): plot = bokeh_renderer.get_plot(labels) cds = plot.handles['cds'] glyph = plot.handles['glyph'] - cmapper = plot.handles['color_color_mapper'] + cmapper = plot.handles['text_color_color_mapper'] self.assertTrue(cmapper, CategoricalColorMapper) self.assertEqual(cmapper.factors, ['A', 'B', 'C']) self.assertEqual(cds.data['text_color'], np.array(['A', 'B', 'C'])) diff --git a/holoviews/tests/plotting/bokeh/testpointplot.py b/holoviews/tests/plotting/bokeh/testpointplot.py index 524ad8e3fa..06b65f4f28 100644 --- a/holoviews/tests/plotting/bokeh/testpointplot.py +++ b/holoviews/tests/plotting/bokeh/testpointplot.py @@ -472,7 +472,7 @@ def test_point_color_index_color_no_clash(self): vdims='color').options(fill_color='color', color_index='color') plot = bokeh_renderer.get_plot(points) glyph = plot.handles['glyph'] - cmapper = plot.handles['color_color_mapper'] + cmapper = plot.handles['fill_color_color_mapper'] cmapper2 = plot.handles['color_mapper'] self.assertEqual(glyph.fill_color, {'field': 'fill_color', 'transform': cmapper}) self.assertEqual(glyph.line_color, {'field': 'color', 'transform': cmapper2}) diff --git a/holoviews/tests/plotting/bokeh/testviolinplot.py b/holoviews/tests/plotting/bokeh/testviolinplot.py index 9077d32487..1a0db7e249 100644 --- a/holoviews/tests/plotting/bokeh/testviolinplot.py +++ b/holoviews/tests/plotting/bokeh/testviolinplot.py @@ -38,9 +38,9 @@ def test_violin_simple(self): scatter_source = plot.handles['scatter_1_source'] self.assertEqual(scatter_source.data['x'], [('', 0)]) self.assertEqual(scatter_source.data['y'], [q2]) - patch_source = plot.handles['patch_0_source'] - self.assertEqual(patch_source.data['x'], kde['x']) - self.assertEqual(patch_source.data['y'], kde['y']) + patch_source = plot.handles['patches_1_source'] + self.assertEqual(patch_source.data['xs'], [kde['y']]) + self.assertEqual(patch_source.data['ys'], [kde['x']]) def test_violin_inner_quartiles(self): values = np.random.rand(100) @@ -74,6 +74,6 @@ def test_violin_multi(self): def test_violin_empty(self): violin = Violin([]) plot = bokeh_renderer.get_plot(violin) - patch_source = plot.handles['patch_0_source'] - self.assertEqual(patch_source.data['x'], np.array([])) - self.assertEqual(patch_source.data['y'], []) + patch_source = plot.handles['patches_1_source'] + self.assertEqual(patch_source.data['xs'], [[]]) + self.assertEqual(patch_source.data['ys'], [np.array([])]) diff --git a/holoviews/tests/plotting/matplotlib/testgraphplot.py b/holoviews/tests/plotting/matplotlib/testgraphplot.py index 4eae05eea8..5e6c39e909 100644 --- a/holoviews/tests/plotting/matplotlib/testgraphplot.py +++ b/holoviews/tests/plotting/matplotlib/testgraphplot.py @@ -3,7 +3,7 @@ import numpy as np from holoviews.core.data import Dataset from holoviews.core.options import Cycle -from holoviews.element import Graph, TriMesh, Chord, circular_layout +from holoviews.element import Graph, Nodes, TriMesh, Chord, circular_layout # Standardize backend due to random inconsistencies try: @@ -83,6 +83,87 @@ def test_plot_graph_numerically_colored_edges(self): self.assertEqual(edges.get_array(), self.weights) self.assertEqual(edges.get_clim(), (self.weights.min(), self.weights.max())) + ########################### + # Styling mapping # + ########################### + + def test_graph_op_node_color(self): + edges = [(0, 1), (0, 2)] + nodes = Nodes([(0, 0, 0, '#000000'), (0, 1, 1, '#FF0000'), (1, 1, 2, '#00FF00')], + vdims='color') + graph = Graph((edges, nodes)).options(node_color='color') + plot = mpl_renderer.get_plot(graph) + artist = plot.handles['nodes'] + self.assertEqual(artist.get_facecolors(), + np.array([[0, 0, 0, 1], [1, 0, 0, 1], [0, 1, 0, 1]])) + + def test_graph_op_node_color_linear(self): + edges = [(0, 1), (0, 2)] + nodes = Nodes([(0, 0, 0, 0.5), (0, 1, 1, 1.5), (1, 1, 2, 2.5)], + vdims='color') + graph = Graph((edges, nodes)).options(node_color='color') + plot = mpl_renderer.get_plot(graph) + artist = plot.handles['nodes'] + self.assertEqual(artist.get_array(), np.array([0.5, 1.5, 2.5])) + + def test_graph_op_node_color_categorical(self): + edges = [(0, 1), (0, 2)] + nodes = Nodes([(0, 0, 0, 'A'), (0, 1, 1, 'B'), (1, 1, 2, 'A')], + vdims='color') + graph = Graph((edges, nodes)).options(node_color='color') + plot = mpl_renderer.get_plot(graph) + artist = plot.handles['nodes'] + self.assertEqual(artist.get_array(), np.array([0, 1, 0])) + + def test_graph_op_node_size(self): + edges = [(0, 1), (0, 2)] + nodes = Nodes([(0, 0, 0, 2), (0, 1, 1, 4), (1, 1, 2, 6)], + vdims='size') + graph = Graph((edges, nodes)).options(node_size='size') + plot = mpl_renderer.get_plot(graph) + artist = plot.handles['nodes'] + self.assertEqual(artist.get_sizes(), np.array([4, 16, 36])) + + def test_graph_op_node_linewidth(self): + edges = [(0, 1), (0, 2)] + nodes = Nodes([(0, 0, 0, 2), (0, 1, 1, 4), (1, 1, 2, 3.5)], vdims='line_width') + graph = Graph((edges, nodes)).options(node_linewidth='line_width') + plot = mpl_renderer.get_plot(graph) + artist = plot.handles['nodes'] + self.assertEqual(artist.get_linewidths(), [2, 4, 3.5]) + + def test_graph_op_node_alpha(self): + edges = [(0, 1), (0, 2)] + nodes = Nodes([(0, 0, 0, 0.2), (0, 1, 1, 0.6), (1, 1, 2, 1)], vdims='alpha') + graph = Graph((edges, nodes)).options(node_alpha='alpha') + with self.assertRaises(Exception): + mpl_renderer.get_plot(graph) + + def test_graph_op_edge_color(self): + edges = [(0, 1, 'red'), (0, 2, 'green'), (1, 3, 'blue')] + graph = Graph(edges, vdims='color').options(edge_color='color') + plot = mpl_renderer.get_plot(graph) + + def test_graph_op_edge_color_linear(self): + edges = [(0, 1, 2), (0, 2, 0.5), (1, 3, 3)] + graph = Graph(edges, vdims='color').options(edge_color='color') + plot = mpl_renderer.get_plot(graph) + + def test_graph_op_edge_color_categorical(self): + edges = [(0, 1, 'C'), (0, 2, 'B'), (1, 3, 'A')] + graph = Graph(edges, vdims='color').options(edge_color='color') + plot = mpl_renderer.get_plot(graph) + + def test_graph_op_edge_alpha(self): + edges = [(0, 1, 0.1), (0, 2, 0.5), (1, 3, 0.3)] + graph = Graph(edges, vdims='alpha').options(edge_alpha='alpha') + with self.assertRaises(Exception): + mpl_renderer.get_plot(graph) + + def test_graph_op_edge_linewidth(self): + edges = [(0, 1, 2), (0, 2, 10), (1, 3, 6)] + graph = Graph(edges, vdims='line_width').options(edge_linewidth='line_width') + plot = mpl_renderer.get_plot(graph) class TestMplTriMeshPlot(TestMPLPlot): From 2edbd80cf467f13fc250113715b21d5bacfa03bd Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Sat, 17 Nov 2018 13:31:27 +0000 Subject: [PATCH 046/117] Renamed op to dim --- .../demos/bokeh/dropdown_economic.ipynb | 2 +- .../demos/matplotlib/dropdown_economic.ipynb | 2 +- .../reference/elements/bokeh/Histogram.ipynb | 4 +- .../reference/elements/bokeh/Points.ipynb | 2 +- .../reference/elements/bokeh/Scatter.ipynb | 2 +- .../elements/matplotlib/Histogram.ipynb | 4 +- holoviews/plotting/bokeh/element.py | 12 ++-- holoviews/plotting/mpl/element.py | 13 ++-- holoviews/util/ops.py | 60 +++++++++---------- holoviews/util/parser.py | 4 +- 10 files changed, 52 insertions(+), 53 deletions(-) diff --git a/examples/gallery/demos/bokeh/dropdown_economic.ipynb b/examples/gallery/demos/bokeh/dropdown_economic.ipynb index 91a8d8788f..bbdbb0e0a7 100644 --- a/examples/gallery/demos/bokeh/dropdown_economic.ipynb +++ b/examples/gallery/demos/bokeh/dropdown_economic.ipynb @@ -54,7 +54,7 @@ "outputs": [], "source": [ "%%opts Overlay [width=700 height=400 show_frame=False]\n", - "%%opts Curve (color='k') Scatter (color='Unemployment' size=op('Unemployment')*1.5 cmap='Blues' line_color='k')\n", + "%%opts Curve (color='k') Scatter (color='Unemployment' size=dim('Unemployment')*1.5 cmap='Blues' line_color='k')\n", "%%opts VLine (color='k' line_width=1)\n", "%%opts Text (text_font_size='13px')\n", "gdp_curves = macro.to.curve('Year', 'GDP Growth')\n", diff --git a/examples/gallery/demos/matplotlib/dropdown_economic.ipynb b/examples/gallery/demos/matplotlib/dropdown_economic.ipynb index c93bb0944f..118f0ce927 100644 --- a/examples/gallery/demos/matplotlib/dropdown_economic.ipynb +++ b/examples/gallery/demos/matplotlib/dropdown_economic.ipynb @@ -53,7 +53,7 @@ "metadata": {}, "outputs": [], "source": [ - "%%opts Curve (color='k') Scatter (color='Unemployment' s=op('Unemployment')*10 cmap='Blues' edgecolors='k')\n", + "%%opts Curve (color='k') Scatter (color='Unemployment' s=dim('Unemployment')*10 cmap='Blues' edgecolors='k')\n", "%%opts Overlay [show_frame=True aspect=2, fig_size=250, show_frame=False]\n", "gdp_curves = macro.to.curve('Year', 'GDP Growth')\n", "gdp_unem_scatter = macro.to.scatter('Year', ['GDP Growth', 'Unemployment'])\n", diff --git a/examples/reference/elements/bokeh/Histogram.ipynb b/examples/reference/elements/bokeh/Histogram.ipynb index d612d9e5cc..86f8ed6e4c 100644 --- a/examples/reference/elements/bokeh/Histogram.ipynb +++ b/examples/reference/elements/bokeh/Histogram.ipynb @@ -67,7 +67,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Like most other elements a ``Histogram`` also supports using ``op`` transforms to map dimensions to visual attributes. To demonstrate this we will use the ``bin`` op to bin the 'y' values into positive and negative values and map those to a 'blue' and 'red' ``fill_color``:" + "Like most other elements a ``Histogram`` also supports using ``dim`` transforms to map dimensions to visual attributes. To demonstrate this we will use the ``bin`` op to bin the 'y' values into positive and negative values and map those to a 'blue' and 'red' ``fill_color``:" ] }, { @@ -76,7 +76,7 @@ "metadata": {}, "outputs": [], "source": [ - "hv.Histogram(curve).options(fill_color=hv.op('y', 'bin', bins=[-1, 0, 1], labels=['red', 'blue']))" + "hv.Histogram(curve).options(fill_color=hv.dim('y').bin(bins=[-1, 0, 1], labels=['red', 'blue']))" ] }, { diff --git a/examples/reference/elements/bokeh/Points.ipynb b/examples/reference/elements/bokeh/Points.ipynb index 6b6037eba0..1609de39bd 100644 --- a/examples/reference/elements/bokeh/Points.ipynb +++ b/examples/reference/elements/bokeh/Points.ipynb @@ -73,7 +73,7 @@ "metadata": {}, "outputs": [], "source": [ - "%%opts Points (color='z' size=op('size')*20)\n", + "%%opts Points (color='z' size=dim('size')*20)\n", "np.random.seed(10)\n", "data = np.random.rand(100,4)\n", "\n", diff --git a/examples/reference/elements/bokeh/Scatter.ipynb b/examples/reference/elements/bokeh/Scatter.ipynb index ccb9e2b129..5d5962a378 100644 --- a/examples/reference/elements/bokeh/Scatter.ipynb +++ b/examples/reference/elements/bokeh/Scatter.ipynb @@ -73,7 +73,7 @@ "metadata": {}, "outputs": [], "source": [ - "%%opts Scatter (color='z' size=op('size')*20)\n", + "%%opts Scatter (color='z' size=dim('size')*20)\n", "np.random.seed(10)\n", "data = np.random.rand(100,4)\n", "\n", diff --git a/examples/reference/elements/matplotlib/Histogram.ipynb b/examples/reference/elements/matplotlib/Histogram.ipynb index 4d3271547f..2e0e3eff83 100644 --- a/examples/reference/elements/matplotlib/Histogram.ipynb +++ b/examples/reference/elements/matplotlib/Histogram.ipynb @@ -67,7 +67,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Like most other elements a ``Histogram`` also supports using ``op`` transforms to map dimensions to visual attributes. To demonstrate this we will use the ``bin`` op to bin the 'y' values into positive and negative values and map those to a 'blue' and 'red' ``fill_color``:" + "Like most other elements a ``Histogram`` also supports using ``dim`` transforms to map dimensions to visual attributes. To demonstrate this we will use the ``bin`` op to bin the 'y' values into positive and negative values and map those to a 'blue' and 'red' ``fill_color``:" ] }, { @@ -76,7 +76,7 @@ "metadata": {}, "outputs": [], "source": [ - "hv.Histogram(curve).options(color=hv.op('y', 'bin', bins=[-1, 0, 1], labels=['red', 'blue']))" + "hv.Histogram(curve).options(color=hv.dim('y').bin(bins=[-1, 0, 1], labels=['red', 'blue']))" ] }, { diff --git a/holoviews/plotting/bokeh/element.py b/holoviews/plotting/bokeh/element.py index 3c590a9a03..009da8aeb2 100644 --- a/holoviews/plotting/bokeh/element.py +++ b/holoviews/plotting/bokeh/element.py @@ -27,7 +27,7 @@ from ...core import util from ...element import Graph, VectorField from ...streams import Buffer -from ...util.ops import op +from ...util.ops import dim from ..plot import GenericElementPlot, GenericOverlayPlot from ..util import dynamic_update, process_cmap, color_intervals from .plot import BokehPlot, TOOLS @@ -662,16 +662,16 @@ def _apply_ops(self, element, source, ranges, style, group=None): for k, v in dict(style).items(): if isinstance(v, util.basestring): if v in element or (isinstance(element, Graph) and v in element.nodes): - v = op(v) + v = dim(v) elif any(d==v for d in self.overlay_dims): - v = op([d for d in self.overlay_dims if d==v][0]) - elif isinstance(v, tuple) and v and isinstance(v[0], (util.basestring, tuple, op)): + v = dim([d for d in self.overlay_dims if d==v][0]) + elif isinstance(v, tuple) and v and isinstance(v[0], (util.basestring, tuple, dim)): try: - v = op.resolve_spec(v) + v = dim.resolve_spec(v) except: continue - if not isinstance(v, op) or (group is not None and not k.startswith(group)): + if not isinstance(v, dim) or (group is not None and not k.startswith(group)): continue dname = v.dimension.name diff --git a/holoviews/plotting/mpl/element.py b/holoviews/plotting/mpl/element.py index 1839bcdb4b..a4a9bc0f79 100644 --- a/holoviews/plotting/mpl/element.py +++ b/holoviews/plotting/mpl/element.py @@ -13,7 +13,7 @@ CompositeOverlay, Element3D, Element) from ...core.options import abbreviated_exception from ...element import Graph -from ...util.ops import op +from ...util.ops import dim from ..plot import GenericElementPlot, GenericOverlayPlot from ..util import dynamic_update, process_cmap, color_intervals from .plot import MPLPlot, mpl_rc_context @@ -520,17 +520,16 @@ def _apply_ops(self, element, ranges, style): for k, v in style.items(): if isinstance(v, util.basestring): if v in element or (isinstance(element, Graph) and v in element.nodes): - v = op(v) + v = dim(v) elif any(d==v for d in self.overlay_dims): - v = op([d for d in self.overlay_dims if d==v][0]) - elif isinstance(v, tuple) and v and isinstance(v[0], (util.basestring, tuple, op)): + v = dim([d for d in self.overlay_dims if d==v][0]) + elif isinstance(v, tuple) and v and isinstance(v[0], (util.basestring, tuple, dim)): try: - v = op.resolve_spec(v) + v = dim.resolve_spec(v) except: continue - - if not isinstance(v, op): + if not isinstance(v, dim): continue dname = v.dimension.name diff --git a/holoviews/util/ops.py b/holoviews/util/ops.py index aa00c6c714..224d05d45c 100644 --- a/holoviews/util/ops.py +++ b/holoviews/util/ops.py @@ -52,7 +52,7 @@ def int_fn(values): return values.astype(int) -class op(object): +class dim(object): _op_registry = {'norm': norm_fn, 'bin': bin_fn, 'cat': cat_fn, str: str_fn, int: int_fn} @@ -100,42 +100,42 @@ def register(cls, key, function): self._op_registry[name] = function # Unary operators - def __abs__(self): return op(self, operator.abs) - def __neg__(self): return op(self, operator.neg) - def __pos__(self): return op(self, operator.pos) + def __abs__(self): return dim(self, operator.abs) + def __neg__(self): return dim(self, operator.neg) + def __pos__(self): return dim(self, operator.pos) # Binary operators - def __add__(self, other): return op(self, operator.add, other) - def __div__(self, other): return op(self, operator.div, other) - def __floordiv__(self, other): return op(self, operator.floordiv, other) - def __pow__(self, other): return op(self, operator.pow, other) - def __mod__(self, other): return op(self, operator.mod, other) - def __mul__(self, other): return op(self, operator.mul, other) - def __sub__(self, other): return op(self, operator.sub, other) - def __truediv__(self, other): return op(self, operator.truediv, other) + def __add__(self, other): return dim(self, operator.add, other) + def __div__(self, other): return dim(self, operator.div, other) + def __floordiv__(self, other): return dim(self, operator.floordiv, other) + def __pow__(self, other): return dim(self, operator.pow, other) + def __mod__(self, other): return dim(self, operator.mod, other) + def __mul__(self, other): return dim(self, operator.mul, other) + def __sub__(self, other): return dim(self, operator.sub, other) + def __truediv__(self, other): return dim(self, operator.truediv, other) # Reverse binary operators - def __radd__(self, other): return op(self, operator.add, other, True) - def __rdiv__(self, other): return op(self, operator.div, other, True) - def __rfloordiv__(self, other): return op(self, operator.floordiv, other, True) - def __rmod__(self, other): return op(self, operator.mod, other, True) - def __rmul__(self, other): return op(self, operator.mul, other, True) - def __rsub__(self, other): return op(self, operator.sub, other, True) - def __rtruediv__(self, other): return op(self, operator.truediv, other, True) + def __radd__(self, other): return dim(self, operator.add, other, True) + def __rdiv__(self, other): return dim(self, operator.div, other, True) + def __rfloordiv__(self, other): return dim(self, operator.floordiv, other, True) + def __rmod__(self, other): return dim(self, operator.mod, other, True) + def __rmul__(self, other): return dim(self, operator.mul, other, True) + def __rsub__(self, other): return dim(self, operator.sub, other, True) + def __rtruediv__(self, other): return dim(self, operator.truediv, other, True) ## NumPy operations def __array_ufunc__(self, *args, **kwargs): ufunc = getattr(args[0], args[1]) kwargs = {k: v for k, v in kwargs.items() if v is not None} - return op(self, ufunc, **kwargs) + return dim(self, ufunc, **kwargs) - def max(self, **kwargs): return op(self, np.max, **kwargs) - def mean(self, **kwargs): return op(self, np.mean, **kwargs) - def min(self, **kwargs): return op(self, np.min, **kwargs) - def sum(self, **kwargs): return op(self, np.sum, **kwargs) - def std(self, **kwargs): return op(self, np.std, **kwargs) - def var(self, **kwargs): return op(self, np.var, **kwargs) - def astype(self, dtype): return op(self, np.asarray, dtype=dtype) + def max(self, **kwargs): return dim(self, np.max, **kwargs) + def mean(self, **kwargs): return dim(self, np.mean, **kwargs) + def min(self, **kwargs): return dim(self, np.min, **kwargs) + def sum(self, **kwargs): return dim(self, np.sum, **kwargs) + def std(self, **kwargs): return dim(self, np.std, **kwargs) + def var(self, **kwargs): return dim(self, np.var, **kwargs) + def astype(self, dtype): return dim(self, np.asarray, dtype=dtype) ## Custom functions @@ -143,14 +143,14 @@ def norm(self): """ Normalizes the data into the given range """ - return op(self, norm_fn) + return dim(self, norm_fn) def cat(self, categories, empty=None): - cat_op = op(self, cat_fn, categories=categories, empty=empty) + cat_op = dim(self, cat_fn, categories=categories, empty=empty) return cat_op def bin(self, bins, labels=None): - bin_op = op(self, bin_fn, categories=categories, empty=empty) + bin_op = dim(self, bin_fn, categories=categories, empty=empty) return bin_op def eval(self, dataset, flat=False, expanded=None, ranges={}): diff --git a/holoviews/util/parser.py b/holoviews/util/parser.py index eab98653c9..5305c2e856 100644 --- a/holoviews/util/parser.py +++ b/holoviews/util/parser.py @@ -17,7 +17,7 @@ from ..core.options import Options, Cycle, Palette from ..core.util import merge_option_dicts from ..operation import Compositor -from .ops import op, norm +from .ops import dim, norm ascii_uppercase = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' allowed = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!#$%&\()*+,-./:;<=>?@\\^_`{|}~' @@ -36,7 +36,7 @@ class Parser(object): """ # Static namespace set in __init__.py of the extension - namespace = {'np': np, 'Cycle': Cycle, 'Palette': Palette, 'op': op, + namespace = {'np': np, 'Cycle': Cycle, 'Palette': Palette, 'dim': dim, 'norm': norm} # If True, raise SyntaxError on eval error otherwise warn abort_on_eval_failure = False From 0b15b45ae9f82a8d39640a5188abd2611b611327 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Sat, 17 Nov 2018 17:57:11 +0000 Subject: [PATCH 047/117] Fixed import --- holoviews/util/ops.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/holoviews/util/ops.py b/holoviews/util/ops.py index 224d05d45c..f6b4ca8172 100644 --- a/holoviews/util/ops.py +++ b/holoviews/util/ops.py @@ -1,5 +1,8 @@ import operator -from itertools import zip_longest +try: + from itertools import zip_longest as zip_longest +except: + from itertools import izip_longest as zip_longest import numpy as np From cae455d849254e3d9a5f60d1c408ecd688b25fad Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Sat, 17 Nov 2018 18:06:38 +0000 Subject: [PATCH 048/117] Fixed flakes --- holoviews/plotting/bokeh/chart.py | 8 ++++---- holoviews/plotting/bokeh/element.py | 10 +++++----- holoviews/plotting/bokeh/stats.py | 2 +- holoviews/plotting/mpl/chart.py | 10 +++++----- holoviews/plotting/mpl/stats.py | 3 +-- holoviews/plotting/mpl/util.py | 1 - holoviews/tests/plotting/bokeh/testerrorbarplot.py | 2 -- .../tests/plotting/bokeh/testvectorfieldplot.py | 9 +-------- .../tests/plotting/matplotlib/testerrorbarplot.py | 2 +- .../tests/plotting/matplotlib/testgraphplot.py | 8 ++++---- .../tests/plotting/matplotlib/testspikeplot.py | 1 + .../plotting/matplotlib/testvectorfieldplot.py | 6 ------ holoviews/util/ops.py | 14 ++++++-------- 13 files changed, 29 insertions(+), 47 deletions(-) diff --git a/holoviews/plotting/bokeh/chart.py b/holoviews/plotting/bokeh/chart.py index 399f63cd25..5b89ccd093 100644 --- a/holoviews/plotting/bokeh/chart.py +++ b/holoviews/plotting/bokeh/chart.py @@ -10,7 +10,7 @@ from ...core.util import max_range, basestring, dimension_sanitizer, isfinite, range_pad from ...element import Bars from ...operation import interpolate_curve -from ...util.ops import op +from ...util.ops import dim from ..util import compute_sizes, get_min_distance, dim_axis_label, get_axis_padding from .element import ElementPlot, ColorbarPlot, LegendPlot from .styles import (expand_batched_style, line_properties, fill_properties, @@ -55,7 +55,7 @@ def _get_size_data(self, element, ranges, style): data, mapping = {}, {} sdim = element.get_dimension(self.size_index) ms = style.get('size', np.sqrt(6)) - if sdim and ((isinstance(ms, basestring) and ms in element) or isinstance(ms, op)): + if sdim and ((isinstance(ms, basestring) and ms in element) or isinstance(ms, dim)): self.warning("Cannot declare style mapping for 'size' option " "and declare a size_index, ignoring the size_index.") sdim = None @@ -135,8 +135,8 @@ def get_batched_data(self, element, ranges): data[k].append(v) if 'hover' in self.handles: - for dim, k in zip(element.dimensions(), key): - sanitized = dimension_sanitizer(dim.name) + for d, k in zip(element.dimensions(), key): + sanitized = dimension_sanitizer(d.name) data[sanitized].append([k]*nvals) data = {k: np.concatenate(v) for k, v in data.items()} diff --git a/holoviews/plotting/bokeh/element.py b/holoviews/plotting/bokeh/element.py index 009da8aeb2..f7d8c37273 100644 --- a/holoviews/plotting/bokeh/element.py +++ b/holoviews/plotting/bokeh/element.py @@ -717,7 +717,7 @@ def _apply_ops(self, element, source, ranges, style, group=None): if np.isscalar(val) and isinstance(val, int): val = str(v)+'pt' elif isinstance(val, np.ndarray) and val.dtype.kind in 'ifu': - val = [str(int(v))+'pt' for v in val] + val = [str(int(s))+'pt' for s in val] if np.isscalar(val): key = val else: @@ -737,17 +737,17 @@ def _apply_ops(self, element, source, ranges, style, group=None): key = {'field': k, 'transform': cmapper} new_style[k] = key - for style, value in list(new_style.items()): + for style, val in list(new_style.items()): # If mapped to color/alpha override static fill/line style for s in ('alpha', 'color'): if prefix+s != style or style not in source.data: continue fill_style = new_style.get(prefix+'fill_'+s) if fill_style and validate(s, fill_style): - new_style[prefix+'fill_'+s] = value + new_style[prefix+'fill_'+s] = val line_style = new_style.get(prefix+'line_'+s) if line_style and validate(s, line_style): - new_style[prefix+'line_'+s] = value + new_style[prefix+'line_'+s] = val return new_style @@ -1324,7 +1324,7 @@ def _get_color_data(self, element, ranges, style, name='color', factors=None, co data, mapping = {}, {} cdim = element.get_dimension(self.color_index) color = style.get(name, None) - if cdim and ((isinstance(color, util.basestring) and color in element) or isinstance(color, op)): + if cdim and ((isinstance(color, util.basestring) and color in element) or isinstance(color, dim)): self.warning("Cannot declare style mapping for '%s' option " "and declare a color_index, ignoring the color_index." % name) diff --git a/holoviews/plotting/bokeh/stats.py b/holoviews/plotting/bokeh/stats.py index 3d8f038f1c..27a23c4345 100644 --- a/holoviews/plotting/bokeh/stats.py +++ b/holoviews/plotting/bokeh/stats.py @@ -15,7 +15,7 @@ from .chart import AreaPlot from .element import CompositeElementPlot, ColorbarPlot, LegendPlot from .path import PolygonPlot -from .styles import fill_properties, line_properties, rgb2hex +from .styles import fill_properties, line_properties from .util import decode_bytes diff --git a/holoviews/plotting/mpl/chart.py b/holoviews/plotting/mpl/chart.py index d447e51cf2..5498cb0199 100644 --- a/holoviews/plotting/mpl/chart.py +++ b/holoviews/plotting/mpl/chart.py @@ -21,7 +21,7 @@ ) from ...element import Raster, HeatMap from ...operation import interpolate_curve -from ...util.ops import op +from ...util.ops import dim from ..plot import PlotSelector from ..util import compute_sizes, get_sideplot_ranges, get_min_distance from .element import ElementPlot, ColorbarPlot, LegendPlot @@ -617,7 +617,7 @@ def _compute_styles(self, element, ranges, style): color = style.pop('color', None) cmap = style.get('cmap', None) - if cdim and ((isinstance(color, basestring) and color in element) or isinstance(color, op)): + if cdim and ((isinstance(color, basestring) and color in element) or isinstance(color, dim)): self.warning("Cannot declare style mapping for 'color' option " "and declare a color_index, ignoring the color_index.") cdim = None @@ -638,7 +638,7 @@ def _compute_styles(self, element, ranges, style): ms = style.get('s', mpl.rcParams['lines.markersize']) sdim = element.get_dimension(self.size_index) - if sdim and ((isinstance(ms, basestring) and ms in element) or isinstance(ms, op)): + if sdim and ((isinstance(ms, basestring) and ms in element) or isinstance(ms, dim)): self.warning("Cannot declare style mapping for 's' option " "and declare a size_index, ignoring the size_index.") sdim = None @@ -747,7 +747,7 @@ def get_data(self, element, ranges, style): args = (xs, ys, magnitudes, [0.0] * len(element)) cdim = element.get_dimension(self.color_index) color = style.get('color', None) - if cdim and ((isinstance(color, basestring) and color in element) or isinstance(color, op)): + if cdim and ((isinstance(color, basestring) and color in element) or isinstance(color, dim)): self.warning("Cannot declare style mapping for 'color' option " "and declare a color_index, ignoring the color_index.") cdim = None @@ -1106,7 +1106,7 @@ def get_data(self, element, ranges, style): cdim = element.get_dimension(self.color_index) color = style.get('color', None) - if cdim and ((isinstance(color, basestring) and color in element) or isinstance(color, op)): + if cdim and ((isinstance(color, basestring) and color in element) or isinstance(color, dim)): self.warning("Cannot declare style mapping for 'color' option " "and declare a color_index, ignoring the color_index.") cdim = None diff --git a/holoviews/plotting/mpl/stats.py b/holoviews/plotting/mpl/stats.py index 699e540462..3475f14263 100644 --- a/holoviews/plotting/mpl/stats.py +++ b/holoviews/plotting/mpl/stats.py @@ -211,8 +211,7 @@ def get_data(self, element, ranges, style): else: element = element.clone([(element.aggregate(function=np.mean),)]) new_style = self._apply_ops(element, ranges, style) - - style = {k: v for k, v in style.items() + style = {k: v for k, v in new_style.items() if k not in ['zorder', 'label']} style['vert'] = not self.invert_axes format_kdims = [kd(value_format=None) for kd in element.kdims] diff --git a/holoviews/plotting/mpl/util.py b/holoviews/plotting/mpl/util.py index 9b5479d628..a06e829065 100644 --- a/holoviews/plotting/mpl/util.py +++ b/holoviews/plotting/mpl/util.py @@ -13,7 +13,6 @@ from ...core.util import basestring, _getargspec from ...element import Raster, RGB, Polygons -from ...element import Raster, RGB from ..util import COLOR_ALIASES, RGB_HEX_REGEX diff --git a/holoviews/tests/plotting/bokeh/testerrorbarplot.py b/holoviews/tests/plotting/bokeh/testerrorbarplot.py index 6f31511b30..692038d781 100644 --- a/holoviews/tests/plotting/bokeh/testerrorbarplot.py +++ b/holoviews/tests/plotting/bokeh/testerrorbarplot.py @@ -2,10 +2,8 @@ from bokeh.models import CategoricalColorMapper, LinearColorMapper -from holoviews.core.overlay import NdOverlay from holoviews.element import ErrorBars -from ..utils import ParamLogStream from .testplot import TestBokehPlot, bokeh_renderer diff --git a/holoviews/tests/plotting/bokeh/testvectorfieldplot.py b/holoviews/tests/plotting/bokeh/testvectorfieldplot.py index 592239a391..35b0218a3a 100644 --- a/holoviews/tests/plotting/bokeh/testvectorfieldplot.py +++ b/holoviews/tests/plotting/bokeh/testvectorfieldplot.py @@ -1,19 +1,12 @@ -import datetime as dt -from unittest import SkipTest - import numpy as np -from holoviews.core import NdOverlay -from holoviews.core.options import Cycle -from holoviews.core.util import pd from holoviews.element import VectorField from .testplot import TestBokehPlot, bokeh_renderer from ..utils import ParamLogStream try: - from bokeh.models import FactorRange, LinearColorMapper, CategoricalColorMapper - from bokeh.models import Scatter + from bokeh.models import LinearColorMapper, CategoricalColorMapper except: pass diff --git a/holoviews/tests/plotting/matplotlib/testerrorbarplot.py b/holoviews/tests/plotting/matplotlib/testerrorbarplot.py index fcd31b780c..1236735c41 100644 --- a/holoviews/tests/plotting/matplotlib/testerrorbarplot.py +++ b/holoviews/tests/plotting/matplotlib/testerrorbarplot.py @@ -1,8 +1,8 @@ import numpy as np from holoviews.element import ErrorBars +from holoviews.plotting.util import hex2rgb -from ..utils import ParamLogStream from .testplot import TestMPLPlot, mpl_renderer diff --git a/holoviews/tests/plotting/matplotlib/testgraphplot.py b/holoviews/tests/plotting/matplotlib/testgraphplot.py index 5e6c39e909..f850ceaab9 100644 --- a/holoviews/tests/plotting/matplotlib/testgraphplot.py +++ b/holoviews/tests/plotting/matplotlib/testgraphplot.py @@ -142,17 +142,17 @@ def test_graph_op_node_alpha(self): def test_graph_op_edge_color(self): edges = [(0, 1, 'red'), (0, 2, 'green'), (1, 3, 'blue')] graph = Graph(edges, vdims='color').options(edge_color='color') - plot = mpl_renderer.get_plot(graph) + mpl_renderer.get_plot(graph) def test_graph_op_edge_color_linear(self): edges = [(0, 1, 2), (0, 2, 0.5), (1, 3, 3)] graph = Graph(edges, vdims='color').options(edge_color='color') - plot = mpl_renderer.get_plot(graph) + mpl_renderer.get_plot(graph) def test_graph_op_edge_color_categorical(self): edges = [(0, 1, 'C'), (0, 2, 'B'), (1, 3, 'A')] graph = Graph(edges, vdims='color').options(edge_color='color') - plot = mpl_renderer.get_plot(graph) + mpl_renderer.get_plot(graph) def test_graph_op_edge_alpha(self): edges = [(0, 1, 0.1), (0, 2, 0.5), (1, 3, 0.3)] @@ -163,7 +163,7 @@ def test_graph_op_edge_alpha(self): def test_graph_op_edge_linewidth(self): edges = [(0, 1, 2), (0, 2, 10), (1, 3, 6)] graph = Graph(edges, vdims='line_width').options(edge_linewidth='line_width') - plot = mpl_renderer.get_plot(graph) + mpl_renderer.get_plot(graph) class TestMplTriMeshPlot(TestMPLPlot): diff --git a/holoviews/tests/plotting/matplotlib/testspikeplot.py b/holoviews/tests/plotting/matplotlib/testspikeplot.py index f0f45bd0eb..191bf1c349 100644 --- a/holoviews/tests/plotting/matplotlib/testspikeplot.py +++ b/holoviews/tests/plotting/matplotlib/testspikeplot.py @@ -2,6 +2,7 @@ from holoviews.core.overlay import NdOverlay from holoviews.element import Spikes +from holoviews.plotting.util import hex2rgb from ..utils import ParamLogStream from .testplot import TestMPLPlot, mpl_renderer diff --git a/holoviews/tests/plotting/matplotlib/testvectorfieldplot.py b/holoviews/tests/plotting/matplotlib/testvectorfieldplot.py index 9bb160aa6e..640ccb1527 100644 --- a/holoviews/tests/plotting/matplotlib/testvectorfieldplot.py +++ b/holoviews/tests/plotting/matplotlib/testvectorfieldplot.py @@ -1,16 +1,10 @@ import numpy as np -from holoviews.core.overlay import NdOverlay from holoviews.element import VectorField from .testplot import TestMPLPlot, mpl_renderer from ..utils import ParamLogStream -try: - from matplotlib import pyplot -except: - pass - class TestVectorFieldPlot(TestMPLPlot): diff --git a/holoviews/util/ops.py b/holoviews/util/ops.py index f6b4ca8172..c65fe92a9b 100644 --- a/holoviews/util/ops.py +++ b/holoviews/util/ops.py @@ -7,7 +7,7 @@ import numpy as np from ..core.dimension import Dimension -from ..core.util import basestring, unique_iterator, isfinite +from ..core.util import basestring, unique_iterator from ..element import Graph @@ -100,7 +100,7 @@ def register(cls, key, function): Register a custom op transform function which can from then on be referenced by the key. """ - self._op_registry[name] = function + cls._op_registry[key] = function # Unary operators def __abs__(self): return dim(self, operator.abs) @@ -149,12 +149,10 @@ def norm(self): return dim(self, norm_fn) def cat(self, categories, empty=None): - cat_op = dim(self, cat_fn, categories=categories, empty=empty) - return cat_op + return dim(self, cat_fn, categories=categories, empty=empty) def bin(self, bins, labels=None): - bin_op = dim(self, bin_fn, categories=categories, empty=empty) - return bin_op + return dim(self, bin_fn, bins=bins, labels=labels) def eval(self, dataset, flat=False, expanded=None, ranges={}): if expanded is None: @@ -166,7 +164,7 @@ def eval(self, dataset, flat=False, expanded=None, ranges={}): for o in self.ops: other = o['other'] if other is not None: - if isinstance(other, op): + if isinstance(other, dim): other = other.eval(dataset, ranges) args = (other, data) if o['reverse'] else (data, other) else: @@ -190,7 +188,7 @@ def __repr__(self): return op_repr -class norm(op): +class norm(dim): def __init__(self, obj, **kwargs): super(norm, self).__init__(obj, norm_fn, **kwargs) From b840e21fae82af283f1075c903410fc5ba1297a1 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Sat, 17 Nov 2018 20:14:24 +0000 Subject: [PATCH 049/117] BarPlot fixes --- holoviews/plotting/bokeh/chart.py | 18 +++++++++--------- holoviews/tests/plotting/bokeh/testbarplot.py | 10 +++++----- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/holoviews/plotting/bokeh/chart.py b/holoviews/plotting/bokeh/chart.py index 5b89ccd093..8ee7af8456 100644 --- a/holoviews/plotting/bokeh/chart.py +++ b/holoviews/plotting/bokeh/chart.py @@ -236,6 +236,7 @@ def get_data(self, element, ranges, style): x0s, x1s = (xs + nxoff, xs - pxoff) y0s, y1s = (ys + nyoff, ys - pyoff) + color = None if self.arrow_heads: arrow_len = (lens/4.) xa1s = x0s - np.cos(rads+np.pi/4)*arrow_len @@ -246,15 +247,14 @@ def get_data(self, element, ranges, style): x1s = np.concatenate([x1s, xa1s, xa2s]) y0s = np.tile(y0s, 3) y1s = np.concatenate([y1s, ya1s, ya2s]) - if cdim: - color = cdata.get(cdim.name) - color = np.tile(color, 3) + if cdim and cdim.name in cdata: + color = np.tile(cdata[cdim.name], 3) elif cdim: color = cdata.get(cdim.name) data = {'x0': x0s, 'x1': x1s, 'y0': y0s, 'y1': y1s} mapping = dict(x0='x0', x1='x1', y0='y0', y1='y1') - if cdim: + if cdim and color: data[cdim.name] = color mapping.update(cmapping) @@ -737,7 +737,7 @@ def get_extents(self, element, ranges, range_type='combined'): ydim = element.vdims[0] # Compute stack heights - if self.stacked: + if self.stacked or self.stack_index: ds = Dataset(element) pos_range = ds.select(**{ydim.name: (0, None)}).aggregate(xdim, function=np.sum).range(ydim) neg_range = ds.select(**{ydim.name: (None, 0)}).aggregate(xdim, function=np.sum).range(ydim) @@ -771,7 +771,7 @@ def _get_factors(self, element): sdim = None if element.ndims == 1: pass - elif not self.stacked: + elif not (self.stacked or self.stack_index): gdim = element.get_dimension(1) else: sdim = element.get_dimension(1) @@ -798,7 +798,7 @@ def _get_axis_labels(self, *args, **kwargs): if self.batched: element = element.last xlabel = dim_axis_label(element.kdims[0]) - if element.ndims > 1 and not self.stacked: + if element.ndims > 1 and not (self.stacked or self.stack_index): gdim = element.get_dimension(1) else: gdim = None @@ -847,7 +847,7 @@ def _add_color_data(self, ds, ranges, style, cdim, data, mapping, factors, color isinstance(cmapper, CategoricalColorMapper)): mapping['legend'] = cdim.name - if not self.stacked and ds.ndims > 1: + if not (self.stacked or self.stack_index) and ds.ndims > 1: cmapping.pop('legend', None) mapping.pop('legend', None) @@ -876,7 +876,7 @@ def get_data(self, element, ranges, style): group_dim, stack_dim = None, None if element.ndims == 1: grouping = None - elif self.stacked: + elif self.stacked or self.stack_index: grouping = 'stacked' stack_dim = element.get_dimension(1) else: diff --git a/holoviews/tests/plotting/bokeh/testbarplot.py b/holoviews/tests/plotting/bokeh/testbarplot.py index b7bad415f1..a907bd22db 100644 --- a/holoviews/tests/plotting/bokeh/testbarplot.py +++ b/holoviews/tests/plotting/bokeh/testbarplot.py @@ -234,7 +234,7 @@ def test_op_ndoverlay_value(self): def test_bars_color_index_color_clash(self): bars = Bars([(0, 0, 0), (0, 1, 1), (0, 2, 2)], - vdims=['y', 'color']).options(color='color', color_index='color') + vdims=['y', 'color']).options(color='color', color_index='color') with ParamLogStream() as log: plot = bokeh_renderer.get_plot(bars) log_msg = log.stream.read() @@ -245,10 +245,10 @@ def test_bars_color_index_color_clash(self): def test_bars_color_index_color_no_clash(self): bars = Bars([(0, 0, 0), (0, 1, 1), (0, 2, 2)], - vdims=['y', 'color']).options(fill_color='color', color_index='color') + vdims=['y', 'color']).options(line_color='color', color_index='color') plot = bokeh_renderer.get_plot(bars) glyph = plot.handles['glyph'] - cmapper = plot.handles['fill_color_color_mapper'] + cmapper = plot.handles['line_color_color_mapper'] cmapper2 = plot.handles['color_mapper'] - self.assertEqual(glyph.fill_color, {'field': 'fill_color', 'transform': cmapper}) - self.assertEqual(glyph.line_color, {'field': 'color', 'transform': cmapper2}) + self.assertEqual(glyph.line_color, {'field': 'line_color', 'transform': cmapper}) + self.assertEqual(glyph.fill_color, {'field': 'color', 'transform': cmapper2}) From dfc62aef19f0adb06fd4027a0f053149c73bb871 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Sat, 17 Nov 2018 20:21:06 +0000 Subject: [PATCH 050/117] Fixed bokeh tests --- holoviews/plotting/bokeh/hex_tiles.py | 5 ++++- holoviews/tests/plotting/bokeh/testgraphplot.py | 2 -- holoviews/tests/plotting/bokeh/testutils.py | 4 ++-- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/holoviews/plotting/bokeh/hex_tiles.py b/holoviews/plotting/bokeh/hex_tiles.py index 51c1a08f3e..024056cce4 100644 --- a/holoviews/plotting/bokeh/hex_tiles.py +++ b/holoviews/plotting/bokeh/hex_tiles.py @@ -48,7 +48,10 @@ def _process(self, element, key=None): xsize = ((x1-x0)/sx)*(2.0/3.0) ysize = ((y1-y0)/sy)*(2.0/3.0) size = xsize if self.orientation == 'flat' else ysize - scale = ysize/xsize + if isfinite(ysize) and isfinite(xsize) and not xsize == 0: + scale = ysize/xsize + else: + scale = 1 # Compute hexagonal coordinates x, y = (element.dimension_values(i) for i in indexes) diff --git a/holoviews/tests/plotting/bokeh/testgraphplot.py b/holoviews/tests/plotting/bokeh/testgraphplot.py index ba3b23de9e..e949b285e6 100644 --- a/holoviews/tests/plotting/bokeh/testgraphplot.py +++ b/holoviews/tests/plotting/bokeh/testgraphplot.py @@ -131,8 +131,6 @@ def test_graph_nodes_numerically_colormapped(self): self.assertEqual(glyph.fill_color, {'field': 'Weight', 'transform': cmapper}) def test_graph_edges_categorical_colormapped(self): - raise SkipTest('Temporarily disabled until array interface is simplified.') - g = self.graph3.opts(plot=dict(edge_color_index='start'), style=dict(edge_cmap=['#FFFFFF', '#000000'])) plot = bokeh_renderer.get_plot(g) diff --git a/holoviews/tests/plotting/bokeh/testutils.py b/holoviews/tests/plotting/bokeh/testutils.py index 650466c45b..57f6bd53b3 100644 --- a/holoviews/tests/plotting/bokeh/testutils.py +++ b/holoviews/tests/plotting/bokeh/testutils.py @@ -4,8 +4,8 @@ from holoviews.element.comparison import ComparisonTestCase try: - from holoviews.plotting.bokeh.util import ( - expand_batched_style, filter_batched_data ) + from holoviews.plotting.bokeh.util import filter_batched_data + from holoviews.plotting.bokeh.styles import expand_batched_style bokeh_renderer = Store.renderers['bokeh'] except: bokeh_renderer = None From 15230d1fbcd71f7df25cacce227a58ad8ace04f6 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Sat, 17 Nov 2018 20:25:59 +0000 Subject: [PATCH 051/117] Fixes for matplotlib tests --- holoviews/plotting/mpl/__init__.py | 6 +++--- holoviews/plotting/mpl/graphs.py | 2 +- holoviews/plotting/mpl/renderer.py | 4 ++-- holoviews/tests/plotting/matplotlib/testplot.py | 2 ++ 4 files changed, 8 insertions(+), 6 deletions(-) diff --git a/holoviews/plotting/mpl/__init__.py b/holoviews/plotting/mpl/__init__.py index ebde710c2e..a9169be487 100644 --- a/holoviews/plotting/mpl/__init__.py +++ b/holoviews/plotting/mpl/__init__.py @@ -224,10 +224,10 @@ def grid_selector(grid): if not config.style_17: options.Points = Options('plot', show_frame=True) -options.ErrorBars = Options('style', ecolor='k') +options.ErrorBars = Options('style', edgecolor='k') options.Spread = Options('style', facecolor=Cycle(), alpha=0.6, edgecolor='k', linewidth=0.5) -options.Bars = Options('style', ec='k', color=Cycle()) -options.Histogram = Options('style', ec='k', facecolor=Cycle()) +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.Scatter3D = Options('style', c=Cycle(), marker='o') options.Scatter3D = Options('plot', fig_size=150) diff --git a/holoviews/plotting/mpl/graphs.py b/holoviews/plotting/mpl/graphs.py index 707bb2777f..3053bbbf50 100644 --- a/holoviews/plotting/mpl/graphs.py +++ b/holoviews/plotting/mpl/graphs.py @@ -88,7 +88,7 @@ def _compute_styles(self, element, ranges, style): style['edge_colors'] = [colors[v%len(colors)] for v in cvals] style.pop('edge_color', None) if 'edge_array' in style: - self._norm_kwargs(element, ranges, style, edge_cdim, 'edge_') + self._norm_kwargs(element, ranges, style, edge_cdim, prefix='edge_') else: style.pop('edge_cmap', None) if 'edge_vmin' in style: diff --git a/holoviews/plotting/mpl/renderer.py b/holoviews/plotting/mpl/renderer.py index 3e54be8282..2cf2112377 100644 --- a/holoviews/plotting/mpl/renderer.py +++ b/holoviews/plotting/mpl/renderer.py @@ -132,9 +132,9 @@ def show(self, obj): plots.append(self.get_plot(o)) plt.show() except: - MPLPlot._close_figures = True raise - MPLPlot._close_figures = True + finally: + MPLPlot._close_figures = True return plots[0] if len(plots) == 1 else plots diff --git a/holoviews/tests/plotting/matplotlib/testplot.py b/holoviews/tests/plotting/matplotlib/testplot.py index ee14e55990..3c3eb5a490 100644 --- a/holoviews/tests/plotting/matplotlib/testplot.py +++ b/holoviews/tests/plotting/matplotlib/testplot.py @@ -6,6 +6,7 @@ try: import holoviews.plotting.mpl # noqa + import matplotlib.pyplot as plt mpl_renderer = Store.renderers['matplotlib'] except: mpl_renderer = None @@ -34,3 +35,4 @@ def setUp(self): def tearDown(self): Store.current_backend = self.previous_backend mpl_renderer.comm_manager = self.comm_manager + plt.close(plt.gcf()) From d68c09469cffecc458909f53c6754d78182d44a1 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Sat, 17 Nov 2018 20:39:09 +0000 Subject: [PATCH 052/117] Fixed flake --- holoviews/tests/plotting/bokeh/testgraphplot.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/holoviews/tests/plotting/bokeh/testgraphplot.py b/holoviews/tests/plotting/bokeh/testgraphplot.py index e949b285e6..422ad13701 100644 --- a/holoviews/tests/plotting/bokeh/testgraphplot.py +++ b/holoviews/tests/plotting/bokeh/testgraphplot.py @@ -1,7 +1,5 @@ from __future__ import absolute_import -from unittest import SkipTest - import numpy as np from holoviews.core.data import Dataset from holoviews.element import Graph, Nodes, TriMesh, Chord, circular_layout From a436a8b40d6725549f852b9412d94b70759f898e Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Sat, 17 Nov 2018 21:22:58 +0000 Subject: [PATCH 053/117] Made plotting imports consistent --- holoviews/plotting/bokeh/__init__.py | 2 +- holoviews/plotting/bokeh/annotation.py | 2 ++ holoviews/plotting/bokeh/callbacks.py | 2 ++ holoviews/plotting/bokeh/chart.py | 2 ++ holoviews/plotting/bokeh/element.py | 2 ++ holoviews/plotting/bokeh/graphs.py | 2 ++ holoviews/plotting/bokeh/heatmap.py | 2 ++ holoviews/plotting/bokeh/hex_tiles.py | 2 +- holoviews/plotting/bokeh/path.py | 2 ++ holoviews/plotting/bokeh/plot.py | 2 ++ holoviews/plotting/bokeh/raster.py | 2 ++ holoviews/plotting/bokeh/renderer.py | 17 +++++++++-------- holoviews/plotting/bokeh/sankey.py | 2 ++ holoviews/plotting/bokeh/stats.py | 2 ++ holoviews/plotting/bokeh/tabular.py | 2 ++ holoviews/plotting/bokeh/util.py | 15 +++++++++------ holoviews/plotting/bokeh/widgets.py | 2 +- holoviews/plotting/mpl/__init__.py | 2 ++ holoviews/plotting/mpl/annotation.py | 3 +++ holoviews/plotting/mpl/chart.py | 12 +++++------- holoviews/plotting/mpl/chart3d.py | 8 ++++---- holoviews/plotting/mpl/element.py | 9 +++++---- holoviews/plotting/mpl/graphs.py | 2 ++ holoviews/plotting/mpl/heatmap.py | 2 ++ holoviews/plotting/mpl/hex_tiles.py | 2 ++ holoviews/plotting/mpl/path.py | 3 +++ holoviews/plotting/mpl/plot.py | 6 ++++-- holoviews/plotting/mpl/raster.py | 4 +++- holoviews/plotting/mpl/renderer.py | 9 +++++---- holoviews/plotting/mpl/sankey.py | 7 +++++-- holoviews/plotting/mpl/stats.py | 3 ++- holoviews/plotting/mpl/tabular.py | 6 +++++- holoviews/plotting/mpl/util.py | 5 +++-- holoviews/plotting/mpl/widgets.py | 2 ++ 34 files changed, 102 insertions(+), 45 deletions(-) diff --git a/holoviews/plotting/bokeh/__init__.py b/holoviews/plotting/bokeh/__init__.py index 13fd9aa3ad..e4f8c15523 100644 --- a/holoviews/plotting/bokeh/__init__.py +++ b/holoviews/plotting/bokeh/__init__.py @@ -1,4 +1,4 @@ -from __future__ import absolute_import +from __future__ import absolute_import, division, unicode_literals from distutils.version import LooseVersion diff --git a/holoviews/plotting/bokeh/annotation.py b/holoviews/plotting/bokeh/annotation.py index f212151f37..20d276b125 100644 --- a/holoviews/plotting/bokeh/annotation.py +++ b/holoviews/plotting/bokeh/annotation.py @@ -1,3 +1,5 @@ +from __future__ import absolute_import, division, unicode_literals + from collections import defaultdict import param diff --git a/holoviews/plotting/bokeh/callbacks.py b/holoviews/plotting/bokeh/callbacks.py index 0156877114..428088eb19 100644 --- a/holoviews/plotting/bokeh/callbacks.py +++ b/holoviews/plotting/bokeh/callbacks.py @@ -1,3 +1,5 @@ +from __future__ import absolute_import, division, unicode_literals + from collections import defaultdict import param diff --git a/holoviews/plotting/bokeh/chart.py b/holoviews/plotting/bokeh/chart.py index 8ee7af8456..a898b40cd6 100644 --- a/holoviews/plotting/bokeh/chart.py +++ b/holoviews/plotting/bokeh/chart.py @@ -1,3 +1,5 @@ +from __future__ import absolute_import, division, unicode_literals + from collections import defaultdict import numpy as np diff --git a/holoviews/plotting/bokeh/element.py b/holoviews/plotting/bokeh/element.py index f7d8c37273..27545bb698 100644 --- a/holoviews/plotting/bokeh/element.py +++ b/holoviews/plotting/bokeh/element.py @@ -1,3 +1,5 @@ +from __future__ import absolute_import, division, unicode_literals + import warnings from types import FunctionType diff --git a/holoviews/plotting/bokeh/graphs.py b/holoviews/plotting/bokeh/graphs.py index f054eaf74b..d31679ff9e 100644 --- a/holoviews/plotting/bokeh/graphs.py +++ b/holoviews/plotting/bokeh/graphs.py @@ -1,3 +1,5 @@ +from __future__ import absolute_import, division, unicode_literals + from collections import defaultdict import param diff --git a/holoviews/plotting/bokeh/heatmap.py b/holoviews/plotting/bokeh/heatmap.py index 762b550466..52b6001014 100644 --- a/holoviews/plotting/bokeh/heatmap.py +++ b/holoviews/plotting/bokeh/heatmap.py @@ -1,3 +1,5 @@ +from __future__ import absolute_import, division, unicode_literals + import param import numpy as np diff --git a/holoviews/plotting/bokeh/hex_tiles.py b/holoviews/plotting/bokeh/hex_tiles.py index 024056cce4..32a8a2fe99 100644 --- a/holoviews/plotting/bokeh/hex_tiles.py +++ b/holoviews/plotting/bokeh/hex_tiles.py @@ -1,4 +1,4 @@ -from __future__ import division +from __future__ import absolute_import, division, unicode_literals import param import numpy as np diff --git a/holoviews/plotting/bokeh/path.py b/holoviews/plotting/bokeh/path.py index 39539c4f0b..b6ce88645b 100644 --- a/holoviews/plotting/bokeh/path.py +++ b/holoviews/plotting/bokeh/path.py @@ -1,3 +1,5 @@ +from __future__ import absolute_import, division, unicode_literals + from collections import defaultdict import param diff --git a/holoviews/plotting/bokeh/plot.py b/holoviews/plotting/bokeh/plot.py index 437b4b42ff..4535b809a8 100644 --- a/holoviews/plotting/bokeh/plot.py +++ b/holoviews/plotting/bokeh/plot.py @@ -1,3 +1,5 @@ +from __future__ import absolute_import, division, unicode_literals + import json from itertools import groupby from collections import defaultdict diff --git a/holoviews/plotting/bokeh/raster.py b/holoviews/plotting/bokeh/raster.py index a9e94e57c4..0632b07fe1 100644 --- a/holoviews/plotting/bokeh/raster.py +++ b/holoviews/plotting/bokeh/raster.py @@ -1,3 +1,5 @@ +from __future__ import absolute_import, division, unicode_literals + import numpy as np import param diff --git a/holoviews/plotting/bokeh/renderer.py b/holoviews/plotting/bokeh/renderer.py index 7d01fd7d57..3bffa15739 100644 --- a/holoviews/plotting/bokeh/renderer.py +++ b/holoviews/plotting/bokeh/renderer.py @@ -1,19 +1,25 @@ -from io import BytesIO +from __future__ import absolute_import, division, unicode_literals + import base64 import logging import signal +from io import BytesIO import param -from param.parameterized import bothmethod - import bokeh + +from param.parameterized import bothmethod from bokeh.application.handlers import FunctionHandler from bokeh.application import Application from bokeh.document import Document +from bokeh.embed.notebook import encode_utf8, notebook_content from bokeh.io import curdoc, show as bkshow +from bokeh.io.notebook import load_notebook from bokeh.models import Model +from bokeh.protocol import Protocol from bokeh.resources import CDN, INLINE from bokeh.server.server import Server +from bokeh.themes.theme import Theme from ...core import Store, HoloMap from ..plot import Plot, GenericElementPlot @@ -21,11 +27,6 @@ from .widgets import BokehScrubberWidget, BokehSelectionWidget, BokehServerWidgets from .util import attach_periodic, compute_plot_size, bokeh_version -from bokeh.io.notebook import load_notebook -from bokeh.protocol import Protocol -from bokeh.embed.notebook import encode_utf8, notebook_content -from bokeh.themes.theme import Theme - NOTEBOOK_DIV = """ {plot_div}