figrecipe 0.6.0__py3-none-any.whl → 0.7.4__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- figrecipe/__init__.py +106 -973
- figrecipe/_api/__init__.py +48 -0
- figrecipe/_api/_extract.py +108 -0
- figrecipe/_api/_notebook.py +61 -0
- figrecipe/_api/_panel.py +46 -0
- figrecipe/_api/_save.py +191 -0
- figrecipe/_api/_seaborn_proxy.py +34 -0
- figrecipe/_api/_style_manager.py +153 -0
- figrecipe/_api/_subplots.py +333 -0
- figrecipe/_api/_validate.py +82 -0
- figrecipe/_dev/__init__.py +2 -93
- figrecipe/_dev/_plotters.py +76 -0
- figrecipe/_dev/_run_demos.py +56 -0
- figrecipe/_dev/demo_plotters/__init__.py +35 -166
- figrecipe/_dev/demo_plotters/_categories.py +81 -0
- figrecipe/_dev/demo_plotters/_figure_creators.py +119 -0
- figrecipe/_dev/demo_plotters/_helpers.py +31 -0
- figrecipe/_dev/demo_plotters/_registry.py +50 -0
- figrecipe/_dev/demo_plotters/bar_categorical/__init__.py +4 -0
- figrecipe/_dev/demo_plotters/contour_surface/__init__.py +4 -0
- figrecipe/_dev/demo_plotters/distribution/__init__.py +4 -0
- figrecipe/_dev/demo_plotters/image_matrix/__init__.py +4 -0
- figrecipe/_dev/demo_plotters/line_curve/__init__.py +4 -0
- figrecipe/_dev/demo_plotters/{plot_plot.py → line_curve/plot_plot.py} +3 -2
- figrecipe/_dev/demo_plotters/scatter_points/__init__.py +4 -0
- figrecipe/_dev/demo_plotters/special/__init__.py +4 -0
- figrecipe/_dev/demo_plotters/{plot_pie.py → special/plot_pie.py} +5 -1
- figrecipe/_dev/demo_plotters/spectral_signal/__init__.py +4 -0
- figrecipe/_dev/demo_plotters/vector_flow/__init__.py +4 -0
- figrecipe/_editor/__init__.py +57 -9
- figrecipe/_editor/_bbox/__init__.py +43 -0
- figrecipe/_editor/_bbox/_collections.py +177 -0
- figrecipe/_editor/_bbox/_elements.py +159 -0
- figrecipe/_editor/_bbox/_extract.py +256 -0
- figrecipe/_editor/_bbox/_extract_axes.py +370 -0
- figrecipe/_editor/_bbox/_extract_text.py +342 -0
- figrecipe/_editor/_bbox/_lines.py +173 -0
- figrecipe/_editor/_bbox/_transforms.py +146 -0
- figrecipe/_editor/_flask_app.py +68 -1039
- figrecipe/_editor/_helpers.py +242 -0
- figrecipe/_editor/_hitmap/__init__.py +76 -0
- figrecipe/_editor/_hitmap/_artists/__init__.py +21 -0
- figrecipe/_editor/_hitmap/_artists/_collections.py +345 -0
- figrecipe/_editor/_hitmap/_artists/_images.py +68 -0
- figrecipe/_editor/_hitmap/_artists/_lines.py +107 -0
- figrecipe/_editor/_hitmap/_artists/_patches.py +163 -0
- figrecipe/_editor/_hitmap/_artists/_text.py +190 -0
- figrecipe/_editor/_hitmap/_colors.py +181 -0
- figrecipe/_editor/_hitmap/_detect.py +137 -0
- figrecipe/_editor/_hitmap/_restore.py +154 -0
- figrecipe/_editor/_hitmap_main.py +182 -0
- figrecipe/_editor/_preferences.py +135 -0
- figrecipe/_editor/_render_overrides.py +480 -0
- figrecipe/_editor/_renderer.py +35 -185
- figrecipe/_editor/_routes_axis.py +453 -0
- figrecipe/_editor/_routes_core.py +284 -0
- figrecipe/_editor/_routes_element.py +317 -0
- figrecipe/_editor/_routes_style.py +223 -0
- figrecipe/_editor/_templates/__init__.py +78 -1
- figrecipe/_editor/_templates/_html.py +109 -13
- figrecipe/_editor/_templates/_scripts/__init__.py +120 -0
- figrecipe/_editor/_templates/_scripts/_api.py +228 -0
- figrecipe/_editor/_templates/_scripts/_colors.py +485 -0
- figrecipe/_editor/_templates/_scripts/_core.py +436 -0
- figrecipe/_editor/_templates/_scripts/_debug_snapshot.py +186 -0
- figrecipe/_editor/_templates/_scripts/_element_editor.py +310 -0
- figrecipe/_editor/_templates/_scripts/_files.py +195 -0
- figrecipe/_editor/_templates/_scripts/_hitmap.py +509 -0
- figrecipe/_editor/_templates/_scripts/_inspector.py +315 -0
- figrecipe/_editor/_templates/_scripts/_labels.py +464 -0
- figrecipe/_editor/_templates/_scripts/_legend_drag.py +265 -0
- figrecipe/_editor/_templates/_scripts/_modals.py +226 -0
- figrecipe/_editor/_templates/_scripts/_overlays.py +292 -0
- figrecipe/_editor/_templates/_scripts/_panel_drag.py +334 -0
- figrecipe/_editor/_templates/_scripts/_panel_position.py +279 -0
- figrecipe/_editor/_templates/_scripts/_selection.py +237 -0
- figrecipe/_editor/_templates/_scripts/_tabs.py +89 -0
- figrecipe/_editor/_templates/_scripts/_view_mode.py +107 -0
- figrecipe/_editor/_templates/_scripts/_zoom.py +179 -0
- figrecipe/_editor/_templates/_styles/__init__.py +69 -0
- figrecipe/_editor/_templates/_styles/_base.py +64 -0
- figrecipe/_editor/_templates/_styles/_buttons.py +206 -0
- figrecipe/_editor/_templates/_styles/_color_input.py +123 -0
- figrecipe/_editor/_templates/_styles/_controls.py +265 -0
- figrecipe/_editor/_templates/_styles/_dynamic_props.py +144 -0
- figrecipe/_editor/_templates/_styles/_forms.py +126 -0
- figrecipe/_editor/_templates/_styles/_hitmap.py +184 -0
- figrecipe/_editor/_templates/_styles/_inspector.py +90 -0
- figrecipe/_editor/_templates/_styles/_labels.py +118 -0
- figrecipe/_editor/_templates/_styles/_modals.py +98 -0
- figrecipe/_editor/_templates/_styles/_overlays.py +130 -0
- figrecipe/_editor/_templates/_styles/_preview.py +225 -0
- figrecipe/_editor/_templates/_styles/_selection.py +73 -0
- figrecipe/_params/_DECORATION_METHODS.py +6 -0
- figrecipe/_recorder.py +35 -106
- figrecipe/_recorder_utils.py +124 -0
- figrecipe/_reproducer/__init__.py +18 -0
- figrecipe/_reproducer/_core.py +498 -0
- figrecipe/_reproducer/_custom_plots.py +279 -0
- figrecipe/_reproducer/_seaborn.py +100 -0
- figrecipe/_reproducer/_violin.py +186 -0
- figrecipe/_signatures/_kwargs.py +273 -0
- figrecipe/_signatures/_loader.py +21 -423
- figrecipe/_signatures/_parsing.py +147 -0
- figrecipe/_wrappers/_axes.py +119 -910
- figrecipe/_wrappers/_axes_helpers.py +136 -0
- figrecipe/_wrappers/_axes_plots.py +418 -0
- figrecipe/_wrappers/_axes_seaborn.py +157 -0
- figrecipe/_wrappers/_figure.py +162 -0
- figrecipe/_wrappers/_panel_labels.py +127 -0
- figrecipe/_wrappers/_plot_helpers.py +143 -0
- figrecipe/_wrappers/_violin_helpers.py +180 -0
- figrecipe/styles/__init__.py +8 -6
- figrecipe/styles/_dotdict.py +72 -0
- figrecipe/styles/_finalize.py +134 -0
- figrecipe/styles/_fonts.py +77 -0
- figrecipe/styles/_kwargs_converter.py +178 -0
- figrecipe/styles/_plot_styles.py +209 -0
- figrecipe/styles/_style_applier.py +32 -478
- figrecipe/styles/_style_loader.py +16 -192
- figrecipe/styles/_themes.py +151 -0
- figrecipe/styles/presets/MATPLOTLIB.yaml +2 -1
- figrecipe/styles/presets/SCITEX.yaml +29 -24
- {figrecipe-0.6.0.dist-info → figrecipe-0.7.4.dist-info}/METADATA +37 -2
- figrecipe-0.7.4.dist-info/RECORD +188 -0
- figrecipe/_editor/_bbox.py +0 -978
- figrecipe/_editor/_hitmap.py +0 -937
- figrecipe/_editor/_templates/_scripts.py +0 -2778
- figrecipe/_editor/_templates/_styles.py +0 -1326
- figrecipe/_reproducer.py +0 -975
- figrecipe-0.6.0.dist-info/RECORD +0 -90
- /figrecipe/_dev/demo_plotters/{plot_bar.py → bar_categorical/plot_bar.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_barh.py → bar_categorical/plot_barh.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_contour.py → contour_surface/plot_contour.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_contourf.py → contour_surface/plot_contourf.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_tricontour.py → contour_surface/plot_tricontour.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_tricontourf.py → contour_surface/plot_tricontourf.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_tripcolor.py → contour_surface/plot_tripcolor.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_triplot.py → contour_surface/plot_triplot.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_boxplot.py → distribution/plot_boxplot.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_ecdf.py → distribution/plot_ecdf.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_hist.py → distribution/plot_hist.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_hist2d.py → distribution/plot_hist2d.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_violinplot.py → distribution/plot_violinplot.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_hexbin.py → image_matrix/plot_hexbin.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_imshow.py → image_matrix/plot_imshow.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_matshow.py → image_matrix/plot_matshow.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_pcolor.py → image_matrix/plot_pcolor.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_pcolormesh.py → image_matrix/plot_pcolormesh.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_spy.py → image_matrix/plot_spy.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_errorbar.py → line_curve/plot_errorbar.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_fill.py → line_curve/plot_fill.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_fill_between.py → line_curve/plot_fill_between.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_fill_betweenx.py → line_curve/plot_fill_betweenx.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_stackplot.py → line_curve/plot_stackplot.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_stairs.py → line_curve/plot_stairs.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_step.py → line_curve/plot_step.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_scatter.py → scatter_points/plot_scatter.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_eventplot.py → special/plot_eventplot.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_loglog.py → special/plot_loglog.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_semilogx.py → special/plot_semilogx.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_semilogy.py → special/plot_semilogy.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_stem.py → special/plot_stem.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_acorr.py → spectral_signal/plot_acorr.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_angle_spectrum.py → spectral_signal/plot_angle_spectrum.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_cohere.py → spectral_signal/plot_cohere.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_csd.py → spectral_signal/plot_csd.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_magnitude_spectrum.py → spectral_signal/plot_magnitude_spectrum.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_phase_spectrum.py → spectral_signal/plot_phase_spectrum.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_psd.py → spectral_signal/plot_psd.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_specgram.py → spectral_signal/plot_specgram.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_xcorr.py → spectral_signal/plot_xcorr.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_barbs.py → vector_flow/plot_barbs.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_quiver.py → vector_flow/plot_quiver.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_streamplot.py → vector_flow/plot_streamplot.py} +0 -0
- {figrecipe-0.6.0.dist-info → figrecipe-0.7.4.dist-info}/WHEEL +0 -0
- {figrecipe-0.6.0.dist-info → figrecipe-0.7.4.dist-info}/licenses/LICENSE +0 -0
figrecipe/_editor/__init__.py
CHANGED
|
@@ -26,10 +26,12 @@ from ._flask_app import FigureEditor
|
|
|
26
26
|
|
|
27
27
|
|
|
28
28
|
def edit(
|
|
29
|
-
source: Union[RecordingFigure, str, Path],
|
|
29
|
+
source: Optional[Union[RecordingFigure, str, Path]] = None,
|
|
30
30
|
style: Optional[Union[str, Dict[str, Any]]] = None,
|
|
31
31
|
port: int = 5050,
|
|
32
32
|
open_browser: bool = True,
|
|
33
|
+
hot_reload: bool = False,
|
|
34
|
+
working_dir: Optional[Union[str, Path]] = None,
|
|
33
35
|
) -> Dict[str, Any]:
|
|
34
36
|
"""
|
|
35
37
|
Launch interactive GUI editor for figure styling.
|
|
@@ -39,8 +41,9 @@ def edit(
|
|
|
39
41
|
|
|
40
42
|
Parameters
|
|
41
43
|
----------
|
|
42
|
-
source : RecordingFigure, str, or
|
|
43
|
-
Either a live RecordingFigure object
|
|
44
|
+
source : RecordingFigure, str, Path, or None
|
|
45
|
+
Either a live RecordingFigure object, path to a .yaml/.png file,
|
|
46
|
+
or None to create a new blank figure.
|
|
44
47
|
style : str or dict, optional
|
|
45
48
|
Style preset name (e.g., 'SCITEX', 'SCITEX_DARK') or style dict.
|
|
46
49
|
If None, uses the currently loaded global style.
|
|
@@ -48,6 +51,12 @@ def edit(
|
|
|
48
51
|
Flask server port (default: 5050). Auto-finds available port if occupied.
|
|
49
52
|
open_browser : bool, optional
|
|
50
53
|
Whether to open browser automatically (default: True).
|
|
54
|
+
hot_reload : bool, optional
|
|
55
|
+
Enable hot reload - server restarts when source files change (default: False).
|
|
56
|
+
Like Django's development server. Browser auto-refreshes on reconnect.
|
|
57
|
+
working_dir : str or Path, optional
|
|
58
|
+
Working directory for file switching feature (default: current directory).
|
|
59
|
+
The file switcher will list recipe files from this directory.
|
|
51
60
|
|
|
52
61
|
Returns
|
|
53
62
|
-------
|
|
@@ -109,6 +118,9 @@ def edit(
|
|
|
109
118
|
hitmap, color_map = generate_hitmap(fig)
|
|
110
119
|
hitmap_base64 = hitmap_to_base64(hitmap)
|
|
111
120
|
|
|
121
|
+
# Resolve working directory
|
|
122
|
+
resolved_working_dir = Path(working_dir) if working_dir else Path.cwd()
|
|
123
|
+
|
|
112
124
|
# Create and run editor with pre-rendered static PNG
|
|
113
125
|
editor = FigureEditor(
|
|
114
126
|
fig=fig,
|
|
@@ -118,25 +130,46 @@ def edit(
|
|
|
118
130
|
static_png_path=static_png_path,
|
|
119
131
|
hitmap_base64=hitmap_base64,
|
|
120
132
|
color_map=color_map,
|
|
133
|
+
hot_reload=hot_reload,
|
|
134
|
+
working_dir=resolved_working_dir,
|
|
121
135
|
)
|
|
122
136
|
|
|
123
137
|
return editor.run(open_browser=open_browser)
|
|
124
138
|
|
|
125
139
|
|
|
126
|
-
def _resolve_source(source: Union[RecordingFigure, str, Path]):
|
|
140
|
+
def _resolve_source(source: Optional[Union[RecordingFigure, str, Path]]):
|
|
127
141
|
"""
|
|
128
142
|
Resolve source to figure and optional recipe path.
|
|
129
143
|
|
|
130
144
|
Parameters
|
|
131
145
|
----------
|
|
132
|
-
source : RecordingFigure, str, or
|
|
133
|
-
Input source.
|
|
146
|
+
source : RecordingFigure, str, Path, or None
|
|
147
|
+
Input source. If None, creates a new blank figure.
|
|
148
|
+
If PNG path, tries to find associated YAML recipe.
|
|
134
149
|
|
|
135
150
|
Returns
|
|
136
151
|
-------
|
|
137
152
|
tuple
|
|
138
|
-
(RecordingFigure
|
|
153
|
+
(RecordingFigure, Path or None)
|
|
139
154
|
"""
|
|
155
|
+
# Handle None - create new blank figure
|
|
156
|
+
if source is None:
|
|
157
|
+
from .. import subplots
|
|
158
|
+
|
|
159
|
+
fig, ax = subplots()
|
|
160
|
+
ax.set_title("New Figure")
|
|
161
|
+
ax.text(
|
|
162
|
+
0.5,
|
|
163
|
+
0.5,
|
|
164
|
+
"Add plots using fr.edit(fig)",
|
|
165
|
+
ha="center",
|
|
166
|
+
va="center",
|
|
167
|
+
transform=ax.transAxes,
|
|
168
|
+
fontsize=12,
|
|
169
|
+
color="gray",
|
|
170
|
+
)
|
|
171
|
+
return fig, None
|
|
172
|
+
|
|
140
173
|
if isinstance(source, RecordingFigure):
|
|
141
174
|
return source, None
|
|
142
175
|
|
|
@@ -160,10 +193,25 @@ def _resolve_source(source: Union[RecordingFigure, str, Path]):
|
|
|
160
193
|
# Assume it's a path
|
|
161
194
|
path = Path(source)
|
|
162
195
|
if not path.exists():
|
|
163
|
-
raise FileNotFoundError(f"
|
|
196
|
+
raise FileNotFoundError(f"File not found: {path}")
|
|
197
|
+
|
|
198
|
+
# Handle PNG path - find associated YAML
|
|
199
|
+
if path.suffix.lower() == ".png":
|
|
200
|
+
yaml_path = path.with_suffix(".yaml")
|
|
201
|
+
if yaml_path.exists():
|
|
202
|
+
path = yaml_path
|
|
203
|
+
else:
|
|
204
|
+
yml_path = path.with_suffix(".yml")
|
|
205
|
+
if yml_path.exists():
|
|
206
|
+
path = yml_path
|
|
207
|
+
else:
|
|
208
|
+
raise FileNotFoundError(
|
|
209
|
+
f"No recipe found for {path.name}. "
|
|
210
|
+
f"Expected {yaml_path.name} or {yml_path.name}"
|
|
211
|
+
)
|
|
164
212
|
|
|
165
213
|
if path.suffix.lower() not in (".yaml", ".yml"):
|
|
166
|
-
raise ValueError(f"Expected .yaml or .
|
|
214
|
+
raise ValueError(f"Expected .yaml, .yml, or .png file, got: {path.suffix}")
|
|
167
215
|
|
|
168
216
|
# Load recipe and reproduce figure
|
|
169
217
|
from .._reproducer import reproduce
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""
|
|
4
|
+
Modular bbox extraction for figure elements.
|
|
5
|
+
|
|
6
|
+
This package provides functions for extracting bounding boxes from
|
|
7
|
+
matplotlib figure elements for hit detection in the GUI editor.
|
|
8
|
+
|
|
9
|
+
The main function `extract_bboxes` is re-exported from _bbox_main.py
|
|
10
|
+
for backward compatibility.
|
|
11
|
+
|
|
12
|
+
Modules:
|
|
13
|
+
- _transforms: Coordinate transformation utilities
|
|
14
|
+
- _elements: General element, text, and tick bbox extraction
|
|
15
|
+
- _lines: Line and quiver bbox extraction
|
|
16
|
+
- _collections: Collection (scatter, fill) and patch bbox extraction
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
# Re-export main function from _extract.py
|
|
20
|
+
# Import modular helpers
|
|
21
|
+
from ._collections import get_collection_bbox, get_patch_bbox
|
|
22
|
+
from ._elements import get_element_bbox, get_text_bbox, get_tick_labels_bbox
|
|
23
|
+
from ._extract import extract_bboxes
|
|
24
|
+
from ._lines import get_line_bbox, get_quiver_bbox
|
|
25
|
+
from ._transforms import display_to_image, transform_bbox
|
|
26
|
+
|
|
27
|
+
__all__ = [
|
|
28
|
+
# Main function
|
|
29
|
+
"extract_bboxes",
|
|
30
|
+
# Transforms
|
|
31
|
+
"transform_bbox",
|
|
32
|
+
"display_to_image",
|
|
33
|
+
# Elements
|
|
34
|
+
"get_element_bbox",
|
|
35
|
+
"get_text_bbox",
|
|
36
|
+
"get_tick_labels_bbox",
|
|
37
|
+
# Lines
|
|
38
|
+
"get_line_bbox",
|
|
39
|
+
"get_quiver_bbox",
|
|
40
|
+
# Collections
|
|
41
|
+
"get_collection_bbox",
|
|
42
|
+
"get_patch_bbox",
|
|
43
|
+
]
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""
|
|
4
|
+
Collection bbox extraction for scatter, fill, and patch elements.
|
|
5
|
+
|
|
6
|
+
This module handles bbox extraction for matplotlib collections
|
|
7
|
+
(scatter plots, fills, bars, etc.).
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import math
|
|
11
|
+
from typing import Any, Dict, Optional
|
|
12
|
+
|
|
13
|
+
from matplotlib.axes import Axes
|
|
14
|
+
from matplotlib.collections import PathCollection
|
|
15
|
+
from matplotlib.figure import Figure
|
|
16
|
+
from matplotlib.transforms import Bbox
|
|
17
|
+
|
|
18
|
+
from ._elements import get_element_bbox
|
|
19
|
+
from ._transforms import display_to_image, transform_bbox
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def get_collection_bbox(
|
|
23
|
+
coll,
|
|
24
|
+
ax: Axes,
|
|
25
|
+
fig: Figure,
|
|
26
|
+
renderer,
|
|
27
|
+
tight_bbox: Bbox,
|
|
28
|
+
img_width: int,
|
|
29
|
+
img_height: int,
|
|
30
|
+
scale_x: float,
|
|
31
|
+
scale_y: float,
|
|
32
|
+
pad_inches: float,
|
|
33
|
+
saved_height_inches: float,
|
|
34
|
+
include_points: bool = True,
|
|
35
|
+
) -> Optional[Dict[str, Any]]:
|
|
36
|
+
"""Get bbox and points for a collection (scatter, fill)."""
|
|
37
|
+
try:
|
|
38
|
+
bbox = None
|
|
39
|
+
|
|
40
|
+
# For scatter plots, get_window_extent() can fail or return empty
|
|
41
|
+
# So we calculate bbox from data points as fallback
|
|
42
|
+
if isinstance(coll, PathCollection):
|
|
43
|
+
offsets = coll.get_offsets()
|
|
44
|
+
if len(offsets) > 0:
|
|
45
|
+
transform = ax.transData
|
|
46
|
+
points = []
|
|
47
|
+
|
|
48
|
+
# Limit to reasonable number of points
|
|
49
|
+
max_points = 200
|
|
50
|
+
step = max(1, len(offsets) // max_points)
|
|
51
|
+
|
|
52
|
+
for i in range(0, len(offsets), step):
|
|
53
|
+
try:
|
|
54
|
+
offset = offsets[i]
|
|
55
|
+
display_coords = transform.transform(offset)
|
|
56
|
+
img_coords = display_to_image(
|
|
57
|
+
display_coords[0],
|
|
58
|
+
display_coords[1],
|
|
59
|
+
fig,
|
|
60
|
+
tight_bbox,
|
|
61
|
+
img_width,
|
|
62
|
+
img_height,
|
|
63
|
+
scale_x,
|
|
64
|
+
scale_y,
|
|
65
|
+
pad_inches,
|
|
66
|
+
saved_height_inches,
|
|
67
|
+
)
|
|
68
|
+
if img_coords:
|
|
69
|
+
points.append(img_coords)
|
|
70
|
+
except Exception:
|
|
71
|
+
continue
|
|
72
|
+
|
|
73
|
+
# Calculate bbox from points
|
|
74
|
+
if points:
|
|
75
|
+
xs = [p[0] for p in points]
|
|
76
|
+
ys = [p[1] for p in points]
|
|
77
|
+
# Add padding around scatter points for easier clicking
|
|
78
|
+
padding = 10 # pixels
|
|
79
|
+
bbox = {
|
|
80
|
+
"x": float(min(xs) - padding),
|
|
81
|
+
"y": float(min(ys) - padding),
|
|
82
|
+
"width": float(max(xs) - min(xs) + 2 * padding),
|
|
83
|
+
"height": float(max(ys) - min(ys) + 2 * padding),
|
|
84
|
+
"points": points,
|
|
85
|
+
}
|
|
86
|
+
return bbox
|
|
87
|
+
|
|
88
|
+
# Fallback: try standard window extent
|
|
89
|
+
window_extent = coll.get_window_extent(renderer)
|
|
90
|
+
if window_extent is None:
|
|
91
|
+
# Use axes extent as fallback
|
|
92
|
+
return get_element_bbox(
|
|
93
|
+
ax,
|
|
94
|
+
fig,
|
|
95
|
+
renderer,
|
|
96
|
+
tight_bbox,
|
|
97
|
+
img_width,
|
|
98
|
+
img_height,
|
|
99
|
+
scale_x,
|
|
100
|
+
scale_y,
|
|
101
|
+
pad_inches,
|
|
102
|
+
saved_height_inches,
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
# Check if window_extent is valid (not inf)
|
|
106
|
+
if (
|
|
107
|
+
math.isinf(window_extent.x0)
|
|
108
|
+
or math.isinf(window_extent.y0)
|
|
109
|
+
or math.isinf(window_extent.x1)
|
|
110
|
+
or math.isinf(window_extent.y1)
|
|
111
|
+
):
|
|
112
|
+
# Invalid extent - use axes extent as fallback
|
|
113
|
+
return get_element_bbox(
|
|
114
|
+
ax,
|
|
115
|
+
fig,
|
|
116
|
+
renderer,
|
|
117
|
+
tight_bbox,
|
|
118
|
+
img_width,
|
|
119
|
+
img_height,
|
|
120
|
+
scale_x,
|
|
121
|
+
scale_y,
|
|
122
|
+
pad_inches,
|
|
123
|
+
saved_height_inches,
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
bbox = transform_bbox(
|
|
127
|
+
window_extent,
|
|
128
|
+
fig,
|
|
129
|
+
tight_bbox,
|
|
130
|
+
img_width,
|
|
131
|
+
img_height,
|
|
132
|
+
scale_x,
|
|
133
|
+
scale_y,
|
|
134
|
+
pad_inches,
|
|
135
|
+
saved_height_inches,
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
return bbox
|
|
139
|
+
|
|
140
|
+
except Exception:
|
|
141
|
+
return None
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def get_patch_bbox(
|
|
145
|
+
patch,
|
|
146
|
+
ax: Axes,
|
|
147
|
+
fig: Figure,
|
|
148
|
+
renderer,
|
|
149
|
+
tight_bbox: Bbox,
|
|
150
|
+
img_width: int,
|
|
151
|
+
img_height: int,
|
|
152
|
+
scale_x: float,
|
|
153
|
+
scale_y: float,
|
|
154
|
+
pad_inches: float,
|
|
155
|
+
saved_height_inches: float,
|
|
156
|
+
) -> Optional[Dict[str, float]]:
|
|
157
|
+
"""Get bbox for a patch (bar, rectangle)."""
|
|
158
|
+
try:
|
|
159
|
+
window_extent = patch.get_window_extent(renderer)
|
|
160
|
+
if window_extent is None:
|
|
161
|
+
return None
|
|
162
|
+
return transform_bbox(
|
|
163
|
+
window_extent,
|
|
164
|
+
fig,
|
|
165
|
+
tight_bbox,
|
|
166
|
+
img_width,
|
|
167
|
+
img_height,
|
|
168
|
+
scale_x,
|
|
169
|
+
scale_y,
|
|
170
|
+
pad_inches,
|
|
171
|
+
saved_height_inches,
|
|
172
|
+
)
|
|
173
|
+
except Exception:
|
|
174
|
+
return None
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
__all__ = ["get_collection_bbox", "get_patch_bbox"]
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""
|
|
4
|
+
Element bbox extraction for general elements, text, and ticks.
|
|
5
|
+
|
|
6
|
+
This module handles bbox extraction for axes, text labels, and tick marks.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from typing import Dict, Optional
|
|
10
|
+
|
|
11
|
+
from matplotlib.figure import Figure
|
|
12
|
+
from matplotlib.transforms import Bbox
|
|
13
|
+
|
|
14
|
+
from ._transforms import transform_bbox
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def get_element_bbox(
|
|
18
|
+
element,
|
|
19
|
+
fig: Figure,
|
|
20
|
+
renderer,
|
|
21
|
+
tight_bbox: Bbox,
|
|
22
|
+
img_width: int,
|
|
23
|
+
img_height: int,
|
|
24
|
+
scale_x: float,
|
|
25
|
+
scale_y: float,
|
|
26
|
+
pad_inches: float,
|
|
27
|
+
saved_height_inches: float,
|
|
28
|
+
) -> Optional[Dict[str, float]]:
|
|
29
|
+
"""Get bbox for a general element."""
|
|
30
|
+
try:
|
|
31
|
+
window_extent = element.get_window_extent(renderer)
|
|
32
|
+
if window_extent is None:
|
|
33
|
+
return None
|
|
34
|
+
return transform_bbox(
|
|
35
|
+
window_extent,
|
|
36
|
+
fig,
|
|
37
|
+
tight_bbox,
|
|
38
|
+
img_width,
|
|
39
|
+
img_height,
|
|
40
|
+
scale_x,
|
|
41
|
+
scale_y,
|
|
42
|
+
pad_inches,
|
|
43
|
+
saved_height_inches,
|
|
44
|
+
)
|
|
45
|
+
except Exception:
|
|
46
|
+
return None
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def get_text_bbox(
|
|
50
|
+
text,
|
|
51
|
+
fig: Figure,
|
|
52
|
+
renderer,
|
|
53
|
+
tight_bbox: Bbox,
|
|
54
|
+
img_width: int,
|
|
55
|
+
img_height: int,
|
|
56
|
+
scale_x: float,
|
|
57
|
+
scale_y: float,
|
|
58
|
+
pad_inches: float,
|
|
59
|
+
saved_height_inches: float,
|
|
60
|
+
) -> Optional[Dict[str, float]]:
|
|
61
|
+
"""Get bbox for a text element."""
|
|
62
|
+
try:
|
|
63
|
+
window_extent = text.get_window_extent(renderer)
|
|
64
|
+
if window_extent is None:
|
|
65
|
+
return None
|
|
66
|
+
return transform_bbox(
|
|
67
|
+
window_extent,
|
|
68
|
+
fig,
|
|
69
|
+
tight_bbox,
|
|
70
|
+
img_width,
|
|
71
|
+
img_height,
|
|
72
|
+
scale_x,
|
|
73
|
+
scale_y,
|
|
74
|
+
pad_inches,
|
|
75
|
+
saved_height_inches,
|
|
76
|
+
)
|
|
77
|
+
except Exception:
|
|
78
|
+
return None
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def get_tick_labels_bbox(
|
|
82
|
+
axis,
|
|
83
|
+
axis_type: str, # 'x' or 'y'
|
|
84
|
+
fig: Figure,
|
|
85
|
+
renderer,
|
|
86
|
+
tight_bbox: Bbox,
|
|
87
|
+
img_width: int,
|
|
88
|
+
img_height: int,
|
|
89
|
+
scale_x: float,
|
|
90
|
+
scale_y: float,
|
|
91
|
+
pad_inches: float,
|
|
92
|
+
saved_height_inches: float,
|
|
93
|
+
) -> Optional[Dict[str, float]]:
|
|
94
|
+
"""
|
|
95
|
+
Get bbox for tick labels, extended to span the full axis dimension.
|
|
96
|
+
|
|
97
|
+
For x-axis: tick labels bbox spans the full width of the plot area.
|
|
98
|
+
For y-axis: tick labels bbox spans the full height of the plot area.
|
|
99
|
+
"""
|
|
100
|
+
try:
|
|
101
|
+
all_bboxes = []
|
|
102
|
+
|
|
103
|
+
# Get all tick label bboxes
|
|
104
|
+
for tick in axis.get_major_ticks():
|
|
105
|
+
tick_label = tick.label1 if hasattr(tick, "label1") else tick.label
|
|
106
|
+
if tick_label and tick_label.get_visible():
|
|
107
|
+
try:
|
|
108
|
+
tick_extent = tick_label.get_window_extent(renderer)
|
|
109
|
+
if tick_extent is not None and tick_extent.width > 0:
|
|
110
|
+
all_bboxes.append(tick_extent)
|
|
111
|
+
except Exception:
|
|
112
|
+
pass
|
|
113
|
+
|
|
114
|
+
if not all_bboxes:
|
|
115
|
+
return None
|
|
116
|
+
|
|
117
|
+
# Merge all tick label bboxes
|
|
118
|
+
merged = all_bboxes[0]
|
|
119
|
+
for bbox in all_bboxes[1:]:
|
|
120
|
+
merged = Bbox.union([merged, bbox])
|
|
121
|
+
|
|
122
|
+
# Get the axes extent to extend the tick labels region
|
|
123
|
+
ax = axis.axes
|
|
124
|
+
ax_bbox = ax.get_window_extent(renderer)
|
|
125
|
+
|
|
126
|
+
if axis_type == "x":
|
|
127
|
+
# For x-axis: extend width to match axes width, keep tick labels height
|
|
128
|
+
merged = Bbox.from_extents(
|
|
129
|
+
ax_bbox.x0, # Align left with axes
|
|
130
|
+
merged.y0, # Keep tick labels y position
|
|
131
|
+
ax_bbox.x1, # Align right with axes
|
|
132
|
+
merged.y1, # Keep tick labels height
|
|
133
|
+
)
|
|
134
|
+
else: # y-axis
|
|
135
|
+
# For y-axis: extend height to match axes height, keep tick labels width
|
|
136
|
+
merged = Bbox.from_extents(
|
|
137
|
+
merged.x0, # Keep tick labels x position
|
|
138
|
+
ax_bbox.y0, # Align bottom with axes
|
|
139
|
+
merged.x1, # Keep tick labels width
|
|
140
|
+
ax_bbox.y1, # Align top with axes
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
return transform_bbox(
|
|
144
|
+
merged,
|
|
145
|
+
fig,
|
|
146
|
+
tight_bbox,
|
|
147
|
+
img_width,
|
|
148
|
+
img_height,
|
|
149
|
+
scale_x,
|
|
150
|
+
scale_y,
|
|
151
|
+
pad_inches,
|
|
152
|
+
saved_height_inches,
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
except Exception:
|
|
156
|
+
return None
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
__all__ = ["get_element_bbox", "get_text_bbox", "get_tick_labels_bbox"]
|