Skip to content

Navigation Menu

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit fc92806

Browse filesBrowse files
committed
adds path.effects rcparam support for list of (funcname, {**kwargs})
adds new path.effects validation created xkcd.mplstyle and shimmed it into plt.xkcd() Co-authored-by: Antony Lee <anntzer.lee@gmail.com> [ci doc] [skip actions] [skip appveyor] [skip azp]
1 parent 3ff8233 commit fc92806
Copy full SHA for fc92806

File tree

11 files changed

+194
-47
lines changed
Filter options

11 files changed

+194
-47
lines changed

‎.circleci/config.yml

Copy file name to clipboardExpand all lines: .circleci/config.yml
+1Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ commands:
7373
command: |
7474
mkdir -p ~/.local/share/fonts
7575
wget -nc https://github.com/google/fonts/blob/master/ofl/felipa/Felipa-Regular.ttf?raw=true -O ~/.local/share/fonts/Felipa-Regular.ttf || true
76+
wget -nc https://github.com/ipython/xkcd-font/blob/master/xkcd-script/font/xkcd-script.ttf?raw=true -O ~/.local/share/fonts/xkcd-script.ttf || true
7677
fc-cache -f -v
7778
- save_cache:
7879
key: fonts-2

‎galleries/examples/style_sheets/style_sheets_reference.py

Copy file name to clipboardExpand all lines: galleries/examples/style_sheets/style_sheets_reference.py
+22-22Lines changed: 22 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import matplotlib.pyplot as plt
2424
import numpy as np
2525

26+
import matplotlib as mpl
2627
import matplotlib.colors as mcolors
2728
from matplotlib.patches import Rectangle
2829

@@ -47,7 +48,7 @@ def plot_colored_lines(ax):
4748
def sigmoid(t, t0):
4849
return 1 / (1 + np.exp(-(t - t0)))
4950

50-
nb_colors = len(plt.rcParams['axes.prop_cycle'])
51+
nb_colors = len(mpl.rcParams['axes.prop_cycle'])
5152
shifts = np.linspace(-5, 5, nb_colors)
5253
amplitudes = np.linspace(1, 1.5, nb_colors)
5354
for t0, a in zip(shifts, amplitudes):
@@ -75,14 +76,15 @@ def plot_colored_circles(ax, prng, nb_samples=15):
7576
the color cycle, because different styles may have different numbers
7677
of colors.
7778
"""
78-
for sty_dict, j in zip(plt.rcParams['axes.prop_cycle'](),
79+
for sty_dict, j in zip(mpl.rcParams['axes.prop_cycle'](),
7980
range(nb_samples)):
8081
ax.add_patch(plt.Circle(prng.normal(scale=3, size=2),
8182
radius=1.0, color=sty_dict['color']))
8283
ax.grid(visible=True)
8384

8485
# Add title for enabling grid
85-
plt.title('ax.grid(True)', family='monospace', fontsize='small')
86+
font_family = mpl.rcParams.get('font.family', 'monospace')
87+
ax.set_title('ax.grid(True)', family=font_family, fontsize='medium')
8688

8789
ax.set_xlim([-4, 8])
8890
ax.set_ylim([-5, 6])
@@ -133,11 +135,12 @@ def plot_figure(style_label=""):
133135
# make a suptitle, in the same style for all subfigures,
134136
# except those with dark backgrounds, which get a lighter color:
135137
background_color = mcolors.rgb_to_hsv(
136-
mcolors.to_rgb(plt.rcParams['figure.facecolor']))[2]
138+
mcolors.to_rgb(mpl.rcParams['figure.facecolor']))[2]
137139
if background_color < 0.5:
138140
title_color = [0.8, 0.8, 1]
139141
else:
140142
title_color = np.array([19, 6, 84]) / 256
143+
141144
fig.suptitle(style_label, x=0.01, ha='left', color=title_color,
142145
fontsize=14, fontfamily='DejaVu Sans', fontweight='normal')
143146

@@ -147,28 +150,25 @@ def plot_figure(style_label=""):
147150
plot_colored_lines(axs[3])
148151
plot_histograms(axs[4], prng)
149152
plot_colored_circles(axs[5], prng)
150-
151153
# add divider
152154
rec = Rectangle((1 + 0.025, -2), 0.05, 16,
153155
clip_on=False, color='gray')
154156

155157
axs[4].add_artist(rec)
156158

157-
if __name__ == "__main__":
158-
159-
# Set up a list of all available styles, in alphabetical order but
160-
# the `default` and `classic` ones, which will be forced resp. in
161-
# first and second position.
162-
# styles with leading underscores are for internal use such as testing
163-
# and plot types gallery. These are excluded here.
164-
style_list = ['default', 'classic'] + sorted(
165-
style for style in plt.style.available
166-
if style != 'classic' and not style.startswith('_'))
167-
168-
# Plot a demonstration figure for every available style sheet.
169-
for style_label in style_list:
170-
with plt.rc_context({"figure.max_open_warning": len(style_list)}):
171-
with plt.style.context(style_label):
172-
plot_figure(style_label=style_label)
173159

174-
plt.show()
160+
# Set up a list of all available styles, in alphabetical order but
161+
# the `default` and `classic` ones, which will be forced resp. in
162+
# first and second position.
163+
# styles with leading underscores are for internal use such as testing
164+
# and plot types gallery. These are excluded here.
165+
style_list = ['default', 'classic'] + sorted(
166+
style for style in mpl.style.available
167+
if style != 'classic' and not style.startswith('_'))
168+
169+
# Plot a demonstration figure for every available style sheet:
170+
for style_label in style_list:
171+
with mpl.rc_context({"figure.max_open_warning": len(style_list)}):
172+
with mpl.style.context(style_label, after_reset=True):
173+
plot_figure(style_label=style_label)
174+
plt.show()

‎lib/matplotlib/__init__.py

Copy file name to clipboardExpand all lines: lib/matplotlib/__init__.py
+8-1Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -163,7 +163,6 @@
163163
from matplotlib._api import MatplotlibDeprecationWarning
164164
from matplotlib.rcsetup import validate_backend, cycler
165165

166-
167166
_log = logging.getLogger(__name__)
168167

169168
__bibtex__ = r"""@Article{Hunter:2007,
@@ -764,6 +763,14 @@ def __getitem__(self, key):
764763
from matplotlib import pyplot as plt
765764
plt.switch_backend(rcsetup._auto_backend_sentinel)
766765

766+
elif key == "path.effects" and self is globals().get("rcParams"):
767+
# defers loading of patheffects to avoid circular imports
768+
import matplotlib.patheffects as path_effects
769+
# use patheffects object or instantiate patheffects.object(**kwargs)
770+
return [pe if isinstance(pe, path_effects.AbstractPathEffect)
771+
else getattr(path_effects, pe[0])(**pe[1])
772+
for pe in self._get('path.effects')]
773+
767774
return self._get(key)
768775

769776
def _get_backend_or_none(self):

‎lib/matplotlib/mpl-data/matplotlibrc

Copy file name to clipboardExpand all lines: lib/matplotlib/mpl-data/matplotlibrc
+3-1Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -677,7 +677,9 @@
677677
# line (in pixels).
678678
# - *randomness* is the factor by which the length is
679679
# randomly scaled.
680-
#path.effects:
680+
#path.effects: # list of (patheffects function name, {**kwargs} tuples
681+
# ('withStroke', {'linewidth': 4}), ('SimpleLineShadow')
682+
681683

682684

683685
## ***************************************************************************
+30Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
## default xkcd style
2+
3+
# line
4+
lines.linewidth : 2.0
5+
6+
# font
7+
font.family : xkcd, xkcd Script, Comic Neue, Comic Sans MS
8+
font.size : 14.0
9+
10+
# axes
11+
axes.linewidth : 1.5
12+
axes.grid : False
13+
axes.unicode_minus: False
14+
axes.edgecolor: black
15+
16+
# ticks
17+
xtick.major.size : 8
18+
xtick.major.width: 3
19+
ytick.major.size : 8
20+
ytick.major.width: 3
21+
22+
# grids
23+
grid.linewidth: 0.0
24+
25+
# figure
26+
figure.facecolor: white
27+
28+
# path
29+
path.sketch : 1, 100, 2
30+
path.effects: ('withStroke', {'linewidth': 4, 'foreground': 'w' })

‎lib/matplotlib/patheffects.py

Copy file name to clipboardExpand all lines: lib/matplotlib/patheffects.py
+3Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,9 @@ class Normal(AbstractPathEffect):
161161
no special path effect.
162162
"""
163163

164+
def __init__(self, offset=(0., 0.)):
165+
super().__init__(offset)
166+
164167

165168
def _subclass_with_normal(effect_class):
166169
"""

‎lib/matplotlib/pyplot.py

Copy file name to clipboardExpand all lines: lib/matplotlib/pyplot.py
+2-21Lines changed: 2 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -747,27 +747,8 @@ def xkcd(
747747
stack = ExitStack()
748748
stack.callback(dict.update, rcParams, rcParams.copy()) # type: ignore
749749

750-
from matplotlib import patheffects
751-
rcParams.update({
752-
'font.family': ['xkcd', 'xkcd Script', 'Humor Sans', 'Comic Neue',
753-
'Comic Sans MS'],
754-
'font.size': 14.0,
755-
'path.sketch': (scale, length, randomness),
756-
'path.effects': [
757-
patheffects.withStroke(linewidth=4, foreground="w")],
758-
'axes.linewidth': 1.5,
759-
'lines.linewidth': 2.0,
760-
'figure.facecolor': 'white',
761-
'grid.linewidth': 0.0,
762-
'axes.grid': False,
763-
'axes.unicode_minus': False,
764-
'axes.edgecolor': 'black',
765-
'xtick.major.size': 8,
766-
'xtick.major.width': 3,
767-
'ytick.major.size': 8,
768-
'ytick.major.width': 3,
769-
})
770-
750+
rcParams.update({**style.library["xkcd"],
751+
'path.sketch': (scale, length, randomness)})
771752
return stack
772753

773754

‎lib/matplotlib/rcsetup.py

Copy file name to clipboardExpand all lines: lib/matplotlib/rcsetup.py
+36-1Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -568,6 +568,41 @@ def validate_sketch(s):
568568
raise ValueError("Expected a (scale, length, randomness) tuple") from exc
569569

570570

571+
def validate_path_effects(s):
572+
if not s:
573+
return []
574+
if isinstance(s, str) and s.strip().startswith("("):
575+
s = ast.literal_eval(s)
576+
577+
_validate_name = ValidateInStrings("path.effects.function",
578+
["Normal",
579+
"PathPatchEffect",
580+
"SimpleLineShadow",
581+
"SimplePatchShadow",
582+
"Stroke",
583+
"TickedStroke",
584+
"withSimplePatchShadow",
585+
"withStroke",
586+
"withTickedStroke"])
587+
588+
def _validate_dict(d):
589+
if not isinstance(d, dict):
590+
raise ValueError("Expected a dictionary of keyword arguments")
591+
return d
592+
593+
try:
594+
# cast to list for the 1 tuple case
595+
s = [s] if isinstance(s[0], str) else s
596+
# patheffects.{AbstractPathEffect} object or (_valid_name, {**kwargs})
597+
return [pe if getattr(pe, '__module__', "") == 'matplotlib.patheffects'
598+
else (_validate_name(pe[0].strip()),
599+
{} if len(pe) < 2 else _validate_dict(pe[1]))
600+
for pe in s]
601+
except TypeError:
602+
raise ValueError("Expected a list of patheffects functions"
603+
" or (funcname, {**kwargs}) tuples")
604+
605+
571606
def _validate_greaterthan_minushalf(s):
572607
s = validate_float(s)
573608
if s > -0.5:
@@ -1293,7 +1328,7 @@ def _convert_validator_spec(key, conv):
12931328
"path.simplify_threshold": _validate_greaterequal0_lessequal1,
12941329
"path.snap": validate_bool,
12951330
"path.sketch": validate_sketch,
1296-
"path.effects": validate_anylist,
1331+
"path.effects": validate_path_effects,
12971332
"agg.path.chunksize": validate_int, # 0 to disable chunking
12981333

12991334
# key-mappings (multi-character mappings should be a list/tuple)

‎lib/matplotlib/rcsetup.pyi

Copy file name to clipboardExpand all lines: lib/matplotlib/rcsetup.pyi
+3Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ from cycler import Cycler
22

33
from collections.abc import Callable, Iterable
44
from typing import Any, Literal, TypeVar
5+
from matplotlib.patheffects import AbstractPathEffect
56
from matplotlib.typing import ColorType, LineStyleType, MarkEveryType
67

78
interactive_bk: list[str]
@@ -140,6 +141,8 @@ def _validate_linestyle(s: Any) -> LineStyleType: ...
140141
def validate_markeverylist(s: Any) -> list[MarkEveryType]: ...
141142
def validate_bbox(s: Any) -> Literal["tight", "standard"] | None: ...
142143
def validate_sketch(s: Any) -> None | tuple[float, float, float]: ...
144+
def validate_path_effects(s: Any
145+
) -> list[None|AbstractPathEffect|tuple[str, dict[str, Any]]]: ...
143146
def validate_hatch(s: Any) -> str: ...
144147
def validate_hatchlist(s: Any) -> list[str]: ...
145148
def validate_dashlist(s: Any) -> list[list[float]]: ...

‎lib/matplotlib/tests/test_rcparams.py

Copy file name to clipboardExpand all lines: lib/matplotlib/tests/test_rcparams.py
+73-1Lines changed: 73 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@
1212
from matplotlib import _api, _c_internal_utils
1313
import matplotlib.pyplot as plt
1414
import matplotlib.colors as mcolors
15+
import matplotlib.patheffects as path_effects
16+
from matplotlib.testing.decorators import check_figures_equal
17+
1518
import numpy as np
1619
from matplotlib.rcsetup import (
1720
validate_bool,
@@ -28,8 +31,10 @@
2831
validate_markevery,
2932
validate_stringlist,
3033
validate_sketch,
34+
validate_path_effects,
3135
_validate_linestyle,
32-
_listify_validator)
36+
_listify_validator,
37+
)
3338

3439

3540
def test_rcparams(tmpdir):
@@ -630,6 +635,73 @@ def test_rcparams_legend_loc_from_file(tmpdir, value):
630635
with mpl.rc_context(fname=rc_path):
631636
assert mpl.rcParams["legend.loc"] == value
632637

638+
ped = [('Normal', {}),
639+
('Stroke', {'offset': (1, 2)}),
640+
('withStroke', {'linewidth': 4, 'foreground': 'w'})]
641+
642+
pel = [path_effects.Normal(),
643+
path_effects.Stroke((1, 2)),
644+
path_effects.withStroke(linewidth=4, foreground='w')]
645+
646+
647+
@pytest.mark.parametrize("value", [pel, ped], ids=["func", "dict"])
648+
def test_path_effects(value):
649+
assert validate_path_effects(value) == value
650+
for v in value:
651+
assert validate_path_effects(value) == value
652+
653+
654+
def test_path_effects_string():
655+
"""test list of dicts properly parsed"""
656+
pstr = "('Normal', ), "
657+
pstr += "('Stroke', {'offset': (1, 2)}),"
658+
pstr += "('withStroke', {'linewidth': 4, 'foreground': 'w'})"
659+
assert validate_path_effects(pstr) == ped
660+
661+
662+
@pytest.mark.parametrize("fdict, flist",
663+
[([ped[0]], [pel[0]]),
664+
([ped[1]], [pel[1]]),
665+
([ped[2]], [ped[2]]),
666+
(ped, pel)],
667+
ids=['function', 'args', 'kwargs', 'all'])
668+
@check_figures_equal()
669+
def test_path_effects_picture(fig_test, fig_ref, fdict, flist):
670+
with mpl.rc_context({'path.effects': fdict}):
671+
fig_test.subplots().plot([1, 2, 3])
672+
673+
with mpl.rc_context({'path.effects': flist}):
674+
fig_ref.subplots().plot([1, 2, 3])
675+
676+
677+
@pytest.mark.parametrize("s, msg", [
678+
([1, 2, 3], "Expected a list of patheffects .*"),
679+
(("Happy", ), r".* 'Happy' is not a valid value for path\.effects\.function.*"),
680+
(("Normal", [1, 2, 3]), "Expected a dictionary .*"),])
681+
def test_validate_path_effect_errors(s, msg):
682+
with pytest.raises(ValueError, match=msg):
683+
mpl.rcParams['path.effects'] = s
684+
685+
686+
def test_path_effects_wrong_kwargs():
687+
mpl.rcParams['path.effects'] = [('Normal', {'invalid_kwarg': 1})]
688+
689+
msg = ".* got an unexpected keyword argument 'invalid_kwarg'"
690+
with pytest.raises(TypeError, match=msg):
691+
mpl.rcParams.get('path.effects')
692+
693+
694+
def test_path_effects_from_file(tmpdir):
695+
# rcParams['legend.loc'] should be settable from matplotlibrc.
696+
# if any of these are not allowed, an exception will be raised.
697+
# test for gh issue #22338
698+
rc_path = tmpdir.join("matplotlibrc")
699+
rc_path.write("path.effects: ('Normal', {}), ('withStroke', {'linewidth': 2})")
700+
701+
with mpl.rc_context(fname=rc_path):
702+
assert isinstance(mpl.rcParams["path.effects"][0], path_effects.Normal)
703+
assert isinstance(mpl.rcParams["path.effects"][1], path_effects.withStroke)
704+
633705

634706
@pytest.mark.parametrize("value", [(1, 2, 3), '1, 2, 3', '(1, 2, 3)'])
635707
def test_validate_sketch(value):

‎lib/matplotlib/tests/test_style.py

Copy file name to clipboardExpand all lines: lib/matplotlib/tests/test_style.py
+13Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
import matplotlib as mpl
1010
from matplotlib import pyplot as plt, style
11+
from matplotlib.testing.decorators import check_figures_equal
1112
from matplotlib.style.core import USER_LIBRARY_PATHS, STYLE_EXTENSION
1213

1314

@@ -177,6 +178,18 @@ def test_xkcd_cm():
177178
assert mpl.rcParams["path.sketch"] is None
178179

179180

181+
@check_figures_equal()
182+
def test_xkcd_style(fig_test, fig_ref):
183+
184+
with style.context('xkcd'):
185+
fig_test.subplots().plot([1, 2, 3])
186+
fig_test.text(.5, .5, "Hello World!")
187+
188+
with plt.xkcd():
189+
fig_ref.subplots().plot([1, 2, 3])
190+
fig_ref.text(.5, .5, "Hello World!")
191+
192+
180193
def test_up_to_date_blacklist():
181194
assert mpl.style.core.STYLE_BLACKLIST <= {*mpl.rcsetup._validators}
182195

0 commit comments

Comments
0 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.