Skip to content

Commit

Permalink
Merge pull request #928 from ioam/adjoined_hist_js
Browse files Browse the repository at this point in the history
Bokeh adjoined histogram adds support for live colormapping
  • Loading branch information
jlstevens authored Oct 12, 2016
2 parents e67b9a2 + 56f9db5 commit a00ffbf
Show file tree
Hide file tree
Showing 4 changed files with 61 additions and 16 deletions.
5 changes: 3 additions & 2 deletions holoviews/plotting/bokeh/callbacks.py
Original file line number Diff line number Diff line change
Expand Up @@ -186,11 +186,12 @@ def set_customjs(self, handle):
attributes = attributes_js(self.attributes)
code = 'var data = {};\n' + attributes + self.code + self_callback

handles = dict(self.plot.handles)
handles = {}
subplots = list(self.plot.subplots.values())[::-1] if self.plot.subplots else []
plots = [self.plot] + subplots
for plot in plots:
handles.update(plot.handles)
handles.update({k: v for k, v in plot.handles.items()
if k in self.handles})
# Set callback
if id(handle.callback) in self._callbacks:
cb = self._callbacks[id(handle.callback)]
Expand Down
53 changes: 42 additions & 11 deletions holoviews/plotting/bokeh/chart.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@
from bokeh.charts import Bar, BoxPlot as BokehBoxPlot
except:
Bar, BokehBoxPlot = None, None
from bokeh.models import Circle, GlyphRenderer, ColumnDataSource, Range1d
from bokeh.models import (Circle, GlyphRenderer, ColumnDataSource,
Range1d, CustomJS)
from bokeh.models.tools import BoxSelectTool

from ...element import Raster, Points, Polygons, Spikes
from ...core.util import max_range, basestring, dimension_sanitizer
Expand Down Expand Up @@ -250,6 +252,17 @@ class SideHistogramPlot(ColorbarPlot, HistogramPlot):
show_title = param.Boolean(default=False, doc="""
Whether to display the plot title.""")

default_tools = param.List(default=['save', 'pan', 'wheel_zoom',
'box_zoom', 'reset', 'box_select'],
doc="A list of plugin tools to use on the plot.")

_callback = """
color_mapper.low = cb_data['geometry']['y0'];
color_mapper.high = cb_data['geometry']['y1'];
source.trigger('change')
main_source.trigger('change')
"""

def get_data(self, element, ranges=None, empty=None):
if self.invert_axes:
mapping = dict(top='left', bottom='right', left=0, right='top')
Expand All @@ -263,23 +276,41 @@ def get_data(self, element, ranges=None, empty=None):
right=element.edges[1:])

dim = element.get_dimension(0)
main = self.adjoined.main
range_item, main_range, _ = get_sideplot_ranges(self, element, main, ranges)
if isinstance(range_item, (Raster, Points, Polygons, Spikes)):
style = self.lookup_options(range_item, 'style')[self.cyclic_index]
else:
style = {}

if 'cmap' in style or 'palette' in style:
main_range = {dim.name: main_range}
cmapper = self._get_colormapper(dim, element, main_range, style)
cmapper = self._get_colormapper(dim, element, {}, {})
if cmapper:
data[dim.name] = [] if empty else element.dimension_values(dim)
mapping['fill_color'] = {'field': dim.name,
'transform': cmapper}
self._get_hover_data(data, element, empty)
return (data, mapping)


def _init_glyph(self, plot, mapping, properties):
"""
Returns a Bokeh glyph object.
"""
ret = super(SideHistogramPlot, self)._init_glyph(plot, mapping, properties)
if not 'field' in mapping.get('fill_color', {}):
return ret
dim = mapping['fill_color']['field']
sources = self.adjoined.traverse(lambda x: (x.handles.get('color_dim'),
x.handles.get('source')))
sources = [src for cdim, src in sources if cdim == dim]
tools = [t for t in self.handles['plot'].tools
if isinstance(t, BoxSelectTool)]
if not tools or not sources:
return
box_select, main_source = tools[0], sources[0]
handles = {'color_mapper': self.handles['color_mapper'],
'source': self.handles['source'],
'main_source': main_source}
if box_select.callback:
box_select.callback.code += self._callback
box_select.callback.args.update(handles)
else:
box_select.callback = CustomJS(args=handles, code=self._callback)
return ret


class ErrorPlot(PathPlot):

Expand Down
17 changes: 15 additions & 2 deletions holoviews/plotting/bokeh/element.py
Original file line number Diff line number Diff line change
Expand Up @@ -205,18 +205,20 @@ def _init_tools(self, element, callbacks=[]):
for d in dims]

callbacks = callbacks+self.callbacks
cb_tools = []
cb_tools, tool_names = [], []
for cb in callbacks:
for handle in cb.handles:
if handle and handle in known_tools:
tool_names.append(handle)
if handle == 'hover':
tool = HoverTool(tooltips=tooltips)
else:
tool = known_tools[handle]()
cb_tools.append(tool)
self.handles[handle] = tool

tools = cb_tools + self.default_tools + self.tools
tools = [t for t in cb_tools + self.default_tools + self.tools
if t not in tool_names]
if 'hover' in tools:
tools[tools.index('hover')] = HoverTool(tooltips=tooltips)
return tools
Expand Down Expand Up @@ -784,6 +786,16 @@ def _get_colormapper(self, dim, element, ranges, style):
# and then only updated
low, high = ranges.get(dim.name, element.range(dim.name))
palette = mplcmap_to_palette(style.pop('cmap', 'viridis'))
if self.adjoined:
cmappers = self.adjoined.traverse(lambda x: (x.handles.get('color_dim'),
x.handles.get('color_mapper')))
cmappers = [cmap for cdim, cmap in cmappers if cdim == dim]
if cmappers:
cmapper = cmappers[0]
self.handles['color_mapper'] = cmapper
return cmapper
else:
return None
if 'color_mapper' in self.handles:
cmapper = self.handles['color_mapper']
cmapper.low = low
Expand All @@ -793,6 +805,7 @@ def _get_colormapper(self, dim, element, ranges, style):
colormapper = LogColorMapper if self.logz else LinearColorMapper
cmapper = colormapper(palette, low=low, high=high)
self.handles['color_mapper'] = cmapper
self.handles['color_dim'] = dim
return cmapper


Expand Down
2 changes: 1 addition & 1 deletion holoviews/plotting/bokeh/plot.py
Original file line number Diff line number Diff line change
Expand Up @@ -363,14 +363,14 @@ def _create_subplots(self, layout, positions, layout_dimensions, ranges, num=0):
"""
subplots = {}
adjoint_clone = layout.clone(shared_data=False, id=layout.id)
subplot_opts = dict(adjoined=layout)
main_plot = None
for pos in positions:
# Pos will be one of 'main', 'top' or 'right' or None
element = layout.get(pos, None)
if element is None:
continue

subplot_opts = dict(adjoined=main_plot)
# Options common for any subplot
if type(element) in (NdLayout, Layout):
raise SkipRendering("Cannot plot nested Layouts.")
Expand Down

0 comments on commit a00ffbf

Please sign in to comment.