figrecipe 0.6.0__py3-none-any.whl → 0.7.4__py3-none-any.whl

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