diff --git a/examples/reference/streams/bokeh/Bounds.ipynb b/examples/reference/streams/bokeh/Bounds.ipynb index b4bbb33fd6..376814d7bc 100644 --- a/examples/reference/streams/bokeh/Bounds.ipynb +++ b/examples/reference/streams/bokeh/Bounds.ipynb @@ -45,7 +45,7 @@ " kdims=[], streams=[sel])\n", "\n", "# Declare a Bounds stream and DynamicMap to get box_select geometry and draw it\n", - "box = streams.Bounds(source=points, bounds=(0,0,0,0))\n", + "box = streams.BoundsXY(source=points, bounds=(0,0,0,0))\n", "bounds = hv.DynamicMap(lambda bounds: hv.Bounds(bounds), streams=[box])\n", "\n", "# Declare DynamicMap to apply bounds selection\n", diff --git a/examples/reference/streams/bokeh/BoundsX.ipynb b/examples/reference/streams/bokeh/BoundsX.ipynb index b9ab250d37..b6e71029e6 100644 --- a/examples/reference/streams/bokeh/BoundsX.ipynb +++ b/examples/reference/streams/bokeh/BoundsX.ipynb @@ -33,8 +33,6 @@ "metadata": {}, "outputs": [], "source": [ - "%%opts Curve[tools=['xbox_select']]\n", - "\n", "n=200\n", "xs = np.linspace(0, 1, n)\n", "ys = np.cumsum(np.random.randn(n))\n", @@ -45,7 +43,9 @@ " sub = df.set_index('x').loc[boundsx[0]:boundsx[1]]\n", " return hv.Table(sub.describe().reset_index().values, 'stat', 'value')\n", "\n", - "curve + hv.DynamicMap(make_from_boundsx, streams=[streams.BoundsX(source=curve, boundsx=(0,0))])" + "dmap = hv.DynamicMap(make_from_boundsx, streams=[streams.BoundsX(source=curve, boundsx=(0,0))])\n", + "\n", + "curve.options(tools=['xbox_select']) + dmap" ] }, { diff --git a/examples/reference/streams/bokeh/BoundsY.ipynb b/examples/reference/streams/bokeh/BoundsY.ipynb index 1ffd011516..1d49b0b391 100644 --- a/examples/reference/streams/bokeh/BoundsY.ipynb +++ b/examples/reference/streams/bokeh/BoundsY.ipynb @@ -22,7 +22,8 @@ "source": [ "import numpy as np\n", "import holoviews as hv\n", - "from holoviews import streams\n", + "from holoviews import opts, streams\n", + "\n", "hv.extension('bokeh')" ] }, @@ -32,8 +33,6 @@ "metadata": {}, "outputs": [], "source": [ - "%%opts Curve[tools=['ybox_select']]\n", - "\n", "xs = np.linspace(0, 1, 200)\n", "ys = xs*(1-xs)\n", "curve = hv.Curve((xs,ys))\n", @@ -48,8 +47,13 @@ " times = [\"{0:.2f}\".format(x) for x in sorted(np.roots([-1,1,-boundsy[0]])) + sorted(np.roots([-1,1,-boundsy[1]]))]\n", " return hv.ItemTable(sorted(zip(['1_entry', '2_exit', '1_exit', '2_entry'], times)))\n", "\n", + "area_dmap = hv.DynamicMap(make_area, streams=[bounds_stream])\n", + "table_dmap = hv.DynamicMap(make_items, streams=[bounds_stream])\n", "\n", - "curve * hv.DynamicMap(make_area, streams=[bounds_stream]) + hv.DynamicMap(make_items, streams=[bounds_stream])" + "(curve * area_dmap + table_dmap).options(\n", + " opts.Layout(merge_tools=False),\n", + " opts.Overlay(tools=['ybox_select'], active_tools=['ybox_select'])\n", + ")" ] }, { diff --git a/examples/reference/streams/bokeh/BoxEdit.ipynb b/examples/reference/streams/bokeh/BoxEdit.ipynb index a78fb0f2a4..bc4d713070 100644 --- a/examples/reference/streams/bokeh/BoxEdit.ipynb +++ b/examples/reference/streams/bokeh/BoxEdit.ipynb @@ -57,10 +57,10 @@ "metadata": {}, "outputs": [], "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, num_objects=2)\n", - "boxes" + "boxes.options(\n", + " active_tools=['box_edit'], fill_alpha=0.5, height=400, width=400)" ] }, { diff --git a/examples/reference/streams/bokeh/FreehandDraw.ipynb b/examples/reference/streams/bokeh/FreehandDraw.ipynb index 318475d81e..5e34e30cdb 100644 --- a/examples/reference/streams/bokeh/FreehandDraw.ipynb +++ b/examples/reference/streams/bokeh/FreehandDraw.ipynb @@ -34,9 +34,11 @@ "metadata": {}, "outputs": [], "source": [ - "path = hv.Path([]).options(width=400, height=400, default_tools=[], line_width=10)\n", + "path = hv.Path([])\n", "freehand = streams.FreehandDraw(source=path, num_objects=3)\n", - "path" + "\n", + "path.options(\n", + " active_tools=['freehand_draw'], height=400, line_width=10, width=400)" ] }, { diff --git a/examples/reference/streams/bokeh/PointDraw.ipynb b/examples/reference/streams/bokeh/PointDraw.ipynb index ff09acc8ab..ebb701579a 100644 --- a/examples/reference/streams/bokeh/PointDraw.ipynb +++ b/examples/reference/streams/bokeh/PointDraw.ipynb @@ -21,7 +21,9 @@ "outputs": [], "source": [ "import holoviews as hv\n", - "from holoviews import streams\n", + "from holoviews import opts, streams\n", + "from holoviews.plotting.links import DataLink\n", + "\n", "hv.extension('bokeh')" ] }, @@ -59,12 +61,17 @@ "metadata": {}, "outputs": [], "source": [ - "%%opts Points (color='color' size=10) [tools=['hover'] width=400 height=400] \n", - "%%opts Layout [shared_datasource=True] Table (editable=True)\n", "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')" + "table = hv.Table(points, ['x', 'y'], 'color')\n", + "DataLink(points, table)\n", + "\n", + "(points + table).options(\n", + " opts.Layout(merge_tools=False),\n", + " opts.Points(active_tools=['point_draw'], color='color', height=400,\n", + " size=10, tools=['hover'], width=400),\n", + " opts.Table(editable=True))" ] }, { diff --git a/examples/reference/streams/bokeh/PolyDraw.ipynb b/examples/reference/streams/bokeh/PolyDraw.ipynb index 838053edec..e03d22c3ba 100644 --- a/examples/reference/streams/bokeh/PolyDraw.ipynb +++ b/examples/reference/streams/bokeh/PolyDraw.ipynb @@ -21,7 +21,7 @@ "outputs": [], "source": [ "import holoviews as hv\n", - "from holoviews import streams\n", + "from holoviews import opts, streams\n", "\n", "hv.extension('bokeh')" ] @@ -58,12 +58,14 @@ "metadata": {}, "outputs": [], "source": [ - "%%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, show_vertices=True)\n", "poly_stream = streams.PolyDraw(source=poly, drag=True, num_objects=2, show_vertices=True)\n", - "path * poly" + "\n", + "(path * poly).options(\n", + " opts.Path(color='red', height=400, line_width=5, width=400),\n", + " opts.Polygons(fill_alpha=0.3, active_tools=['poly_draw']))" ] }, { diff --git a/examples/reference/streams/bokeh/PolyEdit.ipynb b/examples/reference/streams/bokeh/PolyEdit.ipynb index d2b44bd5ea..4de5de9370 100644 --- a/examples/reference/streams/bokeh/PolyEdit.ipynb +++ b/examples/reference/streams/bokeh/PolyEdit.ipynb @@ -22,7 +22,7 @@ "source": [ "import numpy as np\n", "import holoviews as hv\n", - "from holoviews import streams\n", + "from holoviews import opts, streams\n", "\n", "hv.extension('bokeh')" ] @@ -59,14 +59,15 @@ "metadata": {}, "outputs": [], "source": [ - "%%opts Polygons [width=400 height=400] (fill_alpha=0.4)\n", "polys = hv.Polygons([hv.Box(*i, spec=np.random.rand()/3)\n", " for i in np.random.rand(10, 2)])\n", "ovals = hv.Polygons([hv.Ellipse(*i, spec=np.random.rand()/3)\n", " for i in np.random.rand(10, 2)])\n", "poly_edit = streams.PolyEdit(source=polys, vertex_style={'color': 'red'}, shared=True)\n", "poly_edit2 = streams.PolyEdit(source=ovals, shared=True)\n", - "polys * ovals" + "\n", + "(polys * ovals).options(\n", + " opts.Polygons(active_tools=['poly_edit'], fill_alpha=0.4, height=400, width=400))" ] }, { diff --git a/examples/reference/streams/bokeh/RangeXY.ipynb b/examples/reference/streams/bokeh/RangeXY.ipynb index e966921786..0318b68840 100644 --- a/examples/reference/streams/bokeh/RangeXY.ipynb +++ b/examples/reference/streams/bokeh/RangeXY.ipynb @@ -22,6 +22,7 @@ "source": [ "import numpy as np\n", "import holoviews as hv\n", + "\n", "hv.extension('bokeh')" ] }, diff --git a/examples/reference/streams/bokeh/Selection1D_paired.ipynb b/examples/reference/streams/bokeh/Selection1D_paired.ipynb index dc2b00d3d0..4047e45a1c 100644 --- a/examples/reference/streams/bokeh/Selection1D_paired.ipynb +++ b/examples/reference/streams/bokeh/Selection1D_paired.ipynb @@ -22,7 +22,8 @@ "source": [ "import numpy as np\n", "import holoviews as hv\n", - "from holoviews import streams\n", + "from holoviews import opts, streams\n", + "\n", "hv.extension('bokeh')" ] }, @@ -32,8 +33,6 @@ "metadata": {}, "outputs": [], "source": [ - "%%opts Points [tools=['box_select', 'lasso_select', 'tap']]\n", - "\n", "# Declare two sets of points generated from multivariate distribution\n", "points = hv.Points(np.random.multivariate_normal((0, 0), [[1, 0.1], [0.1, 1]], (1000,)))\n", "points2 = hv.Points(np.random.multivariate_normal((3, 3), [[1, 0.1], [0.1, 1]], (1000,)))\n", @@ -47,7 +46,9 @@ "hline2 = hv.DynamicMap(lambda index: hv.HLine(points2['y'][index].mean() if index else -10), streams=[sel2])\n", "\n", "# Combine points and dynamic HLines\n", - "points * points2 * hline1 * hline2" + "(points * points2 * hline1 * hline2).options(\n", + " opts.Points(active_tools=['box_select', 'tap'], height=400,\n", + " tools=['box_select', 'lasso_select', 'tap'], width=400))" ] }, { diff --git a/examples/reference/streams/bokeh/Tap.ipynb b/examples/reference/streams/bokeh/Tap.ipynb index ff1d5cad95..ccd1484bbd 100644 --- a/examples/reference/streams/bokeh/Tap.ipynb +++ b/examples/reference/streams/bokeh/Tap.ipynb @@ -23,6 +23,8 @@ "import pandas as pd\n", "import numpy as np\n", "import holoviews as hv\n", + "from holoviews import opts\n", + "\n", "hv.extension('bokeh', width=90)" ] }, @@ -32,9 +34,6 @@ "metadata": {}, "outputs": [], "source": [ - "%opts HeatMap [width=700 height=500 logz=True fontsize={'xticks': '6pt'}, tools=['hover'] xrotation=90] (cmap='RdBu_r') \n", - "%opts Curve [width=375 height=500 yaxis='right'] (line_color='black') {+framewise}\n", - "\n", "# Declare dataset\n", "df = pd.read_csv('http://assets.holoviews.org/data/diseases.csv.gz', compression='gzip')\n", "dataset = hv.Dataset(df, vdims=('measles','Measles Incidence'))\n", @@ -51,7 +50,13 @@ " return hv.Curve(dataset.select(State=y, Year=int(x)), kdims='Week',\n", " label='Year: %s, State: %s' % (x, y))\n", "\n", - "heatmap + hv.DynamicMap(tap_histogram, kdims=[], streams=[posxy])" + "tap_dmap = hv.DynamicMap(tap_histogram, streams=[posxy])\n", + "\n", + "(heatmap + tap_dmap).options(\n", + " opts.Curve(framewise=True, height=500, line_color='black', width=375, yaxis='right'),\n", + " opts.HeatMap(cmap='RdBu_r', fontsize={'xticks': '6pt'}, height=500,\n", + " logz=True, tools=['hover'], width=700, xrotation=90)\n", + ")" ] }, { diff --git a/examples/user_guide/Linking_Plots.ipynb b/examples/user_guide/Linking_Plots.ipynb index a3ae24d660..6a62c51b40 100644 --- a/examples/user_guide/Linking_Plots.ipynb +++ b/examples/user_guide/Linking_Plots.ipynb @@ -37,7 +37,7 @@ "\n", "dlink = DataLink(scatter1, scatter2)\n", "\n", - "(scatter1 + scatter2).options('Scatter', tools=['box_select', 'lasso_select'], clone=False)" + "(scatter1 + scatter2).options('Scatter', tools=['box_select', 'lasso_select'])" ] }, { @@ -75,12 +75,12 @@ "\n", "data = np.random.randn(1000).cumsum()\n", "\n", - "source = hv.Curve(data).options(width=800, height=100, axiswise=True)\n", - "target = hv.Curve(data).options(width=800, labelled=['y'])\n", + "source = hv.Curve(data).options(width=800, height=125, axiswise=True, default_tools=[])\n", + "target = hv.Curve(data).options(width=800, labelled=['y'], toolbar=None)\n", "\n", "rtlink = RangeToolLink(source, target)\n", "\n", - "(target + source).cols(1)" + "(target + source).options(merge_tools=False).cols(1)" ] }, { @@ -228,7 +228,8 @@ "source": [ "options = dict(\n", " selection_fill_color='firebrick', alpha=0.4, line_color='black', size=8,\n", - " tools=['lasso_select', 'box_select'], width=500, height=500\n", + " tools=['lasso_select', 'box_select'], width=500, height=500,\n", + " active_tools=['lasso_select']\n", ")\n", "scatter = hv.Scatter(np.random.randn(500, 2)).options(**options)\n", "vline = hv.VLine(scatter['x'].mean()).options(color='black')\n", diff --git a/holoviews/plotting/bokeh/element.py b/holoviews/plotting/bokeh/element.py index f0b3f488c9..d0816dc92c 100644 --- a/holoviews/plotting/bokeh/element.py +++ b/holoviews/plotting/bokeh/element.py @@ -10,9 +10,10 @@ from bokeh.core.properties import value from bokeh.document.events import ModelChangedEvent -from bokeh.models import (HoverTool, Renderer, Range1d, DataRange1d, Title, - FactorRange, FuncTickFormatter, Tool, Legend, - TickFormatter, PrintfTickFormatter) +from bokeh.models import tools +from bokeh.models import ( + Renderer, Range1d, DataRange1d, Title, FactorRange, Legend, + FuncTickFormatter, TickFormatter, PrintfTickFormatter) from bokeh.models.tickers import Ticker, BasicTicker, FixedTicker, LogTicker from bokeh.models.widgets import Panel, Tabs from bokeh.models.mappers import LinearColorMapper @@ -32,21 +33,28 @@ from ...util.transform import dim from ..plot import GenericElementPlot, GenericOverlayPlot from ..util import dynamic_update, process_cmap, color_intervals, dim_range_key -from .plot import BokehPlot, TOOLS +from .plot import BokehPlot from .styles import ( legend_dimensions, line_properties, mpl_to_bokeh, property_prefixes, rgba_tuple, text_properties, validate ) from .util import ( - bokeh_version, decode_bytes, get_tab_title, glyph_order, - py2js_tickformatter, recursive_model_update, theme_attr_json, - cds_column_replace, hold_policy, match_dim_specs, date_to_integer + TOOL_TYPES, bokeh_version, date_to_integer, decode_bytes, + get_tab_title, glyph_order, py2js_tickformatter, + recursive_model_update, theme_attr_json, cds_column_replace, + hold_policy, match_dim_specs ) class ElementPlot(BokehPlot, GenericElementPlot): + active_tools = param.List(default=[], doc=""" + Allows specifying which tools are active by default. Note + that only one tool per gesture type can be active, e.g. + both 'pan' and 'box_zoom' are drag tools, so if both are + listed only the last one will be active.""") + border = param.Number(default=10, doc=""" Minimum border around plot.""") @@ -168,26 +176,27 @@ def _init_tools(self, element, callbacks=[]): if handle and handle in known_tools: tool_names.append(handle) if handle == 'hover': - tool = HoverTool(tooltips=tooltips, **hover_opts) + tool = tools.HoverTool(tooltips=tooltips, **hover_opts) hover = tool else: tool = known_tools[handle]() cb_tools.append(tool) self.handles[handle] = tool - tools = [t for t in cb_tools + self.default_tools + self.tools - if t not in tool_names] + tool_list = [ + t for t in cb_tools + self.default_tools + self.tools + if t not in tool_names] copied_tools = [] - for tool in tools: - if isinstance(tool, Tool): + for tool in tool_list: + if isinstance(tool, tools.Tool): properties = tool.properties_with_values(include_defaults=False) tool = type(tool)(**properties) copied_tools.append(tool) - hover_tools = [t for t in copied_tools if isinstance(t, HoverTool)] + hover_tools = [t for t in copied_tools if isinstance(t, tools.HoverTool)] if 'hover' in copied_tools: - hover = HoverTool(tooltips=tooltips, **hover_opts) + hover = tools.HoverTool(tooltips=tooltips, **hover_opts) copied_tools[copied_tools.index('hover')] = hover elif any(hover_tools): hover = hover_tools[0] @@ -408,6 +417,28 @@ def _plot_properties(self, key, plot, element): return plot_props + def _set_active_tools(self, plot): + "Activates the list of active tools" + for tool in self.active_tools: + if isinstance(tool, util.basestring): + tool_type = TOOL_TYPES[tool] + matching = [t for t in plot.toolbar.tools + if isinstance(t, tool_type)] + if not matching: + raise ValueError('Tool of type %r could not be found ' + 'and could not be activated by default.' + % tool) + tool = matching[0] + if isinstance(tool, tools.Drag): + plot.toolbar.active_drag = tool + if isinstance(tool, tools.Scroll): + plot.toolbar.active_scroll = tool + if isinstance(tool, tools.Tap): + plot.toolbar.active_tap = tool + if isinstance(tool, tools.Inspection): + plot.toolbar.active_inspect.append(tool) + + def _title_properties(self, key, plot, element): if self.show_title and self.adjoined is None: title = self._format_title(key, separator=' ') @@ -1007,6 +1038,7 @@ def initialize_plot(self, ranges=None, plot=None, plots=None, source=None): cb.initialize() if not self.overlaid: + self._set_active_tools(plot) self._process_legend() self._execute_hooks(element) @@ -1084,6 +1116,7 @@ def update_frame(self, key, ranges=None, plot=None, element=None): if not self.overlaid: self._update_ranges(style_element, ranges) self._update_plot(key, plot, style_element) + self._set_active_tools(plot) self._update_glyphs(element, ranges, self.style[self.cyclic_index]) self._execute_hooks(element) @@ -1563,7 +1596,7 @@ class OverlayPlot(GenericOverlayPlot, LegendPlot): 'title_format', 'legend_position', 'legend_offset', 'legend_cols', 'gridstyle', 'legend_muted', 'padding', 'xlabel', 'ylabel', 'xlim', 'ylim', 'zlim', - 'xformatter', 'yformatter'] + 'xformatter', 'yformatter', 'active_tools'] def __init__(self, overlay, **params): super(OverlayPlot, self).__init__(overlay, **params) @@ -1677,19 +1710,18 @@ def _init_tools(self, element, callbacks=[]): """ Processes the list of tools to be supplied to the plot. """ - tools = [] hover_tools = {} - tool_types = [] + init_tools, tool_types = [], [] for key, subplot in self.subplots.items(): el = element.get(key) if el is not None: el_tools = subplot._init_tools(el, self.callbacks) for tool in el_tools: if isinstance(tool, util.basestring): - tool_type = TOOLS.get(tool) + tool_type = TOOL_TYPES.get(tool) else: tool_type = type(tool) - if isinstance(tool, HoverTool): + if isinstance(tool, tools.HoverTool): tooltips = tuple(tool.tooltips) if tool.tooltips else () if tooltips in hover_tools: continue @@ -1699,9 +1731,9 @@ def _init_tools(self, element, callbacks=[]): continue else: tool_types.append(tool_type) - tools.append(tool) + init_tools.append(tool) self.handles['hover_tools'] = hover_tools - return tools + return init_tools def _merge_tools(self, subplot): @@ -1791,6 +1823,7 @@ def initialize_plot(self, ranges=None, plot=None, plots=None): self.handles['plot'] = Tabs(tabs=panels) elif not self.overlaid: self._process_legend() + self._set_active_tools(plot) self.drawn = True self.handles['plots'] = plots @@ -1880,7 +1913,9 @@ def update_frame(self, key, ranges=None, element=None): self._process_legend() if element and not self.overlaid and not self.tabs and not self.batched: - self._update_plot(key, self.handles['plot'], element) + plot = self.handles['plot'] + self._update_plot(key, plot, element) + self._set_active_tools(plot) self._process_legend() diff --git a/holoviews/plotting/bokeh/plot.py b/holoviews/plotting/bokeh/plot.py index f7c745ee88..d1b08f78b4 100644 --- a/holoviews/plotting/bokeh/plot.py +++ b/holoviews/plotting/bokeh/plot.py @@ -10,7 +10,6 @@ from bokeh.layouts import gridplot from bokeh.models import (ColumnDataSource, Column, Row, Div) from bokeh.models.widgets import Panel, Tabs -from bokeh.plotting.helpers import _known_tools as known_tools from ...core import ( OrderedDict, Store, AdjointLayout, NdLayout, Layout, Empty, @@ -29,14 +28,11 @@ from ..util import attach_streams, displayable, collate from .callbacks import LinkCallback from .util import ( - layout_padding, pad_plots, filter_toolboxes, make_axis, - update_shared_sources, empty_plot, decode_bytes, theme_attr_json, - cds_column_replace + TOOL_TYPES, layout_padding, pad_plots, filter_toolboxes, + make_axis, update_shared_sources, empty_plot, decode_bytes, + theme_attr_json, cds_column_replace ) -TOOLS = {name: tool if isinstance(tool, basestring) else type(tool()) - for name, tool in known_tools.items()} - class BokehPlot(DimensionedPlot): """ @@ -290,7 +286,7 @@ def _update_callbacks(self, plot): the root level bokeh model. """ subplots = self.traverse(lambda x: x, [GenericElementPlot]) - merged_tools = {t: list(plot.select({'type': TOOLS[t]})) + merged_tools = {t: list(plot.select({'type': TOOL_TYPES[t]})) for t in self._merged_tools} for subplot in subplots: for cb in subplot.callbacks: diff --git a/holoviews/plotting/bokeh/util.py b/holoviews/plotting/bokeh/util.py index 94063666d9..5e65e29777 100644 --- a/holoviews/plotting/bokeh/util.py +++ b/holoviews/plotting/bokeh/util.py @@ -16,6 +16,7 @@ from bokeh.core.json_encoder import serialize_json # noqa (API import) from bokeh.core.properties import value from bokeh.layouts import WidgetBox, Row, Column +from bokeh.models import tools from bokeh.models import Model, ToolbarBox, FactorRange, Range1d, Plot, Spacer, CustomJS from bokeh.models.widgets import DataTable, Tabs, Div from bokeh.plotting import Figure @@ -41,6 +42,46 @@ bokeh_version = LooseVersion(bokeh.__version__) # noqa +TOOL_TYPES = { + 'pan': tools.PanTool, + 'xpan': tools.PanTool, + 'ypan': tools.PanTool, + 'xwheel_pan': tools.WheelPanTool, + 'ywheel_pan': tools.WheelPanTool, + 'wheel_zoom': tools.WheelZoomTool, + 'xwheel_zoom': tools.WheelZoomTool, + 'ywheel_zoom': tools.WheelZoomTool, + 'zoom_in': tools.ZoomInTool, + 'xzoom_in': tools.ZoomInTool, + 'yzoom_in': tools.ZoomInTool, + 'zoom_out': tools.ZoomOutTool, + 'xzoom_out': tools.ZoomOutTool, + 'yzoom_out': tools.ZoomOutTool, + 'click': tools.TapTool, + 'tap': tools.TapTool, + 'crosshair': tools.CrosshairTool, + 'box_select': tools.BoxSelectTool, + 'xbox_select': tools.BoxSelectTool, + 'ybox_select': tools.BoxSelectTool, + 'poly_select': tools.PolySelectTool, + 'lasso_select': tools.LassoSelectTool, + 'box_zoom': tools.BoxZoomTool, + 'xbox_zoom': tools.BoxZoomTool, + 'ybox_zoom': tools.BoxZoomTool, + 'hover': tools.HoverTool, + 'save': tools.SaveTool, + 'undo': tools.UndoTool, + 'redo': tools.RedoTool, + 'reset': tools.ResetTool, + 'help': tools.HelpTool, + 'box_edit': tools.BoxEditTool, + 'point_draw': tools.PointDrawTool, + 'poly_draw': tools.PolyDrawTool, + 'poly_edit': tools.PolyEditTool, + 'freehand_draw': tools.FreehandDrawTool +} + + def convert_timestamp(timestamp): """ Converts bokehJS timestamp to datetime64. diff --git a/holoviews/tests/plotting/bokeh/testelementplot.py b/holoviews/tests/plotting/bokeh/testelementplot.py index ac8bece0fe..c1b38a17fa 100644 --- a/holoviews/tests/plotting/bokeh/testelementplot.py +++ b/holoviews/tests/plotting/bokeh/testelementplot.py @@ -6,7 +6,7 @@ from bokeh.core.properties import value from holoviews.core import Dimension, DynamicMap, NdOverlay from holoviews.element import Curve, Image, Scatter, Labels -from holoviews.streams import Stream +from holoviews.streams import Stream, PointDraw from holoviews.plotting.util import process_cmap from .testplot import TestBokehPlot, bokeh_renderer @@ -14,6 +14,7 @@ try: from bokeh.document import Document + from bokeh.models import tools from bokeh.models import FuncTickFormatter, PrintfTickFormatter, NumeralTickFormatter except: pass @@ -332,6 +333,32 @@ def test_cftime_transform_noleap_warn(self): "tick formatting switch to the matplotlib backend.") self.log_handler.assertEndsWith('WARNING', substr) + def test_active_tools_drag(self): + curve = Curve([1, 2, 3]).options(active_tools=['box_zoom']) + plot = bokeh_renderer.get_plot(curve) + toolbar = plot.state.toolbar + self.assertIsInstance(toolbar.active_drag, tools.BoxZoomTool) + + def test_active_tools_scroll(self): + curve = Curve([1, 2, 3]).options(active_tools=['wheel_zoom']) + plot = bokeh_renderer.get_plot(curve) + toolbar = plot.state.toolbar + self.assertIsInstance(toolbar.active_scroll, tools.WheelZoomTool) + + def test_active_tools_tap(self): + curve = Curve([1, 2, 3]).options(active_tools=['tap'], tools=['tap']) + plot = bokeh_renderer.get_plot(curve) + toolbar = plot.state.toolbar + self.assertIsInstance(toolbar.active_tap, tools.TapTool) + + def test_active_tools_draw_stream(self): + scatter = Scatter([1, 2, 3]).options(active_tools=['point_draw']) + PointDraw(source=scatter) + plot = bokeh_renderer.get_plot(scatter) + toolbar = plot.state.toolbar + self.assertIsInstance(toolbar.active_tap, tools.PointDrawTool) + self.assertIsInstance(toolbar.active_drag, tools.PointDrawTool) + class TestColorbarPlot(TestBokehPlot): @@ -408,3 +435,37 @@ def test_overlay_legend_muted(self): plot = bokeh_renderer.get_plot(overlay) for sp in plot.subplots.values(): self.assertTrue(sp.handles['glyph_renderer'].muted) + + def test_active_tools_drag(self): + curve = Curve([1, 2, 3]) + scatter = Scatter([1, 2, 3]) + overlay = (scatter * curve).options(active_tools=['box_zoom']) + plot = bokeh_renderer.get_plot(overlay) + toolbar = plot.state.toolbar + self.assertIsInstance(toolbar.active_drag, tools.BoxZoomTool) + + def test_active_tools_scroll(self): + curve = Curve([1, 2, 3]) + scatter = Scatter([1, 2, 3]) + overlay = (scatter * curve).options(active_tools=['wheel_zoom']) + plot = bokeh_renderer.get_plot(overlay) + toolbar = plot.state.toolbar + self.assertIsInstance(toolbar.active_scroll, tools.WheelZoomTool) + + def test_active_tools_tap(self): + curve = Curve([1, 2, 3]) + scatter = Scatter([1, 2, 3]).options(tools=['tap']) + overlay = (scatter * curve).options(active_tools=['tap']) + plot = bokeh_renderer.get_plot(overlay) + toolbar = plot.state.toolbar + self.assertIsInstance(toolbar.active_tap, tools.TapTool) + + def test_active_tools_draw_stream(self): + curve = Curve([1, 2, 3]) + scatter = Scatter([1, 2, 3]).options(active_tools=['point_draw']) + PointDraw(source=scatter) + overlay = (scatter * curve) + plot = bokeh_renderer.get_plot(overlay) + toolbar = plot.state.toolbar + self.assertIsInstance(toolbar.active_tap, tools.PointDrawTool) + self.assertIsInstance(toolbar.active_drag, tools.PointDrawTool)