figrecipe 0.5.0__py3-none-any.whl → 0.7.4__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.
Files changed (189) hide show
  1. figrecipe/__init__.py +220 -819
  2. figrecipe/_api/__init__.py +48 -0
  3. figrecipe/_api/_extract.py +108 -0
  4. figrecipe/_api/_notebook.py +61 -0
  5. figrecipe/_api/_panel.py +46 -0
  6. figrecipe/_api/_save.py +191 -0
  7. figrecipe/_api/_seaborn_proxy.py +34 -0
  8. figrecipe/_api/_style_manager.py +153 -0
  9. figrecipe/_api/_subplots.py +333 -0
  10. figrecipe/_api/_validate.py +82 -0
  11. figrecipe/_dev/__init__.py +29 -0
  12. figrecipe/_dev/_plotters.py +76 -0
  13. figrecipe/_dev/_run_demos.py +56 -0
  14. figrecipe/_dev/demo_plotters/__init__.py +64 -0
  15. figrecipe/_dev/demo_plotters/_categories.py +81 -0
  16. figrecipe/_dev/demo_plotters/_figure_creators.py +119 -0
  17. figrecipe/_dev/demo_plotters/_helpers.py +31 -0
  18. figrecipe/_dev/demo_plotters/_registry.py +50 -0
  19. figrecipe/_dev/demo_plotters/bar_categorical/__init__.py +4 -0
  20. figrecipe/_dev/demo_plotters/bar_categorical/plot_bar.py +25 -0
  21. figrecipe/_dev/demo_plotters/bar_categorical/plot_barh.py +25 -0
  22. figrecipe/_dev/demo_plotters/contour_surface/__init__.py +4 -0
  23. figrecipe/_dev/demo_plotters/contour_surface/plot_contour.py +30 -0
  24. figrecipe/_dev/demo_plotters/contour_surface/plot_contourf.py +29 -0
  25. figrecipe/_dev/demo_plotters/contour_surface/plot_tricontour.py +28 -0
  26. figrecipe/_dev/demo_plotters/contour_surface/plot_tricontourf.py +28 -0
  27. figrecipe/_dev/demo_plotters/contour_surface/plot_tripcolor.py +29 -0
  28. figrecipe/_dev/demo_plotters/contour_surface/plot_triplot.py +25 -0
  29. figrecipe/_dev/demo_plotters/distribution/__init__.py +4 -0
  30. figrecipe/_dev/demo_plotters/distribution/plot_boxplot.py +24 -0
  31. figrecipe/_dev/demo_plotters/distribution/plot_ecdf.py +24 -0
  32. figrecipe/_dev/demo_plotters/distribution/plot_hist.py +24 -0
  33. figrecipe/_dev/demo_plotters/distribution/plot_hist2d.py +25 -0
  34. figrecipe/_dev/demo_plotters/distribution/plot_violinplot.py +25 -0
  35. figrecipe/_dev/demo_plotters/image_matrix/__init__.py +4 -0
  36. figrecipe/_dev/demo_plotters/image_matrix/plot_hexbin.py +25 -0
  37. figrecipe/_dev/demo_plotters/image_matrix/plot_imshow.py +23 -0
  38. figrecipe/_dev/demo_plotters/image_matrix/plot_matshow.py +23 -0
  39. figrecipe/_dev/demo_plotters/image_matrix/plot_pcolor.py +29 -0
  40. figrecipe/_dev/demo_plotters/image_matrix/plot_pcolormesh.py +29 -0
  41. figrecipe/_dev/demo_plotters/image_matrix/plot_spy.py +29 -0
  42. figrecipe/_dev/demo_plotters/line_curve/__init__.py +4 -0
  43. figrecipe/_dev/demo_plotters/line_curve/plot_errorbar.py +28 -0
  44. figrecipe/_dev/demo_plotters/line_curve/plot_fill.py +29 -0
  45. figrecipe/_dev/demo_plotters/line_curve/plot_fill_between.py +30 -0
  46. figrecipe/_dev/demo_plotters/line_curve/plot_fill_betweenx.py +28 -0
  47. figrecipe/_dev/demo_plotters/line_curve/plot_plot.py +28 -0
  48. figrecipe/_dev/demo_plotters/line_curve/plot_stackplot.py +29 -0
  49. figrecipe/_dev/demo_plotters/line_curve/plot_stairs.py +27 -0
  50. figrecipe/_dev/demo_plotters/line_curve/plot_step.py +27 -0
  51. figrecipe/_dev/demo_plotters/scatter_points/__init__.py +4 -0
  52. figrecipe/_dev/demo_plotters/scatter_points/plot_scatter.py +24 -0
  53. figrecipe/_dev/demo_plotters/special/__init__.py +4 -0
  54. figrecipe/_dev/demo_plotters/special/plot_eventplot.py +25 -0
  55. figrecipe/_dev/demo_plotters/special/plot_loglog.py +27 -0
  56. figrecipe/_dev/demo_plotters/special/plot_pie.py +27 -0
  57. figrecipe/_dev/demo_plotters/special/plot_semilogx.py +27 -0
  58. figrecipe/_dev/demo_plotters/special/plot_semilogy.py +27 -0
  59. figrecipe/_dev/demo_plotters/special/plot_stem.py +27 -0
  60. figrecipe/_dev/demo_plotters/spectral_signal/__init__.py +4 -0
  61. figrecipe/_dev/demo_plotters/spectral_signal/plot_acorr.py +24 -0
  62. figrecipe/_dev/demo_plotters/spectral_signal/plot_angle_spectrum.py +28 -0
  63. figrecipe/_dev/demo_plotters/spectral_signal/plot_cohere.py +29 -0
  64. figrecipe/_dev/demo_plotters/spectral_signal/plot_csd.py +29 -0
  65. figrecipe/_dev/demo_plotters/spectral_signal/plot_magnitude_spectrum.py +28 -0
  66. figrecipe/_dev/demo_plotters/spectral_signal/plot_phase_spectrum.py +28 -0
  67. figrecipe/_dev/demo_plotters/spectral_signal/plot_psd.py +29 -0
  68. figrecipe/_dev/demo_plotters/spectral_signal/plot_specgram.py +30 -0
  69. figrecipe/_dev/demo_plotters/spectral_signal/plot_xcorr.py +25 -0
  70. figrecipe/_dev/demo_plotters/vector_flow/__init__.py +4 -0
  71. figrecipe/_dev/demo_plotters/vector_flow/plot_barbs.py +30 -0
  72. figrecipe/_dev/demo_plotters/vector_flow/plot_quiver.py +30 -0
  73. figrecipe/_dev/demo_plotters/vector_flow/plot_streamplot.py +30 -0
  74. figrecipe/_editor/__init__.py +278 -0
  75. figrecipe/_editor/_bbox/__init__.py +43 -0
  76. figrecipe/_editor/_bbox/_collections.py +177 -0
  77. figrecipe/_editor/_bbox/_elements.py +159 -0
  78. figrecipe/_editor/_bbox/_extract.py +256 -0
  79. figrecipe/_editor/_bbox/_extract_axes.py +370 -0
  80. figrecipe/_editor/_bbox/_extract_text.py +342 -0
  81. figrecipe/_editor/_bbox/_lines.py +173 -0
  82. figrecipe/_editor/_bbox/_transforms.py +146 -0
  83. figrecipe/_editor/_flask_app.py +258 -0
  84. figrecipe/_editor/_helpers.py +242 -0
  85. figrecipe/_editor/_hitmap/__init__.py +76 -0
  86. figrecipe/_editor/_hitmap/_artists/__init__.py +21 -0
  87. figrecipe/_editor/_hitmap/_artists/_collections.py +345 -0
  88. figrecipe/_editor/_hitmap/_artists/_images.py +68 -0
  89. figrecipe/_editor/_hitmap/_artists/_lines.py +107 -0
  90. figrecipe/_editor/_hitmap/_artists/_patches.py +163 -0
  91. figrecipe/_editor/_hitmap/_artists/_text.py +190 -0
  92. figrecipe/_editor/_hitmap/_colors.py +181 -0
  93. figrecipe/_editor/_hitmap/_detect.py +137 -0
  94. figrecipe/_editor/_hitmap/_restore.py +154 -0
  95. figrecipe/_editor/_hitmap_main.py +182 -0
  96. figrecipe/_editor/_overrides.py +318 -0
  97. figrecipe/_editor/_preferences.py +135 -0
  98. figrecipe/_editor/_render_overrides.py +480 -0
  99. figrecipe/_editor/_renderer.py +199 -0
  100. figrecipe/_editor/_routes_axis.py +453 -0
  101. figrecipe/_editor/_routes_core.py +284 -0
  102. figrecipe/_editor/_routes_element.py +317 -0
  103. figrecipe/_editor/_routes_style.py +223 -0
  104. figrecipe/_editor/_templates/__init__.py +152 -0
  105. figrecipe/_editor/_templates/_html.py +502 -0
  106. figrecipe/_editor/_templates/_scripts/__init__.py +120 -0
  107. figrecipe/_editor/_templates/_scripts/_api.py +228 -0
  108. figrecipe/_editor/_templates/_scripts/_colors.py +485 -0
  109. figrecipe/_editor/_templates/_scripts/_core.py +436 -0
  110. figrecipe/_editor/_templates/_scripts/_debug_snapshot.py +186 -0
  111. figrecipe/_editor/_templates/_scripts/_element_editor.py +310 -0
  112. figrecipe/_editor/_templates/_scripts/_files.py +195 -0
  113. figrecipe/_editor/_templates/_scripts/_hitmap.py +509 -0
  114. figrecipe/_editor/_templates/_scripts/_inspector.py +315 -0
  115. figrecipe/_editor/_templates/_scripts/_labels.py +464 -0
  116. figrecipe/_editor/_templates/_scripts/_legend_drag.py +265 -0
  117. figrecipe/_editor/_templates/_scripts/_modals.py +226 -0
  118. figrecipe/_editor/_templates/_scripts/_overlays.py +292 -0
  119. figrecipe/_editor/_templates/_scripts/_panel_drag.py +334 -0
  120. figrecipe/_editor/_templates/_scripts/_panel_position.py +279 -0
  121. figrecipe/_editor/_templates/_scripts/_selection.py +237 -0
  122. figrecipe/_editor/_templates/_scripts/_tabs.py +89 -0
  123. figrecipe/_editor/_templates/_scripts/_view_mode.py +107 -0
  124. figrecipe/_editor/_templates/_scripts/_zoom.py +179 -0
  125. figrecipe/_editor/_templates/_styles/__init__.py +69 -0
  126. figrecipe/_editor/_templates/_styles/_base.py +64 -0
  127. figrecipe/_editor/_templates/_styles/_buttons.py +206 -0
  128. figrecipe/_editor/_templates/_styles/_color_input.py +123 -0
  129. figrecipe/_editor/_templates/_styles/_controls.py +265 -0
  130. figrecipe/_editor/_templates/_styles/_dynamic_props.py +144 -0
  131. figrecipe/_editor/_templates/_styles/_forms.py +126 -0
  132. figrecipe/_editor/_templates/_styles/_hitmap.py +184 -0
  133. figrecipe/_editor/_templates/_styles/_inspector.py +90 -0
  134. figrecipe/_editor/_templates/_styles/_labels.py +118 -0
  135. figrecipe/_editor/_templates/_styles/_modals.py +98 -0
  136. figrecipe/_editor/_templates/_styles/_overlays.py +130 -0
  137. figrecipe/_editor/_templates/_styles/_preview.py +225 -0
  138. figrecipe/_editor/_templates/_styles/_selection.py +73 -0
  139. figrecipe/_params/_DECORATION_METHODS.py +33 -0
  140. figrecipe/_params/_PLOTTING_METHODS.py +58 -0
  141. figrecipe/_params/__init__.py +9 -0
  142. figrecipe/_recorder.py +92 -110
  143. figrecipe/_recorder_utils.py +124 -0
  144. figrecipe/_reproducer/__init__.py +18 -0
  145. figrecipe/_reproducer/_core.py +498 -0
  146. figrecipe/_reproducer/_custom_plots.py +279 -0
  147. figrecipe/_reproducer/_seaborn.py +100 -0
  148. figrecipe/_reproducer/_violin.py +186 -0
  149. figrecipe/_seaborn.py +14 -9
  150. figrecipe/_serializer.py +2 -2
  151. figrecipe/_signatures/README.md +68 -0
  152. figrecipe/_signatures/__init__.py +12 -2
  153. figrecipe/_signatures/_kwargs.py +273 -0
  154. figrecipe/_signatures/_loader.py +114 -57
  155. figrecipe/_signatures/_parsing.py +147 -0
  156. figrecipe/_utils/__init__.py +6 -4
  157. figrecipe/_utils/_crop.py +10 -4
  158. figrecipe/_utils/_image_diff.py +37 -33
  159. figrecipe/_utils/_numpy_io.py +0 -1
  160. figrecipe/_utils/_units.py +11 -3
  161. figrecipe/_validator.py +12 -3
  162. figrecipe/_wrappers/_axes.py +193 -170
  163. figrecipe/_wrappers/_axes_helpers.py +136 -0
  164. figrecipe/_wrappers/_axes_plots.py +418 -0
  165. figrecipe/_wrappers/_axes_seaborn.py +157 -0
  166. figrecipe/_wrappers/_figure.py +277 -18
  167. figrecipe/_wrappers/_panel_labels.py +127 -0
  168. figrecipe/_wrappers/_plot_helpers.py +143 -0
  169. figrecipe/_wrappers/_violin_helpers.py +180 -0
  170. figrecipe/plt.py +0 -1
  171. figrecipe/pyplot.py +2 -1
  172. figrecipe/styles/__init__.py +12 -11
  173. figrecipe/styles/_dotdict.py +72 -0
  174. figrecipe/styles/_finalize.py +134 -0
  175. figrecipe/styles/_fonts.py +77 -0
  176. figrecipe/styles/_kwargs_converter.py +178 -0
  177. figrecipe/styles/_plot_styles.py +209 -0
  178. figrecipe/styles/_style_applier.py +60 -202
  179. figrecipe/styles/_style_loader.py +73 -121
  180. figrecipe/styles/_themes.py +151 -0
  181. figrecipe/styles/presets/MATPLOTLIB.yaml +95 -0
  182. figrecipe/styles/presets/SCITEX.yaml +181 -0
  183. figrecipe-0.7.4.dist-info/METADATA +429 -0
  184. figrecipe-0.7.4.dist-info/RECORD +188 -0
  185. figrecipe/_reproducer.py +0 -358
  186. figrecipe-0.5.0.dist-info/METADATA +0 -336
  187. figrecipe-0.5.0.dist-info/RECORD +0 -26
  188. {figrecipe-0.5.0.dist-info → figrecipe-0.7.4.dist-info}/WHEEL +0 -0
  189. {figrecipe-0.5.0.dist-info → figrecipe-0.7.4.dist-info}/licenses/LICENSE +0 -0
@@ -2,6 +2,16 @@
2
2
  # -*- coding: utf-8 -*-
3
3
  """Matplotlib function signatures for validation and defaults."""
4
4
 
5
- from ._loader import get_signature, get_defaults, validate_kwargs
5
+ from ._loader import (
6
+ get_defaults,
7
+ get_signature,
8
+ list_plotting_methods,
9
+ validate_kwargs,
10
+ )
6
11
 
7
- __all__ = ["get_signature", "get_defaults", "validate_kwargs"]
12
+ __all__ = [
13
+ "get_signature",
14
+ "get_defaults",
15
+ "validate_kwargs",
16
+ "list_plotting_methods",
17
+ ]
@@ -0,0 +1,273 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """Kwargs expansion utilities for matplotlib signature extraction."""
4
+
5
+ import re
6
+ from typing import Any, Dict, List, Optional
7
+
8
+
9
+ def get_setter_type(obj: Any, prop_name: str) -> Optional[str]:
10
+ """Get type from set_* method docstring."""
11
+ setter_name = f"set_{prop_name}"
12
+ if not hasattr(obj, setter_name):
13
+ return None
14
+
15
+ method = getattr(obj, setter_name)
16
+ if not method.__doc__:
17
+ return None
18
+
19
+ match = re.search(
20
+ r"Parameters\s*[-]+\s*\n\s*(\w+)\s*:\s*(.+?)(?:\n\s*\n|\Z)",
21
+ method.__doc__,
22
+ re.DOTALL,
23
+ )
24
+ if match:
25
+ type_str = match.group(2).split("\n")[0].strip()
26
+ return type_str
27
+ return None
28
+
29
+
30
+ def build_kwargs_with_types() -> (
31
+ tuple[
32
+ List[Dict[str, Any]],
33
+ List[Dict[str, Any]],
34
+ List[Dict[str, Any]],
35
+ List[Dict[str, Any]],
36
+ ]
37
+ ):
38
+ """Build kwargs lists with types from matplotlib classes."""
39
+ from matplotlib.artist import Artist
40
+ from matplotlib.lines import Line2D
41
+ from matplotlib.patches import Patch
42
+ from matplotlib.text import Text
43
+
44
+ line = Line2D([0], [0])
45
+ patch = Patch()
46
+ text = Text()
47
+ artist = Artist()
48
+
49
+ def get_type(obj, name):
50
+ return get_setter_type(obj, name)
51
+
52
+ ARTIST_KWARGS = [
53
+ {"name": "agg_filter", "type": get_type(artist, "agg_filter"), "default": None},
54
+ {"name": "alpha", "type": get_type(artist, "alpha"), "default": None},
55
+ {"name": "animated", "type": get_type(artist, "animated"), "default": False},
56
+ {"name": "clip_box", "type": get_type(artist, "clip_box"), "default": None},
57
+ {"name": "clip_on", "type": get_type(artist, "clip_on"), "default": True},
58
+ {"name": "clip_path", "type": get_type(artist, "clip_path"), "default": None},
59
+ {"name": "gid", "type": get_type(artist, "gid"), "default": None},
60
+ {"name": "label", "type": get_type(artist, "label"), "default": ""},
61
+ {
62
+ "name": "path_effects",
63
+ "type": get_type(artist, "path_effects"),
64
+ "default": None,
65
+ },
66
+ {"name": "picker", "type": get_type(artist, "picker"), "default": None},
67
+ {"name": "rasterized", "type": get_type(artist, "rasterized"), "default": None},
68
+ {
69
+ "name": "sketch_params",
70
+ "type": get_type(artist, "sketch_params"),
71
+ "default": None,
72
+ },
73
+ {"name": "snap", "type": get_type(artist, "snap"), "default": None},
74
+ {"name": "transform", "type": get_type(artist, "transform"), "default": None},
75
+ {"name": "url", "type": get_type(artist, "url"), "default": None},
76
+ {"name": "visible", "type": get_type(artist, "visible"), "default": True},
77
+ {"name": "zorder", "type": get_type(artist, "zorder"), "default": None},
78
+ ]
79
+
80
+ LINE2D_KWARGS = [
81
+ {"name": "color", "type": get_type(line, "color"), "default": None},
82
+ {"name": "linestyle", "type": get_type(line, "linestyle"), "default": "-"},
83
+ {"name": "linewidth", "type": get_type(line, "linewidth"), "default": None},
84
+ {"name": "marker", "type": get_type(line, "marker"), "default": ""},
85
+ {
86
+ "name": "markeredgecolor",
87
+ "type": get_type(line, "markeredgecolor"),
88
+ "default": None,
89
+ },
90
+ {
91
+ "name": "markeredgewidth",
92
+ "type": get_type(line, "markeredgewidth"),
93
+ "default": None,
94
+ },
95
+ {
96
+ "name": "markerfacecolor",
97
+ "type": get_type(line, "markerfacecolor"),
98
+ "default": None,
99
+ },
100
+ {"name": "markersize", "type": get_type(line, "markersize"), "default": None},
101
+ {"name": "antialiased", "type": get_type(line, "antialiased"), "default": True},
102
+ {
103
+ "name": "dash_capstyle",
104
+ "type": get_type(line, "dash_capstyle"),
105
+ "default": "butt",
106
+ },
107
+ {
108
+ "name": "dash_joinstyle",
109
+ "type": get_type(line, "dash_joinstyle"),
110
+ "default": "round",
111
+ },
112
+ {
113
+ "name": "solid_capstyle",
114
+ "type": get_type(line, "solid_capstyle"),
115
+ "default": "projecting",
116
+ },
117
+ {
118
+ "name": "solid_joinstyle",
119
+ "type": get_type(line, "solid_joinstyle"),
120
+ "default": "round",
121
+ },
122
+ {
123
+ "name": "drawstyle",
124
+ "type": get_type(line, "drawstyle"),
125
+ "default": "default",
126
+ },
127
+ {"name": "fillstyle", "type": get_type(line, "fillstyle"), "default": "full"},
128
+ ]
129
+
130
+ PATCH_KWARGS = [
131
+ {"name": "color", "type": get_type(patch, "color"), "default": None},
132
+ {"name": "edgecolor", "type": get_type(patch, "edgecolor"), "default": None},
133
+ {"name": "facecolor", "type": get_type(patch, "facecolor"), "default": None},
134
+ {"name": "fill", "type": get_type(patch, "fill"), "default": True},
135
+ {"name": "hatch", "type": get_type(patch, "hatch"), "default": None},
136
+ {"name": "linestyle", "type": get_type(patch, "linestyle"), "default": "-"},
137
+ {"name": "linewidth", "type": get_type(patch, "linewidth"), "default": None},
138
+ {
139
+ "name": "antialiased",
140
+ "type": get_type(patch, "antialiased"),
141
+ "default": None,
142
+ },
143
+ {"name": "capstyle", "type": get_type(patch, "capstyle"), "default": "butt"},
144
+ {"name": "joinstyle", "type": get_type(patch, "joinstyle"), "default": "miter"},
145
+ ]
146
+
147
+ TEXT_KWARGS = [
148
+ {"name": "color", "type": get_type(text, "color"), "default": "black"},
149
+ {"name": "fontfamily", "type": get_type(text, "fontfamily"), "default": None},
150
+ {"name": "fontsize", "type": get_type(text, "fontsize"), "default": None},
151
+ {"name": "fontstretch", "type": get_type(text, "fontstretch"), "default": None},
152
+ {"name": "fontstyle", "type": get_type(text, "fontstyle"), "default": "normal"},
153
+ {
154
+ "name": "fontvariant",
155
+ "type": get_type(text, "fontvariant"),
156
+ "default": "normal",
157
+ },
158
+ {
159
+ "name": "fontweight",
160
+ "type": get_type(text, "fontweight"),
161
+ "default": "normal",
162
+ },
163
+ {
164
+ "name": "horizontalalignment",
165
+ "type": get_type(text, "horizontalalignment"),
166
+ "default": "center",
167
+ },
168
+ {
169
+ "name": "verticalalignment",
170
+ "type": get_type(text, "verticalalignment"),
171
+ "default": "center",
172
+ },
173
+ {"name": "rotation", "type": get_type(text, "rotation"), "default": None},
174
+ {"name": "linespacing", "type": get_type(text, "linespacing"), "default": None},
175
+ {
176
+ "name": "multialignment",
177
+ "type": get_type(text, "multialignment"),
178
+ "default": None,
179
+ },
180
+ {"name": "wrap", "type": get_type(text, "wrap"), "default": False},
181
+ ]
182
+
183
+ return ARTIST_KWARGS, LINE2D_KWARGS, PATCH_KWARGS, TEXT_KWARGS
184
+
185
+
186
+ # Lazy cache for kwargs mapping
187
+ _KWARGS_CACHE: Optional[Dict[str, List[Dict[str, Any]]]] = None
188
+
189
+
190
+ def get_kwargs_mapping() -> Dict[str, List[Dict[str, Any]]]:
191
+ """Get kwargs mapping, building it lazily on first call."""
192
+ global _KWARGS_CACHE
193
+ if _KWARGS_CACHE is not None:
194
+ return _KWARGS_CACHE
195
+
196
+ ARTIST_KWARGS, LINE2D_KWARGS, PATCH_KWARGS, TEXT_KWARGS = build_kwargs_with_types()
197
+
198
+ _KWARGS_CACHE = {
199
+ "plot": LINE2D_KWARGS + ARTIST_KWARGS,
200
+ "scatter": ARTIST_KWARGS,
201
+ "bar": PATCH_KWARGS + ARTIST_KWARGS,
202
+ "barh": PATCH_KWARGS + ARTIST_KWARGS,
203
+ "fill": PATCH_KWARGS + ARTIST_KWARGS,
204
+ "fill_between": PATCH_KWARGS + ARTIST_KWARGS,
205
+ "fill_betweenx": PATCH_KWARGS + ARTIST_KWARGS,
206
+ "step": LINE2D_KWARGS + ARTIST_KWARGS,
207
+ "errorbar": LINE2D_KWARGS + ARTIST_KWARGS,
208
+ "hist": PATCH_KWARGS + ARTIST_KWARGS,
209
+ "hist2d": ARTIST_KWARGS,
210
+ "imshow": ARTIST_KWARGS,
211
+ "pcolor": ARTIST_KWARGS,
212
+ "pcolormesh": ARTIST_KWARGS,
213
+ "pcolorfast": ARTIST_KWARGS,
214
+ "contour": ARTIST_KWARGS,
215
+ "contourf": ARTIST_KWARGS,
216
+ "hexbin": ARTIST_KWARGS,
217
+ "quiver": ARTIST_KWARGS,
218
+ "barbs": ARTIST_KWARGS,
219
+ "specgram": ARTIST_KWARGS,
220
+ "psd": LINE2D_KWARGS + ARTIST_KWARGS,
221
+ "csd": LINE2D_KWARGS + ARTIST_KWARGS,
222
+ "cohere": LINE2D_KWARGS + ARTIST_KWARGS,
223
+ "acorr": LINE2D_KWARGS + ARTIST_KWARGS,
224
+ "xcorr": LINE2D_KWARGS + ARTIST_KWARGS,
225
+ "angle_spectrum": LINE2D_KWARGS + ARTIST_KWARGS,
226
+ "magnitude_spectrum": LINE2D_KWARGS + ARTIST_KWARGS,
227
+ "phase_spectrum": LINE2D_KWARGS + ARTIST_KWARGS,
228
+ "stackplot": PATCH_KWARGS + ARTIST_KWARGS,
229
+ "stairs": PATCH_KWARGS + ARTIST_KWARGS,
230
+ "eventplot": ARTIST_KWARGS,
231
+ "broken_barh": PATCH_KWARGS + ARTIST_KWARGS,
232
+ "loglog": LINE2D_KWARGS + ARTIST_KWARGS,
233
+ "semilogx": LINE2D_KWARGS + ARTIST_KWARGS,
234
+ "semilogy": LINE2D_KWARGS + ARTIST_KWARGS,
235
+ "annotate": TEXT_KWARGS + ARTIST_KWARGS,
236
+ "text": TEXT_KWARGS + ARTIST_KWARGS,
237
+ "arrow": PATCH_KWARGS + ARTIST_KWARGS,
238
+ "axhline": LINE2D_KWARGS + ARTIST_KWARGS,
239
+ "axvline": LINE2D_KWARGS + ARTIST_KWARGS,
240
+ "hlines": ARTIST_KWARGS,
241
+ "vlines": ARTIST_KWARGS,
242
+ "axhspan": PATCH_KWARGS + ARTIST_KWARGS,
243
+ "axvspan": PATCH_KWARGS + ARTIST_KWARGS,
244
+ "axline": LINE2D_KWARGS + ARTIST_KWARGS,
245
+ "legend": ARTIST_KWARGS,
246
+ "grid": LINE2D_KWARGS + ARTIST_KWARGS,
247
+ "table": ARTIST_KWARGS,
248
+ "clabel": TEXT_KWARGS + ARTIST_KWARGS,
249
+ "bar_label": TEXT_KWARGS + ARTIST_KWARGS,
250
+ "quiverkey": ARTIST_KWARGS,
251
+ "ecdf": LINE2D_KWARGS + ARTIST_KWARGS,
252
+ "tricontour": ARTIST_KWARGS,
253
+ "tricontourf": ARTIST_KWARGS,
254
+ "tripcolor": ARTIST_KWARGS,
255
+ "triplot": LINE2D_KWARGS + ARTIST_KWARGS,
256
+ "matshow": ARTIST_KWARGS,
257
+ "spy": ARTIST_KWARGS + LINE2D_KWARGS,
258
+ "boxplot": ARTIST_KWARGS,
259
+ "violinplot": ARTIST_KWARGS,
260
+ "pie": PATCH_KWARGS + ARTIST_KWARGS,
261
+ "stem": LINE2D_KWARGS + ARTIST_KWARGS,
262
+ }
263
+
264
+ return _KWARGS_CACHE
265
+
266
+
267
+ __all__ = [
268
+ "get_setter_type",
269
+ "build_kwargs_with_types",
270
+ "get_kwargs_mapping",
271
+ ]
272
+
273
+ # EOF
@@ -1,32 +1,68 @@
1
1
  #!/usr/bin/env python3
2
2
  # -*- coding: utf-8 -*-
3
- """Load and query matplotlib function signatures."""
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
- from typing import Any, Dict, List, Optional, Set
12
+ from typing import Any, Dict, List, Optional
7
13
 
8
14
  import matplotlib.pyplot as plt
9
15
 
16
+ from ._kwargs import get_kwargs_mapping
17
+ from ._parsing import extract_args_from_docstring, parse_parameter_types
10
18
 
11
19
  # Cache for signatures
12
20
  _SIGNATURE_CACHE: Dict[str, Dict[str, Any]] = {}
13
21
 
14
22
 
15
- def get_signature(method_name: str) -> Dict[str, Any]:
16
- """Get signature for a matplotlib Axes method.
23
+ def _get_type_str(annotation) -> Optional[str]:
24
+ """Convert annotation to string."""
25
+ if annotation is inspect.Parameter.empty:
26
+ return None
27
+ if hasattr(annotation, "__name__"):
28
+ return annotation.__name__
29
+ return str(annotation)
30
+
31
+
32
+ def _serialize_default(default) -> Any:
33
+ """Serialize default value."""
34
+ if default is inspect.Parameter.empty:
35
+ return None
36
+ if callable(default):
37
+ return f"<{type(default).__name__}>"
38
+ try:
39
+ import json
40
+
41
+ json.dumps(default)
42
+ return default
43
+ except (TypeError, ValueError):
44
+ return repr(default)
45
+
46
+
47
+ def get_signature(method_name: str, expand_kwargs: bool = True) -> Dict[str, Any]:
48
+ """Get signature for a matplotlib Axes method with deep inspection.
17
49
 
18
50
  Parameters
19
51
  ----------
20
52
  method_name : str
21
53
  Name of the method (e.g., 'plot', 'scatter').
54
+ expand_kwargs : bool
55
+ If True, expand **kwargs to actual parameters.
22
56
 
23
57
  Returns
24
58
  -------
25
59
  dict
26
60
  Signature information with 'args' and 'kwargs' keys.
61
+ Args and kwargs are expanded from docstrings when possible.
27
62
  """
28
- if method_name in _SIGNATURE_CACHE:
29
- return _SIGNATURE_CACHE[method_name]
63
+ cache_key = f"{method_name}_{expand_kwargs}"
64
+ if cache_key in _SIGNATURE_CACHE:
65
+ return _SIGNATURE_CACHE[cache_key]
30
66
 
31
67
  # Create a temporary axes to introspect
32
68
  fig, ax = plt.subplots()
@@ -41,58 +77,83 @@ def get_signature(method_name: str) -> Dict[str, Any]:
41
77
  except (ValueError, TypeError):
42
78
  return {"args": [], "kwargs": {}}
43
79
 
80
+ # Parse parameter types from docstring
81
+ param_types = parse_parameter_types(method.__doc__)
82
+
44
83
  args = []
45
84
  kwargs = {}
85
+ has_var_positional = False
86
+ has_var_keyword = False
46
87
 
47
88
  for name, param in sig.parameters.items():
48
89
  if name == "self":
49
90
  continue
50
91
 
51
92
  if param.kind == inspect.Parameter.VAR_POSITIONAL:
52
- args.append({"name": f"*{name}", "type": "*args"})
93
+ has_var_positional = True
53
94
  elif param.kind == inspect.Parameter.VAR_KEYWORD:
54
- kwargs["**kwargs"] = {"type": "**kwargs"}
55
- elif param.default is inspect.Parameter.empty:
56
- # Positional argument
57
- args.append({
58
- "name": name,
59
- "type": _get_type_str(param.annotation),
60
- })
95
+ has_var_keyword = True
96
+ else:
97
+ # Try annotation first, then docstring
98
+ typehint = _get_type_str(param.annotation)
99
+ if not typehint:
100
+ typehint = param_types.get(name.lower())
101
+
102
+ # Handle POSITIONAL_OR_KEYWORD and POSITIONAL_ONLY parameters
103
+ if param.kind in (
104
+ inspect.Parameter.POSITIONAL_ONLY,
105
+ inspect.Parameter.POSITIONAL_OR_KEYWORD,
106
+ ):
107
+ args.append(
108
+ {
109
+ "name": name,
110
+ "type": typehint,
111
+ }
112
+ )
113
+ if param.default is not inspect.Parameter.empty:
114
+ kwargs[name] = {
115
+ "type": typehint,
116
+ "default": _serialize_default(param.default),
117
+ }
118
+ elif param.kind == inspect.Parameter.KEYWORD_ONLY:
119
+ kwargs[name] = {
120
+ "type": typehint,
121
+ "default": _serialize_default(param.default)
122
+ if param.default is not inspect.Parameter.empty
123
+ else None,
124
+ }
125
+
126
+ # Expand *args from docstring
127
+ if has_var_positional:
128
+ docstring_args = extract_args_from_docstring(method.__doc__, method_name)
129
+ if docstring_args:
130
+ for i, arg in enumerate(docstring_args):
131
+ args.insert(i, arg)
61
132
  else:
62
- # Keyword argument with default
63
- kwargs[name] = {
64
- "type": _get_type_str(param.annotation),
65
- "default": _serialize_default(param.default),
66
- }
133
+ args.insert(0, {"name": "*args", "type": "*args"})
134
+
135
+ # Expand **kwargs based on function type
136
+ if has_var_keyword and expand_kwargs:
137
+ kwargs_mapping = get_kwargs_mapping()
138
+ if method_name in kwargs_mapping:
139
+ expanded_kwargs = kwargs_mapping[method_name]
140
+ existing_names = {p["name"] for p in args} | set(kwargs.keys())
141
+ for kwarg in expanded_kwargs:
142
+ if kwarg["name"] not in existing_names:
143
+ kwargs[kwarg["name"]] = {
144
+ "type": kwarg["type"],
145
+ "default": kwarg["default"],
146
+ }
147
+ else:
148
+ kwargs["**kwargs"] = {"type": "**kwargs"}
149
+ elif has_var_keyword:
150
+ kwargs["**kwargs"] = {"type": "**kwargs"}
67
151
 
68
152
  result = {"args": args, "kwargs": kwargs}
69
- _SIGNATURE_CACHE[method_name] = result
153
+ _SIGNATURE_CACHE[cache_key] = result
70
154
  return result
71
155
 
72
156
 
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
157
  def get_defaults(method_name: str) -> Dict[str, Any]:
97
158
  """Get default values for a method's kwargs.
98
159
 
@@ -137,7 +198,6 @@ def validate_kwargs(
137
198
  sig = get_signature(method_name)
138
199
  known_kwargs = set(sig.get("kwargs", {}).keys()) - {"**kwargs"}
139
200
 
140
- # If method accepts **kwargs, all are valid
141
201
  if "**kwargs" in sig.get("kwargs", {}):
142
202
  return {
143
203
  "valid": list(kwargs.keys()),
@@ -157,30 +217,27 @@ def validate_kwargs(
157
217
  return {
158
218
  "valid": valid,
159
219
  "unknown": unknown,
160
- "missing": [], # Not checking required kwargs for now
220
+ "missing": [],
161
221
  }
162
222
 
163
223
 
164
224
  def list_plotting_methods() -> List[str]:
165
225
  """List all available plotting methods.
166
226
 
227
+ Uses _params.PLOTTING_METHODS as single source of truth,
228
+ filtered to methods that actually exist on the current matplotlib version.
229
+
167
230
  Returns
168
231
  -------
169
232
  list
170
233
  Names of plotting methods.
171
234
  """
235
+ from .._params import PLOTTING_METHODS
236
+
172
237
  fig, ax = plt.subplots()
173
238
  plt.close(fig)
174
239
 
175
- # Common plotting methods
176
- methods = [
177
- "plot", "scatter", "bar", "barh", "hist", "hist2d",
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
- ]
184
-
185
- # Filter to only those that exist
186
- return [m for m in methods if hasattr(ax, m)]
240
+ return sorted([m for m in PLOTTING_METHODS if hasattr(ax, m)])
241
+
242
+
243
+ # EOF
@@ -0,0 +1,147 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """Docstring parsing utilities for matplotlib signature extraction."""
4
+
5
+ import re
6
+ from typing import Any, Dict, List, Optional
7
+
8
+
9
+ def parse_parameter_types(docstring: Optional[str]) -> Dict[str, str]:
10
+ """Extract parameter types from NumPy-style docstring Parameters section."""
11
+ if not docstring:
12
+ return {}
13
+
14
+ types = {}
15
+
16
+ # Find Parameters section
17
+ params_match = re.search(
18
+ r"Parameters\s*[-]+\s*(.*?)(?:\n\s*Returns|\n\s*See Also|\n\s*Notes|\n\s*Examples|\n\s*Other Parameters|\Z)",
19
+ docstring,
20
+ re.DOTALL,
21
+ )
22
+ if not params_match:
23
+ return {}
24
+
25
+ params_text = params_match.group(1)
26
+
27
+ # Parse lines like "x, y : array-like or float" or "fmt : str, optional"
28
+ for match in re.finditer(
29
+ r"^(\w+(?:\s*,\s*\w+)*)\s*:\s*(.+?)(?=\n\s*\n|\n\w+\s*:|\Z)",
30
+ params_text,
31
+ re.MULTILINE | re.DOTALL,
32
+ ):
33
+ names_str = match.group(1)
34
+ type_str = match.group(2).split("\n")[0].strip()
35
+
36
+ # Clean up type string
37
+ type_str = re.sub(r",?\s*optional\s*$", "", type_str).strip()
38
+ type_str = re.sub(
39
+ r",?\s*default[^,]*$", "", type_str, flags=re.IGNORECASE
40
+ ).strip()
41
+
42
+ # Handle multiple names like "x, y"
43
+ for name in re.split(r"\s*,\s*", names_str):
44
+ name = name.strip()
45
+ if name:
46
+ types[name.lower()] = type_str
47
+
48
+ return types
49
+
50
+
51
+ def parse_args_pattern(
52
+ args_str: str, param_types: Dict[str, str]
53
+ ) -> List[Dict[str, Any]]:
54
+ """Parse args pattern like '[x], y, [fmt]' into list of arg dicts."""
55
+ if not args_str:
56
+ return []
57
+
58
+ args = []
59
+ parts = re.split(r",\s*", args_str)
60
+
61
+ for part in parts:
62
+ part = part.strip()
63
+ if not part or part == "/":
64
+ continue
65
+
66
+ optional = part.startswith("[") and part.endswith("]")
67
+ if optional:
68
+ name = part[1:-1].strip()
69
+ else:
70
+ name = part.strip("[]").strip()
71
+
72
+ if name and name not in ("...", "*"):
73
+ type_str = param_types.get(name.lower())
74
+ args.append(
75
+ {
76
+ "name": name,
77
+ "type": type_str,
78
+ "optional": optional,
79
+ }
80
+ )
81
+
82
+ return args
83
+
84
+
85
+ # Manual *args patterns for functions without parseable call signatures
86
+ MANUAL_ARGS_PATTERNS = {
87
+ "fill": "[x], y, [color]",
88
+ "stackplot": "x, *ys",
89
+ "legend": "[handles], [labels]",
90
+ "stem": "[locs], heads",
91
+ "tricontour": "[triangulation], x, y, z, [levels]",
92
+ "tricontourf": "[triangulation], x, y, z, [levels]",
93
+ "triplot": "[triangulation], x, y, [triangles]",
94
+ "loglog": "[x], y, [fmt]",
95
+ "semilogx": "[x], y, [fmt]",
96
+ "semilogy": "[x], y, [fmt]",
97
+ "barbs": "[X], [Y], U, V, [C]",
98
+ "quiver": "[X], [Y], U, V, [C]",
99
+ "pcolor": "[X], [Y], C",
100
+ "pcolormesh": "[X], [Y], C",
101
+ "pcolorfast": "[X], [Y], C",
102
+ "acorr": "x",
103
+ "xcorr": "x, y",
104
+ "plot": "[x], y, [fmt]",
105
+ }
106
+
107
+
108
+ def extract_args_from_docstring(
109
+ docstring: Optional[str], func_name: str = ""
110
+ ) -> List[Dict[str, Any]]:
111
+ """Extract *args as flattened list from docstring call signature."""
112
+ if not docstring:
113
+ return []
114
+
115
+ # First, parse parameter types
116
+ param_types = parse_parameter_types(docstring)
117
+
118
+ # Check for manual pattern first
119
+ if func_name in MANUAL_ARGS_PATTERNS:
120
+ return parse_args_pattern(MANUAL_ARGS_PATTERNS[func_name], param_types)
121
+
122
+ # Look for "Call signature:" patterns
123
+ patterns = [
124
+ r"Call signatures?::\s*\n\s*(.*?)(?:\n\n|\n[A-Z])",
125
+ r"^\s*(\w+\([^)]+\))\s*$",
126
+ ]
127
+
128
+ for pattern in patterns:
129
+ match = re.search(pattern, docstring, re.MULTILINE | re.DOTALL)
130
+ if match:
131
+ sig_text = match.group(1).strip()
132
+ first_line = sig_text.split("\n")[0].strip()
133
+ inner_match = re.search(r"\(([^*]+?)(?:,\s*\*|,\s*data|\))", first_line)
134
+ if inner_match:
135
+ args_str = inner_match.group(1).strip().rstrip(",")
136
+ return parse_args_pattern(args_str, param_types)
137
+ return []
138
+
139
+
140
+ __all__ = [
141
+ "parse_parameter_types",
142
+ "parse_args_pattern",
143
+ "extract_args_from_docstring",
144
+ "MANUAL_ARGS_PATTERNS",
145
+ ]
146
+
147
+ # EOF