Source code for holoviews.plotting.bokeh.chart

from collections import defaultdict

import numpy as np
import param
from bokeh.models import (DataRange1d, CategoricalColorMapper, CustomJS,
                          HoverTool, FactorRange, Whisker, Band)
from bokeh.models.tools import BoxSelectTool

from ...core import Dataset, OrderedDict
from ...core.dimension import Dimension
from ...core.util import (max_range, basestring, dimension_sanitizer,
                          wrap_tuple, unique_iterator)
from ...element import Bars
from ...operation import interpolate_curve
from ..util import compute_sizes, get_min_distance, dim_axis_label
from .element import (ElementPlot, ColorbarPlot, LegendPlot, CompositeElementPlot,
                      line_properties, fill_properties)
from .path import PathPlot, PolygonPlot
from .util import expand_batched_style, categorize_array, rgb2hex, mpl_to_bokeh


class PointPlot(LegendPlot, ColorbarPlot):

    color_index = param.ClassSelector(default=None, class_=(basestring, int),
                                      allow_None=True, doc="""
      Index of the dimension from which the color will the drawn""")

    size_index = param.ClassSelector(default=None, class_=(basestring, int),
                                     allow_None=True, doc="""
      Index of the dimension from which the sizes will the drawn.""")

    scaling_method = param.ObjectSelector(default="area",
                                          objects=["width", "area"],
                                          doc="""
      Determines whether the `scaling_factor` should be applied to
      the width or area of each point (default: "area").""")

    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 = (['cmap', 'palette', 'marker', 'size'] +
                  line_properties + fill_properties)

    _plot_methods = dict(single='scatter', batched='scatter')
    _batched_style_opts = line_properties + fill_properties + ['size']

    def _get_size_data(self, element, ranges, style):
        data, mapping = {}, {}
        sdim = element.get_dimension(self.size_index)
        if not sdim or self.static_source:
            return data, mapping
        
        map_key = 'size_' + sdim.name
        ms = style.get('size', np.sqrt(6))**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.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


    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:
            xdim, ydim = dims[xidx], dims[yidx]
            data[xdim] = element.dimension_values(xidx)
            data[ydim] = element.dimension_values(yidx)
            self._categorize_data(data, (xdim, ydim), 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)

        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)
        for (key, el), zorder in zip(element.data.items(), zorders):
            self.set_param(**self.lookup_options(el, 'plot').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)
            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)
            elmapping.update(smapping)
            for k, v in sdata.items():
                data[k].append(v)

            if any(isinstance(t, HoverTool) for t in self.state.tools):
                for dim, k in zip(element.dimensions(), key):
                    sanitized = dimension_sanitizer(dim.name)
                    data[sanitized].append([k]*nvals)

        data = {k: np.concatenate(v) for k, v in data.items()}
        return data, elmapping, style



class VectorFieldPlot(ColorbarPlot):

    arrow_heads = param.Boolean(default=True, doc="""
       Whether or not to draw arrow heads.""")

    color_index = param.ClassSelector(default=None, class_=(basestring, int),
                                      allow_None=True, doc="""
      Index of the dimension from which the color will the drawn""")

    size_index = param.ClassSelector(default=None, class_=(basestring, int),
                                     allow_None=True, doc="""
      Index of the dimension from which the sizes will the drawn.""")

    normalize_lengths = param.Boolean(default=True, doc="""
       Whether to normalize vector magnitudes automatically. If False,
       it will be assumed that the lengths have already been correctly
       normalized.""")

    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.""")

    style_opts = line_properties + ['scale', 'cmap']
    _plot_methods = dict(single='segment')

    def _get_lengths(self, element, ranges):
        mag_dim = element.get_dimension(self.size_index)
        (x0, x1), (y0, y1) = (element.range(i) for i in range(2))
        base_dist = get_min_distance(element)
        if mag_dim:
            magnitudes = element.dimension_values(mag_dim)
            _, max_magnitude = ranges[mag_dim.name]
            if self.normalize_lengths and max_magnitude != 0:
                magnitudes = magnitudes / max_magnitude
            if self.rescale_lengths:
                magnitudes *= base_dist
        else:
            magnitudes = np.ones(len(element))
            if self.rescale_lengths:
                magnitudes *= base_dist

        return magnitudes

    def _glyph_properties(self, *args):
        properties = super(VectorFieldPlot, self)._glyph_properties(*args)
        properties.pop('scale', None)
        return properties


    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 = rads+1.5*np.pi
        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)

        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.concatenate([x0s, x0s, x0s])
            x1s = np.concatenate([x1s, xa1s, xa2s])
            y0s = np.concatenate([y0s, y0s, y0s])
            y1s = np.concatenate([y1s, ya1s, ya2s])
            if cdim:
                color = cdata.get(cdim.name)
                color = np.concatenate([color, color, color])
        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:
            data[cdim.name] = color
            mapping.update(cmapping)

        return (data, mapping, style)



class CurvePlot(ElementPlot):

    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'.""")

    style_opts = line_properties
    _plot_methods = dict(single='line', batched='multi_line')
    _batched_style_opts = line_properties

    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:
            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):
            self.set_param(**self.lookup_options(el, 'plot').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



class HistogramPlot(ElementPlot):

    style_opts = line_properties + fill_properties
    _plot_methods = dict(single='quad')

    def get_data(self, element, ranges, style):
        if self.invert_axes:
            mapping = dict(top='left', bottom='right', 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:
            data = dict(top=element.values, left=element.edges[:-1],
                        right=element.edges[1:])
        self._get_hover_data(data, element)
        return (data, mapping, style)

    def get_extents(self, element, ranges):
        x0, y0, x1, y1 = super(HistogramPlot, self).get_extents(element, ranges)
        ylow, yhigh = element.get_dimension(1).range
        y0 = np.nanmin([0, y0]) if ylow is None or not np.isfinite(ylow) else ylow
        y1 = np.nanmax([0, y1]) if yhigh is None or not np.isfinite(yhigh) else yhigh
        return (x0, y0, x1, y1)



class SideHistogramPlot(ColorbarPlot, 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', 'ybox_select'],
        doc="A list of plugin tools to use on the plot.")

    _callback = """
    color_mapper.low = cb_data['geometry']['y0'];
    color_mapper.high = cb_data['geometry']['y1'];
    source.trigger('change')
    main_source.trigger('change')
    """

    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:
            data = dict(top=element.values, left=element.edges[:-1],
                        right=element.edges[1:])

        color_dims = [d for d in self.adjoined.traverse(lambda x: x.handles.get('color_dim'))
                      if d is not None]
        dim = color_dims[0] if color_dims else None
        cmapper = self._get_colormapper(dim, element, {}, {})
        if cmapper and dim in element.dimensions():
            data[dim.name] = [] if self.static_source else element.dimension_values(dim)
            mapping['fill_color'] = {'field': dim.name,
                                     'transform': cmapper}
        self._get_hover_data(data, element)
        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
        box_select, main_source = tools[0], sources[0]
        handles = {'color_mapper': self.handles['color_mapper'],
                   'source': self.handles['source'],
                   'main_source': main_source}
        if box_select.callback:
            box_select.callback.code += self._callback
            box_select.callback.args.update(handles)
        else:
            box_select.callback = CustomJS(args=handles, code=self._callback)
        return ret



class ErrorPlot(ElementPlot):

    style_opts = line_properties

    _mapping = dict(base="base", upper="upper", lower="lower")

    _plot_methods = dict(single=Whisker)

    def get_data(self, element, ranges, style):
        mapping = dict(self._mapping)
        if self.static_source:
            return {}, mapping, style

        base = element.dimension_values(0)
        ys = element.dimension_values(1)
        if len(element.vdims) > 2:
            neg, pos = (element.dimension_values(vd) for vd in element.vdims[1:3])
            lower, upper = ys-neg, ys+pos
        else:
            err = element.dimension_values(2)
            lower, upper = ys-err, ys+err
        data = dict(base=base, lower=lower, upper=upper)

        if self.invert_axes:
            mapping['dimension'] = 'width'
        else:
            mapping['dimension'] = 'height'
        self._categorize_data(data, ('base',), element.dimensions())
        return (data, mapping, style)


    def _init_glyph(self, plot, mapping, properties):
        """
        Returns a Bokeh glyph object.
        """
        properties.pop('legend', None)
        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



class SpreadPlot(ErrorPlot):

    style_opts = line_properties + fill_properties
    _plot_methods = dict(single=Band)



class AreaPlot(SpreadPlot):

    def get_extents(self, element, ranges):
        vdims = element.vdims
        vdim = vdims[0].name
        if len(vdims) > 1:
            ranges[vdim] = max_range([ranges[vd.name] for vd in vdims])
        else:
            vdim = vdims[0].name
            ranges[vdim] = (np.nanmin([0, ranges[vdim][0]]), ranges[vdim][1])
        return super(AreaPlot, self).get_extents(element, ranges)

    def get_data(self, element, ranges, style):
        mapping = dict(self._mapping)
        if self.static_source:
            return {}, mapping, style

        xs = element.dimension_values(0)
        if len(element.vdims) > 1:
            lower = element.dimension_values(2)
        else:
            lower = np.zeros(len(element))
        upper = element.dimension_values(1)
        data = dict(base=xs, upper=upper, lower=lower)

        if self.invert_axes:
            mapping['dimension'] = 'width'
        else:
            mapping['dimension'] = 'height'
        return data, mapping, style



class SpikesPlot(ColorbarPlot):

    color_index = param.ClassSelector(default=None, allow_None=True,
                                      class_=(basestring, int), doc="""
      Index of the dimension from which the color will the drawn""")

    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.""")

    style_opts = (['color', 'cmap', 'palette'] + line_properties)

    _plot_methods = dict(single='segment')

    def get_extents(self, element, ranges):
        l, b, r, t = super(SpikesPlot, self).get_extents(element, ranges)
        if len(element.dimensions()) == 1:
            if self.batched:
                bs, ts = [], []
                # Iterate over current NdOverlay and compute extents
                # from position and length plot options
                frame = self.current_frame or self.hmap.last 
                for el in frame.values():
                    opts = self.lookup_options(el, 'plot').options
                    pos = opts.get('position', self.position)
                    length = opts.get('spike_length', self.spike_length)
                    bs.append(pos)
                    ts.append(pos+length)
                b = np.nanmin(bs)
                t = np.nanmax(ts)
            else:
                b, t = self.position, self.position+self.spike_length
        else:
            b = np.nanmin([0, b])
            t = np.nanmax([0, t])
        return l, b, r, t

    def get_data(self, element, ranges, style):
        dims = element.dimensions(label=True)

        data = {}
        pos = self.position
        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:
                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'}
        cdim = element.get_dimension(self.color_index)
        if cdim:
            cmapper = self._get_colormapper(cdim, element, ranges, style)
            data[cdim.name] = [] if self.static_source else element.dimension_values(cdim)
            mapping['color'] = {'field': cdim.name,
                                'transform': cmapper}

        if any(isinstance(t, HoverTool) for t in self.state.tools) and not self.static_source:
            for d in dims:
                data[dimension_sanitizer(d)] = element.dimension_values(d)

        return data, mapping, style


[docs]class SideSpikesPlot(SpikesPlot): """ SpikesPlot with useful defaults for plotting adjoined rug plot. """ 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(ColorbarPlot, LegendPlot): """ BarPlot allows generating single- or multi-category bar Charts, by selecting which key dimensions are mapped onto separate groups, categories and stacks. """ color_index = param.ClassSelector(default=None, class_=(basestring, int), allow_None=True, doc=""" Index of the dimension from which the color will the drawn""") group_index = param.ClassSelector(default=1, class_=(basestring, int), allow_None=True, doc=""" Index of the dimension in the supplied Bars Element, which will be laid out into groups.""") stack_index = param.ClassSelector(default=None, class_=(basestring, int), allow_None=True, doc=""" Index of the dimension in the supplied Bars Element, which will stacked.""") style_opts = line_properties + fill_properties + ['width', 'cmap'] _plot_methods = dict(single=('vbar', 'hbar')) # Declare that y-range should auto-range if not bounded _y_range_type = DataRange1d
[docs] def get_extents(self, element, ranges): """ Make adjustments to plot extents by computing stacked bar heights, adjusting the bar baseline and forcing the x-axis to be categorical. """ if self.batched: overlay = self.current_frame element = Bars(overlay.table(), kdims=element.kdims+overlay.kdims, vdims=element.vdims) for kd in overlay.kdims: ranges[kd.name] = overlay.range(kd) stacked = element.get_dimension(self.stack_index) extents = super(BarPlot, self).get_extents(element, ranges) xdim = element.kdims[0] ydim = element.vdims[0] # Compute stack heights if stacked: ds = Dataset(element) pos_range = ds.select(**{ydim.name: (0, None)}).aggregate(xdim, function=np.sum).range(ydim) neg_range = ds.select(**{ydim.name: (None, 0)}).aggregate(xdim, function=np.sum).range(ydim) y0, y1 = max_range([pos_range, neg_range]) else: y0, y1 = ranges[ydim.name] # Set y-baseline if y0 < 0: y1 = max([y1, 0]) elif self.logy: y0 = (ydim.range[0] or (10**(np.log10(y1)-2)) if y1 else 0.01) else: y0 = 0 # Ensure x-axis is picked up as categorical x0 = xdim.pprint_value(extents[0]) x1 = xdim.pprint_value(extents[2]) return (x0, y0, x1, y1)
def _get_factors(self, element): """ Get factors for categorical axes. """ gdim = element.get_dimension(self.group_index) if gdim not in element.kdims: gdim = None sdim = element.get_dimension(self.stack_index) if sdim not in element.kdims: sdim = None xdim, ydim = element.dimensions()[:2] xvals = element.dimension_values(0, False) xvals = [x if xvals.dtype.kind in 'SU' else xdim.pprint_value(x) for x in xvals] if gdim and not sdim: gvals = element.dimension_values(gdim, False) gvals = [g if gvals.dtype.kind in 'SU' else gdim.pprint_value(g) for g in gvals] coords = ([(x, g) for x in xvals for g in gvals], []) else: coords = (xvals, []) if self.invert_axes: coords = coords[::-1] return coords def _get_axis_labels(self, *args, **kwargs): """ Override axis mapping by setting the first key and value dimension as the x-axis and y-axis labels. """ element = self.current_frame if self.batched: element = element.last xlabel = dim_axis_label(element.kdims[0]) gdim = element.get_dimension(self.group_index) if gdim and gdim in element.kdims: xlabel = ', '.join([xlabel, dim_axis_label(gdim)]) return (xlabel, dim_axis_label(element.vdims[0]), None)
[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): props = super(BarPlot, self)._glyph_properties(*args) del props['width'] return props def _add_color_data(self, ds, ranges, style, cdim, data, mapping, factors, colors): # Get colormapper 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'] if ('color' in cmapping and self.show_legend and isinstance(cmapper, CategoricalColorMapper)): mapping['legend'] = cdim.name # Merge data and mappings mapping.update(cmapping) for k, cd in cdata.items(): if self.color_index is None and cd.dtype.kind in 'if': cd = categorize_array(cd, color_dim) 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 def get_data(self, element, ranges, style): # Get x, y, group, stack and color dimensions grouping = None group_dim = element.get_dimension(self.group_index) if group_dim not in element.kdims: group_dim = None else: grouping = 'grouped' stack_dim = element.get_dimension(self.stack_index) if stack_dim not in element.kdims: stack_dim = None else: grouping = 'stacked' group_dim = None 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('width', 1) cmap = style.get('cmap') hover = any(t == 'hover' or isinstance(t, HoverTool) for t in self.tools+self.default_tools) # 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, (None, None)) 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 cvals = element.dimension_values(cdim, expanded=False) if cdim else None if cvals is not None: if cvals.dtype.kind in 'if' and no_cidx: cvals = categorize_array(cvals, color_dim) factors = None if cvals.dtype.kind in 'if' 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}) 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) 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 not grouping == 'stacked': 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
class BoxWhiskerPlot(CompositeElementPlot, ColorbarPlot, LegendPlot): color_index = param.ClassSelector(default=None, class_=(basestring, int), allow_None=True, doc=""" Index of the dimension from which the color will the drawn""") show_legend = param.Boolean(default=False, doc=""" Whether to show legend for the plot.""") # X-axis is categorical _x_range_type = FactorRange # Declare that y-range should auto-range if not bounded _y_range_type = DataRange1d # Map each glyph to a style group _style_groups = {'rect': 'whisker', 'segment': 'whisker', 'vbar': 'box', 'hbar': 'box', 'circle': 'outlier'} style_opts = (['whisker_'+p for p in line_properties] +\ ['box_'+p for p in fill_properties+line_properties] +\ ['outlier_'+p for p in fill_properties+line_properties] + ['width', 'cmap']) _stream_data = False # Plot does not support streaming data def get_extents(self, element, ranges): """ Extents are set to '' and None because x-axis is categorical and y-axis auto-ranges. """ yrange = element.range(element.vdims[0], data_range=False) return ('', yrange[0], '', yrange[1]) def _get_axis_labels(self, *args, **kwargs): """ Override axis labels to group all key dimensions together. """ element = self.current_frame xlabel = ', '.join([kd.pprint_label for kd in element.kdims]) ylabel = element.vdims[0].pprint_label return xlabel, ylabel, None def _glyph_properties(self, plot, element, source, ranges, style): properties = dict(style, source=source) if self.show_legend and not element.kdims: properties['legend'] = element.label return properties def _get_factors(self, element): """ Get factors for categorical axes. """ if not element.kdims: xfactors, yfactors = [element.label], [] else: factors = [tuple(d.pprint_value(v) for d, v in zip(element.kdims, key)) for key in element.groupby(element.kdims).data.keys()] factors = [f[0] if len(f) == 1 else f for f in factors] xfactors, yfactors = factors, [] return (yfactors, xfactors) if self.invert_axes else (xfactors, yfactors) def get_data(self, element, ranges, style): if element.kdims: groups = element.groupby(element.kdims).data else: groups = dict([(element.label, element)]) vdim = dimension_sanitizer(element.vdims[0].name) # Define CDS data r1_data, r2_data = ({'index': [], 'top': [], 'bottom': []} for i in range(2)) s1_data, s2_data = ({'x0': [], 'y0': [], 'x1': [], 'y1': []} for i in range(2)) w1_data, w2_data = ({'index': [], vdim: []} for i in range(2)) out_data = defaultdict(list, {'index': [], vdim: []}) # Define glyph-data mapping width = style.get('width', 0.7) if self.invert_axes: vbar_map = {'y': 'index', 'left': 'top', 'right': 'bottom', 'height': width} seg_map = {'y0': 'x0', 'y1': 'x1', 'x0': 'y0', 'x1': 'y1'} whisk_map = {'y': 'index', 'x': vdim, 'height': 0.2, 'width': 0.001} out_map = {'y': 'index', 'x': vdim} else: vbar_map = {'x': 'index', 'top': 'top', 'bottom': 'bottom', 'width': width} seg_map = {'x0': 'x0', 'x1': 'x1', 'y0': 'y0', 'y1': 'y1'} whisk_map = {'x': 'index', 'y': vdim, 'width': 0.2, 'height': 0.001} out_map = {'x': 'index', 'y': vdim} vbar2_map = dict(vbar_map) # Get color values if self.color_index is not None: cdim = element.get_dimension(self.color_index) cidx = element.get_dimension_index(self.color_index) else: cdim, cidx = None, None factors = [] for key, g in groups.items(): # Compute group label if element.kdims: label = tuple(d.pprint_value(v) for d, v in zip(element.kdims, key)) if len(label) == 1: label = label[0] else: label = key # Add color factor if cidx is not None and cidx<element.ndims: factors.append(cdim.pprint_value(wrap_tuple(key)[cidx])) else: factors.append(label) # Compute statistics vals = g.dimension_values(g.vdims[0]) if len(vals): qmin, q1, q2, q3, qmax = (np.percentile(vals, q=q) for q in range(0,125,25)) iqr = q3 - q1 upper = min(q3 + 1.5*iqr, vals.max()) lower = max(q1 - 1.5*iqr, vals.min()) else: qmin, q1, q2, q3, qmax = 0, 0, 0, 0, 0 lower, upper = 0, 0 outliers = vals[(vals>upper) | (vals<lower)] # Add to CDS data for data in [r1_data, r2_data, w1_data, w2_data]: data['index'].append(label) for data in [s1_data, s2_data]: data['x0'].append(label) data['x1'].append(label) r1_data['top'].append(q2) r2_data['top'].append(q1) r1_data['bottom'].append(q3) r2_data['bottom'].append(q2) s1_data['y0'].append(upper) s2_data['y0'].append(lower) s1_data['y1'].append(q3) s2_data['y1'].append(q1) w1_data[vdim].append(lower) w2_data[vdim].append(upper) if len(outliers): out_data['index'] += [label]*len(outliers) out_data[vdim] += list(outliers) if any(isinstance(t, HoverTool) for t in self.state.tools): for kd, k in zip(element.kdims, wrap_tuple(key)): out_data[dimension_sanitizer(kd.name)] += [k]*len(outliers) # Define combined data and mappings bar_glyph = 'hbar' if self.invert_axes else 'vbar' data = { bar_glyph+'_1': r1_data, bar_glyph+'_2': r2_data, 'segment_1': s1_data, 'segment_2': s2_data, 'rect_1': w1_data, 'rect_2': w2_data, 'circle': out_data } mapping = { bar_glyph+'_1': vbar_map, bar_glyph+'_2': vbar2_map, 'segment_1': seg_map, 'segment_2': seg_map, 'rect_1': whisk_map, 'rect_2': whisk_map, 'circle': out_map } # Cast data to arrays to take advantage of base64 encoding for gdata in [r1_data, r2_data, s1_data, s2_data, w1_data, w2_data, out_data]: for k, values in gdata.items(): gdata[k] = np.array(values) # Return if not grouped if not element.kdims: return data, mapping, style # Define color dimension and data if cidx is None or cidx>=element.ndims: cdim = Dimension('index') else: r1_data[dimension_sanitizer(cdim.name)] = factors r2_data[dimension_sanitizer(cdim.name)] = factors factors = list(unique_iterator(factors)) # Get colors and define categorical colormapper cname = dimension_sanitizer(cdim.name) cmap = style.get('cmap') if cmap is None: cycle_style = self.lookup_options(element, 'style') styles = cycle_style.max_cycles(len(factors)) colors = [styles[i].get('box_color', styles[i]['box_fill_color']) for i in range(len(factors))] colors = [rgb2hex(c) if isinstance(c, tuple) else c for c in colors] else: colors = None mapper = self._get_colormapper(cdim, element, ranges, style, factors, colors) vbar_map['fill_color'] = {'field': cname, 'transform': mapper} vbar2_map['fill_color'] = {'field': cname, 'transform': mapper} if self.show_legend: vbar_map['legend'] = cdim.name return data, mapping, style