from __future__ import absolute_import, division, unicode_literals

import warnings
from types import FunctionType

import param
import numpy as np
import bokeh
import bokeh.plotting

from bokeh.core.properties import value
from bokeh.document.events import ModelChangedEvent
from bokeh.models import Renderer, Title, Legend, ColorBar, tools
from bokeh.models.axes import CategoricalAxis, DatetimeAxis
from bokeh.models.formatters import (
    FuncTickFormatter, TickFormatter, PrintfTickFormatter,
    MercatorTickFormatter)
from bokeh.models.mappers import (
    LinearColorMapper, LogColorMapper, CategoricalColorMapper)
from bokeh.models.ranges import Range1d, DataRange1d, FactorRange
from bokeh.models.tickers import (
    Ticker, BasicTicker, FixedTicker, LogTicker, MercatorTicker)
from bokeh.models.widgets import Panel, Tabs
from bokeh.plotting.helpers import _known_tools as known_tools

from ...core import DynamicMap, CompositeOverlay, Element, Dimension, Dataset
from ...core.options import abbreviated_exception, SkipRendering
from ...core import util
from ...element import Graph, VectorField, Path, Contours, Tiles
from ...streams import Buffer, PlotSize
from ...util.transform import dim
from ..plot import GenericElementPlot, GenericOverlayPlot
from ..util import dynamic_update, process_cmap, color_intervals, dim_range_key
from .callbacks import PlotSizeCallback
from .plot import BokehPlot
from .styles import (
    legend_dimensions, line_properties, mpl_to_bokeh, property_prefixes,
    rgba_tuple, text_properties, validate)
from .util import (
    TOOL_TYPES, date_to_integer, decode_bytes, get_tab_title,
    glyph_order, py2js_tickformatter, recursive_model_update,
    theme_attr_json, cds_column_replace, hold_policy, match_dim_specs,
    compute_layout_properties)



class ElementPlot(BokehPlot, GenericElementPlot):

    active_tools = param.List(default=[], doc="""
        Allows specifying which tools are active by default. Note
        that only one tool per gesture type can be active, e.g.
        both 'pan' and 'box_zoom' are drag tools, so if both are
        listed only the last one will be active.""")

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

    aspect = param.Parameter(default=None, doc="""
        The aspect ratio mode of the plot. By default, a plot may
        select its own appropriate aspect ratio but sometimes it may
        be necessary to force a square aspect ratio (e.g. to display
        the plot as an element of a grid). The modes 'auto' and
        'equal' correspond to the axis modes of the same name in
        matplotlib, a numeric value specifying the ratio between plot
        width and height may also be passed. To control the aspect
        ratio between the axis scales use the data_aspect option
        instead.""")

    data_aspect = param.Number(default=None, doc="""
        Defines the aspect of the axis scaling, i.e. the ratio of
        y-unit to x-unit.""")

    width = param.Integer(default=300, allow_None=True, bounds=(0, None), doc="""
        The width of the component (in pixels). This can be either
        fixed or preferred width, depending on width sizing policy.""")

    height = param.Integer(default=300, allow_None=True, bounds=(0, None), doc="""
        The height of the component (in pixels).  This can be either
        fixed or preferred height, depending on height sizing policy.""")

    frame_width = param.Integer(default=None, allow_None=True, bounds=(0, None), doc="""
        The width of the component (in pixels). This can be either
        fixed or preferred width, depending on width sizing policy.""")

    frame_height = param.Integer(default=None, allow_None=True, bounds=(0, None), doc="""
        The height of the component (in pixels).  This can be either
        fixed or preferred height, depending on height sizing policy.""")

    min_width = param.Integer(default=None, bounds=(0, None), doc="""
        Minimal width of the component (in pixels) if width is adjustable.""")

    min_height = param.Integer(default=None, bounds=(0, None), doc="""
        Minimal height of the component (in pixels) if height is adjustable.""")

    max_width = param.Integer(default=None, bounds=(0, None), doc="""
        Minimal width of the component (in pixels) if width is adjustable.""")

    max_height = param.Integer(default=None, bounds=(0, None), doc="""
        Minimal height of the component (in pixels) if height is adjustable.""")

    margin = param.Parameter(default=None, doc="""
        Allows to create additional space around the component. May
        be specified as a two-tuple of the form (vertical, horizontal)
        or a four-tuple (top, right, bottom, left).""")

    responsive = param.ObjectSelector(default=False, objects=[False, True, 'width', 'height'])

    finalize_hooks = param.HookList(default=[], doc="""
        Deprecated; use hooks options instead.""")

    hooks = param.HookList(default=[], doc="""
        Optional list of hooks called when finalizing a plot. The
        hook is passed the plot object and the displayed element, and
        other plotting handles can be accessed via plot.handles.""")

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

    gridstyle = param.Dict(default={}, doc="""
        Allows customizing the grid style, e.g. grid_line_color defines
        the line color for both grids while xgrid_line_color exclusively
        customizes the x-axis grid lines.""")

    labelled = param.List(default=['x', 'y'], doc="""
        Whether to plot the 'x' and 'y' labels.""")

    lod = param.Dict(default={'factor': 10, 'interval': 300,
                              'threshold': 2000, 'timeout': 500}, doc="""
        Bokeh plots offer "Level of Detail" (LOD) capability to
        accommodate 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_frame = param.Boolean(default=True, doc="""
        Whether or not to show a complete frame around 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", "disable", None],
                                   doc="""
        The toolbar location, must be one of 'above', 'below',
        'left', 'right', None.""")

    xformatter = param.ClassSelector(
        default=None, class_=(util.basestring, TickFormatter, FunctionType), doc="""
        Formatter for ticks along the x-axis.""")

    yformatter = param.ClassSelector(
        default=None, class_=(util.basestring, TickFormatter, FunctionType), doc="""
        Formatter for ticks along the x-axis.""")

    _categorical = False

    # Declare which styles cannot be mapped to a non-scalar dimension
    _nonvectorized_styles = []

    # Declares the default types for continuous x- and y-axes
    _x_range_type = Range1d
    _y_range_type = Range1d

    # Whether the plot supports streaming data
    _stream_data = True

    def __init__(self, element, plot=None, **params):
        self.current_ranges = None
        super(ElementPlot, self).__init__(element, **params)
        self.handles = {} if plot is None else self.handles['plot']
        self.static = len(self.hmap) == 1 and len(self.keys) == len(self.hmap)
        self.callbacks = self._construct_callbacks()
        self.static_source = False
        self.streaming = [s for s in self.streams if isinstance(s, Buffer)]
        self.geographic = bool(self.hmap.last.traverse(lambda x: x, Tiles))
        if self.geographic and self.projection is None:
            self.projection = 'mercator'

        # Whether axes are shared between plots
        self._shared = {'x': False, 'y': False}


    def _hover_opts(self, element):
        if self.batched:
            dims = list(self.hmap.last.kdims)
        else:
            dims = list(self.overlay_dims.keys())
        dims += element.dimensions()
        return list(util.unique_iterator(dims)), {}


    def _init_tools(self, element, callbacks=[]):
        """
        Processes the list of tools to be supplied to the plot.
        """
        tooltips, hover_opts = self._hover_opts(element)
        tooltips = [(ttp.pprint_label, '@{%s}' % util.dimension_sanitizer(ttp.name))
                    if isinstance(ttp, Dimension) else ttp for ttp in tooltips]
        if not tooltips: tooltips = None

        callbacks = callbacks+self.callbacks
        cb_tools, tool_names = [], []
        hover = False
        for cb in callbacks:
            for handle in cb.models+cb.extra_models:
                if handle and handle in known_tools:
                    tool_names.append(handle)
                    if handle == 'hover':
                        tool = tools.HoverTool(tooltips=tooltips, **hover_opts)
                        hover = tool
                    else:
                        tool = known_tools[handle]()
                    cb_tools.append(tool)
                    self.handles[handle] = tool

        tool_list = [
            t for t in cb_tools + self.default_tools + self.tools
            if t not in tool_names]

        copied_tools = []
        for tool in tool_list:
            if isinstance(tool, tools.Tool):
                properties = tool.properties_with_values(include_defaults=False)
                tool = type(tool)(**properties)
            copied_tools.append(tool)

        hover_tools = [t for t in copied_tools if isinstance(t, tools.HoverTool)]
        if 'hover' in copied_tools:
            hover = tools.HoverTool(tooltips=tooltips, **hover_opts)
            copied_tools[copied_tools.index('hover')] = hover
        elif any(hover_tools):
            hover = hover_tools[0]
        if hover:
            self.handles['hover'] = hover
        return copied_tools


    def _get_hover_data(self, data, element, dimensions=None):
        """
        Initializes hover data based on Element dimension values.
        If empty initializes with no data.
        """
        if 'hover' not in self.handles or self.static_source:
            return

        for d in (dimensions or element.dimensions()):
            dim = util.dimension_sanitizer(d.name)
            if dim not in data:
                data[dim] = element.dimension_values(d)
            values = np.asarray(data[dim])
            if (values.dtype.kind == 'M' or (
                    len(values) and isinstance(values[0], util.datetime_types))):
                data[dim+'_dt_strings'] = [d.pprint_value(v) for v in values]

        for k, v in self.overlay_dims.items():
            dim = util.dimension_sanitizer(k.name)
            if dim not in data:
                data[dim] = [v for _ in range(len(list(data.values())[0]))]


    def _merge_ranges(self, plots, xspecs, yspecs):
        """
        Given a list of other plots return axes that are shared
        with another plot by matching the dimensions specs stored
        as tags on the dimensions.
        """
        plot_ranges = {}
        for plot in plots:
            if plot is None:
                continue
            if hasattr(plot, 'x_range') and plot.x_range.tags and xspecs is not None:
                if match_dim_specs(plot.x_range.tags[0], xspecs):
                    plot_ranges['x_range'] = plot.x_range
                if match_dim_specs(plot.x_range.tags[0], yspecs):
                    plot_ranges['y_range'] = plot.x_range
            if hasattr(plot, 'y_range') and plot.y_range.tags and yspecs is not None:
                if match_dim_specs(plot.y_range.tags[0], yspecs):
                    plot_ranges['y_range'] = plot.y_range
                if match_dim_specs(plot.y_range.tags[0], xspecs):
                    plot_ranges['x_range'] = plot.y_range
        return plot_ranges


    def _get_axis_dims(self, element):
        """Returns the dimensions corresponding to each axis.

        Should return a list of dimensions or list of lists of
        dimensions, which will be formatted to label the axis
        and to link axes.
        """
        dims = element.dimensions()[:2]
        if len(dims) == 1:
            return dims + [None, None]
        else:
            return dims + [None]


    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 = self._get_axis_dims(el)
        xlabel, ylabel, zlabel = self._get_axis_labels(dims)
        if self.invert_axes:
            xlabel, ylabel = ylabel, xlabel
            dims = dims[:2][::-1]
        xdims, ydims = dims[:2]
        if xdims:
            if not isinstance(xdims, list):
                xdims = [xdims]
            xspecs = tuple((xd.name, xd.label, xd.unit) for xd in xdims)
        else:
            xspecs = None
        if ydims:
            if not isinstance(ydims, list):
                ydims = [ydims]
            yspecs = tuple((yd.name, yd.label, yd.unit) for yd in ydims)
        else:
            yspecs = None

        plot_ranges = {}
        # Try finding shared ranges in other plots in the same Layout
        norm_opts = self.lookup_options(el, 'norm').options
        if plots and self.shared_axes and not norm_opts.get('axiswise', False):
            plot_ranges = self._merge_ranges(plots, xspecs, yspecs)

        # 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 self.invert_axes:
            l, b, r, t = b, l, t, r

        categorical = any(self.traverse(lambda x: x._categorical))
        categorical_x = any(isinstance(x, util.basestring) for x in (l, r))
        categorical_y = any(isinstance(y, util.basestring) for y in (b, t))

        x_axis_type = 'log' if self.logx else 'auto'
        if xdims:
            if len(xdims) > 1:
                x_axis_type = 'auto'
                categorical_x = True
            else:
                if isinstance(el, Graph):
                    xtype = el.nodes.get_dimension_type(xdims[0])
                else:
                    xtype = el.get_dimension_type(xdims[0])
                if ((xtype is np.object_ and issubclass(type(l), util.datetime_types)) or
                    xtype in util.datetime_types):
                    x_axis_type = 'datetime'

        y_axis_type = 'log' if self.logy else 'auto'
        if ydims:
            if len(ydims) > 1:
                y_axis_type = 'auto'
                categorical_y = True
            else:
                if isinstance(el, Graph):
                    ytype = el.nodes.get_dimension_type(ydims[0])
                else:
                    ytype = el.get_dimension_type(ydims[0])
                if ((ytype is np.object_ and issubclass(type(b), util.datetime_types))
                    or ytype in util.datetime_types):
                    y_axis_type = 'datetime'

        # Declare shared axes
        if 'x_range' in plot_ranges:
            self._shared['x'] = True
        if 'y_range' in plot_ranges:
            self._shared['y'] = True

        range_types = (self._x_range_type, self._y_range_type)
        if self.invert_axes: range_types = range_types[::-1]
        x_range_type, y_range_type = range_types
        if categorical or categorical_x:
            x_axis_type = 'auto'
            plot_ranges['x_range'] = FactorRange()
        elif 'x_range' not in plot_ranges:
            plot_ranges['x_range'] = x_range_type()

        if categorical or categorical_y:
            y_axis_type = 'auto'
            plot_ranges['y_range'] = FactorRange()
        elif 'y_range' not in plot_ranges:
            plot_ranges['y_range'] = y_range_type()

        x_range, y_range = plot_ranges['x_range'], plot_ranges['y_range']
        if not x_range.tags and xspecs is not None:
            x_range.tags.append(xspecs)
        if not y_range.tags and yspecs is not None:
            y_range.tags.append(yspecs)

        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.labelled or self.xlabel else ' '
        properties['y_axis_label'] = ylabel if 'y' in self.labelled or self.ylabel else ' '

        if not self.show_frame:
            properties['outline_line_alpha'] = 0

        if self.show_title and self.adjoined is None:
            title = self._format_title(key, separator=' ')
        else:
            title = ''

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

        if self.renderer.webgl:
            properties['output_backend'] = 'webgl'

        properties.update(**self._plot_properties(key, element))

        with warnings.catch_warnings():
            # Bokeh raises warnings about duplicate tools but these
            # are not really an issue
            warnings.simplefilter('ignore', UserWarning)
            return bokeh.plotting.Figure(x_axis_type=x_axis_type,
                                         y_axis_type=y_axis_type, title=title,
                                         **properties)


    def _plot_properties(self, key, element):
        """
        Returns a dictionary of plot properties.
        """
        init = 'plot' not in self.handles
        size_multiplier = self.renderer.size/100.
        options = self._traverse_options(element, 'plot', ['width', 'height'], defaults=False)

        logger = self.param if init else None
        aspect_props, dimension_props = compute_layout_properties(
            self.width, self.height, self.frame_width, self.frame_height,
            options['width'], options['height'], self.aspect, self.data_aspect,
            self.responsive, size_multiplier, logger=logger)

        if not init:
            if aspect_props['aspect_ratio'] is None:
                aspect_props['aspect_ratio'] = self.state.aspect_ratio

        if self.dynamic and aspect_props['match_aspect']:
            # Sync the plot size on dynamic plots to support accurate
            # scaling of dimension ranges
            stream = PlotSize(subscribers=[self._update_size])
            self.callbacks.append(PlotSizeCallback(self, [stream], None))

        plot_props = {
            'margin':        self.margin,
            'max_width':     self.max_width,
            'max_height':    self.max_height,
            'min_width':     self.min_width,
            'min_height':    self.min_height
        }
        plot_props.update(aspect_props)
        if not self.drawn:
            plot_props.update(dimension_props)

        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.param.defaults().get('lod', {}), **self.lod)
        for lod_prop, v in lod.items():
            plot_props['lod_'+lod_prop] = v
        return plot_props

    def _update_size(self, width, height, scale):
        self.state.frame_width = width
        self.state.frame_height = height

    def _set_active_tools(self, plot):
        "Activates the list of active tools"
        for tool in self.active_tools:
            if isinstance(tool, util.basestring):
                tool_type = TOOL_TYPES[tool]
                matching = [t for t in plot.toolbar.tools
                            if isinstance(t, tool_type)]
                if not matching:
                    self.param.warning('Tool of type %r could not be found '
                                       'and could not be activated by default.'
                                       % tool)
                    continue
                tool = matching[0]
            if isinstance(tool, tools.Drag):
                plot.toolbar.active_drag = tool
            if isinstance(tool, tools.Scroll):
                plot.toolbar.active_scroll = tool
            if isinstance(tool, tools.Tap):
                plot.toolbar.active_tap = tool
            if isinstance(tool, tools.Inspection):
                plot.toolbar.active_inspect.append(tool)


    def _title_properties(self, key, plot, element):
        if self.show_title and self.adjoined is None:
            title = self._format_title(key, separator=' ')
        else:
            title = ''
        opts = dict(text=title)

        # this will override theme if not set to the default 12pt
        title_font = self._fontsize('title').get('fontsize')
        if title_font != '12pt':
            opts['text_font_size'] = value(title_font)
        return opts


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

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


    def _axis_properties(self, axis, key, plot, dimension=None,
                         ax_mapping={'x': 0, 'y': 1}):
        """
        Returns a dictionary of axis properties depending
        on the specified axis.
        """
        # need to copy dictionary by calling dict() on it
        axis_props = dict(theme_attr_json(self.renderer.theme, 'Axis'))

        if ((axis == 'x' and self.xaxis in ['bottom-bare', 'top-bare', 'bare']) or
            (axis == 'y' and self.yaxis in ['left-bare', 'right-bare', 'bare'])):
            axis_props['axis_label_text_font_size'] = value('0pt')
            axis_props['major_label_text_font_size'] = value('0pt')
            axis_props['major_tick_line_color'] = None
            axis_props['minor_tick_line_color'] = None
        else:
            labelsize = self._fontsize('%slabel' % axis).get('fontsize')
            if labelsize:
                axis_props['axis_label_text_font_size'] = labelsize
            ticksize = self._fontsize('%sticks' % axis, common=False).get('fontsize')
            if ticksize:
                axis_props['major_label_text_font_size'] = value(ticksize)
            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):
                    ticks, labels = zip(*ticker)
                    # Ensure floats which are integers are serialized as ints
                    # because in JS the lookup fails otherwise
                    ticks = [int(t) if isinstance(t, float) and t.is_integer() else t
                             for t in ticks]
                    labels = [l if isinstance(l, util.basestring) else str(l)
                              for l in labels]
                    axis_props['ticker'] = FixedTicker(ticks=ticks)
                    axis_props['major_label_overrides'] = dict(zip(ticks, labels))
                else:
                    axis_props['ticker'] = FixedTicker(ticks=ticker)

        formatter = self.xformatter if axis == 'x' else self.yformatter
        if formatter:
            if isinstance(formatter, TickFormatter):
                pass
            elif isinstance(formatter, FunctionType):
                msg = ('%sformatter could not be '
                       'converted to tick formatter. ' % axis)
                jsfunc = py2js_tickformatter(formatter, msg)
                if jsfunc:
                    formatter = FuncTickFormatter(code=jsfunc)
                else:
                    formatter = None
            else:
                formatter = PrintfTickFormatter(format=formatter)
            if formatter is not None:
                axis_props['formatter'] = formatter
        elif FuncTickFormatter is not None and ax_mapping and isinstance(dimension, 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)
                jsfunc = py2js_tickformatter(formatter, msg)
                if jsfunc:
                    formatter = FuncTickFormatter(code=jsfunc)
                    axis_props['formatter'] = formatter

        if axis == 'x':
            axis_obj = plot.xaxis[0]
        elif axis == 'y':
            axis_obj = plot.yaxis[0]

        if self.geographic and self.projection == 'mercator':
            dimension = 'lon' if axis == 'x' else 'lat'
            axis_props['ticker'] = MercatorTicker(dimension=dimension)
            axis_props['formatter'] = MercatorTickFormatter(dimension=dimension)
            box_zoom = self.state.select(type=tools.BoxZoomTool)
            if box_zoom:
                box_zoom[0].match_aspect = True
        elif isinstance(axis_obj, CategoricalAxis):
            for key in list(axis_props):
                if key.startswith('major_label'):
                    # set the group labels equal to major (actually minor)
                    new_key = key.replace('major_label', 'group')
                    axis_props[new_key] = axis_props[key]

            # major ticks are actually minor ticks in a categorical
            # so if user inputs minor ticks sizes, then use that;
            # else keep major (group) == minor (subgroup)
            msize = self._fontsize('minor_{0}ticks'.format(axis),
                common=False).get('fontsize')
            if msize is not None:
                axis_props['major_label_text_font_size'] = msize

        return axis_props


    def _update_plot(self, key, plot, element=None):
        """
        Updates plot parameters on every frame
        """
        plot.update(**self._plot_properties(key, element))
        self._update_labels(key, plot, element)
        self._update_title(key, plot, element)
        self._update_grid(plot)


    def _update_labels(self, key, plot, element):
        el = element.traverse(lambda x: x, [Element])
        el = el[0] if el else element
        dimensions = self._get_axis_dims(el)
        props = {axis: self._axis_properties(axis, key, plot, dim)
                 for axis, dim in zip(['x', 'y'], dimensions)}
        xlabel, ylabel, zlabel = self._get_axis_labels(dimensions)
        if self.invert_axes:
            xlabel, ylabel = ylabel, xlabel
        props['x']['axis_label'] = xlabel if 'x' in self.labelled or self.xlabel else ''
        props['y']['axis_label'] = ylabel if 'y' in self.labelled or self.ylabel else ''
        recursive_model_update(plot.xaxis[0], props.get('x', {}))
        recursive_model_update(plot.yaxis[0], props.get('y', {}))


    def _update_title(self, key, plot, element):
        if plot.title:
            plot.title.update(**self._title_properties(key, plot, element))
        else:
            plot.title = Title(**self._title_properties(key, plot, element))


    def _update_grid(self, plot):
        if not self.show_grid:
            plot.xgrid.grid_line_color = None
            plot.ygrid.grid_line_color = None
            return
        replace = ['bounds', 'bands', 'visible', 'level', 'ticker', 'visible']
        style_items = list(self.gridstyle.items())
        both = {k: v for k, v in style_items if k.startswith('grid_') or k.startswith('minor_grid')}
        xgrid = {k.replace('xgrid', 'grid'): v for k, v in style_items if 'xgrid' in k}
        ygrid = {k.replace('ygrid', 'grid'): v for k, v in style_items if 'ygrid' in k}
        xopts = {k.replace('grid_', '') if any(r in k for r in replace) else k: v
                 for k, v in dict(both, **xgrid).items()}
        yopts = {k.replace('grid_', '') if any(r in k for r in replace) else k: v
                 for k, v in dict(both, **ygrid).items()}
        if plot.xaxis and 'ticker' not in xopts:
            xopts['ticker'] = plot.xaxis[0].ticker
        if plot.yaxis and 'ticker' not in yopts:
            yopts['ticker'] = plot.yaxis[0].ticker
        plot.xgrid[0].update(**xopts)
        plot.ygrid[0].update(**yopts)


    def _update_ranges(self, element, ranges):
        x_range = self.handles['x_range']
        y_range = self.handles['y_range']

        l, b, r, t = None, None, None, None
        if any(isinstance(r, (Range1d, DataRange1d)) for r in [x_range, y_range]):
            l, b, r, t = self.get_extents(element, ranges)
            if self.invert_axes:
                l, b, r, t = b, l, t, r

        xfactors, yfactors = None, None
        if any(isinstance(ax_range, FactorRange) for ax_range in [x_range, y_range]):
            xfactors, yfactors = self._get_factors(element)
        framewise = self.framewise
        streaming = (self.streaming and any(stream._triggering for stream in self.streaming))
        xupdate = ((not self.model_changed(x_range) and (framewise or streaming))
                   or xfactors is not None)
        yupdate = ((not self.model_changed(y_range) and (framewise or streaming))
                   or yfactors is not None)

        options = self._traverse_options(element, 'plot', ['width', 'height'], defaults=False)
        fixed_width = (self.frame_width or options['width'])
        fixed_height = (self.frame_height or options['height'])

        data_aspect = (self.aspect == 'equal' or self.data_aspect)
        xaxis, yaxis = self.handles['xaxis'], self.handles['yaxis']
        categorical = isinstance(xaxis, CategoricalAxis) or isinstance(yaxis, CategoricalAxis)
        datetime = isinstance(xaxis, DatetimeAxis) or isinstance(yaxis, CategoricalAxis)

        if data_aspect and fixed_width and fixed_height:
            pass
        elif data_aspect and (categorical or datetime):
            ax_type = 'categorical' if categorical else 'datetime axes'
            self.param.warning('Cannot set data_aspect if one or both '
                               'axes are %s, the option will '
                               'be ignored.' % ax_type)
        elif data_aspect:
            plot = self.handles['plot']
            xspan = r-l if util.is_number(l) and util.is_number(r) else None
            yspan = t-b if util.is_number(b) and util.is_number(t) else None
            if self.drawn or self.aspect not in ['equal', None]:
                # After initial draw or if aspect is explicit
                # adjust range to match the plot dimension aspect
                ratio = self.data_aspect or 1
                if self.aspect == 'square':
                    frame_aspect = 1
                elif self.aspect and self.aspect != 'equal':
                    frame_aspect = self.aspect
                else:
                    frame_aspect = plot.frame_width/plot.frame_height

                desired_xspan = yspan*1./(ratio/frame_aspect)
                desired_yspan = (xspan*(ratio/frame_aspect))
                if (np.allclose(desired_xspan, xspan, rtol=0.01) and
                    np.allclose(desired_yspan, yspan, rtol=0.01)):
                    pass
                elif desired_yspan >= yspan:
                    ypad = (desired_yspan-yspan)/2.
                    b, t = b-ypad, t+ypad
                    yupdate = True
                else:
                    xpad = (desired_xspan-xspan)/2.
                    l, r = l-xpad, r+xpad
                    xupdate = True
            else:
                # Set initial aspect
                aspect = self.get_aspect(xspan, yspan)
                width = plot.frame_width or plot.plot_width or 300
                height = plot.frame_height or plot.plot_height or 300

                if not (fixed_width or fixed_height) and not self.responsive:
                    fixed_height = True

                if fixed_height:
                    plot.frame_height = height
                    plot.frame_width = int(height*aspect)
                    plot.plot_width, plot.plot_height = None, None
                elif fixed_width:
                    plot.frame_width = width
                    plot.frame_height = int(width/aspect)
                    plot.plot_width, plot.plot_height = None, None
                else:
                    plot.aspect_ratio = 1./aspect

            box_zoom = plot.select(type=tools.BoxZoomTool)
            scroll_zoom = plot.select(type=tools.WheelZoomTool)
            if box_zoom:
                box_zoom.match_aspect = True
            if scroll_zoom:
                scroll_zoom.zoom_on_axis = False

        if not self.drawn or xupdate:
            self._update_range(x_range, l, r, xfactors, self.invert_xaxis,
                               self._shared['x'], self.logx, streaming)
        if not self.drawn or yupdate:
            self._update_range(y_range, b, t, yfactors, self.invert_yaxis,
                               self._shared['y'], self.logy, streaming)


    def _update_range(self, axis_range, low, high, factors, invert, shared, log, streaming=False):
        if isinstance(axis_range, (Range1d, DataRange1d)) and self.apply_ranges:
            if isinstance(low, util.cftime_types):
                pass
            elif (low == high and low is not None):
                if isinstance(low, util.datetime_types):
                    offset = np.timedelta64(500, 'ms')
                    low, high = np.datetime64(low), np.datetime64(high)
                    low -= offset
                    high += offset
                else:
                    offset = abs(low*0.1 if low else 0.5)
                    low -= offset
                    high += offset
            if shared:
                shared = (axis_range.start, axis_range.end)
                low, high = util.max_range([(low, high), shared])
            if invert: low, high = high, low
            if not isinstance(low, util.datetime_types) and log and (low is None or low <= 0):
                low = 0.01 if high < 0.01 else 10**(np.log10(high)-2)
                self.param.warning(
                    "Logarithmic axis range encountered value less "
                    "than or equal to zero, please supply explicit "
                    "lower-bound to override default of %.3f." % low)
            updates = {}
            if util.isfinite(low):
                updates['start'] = (axis_range.start, low)
                updates['reset_start'] = updates['start']
            if util.isfinite(high):
                updates['end'] = (axis_range.end, high)
                updates['reset_end'] = updates['end']
            for k, (old, new) in updates.items():
                if isinstance(new, util.cftime_types):
                    new = date_to_integer(new)
                axis_range.update(**{k:new})
                if streaming and not k.startswith('reset_'):
                    axis_range.trigger(k, old, new)
        elif isinstance(axis_range, FactorRange):
            factors = list(decode_bytes(factors))
            if invert: factors = factors[::-1]
            axis_range.factors = factors


    def _categorize_data(self, data, cols, dims):
        """
        Transforms non-string or integer types in datasource if the
        axis to be plotted on is categorical. Accepts the column data
        source data, the columns corresponding to the axes and the
        dimensions for each axis, changing the data inplace.
        """
        if self.invert_axes:
            cols = cols[::-1]
            dims = dims[:2][::-1]
        ranges = [self.handles['%s_range' % ax] for ax in 'xy']
        for i, col in enumerate(cols):
            column = data[col]
            if (isinstance(ranges[i], FactorRange) and
                (isinstance(column, list) or column.dtype.kind not in 'SU')):
                data[col] = [dims[i].pprint_value(v) for v in column]


    def get_aspect(self, xspan, yspan):
        """
        Computes the aspect ratio of the plot
        """
        if self.data_aspect:
            return (yspan/xspan)*self.data_aspect
        elif self.aspect == 'equal':
            return xspan/yspan
        elif self.aspect == 'square':
            return 1
        elif self.aspect is not None:
            return self.aspect
        elif self.width is not None and self.height is not None:
            return self.width/self.height
        else:
            return 1


    def _get_factors(self, element):
        """
        Get factors for categorical axes.
        """
        xdim, ydim = element.dimensions()[:2]
        xvals, yvals = [element.dimension_values(i, False)
                        for i in range(2)]
        coords = tuple([v if vals.dtype.kind in 'SU' else dim.pprint_value(v) for v in vals]
                  for dim, vals in [(xdim, xvals), (ydim, yvals)])
        if self.invert_axes: coords = coords[::-1]
        return coords


    def _process_legend(self):
        """
        Disables legends if show_legend is disabled.
        """
        for l in self.handles['plot'].legend:
            l.items[:] = []
            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')
        if isinstance(plot_method, tuple):
            # Handle alternative plot method for flipped axes
            plot_method = plot_method[int(self.invert_axes)]
        renderer = getattr(plot, plot_method)(**dict(properties, **mapping))
        return renderer, renderer.glyph


    def _apply_transforms(self, element, data, ranges, style, group=None):
        new_style = dict(style)
        prefix = group+'_' if group else ''
        for k, v in dict(style).items():
            if isinstance(v, util.basestring):
                if validate(k, v) == True:
                    continue
                elif v in element or (isinstance(element, Graph) and v in element.nodes):
                    v = dim(v)
                elif any(d==v for d in self.overlay_dims):
                    v = dim([d for d in self.overlay_dims if d==v][0])

            if (not isinstance(v, dim) or (group is not None and not k.startswith(group))):
                continue
            elif (not v.applies(element) and v.dimension not in self.overlay_dims):
                new_style.pop(k)
                self.param.warning(
                    'Specified %s dim transform %r could not be applied, '
                    'as not all dimensions could be resolved.' % (k, v))
                continue

            if v.dimension in self.overlay_dims:
                ds = Dataset({d.name: v for d, v in self.overlay_dims.items()},
                             list(self.overlay_dims))
                val = v.apply(ds, ranges=ranges, flat=True)[0]
            elif isinstance(element, Path) and not isinstance(element, Contours):
                val = np.concatenate([v.apply(el, ranges=ranges, flat=True)[:-1]
                                      for el in element.split()])
            else:
                val = v.apply(element, ranges=ranges, flat=True)

            if (not util.isscalar(val) and len(util.unique_array(val)) == 1 and
                ((not 'color' in k or validate('color', val)) or k in self._nonvectorized_styles)):
                val = val[0]

            if not util.isscalar(val):
                if k in self._nonvectorized_styles:
                    element = type(element).__name__
                    raise ValueError('Mapping a dimension to the "{style}" '
                                     'style option is not supported by the '
                                     '{element} element using the {backend} '
                                     'backend. To map the "{dim}" dimension '
                                     'to the {style} use a groupby operation '
                                     'to overlay your data along the dimension.'.format(
                                         style=k, dim=v.dimension, element=element,
                                         backend=self.renderer.backend))
                elif data and len(val) != len(list(data.values())[0]):
                    if isinstance(element, VectorField):
                        val = np.tile(val, 3)
                    else:
                        continue

            if k == 'angle':
                val = np.deg2rad(val)
            elif k.endswith('font_size'):
                if util.isscalar(val) and isinstance(val, int):
                    val = str(v)+'pt'
                elif isinstance(val, np.ndarray) and val.dtype.kind in 'ifu':
                    val = [str(int(s))+'pt' for s in val]
            if util.isscalar(val):
                key = val
            else:
                key = {'field': k}
                data[k] = val

            # If color is not valid colorspec add colormapper
            numeric = isinstance(val, np.ndarray) and val.dtype.kind in 'uifMm'
            if ('color' in k and isinstance(val, np.ndarray) and
                (numeric or not validate('color', val))):
                kwargs = {}
                if val.dtype.kind not in 'ifMu':
                    range_key = dim_range_key(v)
                    if range_key in ranges and 'factors' in ranges[range_key]:
                        factors = ranges[range_key]['factors']
                    else:
                        factors = util.unique_array(val)
                    kwargs['factors'] = factors
                    if factors is not None and getattr(self, 'show_legend', False):
                        new_style['legend'] = key
                cmapper = self._get_colormapper(v, element, ranges,
                                                dict(style), name=k+'_color_mapper',
                                                group=group, **kwargs)
                if isinstance(cmapper, CategoricalColorMapper) and val.dtype.kind in 'ifMu':
                    if v.dimension in element:
                        formatter = element.get_dimension(v.dimension).pprint_value
                    else:
                        formatter = str
                    field = k + '_str__'
                    data[k+'_str__'] = [formatter(d) for d in val]
                else:
                    field = k
                key = {'field': field, 'transform': cmapper}
            new_style[k] = key

        # Process color/alpha styles and expand to fill/line style
        for style, val in list(new_style.items()):
            for s in ('alpha', 'color'):
                if prefix+s != style or style not in data or validate(s, val, True):
                    continue
                supports_fill = any(
                    o.startswith(prefix+'fill') and (prefix != 'edge_' or getattr(self, 'filled', True))
                    for o in self.style_opts)
                for pprefix in [p+'_' for p in property_prefixes]+['']:
                    fill_key = prefix+pprefix+'fill_'+s
                    fill_style = new_style.get(fill_key)

                    # Do not override custom nonselection/muted alpha
                    if ((pprefix in ('nonselection_', 'muted_') and s == 'alpha')
                        or fill_key not in self.style_opts):
                        continue

                    # Override empty and non-vectorized fill_style if not hover style
                    hover = pprefix == 'hover_'
                    if ((fill_style is None or (validate(s, fill_style, True) and not hover))
                        and supports_fill):
                        new_style[fill_key] = val

                    line_key = prefix+pprefix+'line_'+s
                    line_style = new_style.get(line_key)

                    # If glyph has fill and line style is set overriding line color
                    if supports_fill and line_style is not None:
                        continue

                    # If glyph does not support fill override non-vectorized line_color
                    if ((line_style is not None and (validate(s, line_style) and not hover)) or
                        (line_style is None and not supports_fill)):
                        new_style[line_key] = val
        return new_style


    def _glyph_properties(self, plot, element, source, ranges, style, group=None):
        properties = dict(style, source=source)
        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
            if legend and self.overlaid:
                properties['legend'] = value(legend)
        return properties


    def _filter_properties(self, properties, glyph_type, allowed):
        glyph_props = dict(properties)
        for gtype in ((glyph_type, '') if glyph_type else ('',)):
            for prop in ('color', 'alpha'):
                glyph_prop = properties.get(gtype+prop)
                if glyph_prop and ('line_'+prop not in glyph_props or gtype):
                    glyph_props['line_'+prop] = glyph_prop
                if glyph_prop and ('fill_'+prop not in glyph_props or gtype):
                    glyph_props['fill_'+prop] = glyph_prop

            props = {k[len(gtype):]: v for k, v in glyph_props.items()
                     if k.startswith(gtype)}
            if self.batched:
                glyph_props = dict(props, **glyph_props)
            else:
                glyph_props.update(props)
        return {k: v for k, v in glyph_props.items() if k in allowed}


    def _update_glyph(self, renderer, properties, mapping, glyph, source, data):
        allowed_properties = glyph.properties()
        properties = mpl_to_bokeh(properties)
        merged = dict(properties, **mapping)
        legend = merged.pop('legend', None)
        columns = list(source.data.keys())
        glyph_updates = []
        for glyph_type in ('', 'selection_', 'nonselection_', 'hover_', 'muted_'):
            if renderer:
                glyph = getattr(renderer, glyph_type+'glyph', None)
            if not glyph or (not renderer and glyph_type):
                continue
            filtered = self._filter_properties(merged, glyph_type, allowed_properties)

            # Ensure that data is populated before updating glyph
            dataspecs = glyph.dataspecs()
            for spec in dataspecs:
                new_spec = filtered.get(spec)
                old_spec = getattr(glyph, spec)
                new_field = new_spec.get('field') if isinstance(new_spec, dict) else new_spec
                old_field = old_spec.get('field') if isinstance(old_spec, dict) else old_spec
                if (data is None) or (new_field not in data or new_field in source.data or new_field == old_field):
                    continue
                columns.append(new_field)
            glyph_updates.append((glyph, filtered))

        # If a dataspec has changed and the CDS.data will be replaced
        # the GlyphRenderer will not find the column, therefore we
        # craft an event which will make the column available.
        cds_replace = True if data is None else cds_column_replace(source, data)
        if not cds_replace:
            if not self.static_source:
                self._update_datasource(source, data)
        elif self.document:
            server = self.renderer.mode == 'server'
            with hold_policy(self.document, 'collect', server=server):
                empty_data = {c: [] for c in columns}
                event = ModelChangedEvent(self.document, source, 'data',
                                          source.data, empty_data, empty_data,
                                          setter='empty')
                self.document._held_events.append(event)

        if legend is not None:
            for leg in self.state.legend:
                for item in leg.items:
                    if renderer in item.renderers:
                        item.label = legend

        for glyph, update in glyph_updates:
            glyph.update(**update)

        if data is not None and cds_replace and not self.static_source:
            return self._update_datasource(source, data)


    def _postprocess_hover(self, renderer, source):
        """
        Attaches renderer to hover tool and processes tooltips to
        ensure datetime data is displayed correctly.
        """
        hover = self.handles.get('hover')
        if hover is None:
            return
        if hover.renderers == 'auto':
            hover.renderers = []
        hover.renderers.append(renderer)

        # If datetime column is in the data replace hover formatter
        for k, v in source.data.items():
            if k+'_dt_strings' in source.data:
                tooltips = []
                for name, formatter in hover.tooltips:
                    if formatter == '@{%s}' % k:
                        formatter = '@{%s_dt_strings}' % k
                    tooltips.append((name, formatter))
                hover.tooltips = tooltips


    def _init_glyphs(self, plot, element, ranges, source):
        style_element = element.last if self.batched else element

        # Get data and initialize data source
        if self.batched:
            current_id = tuple(element.traverse(lambda x: x._plot_id, [Element]))
            data, mapping, style = self.get_batched_data(element, ranges)
        else:
            style = self.style[self.cyclic_index]
            data, mapping, style = self.get_data(element, ranges, style)
            current_id = element._plot_id

        with abbreviated_exception():
            style = self._apply_transforms(element, data, ranges, style)

        if source is None:
            source = self._init_datasource(data)
        self.handles['previous_id'] = current_id
        self.handles['source'] = self.handles['cds'] = source
        self.handles['selected'] = source.selected

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

        self._postprocess_hover(renderer, source)

        # Update plot, source and glyph
        with abbreviated_exception():
            self._update_glyph(renderer, properties, mapping, glyph, source, source.data)


    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
        if self.batched:
            element = [el for el in self.hmap.data.values() if el][-1]
        else:
            element = self.hmap.last
        key = util.wrap_tuple(self.hmap.last_key)
        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)
        else:
            self.handles['xaxis'] = plot.xaxis[0]
            self.handles['x_range'] = plot.x_range
            self.handles['yaxis'] = plot.yaxis[0]
            self.handles['y_range'] = plot.y_range
        self.handles['plot'] = plot

        self._init_glyphs(plot, element, ranges, source)
        if not self.overlaid:
            self._update_plot(key, plot, style_element)
            self._update_ranges(style_element, ranges)

        for cb in self.callbacks:
            cb.initialize()

        if not self.overlaid:
            self._set_active_tools(plot)
            self._process_legend()
        self._execute_hooks(element)

        self.drawn = True

        return plot


    def _update_glyphs(self, element, ranges, style):
        plot = self.handles['plot']
        glyph = self.handles.get('glyph')
        source = self.handles['source']
        mapping = {}

        # Cache frame object id to skip updating data if unchanged
        previous_id = self.handles.get('previous_id', None)
        if self.batched:
            current_id = tuple(element.traverse(lambda x: x._plot_id, [Element]))
        else:
            current_id = element._plot_id
        self.handles['previous_id'] = current_id
        self.static_source = (self.dynamic and (current_id == previous_id))
        if self.batched:
            data, mapping, style = self.get_batched_data(element, ranges)
        else:
            data, mapping, style = self.get_data(element, ranges, style)

        with abbreviated_exception():
            style = self._apply_transforms(element, data, ranges, style)

        if glyph:
            properties = self._glyph_properties(plot, element, source, ranges, style)
            renderer = self.handles.get('glyph_renderer')
            with abbreviated_exception():
                self._update_glyph(renderer, properties, mapping, glyph, source, data)
        elif not self.static_source:
            self._update_datasource(source, data)


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

        renderer = self.handles.get('glyph_renderer', None)
        glyph = self.handles.get('glyph', None)
        visible = element is not None
        if hasattr(renderer, 'visible'):
            renderer.visible = visible
        if hasattr(glyph, 'visible'):
            glyph.visible = visible

        if ((self.batched and not element) or element is None or (not self.dynamic and self.static) or
            (self.streaming and self.streaming[0].data is self.current_frame.data and not self.streaming[0]._triggering)):
            return

        if self.batched:
            style_element = element.last
            max_cycles = None
        else:
            style_element = element
            max_cycles = self.style._max_cycles
        style = self.lookup_options(style_element, 'style')
        self.style = style.max_cycles(max_cycles) if max_cycles else style

        ranges = self.compute_ranges(self.hmap, key, ranges)
        self.param.set_param(**self.lookup_options(style_element, 'plot').options)
        ranges = util.match_spec(style_element, ranges)
        self.current_ranges = ranges
        plot = self.handles['plot']
        if not self.overlaid:
            self._update_ranges(style_element, ranges)
            self._update_plot(key, plot, style_element)
            self._set_active_tools(plot)

        self._update_glyphs(element, ranges, self.style[self.cyclic_index])
        self._execute_hooks(element)


    def model_changed(self, model):
        """
        Determines if the bokeh model was just changed on the frontend.
        Useful to suppress boomeranging events, e.g. when the frontend
        just sent an update to the x_range this should not trigger an
        update on the backend.
        """
        callbacks = [cb for cbs in self.traverse(lambda x: x.callbacks)
                     for cb in cbs]
        stream_metadata = [stream._metadata for cb in callbacks
                           for stream in cb.streams if stream._metadata]
        return any(md['id'] == model.ref['id'] for models in stream_metadata
                   for md in models.values())


    @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 [])]
        current_frames = util.unique_iterator(current_frames)
        return any(self.lookup_options(frame, 'norm').options.get('framewise')
                   for frame in current_frames)


class CompositeElementPlot(ElementPlot):
    """
    A CompositeElementPlot is an Element plot type that coordinates
    drawing of multiple glyphs.
    """

    # Mapping between glyph names and style groups
    _style_groups = {}

    # Defines the order in which glyphs are drawn, defined by glyph name
    _draw_order = []

    def _init_glyphs(self, plot, element, ranges, source, data=None, mapping=None, style=None):
        # Get data and initialize data source
        if None in (data, mapping):
            style = self.style[self.cyclic_index]
            data, mapping, style = self.get_data(element, ranges, style)


        keys = glyph_order(dict(data, **mapping), self._draw_order)

        source_cache = {}
        current_id = element._plot_id
        self.handles['previous_id'] = current_id
        for key in keys:
            style_group = self._style_groups.get('_'.join(key.split('_')[:-1]))
            group_style = dict(style)
            ds_data = data.get(key, {})
            with abbreviated_exception():
                group_style = self._apply_transforms(element, ds_data, ranges, group_style, style_group)
            if id(ds_data) in source_cache:
                source = source_cache[id(ds_data)]
            else:
                source = self._init_datasource(ds_data)
                source_cache[id(ds_data)] = source
            self.handles[key+'_source'] = source
            properties = self._glyph_properties(plot, element, source, ranges, group_style, style_group)
            properties = self._process_properties(key, properties, mapping.get(key, {}))

            with abbreviated_exception():
                renderer, glyph = self._init_glyph(plot, mapping.get(key, {}), properties, key)
            self.handles[key+'_glyph'] = glyph
            if isinstance(renderer, Renderer):
                self.handles[key+'_glyph_renderer'] = renderer

            self._postprocess_hover(renderer, source)

            # Update plot, source and glyph
            with abbreviated_exception():
                self._update_glyph(renderer, properties, mapping.get(key, {}), glyph,
                                   source, source.data)
        if getattr(self, 'colorbar', False):
            for k, v in list(self.handles.items()):
                if not k.endswith('color_mapper'):
                    continue
                self._draw_colorbar(plot, v, k[:-12])


    def _process_properties(self, key, properties, mapping):
        key = '_'.join(key.split('_')[:-1]) if '_' in key else key
        style_group = self._style_groups[key]
        group_props = {}
        for k, v in properties.items():
            if k in self.style_opts:
                group = k.split('_')[0]
                if group == style_group:
                    if k in mapping:
                        v = mapping[k]
                    k = '_'.join(k.split('_')[1:])
                else:
                    continue
            group_props[k] = v
        return group_props


    def _update_glyphs(self, element, ranges, style):
        plot = self.handles['plot']

        # Cache frame object id to skip updating data if unchanged
        previous_id = self.handles.get('previous_id', None)
        if self.batched:
            current_id = tuple(element.traverse(lambda x: x._plot_id, [Element]))
        else:
            current_id = element._plot_id
        self.handles['previous_id'] = current_id
        self.static_source = (self.dynamic and (current_id == previous_id))
        data, mapping, style = self.get_data(element, ranges, style)

        keys = glyph_order(dict(data, **mapping), self._draw_order)
        for key in keys:
            gdata = data.get(key)
            source = self.handles[key+'_source']
            glyph = self.handles.get(key+'_glyph')
            if glyph:
                group_style = dict(style)
                style_group = self._style_groups.get('_'.join(key.split('_')[:-1]))
                with abbreviated_exception():
                    group_style = self._apply_transforms(element, gdata, ranges, group_style, style_group)
                properties = self._glyph_properties(plot, element, source, ranges, group_style, style_group)
                properties = self._process_properties(key, properties, mapping[key])
                renderer = self.handles.get(key+'_glyph_renderer')
                with abbreviated_exception():
                    self._update_glyph(renderer, properties, mapping[key],
                                       glyph, source, gdata)
            elif not self.static_source and gdata is not None:
                self._update_datasource(source, gdata)


    def _init_glyph(self, plot, mapping, properties, key):
        """
        Returns a Bokeh glyph object.
        """
        properties = mpl_to_bokeh(properties)
        plot_method = '_'.join(key.split('_')[:-1])
        renderer = getattr(plot, plot_method)(**dict(properties, **mapping))
        return renderer, renderer.glyph



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'}}}

    color_levels = param.ClassSelector(default=None, class_=(int, list), doc="""
        Number of discrete colors to use when colormapping or a set of color
        intervals defining the range of values to map each color to.""")

    clabel = param.String(default=None, doc="""
        An explicit override of the color bar label, if set takes precedence
        over the title key in colorbar_opts.""")

    clim = param.NumericTuple(default=(np.nan, np.nan), length=2, doc="""
       User-specified colorbar axis range limits for the plot, as a tuple (low,high).
       If specified, takes precedence over data and dimension ranges.""")

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

    clipping_colors = param.Dict(default={}, doc="""
        Dictionary to specify colors for clipped values, allows
        setting color for NaN values and for values above and below
        the min and max value. The min, max or NaN color may specify
        an RGB(A) color as a color hex string of the form #FFFFFF or
        #FFFFFFFF or a length 3 or length 4 tuple specifying values in
        the range 0-1 or a named HTML color.""")

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

    symmetric = param.Boolean(default=False, doc="""
        Whether to make the colormap symmetric around zero.""")

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

    _default_nan = '#8b8b8b'

    _nonvectorized_styles = ['cmap', 'palette']

    def _draw_colorbar(self, plot, color_mapper, prefix=''):
        if CategoricalColorMapper and isinstance(color_mapper, CategoricalColorMapper):
            return
        if LogColorMapper and isinstance(color_mapper, LogColorMapper) and color_mapper.low > 0:
            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

        if self.clabel:
            self.colorbar_opts.update({'title': self.clabel})
        opts = dict(cbar_opts['opts'], color_mapper=color_mapper, ticker=ticker,
                    **self._colorbar_defaults)
        color_bar = ColorBar(**dict(opts, **self.colorbar_opts))

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


    def _get_colormapper(self, eldim, element, ranges, style, factors=None, colors=None,
                         group=None, name='color_mapper'):
        # The initial colormapper instance is cached the first time
        # and then only updated
        if eldim is None and colors is None:
            return None
        dim_name = dim_range_key(eldim)

        # Attempt to find matching colormapper on the adjoined plot
        if self.adjoined:
            cmapper_name = dim_name+name
            cmappers = self.adjoined.traverse(lambda x: (x.handles.get('color_dim'),
                                                         x.handles.get(name, x.handles.get(cmapper_name))))
            cmappers = [cmap for cdim, cmap in cmappers if cdim == eldim]
            if cmappers:
                cmapper = cmappers[0]
                self.handles['color_mapper'] = cmapper
                return cmapper
            else:
                return None

        ncolors = None if factors is None else len(factors)
        if eldim:
            # check if there's an actual value (not np.nan)
            if util.isfinite(self.clim).all():
                low, high = self.clim
            elif dim_name in ranges:
                low, high = ranges[dim_name]['combined']
            elif isinstance(eldim, dim):
                low, high = np.nan, np.nan
            else:
                low, high = element.range(eldim.name)
            if self.symmetric:
                sym_max = max(abs(low), high)
                low, high = -sym_max, sym_max
        else:
            low, high = None, None

        prefix = '' if group is None else group+'_'
        cmap = colors or style.get(prefix+'cmap', style.get('cmap', 'viridis'))
        nan_colors = {k: rgba_tuple(v) for k, v in self.clipping_colors.items()}
        if isinstance(cmap, dict):
            if factors is None:
                factors = list(cmap)
            palette = [cmap.get(f, nan_colors.get('NaN', self._default_nan)) for f in factors]
            if isinstance(eldim, dim):
                if eldim.dimension in element:
                    formatter = element.get_dimension(eldim.dimension).pprint_value
                else:
                    formatter = str
            else:
                formatter = eldim.pprint_value
            factors = [formatter(f) for f in factors]
        else:
            categorical = ncolors is not None
            if isinstance(self.color_levels, int):
                ncolors = self.color_levels
            elif isinstance(self.color_levels, list):
                ncolors = len(self.color_levels) - 1
                if isinstance(cmap, list) and len(cmap) != ncolors:
                    raise ValueError('The number of colors in the colormap '
                                     'must match the intervals defined in the '
                                     'color_levels, expected %d colors found %d.'
                                     % (ncolors, len(cmap)))
            palette = process_cmap(cmap, ncolors, categorical=categorical)
            if isinstance(self.color_levels, list):
                palette, (low, high) = color_intervals(palette, self.color_levels, clip=(low, high))
        colormapper, opts = self._get_cmapper_opts(low, high, factors, nan_colors)

        cmapper = self.handles.get(name)
        if cmapper is not None:
            if cmapper.palette != palette:
                cmapper.palette = palette
            opts = {k: opt for k, opt in opts.items()
                    if getattr(cmapper, k) != opt}
            if opts:
                cmapper.update(**opts)
        else:
            cmapper = colormapper(palette=palette, **opts)
            self.handles[name] = cmapper
            self.handles['color_dim'] = eldim
        return cmapper


    def _get_color_data(self, element, ranges, style, name='color', factors=None, colors=None,
                        int_categories=False):
        data, mapping = {}, {}
        cdim = element.get_dimension(self.color_index)
        color = style.get(name, None)
        if cdim and ((isinstance(color, util.basestring) and color in element) or isinstance(color, dim)):
            self.param.warning(
                "Cannot declare style mapping for '%s' option and "
                "declare a color_index; ignoring the color_index."
                % name)
            cdim = None
        if not cdim:
            return data, mapping

        cdata = element.dimension_values(cdim)
        field = util.dimension_sanitizer(cdim.name)
        dtypes = 'iOSU' if int_categories else 'OSU'

        if factors is None and (isinstance(cdata, list) or cdata.dtype.kind in dtypes):
            range_key = dim_range_key(cdim)
            if range_key in ranges and 'factors' in ranges[range_key]:
                factors = ranges[range_key]['factors']
            else:
                factors = util.unique_array(cdata)
        if factors is not None and int_categories and cdata.dtype.kind == 'i':
            field += '_str__'
            cdata = [str(f) for f in cdata]
            factors = [str(f) for f in factors]

        mapper = self._get_colormapper(cdim, element, ranges, style,
                                       factors, colors)
        if factors is None and isinstance(mapper, CategoricalColorMapper):
            field += '_str__'
            cdata = [cdim.pprint_value(c) for c in cdata]
            factors = True

        data[field] = cdata
        if factors is not None and self.show_legend:
            mapping['legend'] = {'field': field}
        mapping[name] = {'field': field, 'transform': mapper}

        return data, mapping


    def _get_cmapper_opts(self, low, high, factors, colors):
        if factors is None:
            colormapper = LogColorMapper if self.logz else LinearColorMapper
            if isinstance(low, (bool, np.bool_)): low = int(low)
            if isinstance(high, (bool, np.bool_)): high = int(high)
            # Pad zero-range to avoid breaking colorbar (as of bokeh 1.0.4)
            if low == high:
                offset = self.default_span / 2
                low -= offset
                high += offset
            opts = {}
            if util.isfinite(low):
                opts['low'] = low
            if util.isfinite(high):
                opts['high'] = high
            color_opts = [('NaN', 'nan_color'), ('max', 'high_color'), ('min', 'low_color')]
            opts.update({opt: colors[name] for name, opt in color_opts if name in colors})
        else:
            colormapper = CategoricalColorMapper
            factors = decode_bytes(factors)
            opts = dict(factors=list(factors))
            if 'NaN' in colors:
                opts['nan_color'] = colors['NaN']
        return colormapper, opts


    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:
            for k, v in list(self.handles.items()):
                if not k.endswith('color_mapper'):
                    continue
                self._draw_colorbar(plot, v, k[:-12])
        return ret


class LegendPlot(ElementPlot):

    legend_position = param.ObjectSelector(objects=["top_right",
                                                    "top_left",
                                                    "bottom_left",
                                                    "bottom_right",
                                                    'right', 'left',
                                                    'top', 'bottom'],
                                                    default="top_right",
                                                    doc="""
        Allows selecting between a number of predefined legend position
        options. The predefined options may be customized in the
        legend_specs class attribute.""")

    legend_muted = param.Boolean(default=False, doc="""
        Controls whether the legend entries are muted by default.""")

    legend_offset = param.NumericTuple(default=(0, 0), doc="""
        If legend is placed outside the axis, this determines the
        (width, height) offset in pixels from the original position.""")

    legend_cols = param.Integer(default=False, doc="""
       Whether to lay out the legend as columns.""")

    legend_specs = {'right': 'right', 'left': 'left', 'top': 'above',
                    'bottom': 'below'}

    def _process_legend(self, plot=None):
        plot = plot or self.handles['plot']
        if not plot.legend:
            return
        legend = plot.legend[0]
        cmappers = [cmapper for cmapper in self.handles.values()
                   if isinstance(cmapper, CategoricalColorMapper)]
        categorical = bool(cmappers)
        if ((not categorical and not self.overlaid and len(legend.items) == 1)
            or not self.show_legend):
            legend.items[:] = []
        else:
            plot.legend.orientation = 'horizontal' if self.legend_cols else 'vertical'
            pos = self.legend_position
            if pos in self.legend_specs:
                plot.legend[:] = []
                legend.location = self.legend_offset
                if pos in ['top', 'bottom']:
                    plot.legend.orientation = 'horizontal'
                plot.add_layout(legend, self.legend_specs[pos])
            else:
                legend.location = pos

            # Apply muting
            for leg in plot.legend:
                for item in leg.items:
                    for r in item.renderers:
                        r.muted = self.legend_muted



class AnnotationPlot(object):
    """
    Mix-in plotting subclass for AnnotationPlots which do not have a legend.
    """


class OverlayPlot(GenericOverlayPlot, LegendPlot):

    tabs = param.Boolean(default=False, doc="""
        Whether to display overlaid plots in separate panes""")

    style_opts = (legend_dimensions + ['border_'+p for p in line_properties] +
                  text_properties + ['background_fill_color', 'background_fill_alpha'])

    multiple_legends = param.Boolean(default=False, doc="""
        Whether to split the legend for subplots into multiple legends.""")

    _propagate_options = ['width', 'height', 'xaxis', 'yaxis', 'labelled',
                          'bgcolor', 'fontsize', 'invert_axes', 'show_frame',
                          'show_grid', 'logx', 'logy', 'xticks', 'toolbar',
                          'yticks', 'xrotation', 'yrotation', 'lod',
                          'border', 'invert_xaxis', 'invert_yaxis', 'sizing_mode',
                          'title', 'title_format', 'legend_position', 'legend_offset',
                          'legend_cols', 'gridstyle', 'legend_muted', 'padding',
                          'xlabel', 'ylabel', 'xlim', 'ylim', 'zlim',
                          'xformatter', 'yformatter', 'active_tools',
                          'min_height', 'max_height', 'min_width', 'min_height',
                          'margin', 'aspect', 'data_aspect', 'frame_width',
                          'frame_height', 'responsive']

    def __init__(self, overlay, **params):
        super(OverlayPlot, self).__init__(overlay, **params)
        self.set_root(params.pop('root', None))

    def _process_legend(self):
        plot = self.handles['plot']
        subplots = self.traverse(lambda x: x, [lambda x: x is not self])
        legend_plots = any(p is not None for p in subplots
                           if isinstance(p, LegendPlot) and
                           not isinstance(p, OverlayPlot))
        non_annotation = [p for p in subplots if not
                          (isinstance(p, OverlayPlot) or isinstance(p, AnnotationPlot))]
        if (not self.show_legend or len(plot.legend) == 0 or
            (len(non_annotation) <= 1 and not (self.dynamic or legend_plots))):
            return super(OverlayPlot, self)._process_legend()

        options = {}
        properties = self.lookup_options(self.hmap.last, 'style')[self.cyclic_index]
        for k, v in properties.items():
            if k in line_properties and 'line' not in k:
                ksplit = k.split('_')
                k = '_'.join(ksplit[:1]+'line'+ksplit[1:])
            if k in text_properties:
                k = 'label_' + k
            if k.startswith('legend_'):
                k = k[7:]
            options[k] = v

        if not plot.legend:
            return

        pos = self.legend_position
        orientation = 'horizontal' if self.legend_cols else 'vertical'
        if pos in ['top', 'bottom']:
            orientation = 'horizontal'

        legend_fontsize = self._fontsize('legend', 'size').get('size',False)
        legend = plot.legend[0]
        legend.update(**options)
        if legend_fontsize:
            legend.label_text_font_size = value(legend_fontsize)

        if pos in self.legend_specs:
            pos = self.legend_specs[pos]
        else:
            legend.location = pos

        legend.orientation = orientation

        if 'legend_items' not in self.handles:
            self.handles['legend_items'] = []
        legend_items = self.handles['legend_items']
        legend_labels = {tuple(sorted(i.label.items())) if isinstance(i.label, dict) else i.label: i
                         for i in legend_items}
        for item in legend.items:
            label = tuple(sorted(item.label.items())) if isinstance(item.label, dict) else item.label
            if not label or (isinstance(item.label, dict) and not item.label.get('value', True)):
                continue
            if label in legend_labels:
                prev_item = legend_labels[label]
                prev_item.renderers[:] = list(util.unique_iterator(prev_item.renderers+item.renderers))
            else:
                legend_labels[label] = item
                legend_items.append(item)
                if item not in self.handles['legend_items']:
                    self.handles['legend_items'].append(item)

        # Ensure that each renderer is only singly referenced by a legend item
        filtered = []
        renderers = []
        for item in legend_items:
            item.renderers[:] = [r for r in item.renderers if r not in renderers]
            if item in filtered or not item.renderers or not any(r.visible for r in item.renderers):
                continue
            renderers += item.renderers
            filtered.append(item)
        legend.items[:] = list(util.unique_iterator(filtered))

        if self.multiple_legends:
            plot.legend.pop(plot.legend.index(legend))
            legend.plot = None
            properties = legend.properties_with_values(include_defaults=False)
            legend_group = []
            for item in legend.items:
                if not isinstance(item.label, dict) or 'value'  in item.label:
                    legend_group.append(item)
                    continue
                new_legend = Legend(**dict(properties, items=[item]))
                new_legend.location = self.legend_offset
                plot.add_layout(new_legend, pos)
            if legend_group:
                new_legend = Legend(**dict(properties, items=legend_group))
                new_legend.location = self.legend_offset
                plot.add_layout(new_legend, pos)
            legend.items[:] = []
        elif pos in ['above', 'below', 'right', 'left']:
            plot.legend.pop(plot.legend.index(legend))
            legend.location = self.legend_offset
            plot.add_layout(legend, pos)

        # Apply muting
        for leg in plot.legend:
            for item in leg.items:
                for r in item.renderers:
                    r.muted = self.legend_muted


    def _init_tools(self, element, callbacks=[]):
        """
        Processes the list of tools to be supplied to the plot.
        """
        hover_tools = {}
        init_tools, tool_types = [], []
        for key, subplot in self.subplots.items():
            el = element.get(key)
            if el is not None:
                el_tools = subplot._init_tools(el, self.callbacks)
                for tool in el_tools:
                    if isinstance(tool, util.basestring):
                        tool_type = TOOL_TYPES.get(tool)
                    else:
                        tool_type = type(tool)
                    if isinstance(tool, tools.HoverTool):
                        tooltips = tuple(tool.tooltips) if tool.tooltips else ()
                        if tooltips in hover_tools:
                            continue
                        else:
                            hover_tools[tooltips] = tool
                    elif tool_type in tool_types:
                        continue
                    else:
                        tool_types.append(tool_type)
                    init_tools.append(tool)
        self.handles['hover_tools'] = hover_tools
        return init_tools


    def _merge_tools(self, subplot):
        """
        Merges tools on the overlay with those on the subplots.
        """
        if self.batched and 'hover' in subplot.handles:
            self.handles['hover'] = subplot.handles['hover']
        elif 'hover' in subplot.handles and 'hover_tools' in self.handles:
            hover = subplot.handles['hover']
            # Datetime formatter may have been applied, remove _dt_strings
            # to match on the hover tooltips, then merge tool renderers
            if hover.tooltips and not isinstance(hover.tooltips, util.basestring):
                tooltips = tuple((name, spec.replace('_dt_strings', ''))
                                  for name, spec in hover.tooltips)
            else:
                tooltips = ()
            tool = self.handles['hover_tools'].get(tooltips)
            if tool:
                tool_renderers = [] if tool.renderers == 'auto' else tool.renderers
                hover_renderers = [] if hover.renderers == 'auto' else hover.renderers
                renderers = tool_renderers + hover_renderers
                tool.renderers = list(util.unique_iterator(renderers))
            if 'hover' not in self.handles:
                self.handles['hover'] = tool


    def _get_factors(self, overlay):
        xfactors, yfactors = [], []
        for k, sp in self.subplots.items():
            el = overlay.data.get(k)
            if el is not None:
                xfs, yfs = sp._get_factors(el)
                xfactors.append(xfs)
                yfactors.append(yfs)
        if xfactors:
            xfactors = np.concatenate(xfactors)
        if yfactors:
            yfactors = np.concatenate(yfactors)
        return util.unique_array(xfactors), util.unique_array(yfactors)


    def _get_axis_dims(self, element):
        subplots = list(self.subplots.values())
        if subplots:
            return subplots[0]._get_axis_dims(element)
        return super(OverlayPlot, self)._get_axis_dims(element)


    def initialize_plot(self, ranges=None, plot=None, plots=None):
        key = util.wrap_tuple(self.hmap.last_key)
        nonempty = [(k, el) for k, el in self.hmap.data.items() if el]
        if not nonempty:
            raise SkipRendering('All Overlays empty, cannot initialize plot.')
        dkey, element = nonempty[-1]
        ranges = self.compute_ranges(self.hmap, key, ranges)
        if plot is None and not self.tabs and not self.batched:
            plot = self._init_plot(key, element, ranges=ranges, plots=plots)
            self._init_axes(plot)
        self.handles['plot'] = plot

        if plot and not self.overlaid:
            self._update_plot(key, plot, element)
            self._update_ranges(element, ranges)

        panels = []
        for key, subplot in self.subplots.items():
            frame = None
            if self.tabs:
                subplot.overlaid = False
            child = subplot.initialize_plot(ranges, plot, plots)
            if isinstance(element, CompositeOverlay):
                # Ensure that all subplots are in the same state
                frame = element.get(key, None)
                subplot.current_frame = frame
                subplot.current_key = dkey
            if self.batched:
                self.handles['plot'] = child
            if self.tabs:
                title = subplot._format_title(key, dimensions=False)
                if not title:
                    title = get_tab_title(key, frame, self.hmap.last)
                panels.append(Panel(child=child, title=title))
            self._merge_tools(subplot)

        if self.tabs:
            self.handles['plot'] = Tabs(tabs=panels)
        elif not self.overlaid:
            self._process_legend()
            self._set_active_tools(plot)
        self.drawn = True
        self.handles['plots'] = plots

        self._update_callbacks(self.handles['plot'])
        if 'plot' in self.handles and not self.tabs:
            plot = self.handles['plot']
            self.handles['xaxis'] = plot.xaxis[0]
            self.handles['yaxis'] = plot.yaxis[0]
            self.handles['x_range'] = plot.x_range
            self.handles['y_range'] = plot.y_range
        for cb in self.callbacks:
            cb.initialize()

        if self.top_level:
            self.init_links()

        self._execute_hooks(element)

        return self.handles['plot']


    def update_frame(self, key, ranges=None, element=None):
        """
        Update the internal state of the Plot to represent the given
        key tuple (where integers represent frames). Returns this
        state.
        """
        reused = isinstance(self.hmap, DynamicMap) and self.overlaid
        if not reused and element is None:
            element = self._get_frame(key)
        elif element is not None:
            self.current_frame = element
            self.current_key = key
        items = [] if element is None else list(element.data.items())

        if isinstance(self.hmap, DynamicMap):
            range_obj = element
        else:
            range_obj = self.hmap

        if element is not None:
            ranges = self.compute_ranges(range_obj, key, ranges)

        # Update plot options
        plot_opts = self.lookup_options(element, 'plot').options
        inherited = self._traverse_options(element, 'plot',
                                           self._propagate_options,
                                           defaults=False)
        plot_opts.update(**{k: v[0] for k, v in inherited.items() if k not in plot_opts})
        self.param.set_param(**plot_opts)

        if element and not self.overlaid and not self.tabs and not self.batched:
            self._update_ranges(element, ranges)

        # Determine which stream (if any) triggered the update
        triggering = [stream for stream in self.streams if stream._triggering]

        for k, subplot in self.subplots.items():
            el = None

            # If in Dynamic mode propagate elements to subplots
            if isinstance(self.hmap, DynamicMap) and element:
                # In batched mode NdOverlay is passed to subplot directly
                if self.batched:
                    el = element
                # If not batched get the Element matching the subplot
                elif element is not None:
                    idx, spec, exact = dynamic_update(self, subplot, k, element, items)
                    if idx is not None:
                        _, el = items.pop(idx)
                        if not exact:
                            self._update_subplot(subplot, spec)

                # Skip updates to subplots when its streams is not one of
                # the streams that initiated the update
                if (triggering and all(s not in triggering for s in subplot.streams) and
                    not subplot in self.dynamic_subplots):
                    continue
            subplot.update_frame(key, ranges, element=el)

        if not self.batched and isinstance(self.hmap, DynamicMap) and items:
            init_kwargs = {'plots': self.handles['plots']}
            if not self.tabs:
                init_kwargs['plot'] = self.handles['plot']
            self._create_dynamic_subplots(key, items, ranges, **init_kwargs)
            if not self.overlaid and not self.tabs:
                self._process_legend()

        if element and not self.overlaid and not self.tabs and not self.batched:
            plot = self.handles['plot']
            self._update_plot(key, plot, element)
            self._set_active_tools(plot)

        self._process_legend()

        self._execute_hooks(element)
