Source code for fastplotlib.layouts._subplot

from typing import Literal, Union

import numpy as np

import pygfx

from rendercanvas import BaseRenderCanvas

from ..graphics import TextGraphic
from ._utils import create_camera, create_controller
from ._plot_area import PlotArea
from ._graphic_methods_mixin import GraphicMethodsMixin
from ..graphics._axes import Axes


# number of pixels taken by the imgui toolbar when present
IMGUI_TOOLBAR_HEIGHT = 39


[docs] class Subplot(PlotArea, GraphicMethodsMixin): def __init__( self, parent: Union["Figure"], position: tuple[int, int], parent_dims: tuple[int, int], camera: Literal["2d", "3d"] | pygfx.PerspectiveCamera, controller: pygfx.Controller, canvas: BaseRenderCanvas | pygfx.Texture, renderer: pygfx.WgpuRenderer = None, name: str = None, ): """ General plot object is found within a ``Figure``. Each ``Figure`` instance will have [n rows, n columns] of subplots. .. important:: ``Subplot`` is not meant to be constructed directly, it only exists as part of a ``Figure`` Parameters ---------- parent: 'Figure' | None parent Figure instance position: (int, int), optional corresponds to the [row, column] position of the subplot within a ``Figure`` parent_dims: (int, int), optional dimensions of the parent ``Figure`` camera: str or pygfx.PerspectiveCamera, default '2d' indicates the FOV for the camera, '2d' sets ``fov = 0``, '3d' sets ``fov = 50``. ``fov`` can be changed at any time. controller: str or pygfx.Controller, optional | if ``None``, uses a PanZoomController for "2d" camera or FlyController for "3d" camera. | if ``str``, must be one of: `"panzoom", "fly", "trackball", or "orbit"`. | also accepts a pygfx.Controller instance canvas: BaseRenderCanvas, or a pygfx.Texture Provides surface on which a scene will be rendered. renderer: WgpuRenderer object used to render scenes using wgpu name: str, optional name of the subplot, will appear as ``TextGraphic`` above the subplot """ super(GraphicMethodsMixin, self).__init__() if position is None: position = (0, 0) if parent_dims is None: parent_dims = (1, 1) self.nrows, self.ncols = parent_dims camera = create_camera(camera) controller = create_controller(controller_type=controller, camera=camera) self._docks = dict() self.spacing = 2 self._title_graphic: TextGraphic = None self._toolbar = True super(Subplot, self).__init__( parent=parent, position=position, camera=camera, controller=controller, scene=pygfx.Scene(), canvas=canvas, renderer=renderer, name=name, ) for pos in ["left", "top", "right", "bottom"]: dv = Dock(self, pos, size=0) dv.name = pos self.docks[pos] = dv self.children.append(dv) if self.name is not None: self.set_title(self.name) self._axes = Axes(self) self.scene.add(self.axes.world_object) @property def axes(self) -> Axes: return self._axes @property def name(self) -> str: return self._name @name.setter def name(self, name: str): self._name = name self.set_title(name) @property def docks(self) -> dict: """ The docks of this plot area. Each ``dock`` is basically just a PlotArea too. The docks are: ["left", "top", "right", "bottom"] Returns ------- Dict[str, Dock] {dock_name: Dock} """ return self._docks @property def toolbar(self) -> bool: """show/hide toolbar""" return self._toolbar @toolbar.setter def toolbar(self, visible: bool): self._toolbar = bool(visible) self.set_viewport_rect()
[docs] def render(self): self.axes.update_using_camera() super().render()
[docs] def set_title(self, text: str): """Sets the plot title, stored as a ``TextGraphic`` in the "top" dock area""" if text is None: return text = str(text) if self._title_graphic is not None: self._title_graphic.text = text else: tg = TextGraphic(text=text, font_size=18) self._title_graphic = tg self.docks["top"].size = 35 self.docks["top"].add_graphic(tg) self.center_title()
[docs] def center_title(self): """Centers name of subplot.""" if self._title_graphic is None: raise AttributeError("No title graphic is set") self._title_graphic.world_object.position = (0, 0, 0) self.docks["top"].center_graphic(self._title_graphic, zoom=1.5) self._title_graphic.world_object.position_y = -3.5
[docs] def get_rect(self) -> np.ndarray: """ Returns the bounding box that defines the Subplot within the canvas. Returns ------- np.ndarray x_position, y_position, width, height """ row_ix, col_ix = self.position x_start_render, y_start_render, width_canvas_render, height_canvas_render = ( self.parent.get_pygfx_render_area() ) x_pos = ( ( (width_canvas_render / self.ncols) + ((col_ix - 1) * (width_canvas_render / self.ncols)) ) + self.spacing + x_start_render ) y_pos = ( ( (height_canvas_render / self.nrows) + ((row_ix - 1) * (height_canvas_render / self.nrows)) ) + self.spacing + y_start_render ) width_subplot = (width_canvas_render / self.ncols) - self.spacing height_subplot = (height_canvas_render / self.nrows) - self.spacing if self.parent.__class__.__name__ == "ImguiFigure" and self.toolbar: # leave space for imgui toolbar height_subplot -= IMGUI_TOOLBAR_HEIGHT # clip so that min values are always 1, otherwise JupyterRenderCanvas causes issues because it # initializes with a width of (0, 0) rect = np.array([x_pos, y_pos, width_subplot, height_subplot]).clip(1) for dv in self.docks.values(): rect = rect + dv.get_parent_rect_adjust() return rect
class Dock(PlotArea): _valid_positions = ["right", "left", "top", "bottom"] def __init__( self, parent: Subplot, position: str, size: int, ): if position not in self._valid_positions: raise ValueError( f"the `position` of an AnchoredViewport must be one of: {self._valid_positions}" ) self._size = size super().__init__( parent=parent, position=position, camera=pygfx.OrthographicCamera(), controller=pygfx.PanZoomController(), scene=pygfx.Scene(), canvas=parent.canvas, renderer=parent.renderer, ) @property def size(self) -> int: """Get or set the size of this dock""" return self._size @size.setter def size(self, s: int): self._size = s self.parent.set_viewport_rect() self.set_viewport_rect() def get_rect(self, *args): """ Returns the bounding box that defines this dock area within the canvas. Returns ------- np.ndarray x_position, y_position, width, height """ if self.size == 0: self.viewport.rect = None return row_ix_parent, col_ix_parent = self.parent.position x_start_render, y_start_render, width_render_canvas, height_render_canvas = ( self.parent.parent.get_pygfx_render_area() ) spacing = 2 # spacing in pixels if self.position == "right": x_pos = ( (width_render_canvas / self.parent.ncols) + ((col_ix_parent - 1) * (width_render_canvas / self.parent.ncols)) + (width_render_canvas / self.parent.ncols) - self.size ) y_pos = ( (height_render_canvas / self.parent.nrows) + ((row_ix_parent - 1) * (height_render_canvas / self.parent.nrows)) ) + spacing width_viewport = self.size height_viewport = (height_render_canvas / self.parent.nrows) - spacing elif self.position == "left": x_pos = (width_render_canvas / self.parent.ncols) + ( (col_ix_parent - 1) * (width_render_canvas / self.parent.ncols) ) y_pos = ( (height_render_canvas / self.parent.nrows) + ((row_ix_parent - 1) * (height_render_canvas / self.parent.nrows)) ) + spacing width_viewport = self.size height_viewport = (height_render_canvas / self.parent.nrows) - spacing elif self.position == "top": x_pos = ( (width_render_canvas / self.parent.ncols) + ((col_ix_parent - 1) * (width_render_canvas / self.parent.ncols)) + spacing ) y_pos = ( (height_render_canvas / self.parent.nrows) + ((row_ix_parent - 1) * (height_render_canvas / self.parent.nrows)) ) + spacing width_viewport = (width_render_canvas / self.parent.ncols) - spacing height_viewport = self.size elif self.position == "bottom": x_pos = ( (width_render_canvas / self.parent.ncols) + ((col_ix_parent - 1) * (width_render_canvas / self.parent.ncols)) + spacing ) y_pos = ( ( (height_render_canvas / self.parent.nrows) + ((row_ix_parent - 1) * (height_render_canvas / self.parent.nrows)) ) + (height_render_canvas / self.parent.nrows) - self.size ) width_viewport = (width_render_canvas / self.parent.ncols) - spacing height_viewport = self.size else: raise ValueError("invalid position") if self.parent.__class__.__name__ == "ImguiFigure" and self.parent.toolbar: # leave space for imgui toolbar height_viewport -= IMGUI_TOOLBAR_HEIGHT return [ x_pos + x_start_render, y_pos + y_start_render, width_viewport, height_viewport, ] def get_parent_rect_adjust(self): if self.position == "right": return np.array( [ 0, # parent subplot x-position is same 0, -self.size, # width of parent subplot is `self.size` smaller 0, ] ) elif self.position == "left": return np.array( [ self.size, # `self.size` added to parent subplot x-position 0, -self.size, # width of parent subplot is `self.size` smaller 0, ] ) elif self.position == "top": return np.array( [ 0, self.size, # `self.size` added to parent subplot y-position 0, -self.size, # height of parent subplot is `self.size` smaller ] ) elif self.position == "bottom": return np.array( [ 0, 0, # parent subplot y-position is same, 0, -self.size, # height of parent subplot is `self.size` smaller ] ) def render(self): if self.size == 0: return super().render()