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
figrecipe/_utils/__init__.py
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
# -*- coding: utf-8 -*-
|
|
3
3
|
"""Utility modules for figrecipe."""
|
|
4
4
|
|
|
5
|
-
from ._numpy_io import load_array, save_array
|
|
6
5
|
from ._diff import get_non_default_kwargs, is_default_value
|
|
7
|
-
from .
|
|
6
|
+
from ._numpy_io import load_array, save_array
|
|
7
|
+
from ._units import inch_to_mm, mm_to_inch, mm_to_pt, pt_to_mm
|
|
8
8
|
|
|
9
9
|
__all__ = [
|
|
10
10
|
"save_array",
|
|
@@ -19,14 +19,16 @@ __all__ = [
|
|
|
19
19
|
|
|
20
20
|
# Optional: image comparison (requires PIL)
|
|
21
21
|
try:
|
|
22
|
-
from ._image_diff import compare_images, create_comparison_figure
|
|
22
|
+
from ._image_diff import compare_images, create_comparison_figure # noqa: F401
|
|
23
|
+
|
|
23
24
|
__all__.extend(["compare_images", "create_comparison_figure"])
|
|
24
25
|
except ImportError:
|
|
25
26
|
pass
|
|
26
27
|
|
|
27
28
|
# Optional: crop utility (requires PIL)
|
|
28
29
|
try:
|
|
29
|
-
from ._crop import crop, find_content_area
|
|
30
|
+
from ._crop import crop, find_content_area # noqa: F401
|
|
31
|
+
|
|
30
32
|
__all__.extend(["crop", "find_content_area"])
|
|
31
33
|
except ImportError:
|
|
32
34
|
pass
|
figrecipe/_utils/_crop.py
CHANGED
|
@@ -8,7 +8,6 @@ and crops them, removing excess whitespace while preserving a specified margin.
|
|
|
8
8
|
|
|
9
9
|
__all__ = ["crop", "find_content_area", "mm_to_pixels"]
|
|
10
10
|
|
|
11
|
-
import os
|
|
12
11
|
from pathlib import Path
|
|
13
12
|
from typing import Optional, Tuple, Union
|
|
14
13
|
|
|
@@ -195,7 +194,9 @@ def crop(
|
|
|
195
194
|
else:
|
|
196
195
|
left, upper, right, lower = find_content_area(input_path)
|
|
197
196
|
if verbose:
|
|
198
|
-
print(
|
|
197
|
+
print(
|
|
198
|
+
f"Content area: left={left}, upper={upper}, right={right}, lower={lower}"
|
|
199
|
+
)
|
|
199
200
|
|
|
200
201
|
# Add margin, clamping to image boundaries
|
|
201
202
|
left = max(left - margin, 0)
|
|
@@ -222,11 +223,14 @@ def crop(
|
|
|
222
223
|
|
|
223
224
|
# Preserve PNG text chunks
|
|
224
225
|
from PIL import PngImagePlugin
|
|
226
|
+
|
|
225
227
|
pnginfo = PngImagePlugin.PngInfo()
|
|
226
228
|
for key, value in img.info.items():
|
|
227
229
|
if isinstance(value, (str, bytes)):
|
|
228
230
|
try:
|
|
229
|
-
pnginfo.add_text(
|
|
231
|
+
pnginfo.add_text(
|
|
232
|
+
key, str(value) if isinstance(value, bytes) else value
|
|
233
|
+
)
|
|
230
234
|
except Exception:
|
|
231
235
|
pass
|
|
232
236
|
save_kwargs["pnginfo"] = pnginfo
|
|
@@ -240,7 +244,9 @@ def crop(
|
|
|
240
244
|
|
|
241
245
|
final_width, final_height = cropped_img.size
|
|
242
246
|
if verbose:
|
|
243
|
-
area_reduction = 1 - (
|
|
247
|
+
area_reduction = 1 - (
|
|
248
|
+
(final_width * final_height) / (original_width * original_height)
|
|
249
|
+
)
|
|
244
250
|
print(f"Saved {area_reduction * 100:.1f}% of original area")
|
|
245
251
|
if output_path != input_path:
|
|
246
252
|
print(f"Saved to: {output_path}")
|
figrecipe/_utils/_image_diff.py
CHANGED
|
@@ -22,7 +22,8 @@ def load_image(path: Union[str, Path]) -> np.ndarray:
|
|
|
22
22
|
Image as (H, W, C) array with values 0-255.
|
|
23
23
|
"""
|
|
24
24
|
from PIL import Image
|
|
25
|
-
|
|
25
|
+
|
|
26
|
+
img = Image.open(path).convert("RGB")
|
|
26
27
|
return np.array(img)
|
|
27
28
|
|
|
28
29
|
|
|
@@ -48,16 +49,18 @@ def compute_diff(
|
|
|
48
49
|
"""
|
|
49
50
|
# Ensure same shape
|
|
50
51
|
if img1.shape != img2.shape:
|
|
51
|
-
raise ValueError(
|
|
52
|
-
f"Image shapes differ: {img1.shape} vs {img2.shape}"
|
|
53
|
-
)
|
|
52
|
+
raise ValueError(f"Image shapes differ: {img1.shape} vs {img2.shape}")
|
|
54
53
|
|
|
55
54
|
# Compute difference
|
|
56
55
|
diff = np.abs(img1.astype(float) - img2.astype(float))
|
|
57
|
-
mse = np.mean(diff
|
|
56
|
+
mse = np.mean(diff**2)
|
|
58
57
|
|
|
59
58
|
# Normalize diff for visualization
|
|
60
|
-
diff_img = (
|
|
59
|
+
diff_img = (
|
|
60
|
+
(diff / diff.max() * 255).astype(np.uint8)
|
|
61
|
+
if diff.max() > 0
|
|
62
|
+
else diff.astype(np.uint8)
|
|
63
|
+
)
|
|
61
64
|
|
|
62
65
|
return mse, diff_img
|
|
63
66
|
|
|
@@ -108,14 +111,14 @@ def compare_images(
|
|
|
108
111
|
mse, diff_img = compute_diff(img1, img2)
|
|
109
112
|
else:
|
|
110
113
|
# Can't compute pixel diff for different sizes
|
|
111
|
-
mse = float(
|
|
114
|
+
mse = float("nan")
|
|
112
115
|
diff_img = None
|
|
113
116
|
|
|
114
117
|
# Peak signal-to-noise ratio
|
|
115
118
|
if mse == 0:
|
|
116
|
-
psnr = float(
|
|
119
|
+
psnr = float("inf")
|
|
117
120
|
elif np.isnan(mse):
|
|
118
|
-
psnr = float(
|
|
121
|
+
psnr = float("nan")
|
|
119
122
|
else:
|
|
120
123
|
psnr = 10 * np.log10(255**2 / mse)
|
|
121
124
|
|
|
@@ -123,23 +126,24 @@ def compare_images(
|
|
|
123
126
|
if same_size:
|
|
124
127
|
max_diff = np.max(np.abs(img1.astype(float) - img2.astype(float)))
|
|
125
128
|
else:
|
|
126
|
-
max_diff = float(
|
|
129
|
+
max_diff = float("nan")
|
|
127
130
|
|
|
128
131
|
# Save diff image if requested
|
|
129
132
|
if diff_path is not None and diff_img is not None:
|
|
130
133
|
from PIL import Image
|
|
134
|
+
|
|
131
135
|
Image.fromarray(diff_img).save(diff_path)
|
|
132
136
|
|
|
133
137
|
return {
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
138
|
+
"identical": mse == 0,
|
|
139
|
+
"mse": float(mse),
|
|
140
|
+
"psnr": float(psnr),
|
|
141
|
+
"max_diff": float(max_diff),
|
|
142
|
+
"size1": (img1.shape[0], img1.shape[1]),
|
|
143
|
+
"size2": (img2.shape[0], img2.shape[1]),
|
|
144
|
+
"same_size": img1.shape == img2.shape,
|
|
145
|
+
"file_size1": file_size1,
|
|
146
|
+
"file_size2": file_size2,
|
|
143
147
|
}
|
|
144
148
|
|
|
145
149
|
|
|
@@ -173,32 +177,32 @@ def create_comparison_figure(
|
|
|
173
177
|
# Different sizes, just show side by side
|
|
174
178
|
fig, axes = plt.subplots(1, 2, figsize=(12, 5))
|
|
175
179
|
axes[0].imshow(img1)
|
|
176
|
-
axes[0].set_title(
|
|
177
|
-
axes[0].axis(
|
|
180
|
+
axes[0].set_title("Original")
|
|
181
|
+
axes[0].axis("off")
|
|
178
182
|
axes[1].imshow(img2)
|
|
179
|
-
axes[1].set_title(
|
|
180
|
-
axes[1].axis(
|
|
181
|
-
fig.suptitle(f
|
|
182
|
-
fig.savefig(output_path, dpi=150, bbox_inches=
|
|
183
|
+
axes[1].set_title("Reproduced")
|
|
184
|
+
axes[1].axis("off")
|
|
185
|
+
fig.suptitle(f"{title}\n(Different sizes)", fontsize=14)
|
|
186
|
+
fig.savefig(output_path, dpi=150, bbox_inches="tight", facecolor="white")
|
|
183
187
|
plt.close(fig)
|
|
184
188
|
return
|
|
185
189
|
|
|
186
190
|
fig, axes = plt.subplots(1, 3, figsize=(15, 5))
|
|
187
191
|
|
|
188
192
|
axes[0].imshow(img1)
|
|
189
|
-
axes[0].set_title(
|
|
190
|
-
axes[0].axis(
|
|
193
|
+
axes[0].set_title("Original")
|
|
194
|
+
axes[0].axis("off")
|
|
191
195
|
|
|
192
196
|
axes[1].imshow(img2)
|
|
193
|
-
axes[1].set_title(
|
|
194
|
-
axes[1].axis(
|
|
197
|
+
axes[1].set_title("Reproduced")
|
|
198
|
+
axes[1].axis("off")
|
|
195
199
|
|
|
196
200
|
axes[2].imshow(diff_img)
|
|
197
|
-
axes[2].set_title(f
|
|
198
|
-
axes[2].axis(
|
|
201
|
+
axes[2].set_title(f"Difference (MSE: {mse:.2f})")
|
|
202
|
+
axes[2].axis("off")
|
|
199
203
|
|
|
200
204
|
status = "IDENTICAL" if mse == 0 else f"MSE: {mse:.2f}"
|
|
201
|
-
fig.suptitle(f
|
|
205
|
+
fig.suptitle(f"{title}\n{status}", fontsize=14, fontweight="bold")
|
|
202
206
|
|
|
203
|
-
fig.savefig(output_path, dpi=150, bbox_inches=
|
|
207
|
+
fig.savefig(output_path, dpi=150, bbox_inches="tight", facecolor="white")
|
|
204
208
|
plt.close(fig)
|
figrecipe/_utils/_numpy_io.py
CHANGED
figrecipe/_utils/_units.py
CHANGED
|
@@ -11,7 +11,14 @@ Constants:
|
|
|
11
11
|
- 1 mm = 72/25.4 points
|
|
12
12
|
"""
|
|
13
13
|
|
|
14
|
-
__all__ = [
|
|
14
|
+
__all__ = [
|
|
15
|
+
"mm_to_inch",
|
|
16
|
+
"inch_to_mm",
|
|
17
|
+
"mm_to_pt",
|
|
18
|
+
"pt_to_mm",
|
|
19
|
+
"mm_to_scatter_size",
|
|
20
|
+
"normalize_color",
|
|
21
|
+
]
|
|
15
22
|
|
|
16
23
|
from typing import List, Tuple, Union
|
|
17
24
|
|
|
@@ -143,12 +150,13 @@ def mm_to_scatter_size(diameter_mm: float) -> float:
|
|
|
143
150
|
where d is diameter in points.
|
|
144
151
|
"""
|
|
145
152
|
import math
|
|
153
|
+
|
|
146
154
|
diameter_pt = mm_to_pt(diameter_mm)
|
|
147
155
|
return math.pi * (diameter_pt / 2) ** 2
|
|
148
156
|
|
|
149
157
|
|
|
150
158
|
def normalize_color(
|
|
151
|
-
color: Union[List[int], Tuple[int, ...], str]
|
|
159
|
+
color: Union[List[int], Tuple[int, ...], str],
|
|
152
160
|
) -> Union[Tuple[float, ...], str]:
|
|
153
161
|
"""Normalize color to matplotlib-compatible format.
|
|
154
162
|
|
|
@@ -195,6 +203,6 @@ if __name__ == "__main__":
|
|
|
195
203
|
print(f" 1 inch = {inch_to_mm(1.0):.1f} mm")
|
|
196
204
|
print(f" 0.2 mm = {mm_to_pt(0.2):.4f} pt")
|
|
197
205
|
print(f" 1 pt = {pt_to_mm(1.0):.4f} mm")
|
|
198
|
-
print(
|
|
206
|
+
print("\nColor normalization:")
|
|
199
207
|
print(f" [0, 128, 192] -> {normalize_color([0, 128, 192])}")
|
|
200
208
|
print(f" '#0080C0' -> {normalize_color('#0080C0')}")
|
figrecipe/_validator.py
CHANGED
|
@@ -61,7 +61,9 @@ class ValidationResult:
|
|
|
61
61
|
f"({'match' if self.same_size else 'DIFFER'})",
|
|
62
62
|
f" Pixel MSE: {self.mse:.2f}",
|
|
63
63
|
f" Max pixel diff: {self.max_diff:.1f}",
|
|
64
|
-
f" PSNR: {self.psnr:.1f} dB"
|
|
64
|
+
f" PSNR: {self.psnr:.1f} dB"
|
|
65
|
+
if not np.isinf(self.psnr)
|
|
66
|
+
else " PSNR: inf (identical)",
|
|
65
67
|
f" File size diff: {self.file_size_diff:+d} bytes",
|
|
66
68
|
]
|
|
67
69
|
if not self.valid:
|
|
@@ -95,6 +97,7 @@ def validate_recipe(
|
|
|
95
97
|
Detailed comparison results.
|
|
96
98
|
"""
|
|
97
99
|
import matplotlib.pyplot as plt
|
|
100
|
+
|
|
98
101
|
from ._reproducer import reproduce
|
|
99
102
|
from ._utils._image_diff import compare_images
|
|
100
103
|
|
|
@@ -115,7 +118,11 @@ def validate_recipe(
|
|
|
115
118
|
reproduced_fig.savefig(reproduced_path, dpi=dpi)
|
|
116
119
|
|
|
117
120
|
# Close reproduced figure to prevent double display in notebooks
|
|
118
|
-
|
|
121
|
+
# Use .fig to get underlying matplotlib Figure since reproduce() returns RecordingFigure
|
|
122
|
+
mpl_fig = (
|
|
123
|
+
reproduced_fig.fig if hasattr(reproduced_fig, "fig") else reproduced_fig
|
|
124
|
+
)
|
|
125
|
+
plt.close(mpl_fig)
|
|
119
126
|
|
|
120
127
|
# Compare images
|
|
121
128
|
diff = compare_images(original_path, reproduced_path)
|
|
@@ -137,7 +144,9 @@ def validate_recipe(
|
|
|
137
144
|
valid=valid,
|
|
138
145
|
mse=mse if not np.isnan(mse) else float("inf"),
|
|
139
146
|
psnr=diff["psnr"],
|
|
140
|
-
max_diff=diff["max_diff"]
|
|
147
|
+
max_diff=diff["max_diff"]
|
|
148
|
+
if not np.isnan(diff["max_diff"])
|
|
149
|
+
else float("inf"),
|
|
141
150
|
size_original=diff["size1"],
|
|
142
151
|
size_reproduced=diff["size2"],
|
|
143
152
|
same_size=diff["same_size"],
|