From f19c365cf052c40dffb62d482504502ceb963606 Mon Sep 17 00:00:00 2001
From: Philipp Rudiger
Date: Fri, 10 Aug 2018 22:35:37 +0100
Subject: [PATCH 1/4] Add new drawing tool functionality
---
holoviews/plotting/bokeh/callbacks.py | 77 +++++++++++++++++++++++----
holoviews/streams.py | 71 +++++++++++++++++++++++-
2 files changed, 136 insertions(+), 12 deletions(-)
diff --git a/holoviews/plotting/bokeh/callbacks.py b/holoviews/plotting/bokeh/callbacks.py
index cfd7266b1f..e0dcb02c38 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(num_objects=stream.num_objects, renderers=[r1])
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
From f0cb916386cd8969437b570c8cbac13599c7dff1 Mon Sep 17 00:00:00 2001
From: Philipp Rudiger
Date: Sat, 11 Aug 2018 12:32:40 +0100
Subject: [PATCH 2/4] Updated draw tool documentation with latest features
---
.../reference/streams/bokeh/BoxEdit.ipynb | 4 +-
.../streams/bokeh/FreehandDraw.ipynb | 103 ++++++++++++++++++
.../reference/streams/bokeh/PointDraw.ipynb | 12 +-
.../reference/streams/bokeh/PolyDraw.ipynb | 6 +-
4 files changed, 115 insertions(+), 10 deletions(-)
create mode 100644 examples/reference/streams/bokeh/FreehandDraw.ipynb
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..65de4bdef9
--- /dev/null
+++ b/examples/reference/streams/bokeh/FreehandDraw.ipynb
@@ -0,0 +1,103 @@
+{
+ "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": {
+ "kernelspec": {
+ "display_name": "Python 3",
+ "language": "python",
+ "name": "python3"
+ },
+ "language_info": {
+ "codemirror_mode": {
+ "name": "ipython",
+ "version": 3
+ },
+ "file_extension": ".py",
+ "mimetype": "text/x-python",
+ "name": "python",
+ "nbconvert_exporter": "python",
+ "pygments_lexer": "ipython3",
+ "version": "3.6.4"
+ }
+ },
+ "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"
]
},
From 14f1eafade3f050a03b0e5e8ed1c1e94bd4c0d06 Mon Sep 17 00:00:00 2001
From: Philipp Rudiger
Date: Sat, 11 Aug 2018 12:59:07 +0100
Subject: [PATCH 3/4] Small fix for BoxEditTool
---
holoviews/plotting/bokeh/callbacks.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/holoviews/plotting/bokeh/callbacks.py b/holoviews/plotting/bokeh/callbacks.py
index e0dcb02c38..3635d26d69 100644
--- a/holoviews/plotting/bokeh/callbacks.py
+++ b/holoviews/plotting/bokeh/callbacks.py
@@ -957,7 +957,7 @@ 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(num_objects=stream.num_objects, 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()
From 27d0927e99b97162546676056720736878dfe277 Mon Sep 17 00:00:00 2001
From: Philipp Rudiger
Date: Mon, 13 Aug 2018 18:06:06 +0100
Subject: [PATCH 4/4] Stripped notebook metadata
---
.../reference/streams/bokeh/FreehandDraw.ipynb | 15 +--------------
1 file changed, 1 insertion(+), 14 deletions(-)
diff --git a/examples/reference/streams/bokeh/FreehandDraw.ipynb b/examples/reference/streams/bokeh/FreehandDraw.ipynb
index 65de4bdef9..318475d81e 100644
--- a/examples/reference/streams/bokeh/FreehandDraw.ipynb
+++ b/examples/reference/streams/bokeh/FreehandDraw.ipynb
@@ -80,22 +80,9 @@
}
],
"metadata": {
- "kernelspec": {
- "display_name": "Python 3",
- "language": "python",
- "name": "python3"
- },
"language_info": {
- "codemirror_mode": {
- "name": "ipython",
- "version": 3
- },
- "file_extension": ".py",
- "mimetype": "text/x-python",
"name": "python",
- "nbconvert_exporter": "python",
- "pygments_lexer": "ipython3",
- "version": "3.6.4"
+ "pygments_lexer": "ipython3"
}
},
"nbformat": 4,