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.
- figrecipe/__init__.py +220 -819
- figrecipe/_api/__init__.py +48 -0
- figrecipe/_api/_extract.py +108 -0
- figrecipe/_api/_notebook.py +61 -0
- figrecipe/_api/_panel.py +46 -0
- figrecipe/_api/_save.py +191 -0
- figrecipe/_api/_seaborn_proxy.py +34 -0
- figrecipe/_api/_style_manager.py +153 -0
- figrecipe/_api/_subplots.py +333 -0
- figrecipe/_api/_validate.py +82 -0
- figrecipe/_dev/__init__.py +29 -0
- figrecipe/_dev/_plotters.py +76 -0
- figrecipe/_dev/_run_demos.py +56 -0
- figrecipe/_dev/demo_plotters/__init__.py +64 -0
- figrecipe/_dev/demo_plotters/_categories.py +81 -0
- figrecipe/_dev/demo_plotters/_figure_creators.py +119 -0
- figrecipe/_dev/demo_plotters/_helpers.py +31 -0
- figrecipe/_dev/demo_plotters/_registry.py +50 -0
- figrecipe/_dev/demo_plotters/bar_categorical/__init__.py +4 -0
- figrecipe/_dev/demo_plotters/bar_categorical/plot_bar.py +25 -0
- figrecipe/_dev/demo_plotters/bar_categorical/plot_barh.py +25 -0
- figrecipe/_dev/demo_plotters/contour_surface/__init__.py +4 -0
- figrecipe/_dev/demo_plotters/contour_surface/plot_contour.py +30 -0
- figrecipe/_dev/demo_plotters/contour_surface/plot_contourf.py +29 -0
- figrecipe/_dev/demo_plotters/contour_surface/plot_tricontour.py +28 -0
- figrecipe/_dev/demo_plotters/contour_surface/plot_tricontourf.py +28 -0
- figrecipe/_dev/demo_plotters/contour_surface/plot_tripcolor.py +29 -0
- figrecipe/_dev/demo_plotters/contour_surface/plot_triplot.py +25 -0
- figrecipe/_dev/demo_plotters/distribution/__init__.py +4 -0
- figrecipe/_dev/demo_plotters/distribution/plot_boxplot.py +24 -0
- figrecipe/_dev/demo_plotters/distribution/plot_ecdf.py +24 -0
- figrecipe/_dev/demo_plotters/distribution/plot_hist.py +24 -0
- figrecipe/_dev/demo_plotters/distribution/plot_hist2d.py +25 -0
- figrecipe/_dev/demo_plotters/distribution/plot_violinplot.py +25 -0
- figrecipe/_dev/demo_plotters/image_matrix/__init__.py +4 -0
- figrecipe/_dev/demo_plotters/image_matrix/plot_hexbin.py +25 -0
- figrecipe/_dev/demo_plotters/image_matrix/plot_imshow.py +23 -0
- figrecipe/_dev/demo_plotters/image_matrix/plot_matshow.py +23 -0
- figrecipe/_dev/demo_plotters/image_matrix/plot_pcolor.py +29 -0
- figrecipe/_dev/demo_plotters/image_matrix/plot_pcolormesh.py +29 -0
- figrecipe/_dev/demo_plotters/image_matrix/plot_spy.py +29 -0
- figrecipe/_dev/demo_plotters/line_curve/__init__.py +4 -0
- figrecipe/_dev/demo_plotters/line_curve/plot_errorbar.py +28 -0
- figrecipe/_dev/demo_plotters/line_curve/plot_fill.py +29 -0
- figrecipe/_dev/demo_plotters/line_curve/plot_fill_between.py +30 -0
- figrecipe/_dev/demo_plotters/line_curve/plot_fill_betweenx.py +28 -0
- figrecipe/_dev/demo_plotters/line_curve/plot_plot.py +28 -0
- figrecipe/_dev/demo_plotters/line_curve/plot_stackplot.py +29 -0
- figrecipe/_dev/demo_plotters/line_curve/plot_stairs.py +27 -0
- figrecipe/_dev/demo_plotters/line_curve/plot_step.py +27 -0
- figrecipe/_dev/demo_plotters/scatter_points/__init__.py +4 -0
- figrecipe/_dev/demo_plotters/scatter_points/plot_scatter.py +24 -0
- figrecipe/_dev/demo_plotters/special/__init__.py +4 -0
- figrecipe/_dev/demo_plotters/special/plot_eventplot.py +25 -0
- figrecipe/_dev/demo_plotters/special/plot_loglog.py +27 -0
- figrecipe/_dev/demo_plotters/special/plot_pie.py +27 -0
- figrecipe/_dev/demo_plotters/special/plot_semilogx.py +27 -0
- figrecipe/_dev/demo_plotters/special/plot_semilogy.py +27 -0
- figrecipe/_dev/demo_plotters/special/plot_stem.py +27 -0
- figrecipe/_dev/demo_plotters/spectral_signal/__init__.py +4 -0
- figrecipe/_dev/demo_plotters/spectral_signal/plot_acorr.py +24 -0
- figrecipe/_dev/demo_plotters/spectral_signal/plot_angle_spectrum.py +28 -0
- figrecipe/_dev/demo_plotters/spectral_signal/plot_cohere.py +29 -0
- figrecipe/_dev/demo_plotters/spectral_signal/plot_csd.py +29 -0
- figrecipe/_dev/demo_plotters/spectral_signal/plot_magnitude_spectrum.py +28 -0
- figrecipe/_dev/demo_plotters/spectral_signal/plot_phase_spectrum.py +28 -0
- figrecipe/_dev/demo_plotters/spectral_signal/plot_psd.py +29 -0
- figrecipe/_dev/demo_plotters/spectral_signal/plot_specgram.py +30 -0
- figrecipe/_dev/demo_plotters/spectral_signal/plot_xcorr.py +25 -0
- figrecipe/_dev/demo_plotters/vector_flow/__init__.py +4 -0
- figrecipe/_dev/demo_plotters/vector_flow/plot_barbs.py +30 -0
- figrecipe/_dev/demo_plotters/vector_flow/plot_quiver.py +30 -0
- figrecipe/_dev/demo_plotters/vector_flow/plot_streamplot.py +30 -0
- figrecipe/_editor/__init__.py +278 -0
- figrecipe/_editor/_bbox/__init__.py +43 -0
- figrecipe/_editor/_bbox/_collections.py +177 -0
- figrecipe/_editor/_bbox/_elements.py +159 -0
- figrecipe/_editor/_bbox/_extract.py +256 -0
- figrecipe/_editor/_bbox/_extract_axes.py +370 -0
- figrecipe/_editor/_bbox/_extract_text.py +342 -0
- figrecipe/_editor/_bbox/_lines.py +173 -0
- figrecipe/_editor/_bbox/_transforms.py +146 -0
- figrecipe/_editor/_flask_app.py +258 -0
- figrecipe/_editor/_helpers.py +242 -0
- figrecipe/_editor/_hitmap/__init__.py +76 -0
- figrecipe/_editor/_hitmap/_artists/__init__.py +21 -0
- figrecipe/_editor/_hitmap/_artists/_collections.py +345 -0
- figrecipe/_editor/_hitmap/_artists/_images.py +68 -0
- figrecipe/_editor/_hitmap/_artists/_lines.py +107 -0
- figrecipe/_editor/_hitmap/_artists/_patches.py +163 -0
- figrecipe/_editor/_hitmap/_artists/_text.py +190 -0
- figrecipe/_editor/_hitmap/_colors.py +181 -0
- figrecipe/_editor/_hitmap/_detect.py +137 -0
- figrecipe/_editor/_hitmap/_restore.py +154 -0
- figrecipe/_editor/_hitmap_main.py +182 -0
- figrecipe/_editor/_overrides.py +318 -0
- figrecipe/_editor/_preferences.py +135 -0
- figrecipe/_editor/_render_overrides.py +480 -0
- figrecipe/_editor/_renderer.py +199 -0
- figrecipe/_editor/_routes_axis.py +453 -0
- figrecipe/_editor/_routes_core.py +284 -0
- figrecipe/_editor/_routes_element.py +317 -0
- figrecipe/_editor/_routes_style.py +223 -0
- figrecipe/_editor/_templates/__init__.py +152 -0
- figrecipe/_editor/_templates/_html.py +502 -0
- figrecipe/_editor/_templates/_scripts/__init__.py +120 -0
- figrecipe/_editor/_templates/_scripts/_api.py +228 -0
- figrecipe/_editor/_templates/_scripts/_colors.py +485 -0
- figrecipe/_editor/_templates/_scripts/_core.py +436 -0
- figrecipe/_editor/_templates/_scripts/_debug_snapshot.py +186 -0
- figrecipe/_editor/_templates/_scripts/_element_editor.py +310 -0
- figrecipe/_editor/_templates/_scripts/_files.py +195 -0
- figrecipe/_editor/_templates/_scripts/_hitmap.py +509 -0
- figrecipe/_editor/_templates/_scripts/_inspector.py +315 -0
- figrecipe/_editor/_templates/_scripts/_labels.py +464 -0
- figrecipe/_editor/_templates/_scripts/_legend_drag.py +265 -0
- figrecipe/_editor/_templates/_scripts/_modals.py +226 -0
- figrecipe/_editor/_templates/_scripts/_overlays.py +292 -0
- figrecipe/_editor/_templates/_scripts/_panel_drag.py +334 -0
- figrecipe/_editor/_templates/_scripts/_panel_position.py +279 -0
- figrecipe/_editor/_templates/_scripts/_selection.py +237 -0
- figrecipe/_editor/_templates/_scripts/_tabs.py +89 -0
- figrecipe/_editor/_templates/_scripts/_view_mode.py +107 -0
- figrecipe/_editor/_templates/_scripts/_zoom.py +179 -0
- figrecipe/_editor/_templates/_styles/__init__.py +69 -0
- figrecipe/_editor/_templates/_styles/_base.py +64 -0
- figrecipe/_editor/_templates/_styles/_buttons.py +206 -0
- figrecipe/_editor/_templates/_styles/_color_input.py +123 -0
- figrecipe/_editor/_templates/_styles/_controls.py +265 -0
- figrecipe/_editor/_templates/_styles/_dynamic_props.py +144 -0
- figrecipe/_editor/_templates/_styles/_forms.py +126 -0
- figrecipe/_editor/_templates/_styles/_hitmap.py +184 -0
- figrecipe/_editor/_templates/_styles/_inspector.py +90 -0
- figrecipe/_editor/_templates/_styles/_labels.py +118 -0
- figrecipe/_editor/_templates/_styles/_modals.py +98 -0
- figrecipe/_editor/_templates/_styles/_overlays.py +130 -0
- figrecipe/_editor/_templates/_styles/_preview.py +225 -0
- figrecipe/_editor/_templates/_styles/_selection.py +73 -0
- figrecipe/_params/_DECORATION_METHODS.py +33 -0
- figrecipe/_params/_PLOTTING_METHODS.py +58 -0
- figrecipe/_params/__init__.py +9 -0
- figrecipe/_recorder.py +92 -110
- figrecipe/_recorder_utils.py +124 -0
- figrecipe/_reproducer/__init__.py +18 -0
- figrecipe/_reproducer/_core.py +498 -0
- figrecipe/_reproducer/_custom_plots.py +279 -0
- figrecipe/_reproducer/_seaborn.py +100 -0
- figrecipe/_reproducer/_violin.py +186 -0
- figrecipe/_seaborn.py +14 -9
- figrecipe/_serializer.py +2 -2
- figrecipe/_signatures/README.md +68 -0
- figrecipe/_signatures/__init__.py +12 -2
- figrecipe/_signatures/_kwargs.py +273 -0
- figrecipe/_signatures/_loader.py +114 -57
- figrecipe/_signatures/_parsing.py +147 -0
- 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 +193 -170
- figrecipe/_wrappers/_axes_helpers.py +136 -0
- figrecipe/_wrappers/_axes_plots.py +418 -0
- figrecipe/_wrappers/_axes_seaborn.py +157 -0
- figrecipe/_wrappers/_figure.py +277 -18
- figrecipe/_wrappers/_panel_labels.py +127 -0
- figrecipe/_wrappers/_plot_helpers.py +143 -0
- figrecipe/_wrappers/_violin_helpers.py +180 -0
- figrecipe/plt.py +0 -1
- figrecipe/pyplot.py +2 -1
- figrecipe/styles/__init__.py +12 -11
- figrecipe/styles/_dotdict.py +72 -0
- figrecipe/styles/_finalize.py +134 -0
- figrecipe/styles/_fonts.py +77 -0
- figrecipe/styles/_kwargs_converter.py +178 -0
- figrecipe/styles/_plot_styles.py +209 -0
- figrecipe/styles/_style_applier.py +60 -202
- figrecipe/styles/_style_loader.py +73 -121
- figrecipe/styles/_themes.py +151 -0
- figrecipe/styles/presets/MATPLOTLIB.yaml +95 -0
- figrecipe/styles/presets/SCITEX.yaml +181 -0
- figrecipe-0.7.4.dist-info/METADATA +429 -0
- figrecipe-0.7.4.dist-info/RECORD +188 -0
- figrecipe/_reproducer.py +0 -358
- 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.7.4.dist-info}/WHEEL +0 -0
- {figrecipe-0.5.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
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""Development utilities for figrecipe.
|
|
4
|
+
|
|
5
|
+
Provides demo plotters for all supported matplotlib plotting methods.
|
|
6
|
+
All plotters follow signature: (plt, rng, ax=None) -> (fig, ax)
|
|
7
|
+
|
|
8
|
+
Usage:
|
|
9
|
+
import figrecipe as fr
|
|
10
|
+
from figrecipe._dev import PLOTTERS, run_all_demos
|
|
11
|
+
|
|
12
|
+
# Run a single demo
|
|
13
|
+
fig, ax = PLOTTERS["plot"](fr, np.random.default_rng(42))
|
|
14
|
+
|
|
15
|
+
# Run all demos
|
|
16
|
+
results = run_all_demos(fr, output_dir="./outputs")
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from ._plotters import PLOTTERS, get_plotter, list_plotters
|
|
20
|
+
from ._run_demos import run_all_demos
|
|
21
|
+
|
|
22
|
+
__all__ = [
|
|
23
|
+
"PLOTTERS",
|
|
24
|
+
"list_plotters",
|
|
25
|
+
"get_plotter",
|
|
26
|
+
"run_all_demos",
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
# EOF
|
|
@@ -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
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""Demo plotters registry for all supported figrecipe plotting methods.
|
|
4
|
+
|
|
5
|
+
Usage
|
|
6
|
+
-----
|
|
7
|
+
>>> from figrecipe._dev.demo_plotters import REGISTRY, create_all_plots_figure
|
|
8
|
+
>>>
|
|
9
|
+
>>> # List all available plot types
|
|
10
|
+
>>> print(list(REGISTRY.keys()))
|
|
11
|
+
>>>
|
|
12
|
+
>>> # List by category
|
|
13
|
+
>>> from figrecipe._dev.demo_plotters import CATEGORIES
|
|
14
|
+
>>> for cat, plots in CATEGORIES.items():
|
|
15
|
+
... print(f"{cat}: {plots}")
|
|
16
|
+
>>>
|
|
17
|
+
>>> # Create a single figure with all plot types
|
|
18
|
+
>>> fig, axes = create_all_plots_figure(fr)
|
|
19
|
+
>>>
|
|
20
|
+
>>> # Run individual plotter
|
|
21
|
+
>>> fig, ax = REGISTRY['plot'](fr, rng, ax)
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from ._categories import CATEGORIES, CATEGORY_DISPLAY_NAMES, REPRESENTATIVES
|
|
25
|
+
from ._figure_creators import (
|
|
26
|
+
create_all_plots_figure,
|
|
27
|
+
run_all_demos,
|
|
28
|
+
run_individual_demos,
|
|
29
|
+
)
|
|
30
|
+
from ._helpers import (
|
|
31
|
+
get_plotter,
|
|
32
|
+
get_representative_plots,
|
|
33
|
+
list_plots,
|
|
34
|
+
list_plots_by_category,
|
|
35
|
+
)
|
|
36
|
+
from ._registry import REGISTRY
|
|
37
|
+
|
|
38
|
+
# Legacy exports for backwards compatibility
|
|
39
|
+
__all__ = [
|
|
40
|
+
"REGISTRY",
|
|
41
|
+
"CATEGORIES",
|
|
42
|
+
"CATEGORY_DISPLAY_NAMES",
|
|
43
|
+
"REPRESENTATIVES",
|
|
44
|
+
"list_plots",
|
|
45
|
+
"list_plots_by_category",
|
|
46
|
+
"get_representative_plots",
|
|
47
|
+
"get_plotter",
|
|
48
|
+
"create_all_plots_figure",
|
|
49
|
+
"run_individual_demos",
|
|
50
|
+
"run_all_demos",
|
|
51
|
+
]
|
|
52
|
+
|
|
53
|
+
# Legacy: export plot_* functions to globals
|
|
54
|
+
for _name, _func in REGISTRY.items():
|
|
55
|
+
globals()[f"plot_{_name}"] = _func
|
|
56
|
+
__all__.append(f"plot_{_name}")
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def list_demos():
|
|
60
|
+
"""Legacy: List all available demo functions."""
|
|
61
|
+
return [f"plot_{name}" for name in REGISTRY.keys()]
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
# EOF
|