Source code for plopp.backends.plotly.canvas

# SPDX-License-Identifier: BSD-3-Clause
# Copyright (c) 2023 Scipp contributors (https://github.com/scipp)

from typing import Literal

import scipp as sc

from ...core.utils import maybe_variable_to_number
from ...graphics.bbox import BoundingBox


[docs] class Canvas: """ Plotly-based canvas used to render 2D graphics. It provides a figure and some axes, as well as functions for controlling the zoom, panning, and the scale of the axes. Parameters ---------- figsize: The width and height of the figure, in inches. title: The title to be placed above the figure. user_vmin: The minimum value for the vertical axis. If a number (without a unit) is supplied, it is assumed that the unit is the same as the current vertical axis unit. user_vmax: The maximum value for the vertical axis. If a number (without a unit) is supplied, it is assumed that the unit is the same as the current vertical axis unit. """
[docs] def __init__( self, figsize: tuple[float, float] | None = None, title: str | None = None, user_vmin: sc.Variable | float = None, user_vmax: sc.Variable | float = None, **ignored, ): # Note on the `**ignored`` keyword arguments: the figure which owns the canvas # creates both the canvas and an artist object (Line or Image). The figure # accepts keyword arguments, and has to somehow forward them to the canvas and # the artist. Since the figure has no detailed knowledge of the underlying # backend that implements the canvas, it cannot have specific arguments (such # as `layout` for specifying a Plotly layout). # Instead, we forward all the kwargs from the figure to both the canvas and the # artist, and filter out the artist kwargs with `**ignored`. import plotly.graph_objects as go self.fig = go.FigureWidget( layout={ 'modebar_remove': [ 'zoom', 'pan', 'select', 'toImage', 'zoomIn', 'zoomOut', 'autoScale', 'resetScale', 'lasso2d', ], 'margin': {'l': 0, 'r': 0, 't': 0 if title is None else 40, 'b': 0}, 'dragmode': False, 'width': 600 if figsize is None else figsize[0], 'height': 400 if figsize is None else figsize[1], } ) self.figsize = figsize self._user_vmin = user_vmin self._user_vmax = user_vmax self.units = {} self.dims = {} self._own_axes = False if title: self.title = title self.bbox = BoundingBox()
def to_widget(self): return self.fig def save(self, filename: str): """ Save the figure to file. The default directory for writing the file is the same as the directory where the script or notebook is running. Parameters ---------- filename: Name of the output file. Possible file extensions are ``.jpg``, ``.png``, ``.svg``, ``.pdf``, and ``.html`. """ ext = filename.split('.')[-1] if ext == 'html': self.fig.write_html(filename) else: self.fig.write_image(filename) def set_axes(self, dims, units, dtypes): """ Set the axes dimensions and units. Parameters ---------- dims: The dimensions of the data. units: The units of the data. dtypes: The data types of the data. """ self.dims = dims self.units = units self.dtypes = dtypes key = 'y' if 'y' in self.units else 'data' self.bbox = BoundingBox( ymin=maybe_variable_to_number(self._user_vmin, unit=self.units[key]), ymax=maybe_variable_to_number(self._user_vmax, unit=self.units[key]), ) @property def empty(self) -> bool: """ Check if the canvas is empty. """ return not self.dims @property def title(self) -> str: """ Get or set the title of the plot. """ return self.fig.layout.title.text @title.setter def title(self, text: str): layout = self.fig.layout if not text: layout.margin.t = 0 elif layout.margin.t == 0: layout.margin.t = 40 layout.title = text @property def xlabel(self) -> str: """ Get or set the label of the x-axis. """ return self.fig.layout.xaxis.title.text @xlabel.setter def xlabel(self, lab: str): self.fig.layout.xaxis.title = lab @property def ylabel(self) -> str: """ Get or set the label of the y-axis. """ return self.fig.layout.yaxis.title.text @ylabel.setter def ylabel(self, lab: str): self.fig.layout.yaxis.title = lab @property def xscale(self) -> Literal['linear', 'log']: """ Get or set the scale of the x-axis ('linear' or 'log'). """ return self.fig.layout.xaxis.type or 'linear' @xscale.setter def xscale(self, scale: Literal['linear', 'log']): self.fig.update_xaxes(type=scale) @property def yscale(self) -> Literal['linear', 'log']: """ Get or set the scale of the y-axis ('linear' or 'log'). """ return self.fig.layout.yaxis.type or 'linear' @yscale.setter def yscale(self, scale: Literal['linear', 'log']): self.fig.update_yaxes(type=scale) @property def xmin(self) -> float: """ Get or set the lower (left) bound of the x-axis. """ return self.fig.layout.xaxis.range[0] @xmin.setter def xmin(self, value: float): self.fig.layout.xaxis.range = [value, self.xmax] @property def xmax(self) -> float: """ Get or set the upper (right) bound of the x-axis. """ return self.fig.layout.xaxis.range[1] @xmax.setter def xmax(self, value: float): self.fig.layout.xaxis.range = [self.xmin, value] @property def xrange(self) -> tuple[float, float]: """ Get or set the range/limits of the x-axis. """ return self.fig.layout.xaxis.range @xrange.setter def xrange(self, value: tuple[float, float]): self.fig.layout.xaxis.range = value @property def ymin(self) -> float: """ Get or set the lower (bottom) bound of the y-axis. """ return self.fig.layout.yaxis.range[0] @ymin.setter def ymin(self, value: float): self.fig.layout.yaxis.range = [value, self.ymax] @property def ymax(self) -> float: """ Get or set the upper (top) bound of the y-axis. """ return self.fig.layout.yaxis.range[1] @ymax.setter def ymax(self, value: float): self.fig.layout.yaxis.range = [self.ymin, value] @property def yrange(self) -> tuple[float, float]: """ Get or set the range/limits of the y-axis. """ return self.fig.layout.yaxis.range @yrange.setter def yrange(self, value: tuple[float, float]): self.fig.layout.yaxis.range = value def reset_mode(self): """ Reset the modebar mode to nothing, to disable all zoom/pan tools. """ self.fig.update_layout(dragmode=False) def zoom(self): """ Activate the underlying Plotly zoom tool. """ self.fig.update_layout(dragmode='zoom') def pan(self): """ Activate the underlying Plotly pan tool. """ self.fig.update_layout(dragmode='pan') def panzoom(self, value: Literal['pan', 'zoom', None]): """ Activate or deactivate the pan or zoom tool, depending on the input value. """ if value == 'zoom': self.zoom() elif value == 'pan': self.pan() elif value is None: self.reset_mode() def download_figure(self): """ Save the figure to a PNG file via a pop-up dialog. """ self.fig.write_image('figure.png') def logx(self): """ Toggle the scale between ``linear`` and ``log`` along the horizontal axis. """ self.xscale = 'log' if self.xscale in ('linear', None) else 'linear' def logy(self): """ Toggle the scale between ``linear`` and ``log`` along the vertical axis. """ self.yscale = 'log' if self.yscale in ('linear', None) else 'linear' def draw(self): pass def update_legend(self): pass