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

Add new drawing tool functionality #2937

Merged
merged 4 commits into from Aug 13, 2018
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 2 additions & 2 deletions examples/reference/streams/bokeh/BoxEdit.ipynb
Expand Up @@ -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."
]
},
{
Expand All @@ -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"
]
},
Expand Down
103 changes: 103 additions & 0 deletions 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": [
"<center><img src=\"https://assets.holoviews.org/gifs/examples/streams/bokeh/freehand_draw.gif\" width=400></center>"
]
},
{
"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:"
]
},
{
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please clear out the notebook metadata...

"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
}
12 changes: 7 additions & 5 deletions examples/reference/streams/bokeh/PointDraw.ipynb
Expand Up @@ -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."
]
},
{
Expand All @@ -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')"
]
},
{
Expand Down
6 changes: 3 additions & 3 deletions examples/reference/streams/bokeh/PolyDraw.ipynb
Expand Up @@ -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. "
]
},
{
Expand All @@ -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"
]
},
Expand Down
77 changes: 67 additions & 10 deletions holoviews/plotting/bokeh/callbacks.py
Expand Up @@ -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):
Expand Down Expand Up @@ -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']

Expand All @@ -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:
Expand All @@ -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)
Expand All @@ -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):
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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

Expand Down