figrecipe 0.6.0__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 (269) hide show
  1. figrecipe/__init__.py +161 -1030
  2. figrecipe/__main__.py +12 -0
  3. figrecipe/_api/__init__.py +48 -0
  4. figrecipe/_api/_extract.py +108 -0
  5. figrecipe/_api/_notebook.py +61 -0
  6. figrecipe/_api/_panel.py +113 -0
  7. figrecipe/_api/_save.py +287 -0
  8. figrecipe/_api/_seaborn_proxy.py +34 -0
  9. figrecipe/_api/_style_manager.py +153 -0
  10. figrecipe/_api/_subplots.py +333 -0
  11. figrecipe/_api/_validate.py +82 -0
  12. figrecipe/_cli/__init__.py +7 -0
  13. figrecipe/_cli/_compose.py +87 -0
  14. figrecipe/_cli/_convert.py +117 -0
  15. figrecipe/_cli/_crop.py +82 -0
  16. figrecipe/_cli/_edit.py +70 -0
  17. figrecipe/_cli/_extract.py +128 -0
  18. figrecipe/_cli/_fonts.py +47 -0
  19. figrecipe/_cli/_info.py +67 -0
  20. figrecipe/_cli/_main.py +58 -0
  21. figrecipe/_cli/_reproduce.py +79 -0
  22. figrecipe/_cli/_style.py +77 -0
  23. figrecipe/_cli/_validate.py +66 -0
  24. figrecipe/_cli/_version.py +50 -0
  25. figrecipe/_composition/__init__.py +32 -0
  26. figrecipe/_composition/_alignment.py +452 -0
  27. figrecipe/_composition/_compose.py +179 -0
  28. figrecipe/_composition/_import_axes.py +127 -0
  29. figrecipe/_composition/_visibility.py +125 -0
  30. figrecipe/_dev/__init__.py +4 -93
  31. figrecipe/_dev/_plotters.py +76 -0
  32. figrecipe/_dev/_run_demos.py +56 -0
  33. figrecipe/_dev/browser/__init__.py +69 -0
  34. figrecipe/_dev/browser/_audio.py +240 -0
  35. figrecipe/_dev/browser/_caption.py +356 -0
  36. figrecipe/_dev/browser/_click_effect.py +146 -0
  37. figrecipe/_dev/browser/_cursor.py +196 -0
  38. figrecipe/_dev/browser/_highlight.py +105 -0
  39. figrecipe/_dev/browser/_narration.py +237 -0
  40. figrecipe/_dev/browser/_recorder.py +446 -0
  41. figrecipe/_dev/browser/_utils.py +178 -0
  42. figrecipe/_dev/browser/_video_trim/__init__.py +152 -0
  43. figrecipe/_dev/browser/_video_trim/_detection.py +223 -0
  44. figrecipe/_dev/browser/_video_trim/_markers.py +140 -0
  45. figrecipe/_dev/demo_plotters/__init__.py +35 -166
  46. figrecipe/_dev/demo_plotters/_categories.py +81 -0
  47. figrecipe/_dev/demo_plotters/_figure_creators.py +119 -0
  48. figrecipe/_dev/demo_plotters/_helpers.py +31 -0
  49. figrecipe/_dev/demo_plotters/_registry.py +50 -0
  50. figrecipe/_dev/demo_plotters/bar_categorical/__init__.py +4 -0
  51. figrecipe/_dev/demo_plotters/contour_surface/__init__.py +4 -0
  52. figrecipe/_dev/demo_plotters/distribution/__init__.py +4 -0
  53. figrecipe/_dev/demo_plotters/image_matrix/__init__.py +4 -0
  54. figrecipe/_dev/demo_plotters/line_curve/__init__.py +4 -0
  55. figrecipe/_dev/demo_plotters/{plot_plot.py → line_curve/plot_plot.py} +3 -2
  56. figrecipe/_dev/demo_plotters/scatter_points/__init__.py +4 -0
  57. figrecipe/_dev/demo_plotters/special/__init__.py +4 -0
  58. figrecipe/_dev/demo_plotters/{plot_pie.py → special/plot_pie.py} +5 -1
  59. figrecipe/_dev/demo_plotters/spectral_signal/__init__.py +4 -0
  60. figrecipe/_dev/demo_plotters/vector_flow/__init__.py +4 -0
  61. figrecipe/_editor/__init__.py +61 -13
  62. figrecipe/_editor/_bbox/__init__.py +43 -0
  63. figrecipe/_editor/_bbox/_collections.py +177 -0
  64. figrecipe/_editor/_bbox/_elements.py +159 -0
  65. figrecipe/_editor/_bbox/_extract.py +402 -0
  66. figrecipe/_editor/_bbox/_extract_axes.py +370 -0
  67. figrecipe/_editor/_bbox/_extract_text.py +466 -0
  68. figrecipe/_editor/_bbox/_lines.py +173 -0
  69. figrecipe/_editor/_bbox/_transforms.py +146 -0
  70. figrecipe/_editor/_call_overrides.py +183 -0
  71. figrecipe/_editor/_datatable_plot_handlers.py +249 -0
  72. figrecipe/_editor/_figure_layout.py +211 -0
  73. figrecipe/_editor/_flask_app.py +200 -1030
  74. figrecipe/_editor/_helpers.py +251 -0
  75. figrecipe/_editor/_hitmap/__init__.py +76 -0
  76. figrecipe/_editor/_hitmap/_artists/__init__.py +21 -0
  77. figrecipe/_editor/_hitmap/_artists/_collections.py +345 -0
  78. figrecipe/_editor/_hitmap/_artists/_images.py +68 -0
  79. figrecipe/_editor/_hitmap/_artists/_lines.py +107 -0
  80. figrecipe/_editor/_hitmap/_artists/_patches.py +163 -0
  81. figrecipe/_editor/_hitmap/_artists/_text.py +190 -0
  82. figrecipe/_editor/_hitmap/_colors.py +181 -0
  83. figrecipe/_editor/_hitmap/_detect.py +194 -0
  84. figrecipe/_editor/_hitmap/_restore.py +154 -0
  85. figrecipe/_editor/_hitmap_main.py +182 -0
  86. figrecipe/_editor/_overrides.py +4 -1
  87. figrecipe/_editor/_plot_types_registry.py +190 -0
  88. figrecipe/_editor/_preferences.py +135 -0
  89. figrecipe/_editor/_render_overrides.py +507 -0
  90. figrecipe/_editor/_renderer.py +81 -186
  91. figrecipe/_editor/_routes_annotation.py +114 -0
  92. figrecipe/_editor/_routes_axis.py +482 -0
  93. figrecipe/_editor/_routes_captions.py +130 -0
  94. figrecipe/_editor/_routes_composition.py +270 -0
  95. figrecipe/_editor/_routes_core.py +126 -0
  96. figrecipe/_editor/_routes_datatable.py +364 -0
  97. figrecipe/_editor/_routes_element.py +335 -0
  98. figrecipe/_editor/_routes_files.py +443 -0
  99. figrecipe/_editor/_routes_image.py +200 -0
  100. figrecipe/_editor/_routes_snapshot.py +94 -0
  101. figrecipe/_editor/_routes_style.py +243 -0
  102. figrecipe/_editor/_templates/__init__.py +116 -1
  103. figrecipe/_editor/_templates/_html.py +154 -64
  104. figrecipe/_editor/_templates/_html_components/__init__.py +13 -0
  105. figrecipe/_editor/_templates/_html_components/_composition_toolbar.py +79 -0
  106. figrecipe/_editor/_templates/_html_components/_file_browser.py +41 -0
  107. figrecipe/_editor/_templates/_html_datatable.py +92 -0
  108. figrecipe/_editor/_templates/_scripts/__init__.py +178 -0
  109. figrecipe/_editor/_templates/_scripts/_accordion.py +328 -0
  110. figrecipe/_editor/_templates/_scripts/_annotation_drag.py +504 -0
  111. figrecipe/_editor/_templates/_scripts/_api.py +228 -0
  112. figrecipe/_editor/_templates/_scripts/_canvas_context_menu.py +182 -0
  113. figrecipe/_editor/_templates/_scripts/_captions.py +231 -0
  114. figrecipe/_editor/_templates/_scripts/_colors.py +485 -0
  115. figrecipe/_editor/_templates/_scripts/_composition.py +283 -0
  116. figrecipe/_editor/_templates/_scripts/_core.py +493 -0
  117. figrecipe/_editor/_templates/_scripts/_datatable/__init__.py +59 -0
  118. figrecipe/_editor/_templates/_scripts/_datatable/_cell_edit.py +97 -0
  119. figrecipe/_editor/_templates/_scripts/_datatable/_clipboard.py +164 -0
  120. figrecipe/_editor/_templates/_scripts/_datatable/_context_menu.py +221 -0
  121. figrecipe/_editor/_templates/_scripts/_datatable/_core.py +150 -0
  122. figrecipe/_editor/_templates/_scripts/_datatable/_editable.py +511 -0
  123. figrecipe/_editor/_templates/_scripts/_datatable/_import.py +161 -0
  124. figrecipe/_editor/_templates/_scripts/_datatable/_plot.py +261 -0
  125. figrecipe/_editor/_templates/_scripts/_datatable/_selection.py +438 -0
  126. figrecipe/_editor/_templates/_scripts/_datatable/_table.py +256 -0
  127. figrecipe/_editor/_templates/_scripts/_datatable/_tabs.py +354 -0
  128. figrecipe/_editor/_templates/_scripts/_debug_snapshot.py +186 -0
  129. figrecipe/_editor/_templates/_scripts/_element_editor.py +325 -0
  130. figrecipe/_editor/_templates/_scripts/_files.py +429 -0
  131. figrecipe/_editor/_templates/_scripts/_files_context_menu.py +240 -0
  132. figrecipe/_editor/_templates/_scripts/_hitmap.py +512 -0
  133. figrecipe/_editor/_templates/_scripts/_image_drop.py +428 -0
  134. figrecipe/_editor/_templates/_scripts/_inspector.py +315 -0
  135. figrecipe/_editor/_templates/_scripts/_labels.py +464 -0
  136. figrecipe/_editor/_templates/_scripts/_legend_drag.py +270 -0
  137. figrecipe/_editor/_templates/_scripts/_modals.py +226 -0
  138. figrecipe/_editor/_templates/_scripts/_multi_select.py +198 -0
  139. figrecipe/_editor/_templates/_scripts/_overlays.py +292 -0
  140. figrecipe/_editor/_templates/_scripts/_panel_drag.py +505 -0
  141. figrecipe/_editor/_templates/_scripts/_panel_drag_snapshot.py +33 -0
  142. figrecipe/_editor/_templates/_scripts/_panel_position.py +463 -0
  143. figrecipe/_editor/_templates/_scripts/_panel_resize.py +230 -0
  144. figrecipe/_editor/_templates/_scripts/_panel_snap.py +307 -0
  145. figrecipe/_editor/_templates/_scripts/_region_select.py +255 -0
  146. figrecipe/_editor/_templates/_scripts/_selection.py +244 -0
  147. figrecipe/_editor/_templates/_scripts/_sync.py +242 -0
  148. figrecipe/_editor/_templates/_scripts/_tabs.py +89 -0
  149. figrecipe/_editor/_templates/_scripts/_undo_redo.py +348 -0
  150. figrecipe/_editor/_templates/_scripts/_view_mode.py +107 -0
  151. figrecipe/_editor/_templates/_scripts/_zoom.py +212 -0
  152. figrecipe/_editor/_templates/_styles/__init__.py +78 -0
  153. figrecipe/_editor/_templates/_styles/_base.py +111 -0
  154. figrecipe/_editor/_templates/_styles/_buttons.py +327 -0
  155. figrecipe/_editor/_templates/_styles/_color_input.py +123 -0
  156. figrecipe/_editor/_templates/_styles/_composition.py +87 -0
  157. figrecipe/_editor/_templates/_styles/_controls.py +430 -0
  158. figrecipe/_editor/_templates/_styles/_datatable/__init__.py +40 -0
  159. figrecipe/_editor/_templates/_styles/_datatable/_editable.py +203 -0
  160. figrecipe/_editor/_templates/_styles/_datatable/_panel.py +268 -0
  161. figrecipe/_editor/_templates/_styles/_datatable/_table.py +479 -0
  162. figrecipe/_editor/_templates/_styles/_datatable/_toolbar.py +384 -0
  163. figrecipe/_editor/_templates/_styles/_datatable/_vars.py +123 -0
  164. figrecipe/_editor/_templates/_styles/_dynamic_props.py +144 -0
  165. figrecipe/_editor/_templates/_styles/_file_browser.py +466 -0
  166. figrecipe/_editor/_templates/_styles/_forms.py +224 -0
  167. figrecipe/_editor/_templates/_styles/_hitmap.py +191 -0
  168. figrecipe/_editor/_templates/_styles/_inspector.py +90 -0
  169. figrecipe/_editor/_templates/_styles/_labels.py +118 -0
  170. figrecipe/_editor/_templates/_styles/_modals.py +127 -0
  171. figrecipe/_editor/_templates/_styles/_overlays.py +130 -0
  172. figrecipe/_editor/_templates/_styles/_preview.py +430 -0
  173. figrecipe/_editor/_templates/_styles/_selection.py +73 -0
  174. figrecipe/_editor/_templates/_styles/_spinner.py +117 -0
  175. figrecipe/_editor/static/audio/click.mp3 +0 -0
  176. figrecipe/_editor/static/click.mp3 +0 -0
  177. figrecipe/_editor/static/icons/favicon.ico +0 -0
  178. figrecipe/_integrations/__init__.py +17 -0
  179. figrecipe/_integrations/_scitex_stats.py +298 -0
  180. figrecipe/_params/_DECORATION_METHODS.py +8 -0
  181. figrecipe/_recorder.py +63 -109
  182. figrecipe/_recorder_utils.py +124 -0
  183. figrecipe/_reproducer/__init__.py +18 -0
  184. figrecipe/_reproducer/_core.py +509 -0
  185. figrecipe/_reproducer/_custom_plots.py +279 -0
  186. figrecipe/_reproducer/_seaborn.py +100 -0
  187. figrecipe/_reproducer/_violin.py +186 -0
  188. figrecipe/_signatures/_kwargs.py +273 -0
  189. figrecipe/_signatures/_loader.py +21 -423
  190. figrecipe/_signatures/_parsing.py +147 -0
  191. figrecipe/_utils/__init__.py +3 -0
  192. figrecipe/_utils/_bundle.py +205 -0
  193. figrecipe/_wrappers/_axes.py +252 -895
  194. figrecipe/_wrappers/_axes_helpers.py +136 -0
  195. figrecipe/_wrappers/_axes_plots.py +418 -0
  196. figrecipe/_wrappers/_axes_seaborn.py +157 -0
  197. figrecipe/_wrappers/_caption_generator.py +218 -0
  198. figrecipe/_wrappers/_figure.py +188 -1
  199. figrecipe/_wrappers/_panel_labels.py +127 -0
  200. figrecipe/_wrappers/_plot_helpers.py +143 -0
  201. figrecipe/_wrappers/_stat_annotation.py +274 -0
  202. figrecipe/_wrappers/_violin_helpers.py +180 -0
  203. figrecipe/styles/__init__.py +8 -6
  204. figrecipe/styles/_dotdict.py +72 -0
  205. figrecipe/styles/_finalize.py +134 -0
  206. figrecipe/styles/_fonts.py +77 -0
  207. figrecipe/styles/_kwargs_converter.py +178 -0
  208. figrecipe/styles/_plot_styles.py +209 -0
  209. figrecipe/styles/_style_applier.py +42 -480
  210. figrecipe/styles/_style_loader.py +16 -192
  211. figrecipe/styles/_themes.py +151 -0
  212. figrecipe/styles/presets/MATPLOTLIB.yaml +2 -1
  213. figrecipe/styles/presets/SCITEX.yaml +40 -28
  214. figrecipe-0.9.0.dist-info/METADATA +427 -0
  215. figrecipe-0.9.0.dist-info/RECORD +277 -0
  216. figrecipe-0.9.0.dist-info/entry_points.txt +2 -0
  217. figrecipe/_editor/_bbox.py +0 -978
  218. figrecipe/_editor/_hitmap.py +0 -937
  219. figrecipe/_editor/_templates/_scripts.py +0 -2778
  220. figrecipe/_editor/_templates/_styles.py +0 -1326
  221. figrecipe/_reproducer.py +0 -975
  222. figrecipe-0.6.0.dist-info/METADATA +0 -394
  223. figrecipe-0.6.0.dist-info/RECORD +0 -90
  224. /figrecipe/_dev/demo_plotters/{plot_bar.py → bar_categorical/plot_bar.py} +0 -0
  225. /figrecipe/_dev/demo_plotters/{plot_barh.py → bar_categorical/plot_barh.py} +0 -0
  226. /figrecipe/_dev/demo_plotters/{plot_contour.py → contour_surface/plot_contour.py} +0 -0
  227. /figrecipe/_dev/demo_plotters/{plot_contourf.py → contour_surface/plot_contourf.py} +0 -0
  228. /figrecipe/_dev/demo_plotters/{plot_tricontour.py → contour_surface/plot_tricontour.py} +0 -0
  229. /figrecipe/_dev/demo_plotters/{plot_tricontourf.py → contour_surface/plot_tricontourf.py} +0 -0
  230. /figrecipe/_dev/demo_plotters/{plot_tripcolor.py → contour_surface/plot_tripcolor.py} +0 -0
  231. /figrecipe/_dev/demo_plotters/{plot_triplot.py → contour_surface/plot_triplot.py} +0 -0
  232. /figrecipe/_dev/demo_plotters/{plot_boxplot.py → distribution/plot_boxplot.py} +0 -0
  233. /figrecipe/_dev/demo_plotters/{plot_ecdf.py → distribution/plot_ecdf.py} +0 -0
  234. /figrecipe/_dev/demo_plotters/{plot_hist.py → distribution/plot_hist.py} +0 -0
  235. /figrecipe/_dev/demo_plotters/{plot_hist2d.py → distribution/plot_hist2d.py} +0 -0
  236. /figrecipe/_dev/demo_plotters/{plot_violinplot.py → distribution/plot_violinplot.py} +0 -0
  237. /figrecipe/_dev/demo_plotters/{plot_hexbin.py → image_matrix/plot_hexbin.py} +0 -0
  238. /figrecipe/_dev/demo_plotters/{plot_imshow.py → image_matrix/plot_imshow.py} +0 -0
  239. /figrecipe/_dev/demo_plotters/{plot_matshow.py → image_matrix/plot_matshow.py} +0 -0
  240. /figrecipe/_dev/demo_plotters/{plot_pcolor.py → image_matrix/plot_pcolor.py} +0 -0
  241. /figrecipe/_dev/demo_plotters/{plot_pcolormesh.py → image_matrix/plot_pcolormesh.py} +0 -0
  242. /figrecipe/_dev/demo_plotters/{plot_spy.py → image_matrix/plot_spy.py} +0 -0
  243. /figrecipe/_dev/demo_plotters/{plot_errorbar.py → line_curve/plot_errorbar.py} +0 -0
  244. /figrecipe/_dev/demo_plotters/{plot_fill.py → line_curve/plot_fill.py} +0 -0
  245. /figrecipe/_dev/demo_plotters/{plot_fill_between.py → line_curve/plot_fill_between.py} +0 -0
  246. /figrecipe/_dev/demo_plotters/{plot_fill_betweenx.py → line_curve/plot_fill_betweenx.py} +0 -0
  247. /figrecipe/_dev/demo_plotters/{plot_stackplot.py → line_curve/plot_stackplot.py} +0 -0
  248. /figrecipe/_dev/demo_plotters/{plot_stairs.py → line_curve/plot_stairs.py} +0 -0
  249. /figrecipe/_dev/demo_plotters/{plot_step.py → line_curve/plot_step.py} +0 -0
  250. /figrecipe/_dev/demo_plotters/{plot_scatter.py → scatter_points/plot_scatter.py} +0 -0
  251. /figrecipe/_dev/demo_plotters/{plot_eventplot.py → special/plot_eventplot.py} +0 -0
  252. /figrecipe/_dev/demo_plotters/{plot_loglog.py → special/plot_loglog.py} +0 -0
  253. /figrecipe/_dev/demo_plotters/{plot_semilogx.py → special/plot_semilogx.py} +0 -0
  254. /figrecipe/_dev/demo_plotters/{plot_semilogy.py → special/plot_semilogy.py} +0 -0
  255. /figrecipe/_dev/demo_plotters/{plot_stem.py → special/plot_stem.py} +0 -0
  256. /figrecipe/_dev/demo_plotters/{plot_acorr.py → spectral_signal/plot_acorr.py} +0 -0
  257. /figrecipe/_dev/demo_plotters/{plot_angle_spectrum.py → spectral_signal/plot_angle_spectrum.py} +0 -0
  258. /figrecipe/_dev/demo_plotters/{plot_cohere.py → spectral_signal/plot_cohere.py} +0 -0
  259. /figrecipe/_dev/demo_plotters/{plot_csd.py → spectral_signal/plot_csd.py} +0 -0
  260. /figrecipe/_dev/demo_plotters/{plot_magnitude_spectrum.py → spectral_signal/plot_magnitude_spectrum.py} +0 -0
  261. /figrecipe/_dev/demo_plotters/{plot_phase_spectrum.py → spectral_signal/plot_phase_spectrum.py} +0 -0
  262. /figrecipe/_dev/demo_plotters/{plot_psd.py → spectral_signal/plot_psd.py} +0 -0
  263. /figrecipe/_dev/demo_plotters/{plot_specgram.py → spectral_signal/plot_specgram.py} +0 -0
  264. /figrecipe/_dev/demo_plotters/{plot_xcorr.py → spectral_signal/plot_xcorr.py} +0 -0
  265. /figrecipe/_dev/demo_plotters/{plot_barbs.py → vector_flow/plot_barbs.py} +0 -0
  266. /figrecipe/_dev/demo_plotters/{plot_quiver.py → vector_flow/plot_quiver.py} +0 -0
  267. /figrecipe/_dev/demo_plotters/{plot_streamplot.py → vector_flow/plot_streamplot.py} +0 -0
  268. {figrecipe-0.6.0.dist-info → figrecipe-0.9.0.dist-info}/WHEEL +0 -0
  269. {figrecipe-0.6.0.dist-info → figrecipe-0.9.0.dist-info}/licenses/LICENSE +0 -0
@@ -1,978 +0,0 @@
1
- #!/usr/bin/env python3
2
- # -*- coding: utf-8 -*-
3
- """
4
- Bounding box extraction for figure elements.
5
-
6
- This module extracts pixel coordinates for all figure elements,
7
- enabling precise hit detection and visual selection overlays.
8
-
9
- Coordinate Pipeline:
10
- Matplotlib display coords (points) → inches → image pixels
11
- """
12
-
13
- from typing import Any, Dict, List, Optional
14
-
15
- from matplotlib.axes import Axes
16
- from matplotlib.collections import PathCollection, PolyCollection
17
- from matplotlib.figure import Figure
18
- from matplotlib.patches import Rectangle
19
- from matplotlib.transforms import Bbox
20
-
21
-
22
- def extract_bboxes(
23
- fig: Figure,
24
- img_width: int,
25
- img_height: int,
26
- include_points: bool = True,
27
- ) -> Dict[str, Dict[str, Any]]:
28
- """
29
- Extract bounding boxes for all figure elements.
30
-
31
- Parameters
32
- ----------
33
- fig : matplotlib.figure.Figure
34
- Figure to extract bboxes from.
35
- img_width : int
36
- Width of the output image in pixels.
37
- img_height : int
38
- Height of the output image in pixels.
39
- include_points : bool, optional
40
- Whether to include point arrays for lines/scatter (default: True).
41
- Enables proximity-based hit detection.
42
-
43
- Returns
44
- -------
45
- dict
46
- Mapping from element key to bbox info:
47
- {
48
- 'element_key': {
49
- 'x': float, # Left edge in pixels
50
- 'y': float, # Top edge in pixels
51
- 'width': float, # Width in pixels
52
- 'height': float, # Height in pixels
53
- 'type': str, # Element type
54
- 'ax_index': int, # Axes index
55
- 'points': [[x, y], ...], # Optional path points
56
- }
57
- }
58
- """
59
- bboxes = {}
60
-
61
- # Get renderer for bbox calculations
62
- fig.canvas.draw()
63
- renderer = fig.canvas.get_renderer()
64
-
65
- # Get tight bbox for coordinate transformation
66
- tight_bbox = fig.get_tightbbox(renderer)
67
- if tight_bbox is None:
68
- tight_bbox = Bbox.from_bounds(0, 0, fig.get_figwidth(), fig.get_figheight())
69
-
70
- # bbox_inches='tight' adds pad_inches (default 0.1) around the tight bbox
71
- pad_inches = 0.1
72
- saved_width_inches = tight_bbox.width + 2 * pad_inches
73
- saved_height_inches = tight_bbox.height + 2 * pad_inches
74
-
75
- # Calculate scale factors from saved image size to pixel size
76
- scale_x = img_width / saved_width_inches if saved_width_inches > 0 else 1
77
- scale_y = img_height / saved_height_inches if saved_height_inches > 0 else 1
78
-
79
- # Process each axes
80
- axes_list = fig.get_axes()
81
- for ax_idx, ax in enumerate(axes_list):
82
- # Axes bounding box
83
- ax_bbox = _get_element_bbox(
84
- ax,
85
- fig,
86
- renderer,
87
- tight_bbox,
88
- img_width,
89
- img_height,
90
- scale_x,
91
- scale_y,
92
- pad_inches,
93
- saved_height_inches,
94
- )
95
- if ax_bbox:
96
- bboxes[f"ax{ax_idx}_axes"] = {
97
- **ax_bbox,
98
- "type": "axes",
99
- "ax_index": ax_idx,
100
- }
101
-
102
- # Lines
103
- for i, line in enumerate(ax.get_lines()):
104
- if not line.get_visible():
105
- continue
106
-
107
- key = f"ax{ax_idx}_line{i}"
108
- bbox = _get_line_bbox(
109
- line,
110
- ax,
111
- fig,
112
- renderer,
113
- tight_bbox,
114
- img_width,
115
- img_height,
116
- scale_x,
117
- scale_y,
118
- pad_inches,
119
- saved_height_inches,
120
- include_points=include_points,
121
- )
122
- if bbox:
123
- bboxes[key] = {
124
- **bbox,
125
- "type": "line",
126
- "label": line.get_label() or f"line_{i}",
127
- "ax_index": ax_idx,
128
- }
129
-
130
- # Scatter plots
131
- scatter_idx = 0
132
- for i, coll in enumerate(ax.collections):
133
- if isinstance(coll, PathCollection):
134
- if not coll.get_visible():
135
- continue
136
-
137
- key = f"ax{ax_idx}_scatter{scatter_idx}"
138
- bbox = _get_collection_bbox(
139
- coll,
140
- ax,
141
- fig,
142
- renderer,
143
- tight_bbox,
144
- img_width,
145
- img_height,
146
- scale_x,
147
- scale_y,
148
- pad_inches,
149
- saved_height_inches,
150
- include_points=include_points,
151
- )
152
- if bbox:
153
- bboxes[key] = {
154
- **bbox,
155
- "type": "scatter",
156
- "label": coll.get_label() or f"scatter_{scatter_idx}",
157
- "ax_index": ax_idx,
158
- }
159
- scatter_idx += 1
160
-
161
- elif isinstance(coll, PolyCollection):
162
- if not coll.get_visible():
163
- continue
164
-
165
- key = f"ax{ax_idx}_fill{i}"
166
- bbox = _get_collection_bbox(
167
- coll,
168
- ax,
169
- fig,
170
- renderer,
171
- tight_bbox,
172
- img_width,
173
- img_height,
174
- scale_x,
175
- scale_y,
176
- pad_inches,
177
- saved_height_inches,
178
- include_points=False, # Fills use bbox only
179
- )
180
- if bbox:
181
- bboxes[key] = {
182
- **bbox,
183
- "type": "fill",
184
- "label": coll.get_label() or f"fill_{i}",
185
- "ax_index": ax_idx,
186
- }
187
-
188
- # Bars
189
- bar_idx = 0
190
- for i, patch in enumerate(ax.patches):
191
- if isinstance(patch, Rectangle):
192
- if not patch.get_visible():
193
- continue
194
- # Skip frame rectangles
195
- if patch.get_width() == 1.0 and patch.get_height() == 1.0:
196
- continue
197
-
198
- key = f"ax{ax_idx}_bar{bar_idx}"
199
- bbox = _get_patch_bbox(
200
- patch,
201
- ax,
202
- fig,
203
- renderer,
204
- tight_bbox,
205
- img_width,
206
- img_height,
207
- scale_x,
208
- scale_y,
209
- pad_inches,
210
- saved_height_inches,
211
- )
212
- if bbox:
213
- bboxes[key] = {
214
- **bbox,
215
- "type": "bar",
216
- "label": patch.get_label() or f"bar_{bar_idx}",
217
- "ax_index": ax_idx,
218
- }
219
- bar_idx += 1
220
-
221
- # Title
222
- title = ax.get_title()
223
- if title:
224
- key = f"ax{ax_idx}_title"
225
- bbox = _get_text_bbox(
226
- ax.title,
227
- fig,
228
- renderer,
229
- tight_bbox,
230
- img_width,
231
- img_height,
232
- scale_x,
233
- scale_y,
234
- pad_inches,
235
- saved_height_inches,
236
- )
237
- if bbox:
238
- bboxes[key] = {
239
- **bbox,
240
- "type": "title",
241
- "label": "title",
242
- "ax_index": ax_idx,
243
- "text": title,
244
- }
245
-
246
- # X label (just the label text)
247
- xlabel = ax.get_xlabel()
248
- if xlabel:
249
- key = f"ax{ax_idx}_xlabel"
250
- bbox = _get_text_bbox(
251
- ax.xaxis.label,
252
- fig,
253
- renderer,
254
- tight_bbox,
255
- img_width,
256
- img_height,
257
- scale_x,
258
- scale_y,
259
- pad_inches,
260
- saved_height_inches,
261
- )
262
- if bbox:
263
- bboxes[key] = {
264
- **bbox,
265
- "type": "xlabel",
266
- "label": "xlabel",
267
- "ax_index": ax_idx,
268
- "text": xlabel,
269
- }
270
-
271
- # X tick labels (separate hit region)
272
- xtick_bbox = _get_tick_labels_bbox(
273
- ax.xaxis,
274
- "x",
275
- fig,
276
- renderer,
277
- tight_bbox,
278
- img_width,
279
- img_height,
280
- scale_x,
281
- scale_y,
282
- pad_inches,
283
- saved_height_inches,
284
- )
285
- if xtick_bbox:
286
- bboxes[f"ax{ax_idx}_xticks"] = {
287
- **xtick_bbox,
288
- "type": "xticks",
289
- "label": "x tick labels",
290
- "ax_index": ax_idx,
291
- }
292
-
293
- # Y label (just the label text)
294
- ylabel = ax.get_ylabel()
295
- if ylabel:
296
- key = f"ax{ax_idx}_ylabel"
297
- bbox = _get_text_bbox(
298
- ax.yaxis.label,
299
- fig,
300
- renderer,
301
- tight_bbox,
302
- img_width,
303
- img_height,
304
- scale_x,
305
- scale_y,
306
- pad_inches,
307
- saved_height_inches,
308
- )
309
- if bbox:
310
- bboxes[key] = {
311
- **bbox,
312
- "type": "ylabel",
313
- "label": "ylabel",
314
- "ax_index": ax_idx,
315
- "text": ylabel,
316
- }
317
-
318
- # Y tick labels (separate hit region)
319
- ytick_bbox = _get_tick_labels_bbox(
320
- ax.yaxis,
321
- "y",
322
- fig,
323
- renderer,
324
- tight_bbox,
325
- img_width,
326
- img_height,
327
- scale_x,
328
- scale_y,
329
- pad_inches,
330
- saved_height_inches,
331
- )
332
- if ytick_bbox:
333
- bboxes[f"ax{ax_idx}_yticks"] = {
334
- **ytick_bbox,
335
- "type": "yticks",
336
- "label": "y tick labels",
337
- "ax_index": ax_idx,
338
- }
339
-
340
- # Legend
341
- legend = ax.get_legend()
342
- if legend is not None and legend.get_visible():
343
- key = f"ax{ax_idx}_legend"
344
- try:
345
- legend_bbox = legend.get_window_extent(renderer)
346
- if legend_bbox is not None:
347
- bbox = _transform_bbox(
348
- legend_bbox,
349
- fig,
350
- tight_bbox,
351
- img_width,
352
- img_height,
353
- scale_x,
354
- scale_y,
355
- pad_inches,
356
- saved_height_inches,
357
- )
358
- if bbox:
359
- bboxes[key] = {
360
- **bbox,
361
- "type": "legend",
362
- "label": "legend",
363
- "ax_index": ax_idx,
364
- }
365
- except Exception:
366
- pass
367
-
368
- # Spines (with padding for easier clicking)
369
- spine_min_size = 8 # Minimum hit region size in pixels
370
- for spine_name, spine in ax.spines.items():
371
- if spine.get_visible():
372
- key = f"ax{ax_idx}_spine_{spine_name}"
373
- try:
374
- spine_bbox = spine.get_window_extent(renderer)
375
- if spine_bbox is not None:
376
- bbox = _transform_bbox(
377
- spine_bbox,
378
- fig,
379
- tight_bbox,
380
- img_width,
381
- img_height,
382
- scale_x,
383
- scale_y,
384
- pad_inches,
385
- saved_height_inches,
386
- )
387
- if bbox:
388
- # Expand thin spines for easier clicking
389
- if bbox["width"] < spine_min_size:
390
- expand = (spine_min_size - bbox["width"]) / 2
391
- bbox["x"] -= expand
392
- bbox["width"] = spine_min_size
393
- if bbox["height"] < spine_min_size:
394
- expand = (spine_min_size - bbox["height"]) / 2
395
- bbox["y"] -= expand
396
- bbox["height"] = spine_min_size
397
- bboxes[key] = {
398
- **bbox,
399
- "type": "spine",
400
- "label": spine_name,
401
- "ax_index": ax_idx,
402
- }
403
- except Exception:
404
- pass
405
-
406
- # Process figure-level text elements (suptitle, supxlabel, supylabel)
407
- # Suptitle
408
- if hasattr(fig, "_suptitle") and fig._suptitle is not None:
409
- suptitle_obj = fig._suptitle
410
- if suptitle_obj.get_text():
411
- try:
412
- suptitle_extent = suptitle_obj.get_window_extent(renderer)
413
- if suptitle_extent is not None:
414
- bbox = _transform_bbox(
415
- suptitle_extent,
416
- fig,
417
- tight_bbox,
418
- img_width,
419
- img_height,
420
- scale_x,
421
- scale_y,
422
- pad_inches,
423
- saved_height_inches,
424
- )
425
- if bbox:
426
- bboxes["fig_suptitle"] = {
427
- **bbox,
428
- "type": "suptitle",
429
- "label": "suptitle",
430
- "ax_index": -1, # Figure-level
431
- "text": suptitle_obj.get_text(),
432
- }
433
- except Exception:
434
- pass
435
-
436
- # Supxlabel
437
- if hasattr(fig, "_supxlabel") and fig._supxlabel is not None:
438
- supxlabel_obj = fig._supxlabel
439
- if supxlabel_obj.get_text():
440
- try:
441
- supxlabel_extent = supxlabel_obj.get_window_extent(renderer)
442
- if supxlabel_extent is not None:
443
- bbox = _transform_bbox(
444
- supxlabel_extent,
445
- fig,
446
- tight_bbox,
447
- img_width,
448
- img_height,
449
- scale_x,
450
- scale_y,
451
- pad_inches,
452
- saved_height_inches,
453
- )
454
- if bbox:
455
- bboxes["fig_supxlabel"] = {
456
- **bbox,
457
- "type": "supxlabel",
458
- "label": "supxlabel",
459
- "ax_index": -1, # Figure-level
460
- "text": supxlabel_obj.get_text(),
461
- }
462
- except Exception:
463
- pass
464
-
465
- # Supylabel
466
- if hasattr(fig, "_supylabel") and fig._supylabel is not None:
467
- supylabel_obj = fig._supylabel
468
- if supylabel_obj.get_text():
469
- try:
470
- supylabel_extent = supylabel_obj.get_window_extent(renderer)
471
- if supylabel_extent is not None:
472
- bbox = _transform_bbox(
473
- supylabel_extent,
474
- fig,
475
- tight_bbox,
476
- img_width,
477
- img_height,
478
- scale_x,
479
- scale_y,
480
- pad_inches,
481
- saved_height_inches,
482
- )
483
- if bbox:
484
- bboxes["fig_supylabel"] = {
485
- **bbox,
486
- "type": "supylabel",
487
- "label": "supylabel",
488
- "ax_index": -1, # Figure-level
489
- "text": supylabel_obj.get_text(),
490
- }
491
- except Exception:
492
- pass
493
-
494
- # Add metadata
495
- bboxes["_meta"] = {
496
- "img_width": img_width,
497
- "img_height": img_height,
498
- "fig_width_inches": fig.get_figwidth(),
499
- "fig_height_inches": fig.get_figheight(),
500
- "dpi": fig.dpi,
501
- }
502
-
503
- return bboxes
504
-
505
-
506
- def _get_element_bbox(
507
- element,
508
- fig: Figure,
509
- renderer,
510
- tight_bbox: Bbox,
511
- img_width: int,
512
- img_height: int,
513
- scale_x: float,
514
- scale_y: float,
515
- pad_inches: float,
516
- saved_height_inches: float,
517
- ) -> Optional[Dict[str, float]]:
518
- """Get bbox for a general element."""
519
- try:
520
- window_extent = element.get_window_extent(renderer)
521
- if window_extent is None:
522
- return None
523
- return _transform_bbox(
524
- window_extent,
525
- fig,
526
- tight_bbox,
527
- img_width,
528
- img_height,
529
- scale_x,
530
- scale_y,
531
- pad_inches,
532
- saved_height_inches,
533
- )
534
- except Exception:
535
- return None
536
-
537
-
538
- def _get_line_bbox(
539
- line,
540
- ax: Axes,
541
- fig: Figure,
542
- renderer,
543
- tight_bbox: Bbox,
544
- img_width: int,
545
- img_height: int,
546
- scale_x: float,
547
- scale_y: float,
548
- pad_inches: float,
549
- saved_height_inches: float,
550
- include_points: bool = True,
551
- ) -> Optional[Dict[str, Any]]:
552
- """Get bbox and points for a line."""
553
- try:
554
- # Get window extent
555
- window_extent = line.get_window_extent(renderer)
556
- if window_extent is None:
557
- return None
558
-
559
- bbox = _transform_bbox(
560
- window_extent,
561
- fig,
562
- tight_bbox,
563
- img_width,
564
- img_height,
565
- scale_x,
566
- scale_y,
567
- pad_inches,
568
- saved_height_inches,
569
- )
570
- if bbox is None:
571
- return None
572
-
573
- # Add path points for proximity detection
574
- if include_points:
575
- xdata = line.get_xdata()
576
- ydata = line.get_ydata()
577
-
578
- if len(xdata) > 0 and len(ydata) > 0:
579
- # Transform data coords to image pixels
580
- transform = ax.transData
581
- points = []
582
-
583
- # Downsample if too many points
584
- max_points = 100
585
- step = max(1, len(xdata) // max_points)
586
-
587
- for i in range(0, len(xdata), step):
588
- try:
589
- display_coords = transform.transform((xdata[i], ydata[i]))
590
- img_coords = _display_to_image(
591
- display_coords[0],
592
- display_coords[1],
593
- fig,
594
- tight_bbox,
595
- img_width,
596
- img_height,
597
- scale_x,
598
- scale_y,
599
- pad_inches,
600
- saved_height_inches,
601
- )
602
- if img_coords:
603
- points.append(img_coords)
604
- except Exception:
605
- continue
606
-
607
- if points:
608
- bbox["points"] = points
609
-
610
- return bbox
611
-
612
- except Exception:
613
- return None
614
-
615
-
616
- def _get_collection_bbox(
617
- coll,
618
- ax: Axes,
619
- fig: Figure,
620
- renderer,
621
- tight_bbox: Bbox,
622
- img_width: int,
623
- img_height: int,
624
- scale_x: float,
625
- scale_y: float,
626
- pad_inches: float,
627
- saved_height_inches: float,
628
- include_points: bool = True,
629
- ) -> Optional[Dict[str, Any]]:
630
- """Get bbox and points for a collection (scatter, fill)."""
631
- try:
632
- bbox = None
633
-
634
- # For scatter plots, get_window_extent() can fail or return empty
635
- # So we calculate bbox from data points as fallback
636
- if isinstance(coll, PathCollection):
637
- offsets = coll.get_offsets()
638
- if len(offsets) > 0:
639
- transform = ax.transData
640
- points = []
641
-
642
- # Limit to reasonable number of points
643
- max_points = 200
644
- step = max(1, len(offsets) // max_points)
645
-
646
- for i in range(0, len(offsets), step):
647
- try:
648
- offset = offsets[i]
649
- display_coords = transform.transform(offset)
650
- img_coords = _display_to_image(
651
- display_coords[0],
652
- display_coords[1],
653
- fig,
654
- tight_bbox,
655
- img_width,
656
- img_height,
657
- scale_x,
658
- scale_y,
659
- pad_inches,
660
- saved_height_inches,
661
- )
662
- if img_coords:
663
- points.append(img_coords)
664
- except Exception:
665
- continue
666
-
667
- # Calculate bbox from points
668
- if points:
669
- xs = [p[0] for p in points]
670
- ys = [p[1] for p in points]
671
- # Add padding around scatter points for easier clicking
672
- padding = 10 # pixels
673
- bbox = {
674
- "x": float(min(xs) - padding),
675
- "y": float(min(ys) - padding),
676
- "width": float(max(xs) - min(xs) + 2 * padding),
677
- "height": float(max(ys) - min(ys) + 2 * padding),
678
- "points": points,
679
- }
680
- return bbox
681
-
682
- # Fallback: try standard window extent
683
- window_extent = coll.get_window_extent(renderer)
684
- if window_extent is None:
685
- return None
686
-
687
- bbox = _transform_bbox(
688
- window_extent,
689
- fig,
690
- tight_bbox,
691
- img_width,
692
- img_height,
693
- scale_x,
694
- scale_y,
695
- pad_inches,
696
- saved_height_inches,
697
- )
698
-
699
- return bbox
700
-
701
- except Exception:
702
- return None
703
-
704
-
705
- def _get_patch_bbox(
706
- patch,
707
- ax: Axes,
708
- fig: Figure,
709
- renderer,
710
- tight_bbox: Bbox,
711
- img_width: int,
712
- img_height: int,
713
- scale_x: float,
714
- scale_y: float,
715
- pad_inches: float,
716
- saved_height_inches: float,
717
- ) -> Optional[Dict[str, float]]:
718
- """Get bbox for a patch (bar, rectangle)."""
719
- try:
720
- window_extent = patch.get_window_extent(renderer)
721
- if window_extent is None:
722
- return None
723
- return _transform_bbox(
724
- window_extent,
725
- fig,
726
- tight_bbox,
727
- img_width,
728
- img_height,
729
- scale_x,
730
- scale_y,
731
- pad_inches,
732
- saved_height_inches,
733
- )
734
- except Exception:
735
- return None
736
-
737
-
738
- def _get_text_bbox(
739
- text,
740
- fig: Figure,
741
- renderer,
742
- tight_bbox: Bbox,
743
- img_width: int,
744
- img_height: int,
745
- scale_x: float,
746
- scale_y: float,
747
- pad_inches: float,
748
- saved_height_inches: float,
749
- ) -> Optional[Dict[str, float]]:
750
- """Get bbox for a text element."""
751
- try:
752
- window_extent = text.get_window_extent(renderer)
753
- if window_extent is None:
754
- return None
755
- return _transform_bbox(
756
- window_extent,
757
- fig,
758
- tight_bbox,
759
- img_width,
760
- img_height,
761
- scale_x,
762
- scale_y,
763
- pad_inches,
764
- saved_height_inches,
765
- )
766
- except Exception:
767
- return None
768
-
769
-
770
- def _get_tick_labels_bbox(
771
- axis,
772
- axis_type: str, # 'x' or 'y'
773
- fig: Figure,
774
- renderer,
775
- tight_bbox: Bbox,
776
- img_width: int,
777
- img_height: int,
778
- scale_x: float,
779
- scale_y: float,
780
- pad_inches: float,
781
- saved_height_inches: float,
782
- ) -> Optional[Dict[str, float]]:
783
- """
784
- Get bbox for tick labels, extended to span the full axis dimension.
785
-
786
- For x-axis: tick labels bbox spans the full width of the plot area.
787
- For y-axis: tick labels bbox spans the full height of the plot area.
788
- """
789
- try:
790
- all_bboxes = []
791
-
792
- # Get all tick label bboxes
793
- for tick in axis.get_major_ticks():
794
- tick_label = tick.label1 if hasattr(tick, "label1") else tick.label
795
- if tick_label and tick_label.get_visible():
796
- try:
797
- tick_extent = tick_label.get_window_extent(renderer)
798
- if tick_extent is not None and tick_extent.width > 0:
799
- all_bboxes.append(tick_extent)
800
- except Exception:
801
- pass
802
-
803
- if not all_bboxes:
804
- return None
805
-
806
- # Merge all tick label bboxes
807
- merged = all_bboxes[0]
808
- for bbox in all_bboxes[1:]:
809
- merged = Bbox.union([merged, bbox])
810
-
811
- # Get the axes extent to extend the tick labels region
812
- ax = axis.axes
813
- ax_bbox = ax.get_window_extent(renderer)
814
-
815
- if axis_type == "x":
816
- # For x-axis: extend width to match axes width, keep tick labels height
817
- merged = Bbox.from_extents(
818
- ax_bbox.x0, # Align left with axes
819
- merged.y0, # Keep tick labels y position
820
- ax_bbox.x1, # Align right with axes
821
- merged.y1, # Keep tick labels height
822
- )
823
- else: # y-axis
824
- # For y-axis: extend height to match axes height, keep tick labels width
825
- merged = Bbox.from_extents(
826
- merged.x0, # Keep tick labels x position
827
- ax_bbox.y0, # Align bottom with axes
828
- merged.x1, # Keep tick labels width
829
- ax_bbox.y1, # Align top with axes
830
- )
831
-
832
- return _transform_bbox(
833
- merged,
834
- fig,
835
- tight_bbox,
836
- img_width,
837
- img_height,
838
- scale_x,
839
- scale_y,
840
- pad_inches,
841
- saved_height_inches,
842
- )
843
-
844
- except Exception:
845
- return None
846
-
847
-
848
- def _transform_bbox(
849
- window_extent: Bbox,
850
- fig: Figure,
851
- tight_bbox: Bbox,
852
- img_width: int,
853
- img_height: int,
854
- scale_x: float,
855
- scale_y: float,
856
- pad_inches: float,
857
- saved_height_inches: float,
858
- ) -> Optional[Dict[str, float]]:
859
- """
860
- Transform matplotlib window extent to image pixel coordinates.
861
-
862
- Parameters
863
- ----------
864
- window_extent : Bbox
865
- Bbox in display coordinates (points).
866
- fig : Figure
867
- Matplotlib figure.
868
- tight_bbox : Bbox
869
- Tight bbox of figure in inches.
870
- img_width, img_height : int
871
- Output image dimensions.
872
- scale_x, scale_y : float
873
- Scale factors from inches to pixels.
874
- pad_inches : float
875
- Padding added by bbox_inches='tight' (default 0.1).
876
- saved_height_inches : float
877
- Total saved image height including padding.
878
-
879
- Returns
880
- -------
881
- dict or None
882
- {x, y, width, height} in image pixels.
883
- """
884
- try:
885
- dpi = fig.dpi
886
-
887
- # Convert display coords to inches
888
- x0_inches = window_extent.x0 / dpi
889
- y0_inches = window_extent.y0 / dpi
890
- x1_inches = window_extent.x1 / dpi
891
- y1_inches = window_extent.y1 / dpi
892
-
893
- # Transform to saved image coordinates
894
- # Account for tight bbox origin and padding
895
- x0_rel = x0_inches - tight_bbox.x0 + pad_inches
896
- x1_rel = x1_inches - tight_bbox.x0 + pad_inches
897
-
898
- # Y coordinate flip: matplotlib Y=0 at bottom, image Y=0 at top
899
- y0_rel = saved_height_inches - (y1_inches - tight_bbox.y0 + pad_inches)
900
- y1_rel = saved_height_inches - (y0_inches - tight_bbox.y0 + pad_inches)
901
-
902
- # Scale to image pixels
903
- x0_px = x0_rel * scale_x
904
- y0_px = y0_rel * scale_y
905
- x1_px = x1_rel * scale_x
906
- y1_px = y1_rel * scale_y
907
-
908
- # Clamp to bounds
909
- x0_px = max(0, min(x0_px, img_width))
910
- x1_px = max(0, min(x1_px, img_width))
911
- y0_px = max(0, min(y0_px, img_height))
912
- y1_px = max(0, min(y1_px, img_height))
913
-
914
- width = x1_px - x0_px
915
- height = y1_px - y0_px
916
-
917
- if width <= 0 or height <= 0:
918
- return None
919
-
920
- return {
921
- "x": float(x0_px),
922
- "y": float(y0_px),
923
- "width": float(width),
924
- "height": float(height),
925
- }
926
-
927
- except Exception:
928
- return None
929
-
930
-
931
- def _display_to_image(
932
- display_x: float,
933
- display_y: float,
934
- fig: Figure,
935
- tight_bbox: Bbox,
936
- img_width: int,
937
- img_height: int,
938
- scale_x: float,
939
- scale_y: float,
940
- pad_inches: float,
941
- saved_height_inches: float,
942
- ) -> Optional[List[float]]:
943
- """
944
- Transform display coordinates to image pixel coordinates.
945
-
946
- Returns
947
- -------
948
- list or None
949
- [x, y] in image pixels.
950
- """
951
- try:
952
- dpi = fig.dpi
953
-
954
- # Convert to inches
955
- x_inches = display_x / dpi
956
- y_inches = display_y / dpi
957
-
958
- # Transform to saved image coordinates with padding
959
- x_rel = x_inches - tight_bbox.x0 + pad_inches
960
-
961
- # Y coordinate flip: matplotlib Y=0 at bottom, image Y=0 at top
962
- y_rel = saved_height_inches - (y_inches - tight_bbox.y0 + pad_inches)
963
-
964
- # Scale to image pixels
965
- x_px = x_rel * scale_x
966
- y_px = y_rel * scale_y
967
-
968
- # Clamp
969
- x_px = max(0, min(x_px, img_width))
970
- y_px = max(0, min(y_px, img_height))
971
-
972
- return [float(x_px), float(y_px)]
973
-
974
- except Exception:
975
- return None
976
-
977
-
978
- __all__ = ["extract_bboxes"]