Source code for holoviews.plotting.mpl.chart

import matplotlib as mpl
import numpy as np
import param
from matplotlib import cm
from matplotlib.collections import LineCollection
from matplotlib.dates import DateFormatter, date2num
from packaging.version import Version

from ...core.dimension import Dimension, dimension_name
from ...core.options import Store, abbreviated_exception
from ...core.util import (
    dt64_to_dt,
    dt_to_int,
    isdatetime,
    isfinite,
    isscalar,
    match_spec,
    search_indices,
    unique_array,
)
from ...element import HeatMap, Raster
from ...operation import interpolate_curve
from ...util.transform import dim
from ..mixins import AreaMixin, BarsMixin, SpikesMixin
from ..plot import PlotSelector
from ..util import compute_sizes, get_min_distance, get_sideplot_ranges
from .element import ColorbarPlot, ElementPlot, LegendPlot
from .path import PathPlot
from .plot import AdjoinedPlot, mpl_rc_context
from .util import mpl_version


[docs]class ChartPlot(ElementPlot): """ Baseclass to plot Chart elements. """
[docs]class CurvePlot(ChartPlot): """ CurvePlot can plot Curve and ViewMaps of Curve, which can be displayed as a single frame or animation. Axes, titles and legends are automatically generated from dim_info. If the dimension is set to cyclic in the dim_info it will rotate the curve so that minimum y values are at the minimum x value to make the plots easier to interpret. """ autotick = param.Boolean(default=False, doc=""" Whether to let matplotlib automatically compute tick marks or to allow the user to control tick marks.""") 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'.""") relative_labels = param.Boolean(default=False, doc=""" If plotted quantity is cyclic and center_cyclic is enabled, will compute tick labels relative to the center.""") padding = param.ClassSelector(default=(0, 0.1), class_=(int, float, tuple)) show_grid = param.Boolean(default=False, doc=""" Enable axis grid.""") show_legend = param.Boolean(default=True, doc=""" Whether to show legend for the plot.""") style_opts = ['alpha', 'color', 'visible', 'linewidth', 'linestyle', 'marker', 'ms'] _nonvectorized_styles = style_opts _plot_methods = dict(single='plot') def get_data(self, element, ranges, style): with abbreviated_exception(): style = self._apply_transforms(element, ranges, style) if 'steps' in self.interpolation: element = interpolate_curve(element, interpolation=self.interpolation) xs = element.dimension_values(0) ys = element.dimension_values(1) dims = element.dimensions() if isdatetime(xs): dimtype = element.get_dimension_type(0) dt_format = Dimension.type_formatters.get(dimtype, '%Y-%m-%d %H:%M:%S') dims[0] = dims[0].clone(value_format=DateFormatter(dt_format)) coords = (ys, xs) if self.invert_axes else (xs, ys) return coords, style, {'dimensions': dims}
[docs] def init_artists(self, ax, plot_args, plot_kwargs): xs, ys = plot_args if isdatetime(xs): artist = ax.plot_date(xs, ys, '-', **plot_kwargs)[0] else: artist = ax.plot(xs, ys, **plot_kwargs)[0] return {'artist': artist}
[docs] def update_handles(self, key, axis, element, ranges, style): artist = self.handles['artist'] (xs, ys), style, axis_kwargs = self.get_data(element, ranges, style) artist.set_xdata(xs) artist.set_ydata(ys) return axis_kwargs
[docs]class ErrorPlot(ColorbarPlot): """ ErrorPlot plots the ErrorBar Element type and supporting both horizontal and vertical error bars via the 'horizontal' plot option. """ style_opts = ['edgecolor', 'elinewidth', 'capsize', 'capthick', 'barsabove', 'lolims', 'uplims', 'xlolims', 'errorevery', 'xuplims', 'alpha', 'linestyle', 'linewidth', 'markeredgecolor', 'markeredgewidth', 'markerfacecolor', 'markersize', 'solid_capstyle', 'solid_joinstyle', 'dashes', 'color'] _plot_methods = dict(single='errorbar')
[docs] def init_artists(self, ax, plot_data, plot_kwargs): handles = ax.errorbar(*plot_data, **plot_kwargs) bottoms, tops = None, None if mpl_version >= Version('2.0'): _, caps, verts = handles if caps: bottoms, tops = caps else: _, (bottoms, tops), verts = handles return {'bottoms': bottoms, 'tops': tops, 'verts': verts[0], 'artist': verts[0]}
def get_data(self, element, ranges, style): with abbreviated_exception(): style = self._apply_transforms(element, ranges, style) color = style.pop('color', None) if isinstance(color, np.ndarray): style['ecolor'] = color if 'edgecolor' in style: style['ecolor'] = style.pop('edgecolor') if 'linewidth' in style: # Raise ValueError if a numpy array, so needs to be a list. style["elinewidth"] = np.asarray(style.pop('linewidth')).tolist() c = style.get('c') if isinstance(c, np.ndarray): with abbreviated_exception(): raise ValueError('Mapping a continuous or categorical ' 'dimension to a color on a ErrorBarPlot ' f'is not supported by the {self.renderer.backend} backend. ' 'To map a dimension to a color supply ' 'an explicit list of rgba colors.' ) style['fmt'] = 'none' dims = element.dimensions() xs, ys = (element.dimension_values(i) for i in range(2)) err = element.array(dimensions=dims[2:4]) err_key = 'xerr' if element.horizontal ^ self.invert_axes else 'yerr' coords = (ys, xs) if self.invert_axes else (xs, ys) style[err_key] = err.T if len(dims) > 3 else err[:, 0] return coords, style, {}
[docs] def update_handles(self, key, axis, element, ranges, style): bottoms = self.handles['bottoms'] tops = self.handles['tops'] verts = self.handles['verts'] _, style, axis_kwargs = self.get_data(element, ranges, style) xs, ys, neg_error = (element.dimension_values(i) for i in range(3)) pos_idx = 3 if len(element.dimensions()) > 3 else 2 pos_error = element.dimension_values(pos_idx) if element.horizontal: bxs, bys = xs - neg_error, ys txs, tys = xs + pos_error, ys else: bxs, bys = xs, ys - neg_error txs, tys = xs, ys + pos_error if self.invert_axes: bxs, bys = bys, bxs txs, tys = tys, txs new_arrays = np.moveaxis(np.array([[bxs, bys], [txs, tys]]), 2, 0) verts.set_paths(new_arrays) if bottoms: bottoms.set_xdata(bxs) bottoms.set_ydata(bys) if tops: tops.set_xdata(txs) tops.set_ydata(tys) if 'ecolor' in style: verts.set_edgecolors(style['ecolor']) if 'elinewidth' in style: verts.set_linewidths(style['elinewidth']) return axis_kwargs
[docs]class AreaPlot(AreaMixin, ChartPlot): padding = param.ClassSelector(default=(0, 0.1), class_=(int, float, tuple)) show_legend = param.Boolean(default=False, doc=""" Whether to show legend for the plot.""") style_opts = ['color', 'facecolor', 'alpha', 'edgecolor', 'linewidth', 'hatch', 'linestyle', 'joinstyle', 'fill', 'capstyle', 'interpolate', 'step'] _nonvectorized_styles = style_opts _plot_methods = dict(single='fill_between') def get_data(self, element, ranges, style): with abbreviated_exception(): style = self._apply_transforms(element, ranges, style) xs = element.dimension_values(0) ys = [element.dimension_values(vdim) for vdim in element.vdims] return tuple([xs]+ys), style, {}
[docs] def init_artists(self, ax, plot_data, plot_kwargs): fill_fn = ax.fill_betweenx if self.invert_axes else ax.fill_between stack = fill_fn(*plot_data, **plot_kwargs) return {'artist': stack}
[docs]class SideAreaPlot(AdjoinedPlot, AreaPlot): bgcolor = param.Parameter(default=(1, 1, 1, 0), doc=""" Make plot background invisible.""") border_size = param.Number(default=0, doc=""" The size of the border expressed as a fraction of the main plot.""") xaxis = param.ObjectSelector(default='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='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'.""")
[docs]class SpreadPlot(AreaPlot): """ SpreadPlot plots the Spread Element type. """ padding = param.ClassSelector(default=(0, 0.1), class_=(int, float, tuple)) show_legend = param.Boolean(default=False, doc=""" Whether to show legend for the plot.""") def __init__(self, element, **params): super().__init__(element, **params) def get_data(self, element, ranges, style): with abbreviated_exception(): style = self._apply_transforms(element, ranges, style) xs = 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) return (xs, mean-neg_error, mean+pos_error), style, {}
[docs] def get_extents(self, element, ranges, range_type='combined', **kwargs): return ChartPlot.get_extents(self, element, ranges, range_type)
[docs]class HistogramPlot(ColorbarPlot): """ HistogramPlot can plot DataHistograms and ViewMaps of DataHistograms, which can be displayed as a single frame or animation. """ style_opts = ['alpha', 'color', 'align', 'visible', 'facecolor', 'edgecolor', 'log', 'capsize', 'error_kw', 'hatch', 'linewidth'] _nonvectorized_styles = ['alpha', 'log', 'error_kw', 'hatch', 'visible', 'align'] def __init__(self, histograms, **params): self.center = False self.cyclic = False super().__init__(histograms, **params) if self.invert_axes: self.axis_settings = ['ylabel', 'xlabel', 'yticks'] else: self.axis_settings = ['xlabel', 'ylabel', 'xticks'] val_dim = self.hmap.last.get_dimension(1) self.cyclic_range = val_dim.range if val_dim.cyclic else None @mpl_rc_context def initialize_plot(self, ranges=None): hist = self.hmap.last key = self.keys[-1] ranges = self.compute_ranges(self.hmap, key, ranges) el_ranges = match_spec(hist, ranges) # Get plot ranges and values dims = hist.dimensions()[:2] edges, hvals, widths, lims, is_datetime = self._process_hist(hist) if is_datetime and not dims[0].value_format: dt_format = Dimension.type_formatters[np.datetime64] dims[0] = dims[0].clone(value_format=DateFormatter(dt_format)) style = self.style[self.cyclic_index] if self.invert_axes: self.offset_linefn = self.handles['axis'].axvline self.plotfn = self.handles['axis'].barh else: self.offset_linefn = self.handles['axis'].axhline self.plotfn = self.handles['axis'].bar with abbreviated_exception(): style = self._apply_transforms(hist, ranges, style) if 'vmin' in style: raise ValueError('Mapping a continuous dimension to a ' 'color on a HistogramPlot is not ' f'supported by the {self.renderer.backend} backend. ' 'To map a dimension to a color supply ' 'an explicit list of rgba colors.' ) # Plot bars and make any adjustments legend = hist.label if self.show_legend else '' bars = self.plotfn(edges, hvals, widths, zorder=self.zorder, label=legend, align='edge', **style) self.handles['artist'] = self._update_plot(self.keys[-1], hist, bars, lims, ranges) # Indexing top ticks = self._compute_ticks(hist, edges, widths, lims) ax_settings = self._process_axsettings(hist, lims, ticks) ax_settings['dimensions'] = dims return self._finalize_axis(self.keys[-1], ranges=el_ranges, element=hist, **ax_settings) def _process_hist(self, hist): """ Get data from histogram, including bin_ranges and values. """ self.cyclic = hist.get_dimension(0).cyclic x = hist.kdims[0] edges = hist.interface.coords(hist, x, edges=True) values = hist.dimension_values(1) hist_vals = np.array(values) xlim = hist.range(0) ylim = hist.range(1) is_datetime = isdatetime(edges) if is_datetime: edges = np.array([dt64_to_dt(e) if isinstance(e, np.datetime64) else e for e in edges]) edges = date2num(edges) xlim = tuple(dt_to_int(v, 'D') for v in xlim) widths = np.diff(edges) return edges[:-1], hist_vals, widths, xlim+ylim, is_datetime def _compute_ticks(self, element, edges, widths, lims): """ Compute the ticks either as cyclic values in degrees or as roughly evenly spaced bin centers. """ if self.xticks is None or not isinstance(self.xticks, int): return None if self.cyclic: x0, x1, _, _ = lims xvals = np.linspace(x0, x1, self.xticks) labels = [f"{np.rad2deg(x):.0f}\N{DEGREE SIGN}" for x in xvals] elif self.xticks: dim = element.get_dimension(0) inds = np.linspace(0, len(edges), self.xticks, dtype=int) edges = list(edges) + [edges[-1] + widths[-1]] xvals = [edges[i] for i in inds] labels = [dim.pprint_value(v) for v in xvals] return [xvals, labels]
[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)
def _process_axsettings(self, hist, lims, ticks): """ Get axis settings options including ticks, x- and y-labels and limits. """ axis_settings = dict(zip(self.axis_settings, [None, None, (None if self.overlaid else ticks)])) return axis_settings def _update_plot(self, key, hist, bars, lims, ranges): """ Process bars can be subclassed to manually adjust bars after being plotted. """ return bars def _update_artists(self, key, hist, edges, hvals, widths, lims, ranges): """ Update all the artists in the histogram. Subclassable to allow updating of further artists. """ plot_vals = zip(self.handles['artist'], edges, hvals, widths) for bar, edge, height, width in plot_vals: if self.invert_axes: bar.set_y(edge) bar.set_width(height) bar.set_height(width) else: bar.set_x(edge) bar.set_height(height) bar.set_width(width)
[docs] def update_handles(self, key, axis, element, ranges, style): # Process values, axes and style edges, hvals, widths, lims, _ = self._process_hist(element) ticks = self._compute_ticks(element, edges, widths, lims) ax_settings = self._process_axsettings(element, lims, ticks) self._update_artists(key, element, edges, hvals, widths, lims, ranges) return ax_settings
[docs]class SideHistogramPlot(AdjoinedPlot, HistogramPlot): bgcolor = param.Parameter(default=(1, 1, 1, 0), doc=""" Make plot background invisible.""") offset = param.Number(default=0.2, bounds=(0,1), doc=""" Histogram value offset for a colorbar.""") show_grid = param.Boolean(default=False, doc=""" Whether to overlay a grid on the axis.""") def _process_hist(self, hist): """ Subclassed to offset histogram by defined amount. """ edges, hvals, widths, lims, isdatetime = super()._process_hist(hist) offset = self.offset * lims[3] hvals = hvals * (1-self.offset) hvals += offset lims = lims[0:3] + (lims[3] + offset,) return edges, hvals, widths, lims, isdatetime def _update_artists(self, n, element, edges, hvals, widths, lims, ranges): super()._update_artists(n, element, edges, hvals, widths, lims, ranges) self._update_plot(n, element, self.handles['artist'], lims, ranges) def _update_plot(self, key, element, bars, lims, ranges): """ Process the bars and draw the offset line as necessary. If a color map is set in the style of the 'main' ViewableElement object, color the bars appropriately, respecting the required normalization settings. """ main = self.adjoined.main _, y1 = element.range(1) offset = self.offset * y1 range_item, main_range, dim = get_sideplot_ranges(self, element, main, ranges) # Check if plot is colormapped plot_type = Store.registry['matplotlib'].get(type(range_item)) if isinstance(plot_type, PlotSelector): plot_type = plot_type.get_plot_class(range_item) opts = self.lookup_options(range_item, 'plot') if plot_type and issubclass(plot_type, ColorbarPlot): cidx = opts.options.get('color_index', None) if cidx is None: opts = self.lookup_options(range_item, 'style') cidx = opts.kwargs.get('color', None) if cidx not in range_item: cidx = None cdim = None if cidx is None else range_item.get_dimension(cidx) else: cdim = None # Get colormapping options if isinstance(range_item, (HeatMap, Raster)) or (cdim and cdim in element): style = self.lookup_options(range_item, 'style')[self.cyclic_index] cmap = cm.get_cmap(style.get('cmap')) main_range = style.get('clims', main_range) else: cmap = None if offset and ('offset_line' not in self.handles): self.handles['offset_line'] = self.offset_linefn(offset, linewidth=1.0, color='k') elif offset: self._update_separator(offset) if cmap is not None and main_range and (None not in main_range): self._colorize_bars(cmap, bars, element, main_range, dim) return bars def _colorize_bars(self, cmap, bars, element, main_range, dim): """ Use the given cmap to color the bars, applying the correct color ranges as necessary. """ cmap_range = main_range[1] - main_range[0] lower_bound = main_range[0] colors = np.array(element.dimension_values(dim)) colors = (colors - lower_bound) / (cmap_range) for c, bar in zip(colors, bars): bar.set_facecolor(cmap(c)) bar.set_clip_on(False) def _update_separator(self, offset): """ Compute colorbar offset and update separator line if map is non-zero. """ offset_line = self.handles['offset_line'] if offset == 0: offset_line.set_visible(False) else: offset_line.set_visible(True) if self.invert_axes: offset_line.set_xdata(offset) else: offset_line.set_ydata(offset)
[docs]class PointPlot(ChartPlot, ColorbarPlot, LegendPlot): """ Note that the 'cmap', 'vmin' and 'vmax' style arguments control how point magnitudes are rendered to different colors. """ show_grid = param.Boolean(default=False, doc=""" Whether to draw grid lines at the tick positions.""") # 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.""") style_opts = ['alpha', 'color', 'edgecolors', 'facecolors', 'linewidth', 'marker', 'size', 'visible', 'cmap', 'vmin', 'vmax', 'norm'] _nonvectorized_styles = ['alpha', 'marker', 'cmap', 'vmin', 'vmax', 'norm', 'visible'] _disabled_opts = ['size'] _plot_methods = dict(single='scatter') def get_data(self, element, ranges, style): xs, ys = (element.dimension_values(i) for i in range(2)) self._compute_styles(element, ranges, style) with abbreviated_exception(): style = self._apply_transforms(element, ranges, style) return (ys, xs) if self.invert_axes else (xs, ys), style, {} def _compute_styles(self, element, ranges, style): cdim = element.get_dimension(self.color_index) color = style.pop('color', None) cmap = style.get('cmap', None) if cdim and ((isinstance(color, str) and color in element) or isinstance(color, dim)): self.param.warning( "Cannot declare style mapping for 'color' option and " "declare a color_index; ignoring the color_index.") cdim = None if cdim and cmap: cs = element.dimension_values(self.color_index) # Check if numeric otherwise treat as categorical if cs.dtype.kind in 'uif': style['c'] = cs else: style['c'] = search_indices(cs, unique_array(cs)) self._norm_kwargs(element, ranges, style, cdim) elif color is not None: style['color'] = color style['edgecolors'] = style.pop('edgecolors', style.pop('edgecolor', 'none')) ms = style.get('s', mpl.rcParams['lines.markersize']) sdim = element.get_dimension(self.size_index) if sdim and ((isinstance(ms, str) and ms in element) or isinstance(ms, dim)): self.param.warning( "Cannot declare style mapping for 's' option and " "declare a size_index; ignoring the size_index.") sdim = None if sdim: 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 ' f'scale {eltype} size.') else: style['s'] = sizes style['edgecolors'] = style.pop('edgecolors', 'none')
[docs] def update_handles(self, key, axis, element, ranges, style): paths = self.handles['artist'] (xs, ys), style, _ = self.get_data(element, ranges, style) xdim, ydim = element.dimensions()[:2] if 'factors' in ranges.get(xdim.name, {}): factors = list(ranges[xdim.name]['factors']) xs = [factors.index(x) for x in xs if x in factors] if 'factors' in ranges.get(ydim.name, {}): factors = list(ranges[ydim.name]['factors']) ys = [factors.index(y) for y in ys if y in factors] paths.set_offsets(np.column_stack([xs, ys])) if 's' in style: sizes = style['s'] if isscalar(sizes): sizes = [sizes] paths.set_sizes(sizes) if 'vmin' in style: paths.set_clim((style['vmin'], style['vmax'])) if 'c' in style: paths.set_array(style['c']) if 'norm' in style: paths.norm = style['norm'] if 'linewidth' in style: paths.set_linewidths(style['linewidth']) if 'edgecolors' in style: paths.set_edgecolors(style['edgecolors']) if 'facecolors' in style: paths.set_edgecolors(style['facecolors'])
[docs]class VectorFieldPlot(ColorbarPlot): """ Renders vector fields in sheet coordinates. The vectors are expressed in polar coordinates and may be displayed according to angle alone (with some common, arbitrary arrow length) or may be true polar vectors. The color or magnitude can be mapped onto any dimension using the color_index and size_index. The length of the arrows is controlled by the 'scale' style option. The scaling of the arrows may also be controlled via the normalize_lengths and rescale_lengths plot option, which will normalize the lengths to a maximum of 1 and scale them according to the minimum distance respectively. """ arrow_heads = param.Boolean(default=True, doc=""" Whether or not to draw arrow heads. If arrowheads are enabled, they may be customized with the 'headlength' and 'headaxislength' style options.""") 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)) 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()`.""") style_opts = ['alpha', 'color', 'edgecolors', 'facecolors', 'linewidth', 'marker', 'visible', 'cmap', 'scale', 'headlength', 'headaxislength', 'pivot', 'width', 'headwidth', 'norm'] _nonvectorized_styles = ['alpha', 'marker', 'cmap', 'visible', 'norm', 'pivot', 'headlength', 'headaxislength', 'headwidth'] _plot_methods = dict(single='quiver') def _get_magnitudes(self, element, style, 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) if mag_dim is not None: 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 else: magnitudes = np.ones(len(element)) return magnitudes def get_data(self, element, ranges, style): # Compute coordinates xidx, yidx = (1, 0) if self.invert_axes else (0, 1) xs = element.dimension_values(xidx) if len(element.data) else [] ys = element.dimension_values(yidx) if len(element.data) else [] # Compute vector angle and magnitude radians = element.dimension_values(2) if len(element.data) else [] if self.invert_axes: radians = radians+1.5*np.pi angles = list(np.rad2deg(radians)) magnitudes = self._get_magnitudes(element, style, ranges) input_scale = style.pop('scale', 1.0) if self.rescale_lengths: min_dist = get_min_distance(element) input_scale = input_scale / min_dist args = (xs, ys, magnitudes, [0.0] * len(element)) # Compute color cdim = element.get_dimension(self.color_index) color = style.get('color', None) if cdim and ((isinstance(color, str) and color in element) or isinstance(color, dim)): self.param.warning( "Cannot declare style mapping for 'color' option and " "declare a color_index; ignoring the color_index.") cdim = None if cdim: colors = element.dimension_values(self.color_index) style['c'] = colors cdim = element.get_dimension(self.color_index) self._norm_kwargs(element, ranges, style, cdim) style.pop('color', None) # Process style with abbreviated_exception(): style = self._apply_transforms(element, ranges, style) style.update(dict(scale=input_scale, angles=angles, units='x', scale_units='x')) if 'vmin' in style: style['clim'] = (style.pop('vmin'), style.pop('vmax')) if 'c' in style: style['array'] = style.pop('c') if 'pivot' not in style: style['pivot'] = 'mid' if not self.arrow_heads: style['headaxislength'] = 0 return args, style, {}
[docs] def update_handles(self, key, axis, element, ranges, style): args, style, axis_kwargs = self.get_data(element, ranges, style) # Set magnitudes, angles and colors if supplied. quiver = self.handles['artist'] quiver.set_offsets(np.column_stack(args[:2])) quiver.U = args[2] quiver.angles = style['angles'] if 'color' in style: quiver.set_facecolors(style['color']) quiver.set_edgecolors(style['color']) if 'array' in style: quiver.set_array(style['array']) if 'clim' in style: quiver.set_clim(style['clim']) if 'linewidth' in style: quiver.set_linewidths(style['linewidth']) return axis_kwargs
[docs]class BarPlot(BarsMixin, ColorbarPlot, LegendPlot): bar_padding = param.Number(default=0.2, doc=""" Defines the padding between groups.""") 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.""") show_legend = param.Boolean(default=True, doc=""" Whether to show legend for the plot.""") style_opts = ['alpha', 'color', 'align', 'visible', 'edgecolor', 'log', 'facecolor', 'capsize', 'error_kw', 'hatch'] _nonvectorized_styles = ['visible'] legend_specs = dict( LegendPlot.legend_specs, top=dict( bbox_to_anchor=(0., 1.02, 1., .102), ncol=3, loc=3, mode="expand", borderaxespad=0. ), bottom=dict( bbox_to_anchor=(0., -0.4, 1., .102), ncol=3, loc=2, mode="expand", borderaxespad=0.1 ) ) def _get_values(self, element, ranges): """ Get unique index value for each bar """ gvals, cvals = self._get_coords(element, ranges, as_string=False) kdims = element.kdims if element.ndims == 1: dimensions = kdims + [None, None] values = {'group': gvals, 'stack': [None]} elif self.stacked: stack_dim = kdims[1] dimensions = [kdims[0], None, stack_dim] 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) values = {'group': gvals, 'stack': stack_order} else: dimensions = kdims + [None] values = {'group': gvals, 'category': cvals} return dimensions, values @mpl_rc_context def initialize_plot(self, ranges=None): element = self.hmap.last vdim = element.vdims[0] axis = self.handles['axis'] key = self.keys[-1] style = dict(zorder=self.zorder, **self.style[self.cyclic_index]) ranges = self.compute_ranges(self.hmap, key, ranges) ranges = match_spec(element, ranges) self.handles['artist'], xticks, xdims = self._create_bars(axis, element, ranges, style) kwargs = {'yticks': xticks} if self.invert_axes else {'xticks': xticks} return self._finalize_axis(key, ranges=ranges, element=element, dimensions=[xdims, vdim], **kwargs) def _finalize_ticks(self, axis, element, xticks, yticks, zticks): """ Apply ticks with appropriate offsets. """ alignments = None ticks = xticks or yticks if ticks is not None: ticks, labels, alignments = zip(*sorted(ticks, key=lambda x: x[0])) ticks = (list(ticks), list(labels)) if xticks: xticks = ticks elif yticks: yticks = ticks super()._finalize_ticks(axis, element, xticks, yticks, zticks) if alignments: if xticks: for t, y in zip(axis.get_xticklabels(), alignments): t.set_y(y) elif yticks: for t, x in zip(axis.get_yticklabels(), alignments): t.set_x(x) def _create_bars(self, axis, element, ranges, style): # Get values dimensions, and style information (gdim, cdim, sdim), values = self._get_values(element, ranges) style_dim = None if sdim: cats = values['stack'] style_dim = sdim elif cdim: cats = values['category'] style_dim = cdim if style_dim: style_map = {style_dim.pprint_value(v): self.style[i] for i, v in enumerate(cats)} else: style_map = {None: {}} # Compute widths width = (1-(2.*self.bar_padding)) / len(values.get('category', [None])) if self.invert_axes: plot_fn = 'barh' x, y, w, bottom = 'y', 'width', 'height', 'left' else: plot_fn = 'bar' x, y, w, bottom = 'x', 'height', 'width', 'bottom' # Iterate over group, category and stack dimension values # computing xticks and drawing bars and applying styles xticks, labels, bar_data = [], [], {} for gidx, grp in enumerate(values.get('group', [None])): sel_key = {} label = None if grp is not None: grp_label = gdim.pprint_value(grp) sel_key[gdim.name] = [grp] yalign = -0.04 if cdim and self.multi_level else 0 xticks.append((gidx+0.5, grp_label, yalign)) for cidx, cat in enumerate(values.get('category', [None])): xpos = gidx+self.bar_padding+(cidx*width) if cat is not None: label = cdim.pprint_value(cat) sel_key[cdim.name] = [cat] if self.multi_level: xticks.append((xpos+width/2., label, 0)) prev = 0 for stk in values.get('stack', [None]): if stk is not None: label = sdim.pprint_value(stk) sel_key[sdim.name] = [stk] el = element.select(**sel_key) vals = el.dimension_values(element.vdims[0].name) val = float(vals[0]) if len(vals) else np.nan xval = xpos+width/2. if label in bar_data: group = bar_data[label] group[x].append(xval) group[y].append(val) group[bottom].append(prev) else: bar_style = dict(style, **style_map.get(label, {})) with abbreviated_exception(): bar_style = self._apply_transforms(el, ranges, bar_style) bar_data[label] = { x:[xval], y: [val], w: width, bottom: [prev], 'label': label, } bar_data[label].update(bar_style) prev += val if isfinite(val) else 0 if label is not None: labels.append(label) # Draw bars bars = [getattr(axis, plot_fn)(**bar_spec) for bar_spec in bar_data.values()] # Generate legend and axis labels ax_dims = [gdim] title = '' if sdim: title = sdim.pprint_label ax_dims.append(sdim) elif cdim: title = cdim.pprint_label if self.multi_level: ax_dims.append(cdim) if self.show_legend and any(len(l) for l in labels) and (sdim or not self.multi_level): leg_spec = self.legend_specs[self.legend_position] if self.legend_cols: leg_spec['ncol'] = self.legend_cols legend_opts = self.legend_opts.copy() legend_opts.update(**leg_spec) axis.legend(title=title, **legend_opts) return bars, xticks, ax_dims
[docs]class SpikesPlot(SpikesMixin, PathPlot, ColorbarPlot): aspect = param.Parameter(default='square', doc=""" The aspect ratio mode of the plot. Allows setting an explicit aspect ratio as width/height as well as 'square' and 'equal' options.""") color_index = param.ClassSelector(default=None, allow_None=True, class_=(str, int), doc=""" Index of the dimension from which the color will the drawn""") spike_length = param.Number(default=0.1, doc=""" The length of each spike if Spikes object is one dimensional.""") padding = param.ClassSelector(default=(0, 0.1), class_=(int, float, tuple)) position = param.Number(default=0., doc=""" The position of the lower end of each spike.""") style_opts = PathPlot.style_opts + ['cmap']
[docs] def init_artists(self, ax, plot_args, plot_kwargs): if 'c' in plot_kwargs: plot_kwargs['array'] = plot_kwargs.pop('c') if 'vmin' in plot_kwargs and 'vmax' in plot_kwargs: plot_kwargs['clim'] = plot_kwargs.pop('vmin'), plot_kwargs.pop('vmax') if "array" not in plot_kwargs and 'cmap' in plot_kwargs: del plot_kwargs['cmap'] line_segments = LineCollection(*plot_args, **plot_kwargs) ax.add_collection(line_segments) return {'artist': line_segments}
def get_data(self, element, ranges, style): dimensions = element.dimensions(label=True) ndims = len(dimensions) opts = self.lookup_options(element, 'plot').options pos = self.position if ndims > 1 and 'spike_length' not in opts: data = element.columns([0, 1]) xs, ys = data[dimensions[0]], data[dimensions[1]] data = [[(x, pos), (x, pos+y)] for x, y in zip(xs, ys)] else: xs = element.array([0]) height = self.spike_length data = [[(x[0], pos), (x[0], pos+height)] for x in xs] if self.invert_axes: data = [(line[0][::-1], line[1][::-1]) for line in data] dims = element.dimensions() clean_spikes = [] for spike in data: xs, ys = zip(*spike) cols = [] for i, vs in enumerate((xs, ys)): vs = np.array(vs) if isdatetime(vs): dt_format = Dimension.type_formatters.get( type(vs[0]), Dimension.type_formatters[np.datetime64] ) vs = date2num(vs) dims[i] = dims[i].clone(value_format=DateFormatter(dt_format)) cols.append(vs) clean_spikes.append(np.column_stack(cols)) cdim = element.get_dimension(self.color_index) color = style.get('color', None) if cdim and ((isinstance(color, str) and color in element) or isinstance(color, dim)): self.param.warning( "Cannot declare style mapping for 'color' option and " "declare a color_index; ignoring the color_index.") cdim = None if cdim: style['array'] = element.dimension_values(cdim) self._norm_kwargs(element, ranges, style, cdim) if 'spike_length' in opts: axis_dims = (element.dimensions()[0], None) elif len(element.dimensions()) == 1: axis_dims = (element.dimensions()[0], None) else: axis_dims = (element.dimensions()[0], element.dimensions()[1]) with abbreviated_exception(): style = self._apply_transforms(element, ranges, style) return (clean_spikes,), style, {'dimensions': axis_dims}
[docs] def update_handles(self, key, axis, element, ranges, style): artist = self.handles['artist'] (data,), kwargs, axis_kwargs = self.get_data(element, ranges, style) artist.set_paths(data) artist.set_visible(style.get('visible', True)) if 'color' in kwargs: artist.set_edgecolors(kwargs['color']) if 'array' in kwargs or 'c' in kwargs: artist.set_array(kwargs.get('array', kwargs.get('c'))) if 'vmin' in kwargs: artist.set_clim((kwargs['vmin'], kwargs['vmax'])) if 'norm' in kwargs: artist.norm = kwargs['norm'] if 'linewidth' in kwargs: artist.set_linewidths(kwargs['linewidth']) return axis_kwargs
[docs]class SideSpikesPlot(AdjoinedPlot, SpikesPlot): bgcolor = param.Parameter(default=(1, 1, 1, 0), doc=""" Make plot background invisible.""") border_size = param.Number(default=0, doc=""" The size of the border expressed as a fraction of the main plot.""") subplot_size = param.Number(default=0.1, doc=""" The size subplots as expressed as a fraction of the main plot.""") spike_length = param.Number(default=1, doc=""" The length of each spike if Spikes object is one dimensional.""") xaxis = param.ObjectSelector(default='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='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'.""")