Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit fc92806

Browse 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 <[email protected]> [ci doc] [skip actions] [skip appveyor] [skip azp]
1 parent 3ff8233 commit fc92806

File tree

11 files changed

+194
-47
lines changed

11 files changed

+194
-47
lines changed

.circleci/config.yml

Lines 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

Lines 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

Lines 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

Lines 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
## ***************************************************************************
Lines 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

Lines 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

Lines 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

Lines 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

Lines 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

Lines 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

Lines 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)