22from pathlib import Path
33from typing import List , Optional , Tuple
44
5+ import matplotlib .style as mplstyle
56import napari
6- from matplotlib .axes import Axes
77from matplotlib .backends .backend_qtagg import (
88 FigureCanvas ,
99 NavigationToolbar2QT ,
@@ -40,8 +40,11 @@ def __init__(
4040 ):
4141 super ().__init__ (parent = parent )
4242 self .viewer = napari_viewer
43+ self ._mpl_style_sheet_path : Optional [Path ] = None
4344
44- self .canvas = FigureCanvas ()
45+ # Sets figure.* style
46+ with mplstyle .context (self .mpl_style_sheet_path ):
47+ self .canvas = FigureCanvas ()
4548
4649 self .canvas .figure .patch .set_facecolor ("none" )
4750 self .canvas .figure .set_layout_engine ("constrained" )
@@ -52,7 +55,7 @@ def __init__(
5255 # callback to update when napari theme changed
5356 # TODO: this isn't working completely (see issue #140)
5457 # most of our styling respects the theme change but not all
55- self .viewer .events .theme .connect (self ._on_theme_change )
58+ self .viewer .events .theme .connect (self ._on_napari_theme_changed )
5659
5760 self .setLayout (QVBoxLayout ())
5861 self .layout ().addWidget (self .toolbar )
@@ -63,47 +66,40 @@ def figure(self) -> Figure:
6366 """Matplotlib figure."""
6467 return self .canvas .figure
6568
69+ @property
70+ def mpl_style_sheet_path (self ) -> Path :
71+ """
72+ Path to the set Matplotlib style sheet.
73+ """
74+ if self ._mpl_style_sheet_path is not None :
75+ return self ._mpl_style_sheet_path
76+ elif self ._napari_theme_has_light_bg ():
77+ return Path (__file__ ).parent / "styles" / "light.mplstyle"
78+ else :
79+ return Path (__file__ ).parent / "styles" / "dark.mplstyle"
80+
81+ @mpl_style_sheet_path .setter
82+ def mpl_style_sheet_path (self , path : Path ) -> None :
83+ self ._mpl_style_sheet_path = Path (path )
84+
6685 def add_single_axes (self ) -> None :
6786 """
6887 Add a single Axes to the figure.
6988
7089 The Axes is saved on the ``.axes`` attribute for later access.
7190 """
72- self .axes = self .figure .subplots ()
73- self .apply_napari_colorscheme (self .axes )
91+ # Sets axes.* style.
92+ # Does not set any text styling set by axes.* keys
93+ with mplstyle .context (self .mpl_style_sheet_path ):
94+ self .axes = self .figure .subplots ()
7495
75- def apply_napari_colorscheme (self , ax : Axes ) -> None :
76- """Apply napari-compatible colorscheme to an Axes."""
77- # get the foreground colours from current theme
78- theme = napari .utils .theme .get_theme (self .viewer .theme , as_dict = False )
79- fg_colour = theme .foreground .as_hex () # fg is a muted contrast to bg
80- text_colour = theme .text .as_hex () # text is high contrast to bg
81-
82- # changing color of axes background to transparent
83- ax .set_facecolor ("none" )
84-
85- # changing colors of all axes
86- for spine in ax .spines :
87- ax .spines [spine ].set_color (fg_colour )
88-
89- ax .xaxis .label .set_color (text_colour )
90- ax .yaxis .label .set_color (text_colour )
91-
92- # changing colors of axes labels
93- ax .tick_params (axis = "x" , colors = text_colour )
94- ax .tick_params (axis = "y" , colors = text_colour )
95-
96- def _on_theme_change (self ) -> None :
97- """Update MPL toolbar and axis styling when `napari.Viewer.theme` is changed.
98-
99- Note:
100- At the moment we only handle the default 'light' and 'dark' napari themes.
96+ def _on_napari_theme_changed (self ) -> None :
97+ """
98+ Called when the napari theme is changed.
10199 """
102100 self ._replace_toolbar_icons ()
103- if self .figure .gca ():
104- self .apply_napari_colorscheme (self .figure .gca ())
105101
106- def _theme_has_light_bg (self ) -> bool :
102+ def _napari_theme_has_light_bg (self ) -> bool :
107103 """
108104 Does this theme have a light background?
109105
@@ -124,7 +120,7 @@ def _get_path_to_icon(self) -> Path:
124120 https://github.com/matplotlib/matplotlib/tree/main/lib/matplotlib/mpl-data/images
125121 """
126122 icon_root = Path (__file__ ).parent / "icons"
127- if self ._theme_has_light_bg ():
123+ if self ._napari_theme_has_light_bg ():
128124 return icon_root / "black"
129125 else :
130126 return icon_root / "white"
@@ -211,6 +207,16 @@ def current_z(self) -> int:
211207 """
212208 return self .viewer .dims .current_step [0 ]
213209
210+ def _on_napari_theme_changed (self ) -> None :
211+ """Update MPL toolbar and axis styling when `napari.Viewer.theme` is changed.
212+
213+ Note:
214+ At the moment we only handle the default 'light' and 'dark' napari themes.
215+ """
216+ super ()._on_napari_theme_changed ()
217+ self .clear ()
218+ self .draw ()
219+
214220 def _setup_callbacks (self ) -> None :
215221 """
216222 Sets up callbacks.
@@ -240,12 +246,14 @@ def _draw(self) -> None:
240246 Clear current figure, check selected layers are correct, and draw new
241247 figure if so.
242248 """
243- self .clear ()
249+ # Clearing axes sets new defaults, so need to make sure style is applied when
250+ # this happens
251+ with mplstyle .context (self .mpl_style_sheet_path ):
252+ self .clear ()
244253 if self .n_selected_layers in self .n_layers_input and all (
245254 isinstance (layer , self .input_layer_types ) for layer in self .layers
246255 ):
247256 self .draw ()
248- self .apply_napari_colorscheme (self .figure .gca ())
249257 self .canvas .draw ()
250258
251259 def clear (self ) -> None :
@@ -288,7 +296,8 @@ def clear(self) -> None:
288296 """
289297 Clear the axes.
290298 """
291- self .axes .clear ()
299+ with mplstyle .context (self .mpl_style_sheet_path ):
300+ self .axes .clear ()
292301
293302
294303class NapariNavigationToolbar (NavigationToolbar2QT ):
0 commit comments