From 72ca81b31e7259d315f4a6b0af6d6eeb82e7ac28 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Wed, 14 Sep 2016 00:33:37 +0100 Subject: [PATCH 01/21] Implemented ColorbarPlot and LegendPlot baseclasses in bokeh --- holoviews/plotting/bokeh/element.py | 141 +++++++++++++++++++++++----- 1 file changed, 120 insertions(+), 21 deletions(-) diff --git a/holoviews/plotting/bokeh/element.py b/holoviews/plotting/bokeh/element.py index 1493fc7997..4e65648ca9 100644 --- a/holoviews/plotting/bokeh/element.py +++ b/holoviews/plotting/bokeh/element.py @@ -7,6 +7,14 @@ from bokeh.models.tickers import Ticker, BasicTicker, FixedTicker from bokeh.models.widgets import Panel, Tabs +from bokeh.models.mappers import LinearColorMapper +try: + from bokeh.models import ColorBar + from bokeh.models.mappers import LogColorMapper +except ImportError: + LogColorMapper, ColorBar = None, None +from bokeh.models import LogTicker, BasicTicker + try: from bokeh import mpl except ImportError: @@ -22,7 +30,8 @@ from ..util import dynamic_update from .callbacks import Callbacks from .plot import BokehPlot -from .util import mpl_to_bokeh, convert_datetime, update_plot, bokeh_version +from .util import (mpl_to_bokeh, convert_datetime, update_plot, + bokeh_version, mplcmap_to_palette) if bokeh_version >= '0.12': from bokeh.models import FuncTickFormatter @@ -618,6 +627,115 @@ def framewise(self): for frame in current_frames) + +class ColorbarPlot(ElementPlot): + + colorbar = param.Boolean(default=False, doc=""" + Whether to display a colorbar.""") + + colorbar_position = param.ObjectSelector(objects=["top_right", + "top_left", + "bottom_left", + "bottom_right", + 'right', 'left', + 'top', 'bottom'], + default="right", + doc=""" + Allows selecting between a number of predefined colorbar position + options. The predefined options may be customized in the + colorbar_specs class attribute.""") + + colorbar_opts = param.Dict(default={}, doc=""" + Allows setting specific styling options for the colorbar overriding + the options defined in the colorbar_specs class attribute. Includes + location, orientation, height, width, scale_alpha, title, title_props, + margin, padding, background_fill_color and more.""") + + colorbar_specs = {'right': {'pos': 'right', 'opts': {'location': (0, 0)}}, + 'left': {'pos': 'left', 'opts':{'location':(0, 0)}}, + 'top_right': {'pos': 'center', 'opts': {'location': 'top_right'}}, + 'top_left': {'pos': 'center', 'opts': {'location': 'top_left'}}, + 'top': {'opts': {'location':(0, 0), 'orientation':'horizontal'}, + 'pos': 'above'}, + 'bottom': {'opts': {'location': (0, 0), 'orientation':'horizontal'}, + 'pos': 'below'}, + 'bottom_left': {'pos': 'center', + 'opts': {'location': 'bottom_center', + 'orientation': 'horizontal'}}, + 'bottom_right': {'pos': 'center', + 'opts': {'location': 'bottom_right', + 'orientation': 'horizontal'}}} + + logz = param.Boolean(default=False, doc=""" + Whether to apply log scaling to the z-axis.""") + + def _draw_colorbar(self, plot, color_mapper): + if LogColorMapper and isinstance(color_mapper, LogColorMapper): + ticker = LogTicker() + else: + ticker = BasicTicker() + cbar_opts = self.colorbar_specs[self.colorbar_position] + + # Check if there is a colorbar in the same position + pos = cbar_opts['pos'] + if any(isinstance(model, ColorBar) for model in getattr(plot, pos, [])): + return + + color_bar = ColorBar(color_mapper=color_mapper, ticker=ticker, + **dict(cbar_opts['opts'], **self.colorbar_opts)) + + plot.add_layout(color_bar, pos) + self.handles['colorbar'] = color_bar + + + def _get_colormapper(self, dim, element, ranges, style): + low, high = ranges.get(dim.name) + if 'cmap' in style: + palette = mplcmap_to_palette(style.pop('cmap', None)) + colormapper = LogColorMapper if self.logz else LinearColorMapper + cmapper = colormapper(palette, low=low, high=high) + if 'color_mapper' not in self.handles: + self.handles['color_mapper'] = cmapper + return cmapper + + + def _init_glyph(self, plot, mapping, properties): + """ + Returns a Bokeh glyph object and optionally creates a colorbar. + """ + ret = super(ColorbarPlot, self)._init_glyph(plot, mapping, properties) + if self.colorbar and 'color_mapper' in self.handles: + self._draw_colorbar(plot, self.handles['color_mapper']) + return ret + + + +class LegendPlot(ElementPlot): + + legend_position = param.ObjectSelector(objects=["top_right", + "top_left", + "bottom_left", + "bottom_right", + 'right', 'left', + 'top', 'bottom'], + default="top_right", + doc=""" + Allows selecting between a number of predefined legend position + options. The predefined options may be customized in the + legend_specs class attribute.""") + + + legend_cols = param.Integer(default=False, doc=""" + Whether to lay out the legend as columns.""") + + + legend_specs = {'right': dict(pos='right', loc=(5, -40)), + 'left': dict(pos='left', loc=(0, -40)), + 'top': dict(pos='above', loc=(120, 5)), + 'bottom': dict(pos='below', loc=(60, 0))} + + + class BokehMPLWrapper(ElementPlot): """ Wraps an existing HoloViews matplotlib plot and converts @@ -710,22 +828,8 @@ def update_frame(self, key, ranges=None, element=None): self.handles['plot'] = self._render_plot(element) -class OverlayPlot(GenericOverlayPlot, ElementPlot): +class OverlayPlot(GenericOverlayPlot, LegendPlot): - legend_position = param.ObjectSelector(objects=["top_right", - "top_left", - "bottom_left", - "bottom_right", - 'right', 'left', - 'top', 'bottom'], - default="top_right", - doc=""" - Allows selecting between a number of predefined legend position - options. The predefined options may be customized in the - legend_specs class attribute.""") - - legend_cols = param.Integer(default=False, doc=""" - Whether to lay out the legend as columns.""") tabs = param.Boolean(default=False, doc=""" Whether to display overlaid plots in separate panes""") @@ -734,11 +838,6 @@ class OverlayPlot(GenericOverlayPlot, ElementPlot): _update_handles = ['source'] - legend_specs = {'right': dict(pos='right', loc=(5, -40)), - 'left': dict(pos='left', loc=(0, -40)), - 'top': dict(pos='above', loc=(120, 5)), - 'bottom': dict(pos='below', loc=(60, 0))} - def _process_legend(self): plot = self.handles['plot'] if not self.show_legend or len(plot.legend) == 0: From 14048bdbf1967eea1ee71dad1c4072266ef660d3 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Wed, 14 Sep 2016 00:34:31 +0100 Subject: [PATCH 02/21] Added client side colormapping for PointPlot --- holoviews/plotting/bokeh/chart.py | 27 ++++++++++----------------- 1 file changed, 10 insertions(+), 17 deletions(-) diff --git a/holoviews/plotting/bokeh/chart.py b/holoviews/plotting/bokeh/chart.py index 5a857f7940..8e3432b18f 100644 --- a/holoviews/plotting/bokeh/chart.py +++ b/holoviews/plotting/bokeh/chart.py @@ -12,12 +12,12 @@ from ...core.util import max_range, basestring, dimension_sanitizer from ...core.options import abbreviated_exception from ..util import compute_sizes, get_sideplot_ranges, match_spec, map_colors -from .element import ElementPlot, line_properties, fill_properties +from .element import ElementPlot, ColorbarPlot, line_properties, fill_properties from .path import PathPlot, PolygonPlot from .util import get_cmap, mpl_to_bokeh, update_plot, rgb2hex, bokeh_version -class PointPlot(ElementPlot): +class PointPlot(ColorbarPlot): color_index = param.ClassSelector(default=3, class_=(basestring, int), allow_None=True, doc=""" @@ -55,21 +55,12 @@ def get_data(self, element, ranges=None, empty=False): mapping = dict(x=dims[xidx], y=dims[yidx]) data = {} - cmap = style.get('palette', style.get('cmap', None)) cdim = element.get_dimension(self.color_index) - if cdim and cmap: - map_key = 'color_' + cdim.name - mapping['color'] = map_key - if empty: - data[map_key] = [] - else: - cmap = get_cmap(cmap) - colors = element.dimension_values(self.color_index) - if colors.dtype.kind in 'if': - crange = ranges.get(cdim.name, element.range(cdim.name)) - else: - crange = np.unique(colors) - data[map_key] = map_colors(colors, crange, cmap) + if cdim: + mapper = self._get_colormapper(cdim, element, ranges, style) + data[cdim.name] = [] if empty else element.dimension_values(cdim) + mapping['color'] = {'field': cdim.name, + 'transform': mapper} sdim = element.get_dimension(self.size_index) if sdim: @@ -98,7 +89,7 @@ def get_batched_data(self, element, ranges=None, empty=False): eldata, elmapping = self.get_data(el, ranges, empty) for k, eld in eldata.items(): data[k].append(eld) - if 'color' not in eldata: + if 'color' not in elmapping: zorder = self.get_zorder(element, key, el) val = style[zorder].get('color') elmapping['color'] = 'color' @@ -128,6 +119,8 @@ def _init_glyph(self, plot, mapping, properties): else: plot_method = self._plot_methods.get('batched' if self.batched else 'single') renderer = getattr(plot, plot_method)(**dict(properties, **mapping)) + if self.colorbar and 'color_mapper' in self.handles: + self._draw_colorbar(plot, self.handles['color_mapper']) return renderer, renderer.glyph From 3e383f8e2f6d47794100d36141e9b939943ab14c Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Wed, 14 Sep 2016 00:35:21 +0100 Subject: [PATCH 03/21] Added client side colormapping for SideHistogramPlot --- holoviews/plotting/bokeh/chart.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/holoviews/plotting/bokeh/chart.py b/holoviews/plotting/bokeh/chart.py index 8e3432b18f..36472c817c 100644 --- a/holoviews/plotting/bokeh/chart.py +++ b/holoviews/plotting/bokeh/chart.py @@ -232,7 +232,7 @@ def get_data(self, element, ranges=None, empty=None): return (data, mapping) -class SideHistogramPlot(HistogramPlot): +class SideHistogramPlot(HistogramPlot, ColorbarPlot): style_opts = HistogramPlot.style_opts + ['cmap'] @@ -255,19 +255,20 @@ def get_data(self, element, ranges=None, empty=None): data = dict(top=element.values, left=element.edges[:-1], right=element.edges[1:]) - dim = element.get_dimension(0).name + dim = element.get_dimension(0) main = self.adjoined.main - range_item, main_range, dim = get_sideplot_ranges(self, element, main, ranges) - vals = element.dimension_values(dim) + range_item, main_range, _ = get_sideplot_ranges(self, element, main, ranges) if isinstance(range_item, (Raster, Points, Polygons, Spikes)): style = self.lookup_options(range_item, 'style')[self.cyclic_index] else: style = {} if 'cmap' in style or 'palette' in style: - cmap = get_cmap(style.get('cmap', style.get('palette', None))) - data['color'] = [] if empty else map_colors(vals, main_range, cmap) - mapping['fill_color'] = 'color' + main_range = {dim.name: main_range} + mapper = self._get_colormapper(dim, element, main_range, style) + data[dim.name] = [] if empty else element.dimension_values(dim) + mapping['fill_color'] = {'field': dim.name, + 'transform': mapper} self._get_hover_data(data, element, empty) return (data, mapping) From 883e185dc5da1c1cca21d374862a0d3afd5f7e27 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Wed, 14 Sep 2016 00:36:11 +0100 Subject: [PATCH 04/21] Added client side colormapping for PolygonPlot --- holoviews/plotting/bokeh/path.py | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/holoviews/plotting/bokeh/path.py b/holoviews/plotting/bokeh/path.py index eedc5566fa..e4cc8118cf 100644 --- a/holoviews/plotting/bokeh/path.py +++ b/holoviews/plotting/bokeh/path.py @@ -7,7 +7,7 @@ from ...core import util from ..util import map_colors -from .element import ElementPlot, line_properties, fill_properties +from .element import ElementPlot, ColorbarPlot, line_properties, fill_properties from .util import get_cmap, rgb2hex @@ -44,7 +44,7 @@ def get_batched_data(self, element, ranges=None, empty=False): return data, elmapping -class PolygonPlot(PathPlot): +class PolygonPlot(ColorbarPlot, PathPlot): style_opts = ['color', 'cmap', 'palette'] + line_properties + fill_properties _plot_methods = dict(single='patches', batched='patches') @@ -74,15 +74,17 @@ def get_data(self, element, ranges=None, empty=False): data = dict(xs=ys, ys=xs) if self.invert_axes else dict(xs=xs, ys=ys) style = self.style[self.cyclic_index] - cmap = style.get('palette', style.get('cmap', None)) mapping = dict(self._mapping) - if cmap and element.level is not None: - cmap = get_cmap(cmap) - colors = map_colors(np.array([element.level]), ranges[element.vdims[0].name], cmap) - mapping['color'] = 'color' - data['color'] = [] if empty else list(colors)*len(element.data) - dim_name = util.dimension_sanitizer(element.vdims[0].name) + + if element.vdims and element.level is not None: + cdim = element.vdims[0] + mapper = self._get_colormapper(cdim, element, ranges, style) + data[cdim.name] = [] if empty else element.dimension_values(2) + mapping['fill_color'] = {'field': cdim.name, + 'transform': mapper} + if 'hover' in self.tools+self.default_tools: + dim_name = util.dimension_sanitizer(element.vdims[0].name) for k, v in self.overlay_dims.items(): dim = util.dimension_sanitizer(k.name) data[dim] = [v for _ in range(len(xs))] @@ -99,7 +101,7 @@ def get_batched_data(self, element, ranges=None, empty=False): eldata, elmapping = self.get_data(el, ranges, empty) for k, eld in eldata.items(): data[k].extend(eld) - if 'color' not in eldata: + if 'color' not in elmapping: zorder = self.get_zorder(element, key, el) val = style[zorder].get('color') elmapping['color'] = 'color' From ac35b2f4330a9ce5e1d5abdbd621e4010f0b069a Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Wed, 14 Sep 2016 00:37:06 +0100 Subject: [PATCH 05/21] Added colorbars for RasterPlot --- holoviews/plotting/bokeh/raster.py | 27 ++++++--------------------- 1 file changed, 6 insertions(+), 21 deletions(-) diff --git a/holoviews/plotting/bokeh/raster.py b/holoviews/plotting/bokeh/raster.py index 3d1819093a..0ddde4f4d2 100644 --- a/holoviews/plotting/bokeh/raster.py +++ b/holoviews/plotting/bokeh/raster.py @@ -1,24 +1,15 @@ import numpy as np import param -from bokeh.models.mappers import LinearColorMapper -try: - from bokeh.models.mappers import LogColorMapper -except ImportError: - LogColorMapper = None - from ...core.util import cartesian_product from ...element import Image, Raster, RGB from ..renderer import SkipRendering from ..util import map_colors -from .element import ElementPlot, line_properties, fill_properties -from .util import mplcmap_to_palette, get_cmap, hsv_to_rgb - +from .element import ElementPlot, ColorbarPlot, line_properties, fill_properties +from .util import mplcmap_to_palette, get_cmap, hsv_to_rgb, mpl_to_bokeh -class RasterPlot(ElementPlot): - logz = param.Boolean(default=False, doc=""" - Whether to apply log scaling to the z-axis.""") +class RasterPlot(ColorbarPlot): show_legend = param.Boolean(default=False, doc=""" Whether to show legend for the plot.""") @@ -56,15 +47,9 @@ def _glyph_properties(self, plot, element, source, ranges): properties = super(RasterPlot, self)._glyph_properties(plot, element, source, ranges) properties = {k: v for k, v in properties.items()} - val_dim = [d.name for d in element.vdims][0] - low, high = ranges.get(val_dim) - if 'cmap' in properties: - palette = mplcmap_to_palette(properties.pop('cmap', None)) - colormapper = LogColorMapper if self.logz else LinearColorMapper - cmap = colormapper(palette, low=low, high=high) - properties['color_mapper'] = cmap - if 'color_mapper' not in self.handles: - self.handles['color_mapper'] = cmap + val_dim = [d for d in element.vdims][0] + properties['color_mapper'] = self._get_colormapper(val_dim, element, ranges, + properties) return properties From da511ff9fa57ac640ce1861cd8324d9c49bbbfed Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Wed, 14 Sep 2016 00:37:33 +0100 Subject: [PATCH 06/21] Added client-side colormapping for QuadMeshPlot --- holoviews/plotting/bokeh/raster.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/holoviews/plotting/bokeh/raster.py b/holoviews/plotting/bokeh/raster.py index 0ddde4f4d2..8111a5ba4c 100644 --- a/holoviews/plotting/bokeh/raster.py +++ b/holoviews/plotting/bokeh/raster.py @@ -151,7 +151,7 @@ def get_data(self, element, ranges=None, empty=False): return (data, {'x': x, 'y': y, 'fill_color': 'color', 'height': 1, 'width': 1}) -class QuadMeshPlot(ElementPlot): +class QuadMeshPlot(ColorbarPlot): show_legend = param.Boolean(default=False, doc=""" Whether to show legend for the plot.""") @@ -161,24 +161,23 @@ class QuadMeshPlot(ElementPlot): def get_data(self, element, ranges=None, empty=False): x, y, z = element.dimensions(label=True) + style = self.style[self.cyclic_index] + cmapper = self._get_colormapper(element.vdims[0], element, ranges, style) if empty: - data = {x: [], y: [], z: [], 'color': [], 'height': [], 'width': []} + data = {x: [], y: [], z: [], 'height': [], 'width': []} else: - style = self.style[self.cyclic_index] - cmap = style.get('palette', style.get('cmap', None)) - cmap = get_cmap(cmap) if len(set(v.shape for v in element.data)) == 1: raise SkipRendering("Bokeh QuadMeshPlot only supports rectangular meshes") zvals = element.data[2].T.flatten() - colors = map_colors(zvals, ranges[z], cmap) xvals = element.dimension_values(0, False) yvals = element.dimension_values(1, False) widths = np.diff(element.data[0]) heights = np.diff(element.data[1]) xs, ys = cartesian_product([xvals, yvals]) ws, hs = cartesian_product([widths, heights]) - data = {x: xs.flat, y: ys.flat, z: zvals, 'color': colors, + data = {x: xs.flat, y: ys.flat, z: zvals, 'widths': ws.flat, 'heights': hs.flat} - return (data, {'x': x, 'y': y, 'fill_color': 'color', + return (data, {'x': x, 'y': y, + 'fill_color': {'field': z, 'transform': cmapper}, 'height': 'heights', 'width': 'widths'}) From 59b94736bcbc085b7839fa7ffd96e22aeae999b4 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Wed, 14 Sep 2016 00:38:24 +0100 Subject: [PATCH 07/21] Added client side colormapping for SpikesPlot --- holoviews/plotting/bokeh/chart.py | 22 +++++++--------------- 1 file changed, 7 insertions(+), 15 deletions(-) diff --git a/holoviews/plotting/bokeh/chart.py b/holoviews/plotting/bokeh/chart.py index 36472c817c..a7d48ee571 100644 --- a/holoviews/plotting/bokeh/chart.py +++ b/holoviews/plotting/bokeh/chart.py @@ -308,7 +308,7 @@ def get_data(self, element, ranges=None, empty=False): return (data, dict(self._mapping)) -class SpikesPlot(PathPlot): +class SpikesPlot(PathPlot, ColorbarPlot): color_index = param.ClassSelector(default=1, class_=(basestring, int), doc=""" Index of the dimension from which the color will the drawn""") @@ -346,22 +346,14 @@ def get_data(self, element, ranges=None, empty=False): xs, ys = zip(*(((x[0], x[0]), (pos+height, pos)) for x in element.array(dims[:1]))) - if not empty and self.invert_axes: keys = keys[::-1] + if not empty and self.invert_axes: xs, ys = ys, xs data = dict(zip(('xs', 'ys'), (xs, ys))) - - cmap = style.get('palette', style.get('cmap', None)) cdim = element.get_dimension(self.color_index) - if cdim and cmap: - map_key = 'color_' + cdim.name - mapping['color'] = map_key - if empty: - colors = [] - else: - cmap = get_cmap(cmap) - cvals = element.dimension_values(cdim) - crange = ranges.get(cdim.name, None) - colors = map_colors(cvals, crange, cmap) - data[map_key] = colors + if cdim: + mapper = self._get_colormapper(cdim, element, ranges, style) + data[cdim.name] = [] if empty else element.dimension_values(cdim) + mapping['color'] = {'field': cdim.name, + 'transform': mapper} if 'hover' in self.tools+self.default_tools and not empty: for d in dims: From 84a0dc44458078c42a9b79224c9e0fe6cb3928be Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Wed, 14 Sep 2016 00:38:51 +0100 Subject: [PATCH 08/21] Moved toolbar on top to avoid clashes with colorbar --- holoviews/plotting/bokeh/element.py | 1 + 1 file changed, 1 insertion(+) diff --git a/holoviews/plotting/bokeh/element.py b/holoviews/plotting/bokeh/element.py index 4e65648ca9..6239e68a72 100644 --- a/holoviews/plotting/bokeh/element.py +++ b/holoviews/plotting/bokeh/element.py @@ -289,6 +289,7 @@ def _init_plot(self, key, element, plots, ranges=None): properties['webgl'] = Store.renderers[self.renderer.backend].webgl return bokeh.plotting.Figure(x_axis_type=x_axis_type, + toolbar_location='above', y_axis_type=y_axis_type, title=title, tools=tools, **properties) From dee556fb4f8a0d732bf05e97267f6cc43db6dea6 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Wed, 14 Sep 2016 17:37:27 +0100 Subject: [PATCH 09/21] Added client side colormapping for HeatmapPlot --- holoviews/plotting/bokeh/raster.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/holoviews/plotting/bokeh/raster.py b/holoviews/plotting/bokeh/raster.py index 8111a5ba4c..961c244460 100644 --- a/holoviews/plotting/bokeh/raster.py +++ b/holoviews/plotting/bokeh/raster.py @@ -116,7 +116,7 @@ def get_data(self, element, ranges=None, empty=False): return super(HSVPlot, self).get_data(rgb, ranges, empty) -class HeatmapPlot(ElementPlot): +class HeatmapPlot(ColorbarPlot): show_legend = param.Boolean(default=False, doc=""" Whether to show legend for the plot.""") @@ -129,6 +129,7 @@ def _axes_props(self, plots, subplots, element, ranges): labels = self._get_axis_labels(dims) xvals, yvals = [element.dimension_values(i, False) for i in range(2)] + if self.invert_yaxis: yvals = yvals[::-1] plot_ranges = {'x_range': [str(x) for x in xvals], 'y_range': [str(y) for y in yvals]} return ('auto', 'auto'), labels, plot_ranges @@ -136,19 +137,18 @@ def _axes_props(self, plots, subplots, element, ranges): def get_data(self, element, ranges=None, empty=False): x, y, z = element.dimensions(label=True) + style = self.style[self.cyclic_index] + cmapper = self._get_colormapper(element.vdims[0], element, ranges, style) if empty: data = {x: [], y: [], z: [], 'color': []} else: - style = self.style[self.cyclic_index] - cmap = style.get('palette', style.get('cmap', None)) - cmap = get_cmap(cmap) zvals = np.rot90(element.raster, 3).flatten() - colors = map_colors(zvals, ranges[z], cmap) xvals, yvals = [[str(v) for v in element.dimension_values(i)] for i in range(2)] - data = {x: xvals, y: yvals, z: zvals, 'color': colors} + data = {x: xvals, y: yvals, z: zvals} - return (data, {'x': x, 'y': y, 'fill_color': 'color', 'height': 1, 'width': 1}) + return (data, {'x': x, 'y': y, 'fill_color': {'field': z, 'transform': cmapper}, + 'height': 1, 'width': 1}) class QuadMeshPlot(ColorbarPlot): From f2884e1f103c609370954d437ac6c3f90c36d6e7 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Wed, 14 Sep 2016 18:02:34 +0100 Subject: [PATCH 10/21] Handled updating of colormapping ranges --- holoviews/plotting/bokeh/element.py | 13 +++++++++++++ holoviews/plotting/bokeh/raster.py | 10 ---------- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/holoviews/plotting/bokeh/element.py b/holoviews/plotting/bokeh/element.py index 6239e68a72..cf8cd116ff 100644 --- a/holoviews/plotting/bokeh/element.py +++ b/holoviews/plotting/bokeh/element.py @@ -710,6 +710,19 @@ def _init_glyph(self, plot, mapping, properties): return ret + def _update_glyph(self, glyph, properties, mapping): + allowed_properties = glyph.properties() + cmappers = [v.get('transform') for v in mapping.values() + if isinstance(v, dict)] + cmappers.append(properties.pop('color_mapper', None)) + for cm in cmappers: + if cm: + self.handles['color_mapper'].low = cm.low + self.handles['color_mapper'].high = cm.high + merged = dict(properties, **mapping) + glyph.set(**{k: v for k, v in merged.items() + if k in allowed_properties}) + class LegendPlot(ElementPlot): diff --git a/holoviews/plotting/bokeh/raster.py b/holoviews/plotting/bokeh/raster.py index 961c244460..f70e952378 100644 --- a/holoviews/plotting/bokeh/raster.py +++ b/holoviews/plotting/bokeh/raster.py @@ -53,16 +53,6 @@ def _glyph_properties(self, plot, element, source, ranges): return properties - def _update_glyph(self, glyph, properties, mapping): - allowed_properties = glyph.properties() - cmap = properties.pop('color_mapper', None) - if cmap: - glyph.color_mapper.low = cmap.low - glyph.color_mapper.high = cmap.high - merged = dict(properties, **mapping) - glyph.set(**{k: v for k, v in merged.items() - if k in allowed_properties}) - class ImagePlot(RasterPlot): From 8f00b04cdce69c37660dbca25c680a4993c14670 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Wed, 14 Sep 2016 18:09:42 +0100 Subject: [PATCH 11/21] Added colorbar border by default --- holoviews/plotting/bokeh/element.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/holoviews/plotting/bokeh/element.py b/holoviews/plotting/bokeh/element.py index cf8cd116ff..e1d2c4ba0c 100644 --- a/holoviews/plotting/bokeh/element.py +++ b/holoviews/plotting/bokeh/element.py @@ -675,7 +675,9 @@ def _draw_colorbar(self, plot, color_mapper): ticker = LogTicker() else: ticker = BasicTicker() - cbar_opts = self.colorbar_specs[self.colorbar_position] + cbar_opts = dict(self.colorbar_specs[self.colorbar_position], + bar_line_color='black', label_standoff=8, + major_tick_line_color='black') # Check if there is a colorbar in the same position pos = cbar_opts['pos'] From af0b2dd0af6174d6b47ed6ab2e70e7c2587534e1 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Wed, 14 Sep 2016 18:52:32 +0100 Subject: [PATCH 12/21] Made toolbar position customizable Allows avoiding overlap with legends, colorbars and titles --- holoviews/plotting/bokeh/element.py | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/holoviews/plotting/bokeh/element.py b/holoviews/plotting/bokeh/element.py index e1d2c4ba0c..e924b1dadf 100644 --- a/holoviews/plotting/bokeh/element.py +++ b/holoviews/plotting/bokeh/element.py @@ -113,6 +113,13 @@ class ElementPlot(BokehPlot, GenericElementPlot): tools = param.List(default=[], doc=""" A list of plugin tools to use on the plot.""") + toolbar = param.ObjectSelector(default='right', + objects=["above", "below", + "left", "right", None], + doc=""" + The toolbar location, must be one of 'above', 'below', + 'left', 'right', None.""") + xaxis = param.ObjectSelector(default='bottom', objects=['top', 'bottom', 'bare', 'top-bare', 'bottom-bare', None], doc=""" @@ -277,7 +284,6 @@ def _init_plot(self, key, element, plots, ranges=None): axis_types, labels, plot_ranges = self._axes_props(plots, subplots, element, ranges) xlabel, ylabel, _ = labels x_axis_type, y_axis_type = axis_types - tools = self._init_tools(element) properties = dict(plot_ranges) properties['x_axis_label'] = xlabel if 'x' in self.show_labels else ' ' properties['y_axis_label'] = ylabel if 'y' in self.show_labels else ' ' @@ -287,11 +293,15 @@ def _init_plot(self, key, element, plots, ranges=None): else: title = '' + if self.toolbar: + tools = self._init_tools(element) + properties['tools'] = tools + properties['toolbar_location'] = self.toolbar + properties['webgl'] = Store.renderers[self.renderer.backend].webgl return bokeh.plotting.Figure(x_axis_type=x_axis_type, - toolbar_location='above', y_axis_type=y_axis_type, title=title, - tools=tools, **properties) + **properties) def _plot_properties(self, key, plot, element): From 122a3bab22d7161ffb84c9fd6ac95e208f621533 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Wed, 14 Sep 2016 19:04:56 +0100 Subject: [PATCH 13/21] Cleaned up ColorbarPlot and added docstring --- holoviews/plotting/bokeh/element.py | 53 ++++++++++++++++------------- 1 file changed, 30 insertions(+), 23 deletions(-) diff --git a/holoviews/plotting/bokeh/element.py b/holoviews/plotting/bokeh/element.py index e924b1dadf..53451291be 100644 --- a/holoviews/plotting/bokeh/element.py +++ b/holoviews/plotting/bokeh/element.py @@ -640,18 +640,40 @@ def framewise(self): class ColorbarPlot(ElementPlot): + """ + ColorbarPlot provides methods to create colormappers and colorbar + models which can be added to a glyph. Additionally it provides + parameters to control the position and other styling options of + the colorbar. The default colorbar_position options are defined + by the colorbar_specs, but may be overridden by the colorbar_opts. + """ + + colorbar_specs = {'right': {'pos': 'right', + 'opts': {'location': (0, 0)}}, + 'left': {'pos': 'left', + 'opts':{'location':(0, 0)}}, + 'bottom': {'pos': 'below', + 'opts': {'location': (0, 0), + 'orientation':'horizontal'}}, + 'top': {'pos': 'above', + 'opts': {'location':(0, 0), + 'orientation':'horizontal'}}, + 'top_right': {'pos': 'center', + 'opts': {'location': 'top_right'}}, + 'top_left': {'pos': 'center', + 'opts': {'location': 'top_left'}}, + 'bottom_left': {'pos': 'center', + 'opts': {'location': 'bottom_left', + 'orientation': 'horizontal'}}, + 'bottom_right': {'pos': 'center', + 'opts': {'location': 'bottom_right', + 'orientation': 'horizontal'}}} colorbar = param.Boolean(default=False, doc=""" Whether to display a colorbar.""") - colorbar_position = param.ObjectSelector(objects=["top_right", - "top_left", - "bottom_left", - "bottom_right", - 'right', 'left', - 'top', 'bottom'], - default="right", - doc=""" + colorbar_position = param.ObjectSelector(objects=list(colorbar_specs), + default="right", doc=""" Allows selecting between a number of predefined colorbar position options. The predefined options may be customized in the colorbar_specs class attribute.""") @@ -662,21 +684,6 @@ class ColorbarPlot(ElementPlot): location, orientation, height, width, scale_alpha, title, title_props, margin, padding, background_fill_color and more.""") - colorbar_specs = {'right': {'pos': 'right', 'opts': {'location': (0, 0)}}, - 'left': {'pos': 'left', 'opts':{'location':(0, 0)}}, - 'top_right': {'pos': 'center', 'opts': {'location': 'top_right'}}, - 'top_left': {'pos': 'center', 'opts': {'location': 'top_left'}}, - 'top': {'opts': {'location':(0, 0), 'orientation':'horizontal'}, - 'pos': 'above'}, - 'bottom': {'opts': {'location': (0, 0), 'orientation':'horizontal'}, - 'pos': 'below'}, - 'bottom_left': {'pos': 'center', - 'opts': {'location': 'bottom_center', - 'orientation': 'horizontal'}}, - 'bottom_right': {'pos': 'center', - 'opts': {'location': 'bottom_right', - 'orientation': 'horizontal'}}} - logz = param.Boolean(default=False, doc=""" Whether to apply log scaling to the z-axis.""") From b219c23b4f456b9d05d756f69801c4f1a7e6795e Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Wed, 14 Sep 2016 22:04:36 +0100 Subject: [PATCH 14/21] Defined default colormap --- holoviews/plotting/bokeh/element.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/holoviews/plotting/bokeh/element.py b/holoviews/plotting/bokeh/element.py index 53451291be..9f8f9b69ee 100644 --- a/holoviews/plotting/bokeh/element.py +++ b/holoviews/plotting/bokeh/element.py @@ -710,8 +710,7 @@ def _draw_colorbar(self, plot, color_mapper): def _get_colormapper(self, dim, element, ranges, style): low, high = ranges.get(dim.name) - if 'cmap' in style: - palette = mplcmap_to_palette(style.pop('cmap', None)) + palette = mplcmap_to_palette(style.pop('cmap', 'viridis')) colormapper = LogColorMapper if self.logz else LinearColorMapper cmapper = colormapper(palette, low=low, high=high) if 'color_mapper' not in self.handles: From 8a83577b48f9ad442d03e69e29ae6bc2aaa1b357 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Wed, 14 Sep 2016 22:05:55 +0100 Subject: [PATCH 15/21] Added colormapping unit tests --- tests/testplotinstantiation.py | 49 ++++++++++++++++++++++++++++++++-- 1 file changed, 47 insertions(+), 2 deletions(-) diff --git a/tests/testplotinstantiation.py b/tests/testplotinstantiation.py index 339a45eb37..f8c6399cb9 100644 --- a/tests/testplotinstantiation.py +++ b/tests/testplotinstantiation.py @@ -6,8 +6,9 @@ from io import BytesIO import numpy as np -from holoviews import (Dimension, Curve, Scatter, Overlay, DynamicMap, - Store, Image, VLine, NdOverlay, Points) +from holoviews import (Dimension, Overlay, DynamicMap, Store, NdOverlay) +from holoviews.element import (Curve, Scatter, Image, VLine, Points, + HeatMap, QuadMesh, Spikes) from holoviews.element.comparison import ComparisonTestCase from holoviews.streams import PositionXY @@ -24,6 +25,7 @@ try: import holoviews.plotting.bokeh bokeh_renderer = Store.renderers['bokeh'] + from bokeh.models.mappers import LinearColorMapper, LogColorMapper except: bokeh_renderer = None @@ -31,6 +33,8 @@ class TestMPLPlotInstantiation(ComparisonTestCase): def setUp(self): + self.previous_backend = Store.current_backend + Store.current_backend = 'matplotlib' if mpl_renderer is None: raise SkipTest("Matplotlib required to test plot instantiation") self.default_comm, _ = mpl_renderer.comms['default'] @@ -38,6 +42,7 @@ def setUp(self): def teardown(self): mpl_renderer.comms['default'] = (self.default_comm, '') + Store.current_backend = self.previous_backend def test_interleaved_overlay(self): """ @@ -72,11 +77,51 @@ def test_dynamic_streams_refresh(self): class TestBokehPlotInstantiation(ComparisonTestCase): def setUp(self): + self.previous_backend = Store.current_backend + Store.current_backend = 'bokeh' + if not bokeh_renderer: raise SkipTest("Bokeh required to test plot instantiation") + def teardown(self): + Store.current_backend = self.previous_backend + def test_batched_plot(self): overlay = NdOverlay({i: Points(np.arange(i)) for i in range(1, 100)}) plot = bokeh_renderer.get_plot(overlay) extents = plot.get_extents(overlay, {}) self.assertEqual(extents, (0, 0, 98, 98)) + + def _test_colormapping(self, element, dim, log=False): + plot = bokeh_renderer.get_plot(element) + plot.initialize_plot() + fig = plot.state + cmapper = plot.handles['color_mapper'] + low, high = element.range(dim) + self.assertEqual(cmapper.low, low) + self.assertEqual(cmapper.high, high) + mapper_type = LogColorMapper if log else LinearColorMapper + self.assertTrue(isinstance(cmapper, mapper_type)) + + def test_points_colormapping(self): + points = Points(np.random.rand(10, 4), vdims=['a', 'b']) + self._test_colormapping(points, 3) + + def test_image_colormapping(self): + img = Image(np.random.rand(10, 10))(plot=dict(logz=True)) + self._test_colormapping(img, 2, True) + + def test_heatmap_colormapping(self): + hm = HeatMap([(1,1,1), (2,2,0)]) + self._test_colormapping(hm, 2) + + def test_quadmesh_colormapping(self): + n = 21 + xs = np.logspace(1, 3, n) + ys = np.linspace(1, 10, n) + qmesh = QuadMesh((xs, ys, np.random.rand(n-1, n-1))) + self._test_colormapping(qmesh, 2) + + def test_spikes_colormapping(self): + spikes = Spikes(np.random.rand(20, 2), vdims=['Intensity']) + self._test_colormapping(spikes, 1) From 65c0d21cfa948add89e0df5689ace8211998211a Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Wed, 14 Sep 2016 22:20:46 +0100 Subject: [PATCH 16/21] Added colormapper to update handles of ColorbarPlot --- holoviews/plotting/bokeh/element.py | 2 ++ holoviews/plotting/bokeh/raster.py | 1 - 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/holoviews/plotting/bokeh/element.py b/holoviews/plotting/bokeh/element.py index 9f8f9b69ee..fa317814be 100644 --- a/holoviews/plotting/bokeh/element.py +++ b/holoviews/plotting/bokeh/element.py @@ -687,6 +687,8 @@ class ColorbarPlot(ElementPlot): logz = param.Boolean(default=False, doc=""" Whether to apply log scaling to the z-axis.""") + _update_handles = ['color_mapper', 'source', 'glyph'] + def _draw_colorbar(self, plot, color_mapper): if LogColorMapper and isinstance(color_mapper, LogColorMapper): ticker = LogTicker() diff --git a/holoviews/plotting/bokeh/raster.py b/holoviews/plotting/bokeh/raster.py index f70e952378..270cde329b 100644 --- a/holoviews/plotting/bokeh/raster.py +++ b/holoviews/plotting/bokeh/raster.py @@ -16,7 +16,6 @@ class RasterPlot(ColorbarPlot): style_opts = ['cmap'] _plot_methods = dict(single='image') - _update_handles = ['color_mapper', 'source', 'glyph'] def __init__(self, *args, **kwargs): super(RasterPlot, self).__init__(*args, **kwargs) From c211f2a8c222c726584931bc0bc971538a5b4271 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Wed, 14 Sep 2016 22:42:31 +0100 Subject: [PATCH 17/21] Fixed setting of colorbar border --- holoviews/plotting/bokeh/element.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/holoviews/plotting/bokeh/element.py b/holoviews/plotting/bokeh/element.py index fa317814be..6fa80b516f 100644 --- a/holoviews/plotting/bokeh/element.py +++ b/holoviews/plotting/bokeh/element.py @@ -694,17 +694,17 @@ def _draw_colorbar(self, plot, color_mapper): ticker = LogTicker() else: ticker = BasicTicker() - cbar_opts = dict(self.colorbar_specs[self.colorbar_position], - bar_line_color='black', label_standoff=8, - major_tick_line_color='black') + cbar_opts = dict(self.colorbar_specs[self.colorbar_position]) # Check if there is a colorbar in the same position pos = cbar_opts['pos'] if any(isinstance(model, ColorBar) for model in getattr(plot, pos, [])): return + opts = dict(cbar_opts['opts'], bar_line_color='black', + label_standoff=8, major_tick_line_color='black') color_bar = ColorBar(color_mapper=color_mapper, ticker=ticker, - **dict(cbar_opts['opts'], **self.colorbar_opts)) + **dict(opts, **self.colorbar_opts)) plot.add_layout(color_bar, pos) self.handles['colorbar'] = color_bar From f520bff3ca36df40b112391555cc832306883894 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Wed, 14 Sep 2016 23:31:05 +0100 Subject: [PATCH 18/21] Allowed updating the colormapper palette --- holoviews/plotting/bokeh/element.py | 1 + 1 file changed, 1 insertion(+) diff --git a/holoviews/plotting/bokeh/element.py b/holoviews/plotting/bokeh/element.py index 6fa80b516f..523fb9bfef 100644 --- a/holoviews/plotting/bokeh/element.py +++ b/holoviews/plotting/bokeh/element.py @@ -739,6 +739,7 @@ def _update_glyph(self, glyph, properties, mapping): if cm: self.handles['color_mapper'].low = cm.low self.handles['color_mapper'].high = cm.high + self.handles['color_mapper'].palette = cm.palette merged = dict(properties, **mapping) glyph.set(**{k: v for k, v in merged.items() if k in allowed_properties}) From c35d6b26c12b334828b77e083285da177264fcf4 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Wed, 14 Sep 2016 23:34:13 +0100 Subject: [PATCH 19/21] Made colorbar defaults a class attribute --- holoviews/plotting/bokeh/element.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/holoviews/plotting/bokeh/element.py b/holoviews/plotting/bokeh/element.py index 523fb9bfef..d567b93e52 100644 --- a/holoviews/plotting/bokeh/element.py +++ b/holoviews/plotting/bokeh/element.py @@ -689,6 +689,9 @@ class ColorbarPlot(ElementPlot): _update_handles = ['color_mapper', 'source', 'glyph'] + _colorbar_defaults = dict(bar_line_color='black', label_standoff=8, + major_tick_line_color='black') + def _draw_colorbar(self, plot, color_mapper): if LogColorMapper and isinstance(color_mapper, LogColorMapper): ticker = LogTicker() @@ -701,8 +704,7 @@ def _draw_colorbar(self, plot, color_mapper): if any(isinstance(model, ColorBar) for model in getattr(plot, pos, [])): return - opts = dict(cbar_opts['opts'], bar_line_color='black', - label_standoff=8, major_tick_line_color='black') + opts = dict(cbar_opts['opts'], self._colorbar_defaults) color_bar = ColorBar(color_mapper=color_mapper, ticker=ticker, **dict(opts, **self.colorbar_opts)) From 80899e61a533c0f91f9a4e612354c2c2bc5d7c06 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Wed, 14 Sep 2016 23:35:55 +0100 Subject: [PATCH 20/21] Renamed mapper to cmapper --- holoviews/plotting/bokeh/chart.py | 8 ++++---- holoviews/plotting/bokeh/path.py | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/holoviews/plotting/bokeh/chart.py b/holoviews/plotting/bokeh/chart.py index a7d48ee571..5446e3d8c3 100644 --- a/holoviews/plotting/bokeh/chart.py +++ b/holoviews/plotting/bokeh/chart.py @@ -265,10 +265,10 @@ def get_data(self, element, ranges=None, empty=None): if 'cmap' in style or 'palette' in style: main_range = {dim.name: main_range} - mapper = self._get_colormapper(dim, element, main_range, style) + cmapper = self._get_colormapper(dim, element, main_range, style) data[dim.name] = [] if empty else element.dimension_values(dim) mapping['fill_color'] = {'field': dim.name, - 'transform': mapper} + 'transform': cmapper} self._get_hover_data(data, element, empty) return (data, mapping) @@ -350,10 +350,10 @@ def get_data(self, element, ranges=None, empty=False): data = dict(zip(('xs', 'ys'), (xs, ys))) cdim = element.get_dimension(self.color_index) if cdim: - mapper = self._get_colormapper(cdim, element, ranges, style) + cmapper = self._get_colormapper(cdim, element, ranges, style) data[cdim.name] = [] if empty else element.dimension_values(cdim) mapping['color'] = {'field': cdim.name, - 'transform': mapper} + 'transform': cmapper} if 'hover' in self.tools+self.default_tools and not empty: for d in dims: diff --git a/holoviews/plotting/bokeh/path.py b/holoviews/plotting/bokeh/path.py index e4cc8118cf..09930009ce 100644 --- a/holoviews/plotting/bokeh/path.py +++ b/holoviews/plotting/bokeh/path.py @@ -78,10 +78,10 @@ def get_data(self, element, ranges=None, empty=False): if element.vdims and element.level is not None: cdim = element.vdims[0] - mapper = self._get_colormapper(cdim, element, ranges, style) + cmapper = self._get_colormapper(cdim, element, ranges, style) data[cdim.name] = [] if empty else element.dimension_values(2) mapping['fill_color'] = {'field': cdim.name, - 'transform': mapper} + 'transform': cmapper} if 'hover' in self.tools+self.default_tools: dim_name = util.dimension_sanitizer(element.vdims[0].name) From 9e2f1b631ba95d64b75303b4a4abd30c253d9ec2 Mon Sep 17 00:00:00 2001 From: Philipp Rudiger Date: Wed, 14 Sep 2016 23:37:58 +0100 Subject: [PATCH 21/21] Added comment about colormapper instances --- holoviews/plotting/bokeh/element.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/holoviews/plotting/bokeh/element.py b/holoviews/plotting/bokeh/element.py index d567b93e52..d2eb5bc1d9 100644 --- a/holoviews/plotting/bokeh/element.py +++ b/holoviews/plotting/bokeh/element.py @@ -717,6 +717,9 @@ def _get_colormapper(self, dim, element, ranges, style): palette = mplcmap_to_palette(style.pop('cmap', 'viridis')) colormapper = LogColorMapper if self.logz else LinearColorMapper cmapper = colormapper(palette, low=low, high=high) + + # The initial colormapper instance is cached the first time + # and then updated with the values from new instances if 'color_mapper' not in self.handles: self.handles['color_mapper'] = cmapper return cmapper