<commit_before>from io import BytesIO

import numpy as np
import bokeh
import bokeh.plotting
from bokeh.models import Range, HoverTool, Renderer
from bokeh.models.tickers import Ticker, BasicTicker, FixedTicker
from bokeh.models.widgets import Panel, Tabs

from bokeh.models.mappers import LinearColorMapper
try:
    from bokeh.models import ColorBar
    from bokeh.models.mappers import LogColorMapper
except ImportError:
    LogColorMapper, ColorBar = None, None
from bokeh.models import LogTicker, BasicTicker

try:
    from bokeh import mpl
except ImportError:
    mpl = None
import param

from ...core import (Store, HoloMap, Overlay, DynamicMap,
                     CompositeOverlay, Element)
from ...core.options import abbreviated_exception
from ...core import util
from ...element import RGB
from ..plot import GenericElementPlot, GenericOverlayPlot
from ..util import dynamic_update
from .callbacks import Callbacks
from .plot import BokehPlot
from .util import (mpl_to_bokeh, convert_datetime, update_plot,
                   bokeh_version, mplcmap_to_palette)

if bokeh_version >= '0.12':
    from bokeh.models import FuncTickFormatter
else:
    FuncTickFormatter = None


# Define shared style properties for bokeh plots
line_properties = ['line_width', 'line_color', 'line_alpha',
                   'line_join', 'line_cap', 'line_dash']

fill_properties = ['fill_color', 'fill_alpha']

text_properties = ['text_font', 'text_font_size', 'text_font_style', 'text_color',
                   'text_alpha', 'text_align', 'text_baseline']

legend_dimensions = ['label_standoff', 'label_width', 'label_height', 'glyph_width',
                     'glyph_height', 'legend_padding', 'legend_spacing']



class ElementPlot(BokehPlot, GenericElementPlot):

    callbacks = param.ClassSelector(class_=Callbacks, doc="""
        Callbacks object defining any javascript callbacks applied
        to the plot.""")

    bgcolor = param.Parameter(default='white', doc="""
        Background color of the plot.""")

    border = param.Number(default=10, doc="""
        Minimum border around plot.""")

    fontsize = param.Parameter(default={'title': '12pt'}, allow_None=True,  doc="""
       Specifies various fontsizes of the displayed text.

       Finer control is available by supplying a dictionary where any
       unmentioned keys reverts to the default sizes, e.g:

          {'ticks': '20pt', 'title': '15pt', 'ylabel': '5px', 'xlabel': '5px'}""")

    invert_axes = param.Boolean(default=False, doc="""
        Whether to invert the x- and y-axis""")

    invert_xaxis = param.Boolean(default=False, doc="""
        Whether to invert the plot x-axis.""")

    invert_yaxis = param.Boolean(default=False, doc="""
        Whether to invert the plot y-axis.""")

    lod = param.Dict(default={'factor': 10, 'interval': 300,
                              'threshold': 2000, 'timeout': 500}, doc="""
        Bokeh plots offer "Level of Detail" (LOD) capability to
        accomodate large (but not huge) amounts of data. The available
        options are:

          * factor    - Decimation factor to use when applying
                        decimation.
          * interval  - Interval (in ms) downsampling will be enabled
                        after an interactive event.
          * threshold - Number of samples before downsampling is enabled.
          * timeout   - Timeout (in ms) for checking whether interactive
                        tool events are still occurring.""")

    show_grid = param.Boolean(default=True, doc="""
        Whether to show a Cartesian grid on the plot.""")

    show_legend = param.Boolean(default=True, doc="""
        Whether to show legend for the plot.""")

    shared_axes = param.Boolean(default=True, doc="""
        Whether to invert the share axes across plots
        for linked panning and zooming.""")

    default_tools = param.List(default=['save', 'pan', 'wheel_zoom',
                                        'box_zoom', 'reset'],
        doc="A list of plugin tools to use on the plot.")

    tools = param.List(default=[], doc="""
        A list of plugin tools to use on the plot.""")

    toolbar = param.ObjectSelector(default='right',
                                   objects=["above", "below",
                                            "left", "right", None],
                                   doc="""
        The toolbar location, must be one of 'above', 'below',
        'left', 'right', None.""")

    xaxis = param.ObjectSelector(default='bottom',
                                 objects=['top', 'bottom', 'bare', 'top-bare',
                                          'bottom-bare', None], doc="""
        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'.""")

    logx = param.Boolean(default=False, doc="""
        Whether the x-axis of the plot will be a log axis.""")

    xrotation = param.Integer(default=None, bounds=(0, 360), doc="""
        Rotation angle of the xticks.""")

    xticks = param.Parameter(default=None, doc="""
        Ticks along x-axis specified as an integer, explicit list of
        tick locations or bokeh Ticker object. If set to None default
        bokeh ticking behavior is applied.""")

    yaxis = param.ObjectSelector(default='left',
                                      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'.""")

    logy = param.Boolean(default=False, doc="""
        Whether the y-axis of the plot will be a log axis.""")

    yrotation = param.Integer(default=None, bounds=(0, 360), doc="""
        Rotation angle of the yticks.""")

    yticks = param.Parameter(default=None, doc="""
        Ticks along y-axis specified as an integer, explicit list of
        tick locations or bokeh Ticker object. If set to None
        default bokeh ticking behavior is applied.""")

    # The plot objects to be updated on each frame
    # Any entries should be existing keys in the handles
    # instance attribute.
    _update_handles = ['source', 'glyph']

    def __init__(self, element, plot=None, show_labels=['x', 'y'], **params):
        self.show_labels = show_labels
        self.current_ranges = None
        super(ElementPlot, self).__init__(element, **params)
        self.handles = {} if plot is None else self.handles['plot']
        element_ids = self.hmap.traverse(lambda x: id(x), [Element])
        self.static = len(set(element_ids)) == 1 and len(self.keys) == len(self.hmap)


    def _init_tools(self, element):
        """
        Processes the list of tools to be supplied to the plot.
        """
        tools = self.default_tools + self.tools
        if 'hover' in tools:
            tooltips = [(d.pprint_label, '@'+util.dimension_sanitizer(d.name))
                        for d in element.dimensions()]
            tools[tools.index('hover')] = HoverTool(tooltips=tooltips)
        return tools


    def _get_hover_data(self, data, element, empty=False):
        """
        Initializes hover data based on Element dimension values.
        If empty initializes with no data.
        """
        if 'hover' in self.default_tools + self.tools:
            for d in element.dimensions(label=True):
                sanitized = util.dimension_sanitizer(d)
                data[sanitized] = [] if empty else element.dimension_values(d)


    def _axes_props(self, plots, subplots, element, ranges):
        # Get the bottom layer and range element
        el = element.traverse(lambda x: x, [Element])
        el = el[0] if el else element

        dims = el.dimensions()
        xlabel, ylabel, zlabel = self._get_axis_labels(dims)
        if self.invert_axes:
            xlabel, ylabel = ylabel, xlabel

        plot_ranges = {}
        # Try finding shared ranges in other plots in the same Layout
        if plots and self.shared_axes:
            for plot in plots:
                if plot is None or not hasattr(plot, 'xaxis'): continue
                if plot.xaxis[0].axis_label == xlabel:
                    plot_ranges['x_range'] = plot.x_range
                if plot.xaxis[0].axis_label == ylabel:
                    plot_ranges['y_range'] = plot.x_range
                if plot.yaxis[0].axis_label == ylabel:
                    plot_ranges['y_range'] = plot.y_range
                if plot.yaxis[0].axis_label == xlabel:
                    plot_ranges['x_range'] = plot.y_range

        if el.get_dimension_type(0) is np.datetime64:
            x_axis_type = 'datetime'
        else:
            x_axis_type = 'log' if self.logx else 'auto'

        if len(dims) > 1 and el.get_dimension_type(1) is np.datetime64:
            y_axis_type = 'datetime'
        else:
            y_axis_type = 'log' if self.logy else 'auto'

        # Get the Element that determines the range and get_extents
        range_el = el if self.batched and not isinstance(self, OverlayPlot) else element
        l, b, r, t = self.get_extents(range_el, ranges)

        if not 'x_range' in plot_ranges:
            if 'x_range' in ranges:
                plot_ranges['x_range'] = ranges['x_range']
            else:
                low, high = (b, t) if self.invert_axes else (l, r)
                if x_axis_type == 'datetime':
                    low = convert_datetime(low)
                    high = convert_datetime(high)
                elif low == high and low is not None:
                    offset = low*0.1 if low else 0.5
                    low -= offset
                    high += offset
                if all(x is not None and np.isfinite(x) for x in (low, high)):
                    plot_ranges['x_range'] = [low, high]

        if self.invert_xaxis:
            plot_ranges['x_ranges'] = plot_ranges['x_ranges'][::-1]

        if not 'y_range' in plot_ranges:
            if 'y_range' in ranges:
                plot_ranges['y_range'] = ranges['y_range']
            else:
                low, high = (l, r) if self.invert_axes else (b, t)
                if y_axis_type == 'datetime':
                    low = convert_datetime(low)
                    high = convert_datetime(high)
                elif low == high and low is not None:
                    offset = low*0.1 if low else 0.5
                    low -= offset
                    high += offset
                if all(y is not None and np.isfinite(y) for y in (low, high)):
                    plot_ranges['y_range'] = [low, high]
        if self.invert_yaxis:
            yrange = plot_ranges['y_range']
            if isinstance(yrange, Range):
                plot_ranges['y_range'] = yrange.__class__(start=yrange.end,
                                                          end=yrange.start)
            else:
                plot_ranges['y_range'] = yrange[::-1]
        return (x_axis_type, y_axis_type), (xlabel, ylabel, zlabel), plot_ranges


    def _init_plot(self, key, element, plots, ranges=None):
        """
        Initializes Bokeh figure to draw Element into and sets basic
        figure and axis attributes including axes types, labels,
        titles and plot height and width.
        """
        subplots = list(self.subplots.values()) if self.subplots else []

        axis_types, labels, plot_ranges = self._axes_props(plots, subplots, element, ranges)
        xlabel, ylabel, _ = labels
        x_axis_type, y_axis_type = axis_types
        properties = dict(plot_ranges)
        properties['x_axis_label'] = xlabel if 'x' in self.show_labels else ' '
        properties['y_axis_label'] = ylabel if 'y' in self.show_labels else ' '

        if self.show_title:
            title = self._format_title(key, separator=' ')
        else:
            title = ''

        if self.toolbar:
            tools = self._init_tools(element)
            properties['tools'] = tools
            properties['toolbar_location'] = self.toolbar

        properties['webgl'] = Store.renderers[self.renderer.backend].webgl
        return bokeh.plotting.Figure(x_axis_type=x_axis_type,
                                     y_axis_type=y_axis_type, title=title,
                                     **properties)


    def _plot_properties(self, key, plot, element):
        """
        Returns a dictionary of plot properties.
        """
        size_multiplier = Store.renderers[self.renderer.backend].size/100.
        plot_props = dict(plot_height=int(self.height*size_multiplier),
                          plot_width=int(self.width*size_multiplier))
        if bokeh_version < '0.12':
            plot_props.update(self._title_properties(key, plot, element))
        if self.bgcolor:
            plot_props['background_fill_color'] = self.bgcolor
        if self.border is not None:
            for p in ['left', 'right', 'top', 'bottom']:
                plot_props['min_border_'+p] = self.border
        lod = dict(self.defaults()['lod'], **self.lod)
        for lod_prop, v in lod.items():
            plot_props['lod_'+lod_prop] = v
        return plot_props


    def _title_properties(self, key, plot, element):
        if self.show_title:
            title = self._format_title(key, separator=' ')
        else:
            title = ''

        if bokeh_version < '0.12':
            title_font = self._fontsize('title', 'title_text_font_size')
            return dict(title=title, title_text_color='black', **title_font)
        else:
            title_font = self._fontsize('title', 'text_font_size')
            return dict(text=title, text_color='black', **title_font)


    def _init_axes(self, plot):
        if self.xaxis is None:
            plot.xaxis.visible = False
        elif self.xaxis == 'top':
            plot.above = plot.below
            plot.below = []
            plot.xaxis[:] = plot.above

        if self.yaxis is None:
            plot.yaxis.visible = False
        elif self.yaxis == 'right':
            plot.right = plot.left
            plot.left = []
            plot.yaxis[:] = plot.right


    def _axis_properties(self, axis, key, plot, dimension,
                         ax_mapping={'x': 0, 'y': 1}):
        """
        Returns a dictionary of axis properties depending
        on the specified axis.
        """
        axis_props = {}
        if ((axis == 'x' and self.xaxis in ['bottom-bare', 'top-bare']) or
            (axis == 'y' and self.yaxis in ['left-bare', 'right-bare'])):
            axis_props['axis_label'] = ''
            axis_props['major_label_text_font_size'] = '0pt'
            axis_props['major_tick_line_color'] = None
            axis_props['minor_tick_line_color'] = None
        else:
            rotation = self.xrotation if axis == 'x' else self.yrotation
            if rotation:
                axis_props['major_label_orientation'] = np.radians(rotation)
            ticker = self.xticks if axis == 'x' else self.yticks
            if isinstance(ticker, Ticker):
                axis_props['ticker'] = ticker
            elif isinstance(ticker, int):
                axis_props['ticker'] = BasicTicker(desired_num_ticks=ticker)
            elif isinstance(ticker, (tuple, list)):
                if all(isinstance(t, tuple) for t in ticker):
                    pass
                else:
                    axis_props['ticker'] = FixedTicker(ticks=ticker)

        if FuncTickFormatter is not None and ax_mapping and dimension:
            formatter = None
            if dimension.value_format:
                formatter = dimension.value_format
            elif dimension.type in dimension.type_formatters:
                formatter = dimension.type_formatters[dimension.type]
            if formatter:
                msg = ('%s dimension formatter could not be '
                       'converted to tick formatter. ' % dimension.name)
                try:
                    formatter = FuncTickFormatter.from_py_func(formatter)
                except RuntimeError:
                    self.warning(msg+'Ensure Flexx is installed '
                                 '("conda install -c bokeh flexx" or '
                                 '"pip install flexx")')
                except Exception as e:
                    error = 'Pyscript raised an error: {0}'.format(e)
                    error = error.replace('%', '%%')
                    self.warning(msg+error)
                else:
                    axis_props['formatter'] = formatter
        return axis_props


    def _update_plot(self, key, plot, element=None):
        """
        Updates plot parameters on every frame
        """
        el = element.traverse(lambda x: x, [Element])
        dimensions = el[0].dimensions() if el else el.dimensions()
        plot.set(**self._plot_properties(key, plot, element))
        props = {axis: self._axis_properties(axis, key, plot, dim)
                 for axis, dim in zip(['x', 'y'], dimensions)}
        plot.xaxis[0].set(**props.get('x', {}))
        plot.yaxis[0].set(**props.get('y', {}))

        if bokeh_version >= '0.12' and not self.overlaid:
            plot.title.set(**self._title_properties(key, plot, element))

        if not self.show_grid:
            plot.xgrid.grid_line_color = None
            plot.ygrid.grid_line_color = None


    def _update_ranges(self, element, ranges):
        l, b, r, t = self.get_extents(element, ranges)
        plot = self.handles['plot']
        if self.invert_axes:
            l, b, r, t = b, l, t, r
        if l == r:
            offset = abs(l*0.1 if l else 0.5)
            l -= offset
            r += offset
        if b == t:
            offset = abs(b*0.1 if b else 0.5)
            b -= offset
            t += offset

        # Ensure that it never sets a NaN value
        if isinstance(l, np.datetime64) or np.isfinite(l): plot.x_range.start = l
        if isinstance(l, np.datetime64) or np.isfinite(r): plot.x_range.end   = r
        if isinstance(l, np.datetime64) or np.isfinite(b): plot.y_range.start = b
        if isinstance(l, np.datetime64) or np.isfinite(t): plot.y_range.end   = t


    def _process_legend(self):
        """
        Disables legends if show_legend is disabled.
        """
        for l in self.handles['plot'].legend:
            l.legends[:] = []
            l.border_line_alpha = 0
            l.background_fill_alpha = 0


    def _init_glyph(self, plot, mapping, properties):
        """
        Returns a Bokeh glyph object.
        """
        properties = mpl_to_bokeh(properties)
        plot_method = self._plot_methods.get('batched' if self.batched else 'single')
        renderer = getattr(plot, plot_method)(**dict(properties, **mapping))
        return renderer, renderer.glyph


    def _glyph_properties(self, plot, element, source, ranges):
        properties = self.style[self.cyclic_index]

        if self.show_legend:
            if self.overlay_dims:
                legend = ', '.join([d.pprint_value(v) for d, v in
                                    self.overlay_dims.items()])
            else:
                legend = element.label
            properties['legend'] = legend
        properties['source'] = source
        return properties


    def _update_glyph(self, glyph, properties, mapping):
        allowed_properties = glyph.properties()
        properties = mpl_to_bokeh(properties)
        merged = dict(properties, **mapping)
        glyph.set(**{k: v for k, v in merged.items()
                     if k in allowed_properties})


    def initialize_plot(self, ranges=None, plot=None, plots=None, source=None):
        """
        Initializes a new plot object with the last available frame.
        """
        # Get element key and ranges for frame
        element = self.hmap.last
        key = self.keys[-1]
        ranges = self.compute_ranges(self.hmap, key, ranges)
        self.current_ranges = ranges
        self.current_frame = element
        self.current_key = key
        style_element = element.last if self.batched else element
        ranges = util.match_spec(style_element, ranges)

        # Initialize plot, source and glyph
        if plot is None:
            plot = self._init_plot(key, style_element, ranges=ranges, plots=plots)
            self._init_axes(plot)
        self.handles['plot'] = plot

        # Get data and initialize data source
        empty = self.callbacks and self.callbacks.downsample
        if self.batched:
            data, mapping = self.get_batched_data(element, ranges, empty)
        else:
            data, mapping = self.get_data(element, ranges, empty)
        if source is None:
            source = self._init_datasource(data)
        self.handles['source'] = source

        properties = self._glyph_properties(plot, style_element, source, ranges)
        with abbreviated_exception():
            renderer, glyph = self._init_glyph(plot, mapping, properties)
        self.handles['glyph'] = glyph
        if isinstance(renderer, Renderer):
            self.handles['glyph_renderer'] = renderer

        # Update plot, source and glyph
        with abbreviated_exception():
            self._update_glyph(glyph, properties, mapping)
        if not self.overlaid:
            self._update_plot(key, plot, style_element)
        if self.callbacks:
            self.callbacks(self)
            self.callbacks.update(self)
        if not self.overlaid:
            self._process_legend()
        self.drawn = True

        return plot


    def update_frame(self, key, ranges=None, plot=None, element=None, empty=False):
        """
        Updates an existing plot with data corresponding
        to the key.
        """
        reused = isinstance(self.hmap, DynamicMap) and self.overlaid
        if not reused and element is None:
            element = self._get_frame(key)
        else:
            self.current_key = key
            self.current_frame = element

        glyph = self.handles.get('glyph', None)
        if hasattr(glyph, 'visible'):
            glyph.visible = bool(element)
        if not element:
            return

        style_element = element.last if self.batched else element
        self.style = self.lookup_options(style_element, 'style')

        ranges = self.compute_ranges(self.hmap, key, ranges)
        self.set_param(**self.lookup_options(style_element, 'plot').options)
        ranges = util.match_spec(style_element, ranges)
        self.current_ranges = ranges

        plot = self.handles['plot']
        source = self.handles['source']
        empty = (self.callbacks and self.callbacks.downsample) or empty
        if self.batched:
            data, mapping = self.get_batched_data(element, ranges, empty)
        else:
            data, mapping = self.get_data(element, ranges, empty)
        self._update_datasource(source, data)

        if glyph:
            properties = self._glyph_properties(plot, element, source, ranges)
            with abbreviated_exception():
                self._update_glyph(self.handles['glyph'], properties, mapping)
        if not self.overlaid:
            self._update_ranges(style_element, ranges)
            self._update_plot(key, plot, style_element)
        if self.callbacks:
            self.callbacks.update(self)


    @property
    def current_handles(self):
        """
        Returns a list of the plot objects to update.
        """
        handles = []
        if self.static and not self.dynamic:
            return handles

        previous_id = self.handles.get('previous_id', None)
        current_id = id(self.current_frame.data) if self.current_frame else None
        for handle in self._update_handles:
            if (handle == 'source' and self.dynamic and
                current_id == previous_id):
                continue
            if handle in self.handles:
                handles.append(self.handles[handle])

        # Cache frame object id to skip updating if unchanged
        if self.dynamic:
            self.handles['previous_id'] = current_id

        if self.overlaid:
            return handles

        plot = self.state
        handles.append(plot)
        if bokeh_version >= '0.12':
            handles.append(plot.title)

        if self.current_frame:
            if self.framewise or isinstance(self.hmap, DynamicMap):
                handles += [plot.x_range, plot.y_range]
        return handles


    @property
    def framewise(self):
        """
        Property to determine whether the current frame should have
        framewise normalization enabled. Required for bokeh plotting
        classes to determine whether to send updated ranges for each
        frame.
        """
        current_frames = [el for f in self.traverse(lambda x: x.current_frame)
                          for el in (f.traverse(lambda x: x, [Element])
                                     if f else [])]
        return any(self.lookup_options(frame, 'norm').options.get('framewise')
                   for frame in current_frames)



class ColorbarPlot(ElementPlot):
    """
    ColorbarPlot provides methods to create colormappers and colorbar
    models which can be added to a glyph. Additionally it provides
    parameters to control the position and other styling options of
    the colorbar. The default colorbar_position options are defined
    by the colorbar_specs, but may be overridden by the colorbar_opts.
    """

    colorbar_specs = {'right':     {'pos': 'right',
                                    'opts': {'location': (0, 0)}},
                      'left':      {'pos': 'left',
                                    'opts':{'location':(0, 0)}},
                      'bottom':    {'pos': 'below',
                                    'opts': {'location': (0, 0),
                                             'orientation':'horizontal'}},
                      'top':       {'pos': 'above',
                                    'opts': {'location':(0, 0),
                                             'orientation':'horizontal'}},
                      'top_right':   {'pos': 'center',
                                      'opts': {'location': 'top_right'}},
                      'top_left':    {'pos': 'center',
                                      'opts': {'location': 'top_left'}},
                      'bottom_left': {'pos': 'center',
                                      'opts': {'location': 'bottom_left',
                                               'orientation': 'horizontal'}},
                      'bottom_right': {'pos': 'center',
                                      'opts': {'location': 'bottom_right',
                                               'orientation': 'horizontal'}}}

    colorbar = param.Boolean(default=False, doc="""
        Whether to display a colorbar.""")

    colorbar_position = param.ObjectSelector(objects=list(colorbar_specs),
                                             default="right", doc="""
        Allows selecting between a number of predefined colorbar position
        options. The predefined options may be customized in the
        colorbar_specs class attribute.""")

    colorbar_opts = param.Dict(default={}, doc="""
        Allows setting specific styling options for the colorbar overriding
        the options defined in the colorbar_specs class attribute. Includes
        location, orientation, height, width, scale_alpha, title, title_props,
        margin, padding, background_fill_color and more.""")

    logz  = param.Boolean(default=False, doc="""
         Whether to apply log scaling to the z-axis.""")

    _update_handles = ['color_mapper', 'source', 'glyph']

    _colorbar_defaults = dict(bar_line_color='black', label_standoff=8,
                              major_tick_line_color='black')

    def _draw_colorbar(self, plot, color_mapper):
        if LogColorMapper and isinstance(color_mapper, LogColorMapper):
            ticker = LogTicker()
        else:
            ticker = BasicTicker()
        cbar_opts = dict(self.colorbar_specs[self.colorbar_position])

        # Check if there is a colorbar in the same position
        pos = cbar_opts['pos']
        if any(isinstance(model, ColorBar) for model in getattr(plot, pos, [])):
            return

        opts = dict(cbar_opts['opts'], self._colorbar_defaults)
        color_bar = ColorBar(color_mapper=color_mapper, ticker=ticker,
                             **dict(opts, **self.colorbar_opts))

        plot.add_layout(color_bar, pos)
        self.handles['colorbar'] = color_bar


    def _get_colormapper(self, dim, element, ranges, style):
        low, high = ranges.get(dim.name)
        palette = mplcmap_to_palette(style.pop('cmap', 'viridis'))
        colormapper = LogColorMapper if self.logz else LinearColorMapper
        cmapper = colormapper(palette, low=low, high=high)
        if 'color_mapper' not in self.handles:
            self.handles['color_mapper'] = cmapper
        return cmapper


    def _init_glyph(self, plot, mapping, properties):
        """
        Returns a Bokeh glyph object and optionally creates a colorbar.
        """
        ret = super(ColorbarPlot, self)._init_glyph(plot, mapping, properties)
        if self.colorbar and 'color_mapper' in self.handles:
            self._draw_colorbar(plot, self.handles['color_mapper'])
        return ret


    def _update_glyph(self, glyph, properties, mapping):
        allowed_properties = glyph.properties()
        cmappers = [v.get('transform') for v in mapping.values()
                    if isinstance(v, dict)]
        cmappers.append(properties.pop('color_mapper', None))
        for cm in cmappers:
            if cm:<commit_msg>Added comment about colormapper instances<commit_after>from io import BytesIO

import numpy as np
import bokeh
import bokeh.plotting
from bokeh.models import Range, HoverTool, Renderer
from bokeh.models.tickers import Ticker, BasicTicker, FixedTicker
from bokeh.models.widgets import Panel, Tabs

from bokeh.models.mappers import LinearColorMapper
try:
    from bokeh.models import ColorBar
    from bokeh.models.mappers import LogColorMapper
except ImportError:
    LogColorMapper, ColorBar = None, None
from bokeh.models import LogTicker, BasicTicker

try:
    from bokeh import mpl
except ImportError:
    mpl = None
import param

from ...core import (Store, HoloMap, Overlay, DynamicMap,
                     CompositeOverlay, Element)
from ...core.options import abbreviated_exception
from ...core import util
from ...element import RGB
from ..plot import GenericElementPlot, GenericOverlayPlot
from ..util import dynamic_update
from .callbacks import Callbacks
from .plot import BokehPlot
from .util import (mpl_to_bokeh, convert_datetime, update_plot,
                   bokeh_version, mplcmap_to_palette)

if bokeh_version >= '0.12':
    from bokeh.models import FuncTickFormatter
else:
    FuncTickFormatter = None


# Define shared style properties for bokeh plots
line_properties = ['line_width', 'line_color', 'line_alpha',
                   'line_join', 'line_cap', 'line_dash']

fill_properties = ['fill_color', 'fill_alpha']

text_properties = ['text_font', 'text_font_size', 'text_font_style', 'text_color',
                   'text_alpha', 'text_align', 'text_baseline']

legend_dimensions = ['label_standoff', 'label_width', 'label_height', 'glyph_width',
                     'glyph_height', 'legend_padding', 'legend_spacing']



class ElementPlot(BokehPlot, GenericElementPlot):

    callbacks = param.ClassSelector(class_=Callbacks, doc="""
        Callbacks object defining any javascript callbacks applied
        to the plot.""")

    bgcolor = param.Parameter(default='white', doc="""
        Background color of the plot.""")

    border = param.Number(default=10, doc="""
        Minimum border around plot.""")

    fontsize = param.Parameter(default={'title': '12pt'}, allow_None=True,  doc="""
       Specifies various fontsizes of the displayed text.

       Finer control is available by supplying a dictionary where any
       unmentioned keys reverts to the default sizes, e.g:

          {'ticks': '20pt', 'title': '15pt', 'ylabel': '5px', 'xlabel': '5px'}""")

    invert_axes = param.Boolean(default=False, doc="""
        Whether to invert the x- and y-axis""")

    invert_xaxis = param.Boolean(default=False, doc="""
        Whether to invert the plot x-axis.""")

    invert_yaxis = param.Boolean(default=False, doc="""
        Whether to invert the plot y-axis.""")

    lod = param.Dict(default={'factor': 10, 'interval': 300,
                              'threshold': 2000, 'timeout': 500}, doc="""
        Bokeh plots offer "Level of Detail" (LOD) capability to
        accomodate large (but not huge) amounts of data. The available
        options are:

          * factor    - Decimation factor to use when applying
                        decimation.
          * interval  - Interval (in ms) downsampling will be enabled
                        after an interactive event.
          * threshold - Number of samples before downsampling is enabled.
          * timeout   - Timeout (in ms) for checking whether interactive
                        tool events are still occurring.""")

    show_grid = param.Boolean(default=True, doc="""
        Whether to show a Cartesian grid on the plot.""")

    show_legend = param.Boolean(default=True, doc="""
        Whether to show legend for the plot.""")

    shared_axes = param.Boolean(default=True, doc="""
        Whether to invert the share axes across plots
        for linked panning and zooming.""")

    default_tools = param.List(default=['save', 'pan', 'wheel_zoom',
                                        'box_zoom', 'reset'],
        doc="A list of plugin tools to use on the plot.")

    tools = param.List(default=[], doc="""
        A list of plugin tools to use on the plot.""")

    toolbar = param.ObjectSelector(default='right',
                                   objects=["above", "below",
                                            "left", "right", None],
                                   doc="""
        The toolbar location, must be one of 'above', 'below',
        'left', 'right', None.""")

    xaxis = param.ObjectSelector(default='bottom',
                                 objects=['top', 'bottom', 'bare', 'top-bare',
                                          'bottom-bare', None], doc="""
        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'.""")

    logx = param.Boolean(default=False, doc="""
        Whether the x-axis of the plot will be a log axis.""")

    xrotation = param.Integer(default=None, bounds=(0, 360), doc="""
        Rotation angle of the xticks.""")

    xticks = param.Parameter(default=None, doc="""
        Ticks along x-axis specified as an integer, explicit list of
        tick locations or bokeh Ticker object. If set to None default
        bokeh ticking behavior is applied.""")

    yaxis = param.ObjectSelector(default='left',
                                      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'.""")

    logy = param.Boolean(default=False, doc="""
        Whether the y-axis of the plot will be a log axis.""")

    yrotation = param.Integer(default=None, bounds=(0, 360), doc="""
        Rotation angle of the yticks.""")

    yticks = param.Parameter(default=None, doc="""
        Ticks along y-axis specified as an integer, explicit list of
        tick locations or bokeh Ticker object. If set to None
        default bokeh ticking behavior is applied.""")

    # The plot objects to be updated on each frame
    # Any entries should be existing keys in the handles
    # instance attribute.
    _update_handles = ['source', 'glyph']

    def __init__(self, element, plot=None, show_labels=['x', 'y'], **params):
        self.show_labels = show_labels
        self.current_ranges = None
        super(ElementPlot, self).__init__(element, **params)
        self.handles = {} if plot is None else self.handles['plot']
        element_ids = self.hmap.traverse(lambda x: id(x), [Element])
        self.static = len(set(element_ids)) == 1 and len(self.keys) == len(self.hmap)


    def _init_tools(self, element):
        """
        Processes the list of tools to be supplied to the plot.
        """
        tools = self.default_tools + self.tools
        if 'hover' in tools:
            tooltips = [(d.pprint_label, '@'+util.dimension_sanitizer(d.name))
                        for d in element.dimensions()]
            tools[tools.index('hover')] = HoverTool(tooltips=tooltips)
        return tools


    def _get_hover_data(self, data, element, empty=False):
        """
        Initializes hover data based on Element dimension values.
        If empty initializes with no data.
        """
        if 'hover' in self.default_tools + self.tools:
            for d in element.dimensions(label=True):
                sanitized = util.dimension_sanitizer(d)
                data[sanitized] = [] if empty else element.dimension_values(d)


    def _axes_props(self, plots, subplots, element, ranges):
        # Get the bottom layer and range element
        el = element.traverse(lambda x: x, [Element])
        el = el[0] if el else element

        dims = el.dimensions()
        xlabel, ylabel, zlabel = self._get_axis_labels(dims)
        if self.invert_axes:
            xlabel, ylabel = ylabel, xlabel

        plot_ranges = {}
        # Try finding shared ranges in other plots in the same Layout
        if plots and self.shared_axes:
            for plot in plots:
                if plot is None or not hasattr(plot, 'xaxis'): continue
                if plot.xaxis[0].axis_label == xlabel:
                    plot_ranges['x_range'] = plot.x_range
                if plot.xaxis[0].axis_label == ylabel:
                    plot_ranges['y_range'] = plot.x_range
                if plot.yaxis[0].axis_label == ylabel:
                    plot_ranges['y_range'] = plot.y_range
                if plot.yaxis[0].axis_label == xlabel:
                    plot_ranges['x_range'] = plot.y_range

        if el.get_dimension_type(0) is np.datetime64:
            x_axis_type = 'datetime'
        else:
            x_axis_type = 'log' if self.logx else 'auto'

        if len(dims) > 1 and el.get_dimension_type(1) is np.datetime64:
            y_axis_type = 'datetime'
        else:
            y_axis_type = 'log' if self.logy else 'auto'

        # Get the Element that determines the range and get_extents
        range_el = el if self.batched and not isinstance(self, OverlayPlot) else element
        l, b, r, t = self.get_extents(range_el, ranges)

        if not 'x_range' in plot_ranges:
            if 'x_range' in ranges:
                plot_ranges['x_range'] = ranges['x_range']
            else:
                low, high = (b, t) if self.invert_axes else (l, r)
                if x_axis_type == 'datetime':
                    low = convert_datetime(low)
                    high = convert_datetime(high)
                elif low == high and low is not None:
                    offset = low*0.1 if low else 0.5
                    low -= offset
                    high += offset
                if all(x is not None and np.isfinite(x) for x in (low, high)):
                    plot_ranges['x_range'] = [low, high]

        if self.invert_xaxis:
            plot_ranges['x_ranges'] = plot_ranges['x_ranges'][::-1]

        if not 'y_range' in plot_ranges:
            if 'y_range' in ranges:
                plot_ranges['y_range'] = ranges['y_range']
            else:
                low, high = (l, r) if self.invert_axes else (b, t)
                if y_axis_type == 'datetime':
                    low = convert_datetime(low)
                    high = convert_datetime(high)
                elif low == high and low is not None:
                    offset = low*0.1 if low else 0.5
                    low -= offset
                    high += offset
                if all(y is not None and np.isfinite(y) for y in (low, high)):
                    plot_ranges['y_range'] = [low, high]
        if self.invert_yaxis:
            yrange = plot_ranges['y_range']
            if isinstance(yrange, Range):
                plot_ranges['y_range'] = yrange.__class__(start=yrange.end,
                                                          end=yrange.start)
            else:
                plot_ranges['y_range'] = yrange[::-1]
        return (x_axis_type, y_axis_type), (xlabel, ylabel, zlabel), plot_ranges


    def _init_plot(self, key, element, plots, ranges=None):
        """
        Initializes Bokeh figure to draw Element into and sets basic
        figure and axis attributes including axes types, labels,
        titles and plot height and width.
        """
        subplots = list(self.subplots.values()) if self.subplots else []

        axis_types, labels, plot_ranges = self._axes_props(plots, subplots, element, ranges)
        xlabel, ylabel, _ = labels
        x_axis_type, y_axis_type = axis_types
        properties = dict(plot_ranges)
        properties['x_axis_label'] = xlabel if 'x' in self.show_labels else ' '
        properties['y_axis_label'] = ylabel if 'y' in self.show_labels else ' '

        if self.show_title:
            title = self._format_title(key, separator=' ')
        else:
            title = ''

        if self.toolbar:
            tools = self._init_tools(element)
            properties['tools'] = tools
            properties['toolbar_location'] = self.toolbar

        properties['webgl'] = Store.renderers[self.renderer.backend].webgl
        return bokeh.plotting.Figure(x_axis_type=x_axis_type,
                                     y_axis_type=y_axis_type, title=title,
                                     **properties)


    def _plot_properties(self, key, plot, element):
        """
        Returns a dictionary of plot properties.
        """
        size_multiplier = Store.renderers[self.renderer.backend].size/100.
        plot_props = dict(plot_height=int(self.height*size_multiplier),
                          plot_width=int(self.width*size_multiplier))
        if bokeh_version < '0.12':
            plot_props.update(self._title_properties(key, plot, element))
        if self.bgcolor:
            plot_props['background_fill_color'] = self.bgcolor
        if self.border is not None:
            for p in ['left', 'right', 'top', 'bottom']:
                plot_props['min_border_'+p] = self.border
        lod = dict(self.defaults()['lod'], **self.lod)
        for lod_prop, v in lod.items():
            plot_props['lod_'+lod_prop] = v
        return plot_props


    def _title_properties(self, key, plot, element):
        if self.show_title:
            title = self._format_title(key, separator=' ')
        else:
            title = ''

        if bokeh_version < '0.12':
            title_font = self._fontsize('title', 'title_text_font_size')
            return dict(title=title, title_text_color='black', **title_font)
        else:
            title_font = self._fontsize('title', 'text_font_size')
            return dict(text=title, text_color='black', **title_font)


    def _init_axes(self, plot):
        if self.xaxis is None:
            plot.xaxis.visible = False
        elif self.xaxis == 'top':
            plot.above = plot.below
            plot.below = []
            plot.xaxis[:] = plot.above

        if self.yaxis is None:
            plot.yaxis.visible = False
        elif self.yaxis == 'right':
            plot.right = plot.left
            plot.left = []
            plot.yaxis[:] = plot.right


    def _axis_properties(self, axis, key, plot, dimension,
                         ax_mapping={'x': 0, 'y': 1}):
        """
        Returns a dictionary of axis properties depending
        on the specified axis.
        """
        axis_props = {}
        if ((axis == 'x' and self.xaxis in ['bottom-bare', 'top-bare']) or
            (axis == 'y' and self.yaxis in ['left-bare', 'right-bare'])):
            axis_props['axis_label'] = ''
            axis_props['major_label_text_font_size'] = '0pt'
            axis_props['major_tick_line_color'] = None
            axis_props['minor_tick_line_color'] = None
        else:
            rotation = self.xrotation if axis == 'x' else self.yrotation
            if rotation:
                axis_props['major_label_orientation'] = np.radians(rotation)
            ticker = self.xticks if axis == 'x' else self.yticks
            if isinstance(ticker, Ticker):
                axis_props['ticker'] = ticker
            elif isinstance(ticker, int):
                axis_props['ticker'] = BasicTicker(desired_num_ticks=ticker)
            elif isinstance(ticker, (tuple, list)):
                if all(isinstance(t, tuple) for t in ticker):
                    pass
                else:
                    axis_props['ticker'] = FixedTicker(ticks=ticker)

        if FuncTickFormatter is not None and ax_mapping and dimension:
            formatter = None
            if dimension.value_format:
                formatter = dimension.value_format
            elif dimension.type in dimension.type_formatters:
                formatter = dimension.type_formatters[dimension.type]
            if formatter:
                msg = ('%s dimension formatter could not be '
                       'converted to tick formatter. ' % dimension.name)
                try:
                    formatter = FuncTickFormatter.from_py_func(formatter)
                except RuntimeError:
                    self.warning(msg+'Ensure Flexx is installed '
                                 '("conda install -c bokeh flexx" or '
                                 '"pip install flexx")')
                except Exception as e:
                    error = 'Pyscript raised an error: {0}'.format(e)
                    error = error.replace('%', '%%')
                    self.warning(msg+error)
                else:
                    axis_props['formatter'] = formatter
        return axis_props


    def _update_plot(self, key, plot, element=None):
        """
        Updates plot parameters on every frame
        """
        el = element.traverse(lambda x: x, [Element])
        dimensions = el[0].dimensions() if el else el.dimensions()
        plot.set(**self._plot_properties(key, plot, element))
        props = {axis: self._axis_properties(axis, key, plot, dim)
                 for axis, dim in zip(['x', 'y'], dimensions)}
        plot.xaxis[0].set(**props.get('x', {}))
        plot.yaxis[0].set(**props.get('y', {}))

        if bokeh_version >= '0.12' and not self.overlaid:
            plot.title.set(**self._title_properties(key, plot, element))

        if not self.show_grid:
            plot.xgrid.grid_line_color = None
            plot.ygrid.grid_line_color = None


    def _update_ranges(self, element, ranges):
        l, b, r, t = self.get_extents(element, ranges)
        plot = self.handles['plot']
        if self.invert_axes:
            l, b, r, t = b, l, t, r
        if l == r:
            offset = abs(l*0.1 if l else 0.5)
            l -= offset
            r += offset
        if b == t:
            offset = abs(b*0.1 if b else 0.5)
            b -= offset
            t += offset

        # Ensure that it never sets a NaN value
        if isinstance(l, np.datetime64) or np.isfinite(l): plot.x_range.start = l
        if isinstance(l, np.datetime64) or np.isfinite(r): plot.x_range.end   = r
        if isinstance(l, np.datetime64) or np.isfinite(b): plot.y_range.start = b
        if isinstance(l, np.datetime64) or np.isfinite(t): plot.y_range.end   = t


    def _process_legend(self):
        """
        Disables legends if show_legend is disabled.
        """
        for l in self.handles['plot'].legend:
            l.legends[:] = []
            l.border_line_alpha = 0
            l.background_fill_alpha = 0


    def _init_glyph(self, plot, mapping, properties):
        """
        Returns a Bokeh glyph object.
        """
        properties = mpl_to_bokeh(properties)
        plot_method = self._plot_methods.get('batched' if self.batched else 'single')
        renderer = getattr(plot, plot_method)(**dict(properties, **mapping))
        return renderer, renderer.glyph


    def _glyph_properties(self, plot, element, source, ranges):
        properties = self.style[self.cyclic_index]

        if self.show_legend:
            if self.overlay_dims:
                legend = ', '.join([d.pprint_value(v) for d, v in
                                    self.overlay_dims.items()])
            else:
                legend = element.label
            properties['legend'] = legend
        properties['source'] = source
        return properties


    def _update_glyph(self, glyph, properties, mapping):
        allowed_properties = glyph.properties()
        properties = mpl_to_bokeh(properties)
        merged = dict(properties, **mapping)
        glyph.set(**{k: v for k, v in merged.items()
                     if k in allowed_properties})


    def initialize_plot(self, ranges=None, plot=None, plots=None, source=None):
        """
        Initializes a new plot object with the last available frame.
        """
        # Get element key and ranges for frame
        element = self.hmap.last
        key = self.keys[-1]
        ranges = self.compute_ranges(self.hmap, key, ranges)
        self.current_ranges = ranges
        self.current_frame = element
        self.current_key = key
        style_element = element.last if self.batched else element
        ranges = util.match_spec(style_element, ranges)

        # Initialize plot, source and glyph
        if plot is None:
            plot = self._init_plot(key, style_element, ranges=ranges, plots=plots)
            self._init_axes(plot)
        self.handles['plot'] = plot

        # Get data and initialize data source
        empty = self.callbacks and self.callbacks.downsample
        if self.batched:
            data, mapping = self.get_batched_data(element, ranges, empty)
        else:
            data, mapping = self.get_data(element, ranges, empty)
        if source is None:
            source = self._init_datasource(data)
        self.handles['source'] = source

        properties = self._glyph_properties(plot, style_element, source, ranges)
        with abbreviated_exception():
            renderer, glyph = self._init_glyph(plot, mapping, properties)
        self.handles['glyph'] = glyph
        if isinstance(renderer, Renderer):
            self.handles['glyph_renderer'] = renderer

        # Update plot, source and glyph
        with abbreviated_exception():
            self._update_glyph(glyph, properties, mapping)
        if not self.overlaid:
            self._update_plot(key, plot, style_element)
        if self.callbacks:
            self.callbacks(self)
            self.callbacks.update(self)
        if not self.overlaid:
            self._process_legend()
        self.drawn = True

        return plot


    def update_frame(self, key, ranges=None, plot=None, element=None, empty=False):
        """
        Updates an existing plot with data corresponding
        to the key.
        """
        reused = isinstance(self.hmap, DynamicMap) and self.overlaid
        if not reused and element is None:
            element = self._get_frame(key)
        else:
            self.current_key = key
            self.current_frame = element

        glyph = self.handles.get('glyph', None)
        if hasattr(glyph, 'visible'):
            glyph.visible = bool(element)
        if not element:
            return

        style_element = element.last if self.batched else element
        self.style = self.lookup_options(style_element, 'style')

        ranges = self.compute_ranges(self.hmap, key, ranges)
        self.set_param(**self.lookup_options(style_element, 'plot').options)
        ranges = util.match_spec(style_element, ranges)
        self.current_ranges = ranges

        plot = self.handles['plot']
        source = self.handles['source']
        empty = (self.callbacks and self.callbacks.downsample) or empty
        if self.batched:
            data, mapping = self.get_batched_data(element, ranges, empty)
        else:
            data, mapping = self.get_data(element, ranges, empty)
        self._update_datasource(source, data)

        if glyph:
            properties = self._glyph_properties(plot, element, source, ranges)
            with abbreviated_exception():
                self._update_glyph(self.handles['glyph'], properties, mapping)
        if not self.overlaid:
            self._update_ranges(style_element, ranges)
            self._update_plot(key, plot, style_element)
        if self.callbacks:
            self.callbacks.update(self)


    @property
    def current_handles(self):
        """
        Returns a list of the plot objects to update.
        """
        handles = []
        if self.static and not self.dynamic:
            return handles

        previous_id = self.handles.get('previous_id', None)
        current_id = id(self.current_frame.data) if self.current_frame else None
        for handle in self._update_handles:
            if (handle == 'source' and self.dynamic and
                current_id == previous_id):
                continue
            if handle in self.handles:
                handles.append(self.handles[handle])

        # Cache frame object id to skip updating if unchanged
        if self.dynamic:
            self.handles['previous_id'] = current_id

        if self.overlaid:
            return handles

        plot = self.state
        handles.append(plot)
        if bokeh_version >= '0.12':
            handles.append(plot.title)

        if self.current_frame:
            if self.framewise or isinstance(self.hmap, DynamicMap):
                handles += [plot.x_range, plot.y_range]
        return handles


    @property
    def framewise(self):
        """
        Property to determine whether the current frame should have
        framewise normalization enabled. Required for bokeh plotting
        classes to determine whether to send updated ranges for each
        frame.
        """
        current_frames = [el for f in self.traverse(lambda x: x.current_frame)
                          for el in (f.traverse(lambda x: x, [Element])
                                     if f else [])]
        return any(self.lookup_options(frame, 'norm').options.get('framewise')
                   for frame in current_frames)



class ColorbarPlot(ElementPlot):
    """
    ColorbarPlot provides methods to create colormappers and colorbar
    models which can be added to a glyph. Additionally it provides
    parameters to control the position and other styling options of
    the colorbar. The default colorbar_position options are defined
    by the colorbar_specs, but may be overridden by the colorbar_opts.
    """

    colorbar_specs = {'right':     {'pos': 'right',
                                    'opts': {'location': (0, 0)}},
                      'left':      {'pos': 'left',
                                    'opts':{'location':(0, 0)}},
                      'bottom':    {'pos': 'below',
                                    'opts': {'location': (0, 0),
                                             'orientation':'horizontal'}},
                      'top':       {'pos': 'above',
                                    'opts': {'location':(0, 0),
                                             'orientation':'horizontal'}},
                      'top_right':   {'pos': 'center',
                                      'opts': {'location': 'top_right'}},
                      'top_left':    {'pos': 'center',
                                      'opts': {'location': 'top_left'}},
                      'bottom_left': {'pos': 'center',
                                      'opts': {'location': 'bottom_left',
                                               'orientation': 'horizontal'}},
                      'bottom_right': {'pos': 'center',
                                      'opts': {'location': 'bottom_right',
                                               'orientation': 'horizontal'}}}

    colorbar = param.Boolean(default=False, doc="""
        Whether to display a colorbar.""")

    colorbar_position = param.ObjectSelector(objects=list(colorbar_specs),
                                             default="right", doc="""
        Allows selecting between a number of predefined colorbar position
        options. The predefined options may be customized in the
        colorbar_specs class attribute.""")

    colorbar_opts = param.Dict(default={}, doc="""
        Allows setting specific styling options for the colorbar overriding
        the options defined in the colorbar_specs class attribute. Includes
        location, orientation, height, width, scale_alpha, title, title_props,
        margin, padding, background_fill_color and more.""")

    logz  = param.Boolean(default=False, doc="""
         Whether to apply log scaling to the z-axis.""")

    _update_handles = ['color_mapper', 'source', 'glyph']

    _colorbar_defaults = dict(bar_line_color='black', label_standoff=8,
                              major_tick_line_color='black')

    def _draw_colorbar(self, plot, color_mapper):
        if LogColorMapper and isinstance(color_mapper, LogColorMapper):
            ticker = LogTicker()
        else:
            ticker = BasicTicker()
        cbar_opts = dict(self.colorbar_specs[self.colorbar_position])

        # Check if there is a colorbar in the same position
        pos = cbar_opts['pos']
        if any(isinstance(model, ColorBar) for model in getattr(plot, pos, [])):
            return

        opts = dict(cbar_opts['opts'], self._colorbar_defaults)
        color_bar = ColorBar(color_mapper=color_mapper, ticker=ticker,
                             **dict(opts, **self.colorbar_opts))

        plot.add_layout(color_bar, pos)
        self.handles['colorbar'] = color_bar


    def _get_colormapper(self, dim, element, ranges, style):
        low, high = ranges.get(dim.name)
        palette = mplcmap_to_palette(style.pop('cmap', 'viridis'))
        colormapper = LogColorMapper if self.logz else LinearColorMapper
        cmapper = colormapper(palette, low=low, high=high)

        # The initial colormapper instance is cached the first time
        # and then updated with the values from new instances
        if 'color_mapper' not in self.handles:
            self.handles['color_mapper'] = cmapper
        return cmapper


    def _init_glyph(self, plot, mapping, properties):
        """
        Returns a Bokeh glyph object and optionally creates a colorbar.
        """
        ret = super(ColorbarPlot, self)._init_glyph(plot, mapping, properties)
        if self.colorbar and 'color_mapper' in self.handles:
            self._draw_colorbar(plot, self.handles['color_mapper'])
        return ret


    def _update_glyph(self, glyph, properties, mapping):
        allowed_properties = glyph.properties()
        cmappers = [v.get('transform') for v in mapping.values()
                    if isinstance(v, dict)]
        cmappers.append(properties.pop('color_mapper', None))
        for cm in cmappers:
            if cm:
{'fraction_non_alphanumeric': 0.1035456018170583, 'fraction_numerical': 0.0035405915460026386, 'mean_word_length': 2.003511235955056, 'pattern_counts': {'":': 0, '<': 7, '<?xml version=': 0, '>': 11, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 76, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '5090081', 'n_tokens_mistral': 17142, 'n_tokens_neox': 16180, 'n_words': 4631}