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

Allow ability to define style cycles for Draw tools #3612

Merged
merged 5 commits into from
Apr 28, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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