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,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 ._units import mm_to_inch, inch_to_mm, mm_to_pt, pt_to_mm
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(f"Content area: left={left}, upper={upper}, right={right}, lower={lower}")
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(key, str(value) if isinstance(value, bytes) else value)
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 - ((final_width * final_height) / (original_width * original_height))
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}")
@@ -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
- img = Image.open(path).convert('RGB')
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 ** 2)
56
+ mse = np.mean(diff**2)
58
57
 
59
58
  # Normalize diff for visualization
60
- diff_img = (diff / diff.max() * 255).astype(np.uint8) if diff.max() > 0 else diff.astype(np.uint8)
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('nan')
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('inf')
119
+ psnr = float("inf")
117
120
  elif np.isnan(mse):
118
- psnr = float('nan')
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('nan')
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
- 'identical': mse == 0,
135
- 'mse': float(mse),
136
- 'psnr': float(psnr),
137
- 'max_diff': float(max_diff),
138
- 'size1': (img1.shape[0], img1.shape[1]),
139
- 'size2': (img2.shape[0], img2.shape[1]),
140
- 'same_size': img1.shape == img2.shape,
141
- 'file_size1': file_size1,
142
- 'file_size2': file_size2,
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('Original')
177
- axes[0].axis('off')
180
+ axes[0].set_title("Original")
181
+ axes[0].axis("off")
178
182
  axes[1].imshow(img2)
179
- axes[1].set_title('Reproduced')
180
- axes[1].axis('off')
181
- fig.suptitle(f'{title}\n(Different sizes)', fontsize=14)
182
- fig.savefig(output_path, dpi=150, bbox_inches='tight', facecolor='white')
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('Original')
190
- axes[0].axis('off')
193
+ axes[0].set_title("Original")
194
+ axes[0].axis("off")
191
195
 
192
196
  axes[1].imshow(img2)
193
- axes[1].set_title('Reproduced')
194
- axes[1].axis('off')
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'Difference (MSE: {mse:.2f})')
198
- axes[2].axis('off')
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'{title}\n{status}', fontsize=14, fontweight='bold')
205
+ fig.suptitle(f"{title}\n{status}", fontsize=14, fontweight="bold")
202
206
 
203
- fig.savefig(output_path, dpi=150, bbox_inches='tight', facecolor='white')
207
+ fig.savefig(output_path, dpi=150, bbox_inches="tight", facecolor="white")
204
208
  plt.close(fig)
@@ -8,7 +8,6 @@ from typing import Literal, Union
8
8
 
9
9
  import numpy as np
10
10
 
11
-
12
11
  # Threshold for inline vs file storage (in elements)
13
12
  INLINE_THRESHOLD = 100
14
13
 
@@ -11,7 +11,14 @@ Constants:
11
11
  - 1 mm = 72/25.4 points
12
12
  """
13
13
 
14
- __all__ = ["mm_to_inch", "inch_to_mm", "mm_to_pt", "pt_to_mm", "mm_to_scatter_size", "normalize_color"]
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(f"\nColor normalization:")
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" if not np.isinf(self.psnr) else " PSNR: inf (identical)",
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
- plt.close(reproduced_fig)
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"] if not np.isnan(diff["max_diff"]) else float("inf"),
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"],