diff --git a/holoviews/plotting/bokeh/element.py b/holoviews/plotting/bokeh/element.py index 74dca6489e..5081512030 100644 --- a/holoviews/plotting/bokeh/element.py +++ b/holoviews/plotting/bokeh/element.py @@ -47,6 +47,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): @@ -610,26 +612,43 @@ 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 + print(k, val) if k == 'angle': val = np.deg2rad(val) if np.isscalar(val): @@ -638,11 +657,15 @@ def _apply_ops(self, element, source, ranges, style, group=None): key = k source.data[k] = val if ('color' in k and isinstance(val, np.ndarray) and - val.dtype.kind in 'if'): + not all(isinstance(v, util.basestring) and v.startswith('#') 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 + print(new_style) return new_style diff --git a/tests/plotting/bokeh/testpointplot.py b/tests/plotting/bokeh/testpointplot.py index 6ba27f8ebf..aeafb74ea5 100644 --- a/tests/plotting/bokeh/testpointplot.py +++ b/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)