Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Bokeh adjoined histogram adds support for live colormapping #928

Merged
merged 6 commits into from
Oct 12, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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