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.
Files changed (177) hide show
  1. figrecipe/__init__.py +106 -973
  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 +2 -93
  12. figrecipe/_dev/_plotters.py +76 -0
  13. figrecipe/_dev/_run_demos.py +56 -0
  14. figrecipe/_dev/demo_plotters/__init__.py +35 -166
  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/contour_surface/__init__.py +4 -0
  21. figrecipe/_dev/demo_plotters/distribution/__init__.py +4 -0
  22. figrecipe/_dev/demo_plotters/image_matrix/__init__.py +4 -0
  23. figrecipe/_dev/demo_plotters/line_curve/__init__.py +4 -0
  24. figrecipe/_dev/demo_plotters/{plot_plot.py → line_curve/plot_plot.py} +3 -2
  25. figrecipe/_dev/demo_plotters/scatter_points/__init__.py +4 -0
  26. figrecipe/_dev/demo_plotters/special/__init__.py +4 -0
  27. figrecipe/_dev/demo_plotters/{plot_pie.py → special/plot_pie.py} +5 -1
  28. figrecipe/_dev/demo_plotters/spectral_signal/__init__.py +4 -0
  29. figrecipe/_dev/demo_plotters/vector_flow/__init__.py +4 -0
  30. figrecipe/_editor/__init__.py +57 -9
  31. figrecipe/_editor/_bbox/__init__.py +43 -0
  32. figrecipe/_editor/_bbox/_collections.py +177 -0
  33. figrecipe/_editor/_bbox/_elements.py +159 -0
  34. figrecipe/_editor/_bbox/_extract.py +256 -0
  35. figrecipe/_editor/_bbox/_extract_axes.py +370 -0
  36. figrecipe/_editor/_bbox/_extract_text.py +342 -0
  37. figrecipe/_editor/_bbox/_lines.py +173 -0
  38. figrecipe/_editor/_bbox/_transforms.py +146 -0
  39. figrecipe/_editor/_flask_app.py +68 -1039
  40. figrecipe/_editor/_helpers.py +242 -0
  41. figrecipe/_editor/_hitmap/__init__.py +76 -0
  42. figrecipe/_editor/_hitmap/_artists/__init__.py +21 -0
  43. figrecipe/_editor/_hitmap/_artists/_collections.py +345 -0
  44. figrecipe/_editor/_hitmap/_artists/_images.py +68 -0
  45. figrecipe/_editor/_hitmap/_artists/_lines.py +107 -0
  46. figrecipe/_editor/_hitmap/_artists/_patches.py +163 -0
  47. figrecipe/_editor/_hitmap/_artists/_text.py +190 -0
  48. figrecipe/_editor/_hitmap/_colors.py +181 -0
  49. figrecipe/_editor/_hitmap/_detect.py +137 -0
  50. figrecipe/_editor/_hitmap/_restore.py +154 -0
  51. figrecipe/_editor/_hitmap_main.py +182 -0
  52. figrecipe/_editor/_preferences.py +135 -0
  53. figrecipe/_editor/_render_overrides.py +480 -0
  54. figrecipe/_editor/_renderer.py +35 -185
  55. figrecipe/_editor/_routes_axis.py +453 -0
  56. figrecipe/_editor/_routes_core.py +284 -0
  57. figrecipe/_editor/_routes_element.py +317 -0
  58. figrecipe/_editor/_routes_style.py +223 -0
  59. figrecipe/_editor/_templates/__init__.py +78 -1
  60. figrecipe/_editor/_templates/_html.py +109 -13
  61. figrecipe/_editor/_templates/_scripts/__init__.py +120 -0
  62. figrecipe/_editor/_templates/_scripts/_api.py +228 -0
  63. figrecipe/_editor/_templates/_scripts/_colors.py +485 -0
  64. figrecipe/_editor/_templates/_scripts/_core.py +436 -0
  65. figrecipe/_editor/_templates/_scripts/_debug_snapshot.py +186 -0
  66. figrecipe/_editor/_templates/_scripts/_element_editor.py +310 -0
  67. figrecipe/_editor/_templates/_scripts/_files.py +195 -0
  68. figrecipe/_editor/_templates/_scripts/_hitmap.py +509 -0
  69. figrecipe/_editor/_templates/_scripts/_inspector.py +315 -0
  70. figrecipe/_editor/_templates/_scripts/_labels.py +464 -0
  71. figrecipe/_editor/_templates/_scripts/_legend_drag.py +265 -0
  72. figrecipe/_editor/_templates/_scripts/_modals.py +226 -0
  73. figrecipe/_editor/_templates/_scripts/_overlays.py +292 -0
  74. figrecipe/_editor/_templates/_scripts/_panel_drag.py +334 -0
  75. figrecipe/_editor/_templates/_scripts/_panel_position.py +279 -0
  76. figrecipe/_editor/_templates/_scripts/_selection.py +237 -0
  77. figrecipe/_editor/_templates/_scripts/_tabs.py +89 -0
  78. figrecipe/_editor/_templates/_scripts/_view_mode.py +107 -0
  79. figrecipe/_editor/_templates/_scripts/_zoom.py +179 -0
  80. figrecipe/_editor/_templates/_styles/__init__.py +69 -0
  81. figrecipe/_editor/_templates/_styles/_base.py +64 -0
  82. figrecipe/_editor/_templates/_styles/_buttons.py +206 -0
  83. figrecipe/_editor/_templates/_styles/_color_input.py +123 -0
  84. figrecipe/_editor/_templates/_styles/_controls.py +265 -0
  85. figrecipe/_editor/_templates/_styles/_dynamic_props.py +144 -0
  86. figrecipe/_editor/_templates/_styles/_forms.py +126 -0
  87. figrecipe/_editor/_templates/_styles/_hitmap.py +184 -0
  88. figrecipe/_editor/_templates/_styles/_inspector.py +90 -0
  89. figrecipe/_editor/_templates/_styles/_labels.py +118 -0
  90. figrecipe/_editor/_templates/_styles/_modals.py +98 -0
  91. figrecipe/_editor/_templates/_styles/_overlays.py +130 -0
  92. figrecipe/_editor/_templates/_styles/_preview.py +225 -0
  93. figrecipe/_editor/_templates/_styles/_selection.py +73 -0
  94. figrecipe/_params/_DECORATION_METHODS.py +6 -0
  95. figrecipe/_recorder.py +35 -106
  96. figrecipe/_recorder_utils.py +124 -0
  97. figrecipe/_reproducer/__init__.py +18 -0
  98. figrecipe/_reproducer/_core.py +498 -0
  99. figrecipe/_reproducer/_custom_plots.py +279 -0
  100. figrecipe/_reproducer/_seaborn.py +100 -0
  101. figrecipe/_reproducer/_violin.py +186 -0
  102. figrecipe/_signatures/_kwargs.py +273 -0
  103. figrecipe/_signatures/_loader.py +21 -423
  104. figrecipe/_signatures/_parsing.py +147 -0
  105. figrecipe/_wrappers/_axes.py +119 -910
  106. figrecipe/_wrappers/_axes_helpers.py +136 -0
  107. figrecipe/_wrappers/_axes_plots.py +418 -0
  108. figrecipe/_wrappers/_axes_seaborn.py +157 -0
  109. figrecipe/_wrappers/_figure.py +162 -0
  110. figrecipe/_wrappers/_panel_labels.py +127 -0
  111. figrecipe/_wrappers/_plot_helpers.py +143 -0
  112. figrecipe/_wrappers/_violin_helpers.py +180 -0
  113. figrecipe/styles/__init__.py +8 -6
  114. figrecipe/styles/_dotdict.py +72 -0
  115. figrecipe/styles/_finalize.py +134 -0
  116. figrecipe/styles/_fonts.py +77 -0
  117. figrecipe/styles/_kwargs_converter.py +178 -0
  118. figrecipe/styles/_plot_styles.py +209 -0
  119. figrecipe/styles/_style_applier.py +32 -478
  120. figrecipe/styles/_style_loader.py +16 -192
  121. figrecipe/styles/_themes.py +151 -0
  122. figrecipe/styles/presets/MATPLOTLIB.yaml +2 -1
  123. figrecipe/styles/presets/SCITEX.yaml +29 -24
  124. {figrecipe-0.6.0.dist-info → figrecipe-0.7.4.dist-info}/METADATA +37 -2
  125. figrecipe-0.7.4.dist-info/RECORD +188 -0
  126. figrecipe/_editor/_bbox.py +0 -978
  127. figrecipe/_editor/_hitmap.py +0 -937
  128. figrecipe/_editor/_templates/_scripts.py +0 -2778
  129. figrecipe/_editor/_templates/_styles.py +0 -1326
  130. figrecipe/_reproducer.py +0 -975
  131. figrecipe-0.6.0.dist-info/RECORD +0 -90
  132. /figrecipe/_dev/demo_plotters/{plot_bar.py → bar_categorical/plot_bar.py} +0 -0
  133. /figrecipe/_dev/demo_plotters/{plot_barh.py → bar_categorical/plot_barh.py} +0 -0
  134. /figrecipe/_dev/demo_plotters/{plot_contour.py → contour_surface/plot_contour.py} +0 -0
  135. /figrecipe/_dev/demo_plotters/{plot_contourf.py → contour_surface/plot_contourf.py} +0 -0
  136. /figrecipe/_dev/demo_plotters/{plot_tricontour.py → contour_surface/plot_tricontour.py} +0 -0
  137. /figrecipe/_dev/demo_plotters/{plot_tricontourf.py → contour_surface/plot_tricontourf.py} +0 -0
  138. /figrecipe/_dev/demo_plotters/{plot_tripcolor.py → contour_surface/plot_tripcolor.py} +0 -0
  139. /figrecipe/_dev/demo_plotters/{plot_triplot.py → contour_surface/plot_triplot.py} +0 -0
  140. /figrecipe/_dev/demo_plotters/{plot_boxplot.py → distribution/plot_boxplot.py} +0 -0
  141. /figrecipe/_dev/demo_plotters/{plot_ecdf.py → distribution/plot_ecdf.py} +0 -0
  142. /figrecipe/_dev/demo_plotters/{plot_hist.py → distribution/plot_hist.py} +0 -0
  143. /figrecipe/_dev/demo_plotters/{plot_hist2d.py → distribution/plot_hist2d.py} +0 -0
  144. /figrecipe/_dev/demo_plotters/{plot_violinplot.py → distribution/plot_violinplot.py} +0 -0
  145. /figrecipe/_dev/demo_plotters/{plot_hexbin.py → image_matrix/plot_hexbin.py} +0 -0
  146. /figrecipe/_dev/demo_plotters/{plot_imshow.py → image_matrix/plot_imshow.py} +0 -0
  147. /figrecipe/_dev/demo_plotters/{plot_matshow.py → image_matrix/plot_matshow.py} +0 -0
  148. /figrecipe/_dev/demo_plotters/{plot_pcolor.py → image_matrix/plot_pcolor.py} +0 -0
  149. /figrecipe/_dev/demo_plotters/{plot_pcolormesh.py → image_matrix/plot_pcolormesh.py} +0 -0
  150. /figrecipe/_dev/demo_plotters/{plot_spy.py → image_matrix/plot_spy.py} +0 -0
  151. /figrecipe/_dev/demo_plotters/{plot_errorbar.py → line_curve/plot_errorbar.py} +0 -0
  152. /figrecipe/_dev/demo_plotters/{plot_fill.py → line_curve/plot_fill.py} +0 -0
  153. /figrecipe/_dev/demo_plotters/{plot_fill_between.py → line_curve/plot_fill_between.py} +0 -0
  154. /figrecipe/_dev/demo_plotters/{plot_fill_betweenx.py → line_curve/plot_fill_betweenx.py} +0 -0
  155. /figrecipe/_dev/demo_plotters/{plot_stackplot.py → line_curve/plot_stackplot.py} +0 -0
  156. /figrecipe/_dev/demo_plotters/{plot_stairs.py → line_curve/plot_stairs.py} +0 -0
  157. /figrecipe/_dev/demo_plotters/{plot_step.py → line_curve/plot_step.py} +0 -0
  158. /figrecipe/_dev/demo_plotters/{plot_scatter.py → scatter_points/plot_scatter.py} +0 -0
  159. /figrecipe/_dev/demo_plotters/{plot_eventplot.py → special/plot_eventplot.py} +0 -0
  160. /figrecipe/_dev/demo_plotters/{plot_loglog.py → special/plot_loglog.py} +0 -0
  161. /figrecipe/_dev/demo_plotters/{plot_semilogx.py → special/plot_semilogx.py} +0 -0
  162. /figrecipe/_dev/demo_plotters/{plot_semilogy.py → special/plot_semilogy.py} +0 -0
  163. /figrecipe/_dev/demo_plotters/{plot_stem.py → special/plot_stem.py} +0 -0
  164. /figrecipe/_dev/demo_plotters/{plot_acorr.py → spectral_signal/plot_acorr.py} +0 -0
  165. /figrecipe/_dev/demo_plotters/{plot_angle_spectrum.py → spectral_signal/plot_angle_spectrum.py} +0 -0
  166. /figrecipe/_dev/demo_plotters/{plot_cohere.py → spectral_signal/plot_cohere.py} +0 -0
  167. /figrecipe/_dev/demo_plotters/{plot_csd.py → spectral_signal/plot_csd.py} +0 -0
  168. /figrecipe/_dev/demo_plotters/{plot_magnitude_spectrum.py → spectral_signal/plot_magnitude_spectrum.py} +0 -0
  169. /figrecipe/_dev/demo_plotters/{plot_phase_spectrum.py → spectral_signal/plot_phase_spectrum.py} +0 -0
  170. /figrecipe/_dev/demo_plotters/{plot_psd.py → spectral_signal/plot_psd.py} +0 -0
  171. /figrecipe/_dev/demo_plotters/{plot_specgram.py → spectral_signal/plot_specgram.py} +0 -0
  172. /figrecipe/_dev/demo_plotters/{plot_xcorr.py → spectral_signal/plot_xcorr.py} +0 -0
  173. /figrecipe/_dev/demo_plotters/{plot_barbs.py → vector_flow/plot_barbs.py} +0 -0
  174. /figrecipe/_dev/demo_plotters/{plot_quiver.py → vector_flow/plot_quiver.py} +0 -0
  175. /figrecipe/_dev/demo_plotters/{plot_streamplot.py → vector_flow/plot_streamplot.py} +0 -0
  176. {figrecipe-0.6.0.dist-info → figrecipe-0.7.4.dist-info}/WHEEL +0 -0
  177. {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"]