Source code for holoviews.plotting.bokeh.chart

from __future__ import absolute_import, division, unicode_literals

from collections import defaultdict

import numpy as np
import param

from bokeh.models import (
    CategoricalColorMapper, CustomJS, FactorRange, Range1d, Whisker
)
from bokeh.models.tools import BoxSelectTool
from bokeh.transform import jitter

from ...core.data import Dataset
from ...core.dimension import dimension_name
from ...core.util import (
    OrderedDict, basestring, dimension_sanitizer, isfinite
)
from ...operation import interpolate_curve
from ...util.transform import dim
from ..mixins import AreaMixin, BarsMixin, SpikesMixin
from ..util import compute_sizes, get_min_distance
from .element import ElementPlot, ColorbarPlot, LegendPlot, OverlayPlot
from .selection import BokehOverlaySelectionDisplay
from .styles import (
    expand_batched_style, base_properties, line_properties, fill_properties,
    mpl_to_bokeh, rgb2hex
)
from .util import bokeh_version, categorize_array


[docs]class PointPlot(LegendPlot, ColorbarPlot): jitter = param.Number(default=None, bounds=(0, None), doc=""" The amount of jitter to apply to offset the points along the x-axis.""") selected = param.List(default=None, doc=""" The current selection as a list of integers corresponding to the selected items.""") # Deprecated parameters color_index = param.ClassSelector(default=None, class_=(basestring, int), allow_None=True, doc=""" Deprecated in favor of color style mapping, e.g. `color=dim('color')`""") size_index = param.ClassSelector(default=None, class_=(basestring, int), allow_None=True, doc=""" Deprecated in favor of size style mapping, e.g. `size=dim('size')`""") scaling_method = param.ObjectSelector(default="area", objects=["width", "area"], doc=""" Deprecated in favor of size style mapping, e.g. size=dim('size')**2.""") scaling_factor = param.Number(default=1, bounds=(0, None), doc=""" Scaling factor which is applied to either the width or area of each point, depending on the value of `scaling_method`.""") size_fn = param.Callable(default=np.abs, doc=""" Function applied to size values before applying scaling, to remove values lower than zero.""") selection_display = BokehOverlaySelectionDisplay() style_opts = (['cmap', 'palette', 'marker', 'size', 'angle', 'visible'] + line_properties + fill_properties) _plot_methods = dict(single='scatter', batched='scatter') _batched_style_opts = line_properties + fill_properties + ['size', 'marker', 'angle'] def _get_size_data(self, element, ranges, style): data, mapping = {}, {} sdim = element.get_dimension(self.size_index) ms = style.get('size', np.sqrt(6)) if sdim and ((isinstance(ms, basestring) and ms in element) or isinstance(ms, dim)): self.param.warning( "Cannot declare style mapping for 'size' option and " "declare a size_index; ignoring the size_index.") sdim = None if not sdim or self.static_source: return data, mapping map_key = 'size_' + sdim.name ms = ms**2 sizes = element.dimension_values(self.size_index) sizes = compute_sizes(sizes, self.size_fn, self.scaling_factor, self.scaling_method, ms) if sizes is None: eltype = type(element).__name__ self.param.warning( '%s dimension is not numeric, cannot use to scale %s size.' % (sdim.pprint_label, eltype)) else: data[map_key] = np.sqrt(sizes) mapping['size'] = map_key return data, mapping
[docs] def get_data(self, element, ranges, style): dims = element.dimensions(label=True) xidx, yidx = (1, 0) if self.invert_axes else (0, 1) mapping = dict(x=dims[xidx], y=dims[yidx]) data = {} if not self.static_source or self.batched: xdim, ydim = dims[:2] data[xdim] = element.dimension_values(xdim) data[ydim] = element.dimension_values(ydim) self._categorize_data(data, dims[:2], element.dimensions()) cdata, cmapping = self._get_color_data(element, ranges, style) data.update(cdata) mapping.update(cmapping) sdata, smapping = self._get_size_data(element, ranges, style) data.update(sdata) mapping.update(smapping) if 'angle' in style and isinstance(style['angle'], (int, float)): style['angle'] = np.deg2rad(style['angle']) if self.jitter: if self.invert_axes: mapping['y'] = jitter(dims[yidx], self.jitter, range=self.handles['y_range']) else: mapping['x'] = jitter(dims[xidx], self.jitter, range=self.handles['x_range']) self._get_hover_data(data, element) return data, mapping, style
def get_batched_data(self, element, ranges): data = defaultdict(list) zorders = self._updated_zorders(element) # Angles need special handling since they are tied to the # marker in certain cases has_angles = False for (key, el), zorder in zip(element.data.items(), zorders): el_opts = self.lookup_options(el, 'plot').options self.param.set_param(**{k: v for k, v in el_opts.items() if k not in OverlayPlot._propagate_options}) style = self.lookup_options(element.last, 'style') style = style.max_cycles(len(self.ordering))[zorder] eldata, elmapping, style = self.get_data(el, ranges, style) style = mpl_to_bokeh(style) for k, eld in eldata.items(): data[k].append(eld) # Skip if data is empty if not eldata: continue # Apply static styles nvals = len(list(eldata.values())[0]) sdata, smapping = expand_batched_style(style, self._batched_style_opts, elmapping, nvals) if 'angle' in sdata and '__angle' not in data and 'marker' in data: data['__angle'] = [np.zeros(len(d)) for d in data['marker']] has_angles = True elmapping.update(smapping) for k, v in sorted(sdata.items()): if k == 'angle': k = '__angle' has_angles = True data[k].append(v) if has_angles and 'angle' not in sdata: data['__angle'].append(np.zeros(len(v))) if 'hover' in self.handles: for d, k in zip(element.dimensions(), key): sanitized = dimension_sanitizer(d.name) data[sanitized].append([k]*nvals) data = {k: np.concatenate(v) for k, v in data.items()} if '__angle' in data: elmapping['angle'] = {'field': '__angle'} return data, elmapping, style
[docs]class VectorFieldPlot(ColorbarPlot): arrow_heads = param.Boolean(default=True, doc=""" Whether or not to draw arrow heads.""") magnitude = param.ClassSelector(class_=(basestring, dim), doc=""" Dimension or dimension value transform that declares the magnitude of each vector. Magnitude is expected to be scaled between 0-1, by default the magnitudes are rescaled relative to the minimum distance between vectors, this can be disabled with the rescale_lengths option.""") padding = param.ClassSelector(default=0.05, class_=(int, float, tuple)) pivot = param.ObjectSelector(default='mid', objects=['mid', 'tip', 'tail'], doc=""" The point around which the arrows should pivot valid options include 'mid', 'tip' and 'tail'.""") rescale_lengths = param.Boolean(default=True, doc=""" Whether the lengths will be rescaled to take into account the smallest non-zero distance between two vectors.""") # Deprecated parameters color_index = param.ClassSelector(default=None, class_=(basestring, int), allow_None=True, doc=""" Deprecated in favor of dimension value transform on color option, e.g. `color=dim('Magnitude')`. """) size_index = param.ClassSelector(default=None, class_=(basestring, int), allow_None=True, doc=""" Deprecated in favor of the magnitude option, e.g. `magnitude=dim('Magnitude')`. """) normalize_lengths = param.Boolean(default=True, doc=""" Deprecated in favor of rescaling length using dimension value transforms using the magnitude option, e.g. `dim('Magnitude').norm()`.""") selection_display = BokehOverlaySelectionDisplay() style_opts = base_properties + line_properties + ['scale', 'cmap'] _nonvectorized_styles = base_properties + ['scale', 'cmap'] _plot_methods = dict(single='segment') def _get_lengths(self, element, ranges): size_dim = element.get_dimension(self.size_index) mag_dim = self.magnitude if size_dim and mag_dim: self.param.warning( "Cannot declare style mapping for 'magnitude' option " "and declare a size_index; ignoring the size_index.") elif size_dim: mag_dim = size_dim elif isinstance(mag_dim, basestring): mag_dim = element.get_dimension(mag_dim) (x0, x1), (y0, y1) = (element.range(i) for i in range(2)) if mag_dim: if isinstance(mag_dim, dim): magnitudes = mag_dim.apply(element, flat=True) else: magnitudes = element.dimension_values(mag_dim) _, max_magnitude = ranges[dimension_name(mag_dim)]['combined'] if self.normalize_lengths and max_magnitude != 0: magnitudes = magnitudes / max_magnitude if self.rescale_lengths: base_dist = get_min_distance(element) magnitudes *= base_dist else: magnitudes = np.ones(len(element)) if self.rescale_lengths: base_dist = get_min_distance(element) magnitudes *= base_dist return magnitudes def _glyph_properties(self, *args): properties = super(VectorFieldPlot, self)._glyph_properties(*args) properties.pop('scale', None) return properties
[docs] def get_data(self, element, ranges, style): input_scale = style.pop('scale', 1.0) # Get x, y, angle, magnitude and color data rads = element.dimension_values(2) if self.invert_axes: xidx, yidx = (1, 0) rads = np.pi/2 - rads else: xidx, yidx = (0, 1) lens = self._get_lengths(element, ranges)/input_scale cdim = element.get_dimension(self.color_index) cdata, cmapping = self._get_color_data(element, ranges, style, name='line_color') # Compute segments and arrowheads xs = element.dimension_values(xidx) ys = element.dimension_values(yidx) # Compute offset depending on pivot option xoffsets = np.cos(rads)*lens/2. yoffsets = np.sin(rads)*lens/2. if self.pivot == 'mid': nxoff, pxoff = xoffsets, xoffsets nyoff, pyoff = yoffsets, yoffsets elif self.pivot == 'tip': nxoff, pxoff = 0, xoffsets*2 nyoff, pyoff = 0, yoffsets*2 elif self.pivot == 'tail': nxoff, pxoff = xoffsets*2, 0 nyoff, pyoff = yoffsets*2, 0 x0s, x1s = (xs + nxoff, xs - pxoff) y0s, y1s = (ys + nyoff, ys - pyoff) color = None if self.arrow_heads: arrow_len = (lens/4.) xa1s = x0s - np.cos(rads+np.pi/4)*arrow_len ya1s = y0s - np.sin(rads+np.pi/4)*arrow_len xa2s = x0s - np.cos(rads-np.pi/4)*arrow_len ya2s = y0s - np.sin(rads-np.pi/4)*arrow_len x0s = np.tile(x0s, 3) x1s = np.concatenate([x1s, xa1s, xa2s]) y0s = np.tile(y0s, 3) y1s = np.concatenate([y1s, ya1s, ya2s]) if cdim and cdim.name in cdata: color = np.tile(cdata[cdim.name], 3) elif cdim: color = cdata.get(cdim.name) data = {'x0': x0s, 'x1': x1s, 'y0': y0s, 'y1': y1s} mapping = dict(x0='x0', x1='x1', y0='y0', y1='y1') if cdim and color is not None: data[cdim.name] = color mapping.update(cmapping) return (data, mapping, style)
[docs]class CurvePlot(ElementPlot): padding = param.ClassSelector(default=(0, 0.1), class_=(int, float, tuple)) interpolation = param.ObjectSelector(objects=['linear', 'steps-mid', 'steps-pre', 'steps-post'], default='linear', doc=""" Defines how the samples of the Curve are interpolated, default is 'linear', other options include 'steps-mid', 'steps-pre' and 'steps-post'.""") selection_display = BokehOverlaySelectionDisplay() style_opts = base_properties + line_properties _batched_style_opts = line_properties _nonvectorized_styles = base_properties + line_properties _plot_methods = dict(single='line', batched='multi_line')
[docs] def get_data(self, element, ranges, style): xidx, yidx = (1, 0) if self.invert_axes else (0, 1) x = element.get_dimension(xidx).name y = element.get_dimension(yidx).name if self.static_source and not self.batched: return {}, dict(x=x, y=y), style if 'steps' in self.interpolation: element = interpolate_curve(element, interpolation=self.interpolation) data = {x: element.dimension_values(xidx), y: element.dimension_values(yidx)} self._get_hover_data(data, element) self._categorize_data(data, (x, y), element.dimensions()) return (data, dict(x=x, y=y), style)
def _hover_opts(self, element): if self.batched: dims = list(self.hmap.last.kdims) line_policy = 'prev' else: dims = list(self.overlay_dims.keys())+element.dimensions() line_policy = 'nearest' return dims, dict(line_policy=line_policy) def get_batched_data(self, overlay, ranges): data = defaultdict(list) zorders = self._updated_zorders(overlay) for (key, el), zorder in zip(overlay.data.items(), zorders): el_opts = self.lookup_options(el, 'plot').options self.param.set_param(**{k: v for k, v in el_opts.items() if k not in OverlayPlot._propagate_options}) style = self.lookup_options(el, 'style') style = style.max_cycles(len(self.ordering))[zorder] eldata, elmapping, style = self.get_data(el, ranges, style) # Skip if data empty if not eldata: continue for k, eld in eldata.items(): data[k].append(eld) # Apply static styles sdata, smapping = expand_batched_style(style, self._batched_style_opts, elmapping, nvals=1) elmapping.update(smapping) for k, v in sdata.items(): data[k].append(v[0]) for d, k in zip(overlay.kdims, key): sanitized = dimension_sanitizer(d.name) data[sanitized].append(k) data = {opt: vals for opt, vals in data.items() if not any(v is None for v in vals)} mapping = {{'x': 'xs', 'y': 'ys'}.get(k, k): v for k, v in elmapping.items()} return data, mapping, style
[docs]class HistogramPlot(ColorbarPlot): selection_display = BokehOverlaySelectionDisplay(color_prop=['color', 'fill_color']) style_opts = base_properties + fill_properties + line_properties + ['cmap'] _nonvectorized_styles = base_properties + ['line_dash'] _plot_methods = dict(single='quad')
[docs] def get_data(self, element, ranges, style): if self.invert_axes: mapping = dict(top='right', bottom='left', left=0, right='top') else: mapping = dict(top='top', bottom=0, left='left', right='right') if self.static_source: data = dict(top=[], left=[], right=[]) else: x = element.kdims[0] values = element.dimension_values(1) edges = element.interface.coords(element, x, edges=True) data = dict(top=values, left=edges[:-1], right=edges[1:]) self._get_hover_data(data, element) return (data, mapping, style)
[docs] def get_extents(self, element, ranges, range_type='combined'): ydim = element.get_dimension(1) s0, s1 = ranges[ydim.name]['soft'] s0 = min(s0, 0) if isfinite(s0) else 0 s1 = max(s1, 0) if isfinite(s1) else 0 ranges[ydim.name]['soft'] = (s0, s1) return super(HistogramPlot, self).get_extents(element, ranges, range_type)
[docs]class SideHistogramPlot(HistogramPlot): style_opts = HistogramPlot.style_opts + ['cmap'] height = param.Integer(default=125, doc="The height of the plot") width = param.Integer(default=125, doc="The width of the plot") show_title = param.Boolean(default=False, doc=""" Whether to display the plot title.""") default_tools = param.List(default=['save', 'pan', 'wheel_zoom', 'box_zoom', 'reset'], doc="A list of plugin tools to use on the plot.") _callback = """ color_mapper.low = cb_obj['geometry']['{axis}0']; color_mapper.high = cb_obj['geometry']['{axis}1']; source.change.emit() main_source.change.emit() """ def __init__(self, *args, **kwargs): super(SideHistogramPlot, self).__init__(*args, **kwargs) if self.invert_axes: self.default_tools.append('ybox_select') else: self.default_tools.append('xbox_select')
[docs] def get_data(self, element, ranges, style): data, mapping, style = HistogramPlot.get_data(self, element, ranges, style) color_dims = [d for d in self.adjoined.traverse(lambda x: x.handles.get('color_dim')) if d is not None] dimension = color_dims[0] if color_dims else None cmapper = self._get_colormapper(dimension, element, {}, {}) if cmapper and dimension in element.dimensions(): if isinstance(dimension, dim): dim_name = dimension.dimension.name data[dim_name] = [] if self.static_source else dimension.apply(element) else: dim_name = dimension.name data[dim_name] = [] if self.static_source else element.dimension_values(dimension) mapping['fill_color'] = {'field': dim_name, 'transform': cmapper} return (data, mapping, style)
def _init_glyph(self, plot, mapping, properties): """ Returns a Bokeh glyph object. """ ret = super(SideHistogramPlot, self)._init_glyph(plot, mapping, properties) if not 'field' in mapping.get('fill_color', {}): return ret dim = mapping['fill_color']['field'] sources = self.adjoined.traverse(lambda x: (x.handles.get('color_dim'), x.handles.get('source'))) sources = [src for cdim, src in sources if cdim == dim] tools = [t for t in self.handles['plot'].tools if isinstance(t, BoxSelectTool)] if not tools or not sources: return main_source = sources[0] handles = {'color_mapper': self.handles['color_mapper'], 'source': self.handles['source'], 'cds': self.handles['source'], 'main_source': main_source} callback = self._callback.format(axis='y' if self.invert_axes else 'x') self.state.js_on_event("selectiongeometry", CustomJS(args=handles, code=callback)) return ret
[docs]class ErrorPlot(ColorbarPlot): selected = param.List(default=None, doc=""" The current selection as a list of integers corresponding to the selected items.""") selection_display = BokehOverlaySelectionDisplay() style_opts = ([ p for p in line_properties if p.split('_')[0] not in ('hover', 'selection', 'nonselection', 'muted') ] + ['lower_head', 'upper_head'] + base_properties) _nonvectorized_styles = base_properties + ['line_dash'] _mapping = dict(base="base", upper="upper", lower="lower") _plot_methods = dict(single=Whisker)
[docs] def get_data(self, element, ranges, style): mapping = dict(self._mapping) if self.static_source: return {}, mapping, style x_idx, y_idx = (1, 0) if element.horizontal else (0, 1) base = element.dimension_values(x_idx) mean = element.dimension_values(y_idx) neg_error = element.dimension_values(2) pos_idx = 3 if len(element.dimensions()) > 3 else 2 pos_error = element.dimension_values(pos_idx) lower = mean - neg_error upper = mean + pos_error if element.horizontal ^ self.invert_axes: mapping['dimension'] = 'width' else: mapping['dimension'] = 'height' data = dict(base=base, lower=lower, upper=upper) self._categorize_data(data, ('base',), element.dimensions()) return (data, mapping, style)
def _init_glyph(self, plot, mapping, properties): """ Returns a Bokeh glyph object. """ properties = {k: v for k, v in properties.items() if 'legend' not in k} for prop in ['color', 'alpha']: if prop not in properties: continue pval = properties.pop(prop) line_prop = 'line_%s' % prop fill_prop = 'fill_%s' % prop if line_prop not in properties: properties[line_prop] = pval if fill_prop not in properties and fill_prop in self.style_opts: properties[fill_prop] = pval properties = mpl_to_bokeh(properties) plot_method = self._plot_methods['single'] glyph = plot_method(**dict(properties, **mapping)) plot.add_layout(glyph) return None, glyph
[docs]class SpreadPlot(ElementPlot): padding = param.ClassSelector(default=(0, 0.1), class_=(int, float, tuple)) selection_display = BokehOverlaySelectionDisplay() style_opts = base_properties + fill_properties + line_properties _no_op_style = style_opts _nonvectorized_styles = style_opts _plot_methods = dict(single='patch') _stream_data = False # Plot does not support streaming data def _split_area(self, xs, lower, upper): """ Splits area plots at nans and returns x- and y-coordinates for each area separated by nans. """ xnan = np.array([np.datetime64('nat') if xs.dtype.kind == 'M' else np.nan]) ynan = np.array([np.datetime64('nat') if lower.dtype.kind == 'M' else np.nan]) split = np.where(~isfinite(xs) | ~isfinite(lower) | ~isfinite(upper))[0] xvals = np.split(xs, split) lower = np.split(lower, split) upper = np.split(upper, split) band_x, band_y = [], [] for i, (x, l, u) in enumerate(zip(xvals, lower, upper)): if i: x, l, u = x[1:], l[1:], u[1:] if not len(x): continue band_x += [np.append(x, x[::-1]), xnan] band_y += [np.append(l, u[::-1]), ynan] if len(band_x): xs = np.concatenate(band_x[:-1]) ys = np.concatenate(band_y[:-1]) return xs, ys return [], []
[docs] def get_data(self, element, ranges, style): mapping = dict(x='x', y='y') xvals = element.dimension_values(0) mean = element.dimension_values(1) neg_error = element.dimension_values(2) pos_idx = 3 if len(element.dimensions()) > 3 else 2 pos_error = element.dimension_values(pos_idx) lower = mean - neg_error upper = mean + pos_error band_x, band_y = self._split_area(xvals, lower, upper) if self.invert_axes: data = dict(x=band_y, y=band_x) else: data = dict(x=band_x, y=band_y) return data, mapping, style
[docs]class AreaPlot(AreaMixin, SpreadPlot): padding = param.ClassSelector(default=(0, 0.1), class_=(int, float, tuple)) selection_display = BokehOverlaySelectionDisplay() _stream_data = False # Plot does not support streaming data
[docs] def get_data(self, element, ranges, style): mapping = dict(x='x', y='y') xs = element.dimension_values(0) if len(element.vdims) > 1: bottom = element.dimension_values(2) else: bottom = np.zeros(len(element)) top = element.dimension_values(1) band_xs, band_ys = self._split_area(xs, bottom, top) if self.invert_axes: data = dict(x=band_ys, y=band_xs) else: data = dict(x=band_xs, y=band_ys) return data, mapping, style
[docs]class SpikesPlot(SpikesMixin, ColorbarPlot): spike_length = param.Number(default=0.5, doc=""" The length of each spike if Spikes object is one dimensional.""") position = param.Number(default=0., doc=""" The position of the lower end of each spike.""") show_legend = param.Boolean(default=True, doc=""" Whether to show legend for the plot.""") # Deprecated parameters color_index = param.ClassSelector(default=None, class_=(basestring, int), allow_None=True, doc=""" Deprecated in favor of color style mapping, e.g. `color=dim('color')`""") selection_display = BokehOverlaySelectionDisplay() style_opts = base_properties + line_properties + ['cmap', 'palette'] _nonvectorized_styles = base_properties + ['cmap'] _plot_methods = dict(single='segment') def _get_axis_dims(self, element): if 'spike_length' in self.lookup_options(element, 'plot').options: return [element.dimensions()[0], None, None] return super(SpikesPlot, self)._get_axis_dims(element)
[docs] def get_data(self, element, ranges, style): dims = element.dimensions() data = {} pos = self.position opts = self.lookup_options(element, 'plot').options if len(element) == 0 or self.static_source: data = {'x': [], 'y0': [], 'y1': []} else: data['x'] = element.dimension_values(0) data['y0'] = np.full(len(element), pos) if len(dims) > 1 and 'spike_length' not in opts: data['y1'] = element.dimension_values(1)+pos else: data['y1'] = data['y0']+self.spike_length if self.invert_axes: mapping = {'x0': 'y0', 'x1': 'y1', 'y0': 'x', 'y1': 'x'} else: mapping = {'x0': 'x', 'x1': 'x', 'y0': 'y0', 'y1': 'y1'} cdata, cmapping = self._get_color_data(element, ranges, dict(style)) data.update(cdata) mapping.update(cmapping) self._get_hover_data(data, element) return data, mapping, style
[docs]class SideSpikesPlot(SpikesPlot): """ SpikesPlot with useful defaults for plotting adjoined rug plot. """ selected = param.List(default=None, doc=""" The current selection as a list of integers corresponding to the selected items.""") xaxis = param.ObjectSelector(default='top-bare', objects=['top', 'bottom', 'bare', 'top-bare', 'bottom-bare', None], doc=""" Whether and where to display the xaxis, bare options allow suppressing all axis labels including ticks and xlabel. Valid options are 'top', 'bottom', 'bare', 'top-bare' and 'bottom-bare'.""") yaxis = param.ObjectSelector(default='right-bare', objects=['left', 'right', 'bare', 'left-bare', 'right-bare', None], doc=""" Whether and where to display the yaxis, bare options allow suppressing all axis labels including ticks and ylabel. Valid options are 'left', 'right', 'bare' 'left-bare' and 'right-bare'.""") border = param.Integer(default=5, doc="Default borders on plot") height = param.Integer(default=50, doc="Height of plot") width = param.Integer(default=50, doc="Width of plot")
[docs]class BarPlot(BarsMixin, ColorbarPlot, LegendPlot): """ BarPlot allows generating single- or multi-category bar Charts, by selecting which key dimensions are mapped onto separate groups, categories and stacks. """ multi_level = param.Boolean(default=True, doc=""" Whether the Bars should be grouped into a second categorical axis level.""") stacked = param.Boolean(default=False, doc=""" Whether the bars should be stacked or grouped.""") # Deprecated parameters color_index = param.ClassSelector(default=None, class_=(basestring, int), allow_None=True, doc=""" Deprecated in favor of color style mapping, e.g. `color=dim('color')`""") selection_display = BokehOverlaySelectionDisplay() style_opts = (base_properties + fill_properties + line_properties + ['bar_width', 'cmap']) _nonvectorized_styles = base_properties + ['bar_width', 'cmap'] _plot_methods = dict(single=('vbar', 'hbar')) # Declare that y-range should auto-range if not bounded _x_range_type = FactorRange _y_range_type = Range1d def _axis_properties(self, axis, key, plot, dimension=None, ax_mapping={'x': 0, 'y': 1}): props = super(BarPlot, self)._axis_properties(axis, key, plot, dimension, ax_mapping) if (not self.multi_level and not self.stacked and self.current_frame.ndims > 1 and ((not self.invert_axes and axis == 'x') or (self.invert_axes and axis =='y'))): props['separator_line_width'] = 0 props['major_tick_line_alpha'] = 0 props['major_label_text_font_size'] = '0pt' props['group_text_color'] = 'black' props['group_text_font_style'] = "normal" if axis == 'x': props['group_text_align'] = "center" if 'major_label_orientation' in props: props['group_label_orientation'] = props.pop('major_label_orientation') elif axis == 'y': props['group_label_orientation'] = 0 props['group_text_align'] = 'right' props['group_text_baseline'] = 'middle' return props def _get_axis_dims(self, element): if element.ndims > 1 and not (self.stacked or not self.multi_level): xdims = element.kdims else: xdims = element.kdims[0] return (xdims, element.vdims[0]) def _get_factors(self, element, ranges): xvals, gvals = self._get_coords(element, ranges) if gvals is not None: xvals = [(x, g) for x in xvals for g in gvals] return ([], xvals) if self.invert_axes else (xvals, [])
[docs] def get_stack(self, xvals, yvals, baselines, sign='positive'): """ Iterates over a x- and y-values in a stack layer and appropriately offsets the layer on top of the previous layer. """ bottoms, tops = [], [] for x, y in zip(xvals, yvals): baseline = baselines[x][sign] if sign == 'positive': bottom = baseline top = bottom+y baseline = top else: top = baseline bottom = top+y baseline = bottom baselines[x][sign] = baseline bottoms.append(bottom) tops.append(top) return bottoms, tops
def _glyph_properties(self, *args, **kwargs): props = super(BarPlot, self)._glyph_properties(*args, **kwargs) return {k: v for k, v in props.items() if k not in ['width', 'bar_width']} def _add_color_data(self, ds, ranges, style, cdim, data, mapping, factors, colors): cdata, cmapping = self._get_color_data(ds, ranges, dict(style), factors=factors, colors=colors) if 'color' not in cmapping: return # Enable legend if colormapper is categorical cmapper = cmapping['color']['transform'] legend_prop = 'legend_field' if bokeh_version >= '1.3.5' else 'legend' if ('color' in cmapping and self.show_legend and isinstance(cmapper, CategoricalColorMapper)): mapping[legend_prop] = cdim.name if not self.stacked and ds.ndims > 1 and self.multi_level: cmapping.pop(legend_prop, None) mapping.pop(legend_prop, None) # Merge data and mappings mapping.update(cmapping) for k, cd in cdata.items(): if isinstance(cmapper, CategoricalColorMapper) and cd.dtype.kind in 'uif': cd = categorize_array(cd, cdim) if k not in data or len(data[k]) != [len(data[key]) for key in data if key != k][0]: data[k].append(cd) else: data[k][-1] = cd
[docs] def get_data(self, element, ranges, style): # Get x, y, group, stack and color dimensions group_dim, stack_dim = None, None if element.ndims == 1: grouping = None elif self.stacked: grouping = 'stacked' stack_dim = element.get_dimension(1) if stack_dim.values: stack_order = stack_dim.values elif stack_dim in ranges and ranges[stack_dim.name].get('factors'): stack_order = ranges[stack_dim]['factors'] else: stack_order = element.dimension_values(1, False) stack_order = list(stack_order) else: grouping = 'grouped' group_dim = element.get_dimension(1) xdim = element.get_dimension(0) ydim = element.vdims[0] no_cidx = self.color_index is None color_index = (group_dim or stack_dim) if no_cidx else self.color_index color_dim = element.get_dimension(color_index) if color_dim: self.color_index = color_dim.name # Define style information width = style.get('bar_width', style.get('width', 1)) if 'width' in style: self.param.warning("BarPlot width option is deprecated " "use 'bar_width' instead.") cmap = style.get('cmap') hover = 'hover' in self.handles # Group by stack or group dim if necessary if group_dim is None: grouped = {0: element} else: grouped = element.groupby(group_dim, group_type=Dataset, container_type=OrderedDict, datatype=['dataframe', 'dictionary']) y0, y1 = ranges.get(ydim.name, {'combined': (None, None)})['combined'] if self.logy: bottom = (ydim.range[0] or (10**(np.log10(y1)-2)) if y1 else 0.01) else: bottom = 0 # Map attributes to data if grouping == 'stacked': mapping = {'x': xdim.name, 'top': 'top', 'bottom': 'bottom', 'width': width} elif grouping == 'grouped': mapping = {'x': 'xoffsets', 'top': ydim.name, 'bottom': bottom, 'width': width} else: mapping = {'x': xdim.name, 'top': ydim.name, 'bottom': bottom, 'width': width} # Get colors cdim = color_dim or group_dim style_mapping = [v for k, v in style.items() if 'color' in k and (isinstance(v, dim) or v in element)] if style_mapping and not no_cidx and self.color_index is not None: self.param.warning("Cannot declare style mapping for '%s' option " "and declare a color_index; ignoring the color_index." % style_mapping[0]) cdim = None cvals = element.dimension_values(cdim, expanded=False) if cdim else None if cvals is not None: if cvals.dtype.kind in 'uif' and no_cidx: cvals = categorize_array(cvals, color_dim) factors = None if cvals.dtype.kind in 'uif' else list(cvals) if cdim is xdim and factors: factors = list(categorize_array(factors, xdim)) if cmap is None and factors: styles = self.style.max_cycles(len(factors)) colors = [styles[i]['color'] for i in range(len(factors))] colors = [rgb2hex(c) if isinstance(c, tuple) else c for c in colors] else: colors = None else: factors, colors = None, None # Iterate over stacks and groups and accumulate data data = defaultdict(list) baselines = defaultdict(lambda: {'positive': bottom, 'negative': 0}) for i, (k, ds) in enumerate(grouped.items()): k = k[0] if isinstance(k, tuple) else k if group_dim: gval = k if isinstance(k, basestring) else group_dim.pprint_value(k) # Apply stacking or grouping if grouping == 'stacked': for sign, slc in [('negative', (None, 0)), ('positive', (0, None))]: slc_ds = ds.select(**{ds.vdims[0].name: slc}) stack_inds = [stack_order.index(v) if v in stack_order else -1 for v in slc_ds[stack_dim.name]] slc_ds = slc_ds.add_dimension('_stack_order', 0, stack_inds).sort('_stack_order') xs = slc_ds.dimension_values(xdim) ys = slc_ds.dimension_values(ydim) bs, ts = self.get_stack(xs, ys, baselines, sign) data['bottom'].append(bs) data['top'].append(ts) data[xdim.name].append(xs) data[stack_dim.name].append(slc_ds.dimension_values(stack_dim)) if hover: data[ydim.name].append(ys) if not style_mapping: self._add_color_data(slc_ds, ranges, style, cdim, data, mapping, factors, colors) elif grouping == 'grouped': xs = ds.dimension_values(xdim) ys = ds.dimension_values(ydim) xoffsets = [(x if xs.dtype.kind in 'SU' else xdim.pprint_value(x), gval) for x in xs] data['xoffsets'].append(xoffsets) data[ydim.name].append(ys) if hover: data[xdim.name].append(xs) if group_dim not in ds.dimensions(): ds = ds.add_dimension(group_dim.name, ds.ndims, gval) data[group_dim.name].append(ds.dimension_values(group_dim)) else: data[xdim.name].append(ds.dimension_values(xdim)) data[ydim.name].append(ds.dimension_values(ydim)) if hover: for vd in ds.vdims[1:]: data[vd.name].append(ds.dimension_values(vd)) if grouping != 'stacked' and not style_mapping: self._add_color_data(ds, ranges, style, cdim, data, mapping, factors, colors) # Concatenate the stacks or groups sanitized_data = {} for col, vals in data.items(): if len(vals) == 1: sanitized_data[dimension_sanitizer(col)] = vals[0] elif vals: sanitized_data[dimension_sanitizer(col)] = np.concatenate(vals) for name, val in mapping.items(): sanitized = None if isinstance(val, basestring): sanitized = dimension_sanitizer(mapping[name]) mapping[name] = sanitized elif isinstance(val, dict) and 'field' in val: sanitized = dimension_sanitizer(val['field']) val['field'] = sanitized if sanitized is not None and sanitized not in sanitized_data: sanitized_data[sanitized] = [] # Ensure x-values are categorical xname = dimension_sanitizer(xdim.name) if xname in sanitized_data: sanitized_data[xname] = categorize_array(sanitized_data[xname], xdim) # If axes inverted change mapping to match hbar signature if self.invert_axes: mapping.update({'y': mapping.pop('x'), 'left': mapping.pop('bottom'), 'right': mapping.pop('top'), 'height': mapping.pop('width')}) return sanitized_data, mapping, style