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.
- figrecipe/__init__.py +161 -1030
- figrecipe/__main__.py +12 -0
- figrecipe/_api/__init__.py +48 -0
- figrecipe/_api/_extract.py +108 -0
- figrecipe/_api/_notebook.py +61 -0
- figrecipe/_api/_panel.py +113 -0
- figrecipe/_api/_save.py +287 -0
- figrecipe/_api/_seaborn_proxy.py +34 -0
- figrecipe/_api/_style_manager.py +153 -0
- figrecipe/_api/_subplots.py +333 -0
- figrecipe/_api/_validate.py +82 -0
- figrecipe/_cli/__init__.py +7 -0
- figrecipe/_cli/_compose.py +87 -0
- figrecipe/_cli/_convert.py +117 -0
- figrecipe/_cli/_crop.py +82 -0
- figrecipe/_cli/_edit.py +70 -0
- figrecipe/_cli/_extract.py +128 -0
- figrecipe/_cli/_fonts.py +47 -0
- figrecipe/_cli/_info.py +67 -0
- figrecipe/_cli/_main.py +58 -0
- figrecipe/_cli/_reproduce.py +79 -0
- figrecipe/_cli/_style.py +77 -0
- figrecipe/_cli/_validate.py +66 -0
- figrecipe/_cli/_version.py +50 -0
- figrecipe/_composition/__init__.py +32 -0
- figrecipe/_composition/_alignment.py +452 -0
- figrecipe/_composition/_compose.py +179 -0
- figrecipe/_composition/_import_axes.py +127 -0
- figrecipe/_composition/_visibility.py +125 -0
- figrecipe/_dev/__init__.py +4 -93
- figrecipe/_dev/_plotters.py +76 -0
- figrecipe/_dev/_run_demos.py +56 -0
- figrecipe/_dev/browser/__init__.py +69 -0
- figrecipe/_dev/browser/_audio.py +240 -0
- figrecipe/_dev/browser/_caption.py +356 -0
- figrecipe/_dev/browser/_click_effect.py +146 -0
- figrecipe/_dev/browser/_cursor.py +196 -0
- figrecipe/_dev/browser/_highlight.py +105 -0
- figrecipe/_dev/browser/_narration.py +237 -0
- figrecipe/_dev/browser/_recorder.py +446 -0
- figrecipe/_dev/browser/_utils.py +178 -0
- figrecipe/_dev/browser/_video_trim/__init__.py +152 -0
- figrecipe/_dev/browser/_video_trim/_detection.py +223 -0
- figrecipe/_dev/browser/_video_trim/_markers.py +140 -0
- figrecipe/_dev/demo_plotters/__init__.py +35 -166
- figrecipe/_dev/demo_plotters/_categories.py +81 -0
- figrecipe/_dev/demo_plotters/_figure_creators.py +119 -0
- figrecipe/_dev/demo_plotters/_helpers.py +31 -0
- figrecipe/_dev/demo_plotters/_registry.py +50 -0
- figrecipe/_dev/demo_plotters/bar_categorical/__init__.py +4 -0
- figrecipe/_dev/demo_plotters/contour_surface/__init__.py +4 -0
- figrecipe/_dev/demo_plotters/distribution/__init__.py +4 -0
- figrecipe/_dev/demo_plotters/image_matrix/__init__.py +4 -0
- figrecipe/_dev/demo_plotters/line_curve/__init__.py +4 -0
- figrecipe/_dev/demo_plotters/{plot_plot.py → line_curve/plot_plot.py} +3 -2
- figrecipe/_dev/demo_plotters/scatter_points/__init__.py +4 -0
- figrecipe/_dev/demo_plotters/special/__init__.py +4 -0
- figrecipe/_dev/demo_plotters/{plot_pie.py → special/plot_pie.py} +5 -1
- figrecipe/_dev/demo_plotters/spectral_signal/__init__.py +4 -0
- figrecipe/_dev/demo_plotters/vector_flow/__init__.py +4 -0
- figrecipe/_editor/__init__.py +61 -13
- figrecipe/_editor/_bbox/__init__.py +43 -0
- figrecipe/_editor/_bbox/_collections.py +177 -0
- figrecipe/_editor/_bbox/_elements.py +159 -0
- figrecipe/_editor/_bbox/_extract.py +402 -0
- figrecipe/_editor/_bbox/_extract_axes.py +370 -0
- figrecipe/_editor/_bbox/_extract_text.py +466 -0
- figrecipe/_editor/_bbox/_lines.py +173 -0
- figrecipe/_editor/_bbox/_transforms.py +146 -0
- figrecipe/_editor/_call_overrides.py +183 -0
- figrecipe/_editor/_datatable_plot_handlers.py +249 -0
- figrecipe/_editor/_figure_layout.py +211 -0
- figrecipe/_editor/_flask_app.py +200 -1030
- figrecipe/_editor/_helpers.py +251 -0
- figrecipe/_editor/_hitmap/__init__.py +76 -0
- figrecipe/_editor/_hitmap/_artists/__init__.py +21 -0
- figrecipe/_editor/_hitmap/_artists/_collections.py +345 -0
- figrecipe/_editor/_hitmap/_artists/_images.py +68 -0
- figrecipe/_editor/_hitmap/_artists/_lines.py +107 -0
- figrecipe/_editor/_hitmap/_artists/_patches.py +163 -0
- figrecipe/_editor/_hitmap/_artists/_text.py +190 -0
- figrecipe/_editor/_hitmap/_colors.py +181 -0
- figrecipe/_editor/_hitmap/_detect.py +194 -0
- figrecipe/_editor/_hitmap/_restore.py +154 -0
- figrecipe/_editor/_hitmap_main.py +182 -0
- figrecipe/_editor/_overrides.py +4 -1
- figrecipe/_editor/_plot_types_registry.py +190 -0
- figrecipe/_editor/_preferences.py +135 -0
- figrecipe/_editor/_render_overrides.py +507 -0
- figrecipe/_editor/_renderer.py +81 -186
- figrecipe/_editor/_routes_annotation.py +114 -0
- figrecipe/_editor/_routes_axis.py +482 -0
- figrecipe/_editor/_routes_captions.py +130 -0
- figrecipe/_editor/_routes_composition.py +270 -0
- figrecipe/_editor/_routes_core.py +126 -0
- figrecipe/_editor/_routes_datatable.py +364 -0
- figrecipe/_editor/_routes_element.py +335 -0
- figrecipe/_editor/_routes_files.py +443 -0
- figrecipe/_editor/_routes_image.py +200 -0
- figrecipe/_editor/_routes_snapshot.py +94 -0
- figrecipe/_editor/_routes_style.py +243 -0
- figrecipe/_editor/_templates/__init__.py +116 -1
- figrecipe/_editor/_templates/_html.py +154 -64
- figrecipe/_editor/_templates/_html_components/__init__.py +13 -0
- figrecipe/_editor/_templates/_html_components/_composition_toolbar.py +79 -0
- figrecipe/_editor/_templates/_html_components/_file_browser.py +41 -0
- figrecipe/_editor/_templates/_html_datatable.py +92 -0
- figrecipe/_editor/_templates/_scripts/__init__.py +178 -0
- figrecipe/_editor/_templates/_scripts/_accordion.py +328 -0
- figrecipe/_editor/_templates/_scripts/_annotation_drag.py +504 -0
- figrecipe/_editor/_templates/_scripts/_api.py +228 -0
- figrecipe/_editor/_templates/_scripts/_canvas_context_menu.py +182 -0
- figrecipe/_editor/_templates/_scripts/_captions.py +231 -0
- figrecipe/_editor/_templates/_scripts/_colors.py +485 -0
- figrecipe/_editor/_templates/_scripts/_composition.py +283 -0
- figrecipe/_editor/_templates/_scripts/_core.py +493 -0
- figrecipe/_editor/_templates/_scripts/_datatable/__init__.py +59 -0
- figrecipe/_editor/_templates/_scripts/_datatable/_cell_edit.py +97 -0
- figrecipe/_editor/_templates/_scripts/_datatable/_clipboard.py +164 -0
- figrecipe/_editor/_templates/_scripts/_datatable/_context_menu.py +221 -0
- figrecipe/_editor/_templates/_scripts/_datatable/_core.py +150 -0
- figrecipe/_editor/_templates/_scripts/_datatable/_editable.py +511 -0
- figrecipe/_editor/_templates/_scripts/_datatable/_import.py +161 -0
- figrecipe/_editor/_templates/_scripts/_datatable/_plot.py +261 -0
- figrecipe/_editor/_templates/_scripts/_datatable/_selection.py +438 -0
- figrecipe/_editor/_templates/_scripts/_datatable/_table.py +256 -0
- figrecipe/_editor/_templates/_scripts/_datatable/_tabs.py +354 -0
- figrecipe/_editor/_templates/_scripts/_debug_snapshot.py +186 -0
- figrecipe/_editor/_templates/_scripts/_element_editor.py +325 -0
- figrecipe/_editor/_templates/_scripts/_files.py +429 -0
- figrecipe/_editor/_templates/_scripts/_files_context_menu.py +240 -0
- figrecipe/_editor/_templates/_scripts/_hitmap.py +512 -0
- figrecipe/_editor/_templates/_scripts/_image_drop.py +428 -0
- figrecipe/_editor/_templates/_scripts/_inspector.py +315 -0
- figrecipe/_editor/_templates/_scripts/_labels.py +464 -0
- figrecipe/_editor/_templates/_scripts/_legend_drag.py +270 -0
- figrecipe/_editor/_templates/_scripts/_modals.py +226 -0
- figrecipe/_editor/_templates/_scripts/_multi_select.py +198 -0
- figrecipe/_editor/_templates/_scripts/_overlays.py +292 -0
- figrecipe/_editor/_templates/_scripts/_panel_drag.py +505 -0
- figrecipe/_editor/_templates/_scripts/_panel_drag_snapshot.py +33 -0
- figrecipe/_editor/_templates/_scripts/_panel_position.py +463 -0
- figrecipe/_editor/_templates/_scripts/_panel_resize.py +230 -0
- figrecipe/_editor/_templates/_scripts/_panel_snap.py +307 -0
- figrecipe/_editor/_templates/_scripts/_region_select.py +255 -0
- figrecipe/_editor/_templates/_scripts/_selection.py +244 -0
- figrecipe/_editor/_templates/_scripts/_sync.py +242 -0
- figrecipe/_editor/_templates/_scripts/_tabs.py +89 -0
- figrecipe/_editor/_templates/_scripts/_undo_redo.py +348 -0
- figrecipe/_editor/_templates/_scripts/_view_mode.py +107 -0
- figrecipe/_editor/_templates/_scripts/_zoom.py +212 -0
- figrecipe/_editor/_templates/_styles/__init__.py +78 -0
- figrecipe/_editor/_templates/_styles/_base.py +111 -0
- figrecipe/_editor/_templates/_styles/_buttons.py +327 -0
- figrecipe/_editor/_templates/_styles/_color_input.py +123 -0
- figrecipe/_editor/_templates/_styles/_composition.py +87 -0
- figrecipe/_editor/_templates/_styles/_controls.py +430 -0
- figrecipe/_editor/_templates/_styles/_datatable/__init__.py +40 -0
- figrecipe/_editor/_templates/_styles/_datatable/_editable.py +203 -0
- figrecipe/_editor/_templates/_styles/_datatable/_panel.py +268 -0
- figrecipe/_editor/_templates/_styles/_datatable/_table.py +479 -0
- figrecipe/_editor/_templates/_styles/_datatable/_toolbar.py +384 -0
- figrecipe/_editor/_templates/_styles/_datatable/_vars.py +123 -0
- figrecipe/_editor/_templates/_styles/_dynamic_props.py +144 -0
- figrecipe/_editor/_templates/_styles/_file_browser.py +466 -0
- figrecipe/_editor/_templates/_styles/_forms.py +224 -0
- figrecipe/_editor/_templates/_styles/_hitmap.py +191 -0
- figrecipe/_editor/_templates/_styles/_inspector.py +90 -0
- figrecipe/_editor/_templates/_styles/_labels.py +118 -0
- figrecipe/_editor/_templates/_styles/_modals.py +127 -0
- figrecipe/_editor/_templates/_styles/_overlays.py +130 -0
- figrecipe/_editor/_templates/_styles/_preview.py +430 -0
- figrecipe/_editor/_templates/_styles/_selection.py +73 -0
- figrecipe/_editor/_templates/_styles/_spinner.py +117 -0
- figrecipe/_editor/static/audio/click.mp3 +0 -0
- figrecipe/_editor/static/click.mp3 +0 -0
- figrecipe/_editor/static/icons/favicon.ico +0 -0
- figrecipe/_integrations/__init__.py +17 -0
- figrecipe/_integrations/_scitex_stats.py +298 -0
- figrecipe/_params/_DECORATION_METHODS.py +8 -0
- figrecipe/_recorder.py +63 -109
- figrecipe/_recorder_utils.py +124 -0
- figrecipe/_reproducer/__init__.py +18 -0
- figrecipe/_reproducer/_core.py +509 -0
- figrecipe/_reproducer/_custom_plots.py +279 -0
- figrecipe/_reproducer/_seaborn.py +100 -0
- figrecipe/_reproducer/_violin.py +186 -0
- figrecipe/_signatures/_kwargs.py +273 -0
- figrecipe/_signatures/_loader.py +21 -423
- figrecipe/_signatures/_parsing.py +147 -0
- figrecipe/_utils/__init__.py +3 -0
- figrecipe/_utils/_bundle.py +205 -0
- figrecipe/_wrappers/_axes.py +252 -895
- figrecipe/_wrappers/_axes_helpers.py +136 -0
- figrecipe/_wrappers/_axes_plots.py +418 -0
- figrecipe/_wrappers/_axes_seaborn.py +157 -0
- figrecipe/_wrappers/_caption_generator.py +218 -0
- figrecipe/_wrappers/_figure.py +188 -1
- figrecipe/_wrappers/_panel_labels.py +127 -0
- figrecipe/_wrappers/_plot_helpers.py +143 -0
- figrecipe/_wrappers/_stat_annotation.py +274 -0
- figrecipe/_wrappers/_violin_helpers.py +180 -0
- figrecipe/styles/__init__.py +8 -6
- figrecipe/styles/_dotdict.py +72 -0
- figrecipe/styles/_finalize.py +134 -0
- figrecipe/styles/_fonts.py +77 -0
- figrecipe/styles/_kwargs_converter.py +178 -0
- figrecipe/styles/_plot_styles.py +209 -0
- figrecipe/styles/_style_applier.py +42 -480
- figrecipe/styles/_style_loader.py +16 -192
- figrecipe/styles/_themes.py +151 -0
- figrecipe/styles/presets/MATPLOTLIB.yaml +2 -1
- figrecipe/styles/presets/SCITEX.yaml +40 -28
- figrecipe-0.9.0.dist-info/METADATA +427 -0
- figrecipe-0.9.0.dist-info/RECORD +277 -0
- figrecipe-0.9.0.dist-info/entry_points.txt +2 -0
- figrecipe/_editor/_bbox.py +0 -978
- figrecipe/_editor/_hitmap.py +0 -937
- figrecipe/_editor/_templates/_scripts.py +0 -2778
- figrecipe/_editor/_templates/_styles.py +0 -1326
- figrecipe/_reproducer.py +0 -975
- figrecipe-0.6.0.dist-info/METADATA +0 -394
- figrecipe-0.6.0.dist-info/RECORD +0 -90
- /figrecipe/_dev/demo_plotters/{plot_bar.py → bar_categorical/plot_bar.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_barh.py → bar_categorical/plot_barh.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_contour.py → contour_surface/plot_contour.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_contourf.py → contour_surface/plot_contourf.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_tricontour.py → contour_surface/plot_tricontour.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_tricontourf.py → contour_surface/plot_tricontourf.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_tripcolor.py → contour_surface/plot_tripcolor.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_triplot.py → contour_surface/plot_triplot.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_boxplot.py → distribution/plot_boxplot.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_ecdf.py → distribution/plot_ecdf.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_hist.py → distribution/plot_hist.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_hist2d.py → distribution/plot_hist2d.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_violinplot.py → distribution/plot_violinplot.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_hexbin.py → image_matrix/plot_hexbin.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_imshow.py → image_matrix/plot_imshow.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_matshow.py → image_matrix/plot_matshow.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_pcolor.py → image_matrix/plot_pcolor.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_pcolormesh.py → image_matrix/plot_pcolormesh.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_spy.py → image_matrix/plot_spy.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_errorbar.py → line_curve/plot_errorbar.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_fill.py → line_curve/plot_fill.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_fill_between.py → line_curve/plot_fill_between.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_fill_betweenx.py → line_curve/plot_fill_betweenx.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_stackplot.py → line_curve/plot_stackplot.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_stairs.py → line_curve/plot_stairs.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_step.py → line_curve/plot_step.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_scatter.py → scatter_points/plot_scatter.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_eventplot.py → special/plot_eventplot.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_loglog.py → special/plot_loglog.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_semilogx.py → special/plot_semilogx.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_semilogy.py → special/plot_semilogy.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_stem.py → special/plot_stem.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_acorr.py → spectral_signal/plot_acorr.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_angle_spectrum.py → spectral_signal/plot_angle_spectrum.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_cohere.py → spectral_signal/plot_cohere.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_csd.py → spectral_signal/plot_csd.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_magnitude_spectrum.py → spectral_signal/plot_magnitude_spectrum.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_phase_spectrum.py → spectral_signal/plot_phase_spectrum.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_psd.py → spectral_signal/plot_psd.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_specgram.py → spectral_signal/plot_specgram.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_xcorr.py → spectral_signal/plot_xcorr.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_barbs.py → vector_flow/plot_barbs.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_quiver.py → vector_flow/plot_quiver.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_streamplot.py → vector_flow/plot_streamplot.py} +0 -0
- {figrecipe-0.6.0.dist-info → figrecipe-0.9.0.dist-info}/WHEEL +0 -0
- {figrecipe-0.6.0.dist-info → figrecipe-0.9.0.dist-info}/licenses/LICENSE +0 -0
figrecipe/_editor/_bbox.py
DELETED
|
@@ -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"]
|