diff --git a/examples/reference/streams/bokeh/BoxEdit.ipynb b/examples/reference/streams/bokeh/BoxEdit.ipynb index 929ac824a4..a78fb0f2a4 100644 --- a/examples/reference/streams/bokeh/BoxEdit.ipynb +++ b/examples/reference/streams/bokeh/BoxEdit.ipynb @@ -48,7 +48,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "As a very straightforward example we will create a Polygons element containing multiple boxes, then attach it as a source to a ``BoxEdit`` stream instance. When we now plot the ``boxes`` Polygons instance it will add the tool, letting us draw, drag and delete the box polygons:" + "As a very straightforward example we will create a Polygons element containing multiple boxes, then attach it as a source to a ``BoxEdit`` stream instance. When we now plot the ``boxes`` Polygons instance it will add the tool, letting us draw, drag and delete the box polygons. To limit the number of boxes that can be drawn a fixed number of ``num_objects`` may be defined, causing the first box to be dropped when the limit is exceeded." ] }, { @@ -59,7 +59,7 @@ "source": [ "%%opts Polygons [width=400 height=400 default_tools=[]] (fill_alpha=0.5)\n", "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)\n", + "box_stream = streams.BoxEdit(source=boxes, num_objects=2)\n", "boxes" ] }, diff --git a/examples/reference/streams/bokeh/FreehandDraw.ipynb b/examples/reference/streams/bokeh/FreehandDraw.ipynb new file mode 100644 index 0000000000..318475d81e --- /dev/null +++ b/examples/reference/streams/bokeh/FreehandDraw.ipynb @@ -0,0 +1,90 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import holoviews as hv\n", + "from holoviews import streams\n", + "hv.extension('bokeh')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The ``FreehandDraw`` stream adds a bokeh tool to the source plot, which allows freehand drawing on the plot canvas and makes the resulting paths available to Python. The tool supports the following actions:\n", + "\n", + "**Draw**\n", + "\n", + " Click and drag to draw a line or polygon, release mouse to stop drawing\n", + " \n", + "**Delete line**\n", + "\n", + " Tap a line to select it then press BACKSPACE key while the mouse is within the plot area.\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." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "path = hv.Path([]).options(width=400, height=400, default_tools=[], line_width=10)\n", + "freehand = streams.FreehandDraw(source=path, num_objects=3)\n", + "path" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "
" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Whenever the data source is edited the data is synced with Python, both in the notebook and when deployed on the bokeh server. The data is made available as a dictionary of columns:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "freehand.data" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Alternatively we can use the ``element`` property to get an Element containing the returned data:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "freehand.element" + ] + } + ], + "metadata": { + "language_info": { + "name": "python", + "pygments_lexer": "ipython3" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/examples/reference/streams/bokeh/PointDraw.ipynb b/examples/reference/streams/bokeh/PointDraw.ipynb index 9929c6f73e..ff09acc8ab 100644 --- a/examples/reference/streams/bokeh/PointDraw.ipynb +++ b/examples/reference/streams/bokeh/PointDraw.ipynb @@ -48,7 +48,9 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "As a simple example we will create a ``PointDraw`` stream and attach it to a set of ``Points`` with a color dimension. By declaring the data as an ``OrderedDict`` and enabling the ``shared_datasource`` option on the ``Layout`` we can additionally link the ``Points`` to a ``Table``. We can now add drag and delete points, see the x/y position change in the table and edit the a color for each point in the table. Additionally the ``empty_value`` parameter on the ``PointDraw`` stream lets us define the value that will be inserted on columns other than the x/y position, which we can use here to set new points to 'black':" + "As a simple example we will create a ``PointDraw`` stream and attach it to a set of ``Points`` with a color dimension. By enabling the ``shared_datasource`` option on the ``Layout`` and casting to ``Points`` to a ``Table`` we can additionally link two.\n", + "\n", + "We can now add drag and delete points, see the x/y position change in the table and edit the a color for each point in the table. Additionally the ``empty_value`` parameter on the ``PointDraw`` stream lets us define the value that will be inserted on columns other than the x/y position, which we can use here to set new points to 'black'. Finally we can limit the number of points using the ``num_objects`` option, ensuring that once the limit is reached the oldest point is dropped." ] }, { @@ -59,10 +61,10 @@ "source": [ "%%opts Points (color='color' size=10) [tools=['hover'] width=400 height=400] \n", "%%opts Layout [shared_datasource=True] Table (editable=True)\n", - "data = hv.OrderedDict({'x': [0, 0.5, 1], 'y': [0, 0.5, 0], 'color': ['red', 'green', 'blue']})\n", - "points = hv.Points(data, vdims=['color']).redim.range(x=(-.1, 1.1), y=(-.1, 1.1))\n", - "point_stream = streams.PointDraw(data=points.columns(), source=points, empty_value='black')\n", - "points + hv.Table(data, ['x', 'y'], 'color')" + "data = ([0, 0.5, 1], [0, 0.5, 0], ['red', 'green', 'blue'])\n", + "points = hv.Points(data, vdims='color').redim.range(x=(-.1, 1.1), y=(-.1, 1.1))\n", + "point_stream = streams.PointDraw(data=points.columns(), num_objects=10, source=points, empty_value='black')\n", + "points + hv.Table(points, ['x', 'y'], 'color')" ] }, { diff --git a/examples/reference/streams/bokeh/PolyDraw.ipynb b/examples/reference/streams/bokeh/PolyDraw.ipynb index 20e8a9f369..838053edec 100644 --- a/examples/reference/streams/bokeh/PolyDraw.ipynb +++ b/examples/reference/streams/bokeh/PolyDraw.ipynb @@ -49,7 +49,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "As a simple example we will create simple ``Path`` and ``Polygons`` elements and attach each to a ``PolyDraw`` stream. We will also enable the ``drag`` option on the stream to enable dragging of existing glyphs." + "As a simple example we will create simple ``Path`` and ``Polygons`` elements and attach each to a ``PolyDraw`` stream. We will also enable the ``drag`` option on the stream to enable dragging of existing glyphs. Additionally we can enable the ``show_vertices`` option which shows the vertices of the drawn polygons/lines and adds the ability to snap to them. Finally the ``num_objects`` option limits the number of lines/polygons that can be drawn by dropping the first glyph when the limit is exceeded. " ] }, { @@ -61,8 +61,8 @@ "%%opts Path [width=400 height=400] (line_width=5 color='red') Polygons (fill_alpha=0.3)\n", "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)\n", - "poly_stream = streams.PolyDraw(source=poly, drag=True)\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", "path * poly" ] }, diff --git a/holoviews/plotting/bokeh/callbacks.py b/holoviews/plotting/bokeh/callbacks.py index cfd7266b1f..3635d26d69 100644 --- a/holoviews/plotting/bokeh/callbacks.py +++ b/holoviews/plotting/bokeh/callbacks.py @@ -14,11 +14,11 @@ RangeY, PointerX, PointerY, BoundsX, BoundsY, Tap, SingleTap, DoubleTap, MouseEnter, MouseLeave, PlotSize, Draw, BoundsXY, PlotReset, BoxEdit, - PointDraw, PolyDraw, PolyEdit, CDSStream) + PointDraw, PolyDraw, PolyEdit, CDSStream, FreehandDraw) from ...streams import PositionX, PositionY, PositionXY, Bounds # Deprecated: remove in 2.0 from ..links import Link, RangeToolLink, DataLink from ..plot import GenericElementPlot, GenericOverlayPlot -from .util import convert_timestamp +from .util import convert_timestamp, bokeh_version class MessageCallback(object): @@ -836,10 +836,19 @@ def initialize(self, plot_id=None): except Exception: param.main.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.warning('Specifying num_objects to PointDraw stream ' + 'only supported for bokeh version >= 1.0.0.') point_tool = PointDrawTool(drag=all(s.drag for s in self.streams), - empty_value=self.streams[0].empty_value, - renderers=renderers) + empty_value=stream.empty_value, + renderers=renderers, **kwargs) self.plot.state.tools.append(point_tool) source = self.plot.handles['source'] @@ -862,9 +871,48 @@ def initialize(self, plot_id=None): param.main.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.warning('Specifying num_objects to PointDraw stream ' + 'only supported for bokeh versions >=1.0.0.') + 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.warning('Enabling vertices on the PointDraw stream ' + 'only supported for bokeh versions >=1.0.0.') poly_tool = PolyDrawTool(drag=all(s.drag for s in self.streams), - empty_value=self.streams[0].empty_value, - renderers=[plot.handles['glyph_renderer']]) + empty_value=stream.empty_value, + renderers=[plot.handles['glyph_renderer']], + **kwargs) + plot.state.tools.append(poly_tool) + data = dict(plot.handles['source'].data) + for stream in self.streams: + stream.update(data=data) + super(CDSCallback, self).initialize(plot_id) + + +class FreehandDrawCallback(CDSCallback): + + def initialize(self, plot_id=None): + try: + from bokeh.models import FreehandDrawTool + except: + param.main.warning('FreehandDraw requires bokeh >= 0.13.0') + return + plot = self.plot + stream = self.streams[0] + poly_tool = FreehandDrawTool( + empty_value=stream.empty_value, + num_objects=stream.num_objects, + renderers=[plot.handles['glyph_renderer']], + ) plot.state.tools.append(poly_tool) data = dict(plot.handles['source'].data) for stream in self.streams: @@ -883,8 +931,17 @@ def initialize(self): except: param.main.warning('BoxEdit requires bokeh >= 0.12.14') return + plot = self.plot 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.warning('Specifying num_objects to BoxEdit stream ' + 'only supported for bokeh versions >=1.0.0.') xs, ys, widths, heights = [], [], [], [] for el in element.split(): x0, x1 = el.range(0) @@ -900,12 +957,11 @@ def initialize(self): style.pop('cmap', None) r1 = plot.state.rect('x', 'y', 'width', 'height', source=rect_source, **style) plot.handles['rect_source'] = rect_source - box_tool = BoxEditTool(renderers=[r1]) + box_tool = BoxEditTool(renderers=[r1], **kwargs) plot.state.tools.append(box_tool) self.plot.state.renderers.remove(plot.handles['glyph_renderer']) super(BoxEditCallback, self).initialize() - for stream in self.streams: - stream.update(data=self._process_msg({'data': data})['data']) + stream.update(data=self._process_msg({'data': data})['data']) def _process_msg(self, msg): @@ -933,7 +989,7 @@ def initialize(self, plot_id=None): tools = [tool for tool in plot.state.tools if isinstance(tool, PolyEditTool)] vertex_tool = tools[0] if tools else None if vertex_tool is None: - vertex_style = dict(size=10, **self.streams[0].vertex_style) + vertex_style = dict({'size': 10}, **self.streams[0].vertex_style) r1 = plot.state.scatter([], [], **vertex_style) vertex_tool = PolyEditTool(vertex_renderer=r1) plot.state.tools.append(vertex_tool) @@ -966,6 +1022,7 @@ def initialize(self, plot_id=None): callbacks[CDSStream] = CDSCallback callbacks[BoxEdit] = BoxEditCallback callbacks[PointDraw] = PointDrawCallback +callbacks[FreehandDraw] = FreehandDrawCallback callbacks[PolyDraw] = PolyDrawCallback callbacks[PolyEdit] = PolyEditCallback diff --git a/holoviews/streams.py b/holoviews/streams.py index a54982172a..d8f116aa90 100644 --- a/holoviews/streams.py +++ b/holoviews/streams.py @@ -844,11 +844,16 @@ class PointDraw(CDSStream): empty_value: int/float/string/None The value to insert on non-position columns when adding a new polygon + + num_objects: int + The number of polygons that can be drawn before overwriting + the oldest polygon. """ - def __init__(self, empty_value=None, drag=True, **params): + def __init__(self, empty_value=None, drag=True, num_objects=0, **params): self.drag = drag self.empty_value = empty_value + self.num_objects = num_objects super(PointDraw, self).__init__(**params) @property @@ -876,11 +881,27 @@ class PolyDraw(CDSStream): empty_value: int/float/string/None The value to insert on non-position columns when adding a new polygon + + num_objects: int + The number of polygons that can be drawn before overwriting + the oldest polygon. + + show_vertices: boolean + Whether to show the vertices when a polygon is selected + + vertex_style: dict + A dictionary specifying the style options for the vertices. + The usual bokeh style options apply, e.g. fill_color, + line_alpha, size, etc. """ - def __init__(self, empty_value=None, drag=True, **params): + def __init__(self, empty_value=None, drag=True, num_objects=0, + show_vertices=False, vertex_style={}, **params): self.drag = drag self.empty_value = empty_value + self.num_objects = num_objects + self.show_vertices = show_vertices + self.vertex_style = vertex_style super(PolyDraw, self).__init__(**params) @property @@ -904,12 +925,58 @@ def dynamic(self): return DynamicMap(lambda *args, **kwargs: self.element, streams=[self]) +class FreehandDraw(CDSStream): + """ + Attaches a FreehandDrawTool and syncs the datasource. + + empty_value: int/float/string/None + The value to insert on non-position columns when adding a new polygon + + num_objects: int + The number of polygons that can be drawn before overwriting + the oldest polygon. + """ + + def __init__(self, empty_value=None, num_objects=0, **params): + self.empty_value = empty_value + self.num_objects = num_objects + super(FreehandDraw, self).__init__(**params) + + @property + def element(self): + source = self.source + if isinstance(source, UniformNdMapping): + source = source.last + data = self.data + if not data: + return source.clone([]) + cols = list(self.data) + x, y = source.kdims + lookup = {'xs': x.name, 'ys': y.name} + data = [{lookup.get(c, c): data[c][i] for c in self.data} + for i in range(len(data[cols[0]]))] + return source.clone(data) + + @property + 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. + + num_objects: int + The number of boxes that can be drawn before overwriting the + oldest drawn box. """ + def __init__(self, num_objects=0, **params): + self.num_objects = num_objects + super(BoxEdit, self).__init__(**params) + @property def element(self): from .element import Polygons