From 0e2f047dca9976258f9605fe701e1943135c9004 Mon Sep 17 00:00:00 2001 From: David Stansby Date: Fri, 19 May 2023 10:36:55 +0100 Subject: [PATCH] Factor out layer selecton and MPL figure creation --- docs/changelog.rst | 5 ++ src/napari_matplotlib/base.py | 144 ++++++++++++++++++---------------- 2 files changed, 82 insertions(+), 67 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index e0c8da34..be0ea082 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -4,6 +4,11 @@ Changelog 0.3.0 ----- +New features +~~~~~~~~~~~~ +- Added `MPLWidget` as a widget containing just a Matplotlib canvas + without any association with a napari viewer. + Visual improvements ~~~~~~~~~~~~~~~~~~~ - The background of ``napari-matplotlib`` figures and axes is now transparent. diff --git a/src/napari_matplotlib/base.py b/src/napari_matplotlib/base.py index 1480fdca..d6a39f4c 100644 --- a/src/napari_matplotlib/base.py +++ b/src/napari_matplotlib/base.py @@ -17,10 +17,10 @@ # Icons modified from # https://github.com/matplotlib/matplotlib/tree/main/lib/matplotlib/mpl-data/images ICON_ROOT = Path(__file__).parent / "icons" -__all__ = ["NapariMPLWidget"] +__all__ = ["MPLWidget", "NapariMPLWidget"] -class NapariMPLWidget(QWidget): +class MPLWidget(QWidget): """ Widget containing a Matplotlib canvas and toolbar. @@ -28,30 +28,14 @@ class NapariMPLWidget(QWidget): `~matplotlib.figure.Figure`, and an associated toolbar. It is not responsible for creating any Axes, because different widgets may want to implement different subplot layouts. - - This class also handles callbacks to automatically update figures when - the layer selection or z-step is changed in the napari viewer. To take - advantage of this sub-classes should implement the ``clear()`` and - ``draw()`` methods. - - Attributes - ---------- - viewer : `napari.Viewer` - Main napari viewer. - canvas : matplotlib.backends.backend_qt5agg.FigureCanvas - Matplotlib canvas. - layers : `list` - List of currently selected napari layers. """ def __init__( self, - napari_viewer: napari.viewer.Viewer, parent: Optional[QWidget] = None, ): super().__init__(parent=parent) - self.viewer = napari_viewer self.canvas = FigureCanvas() self.canvas.figure.patch.set_facecolor("none") @@ -65,6 +49,81 @@ def __init__( self.layout().addWidget(self.toolbar) self.layout().addWidget(self.canvas) + @property + def figure(self) -> Figure: + """Matplotlib figure.""" + return self.canvas.figure + + def add_single_axes(self) -> None: + """ + Add a single Axes to the figure. + + The Axes is saved on the ``.axes`` attribute for later access. + """ + self.axes = self.figure.subplots() + self.apply_napari_colorscheme(self.axes) + + @staticmethod + def apply_napari_colorscheme(ax: Axes) -> None: + """Apply napari-compatible colorscheme to an Axes.""" + # changing color of axes background to transparent + ax.set_facecolor("none") + + # changing colors of all axes + for spine in ax.spines: + ax.spines[spine].set_color("white") + + ax.xaxis.label.set_color("white") + ax.yaxis.label.set_color("white") + + # changing colors of axes labels + ax.tick_params(axis="x", colors="white") + ax.tick_params(axis="y", colors="white") + + def _replace_toolbar_icons(self) -> None: + # Modify toolbar icons and some tooltips + for action in self.toolbar.actions(): + text = action.text() + if text == "Pan": + action.setToolTip( + "Pan/Zoom: Left button pans; Right button zooms; " + "Click once to activate; Click again to deactivate" + ) + if text == "Zoom": + action.setToolTip( + "Zoom to rectangle; Click once to activate; " + "Click again to deactivate" + ) + if len(text) > 0: # i.e. not a separator item + icon_path = os.path.join(ICON_ROOT, text + ".png") + action.setIcon(QIcon(icon_path)) + + +class NapariMPLWidget(MPLWidget): + """ + Widget containing a Matplotlib canvas and toolbar. + + In addition to `BaseNapariMPLWidget`, this class handles callbacks + to automatically update figures when the layer selection or z-step + is changed in the napari viewer. To take advantage of this sub-classes + should implement the ``clear()`` and ``draw()`` methods. + + Attributes + ---------- + viewer : `napari.Viewer` + Main napari viewer. + layers : `list` + List of currently selected napari layers. + """ + + def __init__( + self, + napari_viewer: napari.viewer.Viewer, + parent: Optional[QWidget] = None, + ): + super().__init__(parent=parent) + + self.viewer = napari_viewer self._setup_callbacks() self.layers: List[napari.layers.Layer] = [] @@ -73,11 +132,6 @@ def __init__( #: Type of layer taken as input input_layer_types: Tuple[napari.layers.Layer, ...] = (napari.layers.Layer,) - @property - def figure(self) -> Figure: - """Matplotlib figure.""" - return self.canvas.figure - @property def n_selected_layers(self) -> int: """ @@ -139,32 +193,6 @@ def draw(self) -> None: This is a no-op, and is intended for derived classes to override. """ - def add_single_axes(self) -> None: - """ - Add a single Axes to the figure. - - The Axes is saved on the ``.axes`` attribute for later access. - """ - self.axes = self.figure.subplots() - self.apply_napari_colorscheme(self.axes) - - @staticmethod - def apply_napari_colorscheme(ax: Axes) -> None: - """Apply napari-compatible colorscheme to an Axes.""" - # changing color of axes background to transparent - ax.set_facecolor("none") - - # changing colors of all axes - for spine in ax.spines: - ax.spines[spine].set_color("white") - - ax.xaxis.label.set_color("white") - ax.yaxis.label.set_color("white") - - # changing colors of axes labels - ax.tick_params(axis="x", colors="white") - ax.tick_params(axis="y", colors="white") - def _on_update_layers(self) -> None: """ Function is called when self.layers is updated via @@ -173,24 +201,6 @@ def _on_update_layers(self) -> None: This is a no-op, and is intended for derived classes to override. """ - def _replace_toolbar_icons(self) -> None: - # Modify toolbar icons and some tooltips - for action in self.toolbar.actions(): - text = action.text() - if text == "Pan": - action.setToolTip( - "Pan/Zoom: Left button pans; Right button zooms; " - "Click once to activate; Click again to deactivate" - ) - if text == "Zoom": - action.setToolTip( - "Zoom to rectangle; Click once to activate; " - "Click again to deactivate" - ) - if len(text) > 0: # i.e. not a separator item - icon_path = os.path.join(ICON_ROOT, text + ".png") - action.setIcon(QIcon(icon_path)) - class NapariNavigationToolbar(NavigationToolbar2QT): """Custom Toolbar style for Napari."""