figrecipe 0.7.4__py3-none-any.whl → 0.9.0__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 (143) hide show
  1. figrecipe/__init__.py +74 -76
  2. figrecipe/__main__.py +12 -0
  3. figrecipe/_api/_panel.py +67 -0
  4. figrecipe/_api/_save.py +100 -4
  5. figrecipe/_cli/__init__.py +7 -0
  6. figrecipe/_cli/_compose.py +87 -0
  7. figrecipe/_cli/_convert.py +117 -0
  8. figrecipe/_cli/_crop.py +82 -0
  9. figrecipe/_cli/_edit.py +70 -0
  10. figrecipe/_cli/_extract.py +128 -0
  11. figrecipe/_cli/_fonts.py +47 -0
  12. figrecipe/_cli/_info.py +67 -0
  13. figrecipe/_cli/_main.py +58 -0
  14. figrecipe/_cli/_reproduce.py +79 -0
  15. figrecipe/_cli/_style.py +77 -0
  16. figrecipe/_cli/_validate.py +66 -0
  17. figrecipe/_cli/_version.py +50 -0
  18. figrecipe/_composition/__init__.py +32 -0
  19. figrecipe/_composition/_alignment.py +452 -0
  20. figrecipe/_composition/_compose.py +179 -0
  21. figrecipe/_composition/_import_axes.py +127 -0
  22. figrecipe/_composition/_visibility.py +125 -0
  23. figrecipe/_dev/__init__.py +2 -0
  24. figrecipe/_dev/browser/__init__.py +69 -0
  25. figrecipe/_dev/browser/_audio.py +240 -0
  26. figrecipe/_dev/browser/_caption.py +356 -0
  27. figrecipe/_dev/browser/_click_effect.py +146 -0
  28. figrecipe/_dev/browser/_cursor.py +196 -0
  29. figrecipe/_dev/browser/_highlight.py +105 -0
  30. figrecipe/_dev/browser/_narration.py +237 -0
  31. figrecipe/_dev/browser/_recorder.py +446 -0
  32. figrecipe/_dev/browser/_utils.py +178 -0
  33. figrecipe/_dev/browser/_video_trim/__init__.py +152 -0
  34. figrecipe/_dev/browser/_video_trim/_detection.py +223 -0
  35. figrecipe/_dev/browser/_video_trim/_markers.py +140 -0
  36. figrecipe/_editor/__init__.py +36 -36
  37. figrecipe/_editor/_bbox/_extract.py +155 -9
  38. figrecipe/_editor/_bbox/_extract_text.py +124 -0
  39. figrecipe/_editor/_call_overrides.py +183 -0
  40. figrecipe/_editor/_datatable_plot_handlers.py +249 -0
  41. figrecipe/_editor/_figure_layout.py +211 -0
  42. figrecipe/_editor/_flask_app.py +157 -16
  43. figrecipe/_editor/_helpers.py +17 -8
  44. figrecipe/_editor/_hitmap/_detect.py +89 -32
  45. figrecipe/_editor/_hitmap_main.py +4 -4
  46. figrecipe/_editor/_overrides.py +4 -1
  47. figrecipe/_editor/_plot_types_registry.py +190 -0
  48. figrecipe/_editor/_render_overrides.py +38 -11
  49. figrecipe/_editor/_renderer.py +46 -1
  50. figrecipe/_editor/_routes_annotation.py +114 -0
  51. figrecipe/_editor/_routes_axis.py +35 -6
  52. figrecipe/_editor/_routes_captions.py +130 -0
  53. figrecipe/_editor/_routes_composition.py +270 -0
  54. figrecipe/_editor/_routes_core.py +15 -173
  55. figrecipe/_editor/_routes_datatable.py +364 -0
  56. figrecipe/_editor/_routes_element.py +37 -19
  57. figrecipe/_editor/_routes_files.py +443 -0
  58. figrecipe/_editor/_routes_image.py +200 -0
  59. figrecipe/_editor/_routes_snapshot.py +94 -0
  60. figrecipe/_editor/_routes_style.py +28 -8
  61. figrecipe/_editor/_templates/__init__.py +40 -2
  62. figrecipe/_editor/_templates/_html.py +97 -103
  63. figrecipe/_editor/_templates/_html_components/__init__.py +13 -0
  64. figrecipe/_editor/_templates/_html_components/_composition_toolbar.py +79 -0
  65. figrecipe/_editor/_templates/_html_components/_file_browser.py +41 -0
  66. figrecipe/_editor/_templates/_html_datatable.py +92 -0
  67. figrecipe/_editor/_templates/_scripts/__init__.py +58 -0
  68. figrecipe/_editor/_templates/_scripts/_accordion.py +328 -0
  69. figrecipe/_editor/_templates/_scripts/_annotation_drag.py +504 -0
  70. figrecipe/_editor/_templates/_scripts/_api.py +1 -1
  71. figrecipe/_editor/_templates/_scripts/_canvas_context_menu.py +182 -0
  72. figrecipe/_editor/_templates/_scripts/_captions.py +231 -0
  73. figrecipe/_editor/_templates/_scripts/_composition.py +283 -0
  74. figrecipe/_editor/_templates/_scripts/_core.py +94 -37
  75. figrecipe/_editor/_templates/_scripts/_datatable/__init__.py +59 -0
  76. figrecipe/_editor/_templates/_scripts/_datatable/_cell_edit.py +97 -0
  77. figrecipe/_editor/_templates/_scripts/_datatable/_clipboard.py +164 -0
  78. figrecipe/_editor/_templates/_scripts/_datatable/_context_menu.py +221 -0
  79. figrecipe/_editor/_templates/_scripts/_datatable/_core.py +150 -0
  80. figrecipe/_editor/_templates/_scripts/_datatable/_editable.py +511 -0
  81. figrecipe/_editor/_templates/_scripts/_datatable/_import.py +161 -0
  82. figrecipe/_editor/_templates/_scripts/_datatable/_plot.py +261 -0
  83. figrecipe/_editor/_templates/_scripts/_datatable/_selection.py +438 -0
  84. figrecipe/_editor/_templates/_scripts/_datatable/_table.py +256 -0
  85. figrecipe/_editor/_templates/_scripts/_datatable/_tabs.py +354 -0
  86. figrecipe/_editor/_templates/_scripts/_element_editor.py +17 -2
  87. figrecipe/_editor/_templates/_scripts/_files.py +274 -40
  88. figrecipe/_editor/_templates/_scripts/_files_context_menu.py +240 -0
  89. figrecipe/_editor/_templates/_scripts/_hitmap.py +87 -84
  90. figrecipe/_editor/_templates/_scripts/_image_drop.py +428 -0
  91. figrecipe/_editor/_templates/_scripts/_legend_drag.py +5 -0
  92. figrecipe/_editor/_templates/_scripts/_multi_select.py +198 -0
  93. figrecipe/_editor/_templates/_scripts/_panel_drag.py +219 -48
  94. figrecipe/_editor/_templates/_scripts/_panel_drag_snapshot.py +33 -0
  95. figrecipe/_editor/_templates/_scripts/_panel_position.py +238 -54
  96. figrecipe/_editor/_templates/_scripts/_panel_resize.py +230 -0
  97. figrecipe/_editor/_templates/_scripts/_panel_snap.py +307 -0
  98. figrecipe/_editor/_templates/_scripts/_region_select.py +255 -0
  99. figrecipe/_editor/_templates/_scripts/_selection.py +8 -1
  100. figrecipe/_editor/_templates/_scripts/_sync.py +242 -0
  101. figrecipe/_editor/_templates/_scripts/_undo_redo.py +348 -0
  102. figrecipe/_editor/_templates/_scripts/_zoom.py +52 -19
  103. figrecipe/_editor/_templates/_styles/__init__.py +9 -0
  104. figrecipe/_editor/_templates/_styles/_base.py +47 -0
  105. figrecipe/_editor/_templates/_styles/_buttons.py +127 -6
  106. figrecipe/_editor/_templates/_styles/_composition.py +87 -0
  107. figrecipe/_editor/_templates/_styles/_controls.py +168 -3
  108. figrecipe/_editor/_templates/_styles/_datatable/__init__.py +40 -0
  109. figrecipe/_editor/_templates/_styles/_datatable/_editable.py +203 -0
  110. figrecipe/_editor/_templates/_styles/_datatable/_panel.py +268 -0
  111. figrecipe/_editor/_templates/_styles/_datatable/_table.py +479 -0
  112. figrecipe/_editor/_templates/_styles/_datatable/_toolbar.py +384 -0
  113. figrecipe/_editor/_templates/_styles/_datatable/_vars.py +123 -0
  114. figrecipe/_editor/_templates/_styles/_dynamic_props.py +5 -5
  115. figrecipe/_editor/_templates/_styles/_file_browser.py +466 -0
  116. figrecipe/_editor/_templates/_styles/_forms.py +98 -0
  117. figrecipe/_editor/_templates/_styles/_hitmap.py +7 -0
  118. figrecipe/_editor/_templates/_styles/_modals.py +29 -0
  119. figrecipe/_editor/_templates/_styles/_overlays.py +5 -5
  120. figrecipe/_editor/_templates/_styles/_preview.py +213 -8
  121. figrecipe/_editor/_templates/_styles/_spinner.py +117 -0
  122. figrecipe/_editor/static/audio/click.mp3 +0 -0
  123. figrecipe/_editor/static/click.mp3 +0 -0
  124. figrecipe/_editor/static/icons/favicon.ico +0 -0
  125. figrecipe/_integrations/__init__.py +17 -0
  126. figrecipe/_integrations/_scitex_stats.py +298 -0
  127. figrecipe/_params/_DECORATION_METHODS.py +2 -0
  128. figrecipe/_recorder.py +28 -3
  129. figrecipe/_reproducer/_core.py +60 -49
  130. figrecipe/_utils/__init__.py +3 -0
  131. figrecipe/_utils/_bundle.py +205 -0
  132. figrecipe/_wrappers/_axes.py +150 -2
  133. figrecipe/_wrappers/_caption_generator.py +218 -0
  134. figrecipe/_wrappers/_figure.py +26 -1
  135. figrecipe/_wrappers/_stat_annotation.py +274 -0
  136. figrecipe/styles/_style_applier.py +10 -2
  137. figrecipe/styles/presets/SCITEX.yaml +11 -4
  138. {figrecipe-0.7.4.dist-info → figrecipe-0.9.0.dist-info}/METADATA +144 -146
  139. figrecipe-0.9.0.dist-info/RECORD +277 -0
  140. figrecipe-0.9.0.dist-info/entry_points.txt +2 -0
  141. figrecipe-0.7.4.dist-info/RECORD +0 -188
  142. {figrecipe-0.7.4.dist-info → figrecipe-0.9.0.dist-info}/WHEEL +0 -0
  143. {figrecipe-0.7.4.dist-info → figrecipe-0.9.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,249 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """Plot handlers for datatable plotting functionality."""
4
+
5
+ import numpy as np
6
+
7
+
8
+ def dispatch_plot(ax, plot_type, plot_data, columns):
9
+ """Dispatch plot based on type and data.
10
+
11
+ Args:
12
+ ax: Matplotlib axes object
13
+ plot_type: Frontend plot type name
14
+ plot_data: Dict mapping column names to data arrays
15
+ columns: List of column names in order
16
+
17
+ Returns:
18
+ True on success
19
+
20
+ Raises:
21
+ ValueError: If plot type is unknown
22
+ """
23
+ # Map frontend names to matplotlib method names
24
+ method_name = plot_type
25
+ if plot_type == "line":
26
+ method_name = "plot"
27
+ elif plot_type == "histogram":
28
+ method_name = "hist"
29
+
30
+ # Get the plotting method
31
+ plot_method = getattr(ax, method_name, None)
32
+ if plot_method is None:
33
+ raise ValueError(f"Unknown plot type: {plot_type}")
34
+
35
+ # Prepare data arrays
36
+ data_arrays = [np.array(plot_data.get(c, [])) for c in columns]
37
+
38
+ # Handle decoration methods
39
+ if _handle_decoration(ax, method_name, data_arrays):
40
+ return True
41
+
42
+ # Handle specialized plot types
43
+ if _handle_specialized(ax, plot_method, method_name, data_arrays, columns):
44
+ return True
45
+
46
+ # Handle standard xy plots
47
+ _handle_standard_xy(ax, plot_method, method_name, data_arrays, columns)
48
+ return True
49
+
50
+
51
+ def _handle_decoration(ax, method_name, data_arrays):
52
+ """Handle decoration methods (scalar-based, iterate over rows)."""
53
+ decoration_methods = {
54
+ "text",
55
+ "annotate",
56
+ "arrow",
57
+ "axhline",
58
+ "axvline",
59
+ "axhspan",
60
+ "axvspan",
61
+ }
62
+
63
+ if method_name not in decoration_methods:
64
+ return False
65
+
66
+ n_rows = len(data_arrays[0]) if data_arrays else 0
67
+
68
+ for row_idx in range(n_rows):
69
+ row_vals = [arr[row_idx] for arr in data_arrays]
70
+
71
+ if method_name == "text" and len(row_vals) >= 2:
72
+ s = str(row_vals[2]) if len(row_vals) >= 3 else ""
73
+ ax.text(row_vals[0], row_vals[1], s)
74
+
75
+ elif method_name == "annotate":
76
+ if len(row_vals) >= 3:
77
+ ax.annotate(str(row_vals[0]), xy=(row_vals[1], row_vals[2]))
78
+ elif len(row_vals) == 2:
79
+ ax.annotate("", xy=(row_vals[0], row_vals[1]))
80
+
81
+ elif method_name == "arrow" and len(row_vals) >= 4:
82
+ ax.arrow(
83
+ row_vals[0],
84
+ row_vals[1],
85
+ row_vals[2],
86
+ row_vals[3],
87
+ head_width=0.1,
88
+ head_length=0.05,
89
+ )
90
+
91
+ elif method_name == "axhline" and len(row_vals) >= 1:
92
+ ax.axhline(y=row_vals[0])
93
+
94
+ elif method_name == "axvline" and len(row_vals) >= 1:
95
+ ax.axvline(x=row_vals[0])
96
+
97
+ elif method_name == "axhspan" and len(row_vals) >= 2:
98
+ ax.axhspan(row_vals[0], row_vals[1], alpha=0.3)
99
+
100
+ elif method_name == "axvspan" and len(row_vals) >= 2:
101
+ ax.axvspan(row_vals[0], row_vals[1], alpha=0.3)
102
+
103
+ return True
104
+
105
+
106
+ def _handle_specialized(ax, plot_method, method_name, data_arrays, columns):
107
+ """Handle specialized plot types that need custom argument handling."""
108
+ if method_name in ("boxplot", "violinplot"):
109
+ if method_name == "boxplot":
110
+ plot_method(data_arrays, labels=columns)
111
+ else:
112
+ plot_method(data_arrays)
113
+ ax.set_xticks(range(1, len(columns) + 1))
114
+ ax.set_xticklabels(columns)
115
+ return True
116
+
117
+ if method_name == "pie":
118
+ labels = columns[1:] if len(columns) > 1 else None
119
+ plot_method(data_arrays[0], labels=labels, autopct="%1.1f%%")
120
+ return True
121
+
122
+ if method_name in (
123
+ "hist",
124
+ "acorr",
125
+ "psd",
126
+ "specgram",
127
+ "angle_spectrum",
128
+ "phase_spectrum",
129
+ "magnitude_spectrum",
130
+ ):
131
+ for i, arr in enumerate(data_arrays):
132
+ plot_method(arr, label=columns[i])
133
+ return True
134
+
135
+ if method_name in ("hist2d", "hexbin", "xcorr", "csd", "cohere"):
136
+ if len(data_arrays) >= 2:
137
+ plot_method(data_arrays[0], data_arrays[1])
138
+ return True
139
+
140
+ if method_name in ("fill_between", "fill_betweenx"):
141
+ if len(data_arrays) >= 3:
142
+ plot_method(data_arrays[0], data_arrays[1], data_arrays[2], alpha=0.5)
143
+ elif len(data_arrays) >= 2:
144
+ plot_method(data_arrays[0], data_arrays[1], alpha=0.5)
145
+ return True
146
+
147
+ if method_name == "errorbar" and len(data_arrays) >= 3:
148
+ plot_method(
149
+ data_arrays[0], data_arrays[1], yerr=data_arrays[2], fmt="o-", capsize=3
150
+ )
151
+ return True
152
+
153
+ if method_name in ("imshow", "matshow"):
154
+ if len(data_arrays) == 1:
155
+ arr = data_arrays[0]
156
+ plot_method(arr.reshape(-1, 1) if arr.ndim == 1 else arr)
157
+ else:
158
+ plot_method(np.column_stack(data_arrays))
159
+ return True
160
+
161
+ if method_name in ("contour", "contourf", "pcolor", "pcolormesh"):
162
+ if len(data_arrays) == 1:
163
+ arr = data_arrays[0]
164
+ plot_method(arr.reshape(-1, 1) if arr.ndim == 1 else arr)
165
+ else:
166
+ plot_method(np.column_stack(data_arrays))
167
+ return True
168
+
169
+ if method_name in ("quiver", "barbs", "streamplot"):
170
+ if len(data_arrays) >= 4:
171
+ plot_method(data_arrays[0], data_arrays[1], data_arrays[2], data_arrays[3])
172
+ elif len(data_arrays) >= 2:
173
+ x = np.arange(len(data_arrays[0]))
174
+ y = np.arange(len(data_arrays[0]))
175
+ plot_method(x, y, data_arrays[0], data_arrays[1])
176
+ return True
177
+
178
+ if method_name == "eventplot":
179
+ plot_method(data_arrays)
180
+ return True
181
+
182
+ return False
183
+
184
+
185
+ def _handle_standard_xy(ax, plot_method, method_name, data_arrays, columns):
186
+ """Handle standard x, y plots."""
187
+ # Detect x and y columns
188
+ x_idx = None
189
+ y_indices = []
190
+ for i, col in enumerate(columns):
191
+ if col.endswith("_x") or col.lower() == "x":
192
+ x_idx = i
193
+ else:
194
+ y_indices.append(i)
195
+
196
+ if x_idx is not None and y_indices:
197
+ x_data = data_arrays[x_idx]
198
+ y_arrays = [data_arrays[i] for i in y_indices]
199
+ y_cols = [columns[i] for i in y_indices]
200
+ elif len(data_arrays) >= 2:
201
+ x_data = data_arrays[0]
202
+ y_arrays = data_arrays[1:]
203
+ y_cols = columns[1:]
204
+ else:
205
+ x_data = np.arange(len(data_arrays[0]))
206
+ y_arrays = data_arrays
207
+ y_cols = columns
208
+
209
+ for i, y_data in enumerate(y_arrays):
210
+ if method_name == "bar" and len(y_arrays) > 1:
211
+ width = 0.8 / len(y_arrays)
212
+ offset = (i - len(y_arrays) / 2 + 0.5) * width
213
+ plot_method(x_data + offset, y_data, width=width, label=y_cols[i])
214
+ elif method_name == "barh":
215
+ plot_method(x_data, y_data, label=y_cols[i])
216
+ elif method_name in (
217
+ "plot",
218
+ "scatter",
219
+ "step",
220
+ "loglog",
221
+ "semilogx",
222
+ "semilogy",
223
+ ):
224
+ plot_method(x_data, y_data, label=y_cols[i])
225
+ elif method_name == "stem":
226
+ plot_method(x_data, y_data, label=y_cols[i])
227
+ elif method_name == "fill":
228
+ plot_method(x_data, y_data, alpha=0.5, label=y_cols[i])
229
+ elif method_name == "stairs":
230
+ plot_method(y_data, label=y_cols[i])
231
+ elif method_name == "stackplot":
232
+ plot_method(x_data, *y_arrays, labels=y_cols)
233
+ break
234
+ else:
235
+ try:
236
+ plot_method(x_data, y_data, label=y_cols[i])
237
+ except TypeError:
238
+ plot_method(y_data, label=y_cols[i])
239
+
240
+ # Set labels
241
+ if len(columns) >= 2:
242
+ ax.set_xlabel(columns[0])
243
+ if len(y_cols) == 1:
244
+ ax.set_ylabel(y_cols[0])
245
+ if len(y_cols) > 1:
246
+ ax.legend()
247
+
248
+
249
+ __all__ = ["dispatch_plot"]
@@ -0,0 +1,211 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """Figure-level layout handling for the editor.
4
+
5
+ Applies spacing and margin overrides to figure layout via subplots_adjust.
6
+ """
7
+
8
+ from typing import Any, Dict, Optional
9
+
10
+ from matplotlib.figure import Figure
11
+
12
+
13
+ def apply_figure_layout_overrides(
14
+ fig: Figure, overrides: Dict[str, Any], record: Optional[Any] = None
15
+ ) -> None:
16
+ """Apply figure-level layout overrides (spacing, margins).
17
+
18
+ Converts mm-based spacing and margin values to subplots_adjust parameters.
19
+
20
+ Parameters
21
+ ----------
22
+ fig : Figure
23
+ Matplotlib figure.
24
+ overrides : dict
25
+ Style overrides containing spacing_horizontal_mm, spacing_vertical_mm,
26
+ margins_left_mm, margins_right_mm, margins_bottom_mm, margins_top_mm.
27
+ record : FigureRecord, optional
28
+ Recording record to get original layout and axes dimensions.
29
+ """
30
+ # Check if any layout overrides are present
31
+ spacing_h = overrides.get("spacing_horizontal_mm")
32
+ spacing_v = overrides.get("spacing_vertical_mm")
33
+ margin_l = overrides.get("margins_left_mm")
34
+ margin_r = overrides.get("margins_right_mm")
35
+ margin_b = overrides.get("margins_bottom_mm")
36
+ margin_t = overrides.get("margins_top_mm")
37
+
38
+ has_layout_overrides = any(
39
+ v is not None
40
+ for v in [spacing_h, spacing_v, margin_l, margin_r, margin_b, margin_t]
41
+ )
42
+
43
+ if not has_layout_overrides:
44
+ return
45
+
46
+ # Skip if figure uses constrained_layout (incompatible with subplots_adjust)
47
+ layout_engine = fig.get_layout_engine()
48
+ if layout_engine is not None:
49
+ layout_name = getattr(layout_engine, "__class__", type(layout_engine)).__name__
50
+ if "Constrained" in layout_name:
51
+ return
52
+
53
+ # Get current figure size in inches
54
+ fig_width_inch, fig_height_inch = fig.get_size_inches()
55
+
56
+ # Convert to mm (1 inch = 25.4 mm)
57
+ fig_width_mm = fig_width_inch * 25.4
58
+ fig_height_mm = fig_height_inch * 25.4
59
+
60
+ # Determine grid dimensions from axes
61
+ axes_list = fig.get_axes()
62
+ if not axes_list:
63
+ return
64
+
65
+ nrows, ncols = _get_grid_dimensions(record, axes_list)
66
+
67
+ # Get current layout parameters
68
+ current_params = fig.subplotpars
69
+ current_left = current_params.left
70
+ current_right = current_params.right
71
+ current_bottom = current_params.bottom
72
+ current_top = current_params.top
73
+ current_wspace = current_params.wspace
74
+ current_hspace = current_params.hspace
75
+
76
+ # Calculate current mm values from figure layout
77
+ current_ml_mm = current_left * fig_width_mm
78
+ current_mr_mm = (1 - current_right) * fig_width_mm
79
+ current_mb_mm = current_bottom * fig_height_mm
80
+ current_mt_mm = (1 - current_top) * fig_height_mm
81
+
82
+ # Calculate axes dimensions
83
+ aw, ah = _calculate_axes_dimensions(
84
+ fig_width_mm,
85
+ fig_height_mm,
86
+ current_ml_mm,
87
+ current_mr_mm,
88
+ current_mb_mm,
89
+ current_mt_mm,
90
+ current_wspace,
91
+ current_hspace,
92
+ nrows,
93
+ ncols,
94
+ )
95
+
96
+ # Apply overrides (use current values as fallback)
97
+ ml = margin_l if margin_l is not None else current_ml_mm
98
+ mr = margin_r if margin_r is not None else current_mr_mm
99
+ mb = margin_b if margin_b is not None else current_mb_mm
100
+ mt = margin_t if margin_t is not None else current_mt_mm
101
+ sw = (
102
+ spacing_h
103
+ if spacing_h is not None
104
+ else (current_wspace * aw if ncols > 1 else 0)
105
+ )
106
+ sh = (
107
+ spacing_v
108
+ if spacing_v is not None
109
+ else (current_hspace * ah if nrows > 1 else 0)
110
+ )
111
+
112
+ # Calculate new layout parameters
113
+ left = ml / fig_width_mm
114
+ right = 1 - (mr / fig_width_mm)
115
+ bottom = mb / fig_height_mm
116
+ top = 1 - (mt / fig_height_mm)
117
+
118
+ # Calculate wspace/hspace as fraction of axes dimensions
119
+ wspace = sw / aw if ncols > 1 and aw > 0 else 0
120
+ hspace = sh / ah if nrows > 1 and ah > 0 else 0
121
+
122
+ # Apply the layout (suppress warning about layout engine incompatibility
123
+ # since we've already checked and skipped constrained_layout figures)
124
+ import warnings
125
+
126
+ with warnings.catch_warnings():
127
+ warnings.filterwarnings("ignore", "This figure was using a layout engine")
128
+ fig.subplots_adjust(
129
+ left=left,
130
+ right=right,
131
+ bottom=bottom,
132
+ top=top,
133
+ wspace=wspace,
134
+ hspace=hspace,
135
+ )
136
+
137
+
138
+ def _get_grid_dimensions(record: Optional[Any], axes_list: list) -> tuple:
139
+ """Get grid dimensions from record or infer from axes.
140
+
141
+ Returns
142
+ -------
143
+ tuple
144
+ (nrows, ncols)
145
+ """
146
+ nrows, ncols = 1, 1
147
+
148
+ if record and hasattr(record, "axes"):
149
+ for ax_key in record.axes.keys():
150
+ parts = ax_key.split("_")
151
+ if len(parts) >= 3:
152
+ nrows = max(nrows, int(parts[1]) + 1)
153
+ ncols = max(ncols, int(parts[2]) + 1)
154
+ else:
155
+ # Infer from axes count (assume square-ish grid)
156
+ import math
157
+
158
+ n_axes = len(axes_list)
159
+ ncols = int(math.ceil(math.sqrt(n_axes)))
160
+ nrows = int(math.ceil(n_axes / ncols))
161
+
162
+ return nrows, ncols
163
+
164
+
165
+ def _calculate_axes_dimensions(
166
+ fig_width_mm: float,
167
+ fig_height_mm: float,
168
+ ml_mm: float,
169
+ mr_mm: float,
170
+ mb_mm: float,
171
+ mt_mm: float,
172
+ wspace: float,
173
+ hspace: float,
174
+ nrows: int,
175
+ ncols: int,
176
+ ) -> tuple:
177
+ """Calculate single axes dimensions from figure layout.
178
+
179
+ Returns
180
+ -------
181
+ tuple
182
+ (axes_width_mm, axes_height_mm)
183
+ """
184
+ content_width_mm = fig_width_mm - ml_mm - mr_mm
185
+ content_height_mm = fig_height_mm - mb_mm - mt_mm
186
+
187
+ # wspace/hspace are fractions of axes width/height
188
+ # total_width = ncols * aw + (ncols-1) * sw = ncols * aw + (ncols-1) * wspace * aw
189
+ # total_width = aw * (ncols + (ncols-1) * wspace)
190
+ if ncols > 1:
191
+ aw = (
192
+ content_width_mm / (ncols + (ncols - 1) * wspace)
193
+ if ncols > 0
194
+ else content_width_mm
195
+ )
196
+ else:
197
+ aw = content_width_mm
198
+
199
+ if nrows > 1:
200
+ ah = (
201
+ content_height_mm / (nrows + (nrows - 1) * hspace)
202
+ if nrows > 0
203
+ else content_height_mm
204
+ )
205
+ else:
206
+ ah = content_height_mm
207
+
208
+ return aw, ah
209
+
210
+
211
+ __all__ = ["apply_figure_layout_overrides"]