diff --git a/holoviews/plotting/bokeh/callbacks.py b/holoviews/plotting/bokeh/callbacks.py index 2673aecbc3..9b625eef25 100644 --- a/holoviews/plotting/bokeh/callbacks.py +++ b/holoviews/plotting/bokeh/callbacks.py @@ -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)] diff --git a/holoviews/plotting/bokeh/chart.py b/holoviews/plotting/bokeh/chart.py index 2c9c8e06d8..741758ed5c 100644 --- a/holoviews/plotting/bokeh/chart.py +++ b/holoviews/plotting/bokeh/chart.py @@ -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 @@ -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') @@ -263,16 +276,8 @@ 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} @@ -280,6 +285,32 @@ def get_data(self, element, ranges=None, empty=None): 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): diff --git a/holoviews/plotting/bokeh/element.py b/holoviews/plotting/bokeh/element.py index fcf5159ffc..856620a345 100644 --- a/holoviews/plotting/bokeh/element.py +++ b/holoviews/plotting/bokeh/element.py @@ -205,10 +205,11 @@ 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: @@ -216,7 +217,8 @@ def _init_tools(self, element, callbacks=[]): 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 @@ -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 @@ -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 diff --git a/holoviews/plotting/bokeh/plot.py b/holoviews/plotting/bokeh/plot.py index 98df9e30ce..0f0a00e999 100644 --- a/holoviews/plotting/bokeh/plot.py +++ b/holoviews/plotting/bokeh/plot.py @@ -363,7 +363,6 @@ 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 @@ -371,6 +370,7 @@ def _create_subplots(self, layout, positions, layout_dimensions, ranges, num=0): 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.")