figrecipe 0.5.0__py3-none-any.whl → 0.6.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- figrecipe/__init__.py +361 -93
- figrecipe/_dev/__init__.py +120 -0
- figrecipe/_dev/demo_plotters/__init__.py +195 -0
- figrecipe/_dev/demo_plotters/plot_acorr.py +24 -0
- figrecipe/_dev/demo_plotters/plot_angle_spectrum.py +28 -0
- figrecipe/_dev/demo_plotters/plot_bar.py +25 -0
- figrecipe/_dev/demo_plotters/plot_barbs.py +30 -0
- figrecipe/_dev/demo_plotters/plot_barh.py +25 -0
- figrecipe/_dev/demo_plotters/plot_boxplot.py +24 -0
- figrecipe/_dev/demo_plotters/plot_cohere.py +29 -0
- figrecipe/_dev/demo_plotters/plot_contour.py +30 -0
- figrecipe/_dev/demo_plotters/plot_contourf.py +29 -0
- figrecipe/_dev/demo_plotters/plot_csd.py +29 -0
- figrecipe/_dev/demo_plotters/plot_ecdf.py +24 -0
- figrecipe/_dev/demo_plotters/plot_errorbar.py +28 -0
- figrecipe/_dev/demo_plotters/plot_eventplot.py +25 -0
- figrecipe/_dev/demo_plotters/plot_fill.py +29 -0
- figrecipe/_dev/demo_plotters/plot_fill_between.py +30 -0
- figrecipe/_dev/demo_plotters/plot_fill_betweenx.py +28 -0
- figrecipe/_dev/demo_plotters/plot_hexbin.py +25 -0
- figrecipe/_dev/demo_plotters/plot_hist.py +24 -0
- figrecipe/_dev/demo_plotters/plot_hist2d.py +25 -0
- figrecipe/_dev/demo_plotters/plot_imshow.py +23 -0
- figrecipe/_dev/demo_plotters/plot_loglog.py +27 -0
- figrecipe/_dev/demo_plotters/plot_magnitude_spectrum.py +28 -0
- figrecipe/_dev/demo_plotters/plot_matshow.py +23 -0
- figrecipe/_dev/demo_plotters/plot_pcolor.py +29 -0
- figrecipe/_dev/demo_plotters/plot_pcolormesh.py +29 -0
- figrecipe/_dev/demo_plotters/plot_phase_spectrum.py +28 -0
- figrecipe/_dev/demo_plotters/plot_pie.py +23 -0
- figrecipe/_dev/demo_plotters/plot_plot.py +27 -0
- figrecipe/_dev/demo_plotters/plot_psd.py +29 -0
- figrecipe/_dev/demo_plotters/plot_quiver.py +30 -0
- figrecipe/_dev/demo_plotters/plot_scatter.py +24 -0
- figrecipe/_dev/demo_plotters/plot_semilogx.py +27 -0
- figrecipe/_dev/demo_plotters/plot_semilogy.py +27 -0
- figrecipe/_dev/demo_plotters/plot_specgram.py +30 -0
- figrecipe/_dev/demo_plotters/plot_spy.py +29 -0
- figrecipe/_dev/demo_plotters/plot_stackplot.py +29 -0
- figrecipe/_dev/demo_plotters/plot_stairs.py +27 -0
- figrecipe/_dev/demo_plotters/plot_stem.py +27 -0
- figrecipe/_dev/demo_plotters/plot_step.py +27 -0
- figrecipe/_dev/demo_plotters/plot_streamplot.py +30 -0
- figrecipe/_dev/demo_plotters/plot_tricontour.py +28 -0
- figrecipe/_dev/demo_plotters/plot_tricontourf.py +28 -0
- figrecipe/_dev/demo_plotters/plot_tripcolor.py +29 -0
- figrecipe/_dev/demo_plotters/plot_triplot.py +25 -0
- figrecipe/_dev/demo_plotters/plot_violinplot.py +25 -0
- figrecipe/_dev/demo_plotters/plot_xcorr.py +25 -0
- figrecipe/_editor/__init__.py +230 -0
- figrecipe/_editor/_bbox.py +978 -0
- figrecipe/_editor/_flask_app.py +1229 -0
- figrecipe/_editor/_hitmap.py +937 -0
- figrecipe/_editor/_overrides.py +318 -0
- figrecipe/_editor/_renderer.py +349 -0
- figrecipe/_editor/_templates/__init__.py +75 -0
- figrecipe/_editor/_templates/_html.py +406 -0
- figrecipe/_editor/_templates/_scripts.py +2778 -0
- figrecipe/_editor/_templates/_styles.py +1326 -0
- figrecipe/_params/_DECORATION_METHODS.py +27 -0
- figrecipe/_params/_PLOTTING_METHODS.py +58 -0
- figrecipe/_params/__init__.py +9 -0
- figrecipe/_recorder.py +126 -73
- figrecipe/_reproducer.py +658 -41
- figrecipe/_seaborn.py +14 -9
- figrecipe/_serializer.py +2 -2
- figrecipe/_signatures/README.md +68 -0
- figrecipe/_signatures/__init__.py +12 -2
- figrecipe/_signatures/_loader.py +515 -56
- figrecipe/_utils/__init__.py +6 -4
- figrecipe/_utils/_crop.py +10 -4
- figrecipe/_utils/_image_diff.py +37 -33
- figrecipe/_utils/_numpy_io.py +0 -1
- figrecipe/_utils/_units.py +11 -3
- figrecipe/_validator.py +12 -3
- figrecipe/_wrappers/_axes.py +860 -46
- figrecipe/_wrappers/_figure.py +115 -18
- figrecipe/plt.py +0 -1
- figrecipe/pyplot.py +2 -1
- figrecipe/styles/__init__.py +9 -10
- figrecipe/styles/_style_applier.py +332 -28
- figrecipe/styles/_style_loader.py +172 -44
- figrecipe/styles/presets/MATPLOTLIB.yaml +94 -0
- figrecipe/styles/presets/SCITEX.yaml +176 -0
- figrecipe-0.6.0.dist-info/METADATA +394 -0
- figrecipe-0.6.0.dist-info/RECORD +90 -0
- figrecipe-0.5.0.dist-info/METADATA +0 -336
- figrecipe-0.5.0.dist-info/RECORD +0 -26
- {figrecipe-0.5.0.dist-info → figrecipe-0.6.0.dist-info}/WHEEL +0 -0
- {figrecipe-0.5.0.dist-info → figrecipe-0.6.0.dist-info}/licenses/LICENSE +0 -0
figrecipe/_signatures/_loader.py
CHANGED
|
@@ -1,32 +1,475 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
2
|
# -*- coding: utf-8 -*-
|
|
3
|
-
|
|
3
|
+
# Timestamp: "2025-12-23 (ywatanabe)"
|
|
4
|
+
# File: /home/ywatanabe/proj/figrecipe/src/figrecipe/_signatures/_loader.py
|
|
5
|
+
|
|
6
|
+
"""Load and query matplotlib function signatures with deep inspection.
|
|
7
|
+
|
|
8
|
+
Parses *args/**kwargs from docstrings and expands them to actual parameters.
|
|
9
|
+
"""
|
|
4
10
|
|
|
5
11
|
import inspect
|
|
6
|
-
|
|
12
|
+
import re
|
|
13
|
+
from typing import Any, Dict, List, Optional
|
|
7
14
|
|
|
8
15
|
import matplotlib.pyplot as plt
|
|
9
16
|
|
|
10
|
-
|
|
11
17
|
# Cache for signatures
|
|
12
18
|
_SIGNATURE_CACHE: Dict[str, Dict[str, Any]] = {}
|
|
13
19
|
|
|
14
20
|
|
|
15
|
-
|
|
16
|
-
|
|
21
|
+
# -----------------------------------------------------------------------------
|
|
22
|
+
# Docstring parsing
|
|
23
|
+
# -----------------------------------------------------------------------------
|
|
24
|
+
def _parse_parameter_types(docstring: Optional[str]) -> Dict[str, str]:
|
|
25
|
+
"""Extract parameter types from NumPy-style docstring Parameters section."""
|
|
26
|
+
if not docstring:
|
|
27
|
+
return {}
|
|
28
|
+
|
|
29
|
+
types = {}
|
|
30
|
+
|
|
31
|
+
# Find Parameters section
|
|
32
|
+
params_match = re.search(
|
|
33
|
+
r"Parameters\s*[-]+\s*(.*?)(?:\n\s*Returns|\n\s*See Also|\n\s*Notes|\n\s*Examples|\n\s*Other Parameters|\Z)",
|
|
34
|
+
docstring,
|
|
35
|
+
re.DOTALL,
|
|
36
|
+
)
|
|
37
|
+
if not params_match:
|
|
38
|
+
return {}
|
|
39
|
+
|
|
40
|
+
params_text = params_match.group(1)
|
|
41
|
+
|
|
42
|
+
# Parse lines like "x, y : array-like or float" or "fmt : str, optional"
|
|
43
|
+
for match in re.finditer(
|
|
44
|
+
r"^(\w+(?:\s*,\s*\w+)*)\s*:\s*(.+?)(?=\n\s*\n|\n\w+\s*:|\Z)",
|
|
45
|
+
params_text,
|
|
46
|
+
re.MULTILINE | re.DOTALL,
|
|
47
|
+
):
|
|
48
|
+
names_str = match.group(1)
|
|
49
|
+
type_str = match.group(2).split("\n")[0].strip() # First line only
|
|
50
|
+
|
|
51
|
+
# Clean up type string
|
|
52
|
+
type_str = re.sub(r",?\s*optional\s*$", "", type_str).strip()
|
|
53
|
+
type_str = re.sub(
|
|
54
|
+
r",?\s*default[^,]*$", "", type_str, flags=re.IGNORECASE
|
|
55
|
+
).strip()
|
|
56
|
+
|
|
57
|
+
# Handle multiple names like "x, y"
|
|
58
|
+
for name in re.split(r"\s*,\s*", names_str):
|
|
59
|
+
name = name.strip()
|
|
60
|
+
if name:
|
|
61
|
+
types[name.lower()] = type_str
|
|
62
|
+
|
|
63
|
+
return types
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _parse_args_pattern(
|
|
67
|
+
args_str: str, param_types: Dict[str, str]
|
|
68
|
+
) -> List[Dict[str, Any]]:
|
|
69
|
+
"""Parse args pattern like '[x], y, [fmt]' into list of arg dicts."""
|
|
70
|
+
if not args_str:
|
|
71
|
+
return []
|
|
72
|
+
|
|
73
|
+
args = []
|
|
74
|
+
# Split by comma, handling brackets
|
|
75
|
+
parts = re.split(r",\s*", args_str)
|
|
76
|
+
|
|
77
|
+
for part in parts:
|
|
78
|
+
part = part.strip()
|
|
79
|
+
if not part or part == "/": # Skip empty or positional-only marker
|
|
80
|
+
continue
|
|
81
|
+
|
|
82
|
+
# Check if optional (wrapped in [])
|
|
83
|
+
optional = part.startswith("[") and part.endswith("]")
|
|
84
|
+
if optional:
|
|
85
|
+
name = part[1:-1].strip()
|
|
86
|
+
else:
|
|
87
|
+
# Handle cases like "[X, Y,] Z" where Z is required
|
|
88
|
+
name = part.strip("[]").strip()
|
|
89
|
+
|
|
90
|
+
if name and name not in ("...", "*"):
|
|
91
|
+
# Look up type from parsed parameters
|
|
92
|
+
type_str = param_types.get(name.lower())
|
|
93
|
+
args.append(
|
|
94
|
+
{
|
|
95
|
+
"name": name,
|
|
96
|
+
"type": type_str,
|
|
97
|
+
"optional": optional,
|
|
98
|
+
}
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
return args
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
# Manual *args patterns for functions without parseable call signatures
|
|
105
|
+
MANUAL_ARGS_PATTERNS = {
|
|
106
|
+
"fill": "[x], y, [color]",
|
|
107
|
+
"stackplot": "x, *ys",
|
|
108
|
+
"legend": "[handles], [labels]",
|
|
109
|
+
"stem": "[locs], heads",
|
|
110
|
+
"tricontour": "[triangulation], x, y, z, [levels]",
|
|
111
|
+
"tricontourf": "[triangulation], x, y, z, [levels]",
|
|
112
|
+
"triplot": "[triangulation], x, y, [triangles]",
|
|
113
|
+
"loglog": "[x], y, [fmt]",
|
|
114
|
+
"semilogx": "[x], y, [fmt]",
|
|
115
|
+
"semilogy": "[x], y, [fmt]",
|
|
116
|
+
"barbs": "[X], [Y], U, V, [C]",
|
|
117
|
+
"quiver": "[X], [Y], U, V, [C]",
|
|
118
|
+
"pcolor": "[X], [Y], C",
|
|
119
|
+
"pcolormesh": "[X], [Y], C",
|
|
120
|
+
"pcolorfast": "[X], [Y], C",
|
|
121
|
+
"acorr": "x",
|
|
122
|
+
"xcorr": "x, y",
|
|
123
|
+
"plot": "[x], y, [fmt]",
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _extract_args_from_docstring(
|
|
128
|
+
docstring: Optional[str], func_name: str = ""
|
|
129
|
+
) -> List[Dict[str, Any]]:
|
|
130
|
+
"""Extract *args as flattened list from docstring call signature."""
|
|
131
|
+
if not docstring:
|
|
132
|
+
return []
|
|
133
|
+
|
|
134
|
+
# First, parse parameter types
|
|
135
|
+
param_types = _parse_parameter_types(docstring)
|
|
136
|
+
|
|
137
|
+
# Check for manual pattern first
|
|
138
|
+
if func_name in MANUAL_ARGS_PATTERNS:
|
|
139
|
+
return _parse_args_pattern(MANUAL_ARGS_PATTERNS[func_name], param_types)
|
|
140
|
+
|
|
141
|
+
# Look for "Call signature:" patterns
|
|
142
|
+
patterns = [
|
|
143
|
+
r"Call signatures?::\s*\n\s*(.*?)(?:\n\n|\n[A-Z])",
|
|
144
|
+
r"^\s*(\w+\([^)]+\))\s*$",
|
|
145
|
+
]
|
|
146
|
+
|
|
147
|
+
for pattern in patterns:
|
|
148
|
+
match = re.search(pattern, docstring, re.MULTILINE | re.DOTALL)
|
|
149
|
+
if match:
|
|
150
|
+
sig_text = match.group(1).strip()
|
|
151
|
+
# Extract first signature line
|
|
152
|
+
first_line = sig_text.split("\n")[0].strip()
|
|
153
|
+
# Parse the args from signature like "plot([x], y, [fmt], *, ...)"
|
|
154
|
+
inner_match = re.search(r"\(([^*]+?)(?:,\s*\*|,\s*data|\))", first_line)
|
|
155
|
+
if inner_match:
|
|
156
|
+
args_str = inner_match.group(1).strip().rstrip(",")
|
|
157
|
+
return _parse_args_pattern(args_str, param_types)
|
|
158
|
+
return []
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
# -----------------------------------------------------------------------------
|
|
162
|
+
# Kwargs expansion via set_* method introspection
|
|
163
|
+
# -----------------------------------------------------------------------------
|
|
164
|
+
def _get_setter_type(obj: Any, prop_name: str) -> Optional[str]:
|
|
165
|
+
"""Get type from set_* method docstring."""
|
|
166
|
+
setter_name = f"set_{prop_name}"
|
|
167
|
+
if not hasattr(obj, setter_name):
|
|
168
|
+
return None
|
|
169
|
+
|
|
170
|
+
method = getattr(obj, setter_name)
|
|
171
|
+
if not method.__doc__:
|
|
172
|
+
return None
|
|
173
|
+
|
|
174
|
+
# Parse Parameters section
|
|
175
|
+
match = re.search(
|
|
176
|
+
r"Parameters\s*[-]+\s*\n\s*(\w+)\s*:\s*(.+?)(?:\n\s*\n|\Z)",
|
|
177
|
+
method.__doc__,
|
|
178
|
+
re.DOTALL,
|
|
179
|
+
)
|
|
180
|
+
if match:
|
|
181
|
+
type_str = match.group(2).split("\n")[0].strip()
|
|
182
|
+
return type_str
|
|
183
|
+
return None
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def _build_kwargs_with_types() -> (
|
|
187
|
+
tuple[
|
|
188
|
+
List[Dict[str, Any]],
|
|
189
|
+
List[Dict[str, Any]],
|
|
190
|
+
List[Dict[str, Any]],
|
|
191
|
+
List[Dict[str, Any]],
|
|
192
|
+
]
|
|
193
|
+
):
|
|
194
|
+
"""Build kwargs lists with types from matplotlib classes."""
|
|
195
|
+
from matplotlib.artist import Artist
|
|
196
|
+
from matplotlib.lines import Line2D
|
|
197
|
+
from matplotlib.patches import Patch
|
|
198
|
+
from matplotlib.text import Text
|
|
199
|
+
|
|
200
|
+
# Create instances for introspection
|
|
201
|
+
line = Line2D([0], [0])
|
|
202
|
+
patch = Patch()
|
|
203
|
+
text = Text()
|
|
204
|
+
artist = Artist()
|
|
205
|
+
|
|
206
|
+
def get_type(obj, name):
|
|
207
|
+
return _get_setter_type(obj, name)
|
|
208
|
+
|
|
209
|
+
ARTIST_KWARGS = [
|
|
210
|
+
{"name": "agg_filter", "type": get_type(artist, "agg_filter"), "default": None},
|
|
211
|
+
{"name": "alpha", "type": get_type(artist, "alpha"), "default": None},
|
|
212
|
+
{"name": "animated", "type": get_type(artist, "animated"), "default": False},
|
|
213
|
+
{"name": "clip_box", "type": get_type(artist, "clip_box"), "default": None},
|
|
214
|
+
{"name": "clip_on", "type": get_type(artist, "clip_on"), "default": True},
|
|
215
|
+
{"name": "clip_path", "type": get_type(artist, "clip_path"), "default": None},
|
|
216
|
+
{"name": "gid", "type": get_type(artist, "gid"), "default": None},
|
|
217
|
+
{"name": "label", "type": get_type(artist, "label"), "default": ""},
|
|
218
|
+
{
|
|
219
|
+
"name": "path_effects",
|
|
220
|
+
"type": get_type(artist, "path_effects"),
|
|
221
|
+
"default": None,
|
|
222
|
+
},
|
|
223
|
+
{"name": "picker", "type": get_type(artist, "picker"), "default": None},
|
|
224
|
+
{"name": "rasterized", "type": get_type(artist, "rasterized"), "default": None},
|
|
225
|
+
{
|
|
226
|
+
"name": "sketch_params",
|
|
227
|
+
"type": get_type(artist, "sketch_params"),
|
|
228
|
+
"default": None,
|
|
229
|
+
},
|
|
230
|
+
{"name": "snap", "type": get_type(artist, "snap"), "default": None},
|
|
231
|
+
{"name": "transform", "type": get_type(artist, "transform"), "default": None},
|
|
232
|
+
{"name": "url", "type": get_type(artist, "url"), "default": None},
|
|
233
|
+
{"name": "visible", "type": get_type(artist, "visible"), "default": True},
|
|
234
|
+
{"name": "zorder", "type": get_type(artist, "zorder"), "default": None},
|
|
235
|
+
]
|
|
236
|
+
|
|
237
|
+
LINE2D_KWARGS = [
|
|
238
|
+
{"name": "color", "type": get_type(line, "color"), "default": None},
|
|
239
|
+
{"name": "linestyle", "type": get_type(line, "linestyle"), "default": "-"},
|
|
240
|
+
{"name": "linewidth", "type": get_type(line, "linewidth"), "default": None},
|
|
241
|
+
{"name": "marker", "type": get_type(line, "marker"), "default": ""},
|
|
242
|
+
{
|
|
243
|
+
"name": "markeredgecolor",
|
|
244
|
+
"type": get_type(line, "markeredgecolor"),
|
|
245
|
+
"default": None,
|
|
246
|
+
},
|
|
247
|
+
{
|
|
248
|
+
"name": "markeredgewidth",
|
|
249
|
+
"type": get_type(line, "markeredgewidth"),
|
|
250
|
+
"default": None,
|
|
251
|
+
},
|
|
252
|
+
{
|
|
253
|
+
"name": "markerfacecolor",
|
|
254
|
+
"type": get_type(line, "markerfacecolor"),
|
|
255
|
+
"default": None,
|
|
256
|
+
},
|
|
257
|
+
{"name": "markersize", "type": get_type(line, "markersize"), "default": None},
|
|
258
|
+
{"name": "antialiased", "type": get_type(line, "antialiased"), "default": True},
|
|
259
|
+
{
|
|
260
|
+
"name": "dash_capstyle",
|
|
261
|
+
"type": get_type(line, "dash_capstyle"),
|
|
262
|
+
"default": "butt",
|
|
263
|
+
},
|
|
264
|
+
{
|
|
265
|
+
"name": "dash_joinstyle",
|
|
266
|
+
"type": get_type(line, "dash_joinstyle"),
|
|
267
|
+
"default": "round",
|
|
268
|
+
},
|
|
269
|
+
{
|
|
270
|
+
"name": "solid_capstyle",
|
|
271
|
+
"type": get_type(line, "solid_capstyle"),
|
|
272
|
+
"default": "projecting",
|
|
273
|
+
},
|
|
274
|
+
{
|
|
275
|
+
"name": "solid_joinstyle",
|
|
276
|
+
"type": get_type(line, "solid_joinstyle"),
|
|
277
|
+
"default": "round",
|
|
278
|
+
},
|
|
279
|
+
{
|
|
280
|
+
"name": "drawstyle",
|
|
281
|
+
"type": get_type(line, "drawstyle"),
|
|
282
|
+
"default": "default",
|
|
283
|
+
},
|
|
284
|
+
{"name": "fillstyle", "type": get_type(line, "fillstyle"), "default": "full"},
|
|
285
|
+
]
|
|
286
|
+
|
|
287
|
+
PATCH_KWARGS = [
|
|
288
|
+
{"name": "color", "type": get_type(patch, "color"), "default": None},
|
|
289
|
+
{"name": "edgecolor", "type": get_type(patch, "edgecolor"), "default": None},
|
|
290
|
+
{"name": "facecolor", "type": get_type(patch, "facecolor"), "default": None},
|
|
291
|
+
{"name": "fill", "type": get_type(patch, "fill"), "default": True},
|
|
292
|
+
{"name": "hatch", "type": get_type(patch, "hatch"), "default": None},
|
|
293
|
+
{"name": "linestyle", "type": get_type(patch, "linestyle"), "default": "-"},
|
|
294
|
+
{"name": "linewidth", "type": get_type(patch, "linewidth"), "default": None},
|
|
295
|
+
{
|
|
296
|
+
"name": "antialiased",
|
|
297
|
+
"type": get_type(patch, "antialiased"),
|
|
298
|
+
"default": None,
|
|
299
|
+
},
|
|
300
|
+
{"name": "capstyle", "type": get_type(patch, "capstyle"), "default": "butt"},
|
|
301
|
+
{"name": "joinstyle", "type": get_type(patch, "joinstyle"), "default": "miter"},
|
|
302
|
+
]
|
|
303
|
+
|
|
304
|
+
TEXT_KWARGS = [
|
|
305
|
+
{"name": "color", "type": get_type(text, "color"), "default": "black"},
|
|
306
|
+
{"name": "fontfamily", "type": get_type(text, "fontfamily"), "default": None},
|
|
307
|
+
{"name": "fontsize", "type": get_type(text, "fontsize"), "default": None},
|
|
308
|
+
{"name": "fontstretch", "type": get_type(text, "fontstretch"), "default": None},
|
|
309
|
+
{"name": "fontstyle", "type": get_type(text, "fontstyle"), "default": "normal"},
|
|
310
|
+
{
|
|
311
|
+
"name": "fontvariant",
|
|
312
|
+
"type": get_type(text, "fontvariant"),
|
|
313
|
+
"default": "normal",
|
|
314
|
+
},
|
|
315
|
+
{
|
|
316
|
+
"name": "fontweight",
|
|
317
|
+
"type": get_type(text, "fontweight"),
|
|
318
|
+
"default": "normal",
|
|
319
|
+
},
|
|
320
|
+
{
|
|
321
|
+
"name": "horizontalalignment",
|
|
322
|
+
"type": get_type(text, "horizontalalignment"),
|
|
323
|
+
"default": "center",
|
|
324
|
+
},
|
|
325
|
+
{
|
|
326
|
+
"name": "verticalalignment",
|
|
327
|
+
"type": get_type(text, "verticalalignment"),
|
|
328
|
+
"default": "center",
|
|
329
|
+
},
|
|
330
|
+
{"name": "rotation", "type": get_type(text, "rotation"), "default": None},
|
|
331
|
+
{"name": "linespacing", "type": get_type(text, "linespacing"), "default": None},
|
|
332
|
+
{
|
|
333
|
+
"name": "multialignment",
|
|
334
|
+
"type": get_type(text, "multialignment"),
|
|
335
|
+
"default": None,
|
|
336
|
+
},
|
|
337
|
+
{"name": "wrap", "type": get_type(text, "wrap"), "default": False},
|
|
338
|
+
]
|
|
339
|
+
|
|
340
|
+
return ARTIST_KWARGS, LINE2D_KWARGS, PATCH_KWARGS, TEXT_KWARGS
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
# Build kwargs with types (lazy initialization)
|
|
344
|
+
_KWARGS_CACHE: Optional[Dict[str, List[Dict[str, Any]]]] = None
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
def _get_kwargs_mapping() -> Dict[str, List[Dict[str, Any]]]:
|
|
348
|
+
"""Get kwargs mapping, building it lazily on first call."""
|
|
349
|
+
global _KWARGS_CACHE
|
|
350
|
+
if _KWARGS_CACHE is not None:
|
|
351
|
+
return _KWARGS_CACHE
|
|
352
|
+
|
|
353
|
+
ARTIST_KWARGS, LINE2D_KWARGS, PATCH_KWARGS, TEXT_KWARGS = _build_kwargs_with_types()
|
|
354
|
+
|
|
355
|
+
_KWARGS_CACHE = {
|
|
356
|
+
"plot": LINE2D_KWARGS + ARTIST_KWARGS,
|
|
357
|
+
"scatter": ARTIST_KWARGS,
|
|
358
|
+
"bar": PATCH_KWARGS + ARTIST_KWARGS,
|
|
359
|
+
"barh": PATCH_KWARGS + ARTIST_KWARGS,
|
|
360
|
+
"fill": PATCH_KWARGS + ARTIST_KWARGS,
|
|
361
|
+
"fill_between": PATCH_KWARGS + ARTIST_KWARGS,
|
|
362
|
+
"fill_betweenx": PATCH_KWARGS + ARTIST_KWARGS,
|
|
363
|
+
"step": LINE2D_KWARGS + ARTIST_KWARGS,
|
|
364
|
+
"errorbar": LINE2D_KWARGS + ARTIST_KWARGS,
|
|
365
|
+
"hist": PATCH_KWARGS + ARTIST_KWARGS,
|
|
366
|
+
"hist2d": ARTIST_KWARGS,
|
|
367
|
+
"imshow": ARTIST_KWARGS,
|
|
368
|
+
"pcolor": ARTIST_KWARGS,
|
|
369
|
+
"pcolormesh": ARTIST_KWARGS,
|
|
370
|
+
"pcolorfast": ARTIST_KWARGS,
|
|
371
|
+
"contour": ARTIST_KWARGS,
|
|
372
|
+
"contourf": ARTIST_KWARGS,
|
|
373
|
+
"hexbin": ARTIST_KWARGS,
|
|
374
|
+
"quiver": ARTIST_KWARGS,
|
|
375
|
+
"barbs": ARTIST_KWARGS,
|
|
376
|
+
"specgram": ARTIST_KWARGS,
|
|
377
|
+
"psd": LINE2D_KWARGS + ARTIST_KWARGS,
|
|
378
|
+
"csd": LINE2D_KWARGS + ARTIST_KWARGS,
|
|
379
|
+
"cohere": LINE2D_KWARGS + ARTIST_KWARGS,
|
|
380
|
+
"acorr": LINE2D_KWARGS + ARTIST_KWARGS,
|
|
381
|
+
"xcorr": LINE2D_KWARGS + ARTIST_KWARGS,
|
|
382
|
+
"angle_spectrum": LINE2D_KWARGS + ARTIST_KWARGS,
|
|
383
|
+
"magnitude_spectrum": LINE2D_KWARGS + ARTIST_KWARGS,
|
|
384
|
+
"phase_spectrum": LINE2D_KWARGS + ARTIST_KWARGS,
|
|
385
|
+
"stackplot": PATCH_KWARGS + ARTIST_KWARGS,
|
|
386
|
+
"stairs": PATCH_KWARGS + ARTIST_KWARGS,
|
|
387
|
+
"eventplot": ARTIST_KWARGS,
|
|
388
|
+
"broken_barh": PATCH_KWARGS + ARTIST_KWARGS,
|
|
389
|
+
"loglog": LINE2D_KWARGS + ARTIST_KWARGS,
|
|
390
|
+
"semilogx": LINE2D_KWARGS + ARTIST_KWARGS,
|
|
391
|
+
"semilogy": LINE2D_KWARGS + ARTIST_KWARGS,
|
|
392
|
+
"annotate": TEXT_KWARGS + ARTIST_KWARGS,
|
|
393
|
+
"text": TEXT_KWARGS + ARTIST_KWARGS,
|
|
394
|
+
"arrow": PATCH_KWARGS + ARTIST_KWARGS,
|
|
395
|
+
"axhline": LINE2D_KWARGS + ARTIST_KWARGS,
|
|
396
|
+
"axvline": LINE2D_KWARGS + ARTIST_KWARGS,
|
|
397
|
+
"hlines": ARTIST_KWARGS,
|
|
398
|
+
"vlines": ARTIST_KWARGS,
|
|
399
|
+
"axhspan": PATCH_KWARGS + ARTIST_KWARGS,
|
|
400
|
+
"axvspan": PATCH_KWARGS + ARTIST_KWARGS,
|
|
401
|
+
"axline": LINE2D_KWARGS + ARTIST_KWARGS,
|
|
402
|
+
"legend": ARTIST_KWARGS,
|
|
403
|
+
"grid": LINE2D_KWARGS + ARTIST_KWARGS,
|
|
404
|
+
"table": ARTIST_KWARGS,
|
|
405
|
+
"clabel": TEXT_KWARGS + ARTIST_KWARGS,
|
|
406
|
+
"bar_label": TEXT_KWARGS + ARTIST_KWARGS,
|
|
407
|
+
"quiverkey": ARTIST_KWARGS,
|
|
408
|
+
"ecdf": LINE2D_KWARGS + ARTIST_KWARGS,
|
|
409
|
+
"tricontour": ARTIST_KWARGS,
|
|
410
|
+
"tricontourf": ARTIST_KWARGS,
|
|
411
|
+
"tripcolor": ARTIST_KWARGS,
|
|
412
|
+
"triplot": LINE2D_KWARGS + ARTIST_KWARGS,
|
|
413
|
+
"matshow": ARTIST_KWARGS,
|
|
414
|
+
"spy": ARTIST_KWARGS + LINE2D_KWARGS,
|
|
415
|
+
"boxplot": ARTIST_KWARGS,
|
|
416
|
+
"violinplot": ARTIST_KWARGS,
|
|
417
|
+
"pie": PATCH_KWARGS + ARTIST_KWARGS,
|
|
418
|
+
"stem": LINE2D_KWARGS + ARTIST_KWARGS,
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
return _KWARGS_CACHE
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
# -----------------------------------------------------------------------------
|
|
425
|
+
# Type helpers
|
|
426
|
+
# -----------------------------------------------------------------------------
|
|
427
|
+
def _get_type_str(annotation) -> Optional[str]:
|
|
428
|
+
"""Convert annotation to string."""
|
|
429
|
+
if annotation is inspect.Parameter.empty:
|
|
430
|
+
return None
|
|
431
|
+
if hasattr(annotation, "__name__"):
|
|
432
|
+
return annotation.__name__
|
|
433
|
+
return str(annotation)
|
|
434
|
+
|
|
435
|
+
|
|
436
|
+
def _serialize_default(default) -> Any:
|
|
437
|
+
"""Serialize default value."""
|
|
438
|
+
if default is inspect.Parameter.empty:
|
|
439
|
+
return None
|
|
440
|
+
if callable(default):
|
|
441
|
+
return f"<{type(default).__name__}>"
|
|
442
|
+
try:
|
|
443
|
+
import json
|
|
444
|
+
|
|
445
|
+
json.dumps(default)
|
|
446
|
+
return default
|
|
447
|
+
except (TypeError, ValueError):
|
|
448
|
+
return repr(default)
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
# -----------------------------------------------------------------------------
|
|
452
|
+
# Main API
|
|
453
|
+
# -----------------------------------------------------------------------------
|
|
454
|
+
def get_signature(method_name: str, expand_kwargs: bool = True) -> Dict[str, Any]:
|
|
455
|
+
"""Get signature for a matplotlib Axes method with deep inspection.
|
|
17
456
|
|
|
18
457
|
Parameters
|
|
19
458
|
----------
|
|
20
459
|
method_name : str
|
|
21
460
|
Name of the method (e.g., 'plot', 'scatter').
|
|
461
|
+
expand_kwargs : bool
|
|
462
|
+
If True, expand **kwargs to actual parameters.
|
|
22
463
|
|
|
23
464
|
Returns
|
|
24
465
|
-------
|
|
25
466
|
dict
|
|
26
467
|
Signature information with 'args' and 'kwargs' keys.
|
|
468
|
+
Args and kwargs are expanded from docstrings when possible.
|
|
27
469
|
"""
|
|
28
|
-
|
|
29
|
-
|
|
470
|
+
cache_key = f"{method_name}_{expand_kwargs}"
|
|
471
|
+
if cache_key in _SIGNATURE_CACHE:
|
|
472
|
+
return _SIGNATURE_CACHE[cache_key]
|
|
30
473
|
|
|
31
474
|
# Create a temporary axes to introspect
|
|
32
475
|
fig, ax = plt.subplots()
|
|
@@ -41,58 +484,76 @@ def get_signature(method_name: str) -> Dict[str, Any]:
|
|
|
41
484
|
except (ValueError, TypeError):
|
|
42
485
|
return {"args": [], "kwargs": {}}
|
|
43
486
|
|
|
487
|
+
# Parse parameter types from docstring
|
|
488
|
+
param_types = _parse_parameter_types(method.__doc__)
|
|
489
|
+
|
|
44
490
|
args = []
|
|
45
491
|
kwargs = {}
|
|
492
|
+
has_var_positional = False
|
|
493
|
+
has_var_keyword = False
|
|
46
494
|
|
|
47
495
|
for name, param in sig.parameters.items():
|
|
48
496
|
if name == "self":
|
|
49
497
|
continue
|
|
50
498
|
|
|
51
499
|
if param.kind == inspect.Parameter.VAR_POSITIONAL:
|
|
52
|
-
|
|
500
|
+
has_var_positional = True
|
|
53
501
|
elif param.kind == inspect.Parameter.VAR_KEYWORD:
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
#
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
502
|
+
has_var_keyword = True
|
|
503
|
+
else:
|
|
504
|
+
# Try annotation first, then docstring
|
|
505
|
+
typehint = _get_type_str(param.annotation)
|
|
506
|
+
if not typehint:
|
|
507
|
+
typehint = param_types.get(name.lower())
|
|
508
|
+
|
|
509
|
+
if param.default is inspect.Parameter.empty:
|
|
510
|
+
# Positional argument
|
|
511
|
+
args.append(
|
|
512
|
+
{
|
|
513
|
+
"name": name,
|
|
514
|
+
"type": typehint,
|
|
515
|
+
}
|
|
516
|
+
)
|
|
517
|
+
else:
|
|
518
|
+
# Keyword argument with default
|
|
519
|
+
kwargs[name] = {
|
|
520
|
+
"type": typehint,
|
|
521
|
+
"default": _serialize_default(param.default),
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
# Expand *args from docstring
|
|
525
|
+
if has_var_positional:
|
|
526
|
+
docstring_args = _extract_args_from_docstring(method.__doc__, method_name)
|
|
527
|
+
if docstring_args:
|
|
528
|
+
# Insert flattened args at the beginning
|
|
529
|
+
for i, arg in enumerate(docstring_args):
|
|
530
|
+
args.insert(i, arg)
|
|
531
|
+
else:
|
|
532
|
+
# No docstring info, keep generic *args
|
|
533
|
+
args.insert(0, {"name": "*args", "type": "*args"})
|
|
534
|
+
|
|
535
|
+
# Expand **kwargs based on function type
|
|
536
|
+
if has_var_keyword and expand_kwargs:
|
|
537
|
+
kwargs_mapping = _get_kwargs_mapping()
|
|
538
|
+
if method_name in kwargs_mapping:
|
|
539
|
+
expanded_kwargs = kwargs_mapping[method_name]
|
|
540
|
+
existing_names = {p["name"] for p in args} | set(kwargs.keys())
|
|
541
|
+
for kwarg in expanded_kwargs:
|
|
542
|
+
if kwarg["name"] not in existing_names:
|
|
543
|
+
kwargs[kwarg["name"]] = {
|
|
544
|
+
"type": kwarg["type"],
|
|
545
|
+
"default": kwarg["default"],
|
|
546
|
+
}
|
|
61
547
|
else:
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
"default": _serialize_default(param.default),
|
|
66
|
-
}
|
|
548
|
+
kwargs["**kwargs"] = {"type": "**kwargs"}
|
|
549
|
+
elif has_var_keyword:
|
|
550
|
+
kwargs["**kwargs"] = {"type": "**kwargs"}
|
|
67
551
|
|
|
68
552
|
result = {"args": args, "kwargs": kwargs}
|
|
69
|
-
_SIGNATURE_CACHE[
|
|
553
|
+
_SIGNATURE_CACHE[cache_key] = result
|
|
70
554
|
return result
|
|
71
555
|
|
|
72
556
|
|
|
73
|
-
def _get_type_str(annotation) -> Optional[str]:
|
|
74
|
-
"""Convert annotation to string."""
|
|
75
|
-
if annotation is inspect.Parameter.empty:
|
|
76
|
-
return None
|
|
77
|
-
if hasattr(annotation, "__name__"):
|
|
78
|
-
return annotation.__name__
|
|
79
|
-
return str(annotation)
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
def _serialize_default(default) -> Any:
|
|
83
|
-
"""Serialize default value."""
|
|
84
|
-
if default is inspect.Parameter.empty:
|
|
85
|
-
return None
|
|
86
|
-
if callable(default):
|
|
87
|
-
return f"<{type(default).__name__}>"
|
|
88
|
-
try:
|
|
89
|
-
import json
|
|
90
|
-
json.dumps(default)
|
|
91
|
-
return default
|
|
92
|
-
except (TypeError, ValueError):
|
|
93
|
-
return repr(default)
|
|
94
|
-
|
|
95
|
-
|
|
96
557
|
def get_defaults(method_name: str) -> Dict[str, Any]:
|
|
97
558
|
"""Get default values for a method's kwargs.
|
|
98
559
|
|
|
@@ -137,7 +598,7 @@ def validate_kwargs(
|
|
|
137
598
|
sig = get_signature(method_name)
|
|
138
599
|
known_kwargs = set(sig.get("kwargs", {}).keys()) - {"**kwargs"}
|
|
139
600
|
|
|
140
|
-
# If method accepts **kwargs, all are valid
|
|
601
|
+
# If method accepts **kwargs and we haven't expanded it, all are valid
|
|
141
602
|
if "**kwargs" in sig.get("kwargs", {}):
|
|
142
603
|
return {
|
|
143
604
|
"valid": list(kwargs.keys()),
|
|
@@ -164,23 +625,21 @@ def validate_kwargs(
|
|
|
164
625
|
def list_plotting_methods() -> List[str]:
|
|
165
626
|
"""List all available plotting methods.
|
|
166
627
|
|
|
628
|
+
Uses _params.PLOTTING_METHODS as single source of truth,
|
|
629
|
+
filtered to methods that actually exist on the current matplotlib version.
|
|
630
|
+
|
|
167
631
|
Returns
|
|
168
632
|
-------
|
|
169
633
|
list
|
|
170
634
|
Names of plotting methods.
|
|
171
635
|
"""
|
|
636
|
+
from .._params import PLOTTING_METHODS
|
|
637
|
+
|
|
172
638
|
fig, ax = plt.subplots()
|
|
173
639
|
plt.close(fig)
|
|
174
640
|
|
|
175
|
-
#
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
"boxplot", "violinplot", "pie", "errorbar", "fill",
|
|
179
|
-
"fill_between", "fill_betweenx", "stackplot", "stem",
|
|
180
|
-
"step", "imshow", "pcolor", "pcolormesh", "contour",
|
|
181
|
-
"contourf", "quiver", "barbs", "streamplot", "hexbin",
|
|
182
|
-
"eventplot", "stairs", "ecdf",
|
|
183
|
-
]
|
|
641
|
+
# Use _params.PLOTTING_METHODS as single source of truth
|
|
642
|
+
return sorted([m for m in PLOTTING_METHODS if hasattr(ax, m)])
|
|
643
|
+
|
|
184
644
|
|
|
185
|
-
|
|
186
|
-
return [m for m in methods if hasattr(ax, m)]
|
|
645
|
+
# EOF
|
figrecipe/_utils/__init__.py
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
# -*- coding: utf-8 -*-
|
|
3
3
|
"""Utility modules for figrecipe."""
|
|
4
4
|
|
|
5
|
-
from ._numpy_io import load_array, save_array
|
|
6
5
|
from ._diff import get_non_default_kwargs, is_default_value
|
|
7
|
-
from .
|
|
6
|
+
from ._numpy_io import load_array, save_array
|
|
7
|
+
from ._units import inch_to_mm, mm_to_inch, mm_to_pt, pt_to_mm
|
|
8
8
|
|
|
9
9
|
__all__ = [
|
|
10
10
|
"save_array",
|
|
@@ -19,14 +19,16 @@ __all__ = [
|
|
|
19
19
|
|
|
20
20
|
# Optional: image comparison (requires PIL)
|
|
21
21
|
try:
|
|
22
|
-
from ._image_diff import compare_images, create_comparison_figure
|
|
22
|
+
from ._image_diff import compare_images, create_comparison_figure # noqa: F401
|
|
23
|
+
|
|
23
24
|
__all__.extend(["compare_images", "create_comparison_figure"])
|
|
24
25
|
except ImportError:
|
|
25
26
|
pass
|
|
26
27
|
|
|
27
28
|
# Optional: crop utility (requires PIL)
|
|
28
29
|
try:
|
|
29
|
-
from ._crop import crop, find_content_area
|
|
30
|
+
from ._crop import crop, find_content_area # noqa: F401
|
|
31
|
+
|
|
30
32
|
__all__.extend(["crop", "find_content_area"])
|
|
31
33
|
except ImportError:
|
|
32
34
|
pass
|