diff --git a/examples/reference/streams/bokeh/BoxEdit.ipynb b/examples/reference/streams/bokeh/BoxEdit.ipynb index 962d51cb7d..169609167c 100644 --- a/examples/reference/streams/bokeh/BoxEdit.ipynb +++ b/examples/reference/streams/bokeh/BoxEdit.ipynb @@ -42,7 +42,13 @@ "\n", "**Delete box**\n", "\n", - " Tap a box to select it then press BACKSPACE key while the mouse is within the plot area." + " Tap a box to select it then press BACKSPACE key while the mouse is within the plot area.\n", + " \n", + "### Properties\n", + "\n", + "* **``empty_value``**: Value to add to non-coordinate columns when adding new box\n", + "* **``num_objects``** (int): Maximum number of boxes to draw before deleting the oldest object\n", + "* **``styles``** (dict): Dictionary of style properties (e.g. line_color, line_width etc.) to apply to each box. If values are lists the values will cycle over the values." ] }, { @@ -59,7 +65,7 @@ "outputs": [], "source": [ "boxes = hv.Polygons([hv.Box(0, 0, 1), hv.Box(2, 1, 1.5), hv.Box(0.5, 1.5, 1)])\n", - "box_stream = streams.BoxEdit(source=boxes, num_objects=2)\n", + "box_stream = streams.BoxEdit(source=boxes, num_objects=3, styles={'fill_color': ['red', 'green', 'blue']})\n", "boxes.opts(\n", " opts.Polygons(active_tools=['box_edit'], fill_alpha=0.5, height=400, width=400))" ] diff --git a/examples/reference/streams/bokeh/FreehandDraw.ipynb b/examples/reference/streams/bokeh/FreehandDraw.ipynb index a430eb4e6b..c8214815b6 100644 --- a/examples/reference/streams/bokeh/FreehandDraw.ipynb +++ b/examples/reference/streams/bokeh/FreehandDraw.ipynb @@ -26,6 +26,12 @@ "\n", " Tap a line to select it then press BACKSPACE key while the mouse is within the plot area.\n", " \n", + "### Properties\n", + "\n", + "* **``empty_value``**: Value to add to non-coordinate columns when adding new path\n", + "* **``num_objects``** (int): Maximum number of paths to draw before deleting the oldest object\n", + "* **``styles``** (dict): Dictionary of style properties (e.g. line_color, line_width etc.) to apply to each path. If values are lists the values will cycle over the values.\n", + " \n", "The tool allows drawing lines and polygons by supplying it with a ``Path`` or ``Polygons`` object as a source. It also allows limiting the number of lines or polygons that can be drawn by setting ``num_objects`` to a finite number, causing the first line to be dropped when the limit is reached." ] }, @@ -36,7 +42,8 @@ "outputs": [], "source": [ "path = hv.Path([])\n", - "freehand = streams.FreehandDraw(source=path, num_objects=3)\n", + "freehand = streams.FreehandDraw(source=path, num_objects=3,\n", + " styles={'line_color': ['red', 'green', 'blue']})\n", "\n", "path.opts(\n", " opts.Path(active_tools=['freehand_draw'], height=400, line_width=10, width=400))" diff --git a/examples/reference/streams/bokeh/PolyDraw.ipynb b/examples/reference/streams/bokeh/PolyDraw.ipynb index 010424192e..3b71e90f6e 100644 --- a/examples/reference/streams/bokeh/PolyDraw.ipynb +++ b/examples/reference/streams/bokeh/PolyDraw.ipynb @@ -42,7 +42,16 @@ "\n", "**Delete patch/multi-line**\n", "\n", - " Tap a patch/multi-line to select it then press BACKSPACE key while the mouse is within the plot area." + " Tap a patch/multi-line to select it then press BACKSPACE key while the mouse is within the plot area.\n", + " \n", + "### Properties\n", + "\n", + "* **``drag``** (boolean): Whether to enable dragging of paths and polygons\n", + "* **``empty_value``**: Value to add to non-coordinate columns when adding new path or polygon\n", + "* **``num_objects``** (int): Maximum number of paths or polygons to draw before deleting the oldest object\n", + "* **``show_vertices``** (boolean): Whether to show the vertices of the paths or polygons\n", + "* **``styles``** (dict): Dictionary of style properties (e.g. line_color, line_width etc.) to apply to each path and polygon. If values are lists the values will cycle over the values) \n", + "* **``vertex_style``** (dict): Dictionary of style properties (e.g. fill_color, line_width etc.) to apply to vertices if ``show_vertices`` enabled" ] }, { @@ -61,7 +70,10 @@ "path = hv.Path([[(1, 5), (9, 5)]])\n", "poly = hv.Polygons([[(2, 2), (5, 8), (8, 2)]])\n", "path_stream = streams.PolyDraw(source=path, drag=True, show_vertices=True)\n", - "poly_stream = streams.PolyDraw(source=poly, drag=True, num_objects=2, show_vertices=True)\n", + "poly_stream = streams.PolyDraw(source=poly, drag=True, num_objects=4,\n", + " show_vertices=True, styles={\n", + " 'fill_color': ['red', 'green', 'blue']\n", + " })\n", "\n", "(path * poly).opts(\n", " opts.Path(color='red', height=400, line_width=5, width=400),\n", diff --git a/holoviews/plotting/bokeh/callbacks.py b/holoviews/plotting/bokeh/callbacks.py index a7274f1b35..04c10bc8a4 100644 --- a/holoviews/plotting/bokeh/callbacks.py +++ b/holoviews/plotting/bokeh/callbacks.py @@ -6,7 +6,8 @@ import numpy as np from bokeh.models import ( CustomJS, FactorRange, DatetimeAxis, ColumnDataSource, ToolbarBox, - Range1d, DataRange1d + Range1d, DataRange1d, PolyDrawTool, BoxEditTool, PolyEditTool, + FreehandDrawTool, PointDrawTool ) from pyviz_comms import JS_CALLBACK @@ -16,10 +17,11 @@ RangeY, PointerX, PointerY, BoundsX, BoundsY, Tap, SingleTap, DoubleTap, MouseEnter, MouseLeave, PlotSize, Draw, BoundsXY, PlotReset, BoxEdit, - PointDraw, PolyDraw, PolyEdit, CDSStream, FreehandDraw) + PointDraw, PolyDraw, PolyEdit, CDSStream, + FreehandDraw) from ..links import Link, RangeToolLink, DataLink from ..plot import GenericElementPlot, GenericOverlayPlot -from .util import convert_timestamp, bokeh_version +from .util import convert_timestamp class MessageCallback(object): @@ -944,25 +946,44 @@ def _process_msg(self, msg): return msg +class GlyphDrawCallback(CDSCallback): + + _style_callback = """ + var types = require("core/util/types"); + var length = cb_obj.data[length_var].length; + for (i = 0; i < length; i++) { + for (var style in styles) { + var value = styles[style]; + if (types.isArray(value)) { + value = value[i % value.length]; + } + cb_obj.data[style][i] = value; + } + } + """ + + def _create_style_callback(self, cds, glyph, length_var): + stream = self.streams[0] + for style, values in stream.styles.items(): + cds.data[style] = [ + values[i % len(values)] + for i in range(len(cds.data[length_var]))] + setattr(glyph, style, style) + cb = CustomJS(code=self._style_callback, + args={'styles': stream.styles, + 'empty': stream.empty_value, + 'length_var': length_var}) + cds.js_on_change('data', cb) + + class PointDrawCallback(CDSCallback): def initialize(self, plot_id=None): - try: - from bokeh.models import PointDrawTool - except Exception: - param.main.param.warning('PointDraw requires bokeh >= 0.12.14') - return - stream = self.streams[0] renderers = [self.plot.handles['glyph_renderer']] kwargs = {} if stream.num_objects: - if bokeh_version >= '1.0.0': - kwargs['num_objects'] = stream.num_objects - else: - param.main.param.warning( - 'Specifying num_objects to PointDraw stream requires ' - 'a bokeh version >= 1.0.0.') + kwargs['num_objects'] = stream.num_objects point_tool = PointDrawTool(drag=all(s.drag for s in self.streams), empty_value=stream.empty_value, renderers=renderers, **kwargs) @@ -979,33 +1000,20 @@ def initialize(self, plot_id=None): super(PointDrawCallback, self).initialize(plot_id) -class PolyDrawCallback(CDSCallback): +class PolyDrawCallback(GlyphDrawCallback): def initialize(self, plot_id=None): - try: - from bokeh.models import PolyDrawTool - except: - param.main.param.warning('PolyDraw requires bokeh >= 0.12.14') - return plot = self.plot stream = self.streams[0] kwargs = {} if stream.num_objects: - if bokeh_version >= '1.0.0': - kwargs['num_objects'] = stream.num_objects - else: - param.main.param.warning( - 'Specifying num_objects to PointDraw stream requires ' - 'a bokeh version >=1.0.0.') + kwargs['num_objects'] = stream.num_objects if stream.show_vertices: - if bokeh_version >= '1.0.0': - vertex_style = dict({'size': 10}, **stream.vertex_style) - r1 = plot.state.scatter([], [], **vertex_style) - kwargs['vertex_renderer'] = r1 - else: - param.main.param.warning( - 'Enabling vertices on the PointDraw stream requires ' - 'a bokeh version >=1.0.0.') + vertex_style = dict({'size': 10}, **stream.vertex_style) + r1 = plot.state.scatter([], [], **vertex_style) + kwargs['vertex_renderer'] = r1 + if stream.styles: + self._create_style_callback(plot.handles['cds'], plot.handles['glyph'], 'xs') poly_tool = PolyDrawTool(drag=all(s.drag for s in self.streams), empty_value=stream.empty_value, renderers=[plot.handles['glyph_renderer']], @@ -1032,13 +1040,10 @@ def _update_cds_vdims(self): class FreehandDrawCallback(PolyDrawCallback): def initialize(self, plot_id=None): - try: - from bokeh.models import FreehandDrawTool - except: - param.main.param.warning('FreehandDraw requires bokeh >= 0.13.0') - return plot = self.plot stream = self.streams[0] + if stream.styles: + self._create_style_callback(plot.handles['cds'], plot.handles['glyph'], 'xs') poly_tool = FreehandDrawTool( empty_value=stream.empty_value, num_objects=stream.num_objects, @@ -1049,30 +1054,19 @@ def initialize(self, plot_id=None): CDSCallback.initialize(self, plot_id) -class BoxEditCallback(CDSCallback): +class BoxEditCallback(GlyphDrawCallback): attributes = {'data': 'rect_source.data'} models = ['rect_source'] def initialize(self, plot_id=None): - try: - from bokeh.models import BoxEditTool - except: - param.main.param.warning('BoxEdit requires bokeh >= 0.12.14') - return - plot = self.plot data = plot.handles['cds'].data element = self.plot.current_frame stream = self.streams[0] kwargs = {} if stream.num_objects: - if bokeh_version >= '1.0.0': - kwargs['num_objects'] = stream.num_objects - else: - param.main.param.warning( - 'Specifying num_objects to BoxEdit stream requires ' - 'a bokeh version >=1.0.0.') + kwargs['num_objects'] = stream.num_objects xs, ys, widths, heights = [], [], [], [] for x, y in zip(data['xs'], data['ys']): x0, x1 = (np.nanmin(x), np.nanmax(x)) @@ -1088,6 +1082,8 @@ def initialize(self, plot_id=None): style.pop('cmap', None) r1 = plot.state.rect('x', 'y', 'width', 'height', source=rect_source, **style) plot.handles['rect_source'] = rect_source + if stream.styles: + self._create_style_callback(rect_source, r1.glyph, 'x') box_tool = BoxEditTool(renderers=[r1], **kwargs) plot.state.tools.append(box_tool) if plot.handles['glyph_renderer'] in self.plot.state.renderers: @@ -1115,11 +1111,6 @@ def _process_msg(self, msg): class PolyEditCallback(PolyDrawCallback): def initialize(self, plot_id=None): - try: - from bokeh.models import PolyEditTool - except: - param.main.param.warning('PolyEdit requires bokeh >= 0.12.14') - return plot = self.plot vertex_tool = None if all(s.shared for s in self.streams): diff --git a/holoviews/streams.py b/holoviews/streams.py index c19f3815c1..044dcbf7a6 100644 --- a/holoviews/streams.py +++ b/holoviews/streams.py @@ -1017,15 +1017,21 @@ class PolyDraw(CDSStream): A dictionary specifying the style options for the vertices. The usual bokeh style options apply, e.g. fill_color, line_alpha, size, etc. + + styles: dict + A dictionary specifying lists of styles to cycle over whenever + a new Poly glyph is drawn. """ def __init__(self, empty_value=None, drag=True, num_objects=0, - show_vertices=False, vertex_style={}, **params): + show_vertices=False, vertex_style={}, styles={}, + **params): self.drag = drag self.empty_value = empty_value self.num_objects = num_objects self.show_vertices = show_vertices self.vertex_style = vertex_style + self.styles = styles super(PolyDraw, self).__init__(**params) @property @@ -1059,11 +1065,16 @@ class FreehandDraw(CDSStream): num_objects: int The number of polygons that can be drawn before overwriting the oldest polygon. + + styles: dict + A dictionary specifying lists of styles to cycle over whenever + a new freehand glyph is drawn. """ - def __init__(self, empty_value=None, num_objects=0, **params): + def __init__(self, empty_value=None, num_objects=0, styles={}, **params): self.empty_value = empty_value self.num_objects = num_objects + self.styles = styles super(FreehandDraw, self).__init__(**params) @property @@ -1085,20 +1096,29 @@ def element(self): def dynamic(self): from .core.spaces import DynamicMap return DynamicMap(lambda *args, **kwargs: self.element, streams=[self]) - + class BoxEdit(CDSStream): """ Attaches a BoxEditTool and syncs the datasource. + empty_value: int/float/string/None + The value to insert on non-position columns when adding a new box + num_objects: int The number of boxes that can be drawn before overwriting the oldest drawn box. + + styles: dict + A dictionary specifying lists of styles to cycle over whenever + a new box glyph is drawn. """ - def __init__(self, num_objects=0, **params): + def __init__(self, empty_value=None, num_objects=0, styles={}, **params): + self.empty_value = empty_value self.num_objects = num_objects + self.styles = styles super(BoxEdit, self).__init__(**params) @property