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 e0c6e1e

Browse filesBrowse 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 <anntzer.lee@gmail.com>
1 parent c16d7db commit e0c6e1e
Copy full SHA for e0c6e1e

File tree

8 files changed

+163
-25
lines changed
Filter options

8 files changed

+163
-25
lines changed

‎lib/matplotlib/__init__.py

Copy file name to clipboardExpand all lines: lib/matplotlib/__init__.py
+9-1Lines 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

Copy file name to clipboardExpand all lines: lib/matplotlib/mpl-data/matplotlibrc
+4-1Lines 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
## ***************************************************************************
+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, 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

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
+37-1Lines 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

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: 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

Copy file name to clipboardExpand all lines: lib/matplotlib/tests/test_rcparams.py
+65-1Lines 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

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.