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
@@ -0,0 +1,402 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ Main bounding box extraction function.
5
+
6
+ This module contains the main extract_bboxes function that coordinates
7
+ all element-specific bbox extractors.
8
+ """
9
+
10
+ from typing import Any, Dict
11
+
12
+ from matplotlib.figure import Figure
13
+ from matplotlib.transforms import Bbox
14
+
15
+ from ._elements import get_element_bbox
16
+ from ._extract_axes import (
17
+ extract_collections,
18
+ extract_images,
19
+ extract_lines,
20
+ extract_patches,
21
+ )
22
+ from ._extract_text import (
23
+ extract_annotations,
24
+ extract_figure_text,
25
+ extract_legend,
26
+ extract_spines,
27
+ extract_text_elements,
28
+ )
29
+
30
+
31
+ def extract_bboxes(
32
+ fig: Figure,
33
+ img_width: int,
34
+ img_height: int,
35
+ include_points: bool = True,
36
+ ) -> Dict[str, Dict[str, Any]]:
37
+ """
38
+ Extract bounding boxes for all figure elements.
39
+
40
+ Parameters
41
+ ----------
42
+ fig : matplotlib.figure.Figure
43
+ Figure to extract bboxes from.
44
+ img_width : int
45
+ Width of the output image in pixels.
46
+ img_height : int
47
+ Height of the output image in pixels.
48
+ include_points : bool, optional
49
+ Whether to include point arrays for lines/scatter (default: True).
50
+
51
+ Returns
52
+ -------
53
+ dict
54
+ Mapping from element key to bbox info.
55
+ """
56
+ bboxes = {}
57
+
58
+ # Get renderer for bbox calculations
59
+ # Handle matplotlib's Done exception from _get_renderer (can occur with corrupted canvas state)
60
+ try:
61
+ fig.canvas.draw()
62
+ except Exception as e:
63
+ # Matplotlib's Done exception or other draw issues - reset canvas and retry
64
+ if "Done" in str(type(e).__name__) or "Done" in str(e):
65
+ from matplotlib.backends.backend_agg import FigureCanvasAgg
66
+
67
+ fig.set_canvas(FigureCanvasAgg(fig))
68
+ try:
69
+ fig.canvas.draw()
70
+ except Exception:
71
+ pass # Continue with potentially stale renderer
72
+ else:
73
+ raise
74
+ renderer = fig.canvas.get_renderer()
75
+
76
+ # Get figure bounds for coordinate transformation
77
+ # NOTE: We do NOT use bbox_inches='tight' in savefig, so use fixed figsize
78
+ fig_width_inches = fig.get_figwidth()
79
+ fig_height_inches = fig.get_figheight()
80
+
81
+ # Create a bbox representing the full figure (for compatibility with element extraction)
82
+ tight_bbox = Bbox.from_bounds(0, 0, fig_width_inches, fig_height_inches)
83
+
84
+ # No padding since we don't use bbox_inches='tight'
85
+ pad_inches = 0.0
86
+ saved_width_inches = fig_width_inches
87
+ saved_height_inches = fig_height_inches
88
+
89
+ # Calculate scale factors from saved image size to pixel size
90
+ scale_x = img_width / saved_width_inches if saved_width_inches > 0 else 1
91
+ scale_y = img_height / saved_height_inches if saved_height_inches > 0 else 1
92
+
93
+ # Process each axes
94
+ for ax_idx, ax in enumerate(fig.get_axes()):
95
+ _extract_axes_bboxes(
96
+ ax,
97
+ ax_idx,
98
+ fig,
99
+ renderer,
100
+ tight_bbox,
101
+ img_width,
102
+ img_height,
103
+ scale_x,
104
+ scale_y,
105
+ pad_inches,
106
+ saved_height_inches,
107
+ include_points,
108
+ bboxes,
109
+ )
110
+
111
+ # Process figure-level text elements
112
+ extract_figure_text(
113
+ fig,
114
+ renderer,
115
+ tight_bbox,
116
+ img_width,
117
+ img_height,
118
+ scale_x,
119
+ scale_y,
120
+ pad_inches,
121
+ saved_height_inches,
122
+ bboxes,
123
+ )
124
+
125
+ # Compute panel bboxes using tight bbox from matplotlib
126
+ panel_bboxes = _compute_panel_bboxes(
127
+ bboxes,
128
+ len(fig.get_axes()),
129
+ fig=fig,
130
+ renderer=renderer,
131
+ img_width=img_width,
132
+ img_height=img_height,
133
+ )
134
+
135
+ # Add metadata
136
+ bboxes["_meta"] = {
137
+ "img_width": img_width,
138
+ "img_height": img_height,
139
+ "fig_width_inches": fig.get_figwidth(),
140
+ "fig_height_inches": fig.get_figheight(),
141
+ "dpi": fig.dpi,
142
+ "panel_bboxes": panel_bboxes,
143
+ }
144
+
145
+ return bboxes
146
+
147
+
148
+ def _extract_axes_bboxes(
149
+ ax,
150
+ ax_idx,
151
+ fig,
152
+ renderer,
153
+ tight_bbox,
154
+ img_width,
155
+ img_height,
156
+ scale_x,
157
+ scale_y,
158
+ pad_inches,
159
+ saved_height_inches,
160
+ include_points,
161
+ bboxes,
162
+ ):
163
+ """Extract bboxes for all elements within an axes."""
164
+ # Axes bounding box
165
+ ax_bbox = get_element_bbox(
166
+ ax,
167
+ fig,
168
+ renderer,
169
+ tight_bbox,
170
+ img_width,
171
+ img_height,
172
+ scale_x,
173
+ scale_y,
174
+ pad_inches,
175
+ saved_height_inches,
176
+ )
177
+ if ax_bbox:
178
+ bboxes[f"ax{ax_idx}_axes"] = {**ax_bbox, "type": "axes", "ax_index": ax_idx}
179
+
180
+ # Extract all element types
181
+ extract_lines(
182
+ ax,
183
+ ax_idx,
184
+ fig,
185
+ renderer,
186
+ tight_bbox,
187
+ img_width,
188
+ img_height,
189
+ scale_x,
190
+ scale_y,
191
+ pad_inches,
192
+ saved_height_inches,
193
+ include_points,
194
+ bboxes,
195
+ )
196
+ extract_collections(
197
+ ax,
198
+ ax_idx,
199
+ fig,
200
+ renderer,
201
+ tight_bbox,
202
+ img_width,
203
+ img_height,
204
+ scale_x,
205
+ scale_y,
206
+ pad_inches,
207
+ saved_height_inches,
208
+ include_points,
209
+ bboxes,
210
+ )
211
+ extract_patches(
212
+ ax,
213
+ ax_idx,
214
+ fig,
215
+ renderer,
216
+ tight_bbox,
217
+ img_width,
218
+ img_height,
219
+ scale_x,
220
+ scale_y,
221
+ pad_inches,
222
+ saved_height_inches,
223
+ bboxes,
224
+ )
225
+ extract_images(
226
+ ax,
227
+ ax_idx,
228
+ fig,
229
+ renderer,
230
+ tight_bbox,
231
+ img_width,
232
+ img_height,
233
+ scale_x,
234
+ scale_y,
235
+ pad_inches,
236
+ saved_height_inches,
237
+ bboxes,
238
+ )
239
+ extract_text_elements(
240
+ ax,
241
+ ax_idx,
242
+ fig,
243
+ renderer,
244
+ tight_bbox,
245
+ img_width,
246
+ img_height,
247
+ scale_x,
248
+ scale_y,
249
+ pad_inches,
250
+ saved_height_inches,
251
+ bboxes,
252
+ )
253
+ extract_legend(
254
+ ax,
255
+ ax_idx,
256
+ fig,
257
+ renderer,
258
+ tight_bbox,
259
+ img_width,
260
+ img_height,
261
+ scale_x,
262
+ scale_y,
263
+ pad_inches,
264
+ saved_height_inches,
265
+ bboxes,
266
+ )
267
+ extract_spines(
268
+ ax,
269
+ ax_idx,
270
+ fig,
271
+ renderer,
272
+ tight_bbox,
273
+ img_width,
274
+ img_height,
275
+ scale_x,
276
+ scale_y,
277
+ pad_inches,
278
+ saved_height_inches,
279
+ bboxes,
280
+ )
281
+ extract_annotations(
282
+ ax,
283
+ ax_idx,
284
+ fig,
285
+ renderer,
286
+ tight_bbox,
287
+ img_width,
288
+ img_height,
289
+ scale_x,
290
+ scale_y,
291
+ pad_inches,
292
+ saved_height_inches,
293
+ bboxes,
294
+ )
295
+
296
+
297
+ def _compute_panel_bboxes(
298
+ bboxes: Dict[str, Any],
299
+ num_axes: int,
300
+ fig: Figure = None,
301
+ renderer=None,
302
+ img_width: int = None,
303
+ img_height: int = None,
304
+ ) -> Dict[int, Dict[str, float]]:
305
+ """
306
+ Compute tight bounding box for each panel (axis).
307
+
308
+ Uses matplotlib's ax.get_tightbbox() for reliable panel bounds that include
309
+ all decorators (title, labels, tick labels) without outliers.
310
+
311
+ Falls back to union of elements if tight bbox is unavailable.
312
+
313
+ Parameters
314
+ ----------
315
+ bboxes : dict
316
+ All extracted element bboxes.
317
+ num_axes : int
318
+ Number of axes in the figure.
319
+ fig : Figure, optional
320
+ Matplotlib figure (needed for tight bbox computation).
321
+ renderer : optional
322
+ Matplotlib renderer (needed for tight bbox computation).
323
+ img_width : int, optional
324
+ Image width in pixels.
325
+ img_height : int, optional
326
+ Image height in pixels.
327
+
328
+ Returns
329
+ -------
330
+ dict
331
+ Mapping from ax_index to panel bbox: {ax_index: {x, y, width, height}}
332
+ """
333
+ panel_bboxes = {}
334
+
335
+ # Try to use tight bbox if figure is available
336
+ if fig is not None and renderer is not None and img_width and img_height:
337
+ fig_width_inches = fig.get_figwidth()
338
+ fig_height_inches = fig.get_figheight()
339
+ dpi = fig.dpi
340
+
341
+ for ax_idx, ax in enumerate(fig.get_axes()):
342
+ try:
343
+ tight = ax.get_tightbbox(renderer)
344
+ if tight is not None:
345
+ # Convert from display coords to image coords
346
+ # Display y=0 is at bottom, image y=0 is at top
347
+ x = tight.x0 * img_width / (fig_width_inches * dpi)
348
+ y = img_height - tight.y1 * img_height / (fig_height_inches * dpi)
349
+ width = tight.width * img_width / (fig_width_inches * dpi)
350
+ height = tight.height * img_height / (fig_height_inches * dpi)
351
+ panel_bboxes[ax_idx] = {
352
+ "x": x,
353
+ "y": y,
354
+ "width": width,
355
+ "height": height,
356
+ }
357
+ except Exception:
358
+ pass # Fall back to union method below
359
+
360
+ # Fall back to union method for any missing panels
361
+ for ax_idx in range(num_axes):
362
+ if ax_idx in panel_bboxes:
363
+ continue
364
+
365
+ min_x, min_y = float("inf"), float("inf")
366
+ max_x, max_y = float("-inf"), float("-inf")
367
+
368
+ for key, bbox in bboxes.items():
369
+ if key == "_meta" or not isinstance(bbox, dict):
370
+ continue
371
+ elem_ax_index = bbox.get("ax_index")
372
+ if elem_ax_index is None:
373
+ if key.startswith("ax") and "_" in key:
374
+ try:
375
+ elem_ax_index = int(key.split("_")[0][2:])
376
+ except (ValueError, IndexError):
377
+ continue
378
+ else:
379
+ continue
380
+ if elem_ax_index != ax_idx:
381
+ continue
382
+ x, y = bbox.get("x"), bbox.get("y")
383
+ w, h = bbox.get("width"), bbox.get("height")
384
+ if x is None or y is None or w is None or h is None:
385
+ continue
386
+ min_x, min_y = min(min_x, x), min(min_y, y)
387
+ max_x, max_y = max(max_x, x + w), max(max_y, y + h)
388
+
389
+ if min_x != float("inf"):
390
+ panel_bboxes[ax_idx] = {
391
+ "x": min_x,
392
+ "y": min_y,
393
+ "width": max_x - min_x,
394
+ "height": max_y - min_y,
395
+ }
396
+
397
+ return panel_bboxes
398
+
399
+
400
+ __all__ = ["extract_bboxes"]
401
+
402
+ # EOF