Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

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

Sign up
Appearance settings

Commita44290f

Browse files
story645anntzer
andcommitted
added support for list of dicts of paths to path.effects rcparams
new path.effects validation of list of patheffect functions and/ordicts specifying functionscreated xkcd.mplstyle and shimmed it into plt.xkcd()new validate_anydict and validate_path_effects methods in rcsetupCo-authored-by: Antony Lee <anntzer.lee@gmail.com>
1 parentc06a97e commita44290f

File tree

8 files changed

+206
-25
lines changed

8 files changed

+206
-25
lines changed

‎lib/matplotlib/__init__.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,7 @@
137137
fromcollectionsimportnamedtuple
138138
fromcollections.abcimportMutableMapping
139139
importcontextlib
140+
importcopy
140141
importfunctools
141142
importimportlib
142143
importinspect
@@ -163,7 +164,6 @@
163164
frommatplotlib._apiimportMatplotlibDeprecationWarning
164165
frommatplotlib.rcsetupimportvalidate_backend,cycler
165166

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

169169
__bibtex__=r"""@Article{Hunter:2007,
@@ -764,6 +764,10 @@ def __getitem__(self, key):
764764
frommatplotlibimportpyplotasplt
765765
plt.switch_backend(rcsetup._auto_backend_sentinel)
766766

767+
elifkey=="path.effects"andselfisglobals().get("rcParams"):
768+
# to avoid circular imports
769+
returnself._load_path_effects()
770+
767771
returnself._get(key)
768772

769773
def_get_backend_or_none(self):
@@ -814,6 +818,14 @@ def copy(self):
814818
rccopy._set(k,self._get(k))
815819
returnrccopy
816820

821+
def_load_path_effects(self):
822+
"""defers loading of patheffects to avoid circular imports"""
823+
importmatplotlib.patheffectsaspath_effects
824+
825+
return [peifisinstance(pe,path_effects.AbstractPathEffect)
826+
elsegetattr(path_effects,pe.pop('name'))(**pe)
827+
forpeincopy.deepcopy(self._get('path.effects'))]
828+
817829

818830
defrc_params(fail_on_error=False):
819831
"""Construct a `RcParams` instance from the default Matplotlib rc file."""

‎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: {'name': '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-
frommatplotlibimportpatheffects
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
returnstack
772753

773754

‎lib/matplotlib/rcsetup.py

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,10 @@ def f(s):
9797
val= [scalar_validator(v.strip())forvinsifv.strip()]
9898
else:
9999
raise
100+
elifisinstance(s,dict):
101+
# assume dict is a value in the iterator and not the iterator
102+
# since iterating over dict only iterates over keys
103+
val= [scalar_validator(s)]
100104
# Allow any ordered sequence type -- generators, np.ndarray, pd.Series
101105
# -- but not sets, whose iteration order is non-deterministic.
102106
elifnp.iterable(s)andnotisinstance(s, (set,frozenset)):
@@ -125,9 +129,35 @@ def f(s):
125129

126130
defvalidate_any(s):
127131
returns
132+
128133
validate_anylist=_listify_validator(validate_any)
129134

130135

136+
defvalidate_anydict(allow_none=True,required_keys=None):
137+
"""Validate dictionary, check if keys are missing"""
138+
139+
required_keys=required_keysifrequired_keyselseset()
140+
141+
def_validate_dict(d):
142+
try:
143+
d=ast.literal_eval(d)
144+
exceptValueError:
145+
pass
146+
147+
ifallow_noneanddisNone:
148+
returnd
149+
150+
ifisinstance(d,dict):
151+
ifmissing_keys:= (required_keys-d.keys()):
152+
raiseValueError(f"Missing required key:{missing_keys!r}")
153+
returnd
154+
155+
raiseValueError(f"Input{d!r} must be a dictionary {{'k': v}} "
156+
f"{'or None'ifallow_noneelse''}")
157+
158+
return_validate_dict
159+
160+
131161
def_validate_date(s):
132162
try:
133163
np.datetime64(s)
@@ -565,6 +595,23 @@ def validate_sketch(s):
565595
raiseValueError("Expected a (scale, length, randomness) triplet")
566596

567597

598+
defvalidate_path_effects(s):
599+
ifnots:
600+
return []
601+
602+
_validate=validate_anydict(allow_none=True,required_keys={'name'})
603+
# string list of dict {k1: 1, k2:2}, {k1:2}
604+
# validate_anylist relies on , for parsing so parse here instead
605+
ifisinstance(s,str)ands.startswith("{"):
606+
s=ast.literal_eval(s)
607+
ifisinstance(s,dict):
608+
# list of one dict
609+
return_validate(s)
610+
611+
return [peifgetattr(pe,'__module__',"")=='matplotlib.patheffects'
612+
else_validate(pe)forpeinvalidate_anylist(s)]
613+
614+
568615
def_validate_greaterthan_minushalf(s):
569616
s=validate_float(s)
570617
ifs>-0.5:
@@ -1290,7 +1337,7 @@ def _convert_validator_spec(key, conv):
12901337
"path.simplify_threshold":_validate_greaterequal0_lessequal1,
12911338
"path.snap":validate_bool,
12921339
"path.sketch":validate_sketch,
1293-
"path.effects":validate_anylist,
1340+
"path.effects":validate_path_effects,
12941341
"agg.path.chunksize":validate_int,# 0 to disable chunking
12951342

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

‎lib/matplotlib/rcsetup.pyi

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ from cycler import Cycler
22

33
fromcollections.abcimportCallable,Iterable
44
fromtypingimportAny,Literal,TypeVar
5+
frommatplotlib.patheffectsimportAbstractPathEffect
56
frommatplotlib.typingimportColorType,LineStyleType,MarkEveryType
67

78
interactive_bk:list[str]
@@ -28,6 +29,8 @@ class ValidateInStrings:
2829

2930
defvalidate_any(s:Any)->Any: ...
3031
defvalidate_anylist(s:Any)->list[Any]: ...
32+
defvalidate_anydict(allow_none:bool=True,required_keys:set[str]|None=None
33+
)->Callable[[dict[str,Any]|None],dict[str,Any]]: ...
3134
defvalidate_bool(b:Any)->bool: ...
3235
defvalidate_axisbelow(s:Any)->bool|Literal["line"]: ...
3336
defvalidate_dpi(s:Any)->Literal["figure"]|float: ...
@@ -140,6 +143,7 @@ def _validate_linestyle(s: Any) -> LineStyleType: ...
140143
defvalidate_markeverylist(s:Any)->list[MarkEveryType]: ...
141144
defvalidate_bbox(s:Any)->Literal["tight","standard"]|None: ...
142145
defvalidate_sketch(s:Any)->None|tuple[float,float,float]: ...
146+
defvalidate_path_effects(s:Any)->list[AbstractPathEffect]|list[dict]: ...
143147
defvalidate_hatch(s:Any)->str: ...
144148
defvalidate_hatchlist(s:Any)->list[str]: ...
145149
defvalidate_dashlist(s:Any)->list[list[float]]: ...

‎lib/matplotlib/tests/test_rcparams.py

Lines changed: 92 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,12 @@
1212
frommatplotlibimport_api,_c_internal_utils
1313
importmatplotlib.pyplotasplt
1414
importmatplotlib.colorsasmcolors
15+
importmatplotlib.patheffectsaspath_effects
16+
frommatplotlib.testing.decoratorsimportcheck_figures_equal
17+
1518
importnumpyasnp
1619
frommatplotlib.rcsetupimport (
20+
validate_anydict,
1721
validate_bool,
1822
validate_color,
1923
validate_colorlist,
@@ -27,8 +31,10 @@
2731
validate_int,
2832
validate_markevery,
2933
validate_stringlist,
34+
validate_path_effects,
3035
_validate_linestyle,
31-
_listify_validator)
36+
_listify_validator,
37+
)
3238

3339

3440
deftest_rcparams(tmpdir):
@@ -628,3 +634,88 @@ def test_rcparams_legend_loc_from_file(tmpdir, value):
628634

629635
withmpl.rc_context(fname=rc_path):
630636
assertmpl.rcParams["legend.loc"]==value
637+
638+
639+
@pytest.mark.parametrize("allow_none", [True,False])
640+
deftest_validate_dict(allow_none):
641+
fval=validate_anydict(allow_none)
642+
assertfval("{'a': 1, 'b': 2}")== {'a':1,'b':2}
643+
withpytest.raises(ValueError,match=r"Input \['a', 'b'\] "):
644+
fval(['a','b'])
645+
646+
fval=validate_anydict(allow_none,required_keys={'a'})
647+
assertfval({'a':1})== {'a':1}
648+
withpytest.raises(ValueError,match="Missing required key: {'a'}"):
649+
fval({'b':1})
650+
651+
652+
deftest_validate_dict_none():
653+
assertvalidate_anydict()(None)isNone
654+
assertvalidate_anydict(required_keys={'a'})(None)isNone
655+
656+
withpytest.raises(ValueError,
657+
match=r"Input None must be a dictionary "):
658+
validate_anydict(False)(None)
659+
withpytest.raises(ValueError,
660+
match=r"Input 0 must be a dictionary {'k': v} or None"):
661+
validate_anydict(True)(0)
662+
663+
664+
ped= [{'name':'Normal'},
665+
{'name':'Stroke','offset': (1,2)},
666+
{'name':'withStroke','linewidth':4,'foreground':'w'}]
667+
668+
pel= [path_effects.Normal(),
669+
path_effects.Stroke((1,2)),
670+
path_effects.withStroke(linewidth=4,foreground='w')]
671+
672+
673+
@pytest.mark.parametrize("value", [pel,ped],ids=["func","dict"])
674+
deftest_path_effects(value):
675+
assertvalidate_path_effects(value)==value
676+
forvinvalue:
677+
assertvalidate_path_effects(value)==value
678+
679+
680+
deftest_path_effects_string_dict():
681+
"""test list of dicts properly parsed"""
682+
pstr="{'name': 'Normal'},"
683+
pstr+="{'name': 'Stroke', 'offset': (1, 2)},"
684+
pstr+="{'name': 'withStroke', 'linewidth': 4, 'foreground': 'w'}"
685+
assertvalidate_path_effects(pstr)==ped
686+
687+
688+
@pytest.mark.parametrize("fdict, flist",
689+
[([ped[0]], [pel[0]]),
690+
([ped[1]], [pel[1]]),
691+
([ped[2]], [ped[2]]),
692+
(ped,pel)],
693+
ids=['function','args','kwargs','all'])
694+
@check_figures_equal()
695+
deftest_path_effects_picture(fig_test,fig_ref,fdict,flist):
696+
withmpl.rc_context({'path.effects':fdict}):
697+
fig_test.subplots().plot([1,2,3])
698+
699+
withmpl.rc_context({'path.effects':flist}):
700+
fig_ref.subplots().plot([1,2,3])
701+
702+
703+
deftest_path_effect_errors():
704+
withpytest.raises(ValueError,match="Missing required key: {'name'}"):
705+
mpl.rcParams['path.effects']= [{'kwargs': {1,2,3}}]
706+
707+
withpytest.raises(ValueError,match=r"Key path.effects: Input 1 "):
708+
mpl.rcParams['path.effects']= [1,2,3]
709+
710+
711+
deftest_path_effects_from_file(tmpdir):
712+
# rcParams['legend.loc'] should be settable from matplotlibrc.
713+
# if any of these are not allowed, an exception will be raised.
714+
# test for gh issue #22338
715+
rc_path=tmpdir.join("matplotlibrc")
716+
rc_path.write("path.effects: "
717+
"{'name': 'Normal'}, {'name': 'withStroke', 'linewidth': 2}")
718+
719+
withmpl.rc_context(fname=rc_path):
720+
assertisinstance(mpl.rcParams["path.effects"][0],path_effects.Normal)
721+
assertisinstance(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
importmatplotlibasmpl
1010
frommatplotlibimportpyplotasplt,style
11+
frommatplotlib.testing.decoratorsimportcheck_figures_equal
1112
frommatplotlib.style.coreimportUSER_LIBRARY_PATHS,STYLE_EXTENSION
1213

1314

@@ -177,6 +178,18 @@ def test_xkcd_cm():
177178
assertmpl.rcParams["path.sketch"]isNone
178179

179180

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

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp