figrecipe 0.6.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 +106 -973
- 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 +2 -93
- figrecipe/_dev/_plotters.py +76 -0
- figrecipe/_dev/_run_demos.py +56 -0
- figrecipe/_dev/demo_plotters/__init__.py +35 -166
- 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/contour_surface/__init__.py +4 -0
- figrecipe/_dev/demo_plotters/distribution/__init__.py +4 -0
- figrecipe/_dev/demo_plotters/image_matrix/__init__.py +4 -0
- figrecipe/_dev/demo_plotters/line_curve/__init__.py +4 -0
- figrecipe/_dev/demo_plotters/{plot_plot.py → line_curve/plot_plot.py} +3 -2
- figrecipe/_dev/demo_plotters/scatter_points/__init__.py +4 -0
- figrecipe/_dev/demo_plotters/special/__init__.py +4 -0
- figrecipe/_dev/demo_plotters/{plot_pie.py → special/plot_pie.py} +5 -1
- figrecipe/_dev/demo_plotters/spectral_signal/__init__.py +4 -0
- figrecipe/_dev/demo_plotters/vector_flow/__init__.py +4 -0
- figrecipe/_editor/__init__.py +57 -9
- 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 +68 -1039
- 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/_preferences.py +135 -0
- figrecipe/_editor/_render_overrides.py +480 -0
- figrecipe/_editor/_renderer.py +35 -185
- 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 +78 -1
- figrecipe/_editor/_templates/_html.py +109 -13
- 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 +6 -0
- figrecipe/_recorder.py +35 -106
- 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/_signatures/_kwargs.py +273 -0
- figrecipe/_signatures/_loader.py +21 -423
- figrecipe/_signatures/_parsing.py +147 -0
- figrecipe/_wrappers/_axes.py +119 -910
- 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 +162 -0
- figrecipe/_wrappers/_panel_labels.py +127 -0
- figrecipe/_wrappers/_plot_helpers.py +143 -0
- figrecipe/_wrappers/_violin_helpers.py +180 -0
- figrecipe/styles/__init__.py +8 -6
- 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 +32 -478
- figrecipe/styles/_style_loader.py +16 -192
- figrecipe/styles/_themes.py +151 -0
- figrecipe/styles/presets/MATPLOTLIB.yaml +2 -1
- figrecipe/styles/presets/SCITEX.yaml +29 -24
- {figrecipe-0.6.0.dist-info → figrecipe-0.7.4.dist-info}/METADATA +37 -2
- figrecipe-0.7.4.dist-info/RECORD +188 -0
- figrecipe/_editor/_bbox.py +0 -978
- figrecipe/_editor/_hitmap.py +0 -937
- figrecipe/_editor/_templates/_scripts.py +0 -2778
- figrecipe/_editor/_templates/_styles.py +0 -1326
- figrecipe/_reproducer.py +0 -975
- figrecipe-0.6.0.dist-info/RECORD +0 -90
- /figrecipe/_dev/demo_plotters/{plot_bar.py → bar_categorical/plot_bar.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_barh.py → bar_categorical/plot_barh.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_contour.py → contour_surface/plot_contour.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_contourf.py → contour_surface/plot_contourf.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_tricontour.py → contour_surface/plot_tricontour.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_tricontourf.py → contour_surface/plot_tricontourf.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_tripcolor.py → contour_surface/plot_tripcolor.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_triplot.py → contour_surface/plot_triplot.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_boxplot.py → distribution/plot_boxplot.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_ecdf.py → distribution/plot_ecdf.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_hist.py → distribution/plot_hist.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_hist2d.py → distribution/plot_hist2d.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_violinplot.py → distribution/plot_violinplot.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_hexbin.py → image_matrix/plot_hexbin.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_imshow.py → image_matrix/plot_imshow.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_matshow.py → image_matrix/plot_matshow.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_pcolor.py → image_matrix/plot_pcolor.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_pcolormesh.py → image_matrix/plot_pcolormesh.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_spy.py → image_matrix/plot_spy.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_errorbar.py → line_curve/plot_errorbar.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_fill.py → line_curve/plot_fill.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_fill_between.py → line_curve/plot_fill_between.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_fill_betweenx.py → line_curve/plot_fill_betweenx.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_stackplot.py → line_curve/plot_stackplot.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_stairs.py → line_curve/plot_stairs.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_step.py → line_curve/plot_step.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_scatter.py → scatter_points/plot_scatter.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_eventplot.py → special/plot_eventplot.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_loglog.py → special/plot_loglog.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_semilogx.py → special/plot_semilogx.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_semilogy.py → special/plot_semilogy.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_stem.py → special/plot_stem.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_acorr.py → spectral_signal/plot_acorr.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_angle_spectrum.py → spectral_signal/plot_angle_spectrum.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_cohere.py → spectral_signal/plot_cohere.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_csd.py → spectral_signal/plot_csd.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_magnitude_spectrum.py → spectral_signal/plot_magnitude_spectrum.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_phase_spectrum.py → spectral_signal/plot_phase_spectrum.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_psd.py → spectral_signal/plot_psd.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_specgram.py → spectral_signal/plot_specgram.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_xcorr.py → spectral_signal/plot_xcorr.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_barbs.py → vector_flow/plot_barbs.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_quiver.py → vector_flow/plot_quiver.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_streamplot.py → vector_flow/plot_streamplot.py} +0 -0
- {figrecipe-0.6.0.dist-info → figrecipe-0.7.4.dist-info}/WHEEL +0 -0
- {figrecipe-0.6.0.dist-info → figrecipe-0.7.4.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""Text, legend, spine, and figure text bbox extraction."""
|
|
4
|
+
|
|
5
|
+
from ._elements import get_text_bbox, get_tick_labels_bbox
|
|
6
|
+
from ._transforms import transform_bbox
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def extract_text_elements(
|
|
10
|
+
ax,
|
|
11
|
+
ax_idx,
|
|
12
|
+
fig,
|
|
13
|
+
renderer,
|
|
14
|
+
tight_bbox,
|
|
15
|
+
img_width,
|
|
16
|
+
img_height,
|
|
17
|
+
scale_x,
|
|
18
|
+
scale_y,
|
|
19
|
+
pad_inches,
|
|
20
|
+
saved_height_inches,
|
|
21
|
+
bboxes,
|
|
22
|
+
):
|
|
23
|
+
"""Extract bboxes for text elements (title, labels, ticks)."""
|
|
24
|
+
# Title
|
|
25
|
+
title = ax.get_title()
|
|
26
|
+
if title:
|
|
27
|
+
bbox = get_text_bbox(
|
|
28
|
+
ax.title,
|
|
29
|
+
fig,
|
|
30
|
+
renderer,
|
|
31
|
+
tight_bbox,
|
|
32
|
+
img_width,
|
|
33
|
+
img_height,
|
|
34
|
+
scale_x,
|
|
35
|
+
scale_y,
|
|
36
|
+
pad_inches,
|
|
37
|
+
saved_height_inches,
|
|
38
|
+
)
|
|
39
|
+
if bbox:
|
|
40
|
+
bboxes[f"ax{ax_idx}_title"] = {
|
|
41
|
+
**bbox,
|
|
42
|
+
"type": "title",
|
|
43
|
+
"label": "title",
|
|
44
|
+
"ax_index": ax_idx,
|
|
45
|
+
"text": title,
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
# X label
|
|
49
|
+
xlabel = ax.get_xlabel()
|
|
50
|
+
if xlabel:
|
|
51
|
+
bbox = get_text_bbox(
|
|
52
|
+
ax.xaxis.label,
|
|
53
|
+
fig,
|
|
54
|
+
renderer,
|
|
55
|
+
tight_bbox,
|
|
56
|
+
img_width,
|
|
57
|
+
img_height,
|
|
58
|
+
scale_x,
|
|
59
|
+
scale_y,
|
|
60
|
+
pad_inches,
|
|
61
|
+
saved_height_inches,
|
|
62
|
+
)
|
|
63
|
+
if bbox:
|
|
64
|
+
bboxes[f"ax{ax_idx}_xlabel"] = {
|
|
65
|
+
**bbox,
|
|
66
|
+
"type": "xlabel",
|
|
67
|
+
"label": "xlabel",
|
|
68
|
+
"ax_index": ax_idx,
|
|
69
|
+
"text": xlabel,
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
# X tick labels
|
|
73
|
+
xtick_bbox = get_tick_labels_bbox(
|
|
74
|
+
ax.xaxis,
|
|
75
|
+
"x",
|
|
76
|
+
fig,
|
|
77
|
+
renderer,
|
|
78
|
+
tight_bbox,
|
|
79
|
+
img_width,
|
|
80
|
+
img_height,
|
|
81
|
+
scale_x,
|
|
82
|
+
scale_y,
|
|
83
|
+
pad_inches,
|
|
84
|
+
saved_height_inches,
|
|
85
|
+
)
|
|
86
|
+
if xtick_bbox:
|
|
87
|
+
bboxes[f"ax{ax_idx}_xticks"] = {
|
|
88
|
+
**xtick_bbox,
|
|
89
|
+
"type": "xticks",
|
|
90
|
+
"label": "x tick labels",
|
|
91
|
+
"ax_index": ax_idx,
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
# Y label
|
|
95
|
+
ylabel = ax.get_ylabel()
|
|
96
|
+
if ylabel:
|
|
97
|
+
bbox = get_text_bbox(
|
|
98
|
+
ax.yaxis.label,
|
|
99
|
+
fig,
|
|
100
|
+
renderer,
|
|
101
|
+
tight_bbox,
|
|
102
|
+
img_width,
|
|
103
|
+
img_height,
|
|
104
|
+
scale_x,
|
|
105
|
+
scale_y,
|
|
106
|
+
pad_inches,
|
|
107
|
+
saved_height_inches,
|
|
108
|
+
)
|
|
109
|
+
if bbox:
|
|
110
|
+
bboxes[f"ax{ax_idx}_ylabel"] = {
|
|
111
|
+
**bbox,
|
|
112
|
+
"type": "ylabel",
|
|
113
|
+
"label": "ylabel",
|
|
114
|
+
"ax_index": ax_idx,
|
|
115
|
+
"text": ylabel,
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
# Y tick labels
|
|
119
|
+
ytick_bbox = get_tick_labels_bbox(
|
|
120
|
+
ax.yaxis,
|
|
121
|
+
"y",
|
|
122
|
+
fig,
|
|
123
|
+
renderer,
|
|
124
|
+
tight_bbox,
|
|
125
|
+
img_width,
|
|
126
|
+
img_height,
|
|
127
|
+
scale_x,
|
|
128
|
+
scale_y,
|
|
129
|
+
pad_inches,
|
|
130
|
+
saved_height_inches,
|
|
131
|
+
)
|
|
132
|
+
if ytick_bbox:
|
|
133
|
+
bboxes[f"ax{ax_idx}_yticks"] = {
|
|
134
|
+
**ytick_bbox,
|
|
135
|
+
"type": "yticks",
|
|
136
|
+
"label": "y tick labels",
|
|
137
|
+
"ax_index": ax_idx,
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def extract_legend(
|
|
142
|
+
ax,
|
|
143
|
+
ax_idx,
|
|
144
|
+
fig,
|
|
145
|
+
renderer,
|
|
146
|
+
tight_bbox,
|
|
147
|
+
img_width,
|
|
148
|
+
img_height,
|
|
149
|
+
scale_x,
|
|
150
|
+
scale_y,
|
|
151
|
+
pad_inches,
|
|
152
|
+
saved_height_inches,
|
|
153
|
+
bboxes,
|
|
154
|
+
):
|
|
155
|
+
"""Extract bbox for legend."""
|
|
156
|
+
legend = ax.get_legend()
|
|
157
|
+
if legend is not None and legend.get_visible():
|
|
158
|
+
try:
|
|
159
|
+
legend_bbox = legend.get_window_extent(renderer)
|
|
160
|
+
if legend_bbox is not None:
|
|
161
|
+
bbox = transform_bbox(
|
|
162
|
+
legend_bbox,
|
|
163
|
+
fig,
|
|
164
|
+
tight_bbox,
|
|
165
|
+
img_width,
|
|
166
|
+
img_height,
|
|
167
|
+
scale_x,
|
|
168
|
+
scale_y,
|
|
169
|
+
pad_inches,
|
|
170
|
+
saved_height_inches,
|
|
171
|
+
)
|
|
172
|
+
if bbox:
|
|
173
|
+
bboxes[f"ax{ax_idx}_legend"] = {
|
|
174
|
+
**bbox,
|
|
175
|
+
"type": "legend",
|
|
176
|
+
"label": "legend",
|
|
177
|
+
"ax_index": ax_idx,
|
|
178
|
+
}
|
|
179
|
+
except Exception:
|
|
180
|
+
pass
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def extract_spines(
|
|
184
|
+
ax,
|
|
185
|
+
ax_idx,
|
|
186
|
+
fig,
|
|
187
|
+
renderer,
|
|
188
|
+
tight_bbox,
|
|
189
|
+
img_width,
|
|
190
|
+
img_height,
|
|
191
|
+
scale_x,
|
|
192
|
+
scale_y,
|
|
193
|
+
pad_inches,
|
|
194
|
+
saved_height_inches,
|
|
195
|
+
bboxes,
|
|
196
|
+
):
|
|
197
|
+
"""Extract bboxes for spines with padding for easier clicking."""
|
|
198
|
+
spine_min_size = 8
|
|
199
|
+
for spine_name, spine in ax.spines.items():
|
|
200
|
+
if spine.get_visible():
|
|
201
|
+
try:
|
|
202
|
+
spine_bbox = spine.get_window_extent(renderer)
|
|
203
|
+
if spine_bbox is not None:
|
|
204
|
+
bbox = transform_bbox(
|
|
205
|
+
spine_bbox,
|
|
206
|
+
fig,
|
|
207
|
+
tight_bbox,
|
|
208
|
+
img_width,
|
|
209
|
+
img_height,
|
|
210
|
+
scale_x,
|
|
211
|
+
scale_y,
|
|
212
|
+
pad_inches,
|
|
213
|
+
saved_height_inches,
|
|
214
|
+
)
|
|
215
|
+
if bbox:
|
|
216
|
+
if bbox["width"] < spine_min_size:
|
|
217
|
+
expand = (spine_min_size - bbox["width"]) / 2
|
|
218
|
+
bbox["x"] -= expand
|
|
219
|
+
bbox["width"] = spine_min_size
|
|
220
|
+
if bbox["height"] < spine_min_size:
|
|
221
|
+
expand = (spine_min_size - bbox["height"]) / 2
|
|
222
|
+
bbox["y"] -= expand
|
|
223
|
+
bbox["height"] = spine_min_size
|
|
224
|
+
bboxes[f"ax{ax_idx}_spine_{spine_name}"] = {
|
|
225
|
+
**bbox,
|
|
226
|
+
"type": "spine",
|
|
227
|
+
"label": spine_name,
|
|
228
|
+
"ax_index": ax_idx,
|
|
229
|
+
}
|
|
230
|
+
except Exception:
|
|
231
|
+
pass
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def extract_figure_text(
|
|
235
|
+
fig,
|
|
236
|
+
renderer,
|
|
237
|
+
tight_bbox,
|
|
238
|
+
img_width,
|
|
239
|
+
img_height,
|
|
240
|
+
scale_x,
|
|
241
|
+
scale_y,
|
|
242
|
+
pad_inches,
|
|
243
|
+
saved_height_inches,
|
|
244
|
+
bboxes,
|
|
245
|
+
):
|
|
246
|
+
"""Extract bboxes for figure-level text (suptitle, supxlabel, supylabel)."""
|
|
247
|
+
# Suptitle
|
|
248
|
+
if hasattr(fig, "_suptitle") and fig._suptitle is not None:
|
|
249
|
+
suptitle_obj = fig._suptitle
|
|
250
|
+
if suptitle_obj.get_text():
|
|
251
|
+
try:
|
|
252
|
+
suptitle_extent = suptitle_obj.get_window_extent(renderer)
|
|
253
|
+
if suptitle_extent is not None:
|
|
254
|
+
bbox = transform_bbox(
|
|
255
|
+
suptitle_extent,
|
|
256
|
+
fig,
|
|
257
|
+
tight_bbox,
|
|
258
|
+
img_width,
|
|
259
|
+
img_height,
|
|
260
|
+
scale_x,
|
|
261
|
+
scale_y,
|
|
262
|
+
pad_inches,
|
|
263
|
+
saved_height_inches,
|
|
264
|
+
)
|
|
265
|
+
if bbox:
|
|
266
|
+
bboxes["fig_suptitle"] = {
|
|
267
|
+
**bbox,
|
|
268
|
+
"type": "suptitle",
|
|
269
|
+
"label": "suptitle",
|
|
270
|
+
"ax_index": -1,
|
|
271
|
+
"text": suptitle_obj.get_text(),
|
|
272
|
+
}
|
|
273
|
+
except Exception:
|
|
274
|
+
pass
|
|
275
|
+
|
|
276
|
+
# Supxlabel
|
|
277
|
+
if hasattr(fig, "_supxlabel") and fig._supxlabel is not None:
|
|
278
|
+
supxlabel_obj = fig._supxlabel
|
|
279
|
+
if supxlabel_obj.get_text():
|
|
280
|
+
try:
|
|
281
|
+
supxlabel_extent = supxlabel_obj.get_window_extent(renderer)
|
|
282
|
+
if supxlabel_extent is not None:
|
|
283
|
+
bbox = transform_bbox(
|
|
284
|
+
supxlabel_extent,
|
|
285
|
+
fig,
|
|
286
|
+
tight_bbox,
|
|
287
|
+
img_width,
|
|
288
|
+
img_height,
|
|
289
|
+
scale_x,
|
|
290
|
+
scale_y,
|
|
291
|
+
pad_inches,
|
|
292
|
+
saved_height_inches,
|
|
293
|
+
)
|
|
294
|
+
if bbox:
|
|
295
|
+
bboxes["fig_supxlabel"] = {
|
|
296
|
+
**bbox,
|
|
297
|
+
"type": "supxlabel",
|
|
298
|
+
"label": "supxlabel",
|
|
299
|
+
"ax_index": -1,
|
|
300
|
+
"text": supxlabel_obj.get_text(),
|
|
301
|
+
}
|
|
302
|
+
except Exception:
|
|
303
|
+
pass
|
|
304
|
+
|
|
305
|
+
# Supylabel
|
|
306
|
+
if hasattr(fig, "_supylabel") and fig._supylabel is not None:
|
|
307
|
+
supylabel_obj = fig._supylabel
|
|
308
|
+
if supylabel_obj.get_text():
|
|
309
|
+
try:
|
|
310
|
+
supylabel_extent = supylabel_obj.get_window_extent(renderer)
|
|
311
|
+
if supylabel_extent is not None:
|
|
312
|
+
bbox = transform_bbox(
|
|
313
|
+
supylabel_extent,
|
|
314
|
+
fig,
|
|
315
|
+
tight_bbox,
|
|
316
|
+
img_width,
|
|
317
|
+
img_height,
|
|
318
|
+
scale_x,
|
|
319
|
+
scale_y,
|
|
320
|
+
pad_inches,
|
|
321
|
+
saved_height_inches,
|
|
322
|
+
)
|
|
323
|
+
if bbox:
|
|
324
|
+
bboxes["fig_supylabel"] = {
|
|
325
|
+
**bbox,
|
|
326
|
+
"type": "supylabel",
|
|
327
|
+
"label": "supylabel",
|
|
328
|
+
"ax_index": -1,
|
|
329
|
+
"text": supylabel_obj.get_text(),
|
|
330
|
+
}
|
|
331
|
+
except Exception:
|
|
332
|
+
pass
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
__all__ = [
|
|
336
|
+
"extract_text_elements",
|
|
337
|
+
"extract_legend",
|
|
338
|
+
"extract_spines",
|
|
339
|
+
"extract_figure_text",
|
|
340
|
+
]
|
|
341
|
+
|
|
342
|
+
# EOF
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""
|
|
4
|
+
Line bbox extraction for line plots.
|
|
5
|
+
|
|
6
|
+
This module handles bbox extraction for line elements,
|
|
7
|
+
including data point sampling for hit detection.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from typing import Any, Dict, Optional
|
|
11
|
+
|
|
12
|
+
from matplotlib.axes import Axes
|
|
13
|
+
from matplotlib.figure import Figure
|
|
14
|
+
from matplotlib.transforms import Bbox
|
|
15
|
+
|
|
16
|
+
from ._transforms import display_to_image, transform_bbox
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def get_line_bbox(
|
|
20
|
+
line,
|
|
21
|
+
ax: Axes,
|
|
22
|
+
fig: Figure,
|
|
23
|
+
renderer,
|
|
24
|
+
tight_bbox: Bbox,
|
|
25
|
+
img_width: int,
|
|
26
|
+
img_height: int,
|
|
27
|
+
scale_x: float,
|
|
28
|
+
scale_y: float,
|
|
29
|
+
pad_inches: float,
|
|
30
|
+
saved_height_inches: float,
|
|
31
|
+
include_points: bool = True,
|
|
32
|
+
) -> Optional[Dict[str, Any]]:
|
|
33
|
+
"""Get bbox and points for a line."""
|
|
34
|
+
try:
|
|
35
|
+
# Get window extent
|
|
36
|
+
window_extent = line.get_window_extent(renderer)
|
|
37
|
+
if window_extent is None:
|
|
38
|
+
return None
|
|
39
|
+
|
|
40
|
+
bbox = transform_bbox(
|
|
41
|
+
window_extent,
|
|
42
|
+
fig,
|
|
43
|
+
tight_bbox,
|
|
44
|
+
img_width,
|
|
45
|
+
img_height,
|
|
46
|
+
scale_x,
|
|
47
|
+
scale_y,
|
|
48
|
+
pad_inches,
|
|
49
|
+
saved_height_inches,
|
|
50
|
+
)
|
|
51
|
+
if bbox is None:
|
|
52
|
+
return None
|
|
53
|
+
|
|
54
|
+
# Add path points for proximity detection
|
|
55
|
+
if include_points:
|
|
56
|
+
xdata = line.get_xdata()
|
|
57
|
+
ydata = line.get_ydata()
|
|
58
|
+
|
|
59
|
+
if len(xdata) > 0 and len(ydata) > 0:
|
|
60
|
+
# Transform data coords to image pixels
|
|
61
|
+
transform = ax.transData
|
|
62
|
+
points = []
|
|
63
|
+
|
|
64
|
+
# Downsample if too many points
|
|
65
|
+
max_points = 100
|
|
66
|
+
step = max(1, len(xdata) // max_points)
|
|
67
|
+
|
|
68
|
+
for i in range(0, len(xdata), step):
|
|
69
|
+
try:
|
|
70
|
+
display_coords = transform.transform((xdata[i], ydata[i]))
|
|
71
|
+
img_coords = display_to_image(
|
|
72
|
+
display_coords[0],
|
|
73
|
+
display_coords[1],
|
|
74
|
+
fig,
|
|
75
|
+
tight_bbox,
|
|
76
|
+
img_width,
|
|
77
|
+
img_height,
|
|
78
|
+
scale_x,
|
|
79
|
+
scale_y,
|
|
80
|
+
pad_inches,
|
|
81
|
+
saved_height_inches,
|
|
82
|
+
)
|
|
83
|
+
if img_coords:
|
|
84
|
+
points.append(img_coords)
|
|
85
|
+
except Exception:
|
|
86
|
+
continue
|
|
87
|
+
|
|
88
|
+
if points:
|
|
89
|
+
bbox["points"] = points
|
|
90
|
+
|
|
91
|
+
return bbox
|
|
92
|
+
|
|
93
|
+
except Exception:
|
|
94
|
+
return None
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def get_quiver_bbox(
|
|
98
|
+
quiver,
|
|
99
|
+
ax: Axes,
|
|
100
|
+
fig: Figure,
|
|
101
|
+
renderer,
|
|
102
|
+
tight_bbox: Bbox,
|
|
103
|
+
img_width: int,
|
|
104
|
+
img_height: int,
|
|
105
|
+
scale_x: float,
|
|
106
|
+
scale_y: float,
|
|
107
|
+
pad_inches: float,
|
|
108
|
+
saved_height_inches: float,
|
|
109
|
+
) -> Optional[Dict[str, Any]]:
|
|
110
|
+
"""Get bbox for a quiver plot from its data points."""
|
|
111
|
+
try:
|
|
112
|
+
# Get X, Y positions from quiver
|
|
113
|
+
# Quiver stores positions in X, Y arrays
|
|
114
|
+
X = quiver.X
|
|
115
|
+
Y = quiver.Y
|
|
116
|
+
|
|
117
|
+
if X is None or Y is None or len(X) == 0:
|
|
118
|
+
return None
|
|
119
|
+
|
|
120
|
+
# Flatten if needed
|
|
121
|
+
import numpy as np
|
|
122
|
+
|
|
123
|
+
X_flat = np.asarray(X).flatten()
|
|
124
|
+
Y_flat = np.asarray(Y).flatten()
|
|
125
|
+
|
|
126
|
+
if len(X_flat) == 0 or len(Y_flat) == 0:
|
|
127
|
+
return None
|
|
128
|
+
|
|
129
|
+
transform = ax.transData
|
|
130
|
+
points = []
|
|
131
|
+
|
|
132
|
+
# Limit to reasonable number of points
|
|
133
|
+
max_points = 100
|
|
134
|
+
step = max(1, len(X_flat) // max_points)
|
|
135
|
+
|
|
136
|
+
for i in range(0, len(X_flat), step):
|
|
137
|
+
try:
|
|
138
|
+
display_coords = transform.transform((X_flat[i], Y_flat[i]))
|
|
139
|
+
img_coords = display_to_image(
|
|
140
|
+
display_coords[0],
|
|
141
|
+
display_coords[1],
|
|
142
|
+
fig,
|
|
143
|
+
tight_bbox,
|
|
144
|
+
img_width,
|
|
145
|
+
img_height,
|
|
146
|
+
scale_x,
|
|
147
|
+
scale_y,
|
|
148
|
+
pad_inches,
|
|
149
|
+
saved_height_inches,
|
|
150
|
+
)
|
|
151
|
+
if img_coords:
|
|
152
|
+
points.append(img_coords)
|
|
153
|
+
except Exception:
|
|
154
|
+
continue
|
|
155
|
+
|
|
156
|
+
if not points:
|
|
157
|
+
return None
|
|
158
|
+
|
|
159
|
+
# Calculate bbox from points
|
|
160
|
+
xs = [p[0] for p in points]
|
|
161
|
+
ys = [p[1] for p in points]
|
|
162
|
+
padding = 15 # pixels - slightly larger for quiver arrows
|
|
163
|
+
return {
|
|
164
|
+
"x": float(min(xs) - padding),
|
|
165
|
+
"y": float(min(ys) - padding),
|
|
166
|
+
"width": float(max(xs) - min(xs) + 2 * padding),
|
|
167
|
+
"height": float(max(ys) - min(ys) + 2 * padding),
|
|
168
|
+
}
|
|
169
|
+
except Exception:
|
|
170
|
+
return None
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
__all__ = ["get_line_bbox", "get_quiver_bbox"]
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""
|
|
4
|
+
Coordinate transformation utilities for bbox extraction.
|
|
5
|
+
|
|
6
|
+
This module handles the transformation from matplotlib display coordinates
|
|
7
|
+
to image pixel coordinates for hit detection.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from typing import Dict, List, Optional
|
|
11
|
+
|
|
12
|
+
from matplotlib.figure import Figure
|
|
13
|
+
from matplotlib.transforms import Bbox
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def transform_bbox(
|
|
17
|
+
window_extent: Bbox,
|
|
18
|
+
fig: Figure,
|
|
19
|
+
tight_bbox: Bbox,
|
|
20
|
+
img_width: int,
|
|
21
|
+
img_height: int,
|
|
22
|
+
scale_x: float,
|
|
23
|
+
scale_y: float,
|
|
24
|
+
pad_inches: float,
|
|
25
|
+
saved_height_inches: float,
|
|
26
|
+
) -> Optional[Dict[str, float]]:
|
|
27
|
+
"""
|
|
28
|
+
Transform matplotlib window extent to image pixel coordinates.
|
|
29
|
+
|
|
30
|
+
Parameters
|
|
31
|
+
----------
|
|
32
|
+
window_extent : Bbox
|
|
33
|
+
Bbox in display coordinates (points).
|
|
34
|
+
fig : Figure
|
|
35
|
+
Matplotlib figure.
|
|
36
|
+
tight_bbox : Bbox
|
|
37
|
+
Tight bbox of figure in inches.
|
|
38
|
+
img_width, img_height : int
|
|
39
|
+
Output image dimensions.
|
|
40
|
+
scale_x, scale_y : float
|
|
41
|
+
Scale factors from inches to pixels.
|
|
42
|
+
pad_inches : float
|
|
43
|
+
Padding added by bbox_inches='tight' (default 0.1).
|
|
44
|
+
saved_height_inches : float
|
|
45
|
+
Total saved image height including padding.
|
|
46
|
+
|
|
47
|
+
Returns
|
|
48
|
+
-------
|
|
49
|
+
dict or None
|
|
50
|
+
{x, y, width, height} in image pixels.
|
|
51
|
+
"""
|
|
52
|
+
try:
|
|
53
|
+
dpi = fig.dpi
|
|
54
|
+
|
|
55
|
+
# Convert display coords to inches
|
|
56
|
+
x0_inches = window_extent.x0 / dpi
|
|
57
|
+
y0_inches = window_extent.y0 / dpi
|
|
58
|
+
x1_inches = window_extent.x1 / dpi
|
|
59
|
+
y1_inches = window_extent.y1 / dpi
|
|
60
|
+
|
|
61
|
+
# Transform to saved image coordinates
|
|
62
|
+
# Account for tight bbox origin and padding
|
|
63
|
+
x0_rel = x0_inches - tight_bbox.x0 + pad_inches
|
|
64
|
+
x1_rel = x1_inches - tight_bbox.x0 + pad_inches
|
|
65
|
+
|
|
66
|
+
# Y coordinate flip: matplotlib Y=0 at bottom, image Y=0 at top
|
|
67
|
+
y0_rel = saved_height_inches - (y1_inches - tight_bbox.y0 + pad_inches)
|
|
68
|
+
y1_rel = saved_height_inches - (y0_inches - tight_bbox.y0 + pad_inches)
|
|
69
|
+
|
|
70
|
+
# Scale to image pixels
|
|
71
|
+
x0_px = x0_rel * scale_x
|
|
72
|
+
y0_px = y0_rel * scale_y
|
|
73
|
+
x1_px = x1_rel * scale_x
|
|
74
|
+
y1_px = y1_rel * scale_y
|
|
75
|
+
|
|
76
|
+
# Clamp to bounds
|
|
77
|
+
x0_px = max(0, min(x0_px, img_width))
|
|
78
|
+
x1_px = max(0, min(x1_px, img_width))
|
|
79
|
+
y0_px = max(0, min(y0_px, img_height))
|
|
80
|
+
y1_px = max(0, min(y1_px, img_height))
|
|
81
|
+
|
|
82
|
+
width = x1_px - x0_px
|
|
83
|
+
height = y1_px - y0_px
|
|
84
|
+
|
|
85
|
+
if width <= 0 or height <= 0:
|
|
86
|
+
return None
|
|
87
|
+
|
|
88
|
+
return {
|
|
89
|
+
"x": float(x0_px),
|
|
90
|
+
"y": float(y0_px),
|
|
91
|
+
"width": float(width),
|
|
92
|
+
"height": float(height),
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
except Exception:
|
|
96
|
+
return None
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def display_to_image(
|
|
100
|
+
display_x: float,
|
|
101
|
+
display_y: float,
|
|
102
|
+
fig: Figure,
|
|
103
|
+
tight_bbox: Bbox,
|
|
104
|
+
img_width: int,
|
|
105
|
+
img_height: int,
|
|
106
|
+
scale_x: float,
|
|
107
|
+
scale_y: float,
|
|
108
|
+
pad_inches: float,
|
|
109
|
+
saved_height_inches: float,
|
|
110
|
+
) -> Optional[List[float]]:
|
|
111
|
+
"""
|
|
112
|
+
Transform display coordinates to image pixel coordinates.
|
|
113
|
+
|
|
114
|
+
Returns
|
|
115
|
+
-------
|
|
116
|
+
list or None
|
|
117
|
+
[x, y] in image pixels.
|
|
118
|
+
"""
|
|
119
|
+
try:
|
|
120
|
+
dpi = fig.dpi
|
|
121
|
+
|
|
122
|
+
# Convert to inches
|
|
123
|
+
x_inches = display_x / dpi
|
|
124
|
+
y_inches = display_y / dpi
|
|
125
|
+
|
|
126
|
+
# Transform to saved image coordinates with padding
|
|
127
|
+
x_rel = x_inches - tight_bbox.x0 + pad_inches
|
|
128
|
+
|
|
129
|
+
# Y coordinate flip: matplotlib Y=0 at bottom, image Y=0 at top
|
|
130
|
+
y_rel = saved_height_inches - (y_inches - tight_bbox.y0 + pad_inches)
|
|
131
|
+
|
|
132
|
+
# Scale to image pixels
|
|
133
|
+
x_px = x_rel * scale_x
|
|
134
|
+
y_px = y_rel * scale_y
|
|
135
|
+
|
|
136
|
+
# Clamp
|
|
137
|
+
x_px = max(0, min(x_px, img_width))
|
|
138
|
+
y_px = max(0, min(y_px, img_height))
|
|
139
|
+
|
|
140
|
+
return [float(x_px), float(y_px)]
|
|
141
|
+
|
|
142
|
+
except Exception:
|
|
143
|
+
return None
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
__all__ = ["transform_bbox", "display_to_image"]
|