diff --git a/MANIFEST.in b/MANIFEST.in index d625d95e..7ce16f9b 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,6 +1,5 @@ include LICENSE include README.md -recursive-include * *.mplstyle recursive-exclude * __pycache__ recursive-exclude * *.py[co] diff --git a/docs/user_guide.rst b/docs/user_guide.rst index fbd48db1..253e3149 100644 --- a/docs/user_guide.rst +++ b/docs/user_guide.rst @@ -40,11 +40,6 @@ To use these: Customising plots ----------------- -`Matplotlib style sheets `__ can be used to customise -the plots generated by ``napari-matplotlib``. -To use a custom style sheet: - -1. Save it as ``napari-matplotlib.mplstyle`` -2. Put it in the Matplotlib configuration directory. - The location of this directory varies on different computers, - and can be found by calling :func:`matplotlib.get_configdir()`. +``napari-matplotlib`` uses colours from the current napari theme to customise the +Matplotlib plots. See `the example on creating a new napari theme +`_ for a helpful guide. diff --git a/pyproject.toml b/pyproject.toml index dea0fd6a..22ff307f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,7 +7,7 @@ write_to = "src/napari_matplotlib/_version.py" [tool.pytest.ini_options] qt_api = "pyqt6" -addopts = "--mpl" +addopts = "--mpl --mpl-baseline-relative" filterwarnings = [ "error", # Coming from vispy diff --git a/src/napari_matplotlib/base.py b/src/napari_matplotlib/base.py index 0ff5e389..fb9e485c 100644 --- a/src/napari_matplotlib/base.py +++ b/src/napari_matplotlib/base.py @@ -2,7 +2,6 @@ from pathlib import Path from typing import Optional -import matplotlib import matplotlib.style as mplstyle import napari from matplotlib.backends.backend_qtagg import ( # type: ignore[attr-defined] @@ -10,17 +9,15 @@ NavigationToolbar2QT, ) from matplotlib.figure import Figure +from napari.utils.events import Event +from napari.utils.theme import get_theme from qtpy.QtGui import QIcon from qtpy.QtWidgets import QLabel, QVBoxLayout, QWidget -from .util import Interval, from_napari_css_get_size_of +from .util import Interval, from_napari_css_get_size_of, style_sheet_from_theme __all__ = ["BaseNapariMPLWidget", "NapariMPLWidget", "SingleAxesWidget"] -_CUSTOM_STYLE_PATH = ( - Path(matplotlib.get_configdir()) / "napari-matplotlib.mplstyle" -) - class BaseNapariMPLWidget(QWidget): """ @@ -45,18 +42,17 @@ def __init__( ): super().__init__(parent=parent) self.viewer = napari_viewer - self._mpl_style_sheet_path: Optional[Path] = None + self.napari_theme_style_sheet = style_sheet_from_theme( + get_theme(napari_viewer.theme, as_dict=False) + ) # Sets figure.* style - with mplstyle.context(self.mpl_style_sheet_path): + with mplstyle.context(self.napari_theme_style_sheet): self.canvas = FigureCanvasQTAgg() # type: ignore[no-untyped-call] self.canvas.figure.set_layout_engine("constrained") self.toolbar = NapariNavigationToolbar(self.canvas, parent=self) self._replace_toolbar_icons() - # callback to update when napari theme changed - # TODO: this isn't working completely (see issue #140) - # most of our styling respects the theme change but not all self.viewer.events.theme.connect(self._on_napari_theme_changed) self.setLayout(QVBoxLayout()) @@ -68,24 +64,6 @@ def figure(self) -> Figure: """Matplotlib figure.""" return self.canvas.figure - @property - def mpl_style_sheet_path(self) -> Path: - """ - Path to the set Matplotlib style sheet. - """ - if self._mpl_style_sheet_path is not None: - return self._mpl_style_sheet_path - elif (_CUSTOM_STYLE_PATH).exists(): - return _CUSTOM_STYLE_PATH - elif self._napari_theme_has_light_bg(): - return Path(__file__).parent / "styles" / "light.mplstyle" - else: - return Path(__file__).parent / "styles" / "dark.mplstyle" - - @mpl_style_sheet_path.setter - def mpl_style_sheet_path(self, path: Path) -> None: - self._mpl_style_sheet_path = Path(path) - def add_single_axes(self) -> None: """ Add a single Axes to the figure. @@ -94,13 +72,21 @@ def add_single_axes(self) -> None: """ # Sets axes.* style. # Does not set any text styling set by axes.* keys - with mplstyle.context(self.mpl_style_sheet_path): + with mplstyle.context(self.napari_theme_style_sheet): self.axes = self.figure.add_subplot() - def _on_napari_theme_changed(self) -> None: + def _on_napari_theme_changed(self, event: Event) -> None: """ Called when the napari theme is changed. + + Parameters + ---------- + event : napari.utils.events.Event + Event that triggered the callback. """ + self.napari_theme_style_sheet = style_sheet_from_theme( + get_theme(event.value, as_dict=False) + ) self._replace_toolbar_icons() def _napari_theme_has_light_bg(self) -> bool: @@ -211,15 +197,18 @@ def current_z(self) -> int: """ return self.viewer.dims.current_step[0] - def _on_napari_theme_changed(self) -> None: + def _on_napari_theme_changed(self, event: Event) -> None: """Update MPL toolbar and axis styling when `napari.Viewer.theme` is changed. - Note: - At the moment we only handle the default 'light' and 'dark' napari themes. + Parameters + ---------- + event : napari.utils.events.Event + Event that triggered the callback. """ - super()._on_napari_theme_changed() - self.clear() - self.draw() + super()._on_napari_theme_changed(event) + # use self._draw instead of self.draw to cope with redraw while there are no + # layers, this makes the self.clear() obsolete + self._draw() def _setup_callbacks(self) -> None: """ @@ -252,13 +241,15 @@ def _draw(self) -> None: """ # Clearing axes sets new defaults, so need to make sure style is applied when # this happens - with mplstyle.context(self.mpl_style_sheet_path): + with mplstyle.context(self.napari_theme_style_sheet): + # everything should be done in the style context self.clear() - if self.n_selected_layers in self.n_layers_input and all( - isinstance(layer, self.input_layer_types) for layer in self.layers - ): - self.draw() - self.canvas.draw() # type: ignore[no-untyped-call] + if self.n_selected_layers in self.n_layers_input and all( + isinstance(layer, self.input_layer_types) + for layer in self.layers + ): + self.draw() + self.canvas.draw() # type: ignore[no-untyped-call] def clear(self) -> None: """ @@ -300,7 +291,7 @@ def clear(self) -> None: """ Clear the axes. """ - with mplstyle.context(self.mpl_style_sheet_path): + with mplstyle.context(self.napari_theme_style_sheet): self.axes.clear() diff --git a/src/napari_matplotlib/styles/README.md b/src/napari_matplotlib/styles/README.md deleted file mode 100644 index 79d3c417..00000000 --- a/src/napari_matplotlib/styles/README.md +++ /dev/null @@ -1,3 +0,0 @@ -This folder contains default built-in Matplotlib style sheets. -See https://matplotlib.org/stable/tutorials/introductory/customizing.html#defining-your-own-style -for more info on Matplotlib style sheets. diff --git a/src/napari_matplotlib/styles/dark.mplstyle b/src/napari_matplotlib/styles/dark.mplstyle deleted file mode 100644 index 1658f9b4..00000000 --- a/src/napari_matplotlib/styles/dark.mplstyle +++ /dev/null @@ -1,12 +0,0 @@ -# Dark-theme napari colour scheme for matplotlib plots - -# text (very light grey - almost white): #f0f1f2 -# foreground (mid grey): #414851 -# background (dark blue-gray): #262930 - -figure.facecolor : none -axes.labelcolor : f0f1f2 -axes.facecolor : none -axes.edgecolor : 414851 -xtick.color : f0f1f2 -ytick.color : f0f1f2 diff --git a/src/napari_matplotlib/styles/light.mplstyle b/src/napari_matplotlib/styles/light.mplstyle deleted file mode 100644 index 3b8d7d1d..00000000 --- a/src/napari_matplotlib/styles/light.mplstyle +++ /dev/null @@ -1,12 +0,0 @@ -# Light-theme napari colour scheme for matplotlib plots - -# text (very dark grey - almost black): #3b3a39 -# foreground (mid grey): #d6d0ce -# background (brownish beige): #efebe9 - -figure.facecolor : none -axes.labelcolor : 3b3a39 -axes.facecolor : none -axes.edgecolor : d6d0ce -xtick.color : 3b3a39 -ytick.color : 3b3a39 diff --git a/src/napari_matplotlib/tests/baseline/test_custom_theme.png b/src/napari_matplotlib/tests/baseline/test_custom_theme.png index a668c103..ffa4635b 100644 Binary files a/src/napari_matplotlib/tests/baseline/test_custom_theme.png and b/src/napari_matplotlib/tests/baseline/test_custom_theme.png differ diff --git a/src/napari_matplotlib/tests/baseline/test_feature_histogram_points.png b/src/napari_matplotlib/tests/baseline/test_feature_histogram_points.png index b98a0170..74b84b2d 100644 Binary files a/src/napari_matplotlib/tests/baseline/test_feature_histogram_points.png and b/src/napari_matplotlib/tests/baseline/test_feature_histogram_points.png differ diff --git a/src/napari_matplotlib/tests/baseline/test_feature_histogram_vectors.png b/src/napari_matplotlib/tests/baseline/test_feature_histogram_vectors.png index 3b90586e..4675192b 100644 Binary files a/src/napari_matplotlib/tests/baseline/test_feature_histogram_vectors.png and b/src/napari_matplotlib/tests/baseline/test_feature_histogram_vectors.png differ diff --git a/src/napari_matplotlib/tests/baseline/test_histogram_2D.png b/src/napari_matplotlib/tests/baseline/test_histogram_2D.png index b043bba8..856d22d3 100644 Binary files a/src/napari_matplotlib/tests/baseline/test_histogram_2D.png and b/src/napari_matplotlib/tests/baseline/test_histogram_2D.png differ diff --git a/src/napari_matplotlib/tests/baseline/test_histogram_3D.png b/src/napari_matplotlib/tests/baseline/test_histogram_3D.png index 724314e1..b5670bca 100644 Binary files a/src/napari_matplotlib/tests/baseline/test_histogram_3D.png and b/src/napari_matplotlib/tests/baseline/test_histogram_3D.png differ diff --git a/src/napari_matplotlib/tests/baseline/test_slice_2D.png b/src/napari_matplotlib/tests/baseline/test_slice_2D.png index d39920be..c1e67637 100644 Binary files a/src/napari_matplotlib/tests/baseline/test_slice_2D.png and b/src/napari_matplotlib/tests/baseline/test_slice_2D.png differ diff --git a/src/napari_matplotlib/tests/baseline/test_slice_3D.png b/src/napari_matplotlib/tests/baseline/test_slice_3D.png index cf563de5..046293f3 100644 Binary files a/src/napari_matplotlib/tests/baseline/test_slice_3D.png and b/src/napari_matplotlib/tests/baseline/test_slice_3D.png differ diff --git a/src/napari_matplotlib/tests/data/test_theme.mplstyle b/src/napari_matplotlib/tests/data/test_theme.mplstyle deleted file mode 100644 index 2f94b31f..00000000 --- a/src/napari_matplotlib/tests/data/test_theme.mplstyle +++ /dev/null @@ -1,15 +0,0 @@ -# Dark-theme napari colour scheme for matplotlib plots - -#f4b8b2 # light red -#b2e4f4 # light blue -#0aa3fc # dark blue -#008939 # dark green - -figure.facecolor : f4b8b2 # light red -axes.facecolor : b2e4f4 # light blue -axes.edgecolor : 0aa3fc # dark blue - -xtick.color : 008939 # dark green -xtick.labelcolor : 008939 # dark green -ytick.color : 008939 # dark green -ytick.labelcolor : 008939 # dark green diff --git a/src/napari_matplotlib/tests/scatter/baseline/test_features_scatter_widget_2D.png b/src/napari_matplotlib/tests/scatter/baseline/test_features_scatter_widget_2D.png index 75965607..9237dbdc 100644 Binary files a/src/napari_matplotlib/tests/scatter/baseline/test_features_scatter_widget_2D.png and b/src/napari_matplotlib/tests/scatter/baseline/test_features_scatter_widget_2D.png differ diff --git a/src/napari_matplotlib/tests/scatter/baseline/test_scatter_2D.png b/src/napari_matplotlib/tests/scatter/baseline/test_scatter_2D.png index 10219106..a11bda5f 100644 Binary files a/src/napari_matplotlib/tests/scatter/baseline/test_scatter_2D.png and b/src/napari_matplotlib/tests/scatter/baseline/test_scatter_2D.png differ diff --git a/src/napari_matplotlib/tests/scatter/baseline/test_scatter_3D.png b/src/napari_matplotlib/tests/scatter/baseline/test_scatter_3D.png index 3e648eec..cd42a8a2 100644 Binary files a/src/napari_matplotlib/tests/scatter/baseline/test_scatter_3D.png and b/src/napari_matplotlib/tests/scatter/baseline/test_scatter_3D.png differ diff --git a/src/napari_matplotlib/tests/test_theme.py b/src/napari_matplotlib/tests/test_theme.py index 1042d3f3..2310f32f 100644 --- a/src/napari_matplotlib/tests/test_theme.py +++ b/src/napari_matplotlib/tests/test_theme.py @@ -1,15 +1,8 @@ -import os -import shutil -from copy import deepcopy -from pathlib import Path - -import matplotlib import napari import numpy as np import pytest -from matplotlib.colors import to_rgba -from napari_matplotlib import HistogramWidget, ScatterWidget +from napari_matplotlib import ScatterWidget from napari_matplotlib.base import NapariMPLWidget @@ -127,66 +120,3 @@ def test_no_theme_side_effects(make_napari_viewer): unrelated_figure.tight_layout() return unrelated_figure - - -@pytest.mark.mpl_image_compare -def test_custom_theme(make_napari_viewer, theme_path, brain_data): - viewer = make_napari_viewer() - viewer.theme = "dark" - - widget = ScatterWidget(viewer) - widget.mpl_style_sheet_path = theme_path - - viewer.add_image(brain_data[0], **brain_data[1], name="brain") - viewer.add_image( - brain_data[0] * -1, **brain_data[1], name="brain_reversed" - ) - - viewer.layers.selection.clear() - viewer.layers.selection.add(viewer.layers[0]) - viewer.layers.selection.add(viewer.layers[1]) - - return deepcopy(widget.figure) - - -def find_mpl_stylesheet(name: str) -> Path: - """Find the built-in matplotlib stylesheet.""" - return Path(matplotlib.__path__[0]) / f"mpl-data/stylelib/{name}.mplstyle" - - -def test_custom_stylesheet(make_napari_viewer, image_data): - """ - Test that a stylesheet in the current directory is given precidence. - - Do this by copying over a stylesheet from matplotlib's built in styles, - naming it correctly, and checking the colours are as expected. - """ - # Copy Solarize_Light2 as if it was a user-overriden stylesheet. - style_sheet_path = ( - Path(matplotlib.get_configdir()) / "napari-matplotlib.mplstyle" - ) - if style_sheet_path.exists(): - pytest.skip("Won't ovewrite existing custom style sheet.") - shutil.copy( - find_mpl_stylesheet("Solarize_Light2"), - style_sheet_path, - ) - - try: - viewer = make_napari_viewer() - viewer.add_image(image_data[0], **image_data[1]) - widget = HistogramWidget(viewer) - assert widget.mpl_style_sheet_path == style_sheet_path - ax = widget.figure.gca() - - # The axes should have a light brownish grey background: - assert ax.get_facecolor() == to_rgba("#eee8d5") - assert ax.patch.get_facecolor() == to_rgba("#eee8d5") - - # The figure background and axis gridlines are light yellow: - assert widget.figure.patch.get_facecolor() == to_rgba("#fdf6e3") - for gridline in ax.get_xgridlines() + ax.get_ygridlines(): - assert gridline.get_visible() is True - assert gridline.get_color() == "#b0b0b0" - finally: - os.remove(style_sheet_path) diff --git a/src/napari_matplotlib/util.py b/src/napari_matplotlib/util.py index 7d72c9e2..ed994256 100644 --- a/src/napari_matplotlib/util.py +++ b/src/napari_matplotlib/util.py @@ -3,6 +3,7 @@ import napari.qt import tinycss2 +from napari.utils.theme import Theme from qtpy.QtCore import QSize @@ -138,3 +139,49 @@ def from_napari_css_get_size_of( RuntimeWarning, ) return QSize(*fallback) + + +def style_sheet_from_theme(theme: Theme) -> dict[str, str]: + """Translate napari theme to a matplotlib style dictionary. + + Parameters + ---------- + theme : napari.utils.theme.Theme + Napari theme object representing the theme of the current viewer. + + Returns + ------- + Dict[str, str] + Matplotlib compatible style dictionary. + """ + return { + "axes.edgecolor": theme.secondary.as_hex(), + # BUG: could be the same as napari canvas, but facecolors do not get + # updated upon redraw for what ever reason + #'axes.facecolor':theme.canvas.as_hex(), + "axes.facecolor": "none", + "axes.labelcolor": theme.text.as_hex(), + "boxplot.boxprops.color": theme.text.as_hex(), + "boxplot.capprops.color": theme.text.as_hex(), + "boxplot.flierprops.markeredgecolor": theme.text.as_hex(), + "boxplot.whiskerprops.color": theme.text.as_hex(), + "figure.edgecolor": theme.secondary.as_hex(), + # BUG: should be the same as napari background, but facecolors do not get + # updated upon redraw for what ever reason + #'figure.facecolor':theme.background.as_hex(), + "figure.facecolor": "none", + "grid.color": theme.foreground.as_hex(), + # COMMENT: the hard coded colors are to match the previous behaviour + # alternativly we could use the theme to style the legend as well + #'legend.edgecolor':theme.secondary.as_hex(), + "legend.edgecolor": "black", + #'legend.facecolor':theme.background.as_hex(), + "legend.facecolor": "white", + #'legend.labelcolor':theme.text.as_hex() + "legend.labelcolor": "black", + "text.color": theme.text.as_hex(), + "xtick.color": theme.secondary.as_hex(), + "xtick.labelcolor": theme.text.as_hex(), + "ytick.color": theme.secondary.as_hex(), + "ytick.labelcolor": theme.text.as_hex(), + } pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy