figrecipe 0.6.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 (177) hide show
  1. figrecipe/__init__.py +106 -973
  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 +2 -93
  12. figrecipe/_dev/_plotters.py +76 -0
  13. figrecipe/_dev/_run_demos.py +56 -0
  14. figrecipe/_dev/demo_plotters/__init__.py +35 -166
  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/contour_surface/__init__.py +4 -0
  21. figrecipe/_dev/demo_plotters/distribution/__init__.py +4 -0
  22. figrecipe/_dev/demo_plotters/image_matrix/__init__.py +4 -0
  23. figrecipe/_dev/demo_plotters/line_curve/__init__.py +4 -0
  24. figrecipe/_dev/demo_plotters/{plot_plot.py → line_curve/plot_plot.py} +3 -2
  25. figrecipe/_dev/demo_plotters/scatter_points/__init__.py +4 -0
  26. figrecipe/_dev/demo_plotters/special/__init__.py +4 -0
  27. figrecipe/_dev/demo_plotters/{plot_pie.py → special/plot_pie.py} +5 -1
  28. figrecipe/_dev/demo_plotters/spectral_signal/__init__.py +4 -0
  29. figrecipe/_dev/demo_plotters/vector_flow/__init__.py +4 -0
  30. figrecipe/_editor/__init__.py +57 -9
  31. figrecipe/_editor/_bbox/__init__.py +43 -0
  32. figrecipe/_editor/_bbox/_collections.py +177 -0
  33. figrecipe/_editor/_bbox/_elements.py +159 -0
  34. figrecipe/_editor/_bbox/_extract.py +256 -0
  35. figrecipe/_editor/_bbox/_extract_axes.py +370 -0
  36. figrecipe/_editor/_bbox/_extract_text.py +342 -0
  37. figrecipe/_editor/_bbox/_lines.py +173 -0
  38. figrecipe/_editor/_bbox/_transforms.py +146 -0
  39. figrecipe/_editor/_flask_app.py +68 -1039
  40. figrecipe/_editor/_helpers.py +242 -0
  41. figrecipe/_editor/_hitmap/__init__.py +76 -0
  42. figrecipe/_editor/_hitmap/_artists/__init__.py +21 -0
  43. figrecipe/_editor/_hitmap/_artists/_collections.py +345 -0
  44. figrecipe/_editor/_hitmap/_artists/_images.py +68 -0
  45. figrecipe/_editor/_hitmap/_artists/_lines.py +107 -0
  46. figrecipe/_editor/_hitmap/_artists/_patches.py +163 -0
  47. figrecipe/_editor/_hitmap/_artists/_text.py +190 -0
  48. figrecipe/_editor/_hitmap/_colors.py +181 -0
  49. figrecipe/_editor/_hitmap/_detect.py +137 -0
  50. figrecipe/_editor/_hitmap/_restore.py +154 -0
  51. figrecipe/_editor/_hitmap_main.py +182 -0
  52. figrecipe/_editor/_preferences.py +135 -0
  53. figrecipe/_editor/_render_overrides.py +480 -0
  54. figrecipe/_editor/_renderer.py +35 -185
  55. figrecipe/_editor/_routes_axis.py +453 -0
  56. figrecipe/_editor/_routes_core.py +284 -0
  57. figrecipe/_editor/_routes_element.py +317 -0
  58. figrecipe/_editor/_routes_style.py +223 -0
  59. figrecipe/_editor/_templates/__init__.py +78 -1
  60. figrecipe/_editor/_templates/_html.py +109 -13
  61. figrecipe/_editor/_templates/_scripts/__init__.py +120 -0
  62. figrecipe/_editor/_templates/_scripts/_api.py +228 -0
  63. figrecipe/_editor/_templates/_scripts/_colors.py +485 -0
  64. figrecipe/_editor/_templates/_scripts/_core.py +436 -0
  65. figrecipe/_editor/_templates/_scripts/_debug_snapshot.py +186 -0
  66. figrecipe/_editor/_templates/_scripts/_element_editor.py +310 -0
  67. figrecipe/_editor/_templates/_scripts/_files.py +195 -0
  68. figrecipe/_editor/_templates/_scripts/_hitmap.py +509 -0
  69. figrecipe/_editor/_templates/_scripts/_inspector.py +315 -0
  70. figrecipe/_editor/_templates/_scripts/_labels.py +464 -0
  71. figrecipe/_editor/_templates/_scripts/_legend_drag.py +265 -0
  72. figrecipe/_editor/_templates/_scripts/_modals.py +226 -0
  73. figrecipe/_editor/_templates/_scripts/_overlays.py +292 -0
  74. figrecipe/_editor/_templates/_scripts/_panel_drag.py +334 -0
  75. figrecipe/_editor/_templates/_scripts/_panel_position.py +279 -0
  76. figrecipe/_editor/_templates/_scripts/_selection.py +237 -0
  77. figrecipe/_editor/_templates/_scripts/_tabs.py +89 -0
  78. figrecipe/_editor/_templates/_scripts/_view_mode.py +107 -0
  79. figrecipe/_editor/_templates/_scripts/_zoom.py +179 -0
  80. figrecipe/_editor/_templates/_styles/__init__.py +69 -0
  81. figrecipe/_editor/_templates/_styles/_base.py +64 -0
  82. figrecipe/_editor/_templates/_styles/_buttons.py +206 -0
  83. figrecipe/_editor/_templates/_styles/_color_input.py +123 -0
  84. figrecipe/_editor/_templates/_styles/_controls.py +265 -0
  85. figrecipe/_editor/_templates/_styles/_dynamic_props.py +144 -0
  86. figrecipe/_editor/_templates/_styles/_forms.py +126 -0
  87. figrecipe/_editor/_templates/_styles/_hitmap.py +184 -0
  88. figrecipe/_editor/_templates/_styles/_inspector.py +90 -0
  89. figrecipe/_editor/_templates/_styles/_labels.py +118 -0
  90. figrecipe/_editor/_templates/_styles/_modals.py +98 -0
  91. figrecipe/_editor/_templates/_styles/_overlays.py +130 -0
  92. figrecipe/_editor/_templates/_styles/_preview.py +225 -0
  93. figrecipe/_editor/_templates/_styles/_selection.py +73 -0
  94. figrecipe/_params/_DECORATION_METHODS.py +6 -0
  95. figrecipe/_recorder.py +35 -106
  96. figrecipe/_recorder_utils.py +124 -0
  97. figrecipe/_reproducer/__init__.py +18 -0
  98. figrecipe/_reproducer/_core.py +498 -0
  99. figrecipe/_reproducer/_custom_plots.py +279 -0
  100. figrecipe/_reproducer/_seaborn.py +100 -0
  101. figrecipe/_reproducer/_violin.py +186 -0
  102. figrecipe/_signatures/_kwargs.py +273 -0
  103. figrecipe/_signatures/_loader.py +21 -423
  104. figrecipe/_signatures/_parsing.py +147 -0
  105. figrecipe/_wrappers/_axes.py +119 -910
  106. figrecipe/_wrappers/_axes_helpers.py +136 -0
  107. figrecipe/_wrappers/_axes_plots.py +418 -0
  108. figrecipe/_wrappers/_axes_seaborn.py +157 -0
  109. figrecipe/_wrappers/_figure.py +162 -0
  110. figrecipe/_wrappers/_panel_labels.py +127 -0
  111. figrecipe/_wrappers/_plot_helpers.py +143 -0
  112. figrecipe/_wrappers/_violin_helpers.py +180 -0
  113. figrecipe/styles/__init__.py +8 -6
  114. figrecipe/styles/_dotdict.py +72 -0
  115. figrecipe/styles/_finalize.py +134 -0
  116. figrecipe/styles/_fonts.py +77 -0
  117. figrecipe/styles/_kwargs_converter.py +178 -0
  118. figrecipe/styles/_plot_styles.py +209 -0
  119. figrecipe/styles/_style_applier.py +32 -478
  120. figrecipe/styles/_style_loader.py +16 -192
  121. figrecipe/styles/_themes.py +151 -0
  122. figrecipe/styles/presets/MATPLOTLIB.yaml +2 -1
  123. figrecipe/styles/presets/SCITEX.yaml +29 -24
  124. {figrecipe-0.6.0.dist-info → figrecipe-0.7.4.dist-info}/METADATA +37 -2
  125. figrecipe-0.7.4.dist-info/RECORD +188 -0
  126. figrecipe/_editor/_bbox.py +0 -978
  127. figrecipe/_editor/_hitmap.py +0 -937
  128. figrecipe/_editor/_templates/_scripts.py +0 -2778
  129. figrecipe/_editor/_templates/_styles.py +0 -1326
  130. figrecipe/_reproducer.py +0 -975
  131. figrecipe-0.6.0.dist-info/RECORD +0 -90
  132. /figrecipe/_dev/demo_plotters/{plot_bar.py → bar_categorical/plot_bar.py} +0 -0
  133. /figrecipe/_dev/demo_plotters/{plot_barh.py → bar_categorical/plot_barh.py} +0 -0
  134. /figrecipe/_dev/demo_plotters/{plot_contour.py → contour_surface/plot_contour.py} +0 -0
  135. /figrecipe/_dev/demo_plotters/{plot_contourf.py → contour_surface/plot_contourf.py} +0 -0
  136. /figrecipe/_dev/demo_plotters/{plot_tricontour.py → contour_surface/plot_tricontour.py} +0 -0
  137. /figrecipe/_dev/demo_plotters/{plot_tricontourf.py → contour_surface/plot_tricontourf.py} +0 -0
  138. /figrecipe/_dev/demo_plotters/{plot_tripcolor.py → contour_surface/plot_tripcolor.py} +0 -0
  139. /figrecipe/_dev/demo_plotters/{plot_triplot.py → contour_surface/plot_triplot.py} +0 -0
  140. /figrecipe/_dev/demo_plotters/{plot_boxplot.py → distribution/plot_boxplot.py} +0 -0
  141. /figrecipe/_dev/demo_plotters/{plot_ecdf.py → distribution/plot_ecdf.py} +0 -0
  142. /figrecipe/_dev/demo_plotters/{plot_hist.py → distribution/plot_hist.py} +0 -0
  143. /figrecipe/_dev/demo_plotters/{plot_hist2d.py → distribution/plot_hist2d.py} +0 -0
  144. /figrecipe/_dev/demo_plotters/{plot_violinplot.py → distribution/plot_violinplot.py} +0 -0
  145. /figrecipe/_dev/demo_plotters/{plot_hexbin.py → image_matrix/plot_hexbin.py} +0 -0
  146. /figrecipe/_dev/demo_plotters/{plot_imshow.py → image_matrix/plot_imshow.py} +0 -0
  147. /figrecipe/_dev/demo_plotters/{plot_matshow.py → image_matrix/plot_matshow.py} +0 -0
  148. /figrecipe/_dev/demo_plotters/{plot_pcolor.py → image_matrix/plot_pcolor.py} +0 -0
  149. /figrecipe/_dev/demo_plotters/{plot_pcolormesh.py → image_matrix/plot_pcolormesh.py} +0 -0
  150. /figrecipe/_dev/demo_plotters/{plot_spy.py → image_matrix/plot_spy.py} +0 -0
  151. /figrecipe/_dev/demo_plotters/{plot_errorbar.py → line_curve/plot_errorbar.py} +0 -0
  152. /figrecipe/_dev/demo_plotters/{plot_fill.py → line_curve/plot_fill.py} +0 -0
  153. /figrecipe/_dev/demo_plotters/{plot_fill_between.py → line_curve/plot_fill_between.py} +0 -0
  154. /figrecipe/_dev/demo_plotters/{plot_fill_betweenx.py → line_curve/plot_fill_betweenx.py} +0 -0
  155. /figrecipe/_dev/demo_plotters/{plot_stackplot.py → line_curve/plot_stackplot.py} +0 -0
  156. /figrecipe/_dev/demo_plotters/{plot_stairs.py → line_curve/plot_stairs.py} +0 -0
  157. /figrecipe/_dev/demo_plotters/{plot_step.py → line_curve/plot_step.py} +0 -0
  158. /figrecipe/_dev/demo_plotters/{plot_scatter.py → scatter_points/plot_scatter.py} +0 -0
  159. /figrecipe/_dev/demo_plotters/{plot_eventplot.py → special/plot_eventplot.py} +0 -0
  160. /figrecipe/_dev/demo_plotters/{plot_loglog.py → special/plot_loglog.py} +0 -0
  161. /figrecipe/_dev/demo_plotters/{plot_semilogx.py → special/plot_semilogx.py} +0 -0
  162. /figrecipe/_dev/demo_plotters/{plot_semilogy.py → special/plot_semilogy.py} +0 -0
  163. /figrecipe/_dev/demo_plotters/{plot_stem.py → special/plot_stem.py} +0 -0
  164. /figrecipe/_dev/demo_plotters/{plot_acorr.py → spectral_signal/plot_acorr.py} +0 -0
  165. /figrecipe/_dev/demo_plotters/{plot_angle_spectrum.py → spectral_signal/plot_angle_spectrum.py} +0 -0
  166. /figrecipe/_dev/demo_plotters/{plot_cohere.py → spectral_signal/plot_cohere.py} +0 -0
  167. /figrecipe/_dev/demo_plotters/{plot_csd.py → spectral_signal/plot_csd.py} +0 -0
  168. /figrecipe/_dev/demo_plotters/{plot_magnitude_spectrum.py → spectral_signal/plot_magnitude_spectrum.py} +0 -0
  169. /figrecipe/_dev/demo_plotters/{plot_phase_spectrum.py → spectral_signal/plot_phase_spectrum.py} +0 -0
  170. /figrecipe/_dev/demo_plotters/{plot_psd.py → spectral_signal/plot_psd.py} +0 -0
  171. /figrecipe/_dev/demo_plotters/{plot_specgram.py → spectral_signal/plot_specgram.py} +0 -0
  172. /figrecipe/_dev/demo_plotters/{plot_xcorr.py → spectral_signal/plot_xcorr.py} +0 -0
  173. /figrecipe/_dev/demo_plotters/{plot_barbs.py → vector_flow/plot_barbs.py} +0 -0
  174. /figrecipe/_dev/demo_plotters/{plot_quiver.py → vector_flow/plot_quiver.py} +0 -0
  175. /figrecipe/_dev/demo_plotters/{plot_streamplot.py → vector_flow/plot_streamplot.py} +0 -0
  176. {figrecipe-0.6.0.dist-info → figrecipe-0.7.4.dist-info}/WHEEL +0 -0
  177. {figrecipe-0.6.0.dist-info → figrecipe-0.7.4.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,333 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """Subplots helper implementation for the public API."""
4
+
5
+ from typing import Any, Dict, Optional, Tuple, Union
6
+
7
+ from numpy.typing import NDArray
8
+
9
+ from .._utils._units import mm_to_inch
10
+ from .._wrappers import RecordingAxes, RecordingFigure
11
+
12
+
13
+ def _get_mm_value(explicit, global_style, style_path, default):
14
+ """Get mm value with priority: explicit > global style > default."""
15
+ if explicit is not None:
16
+ return explicit
17
+ if global_style is not None:
18
+ try:
19
+ val = global_style
20
+ for key in style_path:
21
+ val = val.get(key) if isinstance(val, dict) else getattr(val, key, None)
22
+ if val is None:
23
+ break
24
+ if val is not None:
25
+ return val
26
+ except (KeyError, AttributeError):
27
+ pass
28
+ return default
29
+
30
+
31
+ def _check_mm_layout(
32
+ axes_width_mm,
33
+ axes_height_mm,
34
+ margin_left_mm,
35
+ margin_right_mm,
36
+ margin_bottom_mm,
37
+ margin_top_mm,
38
+ space_w_mm,
39
+ space_h_mm,
40
+ global_style,
41
+ ):
42
+ """Check if mm-based layout is requested."""
43
+ has_explicit_mm = any(
44
+ [
45
+ axes_width_mm is not None,
46
+ axes_height_mm is not None,
47
+ margin_left_mm is not None,
48
+ margin_right_mm is not None,
49
+ margin_bottom_mm is not None,
50
+ margin_top_mm is not None,
51
+ space_w_mm is not None,
52
+ space_h_mm is not None,
53
+ ]
54
+ )
55
+
56
+ has_style_mm = False
57
+ if global_style is not None:
58
+ try:
59
+ has_style_mm = (
60
+ global_style.get("axes", {}).get("width_mm") is not None
61
+ or getattr(getattr(global_style, "axes", None), "width_mm", None)
62
+ is not None
63
+ )
64
+ except (KeyError, AttributeError):
65
+ pass
66
+
67
+ return has_explicit_mm or has_style_mm
68
+
69
+
70
+ def _calculate_mm_layout(
71
+ nrows: int,
72
+ ncols: int,
73
+ axes_width_mm: Optional[float],
74
+ axes_height_mm: Optional[float],
75
+ margin_left_mm: Optional[float],
76
+ margin_right_mm: Optional[float],
77
+ margin_bottom_mm: Optional[float],
78
+ margin_top_mm: Optional[float],
79
+ space_w_mm: Optional[float],
80
+ space_h_mm: Optional[float],
81
+ global_style,
82
+ kwargs: Dict[str, Any],
83
+ ) -> Tuple[Optional[Dict[str, float]], Dict[str, Any]]:
84
+ """Calculate mm-based layout and update kwargs with figsize."""
85
+ aw = _get_mm_value(axes_width_mm, global_style, ["axes", "width_mm"], 40)
86
+ ah = _get_mm_value(axes_height_mm, global_style, ["axes", "height_mm"], 28)
87
+ ml = _get_mm_value(margin_left_mm, global_style, ["margins", "left_mm"], 15)
88
+ mr = _get_mm_value(margin_right_mm, global_style, ["margins", "right_mm"], 5)
89
+ mb = _get_mm_value(margin_bottom_mm, global_style, ["margins", "bottom_mm"], 12)
90
+ mt = _get_mm_value(margin_top_mm, global_style, ["margins", "top_mm"], 8)
91
+ sw = _get_mm_value(space_w_mm, global_style, ["spacing", "horizontal_mm"], 8)
92
+ sh = _get_mm_value(space_h_mm, global_style, ["spacing", "vertical_mm"], 10)
93
+
94
+ # Calculate total figure size
95
+ total_width_mm = ml + (ncols * aw) + ((ncols - 1) * sw) + mr
96
+ total_height_mm = mb + (nrows * ah) + ((nrows - 1) * sh) + mt
97
+
98
+ # Convert to inches and set figsize
99
+ kwargs["figsize"] = (mm_to_inch(total_width_mm), mm_to_inch(total_height_mm))
100
+
101
+ mm_layout = {
102
+ "axes_width_mm": aw,
103
+ "axes_height_mm": ah,
104
+ "margin_left_mm": ml,
105
+ "margin_right_mm": mr,
106
+ "margin_bottom_mm": mb,
107
+ "margin_top_mm": mt,
108
+ "space_w_mm": sw,
109
+ "space_h_mm": sh,
110
+ }
111
+
112
+ return mm_layout, kwargs
113
+
114
+
115
+ def _apply_mm_layout_to_figure(
116
+ fig: RecordingFigure,
117
+ mm_layout: Dict[str, float],
118
+ nrows: int,
119
+ ncols: int,
120
+ ):
121
+ """Apply mm-based layout adjustments to figure."""
122
+ ml = mm_layout["margin_left_mm"]
123
+ mr = mm_layout["margin_right_mm"]
124
+ mb = mm_layout["margin_bottom_mm"]
125
+ mt = mm_layout["margin_top_mm"]
126
+ aw = mm_layout["axes_width_mm"]
127
+ ah = mm_layout["axes_height_mm"]
128
+ sw = mm_layout["space_w_mm"]
129
+ sh = mm_layout["space_h_mm"]
130
+
131
+ total_width_mm = ml + (ncols * aw) + ((ncols - 1) * sw) + mr
132
+ total_height_mm = mb + (nrows * ah) + ((nrows - 1) * sh) + mt
133
+
134
+ # Calculate relative positions (0-1 range)
135
+ left = ml / total_width_mm
136
+ right = 1 - (mr / total_width_mm)
137
+ bottom = mb / total_height_mm
138
+ top = 1 - (mt / total_height_mm)
139
+
140
+ # Calculate spacing as fraction of figure size
141
+ wspace = sw / aw if ncols > 1 else 0
142
+ hspace = sh / ah if nrows > 1 else 0
143
+
144
+ fig.fig.subplots_adjust(
145
+ left=left,
146
+ right=right,
147
+ bottom=bottom,
148
+ top=top,
149
+ wspace=wspace,
150
+ hspace=hspace,
151
+ )
152
+
153
+ # Record layout in figure record for reproduction
154
+ fig.record.layout = {
155
+ "left": left,
156
+ "right": right,
157
+ "bottom": bottom,
158
+ "top": top,
159
+ "wspace": wspace,
160
+ "hspace": hspace,
161
+ }
162
+
163
+
164
+ def _apply_style_to_axes(
165
+ fig: RecordingFigure,
166
+ axes: Union[RecordingAxes, NDArray],
167
+ nrows: int,
168
+ ncols: int,
169
+ style: Optional[Dict[str, Any]],
170
+ apply_style_mm: bool,
171
+ global_style,
172
+ ) -> Optional[Dict[str, Any]]:
173
+ """Apply style to axes and return style dict if applied."""
174
+ import numpy as np
175
+
176
+ from ..styles import apply_style_mm as _apply_style
177
+ from ..styles import to_subplots_kwargs
178
+
179
+ style_dict = None
180
+ should_apply_style = False
181
+
182
+ if style is not None:
183
+ should_apply_style = True
184
+ style_dict = (
185
+ style.to_subplots_kwargs()
186
+ if hasattr(style, "to_subplots_kwargs")
187
+ else style
188
+ )
189
+ elif apply_style_mm and global_style is not None:
190
+ style_dict = to_subplots_kwargs(global_style)
191
+ if style_dict and style_dict.get("axes_thickness_mm") is not None:
192
+ should_apply_style = True
193
+
194
+ if should_apply_style and style_dict:
195
+ if nrows == 1 and ncols == 1:
196
+ _apply_style(axes._ax, style_dict)
197
+ else:
198
+ axes_array = np.array(axes)
199
+ for ax in axes_array.flat:
200
+ _apply_style(ax._ax if hasattr(ax, "_ax") else ax, style_dict)
201
+
202
+ fig.record.style = style_dict
203
+
204
+ return style_dict
205
+
206
+
207
+ def create_subplots(
208
+ nrows: int = 1,
209
+ ncols: int = 1,
210
+ axes_width_mm: Optional[float] = None,
211
+ axes_height_mm: Optional[float] = None,
212
+ margin_left_mm: Optional[float] = None,
213
+ margin_right_mm: Optional[float] = None,
214
+ margin_bottom_mm: Optional[float] = None,
215
+ margin_top_mm: Optional[float] = None,
216
+ space_w_mm: Optional[float] = None,
217
+ space_h_mm: Optional[float] = None,
218
+ style: Optional[Dict[str, Any]] = None,
219
+ apply_style_mm: bool = True,
220
+ panel_labels: Optional[bool] = None,
221
+ **kwargs,
222
+ ) -> Tuple[RecordingFigure, Union[RecordingAxes, NDArray]]:
223
+ """Core subplots implementation."""
224
+ from .._wrappers._figure import create_recording_subplots
225
+ from ..styles._style_loader import _STYLE_CACHE, to_subplots_kwargs
226
+
227
+ global_style = _STYLE_CACHE
228
+
229
+ # Check if mm-based layout is requested
230
+ use_mm_layout = _check_mm_layout(
231
+ axes_width_mm,
232
+ axes_height_mm,
233
+ margin_left_mm,
234
+ margin_right_mm,
235
+ margin_bottom_mm,
236
+ margin_top_mm,
237
+ space_w_mm,
238
+ space_h_mm,
239
+ global_style,
240
+ )
241
+
242
+ if use_mm_layout and "figsize" not in kwargs:
243
+ mm_layout, kwargs = _calculate_mm_layout(
244
+ nrows,
245
+ ncols,
246
+ axes_width_mm,
247
+ axes_height_mm,
248
+ margin_left_mm,
249
+ margin_right_mm,
250
+ margin_bottom_mm,
251
+ margin_top_mm,
252
+ space_w_mm,
253
+ space_h_mm,
254
+ global_style,
255
+ kwargs,
256
+ )
257
+ else:
258
+ mm_layout = None
259
+
260
+ # Apply DPI from global style if not explicitly provided
261
+ if "dpi" not in kwargs and global_style is not None:
262
+ style_dpi = None
263
+ try:
264
+ if hasattr(global_style, "figure") and hasattr(global_style.figure, "dpi"):
265
+ style_dpi = global_style.figure.dpi
266
+ elif hasattr(global_style, "output") and hasattr(
267
+ global_style.output, "dpi"
268
+ ):
269
+ style_dpi = global_style.output.dpi
270
+ except (KeyError, AttributeError):
271
+ pass
272
+ if style_dpi is not None:
273
+ kwargs["dpi"] = style_dpi
274
+
275
+ # Handle style parameter
276
+ if style is not None:
277
+ if hasattr(style, "to_subplots_kwargs"):
278
+ style_kwargs = style.to_subplots_kwargs()
279
+ for key, value in style_kwargs.items():
280
+ if key not in kwargs:
281
+ kwargs[key] = value
282
+
283
+ # Check if style specifies constrained_layout
284
+ style_constrained = False
285
+ if global_style is not None:
286
+ style_dict_check = to_subplots_kwargs(global_style)
287
+ style_constrained = style_dict_check.get("constrained_layout", False)
288
+
289
+ # Use constrained_layout if: style specifies it, or non-mm layout
290
+ if "constrained_layout" not in kwargs:
291
+ if style_constrained:
292
+ kwargs["constrained_layout"] = True
293
+ elif not use_mm_layout:
294
+ kwargs["constrained_layout"] = True
295
+
296
+ # Create the recording subplots
297
+ fig, axes = create_recording_subplots(nrows, ncols, **kwargs)
298
+
299
+ # Record constrained_layout setting for reproduction
300
+ fig.record.constrained_layout = kwargs.get("constrained_layout", False)
301
+
302
+ # Store mm_layout metadata on figure for serialization
303
+ use_constrained = kwargs.get("constrained_layout", False)
304
+ if mm_layout is not None and not use_constrained:
305
+ fig._mm_layout = mm_layout
306
+ _apply_mm_layout_to_figure(fig, mm_layout, nrows, ncols)
307
+
308
+ # Apply styling using helper
309
+ _apply_style_to_axes(fig, axes, nrows, ncols, style, apply_style_mm, global_style)
310
+
311
+ # Determine panel_labels setting
312
+ use_panel_labels = panel_labels
313
+ if use_panel_labels is None and global_style is not None:
314
+ behavior = global_style.get("behavior", {})
315
+ use_panel_labels = behavior.get("panel_labels", False)
316
+
317
+ # Add panel labels if enabled (for multi-panel figures)
318
+ if use_panel_labels and (nrows > 1 or ncols > 1):
319
+ fig.add_panel_labels()
320
+
321
+ return fig, axes
322
+
323
+
324
+ __all__ = [
325
+ "_get_mm_value",
326
+ "_check_mm_layout",
327
+ "_calculate_mm_layout",
328
+ "_apply_mm_layout_to_figure",
329
+ "_apply_style_to_axes",
330
+ "create_subplots",
331
+ ]
332
+
333
+ # EOF
@@ -0,0 +1,82 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """Standalone validation implementation."""
4
+
5
+ import tempfile
6
+ from pathlib import Path
7
+ from typing import Union
8
+
9
+ import numpy as np
10
+
11
+ from .._reproducer import reproduce
12
+ from .._utils._image_diff import compare_images
13
+ from .._validator import ValidationResult
14
+
15
+
16
+ def validate_recipe(
17
+ path: Union[str, Path],
18
+ mse_threshold: float = 100.0,
19
+ ) -> ValidationResult:
20
+ """Validate that a saved recipe can reproduce its original figure.
21
+
22
+ For standalone validation, we reproduce twice and compare
23
+ (This validates the recipe is self-consistent).
24
+
25
+ Parameters
26
+ ----------
27
+ path : str or Path
28
+ Path to .yaml recipe file.
29
+ mse_threshold : float
30
+ Maximum acceptable MSE for validation to pass (default: 100).
31
+
32
+ Returns
33
+ -------
34
+ ValidationResult
35
+ Detailed comparison results.
36
+ """
37
+ path = Path(path)
38
+
39
+ with tempfile.TemporaryDirectory() as tmpdir:
40
+ tmpdir = Path(tmpdir)
41
+
42
+ # Reproduce twice
43
+ fig1, _ = reproduce(path)
44
+ img1_path = tmpdir / "render1.png"
45
+ fig1.savefig(img1_path, dpi=150)
46
+
47
+ fig2, _ = reproduce(path)
48
+ img2_path = tmpdir / "render2.png"
49
+ fig2.savefig(img2_path, dpi=150)
50
+
51
+ # Compare
52
+ diff = compare_images(img1_path, img2_path)
53
+
54
+ mse = diff["mse"]
55
+ if np.isnan(mse):
56
+ valid = False
57
+ message = f"Image dimensions differ: {diff['size1']} vs {diff['size2']}"
58
+ elif mse > mse_threshold:
59
+ valid = False
60
+ message = f"MSE ({mse:.2f}) exceeds threshold ({mse_threshold})"
61
+ else:
62
+ valid = True
63
+ message = "Recipe produces consistent output"
64
+
65
+ return ValidationResult(
66
+ valid=valid,
67
+ mse=mse if not np.isnan(mse) else float("inf"),
68
+ psnr=diff["psnr"],
69
+ max_diff=diff["max_diff"]
70
+ if not np.isnan(diff["max_diff"])
71
+ else float("inf"),
72
+ size_original=diff["size1"],
73
+ size_reproduced=diff["size2"],
74
+ same_size=diff["same_size"],
75
+ file_size_diff=diff["file_size2"] - diff["file_size1"],
76
+ message=message,
77
+ )
78
+
79
+
80
+ __all__ = ["validate_recipe"]
81
+
82
+ # EOF
@@ -16,99 +16,8 @@ Usage:
16
16
  results = run_all_demos(fr, output_dir="./outputs")
17
17
  """
18
18
 
19
- import importlib
20
- from pathlib import Path
21
-
22
- from .._params import PLOTTING_METHODS
23
-
24
- # Auto-import plotters from demo_plotters directory
25
- _demo_dir = Path(__file__).parent / "demo_plotters"
26
- PLOTTERS = {}
27
-
28
- for method_name in sorted(PLOTTING_METHODS):
29
- module_name = f"plot_{method_name}"
30
- func_name = f"plot_{method_name}"
31
- module_path = _demo_dir / f"{module_name}.py"
32
-
33
- if module_path.exists():
34
- try:
35
- module = importlib.import_module(
36
- f".demo_plotters.{module_name}", package="figrecipe._dev"
37
- )
38
- if hasattr(module, func_name):
39
- PLOTTERS[method_name] = getattr(module, func_name)
40
- except ImportError:
41
- pass
42
-
43
-
44
- def list_plotters():
45
- """List all available plotter names."""
46
- return list(PLOTTERS.keys())
47
-
48
-
49
- def get_plotter(name):
50
- """Get a plotter function by name.
51
-
52
- Parameters
53
- ----------
54
- name : str
55
- Name of the plotting method (e.g., 'plot', 'scatter').
56
-
57
- Returns
58
- -------
59
- callable
60
- The plotter function with signature (plt, rng, ax=None) -> (fig, ax).
61
- """
62
- if name in PLOTTERS:
63
- return PLOTTERS[name]
64
- raise KeyError(f"Unknown plotter: {name}. Available: {list(PLOTTERS.keys())}")
65
-
66
-
67
- def run_all_demos(plt, output_dir=None, show=False):
68
- """Run all demo plotters and optionally save outputs.
69
-
70
- Parameters
71
- ----------
72
- plt : module
73
- figrecipe module (e.g., `import figrecipe as fr`).
74
- output_dir : Path or str, optional
75
- Directory to save output images.
76
- show : bool
77
- Whether to show figures interactively.
78
-
79
- Returns
80
- -------
81
- dict
82
- Results for each demo: {name: {'success': bool, 'error': str or None}}
83
- """
84
- import matplotlib.pyplot as _plt
85
- import numpy as np
86
-
87
- rng = np.random.default_rng(42)
88
- results = {}
89
-
90
- if output_dir:
91
- output_dir = Path(output_dir)
92
- output_dir.mkdir(parents=True, exist_ok=True)
93
-
94
- for name, func in PLOTTERS.items():
95
- try:
96
- fig, ax = func(plt, rng)
97
- if output_dir:
98
- out_path = output_dir / f"plot_{name}.png"
99
- mpl_fig = fig.fig if hasattr(fig, "fig") else fig
100
- mpl_fig.savefig(out_path, dpi=100, bbox_inches="tight")
101
- if show:
102
- _plt.show()
103
- else:
104
- mpl_fig = fig.fig if hasattr(fig, "fig") else fig
105
- _plt.close(mpl_fig)
106
- results[name] = {"success": True, "error": None}
107
- except Exception as e:
108
- results[name] = {"success": False, "error": str(e)}
109
-
110
- return results
111
-
19
+ from ._plotters import PLOTTERS, get_plotter, list_plotters
20
+ from ._run_demos import run_all_demos
112
21
 
113
22
  __all__ = [
114
23
  "PLOTTERS",
@@ -0,0 +1,76 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """Plotter registry for demo plotters."""
4
+
5
+ import importlib
6
+ from pathlib import Path
7
+
8
+ from .._params import PLOTTING_METHODS
9
+
10
+ # Auto-import plotters from demo_plotters subdirectories
11
+ _demo_dir = Path(__file__).parent / "demo_plotters"
12
+ PLOTTERS = {}
13
+
14
+ # Category subdirectories
15
+ _category_dirs = [
16
+ "line_curve",
17
+ "scatter_points",
18
+ "bar_categorical",
19
+ "distribution",
20
+ "image_matrix",
21
+ "contour_surface",
22
+ "spectral_signal",
23
+ "vector_flow",
24
+ "special",
25
+ ]
26
+
27
+ # Build mapping from method_name to category
28
+ _method_to_category = {}
29
+ for cat_dir in _category_dirs:
30
+ cat_path = _demo_dir / cat_dir
31
+ if cat_path.is_dir():
32
+ for plot_file in cat_path.glob("plot_*.py"):
33
+ method_name = plot_file.stem.replace("plot_", "")
34
+ _method_to_category[method_name] = cat_dir
35
+
36
+ for method_name in sorted(PLOTTING_METHODS):
37
+ module_name = f"plot_{method_name}"
38
+ func_name = f"plot_{method_name}"
39
+
40
+ # Check if we have this plotter in a category subdirectory
41
+ if method_name in _method_to_category:
42
+ cat_dir = _method_to_category[method_name]
43
+ try:
44
+ module = importlib.import_module(
45
+ f".demo_plotters.{cat_dir}.{module_name}", package="figrecipe._dev"
46
+ )
47
+ if hasattr(module, func_name):
48
+ PLOTTERS[method_name] = getattr(module, func_name)
49
+ except ImportError:
50
+ pass
51
+
52
+
53
+ def list_plotters():
54
+ """List all available plotter names."""
55
+ return list(PLOTTERS.keys())
56
+
57
+
58
+ def get_plotter(name):
59
+ """Get a plotter function by name.
60
+
61
+ Parameters
62
+ ----------
63
+ name : str
64
+ Name of the plotting method (e.g., 'plot', 'scatter').
65
+
66
+ Returns
67
+ -------
68
+ callable
69
+ The plotter function with signature (plt, rng, ax=None) -> (fig, ax).
70
+ """
71
+ if name in PLOTTERS:
72
+ return PLOTTERS[name]
73
+ raise KeyError(f"Unknown plotter: {name}. Available: {list(PLOTTERS.keys())}")
74
+
75
+
76
+ # EOF
@@ -0,0 +1,56 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """Demo runner for all plotters."""
4
+
5
+ from pathlib import Path
6
+
7
+ from ._plotters import PLOTTERS
8
+
9
+
10
+ def run_all_demos(plt, output_dir=None, show=False):
11
+ """Run all demo plotters and optionally save outputs.
12
+
13
+ Parameters
14
+ ----------
15
+ plt : module
16
+ figrecipe module (e.g., `import figrecipe as fr`).
17
+ output_dir : Path or str, optional
18
+ Directory to save output images.
19
+ show : bool
20
+ Whether to show figures interactively.
21
+
22
+ Returns
23
+ -------
24
+ dict
25
+ Results for each demo: {name: {'success': bool, 'error': str or None}}
26
+ """
27
+ import matplotlib.pyplot as _plt
28
+ import numpy as np
29
+
30
+ rng = np.random.default_rng(42)
31
+ results = {}
32
+
33
+ if output_dir:
34
+ output_dir = Path(output_dir)
35
+ output_dir.mkdir(parents=True, exist_ok=True)
36
+
37
+ for name, func in PLOTTERS.items():
38
+ try:
39
+ fig, ax = func(plt, rng)
40
+ if output_dir:
41
+ out_path = output_dir / f"plot_{name}.png"
42
+ mpl_fig = fig.fig if hasattr(fig, "fig") else fig
43
+ mpl_fig.savefig(out_path, dpi=100, bbox_inches="tight")
44
+ if show:
45
+ _plt.show()
46
+ else:
47
+ mpl_fig = fig.fig if hasattr(fig, "fig") else fig
48
+ _plt.close(mpl_fig)
49
+ results[name] = {"success": True, "error": None}
50
+ except Exception as e:
51
+ results[name] = {"success": False, "error": str(e)}
52
+
53
+ return results
54
+
55
+
56
+ # EOF