Source code for panel.pane.holoviews

HoloViews integration for Panel including a Pane to render HoloViews
objects and their widgets and support for Links
from __future__ import annotations

import sys

from collections import OrderedDict, defaultdict
from functools import partial
from typing import (
    TYPE_CHECKING, Any, ClassVar, Mapping, Optional, Type,

import param

from bokeh.models import Range1d, Spacer as _BkSpacer
from bokeh.themes.theme import Theme
from packaging.version import Version

from import state, unlocked
from ..layout import (
    Column, HSpacer, Row, VSpacer, WidgetBox,
from ..viewable import Layoutable, Viewable
from ..widgets import Player
from .base import Pane, PaneBase, RerenderError
from .plot import Bokeh, Matplotlib
from .plotly import Plotly

    from bokeh.document import Document
    from bokeh.model import Model
    from pyviz_comms import Comm

[docs]class HoloViews(PaneBase): """ `HoloViews` panes render any `HoloViews` object using the currently selected backend ('bokeh' (default), 'matplotlib' or 'plotly'). To be able to use the `plotly` backend you must add `plotly` to `pn.extension`. Reference: :Example: >>> HoloViews(some_holoviews_object) """ backend = param.ObjectSelector( default=None, objects=['bokeh', 'plotly', 'matplotlib'], doc=""" The HoloViews backend used to render the plot (if None defaults to the currently selected renderer).""") center = param.Boolean(default=False, doc=""" Whether to center the plot.""") linked_axes = param.Boolean(default=True, doc=""" Whether to link the axes of bokeh plots inside this pane across a panel layout.""") renderer = param.Parameter(default=None, doc=""" Explicit renderer instance to use for rendering the HoloViews plot. Overrides the backend.""") theme = param.ClassSelector(default=None, class_=(Theme, str), allow_None=True, doc=""" Bokeh theme to apply to the HoloViews plot.""") widget_location = param.ObjectSelector(default='right_top', objects=[ 'left', 'bottom', 'right', 'top', 'top_left', 'top_right', 'bottom_left', 'bottom_right', 'left_top', 'left_bottom', 'right_top', 'right_bottom'], doc=""" The layout of the plot and the widgets. The value refers to the position of the widgets relative to the plot.""") widget_layout = param.ObjectSelector( objects=[WidgetBox, Row, Column], constant=True, default=WidgetBox, doc=""" The layout object to display the widgets in.""") widget_type = param.ObjectSelector(default='individual', objects=['individual', 'scrubber'], doc=""") Whether to generate individual widgets for each dimension or on global scrubber.""") widgets = param.Dict(default={}, doc=""" A mapping from dimension name to a widget instance which will be used to override the default widgets.""") priority: ClassVar[float | bool | None] = 0.8 _panes: ClassVar[Mapping[str, Type[PaneBase]]] = { 'bokeh': Bokeh, 'matplotlib': Matplotlib, 'plotly': Plotly } _rename: ClassVar[Mapping[str, str | None]] = { 'backend': None, 'center': None, 'linked_axes': None, 'renderer': None, 'theme': None, 'widgets': None, 'widget_layout': None, 'widget_location': None, 'widget_type': None } _rerender_params = ['object', 'backend'] def __init__(self, object=None, **params): self._initialized = False self._responsive_content = False self._restore_plot = None super().__init__(object, **params) self.widget_box = self.widget_layout() self._widget_container = [] self._plots = {} self._syncing_props = False self._overrides = [ p for p, v in params.items() if p in Layoutable.param and v != self.param[p].default ] watcher =, self._rerender_params) self._callbacks.append(watcher) self._initialized = True self._update_responsive() self._update_widgets() def _param_change(self, *events: param.parameterized.Event) -> None: self._track_overrides(*(e for e in events if in Layoutable.param)) super()._param_change(*(e for e in events if in self._overrides)) @param.depends('center', 'widget_location', watch=True) def _update_layout(self): loc = self.widget_location if not len(self.widget_box): widgets = [] elif loc in ('left', 'right'): widgets = Column(VSpacer(), self.widget_box, VSpacer()) elif loc in ('top', 'bottom'): widgets = Row(HSpacer(), self.widget_box, HSpacer()) elif loc in ('top_left', 'bottom_left'): widgets = Row(self.widget_box, HSpacer()) elif loc in ('top_right', 'bottom_right'): widgets = Row(HSpacer(), self.widget_box) elif loc in ('left_top', 'right_top'): widgets = Column(self.widget_box, VSpacer()) elif loc in ('left_bottom', 'right_bottom'): widgets = Column(VSpacer(), self.widget_box) center = and not self._responsive_content self._widget_container = widgets if not widgets: if center: components = [HSpacer(), self, HSpacer()] else: components = [self] elif center: if loc.startswith('left'): components = [widgets, HSpacer(), self, HSpacer()] elif loc.startswith('right'): components = [HSpacer(), self, HSpacer(), widgets] elif loc.startswith('top'): components = [HSpacer(), Column(widgets, Row(HSpacer(), self, HSpacer())), HSpacer()] elif loc.startswith('bottom'): components = [HSpacer(), Column(Row(HSpacer(), self, HSpacer()), widgets), HSpacer()] else: if loc.startswith('left'): components = [widgets, self] elif loc.startswith('right'): components = [self, widgets] elif loc.startswith('top'): components = [Column(widgets, self)] elif loc.startswith('bottom'): components = [Column(self, widgets)] self.layout[:] = components #---------------------------------------------------------------- # Callback API #---------------------------------------------------------------- @param.depends('theme', watch=True) def _update_theme(self, *events): if self.theme is None: return for (model, _) in self._models.values(): if model.document: model.document.theme = self.theme @param.depends('object', watch=True) def _update_responsive(self): from holoviews import HoloMap, Store from holoviews.plotting import Plot obj = self.object if isinstance(obj, Plot): if 'responsive' in obj.param: responsive = obj.responsive elif 'sizing_mode' in obj.param: mode = obj.sizing_mode if mode: responsive = '_width' in mode or '_both' in mode else: responsive = False else: responsive = False self._responsive_content = responsive return obj = obj.last if isinstance(obj, HoloMap) else obj if obj is None or not Store.renderers: return backend = self.backend or Store.current_backend renderer = self.renderer or Store.renderers[backend] opts = obj.opts.get('plot', backend=backend).kwargs plot_cls = renderer.plotting_class(obj) if backend == 'matplotlib': self._responsive_content = False elif backend == 'plotly': responsive = opts.get('responsive', None) width = opts.get('width', None) self._responsive_content = responsive and not width elif 'sizing_mode' in plot_cls.param: mode = opts.get('sizing_mode') if mode: self._responsive_content = '_width' in mode or '_both' in mode else: self._responsive_content = False else: responsive = opts.get('responsive', None) width = opts.get('width', None) frame_width = opts.get('frame_width', None) self._responsive_content = responsive and not width and not frame_width @param.depends('widget_type', 'widgets', watch=True) def _update_widgets(self, *events): if self.object is None: widgets, values = [], [] else: widgets, values = self.widgets_from_dimensions( self.object, self.widgets, self.widget_type) self._values = values # Clean up anything models listening to the previous widgets for cb in list(self._callbacks): if cb.inst in self.widget_box.objects: cb.inst.param.unwatch(cb) self._callbacks.remove(cb) # Add new widget callbacks for widget in widgets: watcher =, 'value') self._callbacks.append(watcher) self.widget_box[:] = widgets if ((widgets and self.widget_box not in self._widget_container) or (not widgets and self.widget_box in self._widget_container) or not self._initialized): self._update_layout() def _update_plot(self, plot, pane): from holoviews.core.util import cross_index, wrap_tuple_streams widgets = self.widget_box.objects if not widgets: return elif self.widget_type == 'scrubber': key = cross_index([v for v in self._values.values()], widgets[0].value) else: key = tuple(w.value for w in widgets) if plot.dynamic: widget_dims = [ for w in widgets] key = [key[widget_dims.index(kdim)] if kdim in widget_dims else None for kdim in plot.dimensions] key = wrap_tuple_streams(tuple(key), plot.dimensions, plot.streams) if plot.backend == 'bokeh': if plot.comm or state._unblocked(plot.document): with unlocked(): plot.update(key) if plot.comm and 'embedded' not in plot.root.tags: plot.push() else: if plot.document.session_context: plot.document.add_next_tick_callback(partial(plot.update, key)) else: plot.update(key) else: plot.update(key) if hasattr(plot.renderer, 'get_plot_state'): pane.object = plot.renderer.get_plot_state(plot) else: # Compatibility with holoviews<1.13.0 pane.object = plot.state def _widget_callback(self, event): for _, (plot, pane) in self._plots.items(): self._update_plot(plot, pane) def _track_overrides(self, *events): if self._syncing_props: return overrides = list(self._overrides) for e in events: if in overrides and self.param[].default == overrides.remove( else: overrides.append( self._overrides = overrides def _sync_sizing_mode(self, plot): state = plot.state backend = plot.renderer.backend if backend == 'bokeh': params = { 'sizing_mode': state.sizing_mode, 'width': state.width, 'height': state.height } elif backend == 'matplotlib': params = { 'sizing_mode': None, 'width': None, 'height': None } elif backend == 'plotly': if state.get('config', {}).get('responsive'): sizing_mode = 'stretch_both' else: sizing_mode = None params = { 'sizing_mode': sizing_mode, 'width': None, 'height': None } self._syncing_props = True try: self.param.update({k: v for k, v in params.items() if k not in self._overrides}) props = { o: getattr(self, o) for o in self._overrides } if props: plot.state.update(**props) finally: self._syncing_props = False #---------------------------------------------------------------- # Model API #---------------------------------------------------------------- def _get_model( self, doc: Document, root: Optional[Model] = None, parent: Optional[Model] = None, comm: Optional[Comm] = None ) -> Model: from holoviews.plotting.plot import Plot if root is None: return self.get_root(doc, comm) ref = root.ref['id'] if self.object is None: model = _BkSpacer() self._models[ref] = (model, parent) return model if self._restore_plot is not None: plot = self._restore_plot self._restore_plot = None elif isinstance(self.object, Plot): plot = self.object else: plot = self._render(doc, comm, root) plot.pane = self backend = plot.renderer.backend if hasattr(plot.renderer, 'get_plot_state'): state = plot.renderer.get_plot_state(plot) else: # Compatibility with holoviews<1.13.0 state = plot.state # Ensure rerender if content is responsive but layout is centered self._sync_sizing_mode(plot) responsive = self.sizing_mode not in ('fixed', None) and not self.width if and responsive and not self._responsive_content: self._responsive_content = True self._update_layout() self._restore_plot = plot raise RerenderError() kwargs = {p: v for p, v in self.param.values().items() if p in Layoutable.param and p != 'name'} child_pane = self._get_pane(backend, state, **kwargs) self._update_plot(plot, child_pane) model = child_pane._get_model(doc, root, parent, comm) if ref in self._plots: old_plot, old_pane = self._plots[ref] old_plot.comm = None # Ensures comm does not get cleaned up old_plot.cleanup() self._plots[ref] = (plot, child_pane) self._models[ref] = (model, parent) return model def _get_pane(self, backend, state, **kwargs): pane_type = self._panes.get(backend, Pane) if isinstance(pane_type, type): if issubclass(pane_type, Matplotlib): kwargs['tight'] = True if issubclass(pane_type, Bokeh): kwargs['autodispatch'] = False return pane_type(state, **kwargs) def _render(self, doc, comm, root): import holoviews as hv from holoviews import Store, renderer as load_renderer if self.renderer: renderer = self.renderer backend = renderer.backend else: if not Store.renderers: loaded_backend = (self.backend or 'bokeh') load_renderer(loaded_backend) Store.current_backend = loaded_backend backend = self.backend or Store.current_backend renderer = Store.renderers[backend] mode = 'server' if comm is None else 'default' if backend == 'bokeh': params = {} if self.theme is not None: params['theme'] = self.theme elif doc.theme and getattr(doc.theme, '_json') != {'attrs': {}}: params['theme'] = doc.theme if mode != renderer.mode: params['mode'] = mode if params: renderer = renderer.instance(**params) kwargs = {'margin': self.margin} if backend == 'bokeh' or Version(str(hv.__version__)) >= Version('1.13.0'): kwargs['doc'] = doc kwargs['root'] = root if comm: kwargs['comm'] = comm return renderer.get_plot(self.object, **kwargs) def _cleanup(self, root: Model | None = None) -> None: """ Traverses HoloViews object to find and clean up any streams connected to existing plots. """ if root: old_plot, old_pane = self._plots.pop(root.ref['id'], (None, None)) if old_plot: old_plot.cleanup() if old_pane: old_pane._cleanup(root) super()._cleanup(root) #---------------------------------------------------------------- # Public API #----------------------------------------------------------------
[docs] @classmethod def applies(cls, obj: Any) -> float | bool | None: if 'holoviews' not in sys.modules: return False from holoviews.core.dimension import Dimensioned from holoviews.plotting.plot import Plot return isinstance(obj, Dimensioned) or isinstance(obj, Plot)
jslink.__doc__ = PaneBase.jslink.__doc__ @classmethod def widgets_from_dimensions(cls, object, widget_types=None, widgets_type='individual'): from holoviews.core import Dimension, DynamicMap from holoviews.core.options import SkipRendering from holoviews.core.traversal import unique_dimkeys from holoviews.core.util import ( datetime_types, isnumeric, unique_iterator, ) from holoviews.plotting.plot import GenericCompositePlot, Plot from holoviews.plotting.util import get_dynamic_mode from ..widgets import ( DatetimeInput, DiscreteSlider, FloatSlider, IntSlider, Select, Widget, ) if widget_types is None: widget_types = {} if isinstance(object, GenericCompositePlot): object = object.layout elif isinstance(object, Plot): object = object.hmap if isinstance(object, DynamicMap) and object.unbounded: dims = ', '.join('%r' % dim for dim in object.unbounded) msg = ('DynamicMap cannot be displayed without explicit indexing ' 'as {dims} dimension(s) are unbounded. ' '\nSet dimensions bounds with the DynamicMap redim.range ' 'or redim.values methods.') raise SkipRendering(msg.format(dims=dims)) dynamic, bounded = get_dynamic_mode(object) dims, keys = unique_dimkeys(object) if ((dims == [Dimension('Frame')] and keys == [(0,)]) or (not dynamic and len(keys) == 1)): return [], {} nframes = 1 values = {} if dynamic else dict(zip(dims, zip(*keys))) dim_values = OrderedDict() widgets = [] dims = [d for d in dims if values.get(d) is not None or d.values or d.range != (None, None)] for i, dim in enumerate(dims): widget_type, widget, widget_kwargs = None, None, {} if widgets_type == 'individual': if i == 0 and i == (len(dims)-1): margin = (20, 20, 20, 20) elif i == 0: margin = (20, 20, 5, 20) elif i == (len(dims)-1): margin = (5, 20, 20, 20) else: margin = (0, 20, 5, 20) kwargs = {'margin': margin, 'width': 250} else: kwargs = {} vals = dim.values or values.get(dim, None) if vals is not None: vals = list(unique_iterator(vals)) dim_values[] = vals if widgets_type == 'scrubber': if not vals: raise ValueError('Scrubber widget may only be used if all dimensions define values.') nframes *= len(vals) elif in widget_types: widget = widget_types[] if isinstance(widget, Widget): widget.param.update(**kwargs) if not = dim.label widgets.append(widget) continue elif isinstance(widget, dict): widget_type = widget.get('type', widget_type) widget_kwargs = dict(widget) elif isinstance(widget, type) and issubclass(widget, Widget): widget_type = widget else: raise ValueError('Explicit widget definitions expected ' 'to be a widget instance or type, %s ' 'dimension widget declared as %s.' % (dim, widget)) widget_kwargs.update(kwargs) if vals: if all(isnumeric(v) or isinstance(v, datetime_types) for v in vals) and len(vals) > 1: vals = sorted(vals) labels = [str(dim.pprint_value(v)) for v in vals] options = OrderedDict(zip(labels, vals)) widget_type = widget_type or DiscreteSlider else: options = list(vals) widget_type = widget_type or Select default = vals[0] if dim.default is None else dim.default widget_kwargs = dict(dict(name=dim.label, options=options, value=default), **widget_kwargs) widget = widget_type(**widget_kwargs) elif dim.range != (None, None): start, end = dim.range if start == end: continue default = start if dim.default is None else dim.default if widget_type is not None: pass elif all(isinstance(v, int) for v in (start, end, default)): widget_type = IntSlider step = 1 if dim.step is None else dim.step elif isinstance(default, datetime_types): widget_type = DatetimeInput else: widget_type = FloatSlider step = 0.1 if dim.step is None else dim.step widget_kwargs = dict(dict(step=step, name=dim.label, start=dim.range[0], end=dim.range[1], value=default), **widget_kwargs) widget = widget_type(**widget_kwargs) if widget is not None: widgets.append(widget) if widgets_type == 'scrubber': widgets = [Player(length=nframes, width=550)] return widgets, dim_values
[docs]class Interactive(PaneBase): priority: ClassVar[float | bool | None] = None def __init__(self, object=None, **params): super().__init__(object, **params) self._update_layout(), list(Layoutable.param))
[docs] @classmethod def applies(cls, object: Any) -> float | bool | None: if 'hvplot.interactive' not in sys.modules: return False from hvplot.interactive import Interactive return 0.8 if isinstance(object, Interactive) else False
@param.depends('object') def _update_layout(self): if self.object is None: self._layout_panel = None else: self._layout_panel = self.object.layout() self._layout_panel.param.update(**{ p: getattr(self, p) for p in Layoutable.param if p != 'name' }) def _update_layout_properties(self, *events): if self._layout_panel is None: return self._layout_panel.param.update(**{ for e in events}) def _get_model( self, doc: Document, root: Optional[Model] = None, parent: Optional[Model] = None, comm: Optional[Comm] = None ) -> Model: if root is None: return self.get_root(doc, comm) if self._layout_panel is None: model = _BkSpacer(**{ p: getattr(self, p) for p in Layoutable.param if p != 'name' }) else: model = self._layout_panel._get_model(doc, root, parent, comm) self._models[root.ref['id']] = (model, parent) return model def _cleanup(self, root: Model | None = None) -> None: if self._layout_panel is not None: self._layout_panel._cleanup(root) super()._cleanup(root)
[docs]def is_bokeh_element_plot(plot): """ Checks whether plotting instance is a HoloViews ElementPlot rendered with the bokeh backend. """ from holoviews.plotting.plot import ( GenericElementPlot, GenericOverlayPlot, Plot, ) if not isinstance(plot, Plot): return False return (plot.renderer.backend == 'bokeh' and isinstance(plot, GenericElementPlot) and not isinstance(plot, GenericOverlayPlot))
[docs]def generate_panel_bokeh_map(root_model, panel_views): """ mapping panel elements to its bokeh models """ map_hve_bk = defaultdict(list) ref = root_model.ref['id'] for pane in panel_views: if root_model.ref['id'] in pane._models: plot, subpane = pane._plots.get(ref, (None, None)) if plot is None: continue bk_plots = plot.traverse(lambda x: x, [is_bokeh_element_plot]) for plot in bk_plots: for hv_elem in plot.link_sources: map_hve_bk[hv_elem].append(plot) return map_hve_bk
Viewable._preprocessing_hooks.append(link_axes) Viewable._preprocessing_hooks.append(find_links)