Skip to content

Commit

Permalink
Allow ability to define style cycles for Draw tools (#3612)
Browse files Browse the repository at this point in the history
  • Loading branch information
philippjfr authored Apr 28, 2019
1 parent 311e572 commit 9bf1270
Show file tree
Hide file tree
Showing 5 changed files with 103 additions and 67 deletions.
10 changes: 8 additions & 2 deletions examples/reference/streams/bokeh/BoxEdit.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -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."
]
},
{
Expand All @@ -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))"
]
Expand Down
9 changes: 8 additions & 1 deletion examples/reference/streams/bokeh/FreehandDraw.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -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."
]
},
Expand All @@ -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))"
Expand Down
16 changes: 14 additions & 2 deletions examples/reference/streams/bokeh/PolyDraw.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -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"
]
},
{
Expand All @@ -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",
Expand Down
107 changes: 49 additions & 58 deletions holoviews/plotting/bokeh/callbacks.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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):
Expand Down Expand Up @@ -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)
Expand All @@ -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']],
Expand All @@ -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,
Expand All @@ -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))
Expand All @@ -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:
Expand Down Expand Up @@ -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):
Expand Down
28 changes: 24 additions & 4 deletions holoviews/streams.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down

0 comments on commit 9bf1270

Please sign in to comment.