Source code for fastplotlib.layouts._subplot

from __future__ import annotations

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 ._frame import Frame
from ..axes import Axes
from ..utils import global_config


[docs] @global_config.register class Subplot(PlotArea): @global_config.declare("toolbar", "background_color", "frame_kwargs") def __init__( self, parent, camera: Literal["2d", "3d"] | pygfx.PerspectiveCamera, controller: pygfx.Controller | str, canvas: BaseRenderCanvas | pygfx.Texture, rect: np.ndarray = None, extent: np.ndarray = None, resizeable: bool = True, toolbar: bool = True, renderer: pygfx.WgpuRenderer = None, name: str = None, background_color: str | tuple[float, ...] | pygfx.Color = ["black"], frame_kwargs: dict | None = None, ): """ Subplot class. .. important:: ``Subplot`` is not meant to be constructed directly, it only exists as part of a ``Figure`` Parameters ---------- parent: 'Figure' parent Figure instance 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 background_color: tuple[str | pygfx.Color, ...], default ["black"] background color, upto 4 colors, one for each corner frame_kwargs: dict | None, default None options for the Subplot Frame. May contain any of the keys ``"spacing"``, ``"title_kwargs"``, and ``"plane_color"``. Each value is itself a dict that is merged with the defaults, so only the entries you want to change need to be passed. **"spacing"**: dict, spacing of the frame elements in pixels - ``"x0"``: int, default 1, offset of the frame from the left edge - ``"sides"``: int, default 2, padding at the left and right sides - ``"title_flanks"``: int, default 8, space above and below the title text - ``"resize_handle_space"``: int, default 13, space reserved for the resize handle - ``"bottom"``: int, default 8, padding along the bottom edge **"title_kwargs"**: dict, options for the title ``TextGraphic`` - ``"font_size"``: float, default 16 - ``"face_color"``: str | tuple[float, ...] | pygfx.Color, default "w" **"plane_color"**: dict, colors of the frame plane for each interaction state, used to construct a ``SelectorColorStates``. Each value is a str | tuple[float, ...] | pygfx.Color. - ``"idle"``: color when the frame is not being interacted with - ``"highlight"``: color when the frame is hovered - ``"action"``: color while the frame is being moved or resized Example:: frame_kwargs = { "spacing": {"bottom": 12}, "title_kwargs": {"font_size": 20}, "plane_color": {"idle": "w", "highlight": "gray"}, } """ camera = create_camera(camera) controller = create_controller(controller_type=controller, camera=camera) self._docks = dict() toolbar_visible = "Imgui" in parent.__class__.__name__ and toolbar super().__init__( parent=parent, camera=camera, controller=controller, scene=pygfx.Scene(), canvas=canvas, renderer=renderer, name=name, background_color=background_color, ) for pos in ["left", "top", "right", "bottom"]: dv = Dock(self, size=0) dv.name = pos self.docks[pos] = dv self.children.append(dv) # imgui windows confined to this subplot, keyed by location self._imgui_windows = { loc: None for loc in ["left", "right", "top", "bottom", "toolbar"] } self._imgui_right_click = None self._axes = Axes(self) self.scene.add(self.axes.world_object) if frame_kwargs is None: frame_kwargs = {} self._frame = Frame( viewport=self.viewport, rect=rect, extent=extent, resizeable=resizeable, title=name, docks=self.docks, imgui_windows=self._imgui_windows, toolbar_visible=toolbar_visible, canvas_rect=parent.get_pygfx_render_area(), **frame_kwargs, ) @property def axes(self) -> Axes: """Axes object""" return self._axes @property def name(self) -> str: """Subplot name""" return self._name @name.setter def name(self, name: str): if name is None: self._name = None return for subplot in self.get_figure(self): if (subplot is self) or (subplot is None): continue if subplot.name == name: raise ValueError("subplot names must be unique") self._name = 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.frame.toolbar_visible @toolbar.setter def toolbar(self, visible: bool): self.frame.toolbar_visible = visible self.frame.reset_viewport() def _render(self): self.axes.update_using_camera() super()._render() @property def title(self) -> TextGraphic: """subplot title""" return self._frame.title_graphic @title.setter def title(self, text: str): text = str(text) self.title.text = text @property def frame(self) -> Frame: """Frame that the subplot lives in""" return self._frame @property def frame_spacing(self) -> dict: return self._frame.spacing @property def imgui_windows(self) -> dict: """ The imgui windows of this subplot, keyed by location. The locations are the four edges ["left", "right", "top", "bottom"] and "toolbar" Returns ------- dict[str, ImguiWindow] {location: ImguiWindow} """ return self._imgui_windows
[docs] def add_imgui_window( self, window=None, *, location: str = None, size: int = None, title: str = None, window_flags=None, ): """ Add an imgui window confined to this subplot. Can also be used as a decorator, see the ``Figure.add_imgui_window`` examples. Edge windows ("left", "right", "top", "bottom") reserve space outboard of the subplot dock on that edge. The "toolbar" location replaces the subplot toolbar. An existing window at a ``location`` is replaced. Parameters ---------- window: ImguiWindow | ImguiContainer, optional an ``ImguiWindow`` instance, such as a ``Legend``, or an ``ImguiContainer`` such as an ``ImguiColorbar``, which is given a window of its own. Omit when decorating. location: str, "left" | "right" | "top" | "bottom" | "toolbar" edge windows reserve canvas space, "toolbar" replaces the subplot toolbar size: int edge or toolbar thickness in pixels, required for edge windows title: str, optional window title, drawn as a title bar for edge windows. If ``None`` no title bar is drawn. window_flags: ``imgui.WindowFlags_``, optional imgui window flags, used when decorating, uses the ``ImguiWindow`` default flags if not provided """ figure = self.get_figure() if "Imgui" not in figure.__class__.__name__: raise TypeError( "imgui windows can only be added to a subplot of an ImguiFigure" ) from ..ui._base import ImguiContainer, ImguiWindow, EDGES, _wrap_update_call valid = EDGES + ["toolbar"] if location not in valid: raise ValueError( f"subplot imgui window location must be one of: {valid}, you have passed: {location}" ) if location in EDGES and size is None: raise ValueError( f"must provide `size` for an edge window, location: {location}" ) hook_kwargs = dict( figure=figure, subplot=self, location=location, size=size, title=title ) if window_flags is not None: hook_kwargs["window_flags"] = window_flags def decorator(_window): if isinstance(_window, ImguiWindow): win = _window elif isinstance(_window, ImguiContainer): # a container is drawn inline, give it a window of its own _window._fpl_add_hook(figure) win = ImguiWindow(update_call=_window.draw) elif callable(_window): win = ImguiWindow(update_call=_wrap_update_call(_window, self)) else: raise TypeError( "add_imgui_window() must be used as a decorator on a function, or given an `ImguiWindow` or " "`ImguiContainer` instance" ) win._fpl_add_hook(**hook_kwargs) self._imgui_windows[location] = win # edge windows reserve space, reset the layout if location in EDGES: figure._fpl_reset_layout() return _window if window is None: return decorator decorator(window) return window
[docs] def append_imgui_window(self, gui=None, *, location: str = None): """ Append imgui elements to an existing window of this subplot. Can also be used as a decorator. Useful for appending elements to the subplot toolbar with ``location="toolbar"``. Parameters ---------- gui: callable | ImguiContainer, optional function that draws imgui elements, or an ``ImguiContainer`` such as an ``ImguiColorbar``. Omit when decorating. location: str, "left" | "right" | "top" | "bottom" | "toolbar" location of the existing window to append to """ from ..ui._base import ImguiContainer, _wrap_update_call window = self._imgui_windows.get(location) if window is None: raise ValueError(f"no imgui window at location to append to: {location}") def decorator(_gui): if isinstance(_gui, ImguiContainer): _gui._fpl_add_hook(self.get_figure()) window._update_calls.append(_gui.draw) else: window._update_calls.append(_wrap_update_call(_gui, self)) return _gui if gui is None: return decorator return decorator(gui)
[docs] def remove_imgui_window(self, location: str): """ Remove and return the imgui window at the given location Parameters ---------- location: str "left" | "right" | "top" | "bottom" | "toolbar" Returns ------- ImguiWindow the removed window, it can be added again later """ from ..ui._base import EDGES window = self._imgui_windows.get(location) self._imgui_windows[location] = None # edge windows reserve space, reset the layout if location in EDGES: self.get_figure()._fpl_reset_layout() return window
@property def imgui_right_click(self): """ The imgui popup that is opened by a right-click within this subplot. Returns ------- ImguiPopup | None """ return self._imgui_right_click
[docs] def set_imgui_right_click(self, popup=None, *, window_flags=None): """ Set the imgui popup that is opened by a right-click within this subplot, replaces the Figure's popup within this subplot. Can also be used as a decorator, see the ``ImguiFigure.set_imgui_right_click`` examples. Parameters ---------- popup: ImguiPopup | callable, optional an ``ImguiPopup`` instance, or a function that draws imgui elements. Omit when decorating. window_flags: ``imgui.WindowFlags_``, optional imgui window flags for the popup """ figure = self.get_figure() if "Imgui" not in figure.__class__.__name__: raise TypeError( "imgui right-click popups can only be set on a subplot of an ImguiFigure" ) from ..ui._base import ImguiPopup, _wrap_update_call def decorator(_popup): if isinstance(_popup, ImguiPopup): p = _popup elif callable(_popup): p = ImguiPopup(update_call=_wrap_update_call(_popup, self)) else: raise TypeError( "set_imgui_right_click() must be used as a decorator, or given an `ImguiPopup` instance or a " "function that draws imgui elements" ) p._fpl_add_hook(figure=figure, parent=self, window_flags=window_flags) self._imgui_right_click = p return _popup if popup is None: return decorator decorator(popup) return popup
[docs] def append_imgui_right_click(self, gui=None): """ Append imgui elements to the right-click popup of this subplot. Can also be used as a decorator. Parameters ---------- gui: callable, optional function that draws imgui elements, omit when decorating """ from ..ui._base import _wrap_update_call popup = self._imgui_right_click if popup is None: raise ValueError( "no imgui right-click popup set on this subplot to append to, set one using " "`subplot.set_imgui_right_click()`" ) def decorator(_gui): popup._update_calls.append(_wrap_update_call(_gui, self)) return _gui if gui is None: return decorator return decorator(gui)
[docs] def remove_imgui_right_click(self): """ Remove and return the right-click popup of this subplot Returns ------- ImguiPopup the removed popup, it can be set again later """ popup = self._imgui_right_click self._imgui_right_click = None return popup
@global_config.register class Dock(PlotArea): def __init__( self, parent: Subplot, size: int, ): self._size = size super().__init__( parent=parent, camera=pygfx.OrthographicCamera(), controller=pygfx.PanZoomController(), scene=pygfx.Scene(), canvas=parent.canvas, renderer=parent.renderer, background_color=parent.background_color, ) @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.get_figure()._fpl_reset_layout() def _render(self): if self.size == 0: return super()._render()