Skip to content

Commit 6166de7

Browse files
committed
Switch from interrogating the theme to stylesheets
Follows on from matplotlib#2 and matplotlib#86. We can remove the hard-coded theme setup and move to using matplotlib stylesheets (napari-dark.mplstyle and napari-light.mplstyle) instead. The caveat is that this means the plugin is _less_ flexible when presented with a weird and wonderful user theme.
1 parent be3ed98 commit 6166de7

File tree

6 files changed

+67
-34
lines changed

6 files changed

+67
-34
lines changed

src/napari_matplotlib/base.py

Lines changed: 27 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import os
2+
import warnings
23
from pathlib import Path
34
from typing import List, Optional, Tuple
45

56
import matplotlib.style
67
import napari
7-
from matplotlib.axes import Axes
88
from matplotlib.backends.backend_qtagg import (
99
FigureCanvas,
1010
NavigationToolbar2QT,
@@ -41,12 +41,8 @@ def __init__(
4141
):
4242
super().__init__(parent=parent)
4343
self.viewer = napari_viewer
44-
45-
has_mpl_stylesheet = self._apply_user_stylesheet_if_present()
44+
self.apply_style()
4645
self.canvas = FigureCanvas()
47-
48-
if not has_mpl_stylesheet:
49-
self.canvas.figure.patch.set_facecolor("none")
5046
self.canvas.figure.set_layout_engine("constrained")
5147
self.toolbar = NapariNavigationToolbar(
5248
self.canvas, parent=self
@@ -73,47 +69,42 @@ def add_single_axes(self) -> None:
7369
The Axes is saved on the ``.axes`` attribute for later access.
7470
"""
7571
self.axes = self.figure.subplots()
76-
self.apply_style(self.axes)
7772

78-
def apply_style(self, ax: Axes) -> None:
73+
def apply_style(self) -> None:
7974
"""
80-
Use the user-supplied stylesheet if present, otherwise apply the
81-
napari-compatible colorscheme (theme-dependent) to an Axes.
75+
Any user-supplied stylesheet takes highest precidence, otherwise apply
76+
the napari-compatible colorscheme (depends on the napari theme).
8277
"""
8378
if self._apply_user_stylesheet_if_present():
8479
return
8580

86-
# get the foreground colours from current theme
87-
theme = napari.utils.theme.get_theme(self.viewer.theme, as_dict=False)
88-
fg_colour = theme.foreground.as_hex() # fg is a muted contrast to bg
89-
text_colour = theme.text.as_hex() # text is high contrast to bg
90-
91-
# changing color of axes background to transparent
92-
ax.set_facecolor("none")
93-
94-
# changing colors of all axes
95-
for spine in ax.spines:
96-
ax.spines[spine].set_color(fg_colour)
97-
98-
ax.xaxis.label.set_color(text_colour)
99-
ax.yaxis.label.set_color(text_colour)
100-
101-
# changing colors of axes labels
102-
ax.tick_params(axis="x", colors=text_colour)
103-
ax.tick_params(axis="y", colors=text_colour)
81+
stylesheet_dir = self._get_path_to_mpl_stylesheets()
82+
if self.viewer.theme == "dark":
83+
matplotlib.style.use(stylesheet_dir / "napari-dark.mplstyle")
84+
elif self.viewer.theme == "light":
85+
matplotlib.style.use(stylesheet_dir / "napari-light.mplstyle")
86+
else:
87+
warnings.warn(
88+
f"Napari theme '{self.viewer.theme}' is not supported by"
89+
" napari-matplotlib. Will fall back to the matplotlib default."
90+
)
91+
matplotlib.style.use("default")
92+
return
10493

10594
def _apply_user_stylesheet_if_present(self) -> bool:
10695
"""
10796
Apply the user-supplied stylesheet if present.
10897
10998
Returns
11099
-------
111-
True if the stylesheet was present and applied.
112-
False otherwise.
100+
True if the stylesheet was present and applied.
101+
False otherwise.
113102
"""
114103
if (Path.cwd() / "user.mplstyle").exists():
115104
matplotlib.style.use("./user.mplstyle")
116105
return True
106+
# TODO: can put more complicated stuff in here. Like a config dir,
107+
# or take a given named file from the matplotlib user styles
117108
return False
118109

119110
def _on_theme_change(self) -> None:
@@ -123,8 +114,8 @@ def _on_theme_change(self) -> None:
123114
At the moment we only handle the default 'light' and 'dark' napari themes.
124115
"""
125116
self._replace_toolbar_icons()
126-
if self.figure.gca():
127-
self.apply_style(self.figure.gca())
117+
self.apply_style()
118+
# self.canvas.reload()
128119

129120
def _theme_has_light_bg(self) -> bool:
130121
"""
@@ -139,6 +130,9 @@ def _theme_has_light_bg(self) -> bool:
139130
_, _, bg_lightness = theme.background.as_hsl_tuple()
140131
return bg_lightness > 0.5
141132

133+
def _get_path_to_mpl_stylesheets(self) -> Path:
134+
return Path(__file__).parent / "stylesheets"
135+
142136
def _get_path_to_icon(self) -> Path:
143137
"""
144138
Get the icons directory (which is theme-dependent).
@@ -268,7 +262,7 @@ def _draw(self) -> None:
268262
isinstance(layer, self.input_layer_types) for layer in self.layers
269263
):
270264
self.draw()
271-
self.apply_style(self.figure.gca())
265+
self.apply_style()
272266
self.canvas.draw()
273267

274268
def clear(self) -> None:
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# Dark-theme napari colour scheme for matplotlib plots
2+
3+
# text (very light grey - almost white): #f0f1f2
4+
# foreground (mid grey): #414851
5+
# background (dark blue-gray): #262930
6+
7+
figure.facecolor : none
8+
axes.labelcolor : f0f1f2
9+
axes.facecolor : none
10+
axes.edgecolor : 414851
11+
xtick.color : f0f1f2
12+
ytick.color : f0f1f2
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# Light-theme napari colour scheme for matplotlib plots
2+
3+
# text (): #3b3a39
4+
# foreground (): #d6d0ce
5+
# background (): #efebe9
6+
7+
figure.facecolor : none
8+
axes.labelcolor : 3b3a39
9+
axes.facecolor : none
10+
axes.edgecolor : d6d0ce
11+
xtick.color : 3b3a39
12+
ytick.color : 3b3a39
Loading
Loading

src/napari_matplotlib/tests/test_theme.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,8 +54,23 @@ def test_theme_background_check(make_napari_viewer):
5454
assert widget._theme_has_light_bg() is True
5555

5656
_mock_up_theme()
57+
with pytest.warns(UserWarning, match="theme 'blue' is not supported"):
58+
viewer.theme = "blue"
59+
assert widget._theme_has_light_bg() is True
60+
61+
62+
def test_unknown_theme_raises_warning(make_napari_viewer):
63+
"""
64+
Check that widget construction warns if it doesn't recognise napari's theme.
65+
66+
Note that testing for the expected warning when theme is changed _after_ the
67+
widget is created is part of ``test_theme_background_check``.
68+
"""
69+
viewer = make_napari_viewer()
70+
_mock_up_theme() # creates the 'blue' theme which is not a standard napari theme
5771
viewer.theme = "blue"
58-
assert widget._theme_has_light_bg() is True
72+
with pytest.warns(UserWarning, match="theme 'blue' is not supported"):
73+
HistogramWidget(viewer)
5974

6075

6176
@pytest.mark.parametrize(

0 commit comments

Comments
 (0)