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

Skip to content

Commit e0c6e1e

Browse files
story645anntzer
andcommitted
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]>
1 parent c16d7db commit e0c6e1e

File tree

8 files changed

+163
-25
lines changed

8 files changed

+163
-25
lines changed

lib/matplotlib/__init__.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,7 @@
137137
from collections import namedtuple
138138
from collections.abc import MutableMapping
139139
import contextlib
140+
import copy
140141
import functools
141142
import importlib
142143
import inspect
@@ -163,7 +164,6 @@
163164
from matplotlib._api import MatplotlibDeprecationWarning
164165
from matplotlib.rcsetup import validate_backend, cycler
165166

166-
167167
_log = logging.getLogger(__name__)
168168

169169
__bibtex__ = r"""@Article{Hunter:2007,
@@ -764,6 +764,14 @@ def __getitem__(self, key):
764764
from matplotlib import pyplot as plt
765765
plt.switch_backend(rcsetup._auto_backend_sentinel)
766766

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

769777
def _get_backend_or_none(self):

lib/matplotlib/mpl-data/matplotlibrc

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -677,7 +677,10 @@
677677
# line (in pixels).
678678
# - *randomness* is the factor by which the length is
679679
# randomly scaled.
680-
#path.effects:
680+
#path.effects: # patheffects functions, args, and, kwargs, e.g
681+
# {'name': 'withStroke', 'linewidth': 4},
682+
# {'name': 'SimpleLineShadow'}
683+
681684

682685

683686
## ***************************************************************************
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, Humor Sans, 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/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: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -565,6 +565,42 @@ def validate_sketch(s):
565565
raise ValueError("Expected a (scale, length, randomness) triplet")
566566

567567

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

12961332
# 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: str | list[AbstractPathEffect, Tuple[str,dict]])
145+
-> list[AbstractPathEffect | [Tuple[str, dict]]]: ...
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: 65 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,
@@ -27,8 +30,10 @@
2730
validate_int,
2831
validate_markevery,
2932
validate_stringlist,
33+
validate_path_effects,
3034
_validate_linestyle,
31-
_listify_validator)
35+
_listify_validator,
36+
)
3237

3338

3439
def test_rcparams(tmpdir):
@@ -628,3 +633,62 @@ def test_rcparams_legend_loc_from_file(tmpdir, value):
628633

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

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)