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.
- figrecipe/__init__.py +74 -76
- figrecipe/__main__.py +12 -0
- figrecipe/_api/_panel.py +67 -0
- figrecipe/_api/_save.py +100 -4
- figrecipe/_cli/__init__.py +7 -0
- figrecipe/_cli/_compose.py +87 -0
- figrecipe/_cli/_convert.py +117 -0
- figrecipe/_cli/_crop.py +82 -0
- figrecipe/_cli/_edit.py +70 -0
- figrecipe/_cli/_extract.py +128 -0
- figrecipe/_cli/_fonts.py +47 -0
- figrecipe/_cli/_info.py +67 -0
- figrecipe/_cli/_main.py +58 -0
- figrecipe/_cli/_reproduce.py +79 -0
- figrecipe/_cli/_style.py +77 -0
- figrecipe/_cli/_validate.py +66 -0
- figrecipe/_cli/_version.py +50 -0
- figrecipe/_composition/__init__.py +32 -0
- figrecipe/_composition/_alignment.py +452 -0
- figrecipe/_composition/_compose.py +179 -0
- figrecipe/_composition/_import_axes.py +127 -0
- figrecipe/_composition/_visibility.py +125 -0
- figrecipe/_dev/__init__.py +2 -0
- figrecipe/_dev/browser/__init__.py +69 -0
- figrecipe/_dev/browser/_audio.py +240 -0
- figrecipe/_dev/browser/_caption.py +356 -0
- figrecipe/_dev/browser/_click_effect.py +146 -0
- figrecipe/_dev/browser/_cursor.py +196 -0
- figrecipe/_dev/browser/_highlight.py +105 -0
- figrecipe/_dev/browser/_narration.py +237 -0
- figrecipe/_dev/browser/_recorder.py +446 -0
- figrecipe/_dev/browser/_utils.py +178 -0
- figrecipe/_dev/browser/_video_trim/__init__.py +152 -0
- figrecipe/_dev/browser/_video_trim/_detection.py +223 -0
- figrecipe/_dev/browser/_video_trim/_markers.py +140 -0
- figrecipe/_editor/__init__.py +36 -36
- figrecipe/_editor/_bbox/_extract.py +155 -9
- figrecipe/_editor/_bbox/_extract_text.py +124 -0
- figrecipe/_editor/_call_overrides.py +183 -0
- figrecipe/_editor/_datatable_plot_handlers.py +249 -0
- figrecipe/_editor/_figure_layout.py +211 -0
- figrecipe/_editor/_flask_app.py +157 -16
- figrecipe/_editor/_helpers.py +17 -8
- figrecipe/_editor/_hitmap/_detect.py +89 -32
- figrecipe/_editor/_hitmap_main.py +4 -4
- figrecipe/_editor/_overrides.py +4 -1
- figrecipe/_editor/_plot_types_registry.py +190 -0
- figrecipe/_editor/_render_overrides.py +38 -11
- figrecipe/_editor/_renderer.py +46 -1
- figrecipe/_editor/_routes_annotation.py +114 -0
- figrecipe/_editor/_routes_axis.py +35 -6
- figrecipe/_editor/_routes_captions.py +130 -0
- figrecipe/_editor/_routes_composition.py +270 -0
- figrecipe/_editor/_routes_core.py +15 -173
- figrecipe/_editor/_routes_datatable.py +364 -0
- figrecipe/_editor/_routes_element.py +37 -19
- figrecipe/_editor/_routes_files.py +443 -0
- figrecipe/_editor/_routes_image.py +200 -0
- figrecipe/_editor/_routes_snapshot.py +94 -0
- figrecipe/_editor/_routes_style.py +28 -8
- figrecipe/_editor/_templates/__init__.py +40 -2
- figrecipe/_editor/_templates/_html.py +97 -103
- figrecipe/_editor/_templates/_html_components/__init__.py +13 -0
- figrecipe/_editor/_templates/_html_components/_composition_toolbar.py +79 -0
- figrecipe/_editor/_templates/_html_components/_file_browser.py +41 -0
- figrecipe/_editor/_templates/_html_datatable.py +92 -0
- figrecipe/_editor/_templates/_scripts/__init__.py +58 -0
- figrecipe/_editor/_templates/_scripts/_accordion.py +328 -0
- figrecipe/_editor/_templates/_scripts/_annotation_drag.py +504 -0
- figrecipe/_editor/_templates/_scripts/_api.py +1 -1
- figrecipe/_editor/_templates/_scripts/_canvas_context_menu.py +182 -0
- figrecipe/_editor/_templates/_scripts/_captions.py +231 -0
- figrecipe/_editor/_templates/_scripts/_composition.py +283 -0
- figrecipe/_editor/_templates/_scripts/_core.py +94 -37
- figrecipe/_editor/_templates/_scripts/_datatable/__init__.py +59 -0
- figrecipe/_editor/_templates/_scripts/_datatable/_cell_edit.py +97 -0
- figrecipe/_editor/_templates/_scripts/_datatable/_clipboard.py +164 -0
- figrecipe/_editor/_templates/_scripts/_datatable/_context_menu.py +221 -0
- figrecipe/_editor/_templates/_scripts/_datatable/_core.py +150 -0
- figrecipe/_editor/_templates/_scripts/_datatable/_editable.py +511 -0
- figrecipe/_editor/_templates/_scripts/_datatable/_import.py +161 -0
- figrecipe/_editor/_templates/_scripts/_datatable/_plot.py +261 -0
- figrecipe/_editor/_templates/_scripts/_datatable/_selection.py +438 -0
- figrecipe/_editor/_templates/_scripts/_datatable/_table.py +256 -0
- figrecipe/_editor/_templates/_scripts/_datatable/_tabs.py +354 -0
- figrecipe/_editor/_templates/_scripts/_element_editor.py +17 -2
- figrecipe/_editor/_templates/_scripts/_files.py +274 -40
- figrecipe/_editor/_templates/_scripts/_files_context_menu.py +240 -0
- figrecipe/_editor/_templates/_scripts/_hitmap.py +87 -84
- figrecipe/_editor/_templates/_scripts/_image_drop.py +428 -0
- figrecipe/_editor/_templates/_scripts/_legend_drag.py +5 -0
- figrecipe/_editor/_templates/_scripts/_multi_select.py +198 -0
- figrecipe/_editor/_templates/_scripts/_panel_drag.py +219 -48
- figrecipe/_editor/_templates/_scripts/_panel_drag_snapshot.py +33 -0
- figrecipe/_editor/_templates/_scripts/_panel_position.py +238 -54
- figrecipe/_editor/_templates/_scripts/_panel_resize.py +230 -0
- figrecipe/_editor/_templates/_scripts/_panel_snap.py +307 -0
- figrecipe/_editor/_templates/_scripts/_region_select.py +255 -0
- figrecipe/_editor/_templates/_scripts/_selection.py +8 -1
- figrecipe/_editor/_templates/_scripts/_sync.py +242 -0
- figrecipe/_editor/_templates/_scripts/_undo_redo.py +348 -0
- figrecipe/_editor/_templates/_scripts/_zoom.py +52 -19
- figrecipe/_editor/_templates/_styles/__init__.py +9 -0
- figrecipe/_editor/_templates/_styles/_base.py +47 -0
- figrecipe/_editor/_templates/_styles/_buttons.py +127 -6
- figrecipe/_editor/_templates/_styles/_composition.py +87 -0
- figrecipe/_editor/_templates/_styles/_controls.py +168 -3
- figrecipe/_editor/_templates/_styles/_datatable/__init__.py +40 -0
- figrecipe/_editor/_templates/_styles/_datatable/_editable.py +203 -0
- figrecipe/_editor/_templates/_styles/_datatable/_panel.py +268 -0
- figrecipe/_editor/_templates/_styles/_datatable/_table.py +479 -0
- figrecipe/_editor/_templates/_styles/_datatable/_toolbar.py +384 -0
- figrecipe/_editor/_templates/_styles/_datatable/_vars.py +123 -0
- figrecipe/_editor/_templates/_styles/_dynamic_props.py +5 -5
- figrecipe/_editor/_templates/_styles/_file_browser.py +466 -0
- figrecipe/_editor/_templates/_styles/_forms.py +98 -0
- figrecipe/_editor/_templates/_styles/_hitmap.py +7 -0
- figrecipe/_editor/_templates/_styles/_modals.py +29 -0
- figrecipe/_editor/_templates/_styles/_overlays.py +5 -5
- figrecipe/_editor/_templates/_styles/_preview.py +213 -8
- figrecipe/_editor/_templates/_styles/_spinner.py +117 -0
- figrecipe/_editor/static/audio/click.mp3 +0 -0
- figrecipe/_editor/static/click.mp3 +0 -0
- figrecipe/_editor/static/icons/favicon.ico +0 -0
- figrecipe/_integrations/__init__.py +17 -0
- figrecipe/_integrations/_scitex_stats.py +298 -0
- figrecipe/_params/_DECORATION_METHODS.py +2 -0
- figrecipe/_recorder.py +28 -3
- figrecipe/_reproducer/_core.py +60 -49
- figrecipe/_utils/__init__.py +3 -0
- figrecipe/_utils/_bundle.py +205 -0
- figrecipe/_wrappers/_axes.py +150 -2
- figrecipe/_wrappers/_caption_generator.py +218 -0
- figrecipe/_wrappers/_figure.py +26 -1
- figrecipe/_wrappers/_stat_annotation.py +274 -0
- figrecipe/styles/_style_applier.py +10 -2
- figrecipe/styles/presets/SCITEX.yaml +11 -4
- {figrecipe-0.7.4.dist-info → figrecipe-0.9.0.dist-info}/METADATA +144 -146
- figrecipe-0.9.0.dist-info/RECORD +277 -0
- figrecipe-0.9.0.dist-info/entry_points.txt +2 -0
- figrecipe-0.7.4.dist-info/RECORD +0 -188
- {figrecipe-0.7.4.dist-info → figrecipe-0.9.0.dist-info}/WHEEL +0 -0
- {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"]
|