Source code for panel.pane.plot

"""
Pane class which render plots from different libraries
"""
from __future__ import annotations

import sys

from contextlib import contextmanager
from io import BytesIO
from typing import (
    TYPE_CHECKING, Any, ClassVar, Dict, Mapping, Optional,
)

import param

from bokeh.models import (
    CustomJS, LayoutDOM, Model, Spacer as BkSpacer,
)
from bokeh.themes import Theme

from ..io import remove_root
from ..io.notebook import push
from ..util import escape
from ..viewable import Layoutable
from .base import PaneBase
from .image import (
    PDF, PNG, SVG, Image,
)
from .ipywidget import IPyWidget
from .markup import HTML

if TYPE_CHECKING:
    from bokeh.document import Document
    from pyviz_comms import Comm

FOLIUM_BEFORE = '<div style="width:100%;"><div style="position:relative;width:100%;height:0;padding-bottom:60%;">'
FOLIUM_AFTER = '<div style="width:100%;height:100%"><div style="position:relative;width:100%;height:100%;padding-bottom:0%;">'

@contextmanager
def _wrap_callback(cb, wrapped, doc, comm, callbacks):
    """
    Wraps a bokeh callback ensuring that any events triggered by it
    appropriately dispatch events in the notebook. Also temporarily
    replaces the wrapped callback with the real one while the callback
    is executed to ensure the callback can be removed as usual.
    """
    hold = doc.callbacks.hold_value
    doc.hold('combine')
    if wrapped in callbacks:
        index = callbacks.index(wrapped)
        callbacks[index] = cb
    yield
    if cb in callbacks:
        index = callbacks.index(cb)
        callbacks[index] = wrapped
    push(doc, comm)
    doc.hold(hold)


[docs]class Bokeh(PaneBase): """ The Bokeh pane allows displaying any displayable Bokeh model inside a Panel app. Reference: https://panel.holoviz.org/reference/panes/Bokeh.html :Example: >>> Bokeh(some_bokeh_figure) """ autodispatch = param.Boolean(default=True, doc=""" Whether to automatically dispatch events inside bokeh on_change and on_event callbacks in the notebook.""") theme = param.ClassSelector(default=None, class_=(Theme, str), doc=""" Bokeh theme to apply to the plot.""") priority: ClassVar[float | bool | None] = 0.8 _rename: ClassVar[Mapping[str, str | None]] = { 'autodispatch': None, 'theme': None } def __init__(self, object=None, **params): super().__init__(object, **params) self._syncing_props = False self._overrides = [ p for p, v in params.items() if p in Layoutable.param and v != self.param[p].default ] def _param_change(self, *events: param.parameterized.Event) -> None: self._track_overrides(*(e for e in events if e.name in Layoutable.param)) super()._param_change(*(e for e in events if e.name in self._overrides+['css_classes']))
[docs] @classmethod def applies(cls, obj: Any) -> float | bool | None: return isinstance(obj, LayoutDOM)
@classmethod def _property_callback_wrapper(cls, cb, doc, comm, callbacks): def wrapped_callback(attr, old, new): with _wrap_callback(cb, wrapped_callback, doc, comm, callbacks): cb(attr, old, new) return wrapped_callback @classmethod def _event_callback_wrapper(cls, cb, doc, comm, callbacks): def wrapped_callback(event): with _wrap_callback(cb, wrapped_callback, doc, comm, callbacks): cb(event) return wrapped_callback @classmethod def _wrap_bokeh_callbacks(cls, root, bokeh_model, doc, comm): for model in bokeh_model.select({'type': Model}): for key, cbs in model._callbacks.items(): callbacks = model._callbacks[key] callbacks[:] = [ cls._property_callback_wrapper(cb, doc, comm, callbacks) for cb in cbs ] for key, cbs in model._event_callbacks.items(): callbacks = model._event_callbacks[key] callbacks[:] = [ cls._event_callback_wrapper(cb, doc, comm, callbacks) for cb in cbs ] def _track_overrides(self, *events): if self._syncing_props: return overrides = list(self._overrides) for e in events: if e.name in overrides and self.param[e.name].default == e.new: overrides.remove(e.name) else: overrides.append(e.name) self._overrides = overrides self._sync_properties() @param.depends('object', watch=True) def _sync_properties(self): if self.object is None: return self._syncing_props = True try: self.param.update({ p: v for p, v in self.object.properties_with_values().items() if p not in self._overrides and p in Layoutable.param and p not in ('css_classes', 'name') }) props = { o: getattr(self, o) for o in self._overrides } if props: self.object.update(**props) finally: self._syncing_props = False 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.object is None: model = BkSpacer() else: model = self.object if comm and self.autodispatch: self._wrap_bokeh_callbacks(root, model, doc, comm) ref = root.ref['id'] for js in model.select({'type': CustomJS}): js.code = js.code.replace(model.ref['id'], ref) if model._document and doc is not model._document: remove_root(model, doc) self._models[ref] = (model, parent) # Apply theme self._design.apply_bokeh_theme_to_model(model, self.theme) return model
[docs]class Matplotlib(Image, IPyWidget): """ The `Matplotlib` pane allows displaying any displayable Matplotlib figure inside a Panel app. - It will render the plot to PNG at the declared DPI and then embed it. - If you find the figure to be clipped on the edges, you can set `tight=True` to automatically resize objects to fit within the pane. - If you have installed `ipympl` you will also be able to use the interactive backend. Reference: https://panel.holoviz.org/reference/panes/Matplotlib.html :Example: >>> Matplotlib(some_matplotlib_figure, dpi=144) """ dpi = param.Integer(default=144, bounds=(1, None), doc=""" Scales the dpi of the matplotlib figure.""") encode = param.Boolean(default=True, doc=""" Whether to encode SVG out as base64.""") format = param.Selector(default='png', objects=['png', 'svg'], doc=""" The format to render the plot as if the plot is not interactive.""") high_dpi = param.Boolean(default=True, doc=""" Whether to optimize output for high-dpi displays.""") interactive = param.Boolean(default=False, constant=True, doc=""" Whether to render interactive matplotlib plot with ipympl.""") tight = param.Boolean(default=False, doc=""" Automatically adjust the figure size to fit the subplots and other artist elements.""") _rename: ClassVar[Mapping[str, str | None]] = { 'object': 'text', 'interactive': None, 'dpi': None, 'tight': None, 'high_dpi': None, 'format': None, 'encode': None } _rerender_params = PNG._rerender_params + [ 'interactive', 'object', 'dpi', 'tight', 'high_dpi' ]
[docs] @classmethod def applies(cls, obj: Any) -> float | bool | None: if 'matplotlib' not in sys.modules: return False from matplotlib.figure import Figure is_fig = isinstance(obj, Figure) if is_fig and obj.canvas is None: raise ValueError('Matplotlib figure has no canvas and ' 'cannot be rendered.') return is_fig
def __init__(self, object=None, **params): super().__init__(object, **params) self._managers = {} def _get_widget(self, fig): import matplotlib.backends old_backend = getattr(matplotlib.backends, 'backend', 'agg') from ipympl.backend_nbagg import Canvas, FigureManager, is_interactive from matplotlib._pylab_helpers import Gcf matplotlib.use(old_backend) def closer(event): Gcf.destroy(0) canvas = Canvas(fig) fig.patch.set_alpha(0) manager = FigureManager(canvas, 0) if is_interactive(): fig.canvas.draw_idle() canvas.mpl_connect('close_event', closer) return manager @property def _img_type(self): if self.format == 'png': return PNG elif self.format == 'svg': return SVG else: return PDF @property def filetype(self): return self._img_type.filetype def _transform_object(self, obj: Any) -> Dict[str, Any]: return self._img_type._transform_object(self, obj) def _imgshape(self, data): try: return self._img_type._imgshape(data) except TypeError: return self._img_type._imgshape(self, data) def _format_html( self, src: str, width: str | None = None, height: str | None = None ): return self._img_type._format_html(self, src, width, height) def _get_model( self, doc: Document, root: Optional[Model] = None, parent: Optional[Model] = None, comm: Optional[Comm] = None ) -> Model: if not self.interactive: return self._img_type._get_model(self, doc, root, parent, comm) self.object.set_dpi(self.dpi) manager = self._get_widget(self.object) properties = self._get_properties(doc) del properties['text'] model = self._get_ipywidget( manager.canvas, doc, root, comm, **properties ) root = root or model self._models[root.ref['id']] = (model, parent) self._managers[root.ref['id']] = manager return model def _update(self, ref: str, model: Model) -> None: if not self.interactive: model.update(**self._get_properties(model.document)) return manager = self._managers[ref] if self.object is not manager.canvas.figure: self.object.set_dpi(self.dpi) self.object.patch.set_alpha(0) manager.canvas.figure = self.object self.object.set_canvas(manager.canvas) event = {'width': manager.canvas._width, 'height': manager.canvas._height} manager.canvas.handle_resize(event) manager.canvas.draw_idle() def _data(self, obj): try: obj.set_dpi(self.dpi) except Exception as ex: raise Exception("The Matplotlib backend is not configured. Try adding `matplotlib.use('agg')`") from ex b = BytesIO() if self.tight: bbox_inches = 'tight' else: bbox_inches = None obj.canvas.print_figure( b, format=self.format, facecolor=obj.get_facecolor(), edgecolor=obj.get_edgecolor(), dpi=self.dpi, bbox_inches=bbox_inches ) return b.getvalue()
[docs]class RGGPlot(PNG): """ An RGGPlot pane renders an r2py-based ggplot2 figure to png and wraps the base64-encoded data in a bokeh Div model. """ height = param.Integer(default=400) width = param.Integer(default=400) dpi = param.Integer(default=144, bounds=(1, None)) _rerender_params = PNG._rerender_params + ['object', 'dpi', 'width', 'height'] _rename: ClassVar[Mapping[str, str | None]] = {'dpi': None}
[docs] @classmethod def applies(cls, obj: Any) -> float | bool | None: return type(obj).__name__ == 'GGPlot' and hasattr(obj, 'r_repr')
def _data(self, obj): from rpy2 import robjects from rpy2.robjects.lib import grdevices with grdevices.render_to_bytesio(grdevices.png, type="cairo-png", width=self.width, height=self.height, res=self.dpi, antialias="subpixel") as b: robjects.r("print")(obj) return b.getvalue()
[docs]class YT(HTML): """ YT panes wrap plottable objects from the YT library. By default, the height and width are calculated by summing all contained plots, but can optionally be specified explicitly to provide additional space. """ priority: ClassVar[float | bool | None] = 0.5
[docs] @classmethod def applies(cls, obj: bool) -> float | bool | None: return (getattr(obj, '__module__', '').startswith('yt.') and hasattr(obj, "plots") and hasattr(obj, "_repr_html_"))
[docs]class Folium(HTML): """ The Folium pane wraps Folium map components. """ sizing_mode = param.ObjectSelector(default='stretch_width', objects=[ 'fixed', 'stretch_width', 'stretch_height', 'stretch_both', 'scale_width', 'scale_height', 'scale_both', None]) priority: ClassVar[float | bool | None] = 0.6
[docs] @classmethod def applies(cls, obj: Any) -> float | bool | None: return (getattr(obj, '__module__', '').startswith('folium.') and hasattr(obj, "_repr_html_"))
def _transform_object(self, obj: Any) -> Dict[str, Any]: text = '' if obj is None else obj if hasattr(text, '_repr_html_'): text = text._repr_html_().replace(FOLIUM_BEFORE, FOLIUM_AFTER) return dict(object=escape(text))