Skip to content

Commit

Permalink
Merge pull request #1241 from ioam/bokeh_batched_props
Browse files Browse the repository at this point in the history
Handle batched styles consistently
  • Loading branch information
jlstevens authored Apr 4, 2017
2 parents 92ac5b5 + 875ebee commit 780a0c9
Show file tree
Hide file tree
Showing 6 changed files with 305 additions and 57 deletions.
49 changes: 26 additions & 23 deletions holoviews/plotting/bokeh/chart.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@
from .element import (ElementPlot, ColorbarPlot, LegendPlot, line_properties,
fill_properties)
from .path import PathPlot, PolygonPlot
from .util import get_cmap, mpl_to_bokeh, update_plot, rgb2hex, bokeh_version
from .util import (get_cmap, mpl_to_bokeh, update_plot, rgb2hex,
bokeh_version, expand_batched_style, filter_batched_data)


class PointPlot(LegendPlot, ColorbarPlot):
Expand All @@ -46,10 +47,11 @@ class PointPlot(LegendPlot, ColorbarPlot):
Function applied to size values before applying scaling,
to remove values lower than zero.""")

style_opts = (['cmap', 'palette', 'marker', 'size', 's'] +
style_opts = (['cmap', 'palette', 'marker', 'size'] +
line_properties + fill_properties)

_plot_methods = dict(single='scatter', batched='scatter')
_batched_style_opts = line_properties + fill_properties + ['size']

def _get_size_data(self, element, ranges, style):
data, mapping = {}, {}
Expand Down Expand Up @@ -101,26 +103,28 @@ def get_batched_data(self, element, ranges=None, empty=False):
zorders = self._updated_zorders(element)
styles = self.lookup_options(element.last, 'style')
styles = styles.max_cycles(len(self.ordering))

for (key, el), zorder in zip(element.data.items(), zorders):
self.set_param(**self.lookup_options(el, 'plot').options)
eldata, elmapping = self.get_data(el, ranges, empty)
for k, eld in eldata.items():
data[k].append(eld)

nvals = len(data[k][-1])
if 'color' not in elmapping:
val = styles[zorder].get('color')
elmapping['color'] = {'field': 'color'}
if isinstance(val, tuple):
val = rgb2hex(val)
data['color'].append([val]*nvals)
# Apply static styles
nvals = len(list(eldata.values())[0])
style = styles[zorder]
sdata, smapping = expand_batched_style(style, self._batched_style_opts,
elmapping, nvals)
elmapping.update(smapping)
for k, v in sdata.items():
data[k].append(v)

if any(isinstance(t, HoverTool) for t in self.state.tools):
for dim, k in zip(element.dimensions(), key):
sanitized = dimension_sanitizer(dim.name)
data[sanitized].append([k]*nvals)

data = {k: np.concatenate(v) for k, v in data.items()}
filter_batched_data(data, elmapping)
return data, elmapping


Expand Down Expand Up @@ -223,7 +227,7 @@ class CurvePlot(ElementPlot):

style_opts = line_properties
_plot_methods = dict(single='line', batched='multi_line')
_mapping = {p: p for p in ['xs', 'ys', 'color', 'line_alpha']}
_batched_style_opts = line_properties

def get_data(self, element, ranges=None, empty=False):
if 'steps' in self.interpolation:
Expand All @@ -248,7 +252,6 @@ def _hover_opts(self, element):

def get_batched_data(self, overlay, ranges=None, empty=False):
data = defaultdict(list)
opts = ['color', 'line_alpha', 'line_color']

zorders = self._updated_zorders(overlay)
styles = self.lookup_options(overlay.last, 'style')
Expand All @@ -259,23 +262,23 @@ def get_batched_data(self, overlay, ranges=None, empty=False):
for k, eld in eldata.items():
data[k].append(eld)

# Add options
# Apply static styles
style = styles[zorder]
for opt in opts:
if opt not in style:
continue
val = style[opt]
if opt == 'color' and isinstance(val, tuple):
val = rgb2hex(val)
data[opt].append([val])
sdata, smapping = expand_batched_style(style, self._batched_style_opts,
elmapping, nvals=1)
elmapping.update(smapping)
for k, v in sdata.items():
data[k].append(v[0])

for d, k in zip(overlay.kdims, key):
sanitized = dimension_sanitizer(d.name)
data[sanitized].append([k])
data[sanitized].append(k)
data = {opt: vals for opt, vals in data.items()
if not any(v is None for v in vals)}
return data, dict(xs=elmapping['x'], ys=elmapping['y'],
**{o: o for o in opts if o in data})
mapping = {{'x': 'xs', 'y': 'ys'}.get(k, k): v
for k, v in elmapping.items()}
filter_batched_data(data, elmapping)
return data, mapping


class AreaPlot(PolygonPlot):
Expand Down
8 changes: 6 additions & 2 deletions holoviews/plotting/bokeh/element.py
Original file line number Diff line number Diff line change
Expand Up @@ -643,8 +643,12 @@ def _update_glyphs(self, renderer, properties, mapping, glyph):
if glyph_prop and ('fill_'+prop not in glyph_props or gtype):
glyph_props['fill_'+prop] = glyph_prop

glyph_props.update({k[len(gtype):]: v for k, v in glyph_props.items()
if k.startswith(gtype)})
props = {k[len(gtype):]: v for k, v in glyph_props.items()
if k.startswith(gtype)}
if self.batched:
glyph_props = dict(props, **glyph_props)
else:
glyph_props.update(props)
filtered = {k: v for k, v in glyph_props.items()
if k in allowed_properties}
glyph.update(**filtered)
Expand Down
49 changes: 18 additions & 31 deletions holoviews/plotting/bokeh/path.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,19 @@
from ...core import util
from ..util import map_colors
from .element import ElementPlot, ColorbarPlot, line_properties, fill_properties
from .util import get_cmap, rgb2hex
from .util import (get_cmap, rgb2hex, expand_batched_style,
filter_batched_data)


class PathPlot(ElementPlot):

show_legend = param.Boolean(default=False, doc="""
Whether to show legend for the plot.""")

style_opts = ['color'] + line_properties
style_opts = line_properties
_plot_methods = dict(single='multi_line', batched='multi_line')
_mapping = dict(xs='xs', ys='ys')
_batched_style_opts = line_properties

def _hover_opts(self, element):
if self.batched:
Expand All @@ -45,19 +47,26 @@ def get_batched_data(self, element, ranges=None, empty=False):
eldata, elmapping = self.get_data(el, ranges, empty)
for k, eld in eldata.items():
data[k].extend(eld)
val = styles[zorder].get('color')
if val:
elmapping['line_color'] = 'color'
if isinstance(val, tuple):
val = rgb2hex(val)
data['color'] += [val for _ in range(len(list(eldata.values())[0]))]

# Apply static styles
nvals = len(list(eldata.values())[0])
style = styles[zorder]
sdata, smapping = expand_batched_style(style, self._batched_style_opts,
elmapping, nvals)
elmapping.update(smapping)
for k, v in sdata.items():
data[k].extend(list(v))

filter_batched_data(data, elmapping)
return data, elmapping


class PolygonPlot(ColorbarPlot, PathPlot):

style_opts = ['color', 'cmap', 'palette'] + line_properties + fill_properties
style_opts = ['cmap', 'palette'] + line_properties + fill_properties
_plot_methods = dict(single='patches', batched='patches')
_style_opts = ['color', 'cmap', 'palette'] + line_properties + fill_properties
_batched_style_opts = line_properties + fill_properties

def _hover_opts(self, element):
if self.batched:
Expand Down Expand Up @@ -90,25 +99,3 @@ def get_data(self, element, ranges=None, empty=False):
data[dim_name] = [element.level for _ in range(len(xs))]

return data, mapping


def get_batched_data(self, element, ranges=None, empty=False):
data = defaultdict(list)

zorders = self._updated_zorders(element)
styles = self.lookup_options(element.last, 'style')
styles = styles.max_cycles(len(self.ordering))

for (key, el), zorder in zip(element.data.items(), zorders):
self.overlay_dims = dict(zip(element.kdims, key))
eldata, elmapping = self.get_data(el, ranges, empty)
for k, eld in eldata.items():
data[k].extend(eld)
if 'color' not in elmapping:
val = styles[zorder].get('color')
elmapping['color'] = 'color'
if isinstance(val, tuple):
val = rgb2hex(val)
data['color'] += [val for _ in range(len(eldata['xs']))]

return data, elmapping
59 changes: 59 additions & 0 deletions holoviews/plotting/bokeh/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@

from ...core.options import abbreviated_exception
from ...core.overlay import Overlay
from ...core.util import basestring

from ..util import dim_axis_label

Expand Down Expand Up @@ -547,3 +548,61 @@ def get_tab_title(key, frame, overlay):
title = ' | '.join([d.pprint_value_string(k) for d, k in
zip(overlay.kdims, key)])
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:
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):
"""
Iterates over the data and mapping for a ColumnDataSource and
replaces columns with repeating values with scalar
"""
for k, v in list(mapping.items()):
if isinstance(v, dict) and 'field' in v:
v = v['field']
elif not isinstance(v, basestring):
continue
values = data[v]
if len(np.unique(values)) == 1:
mapping[k] = values[0]
del data[v]
85 changes: 85 additions & 0 deletions tests/testbokehutils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
from collections import defaultdict
from unittest import SkipTest

import numpy as np

from holoviews.core import Store
from holoviews.element.comparison import ComparisonTestCase
from holoviews.element import Curve, Points, Path

try:
from holoviews.plotting.bokeh.util import (
bokeh_version, expand_batched_style, filter_batched_data
)
bokeh_renderer = Store.renderers['bokeh']
except:
bokeh_renderer = None


class TestBokehUtilsInstantiation(ComparisonTestCase):

def setUp(self):
if not bokeh_renderer:
raise SkipTest("Bokeh required to test plot instantiation")

def test_expand_style_opts_simple(self):
style = {'line_width': 3}
opts = ['line_width']
data, mapping = expand_batched_style(style, opts, {}, nvals=3)
self.assertEqual(data['line_width'], [3, 3, 3])
self.assertEqual(mapping, {'line_width': {'field': 'line_width'}})

def test_expand_style_opts_multiple(self):
style = {'line_color': 'red', 'line_width': 4}
opts = ['line_color', 'line_width']
data, mapping = expand_batched_style(style, opts, {}, nvals=3)
self.assertEqual(data['line_color'], ['red', 'red', 'red'])
self.assertEqual(data['line_width'], [4, 4, 4])
self.assertEqual(mapping, {'line_color': {'field': 'line_color'},
'line_width': {'field': 'line_width'}})

def test_expand_style_opts_line_color_and_color(self):
style = {'fill_color': 'red', 'color': 'blue'}
opts = ['color', 'line_color', 'fill_color']
data, mapping = expand_batched_style(style, opts, {}, nvals=3)
self.assertEqual(data['line_color'], ['blue', 'blue', 'blue'])
self.assertEqual(data['fill_color'], ['red', 'red', 'red'])
self.assertEqual(mapping, {'line_color': {'field': 'line_color'},
'fill_color': {'field': 'fill_color'}})

def test_expand_style_opts_line_alpha_and_alpha(self):
style = {'fill_alpha': 0.5, 'alpha': 0.2}
opts = ['alpha', 'line_alpha', 'fill_alpha']
data, mapping = expand_batched_style(style, opts, {}, nvals=3)
self.assertEqual(data['line_alpha'], [0.2, 0.2, 0.2])
self.assertEqual(data['fill_alpha'], [0.5, 0.5, 0.5])
self.assertEqual(mapping, {'line_alpha': {'field': 'line_alpha'},
'fill_alpha': {'field': 'fill_alpha'}})

def test_expand_style_opts_color_predefined(self):
style = {'fill_color': 'red'}
opts = ['color', 'line_color', 'fill_color']
data, mapping = expand_batched_style(style, opts, {'color': 'color'}, nvals=3)
self.assertEqual(data['fill_color'], ['red', 'red', 'red'])
self.assertEqual(mapping, {'fill_color': {'field': 'fill_color'}})

def test_filter_batched_data(self):
data = {'line_color': ['red', 'red', 'red']}
mapping = {'line_color': 'line_color'}
filter_batched_data(data, mapping)
self.assertEqual(data, {})
self.assertEqual(mapping, {'line_color': 'red'})

def test_filter_batched_data_as_field(self):
data = {'line_color': ['red', 'red', 'red']}
mapping = {'line_color': {'field': 'line_color'}}
filter_batched_data(data, mapping)
self.assertEqual(data, {})
self.assertEqual(mapping, {'line_color': 'red'})

def test_filter_batched_data_heterogeneous(self):
data = {'line_color': ['red', 'red', 'blue']}
mapping = {'line_color': {'field': 'line_color'}}
filter_batched_data(data, mapping)
self.assertEqual(data, {'line_color': ['red', 'red', 'blue']})
self.assertEqual(mapping, {'line_color': {'field': 'line_color'}})
Loading

0 comments on commit 780a0c9

Please sign in to comment.