Skip to content

Commit 1f05e06

Browse files
authored
Merge pull request #175 from dstansby/stylesheetss
Allow style sheets to be used
2 parents 14b2b3e + 373f945 commit 1f05e06

File tree

11 files changed

+123
-42
lines changed

11 files changed

+123
-42
lines changed

MANIFEST.in

Lines changed: 1 addition & 0 deletions
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]

docs/guide/third_party.rst

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,6 @@ The following properties and methods are useful for working with the figure and
3232

3333
- `~.BaseNapariMPLWidget.figure` provides access to the figure
3434
- :meth:`~.BaseNapariMPLWidget.add_single_axes` adds a single axes to the figure, which can be accessed using the ``.axes`` attribute.
35-
- :meth:`~.BaseNapariMPLWidget.apply_napari_colorscheme` can be used to apply the napari colorscheme to any Axes you setup manually on the figure.
3635

3736
Working with napari layers
3837
--------------------------

src/napari_matplotlib/base.py

Lines changed: 47 additions & 38 deletions
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):

src/napari_matplotlib/scatter.py

Lines changed: 2 additions & 0 deletions
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:
Lines changed: 3 additions & 0 deletions
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.
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 (very dark grey - almost black): #3b3a39
4+
# foreground (mid grey): #d6d0ce
5+
# background (brownish beige): #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

src/napari_matplotlib/tests/conftest.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import os
2+
from pathlib import Path
23

34
import numpy as np
45
import pytest
@@ -52,3 +53,8 @@ def set_strict_qt():
5253
os.environ[env_var] = old_val
5354
else:
5455
del os.environ[env_var]
56+
57+
58+
@pytest.fixture
59+
def theme_path():
60+
return Path(__file__).parent / "data" / "test_theme.mplstyle"
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# Dark-theme napari colour scheme for matplotlib plots
2+
3+
#f4b8b2 # light red
4+
#b2e4f4 # light blue
5+
#0aa3fc # dark blue
6+
#008939 # dark green
7+
8+
figure.facecolor : f4b8b2 # light red
9+
axes.facecolor : b2e4f4 # light blue
10+
axes.edgecolor : 0aa3fc # dark blue
11+
12+
xtick.color : 008939 # dark green
13+
xtick.labelcolor : 008939 # dark green
14+
ytick.color : 008939 # dark green
15+
ytick.labelcolor : 008939 # dark green

0 commit comments

Comments
 (0)
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