2
2
from pathlib import Path
3
3
from typing import List , Optional , Tuple
4
4
5
+ import matplotlib .style as mplstyle
5
6
import napari
6
- from matplotlib .axes import Axes
7
7
from matplotlib .backends .backend_qtagg import (
8
8
FigureCanvas ,
9
9
NavigationToolbar2QT ,
@@ -40,8 +40,11 @@ def __init__(
40
40
):
41
41
super ().__init__ (parent = parent )
42
42
self .viewer = napari_viewer
43
+ self ._mpl_style_sheet_path : Optional [Path ] = None
43
44
44
- self .canvas = FigureCanvas ()
45
+ # Sets figure.* style
46
+ with mplstyle .context (self .mpl_style_sheet_path ):
47
+ self .canvas = FigureCanvas ()
45
48
46
49
self .canvas .figure .patch .set_facecolor ("none" )
47
50
self .canvas .figure .set_layout_engine ("constrained" )
@@ -52,7 +55,7 @@ def __init__(
52
55
# callback to update when napari theme changed
53
56
# TODO: this isn't working completely (see issue #140)
54
57
# 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 )
56
59
57
60
self .setLayout (QVBoxLayout ())
58
61
self .layout ().addWidget (self .toolbar )
@@ -63,47 +66,40 @@ def figure(self) -> Figure:
63
66
"""Matplotlib figure."""
64
67
return self .canvas .figure
65
68
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
+
66
85
def add_single_axes (self ) -> None :
67
86
"""
68
87
Add a single Axes to the figure.
69
88
70
89
The Axes is saved on the ``.axes`` attribute for later access.
71
90
"""
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 ()
74
95
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.
101
99
"""
102
100
self ._replace_toolbar_icons ()
103
- if self .figure .gca ():
104
- self .apply_napari_colorscheme (self .figure .gca ())
105
101
106
- def _theme_has_light_bg (self ) -> bool :
102
+ def _napari_theme_has_light_bg (self ) -> bool :
107
103
"""
108
104
Does this theme have a light background?
109
105
@@ -124,7 +120,7 @@ def _get_path_to_icon(self) -> Path:
124
120
https://github.com/matplotlib/matplotlib/tree/main/lib/matplotlib/mpl-data/images
125
121
"""
126
122
icon_root = Path (__file__ ).parent / "icons"
127
- if self ._theme_has_light_bg ():
123
+ if self ._napari_theme_has_light_bg ():
128
124
return icon_root / "black"
129
125
else :
130
126
return icon_root / "white"
@@ -211,6 +207,16 @@ def current_z(self) -> int:
211
207
"""
212
208
return self .viewer .dims .current_step [0 ]
213
209
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
+
214
220
def _setup_callbacks (self ) -> None :
215
221
"""
216
222
Sets up callbacks.
@@ -240,12 +246,14 @@ def _draw(self) -> None:
240
246
Clear current figure, check selected layers are correct, and draw new
241
247
figure if so.
242
248
"""
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 ()
244
253
if self .n_selected_layers in self .n_layers_input and all (
245
254
isinstance (layer , self .input_layer_types ) for layer in self .layers
246
255
):
247
256
self .draw ()
248
- self .apply_napari_colorscheme (self .figure .gca ())
249
257
self .canvas .draw ()
250
258
251
259
def clear (self ) -> None :
@@ -288,7 +296,8 @@ def clear(self) -> None:
288
296
"""
289
297
Clear the axes.
290
298
"""
291
- self .axes .clear ()
299
+ with mplstyle .context (self .mpl_style_sheet_path ):
300
+ self .axes .clear ()
292
301
293
302
294
303
class NapariNavigationToolbar (NavigationToolbar2QT ):
0 commit comments