Skip to content

Commit abd00bd

Browse files
committed
MVP for using style sheets
1 parent 14b2b3e commit abd00bd

File tree

7 files changed

+80
-41
lines changed

7 files changed

+80
-41
lines changed

Diff for: MANIFEST.in

+1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
include LICENSE
22
include README.md
3+
recursive-include * *.mplstyle
34

45
recursive-exclude * __pycache__
56
recursive-exclude * *.py[co]

Diff for: src/napari_matplotlib/base.py

+47-38
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22
from pathlib import Path
33
from typing import List, Optional, Tuple
44

5+
import matplotlib.style as mplstyle
56
import napari
6-
from matplotlib.axes import Axes
77
from 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

294303
class NapariNavigationToolbar(NavigationToolbar2QT):

Diff for: src/napari_matplotlib/scatter.py

+2
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ def draw(self) -> None:
2323
"""
2424
Scatter the currently selected layers.
2525
"""
26+
if len(self.layers) == 0:
27+
return
2628
x, y, x_axis_name, y_axis_name = self._get_data()
2729

2830
if x.size > self._threshold_to_switch_to_histogram:

Diff for: src/napari_matplotlib/styles/README.md

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
This folder contains default built-in Matplotlib style sheets.
2+
See https://matplotlib.org/stable/tutorials/introductory/customizing.html#defining-your-own-style
3+
for more info on Matplotlib style sheets.

Diff for: src/napari_matplotlib/styles/dark.mplstyle

+12
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

Diff for: src/napari_matplotlib/styles/light.mplstyle

+12
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

Diff for: src/napari_matplotlib/tests/test_theme.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -43,14 +43,14 @@ def test_theme_background_check(make_napari_viewer):
4343
widget = NapariMPLWidget(viewer)
4444

4545
viewer.theme = "dark"
46-
assert widget._theme_has_light_bg() is False
46+
assert widget._napari_theme_has_light_bg() is False
4747

4848
viewer.theme = "light"
49-
assert widget._theme_has_light_bg() is True
49+
assert widget._napari_theme_has_light_bg() is True
5050

5151
_mock_up_theme()
5252
viewer.theme = "blue"
53-
assert widget._theme_has_light_bg() is True
53+
assert widget._napari_theme_has_light_bg() is True
5454

5555

5656
@pytest.mark.parametrize(

0 commit comments

Comments
 (0)