Source code for holoviews.plotting.bokeh.chart

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 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 ColorbarPlot, ElementPlot, LegendPlot, OverlayPlot
from .selection import BokehOverlaySelectionDisplay
from .styles import (
    base_properties,
    expand_batched_style,
    fill_properties,
    line_properties,
    mpl_to_bokeh,
    rgb2hex,
)
from .util import 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_=(str, int), allow_None=True, doc=""" Deprecated in favor of color style mapping, e.g. `color=dim('color')`""") size_index = param.ClassSelector(default=None, class_=(str, 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'] + base_properties + 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, str) 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( f'{sdim.pprint_label} dimension is not numeric, cannot use to scale {eltype} size.') 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.update(**{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(next(iter(eldata.values()))) 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_=(str, 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_=(str, 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_=(str, 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, str): 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 = magnitudes * base_dist else: magnitudes = np.ones(len(element)) if self.rescale_lengths: base_dist = get_min_distance(element) magnitudes = magnitudes * base_dist return magnitudes def _glyph_properties(self, *args): properties = super()._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.update(**{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) if hasattr(edges, 'compute'): edges = edges.compute() 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', **kwargs): 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().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().__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: cvals = None if isinstance(dimension, dim): if dimension.applies(element): dim_name = dimension.dimension.name cvals = [] if self.static_source else dimension.apply(element) elif dimension in element.dimensions(): dim_name = dimension.name cvals = [] if self.static_source else element.dimension_values(dimension) if cvals is not None: data[dim_name] = cvals 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()._init_glyph(plot, mapping, properties) if "field" not 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 = f'line_{prop}' fill_prop = f'fill_{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_=(str, 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')
[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_=(str, 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=None): if ax_mapping is None: ax_mapping = {"x": 0, "y": 1} props = super()._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 # The major_label_text_* is a workaround for 0pt font size not working in Safari. # See: https://github.com/holoviz/holoviews/issues/5672 props['major_label_text_font_size'] = '1px' props['major_label_text_alpha'] = 0 props['major_label_text_line_height'] = 0 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_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()._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 ('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]) != next(len(data[key]) for key in data if key != k): 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=dict, datatype=['dataframe', 'dictionary']) y0, y1 = ranges.get(ydim.name, {'combined': (None, None)})['combined'] if self.logy: bottom = (ydim.range[0] or (0.01 if y1 > 0.01 else 10**(np.log10(y1)-2))) 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 k, ds in grouped.items(): k = k[0] if isinstance(k, tuple) else k if group_dim: gval = k if isinstance(k, str) 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) for vd in slc_ds.vdims[1:]: data[vd.name].append(slc_ds.dimension_values(vd)) 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, 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 and grouping != 'stacked': 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, str): 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