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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (269) hide show
  1. figrecipe/__init__.py +161 -1030
  2. figrecipe/__main__.py +12 -0
  3. figrecipe/_api/__init__.py +48 -0
  4. figrecipe/_api/_extract.py +108 -0
  5. figrecipe/_api/_notebook.py +61 -0
  6. figrecipe/_api/_panel.py +113 -0
  7. figrecipe/_api/_save.py +287 -0
  8. figrecipe/_api/_seaborn_proxy.py +34 -0
  9. figrecipe/_api/_style_manager.py +153 -0
  10. figrecipe/_api/_subplots.py +333 -0
  11. figrecipe/_api/_validate.py +82 -0
  12. figrecipe/_cli/__init__.py +7 -0
  13. figrecipe/_cli/_compose.py +87 -0
  14. figrecipe/_cli/_convert.py +117 -0
  15. figrecipe/_cli/_crop.py +82 -0
  16. figrecipe/_cli/_edit.py +70 -0
  17. figrecipe/_cli/_extract.py +128 -0
  18. figrecipe/_cli/_fonts.py +47 -0
  19. figrecipe/_cli/_info.py +67 -0
  20. figrecipe/_cli/_main.py +58 -0
  21. figrecipe/_cli/_reproduce.py +79 -0
  22. figrecipe/_cli/_style.py +77 -0
  23. figrecipe/_cli/_validate.py +66 -0
  24. figrecipe/_cli/_version.py +50 -0
  25. figrecipe/_composition/__init__.py +32 -0
  26. figrecipe/_composition/_alignment.py +452 -0
  27. figrecipe/_composition/_compose.py +179 -0
  28. figrecipe/_composition/_import_axes.py +127 -0
  29. figrecipe/_composition/_visibility.py +125 -0
  30. figrecipe/_dev/__init__.py +4 -93
  31. figrecipe/_dev/_plotters.py +76 -0
  32. figrecipe/_dev/_run_demos.py +56 -0
  33. figrecipe/_dev/browser/__init__.py +69 -0
  34. figrecipe/_dev/browser/_audio.py +240 -0
  35. figrecipe/_dev/browser/_caption.py +356 -0
  36. figrecipe/_dev/browser/_click_effect.py +146 -0
  37. figrecipe/_dev/browser/_cursor.py +196 -0
  38. figrecipe/_dev/browser/_highlight.py +105 -0
  39. figrecipe/_dev/browser/_narration.py +237 -0
  40. figrecipe/_dev/browser/_recorder.py +446 -0
  41. figrecipe/_dev/browser/_utils.py +178 -0
  42. figrecipe/_dev/browser/_video_trim/__init__.py +152 -0
  43. figrecipe/_dev/browser/_video_trim/_detection.py +223 -0
  44. figrecipe/_dev/browser/_video_trim/_markers.py +140 -0
  45. figrecipe/_dev/demo_plotters/__init__.py +35 -166
  46. figrecipe/_dev/demo_plotters/_categories.py +81 -0
  47. figrecipe/_dev/demo_plotters/_figure_creators.py +119 -0
  48. figrecipe/_dev/demo_plotters/_helpers.py +31 -0
  49. figrecipe/_dev/demo_plotters/_registry.py +50 -0
  50. figrecipe/_dev/demo_plotters/bar_categorical/__init__.py +4 -0
  51. figrecipe/_dev/demo_plotters/contour_surface/__init__.py +4 -0
  52. figrecipe/_dev/demo_plotters/distribution/__init__.py +4 -0
  53. figrecipe/_dev/demo_plotters/image_matrix/__init__.py +4 -0
  54. figrecipe/_dev/demo_plotters/line_curve/__init__.py +4 -0
  55. figrecipe/_dev/demo_plotters/{plot_plot.py → line_curve/plot_plot.py} +3 -2
  56. figrecipe/_dev/demo_plotters/scatter_points/__init__.py +4 -0
  57. figrecipe/_dev/demo_plotters/special/__init__.py +4 -0
  58. figrecipe/_dev/demo_plotters/{plot_pie.py → special/plot_pie.py} +5 -1
  59. figrecipe/_dev/demo_plotters/spectral_signal/__init__.py +4 -0
  60. figrecipe/_dev/demo_plotters/vector_flow/__init__.py +4 -0
  61. figrecipe/_editor/__init__.py +61 -13
  62. figrecipe/_editor/_bbox/__init__.py +43 -0
  63. figrecipe/_editor/_bbox/_collections.py +177 -0
  64. figrecipe/_editor/_bbox/_elements.py +159 -0
  65. figrecipe/_editor/_bbox/_extract.py +402 -0
  66. figrecipe/_editor/_bbox/_extract_axes.py +370 -0
  67. figrecipe/_editor/_bbox/_extract_text.py +466 -0
  68. figrecipe/_editor/_bbox/_lines.py +173 -0
  69. figrecipe/_editor/_bbox/_transforms.py +146 -0
  70. figrecipe/_editor/_call_overrides.py +183 -0
  71. figrecipe/_editor/_datatable_plot_handlers.py +249 -0
  72. figrecipe/_editor/_figure_layout.py +211 -0
  73. figrecipe/_editor/_flask_app.py +200 -1030
  74. figrecipe/_editor/_helpers.py +251 -0
  75. figrecipe/_editor/_hitmap/__init__.py +76 -0
  76. figrecipe/_editor/_hitmap/_artists/__init__.py +21 -0
  77. figrecipe/_editor/_hitmap/_artists/_collections.py +345 -0
  78. figrecipe/_editor/_hitmap/_artists/_images.py +68 -0
  79. figrecipe/_editor/_hitmap/_artists/_lines.py +107 -0
  80. figrecipe/_editor/_hitmap/_artists/_patches.py +163 -0
  81. figrecipe/_editor/_hitmap/_artists/_text.py +190 -0
  82. figrecipe/_editor/_hitmap/_colors.py +181 -0
  83. figrecipe/_editor/_hitmap/_detect.py +194 -0
  84. figrecipe/_editor/_hitmap/_restore.py +154 -0
  85. figrecipe/_editor/_hitmap_main.py +182 -0
  86. figrecipe/_editor/_overrides.py +4 -1
  87. figrecipe/_editor/_plot_types_registry.py +190 -0
  88. figrecipe/_editor/_preferences.py +135 -0
  89. figrecipe/_editor/_render_overrides.py +507 -0
  90. figrecipe/_editor/_renderer.py +81 -186
  91. figrecipe/_editor/_routes_annotation.py +114 -0
  92. figrecipe/_editor/_routes_axis.py +482 -0
  93. figrecipe/_editor/_routes_captions.py +130 -0
  94. figrecipe/_editor/_routes_composition.py +270 -0
  95. figrecipe/_editor/_routes_core.py +126 -0
  96. figrecipe/_editor/_routes_datatable.py +364 -0
  97. figrecipe/_editor/_routes_element.py +335 -0
  98. figrecipe/_editor/_routes_files.py +443 -0
  99. figrecipe/_editor/_routes_image.py +200 -0
  100. figrecipe/_editor/_routes_snapshot.py +94 -0
  101. figrecipe/_editor/_routes_style.py +243 -0
  102. figrecipe/_editor/_templates/__init__.py +116 -1
  103. figrecipe/_editor/_templates/_html.py +154 -64
  104. figrecipe/_editor/_templates/_html_components/__init__.py +13 -0
  105. figrecipe/_editor/_templates/_html_components/_composition_toolbar.py +79 -0
  106. figrecipe/_editor/_templates/_html_components/_file_browser.py +41 -0
  107. figrecipe/_editor/_templates/_html_datatable.py +92 -0
  108. figrecipe/_editor/_templates/_scripts/__init__.py +178 -0
  109. figrecipe/_editor/_templates/_scripts/_accordion.py +328 -0
  110. figrecipe/_editor/_templates/_scripts/_annotation_drag.py +504 -0
  111. figrecipe/_editor/_templates/_scripts/_api.py +228 -0
  112. figrecipe/_editor/_templates/_scripts/_canvas_context_menu.py +182 -0
  113. figrecipe/_editor/_templates/_scripts/_captions.py +231 -0
  114. figrecipe/_editor/_templates/_scripts/_colors.py +485 -0
  115. figrecipe/_editor/_templates/_scripts/_composition.py +283 -0
  116. figrecipe/_editor/_templates/_scripts/_core.py +493 -0
  117. figrecipe/_editor/_templates/_scripts/_datatable/__init__.py +59 -0
  118. figrecipe/_editor/_templates/_scripts/_datatable/_cell_edit.py +97 -0
  119. figrecipe/_editor/_templates/_scripts/_datatable/_clipboard.py +164 -0
  120. figrecipe/_editor/_templates/_scripts/_datatable/_context_menu.py +221 -0
  121. figrecipe/_editor/_templates/_scripts/_datatable/_core.py +150 -0
  122. figrecipe/_editor/_templates/_scripts/_datatable/_editable.py +511 -0
  123. figrecipe/_editor/_templates/_scripts/_datatable/_import.py +161 -0
  124. figrecipe/_editor/_templates/_scripts/_datatable/_plot.py +261 -0
  125. figrecipe/_editor/_templates/_scripts/_datatable/_selection.py +438 -0
  126. figrecipe/_editor/_templates/_scripts/_datatable/_table.py +256 -0
  127. figrecipe/_editor/_templates/_scripts/_datatable/_tabs.py +354 -0
  128. figrecipe/_editor/_templates/_scripts/_debug_snapshot.py +186 -0
  129. figrecipe/_editor/_templates/_scripts/_element_editor.py +325 -0
  130. figrecipe/_editor/_templates/_scripts/_files.py +429 -0
  131. figrecipe/_editor/_templates/_scripts/_files_context_menu.py +240 -0
  132. figrecipe/_editor/_templates/_scripts/_hitmap.py +512 -0
  133. figrecipe/_editor/_templates/_scripts/_image_drop.py +428 -0
  134. figrecipe/_editor/_templates/_scripts/_inspector.py +315 -0
  135. figrecipe/_editor/_templates/_scripts/_labels.py +464 -0
  136. figrecipe/_editor/_templates/_scripts/_legend_drag.py +270 -0
  137. figrecipe/_editor/_templates/_scripts/_modals.py +226 -0
  138. figrecipe/_editor/_templates/_scripts/_multi_select.py +198 -0
  139. figrecipe/_editor/_templates/_scripts/_overlays.py +292 -0
  140. figrecipe/_editor/_templates/_scripts/_panel_drag.py +505 -0
  141. figrecipe/_editor/_templates/_scripts/_panel_drag_snapshot.py +33 -0
  142. figrecipe/_editor/_templates/_scripts/_panel_position.py +463 -0
  143. figrecipe/_editor/_templates/_scripts/_panel_resize.py +230 -0
  144. figrecipe/_editor/_templates/_scripts/_panel_snap.py +307 -0
  145. figrecipe/_editor/_templates/_scripts/_region_select.py +255 -0
  146. figrecipe/_editor/_templates/_scripts/_selection.py +244 -0
  147. figrecipe/_editor/_templates/_scripts/_sync.py +242 -0
  148. figrecipe/_editor/_templates/_scripts/_tabs.py +89 -0
  149. figrecipe/_editor/_templates/_scripts/_undo_redo.py +348 -0
  150. figrecipe/_editor/_templates/_scripts/_view_mode.py +107 -0
  151. figrecipe/_editor/_templates/_scripts/_zoom.py +212 -0
  152. figrecipe/_editor/_templates/_styles/__init__.py +78 -0
  153. figrecipe/_editor/_templates/_styles/_base.py +111 -0
  154. figrecipe/_editor/_templates/_styles/_buttons.py +327 -0
  155. figrecipe/_editor/_templates/_styles/_color_input.py +123 -0
  156. figrecipe/_editor/_templates/_styles/_composition.py +87 -0
  157. figrecipe/_editor/_templates/_styles/_controls.py +430 -0
  158. figrecipe/_editor/_templates/_styles/_datatable/__init__.py +40 -0
  159. figrecipe/_editor/_templates/_styles/_datatable/_editable.py +203 -0
  160. figrecipe/_editor/_templates/_styles/_datatable/_panel.py +268 -0
  161. figrecipe/_editor/_templates/_styles/_datatable/_table.py +479 -0
  162. figrecipe/_editor/_templates/_styles/_datatable/_toolbar.py +384 -0
  163. figrecipe/_editor/_templates/_styles/_datatable/_vars.py +123 -0
  164. figrecipe/_editor/_templates/_styles/_dynamic_props.py +144 -0
  165. figrecipe/_editor/_templates/_styles/_file_browser.py +466 -0
  166. figrecipe/_editor/_templates/_styles/_forms.py +224 -0
  167. figrecipe/_editor/_templates/_styles/_hitmap.py +191 -0
  168. figrecipe/_editor/_templates/_styles/_inspector.py +90 -0
  169. figrecipe/_editor/_templates/_styles/_labels.py +118 -0
  170. figrecipe/_editor/_templates/_styles/_modals.py +127 -0
  171. figrecipe/_editor/_templates/_styles/_overlays.py +130 -0
  172. figrecipe/_editor/_templates/_styles/_preview.py +430 -0
  173. figrecipe/_editor/_templates/_styles/_selection.py +73 -0
  174. figrecipe/_editor/_templates/_styles/_spinner.py +117 -0
  175. figrecipe/_editor/static/audio/click.mp3 +0 -0
  176. figrecipe/_editor/static/click.mp3 +0 -0
  177. figrecipe/_editor/static/icons/favicon.ico +0 -0
  178. figrecipe/_integrations/__init__.py +17 -0
  179. figrecipe/_integrations/_scitex_stats.py +298 -0
  180. figrecipe/_params/_DECORATION_METHODS.py +8 -0
  181. figrecipe/_recorder.py +63 -109
  182. figrecipe/_recorder_utils.py +124 -0
  183. figrecipe/_reproducer/__init__.py +18 -0
  184. figrecipe/_reproducer/_core.py +509 -0
  185. figrecipe/_reproducer/_custom_plots.py +279 -0
  186. figrecipe/_reproducer/_seaborn.py +100 -0
  187. figrecipe/_reproducer/_violin.py +186 -0
  188. figrecipe/_signatures/_kwargs.py +273 -0
  189. figrecipe/_signatures/_loader.py +21 -423
  190. figrecipe/_signatures/_parsing.py +147 -0
  191. figrecipe/_utils/__init__.py +3 -0
  192. figrecipe/_utils/_bundle.py +205 -0
  193. figrecipe/_wrappers/_axes.py +252 -895
  194. figrecipe/_wrappers/_axes_helpers.py +136 -0
  195. figrecipe/_wrappers/_axes_plots.py +418 -0
  196. figrecipe/_wrappers/_axes_seaborn.py +157 -0
  197. figrecipe/_wrappers/_caption_generator.py +218 -0
  198. figrecipe/_wrappers/_figure.py +188 -1
  199. figrecipe/_wrappers/_panel_labels.py +127 -0
  200. figrecipe/_wrappers/_plot_helpers.py +143 -0
  201. figrecipe/_wrappers/_stat_annotation.py +274 -0
  202. figrecipe/_wrappers/_violin_helpers.py +180 -0
  203. figrecipe/styles/__init__.py +8 -6
  204. figrecipe/styles/_dotdict.py +72 -0
  205. figrecipe/styles/_finalize.py +134 -0
  206. figrecipe/styles/_fonts.py +77 -0
  207. figrecipe/styles/_kwargs_converter.py +178 -0
  208. figrecipe/styles/_plot_styles.py +209 -0
  209. figrecipe/styles/_style_applier.py +42 -480
  210. figrecipe/styles/_style_loader.py +16 -192
  211. figrecipe/styles/_themes.py +151 -0
  212. figrecipe/styles/presets/MATPLOTLIB.yaml +2 -1
  213. figrecipe/styles/presets/SCITEX.yaml +40 -28
  214. figrecipe-0.9.0.dist-info/METADATA +427 -0
  215. figrecipe-0.9.0.dist-info/RECORD +277 -0
  216. figrecipe-0.9.0.dist-info/entry_points.txt +2 -0
  217. figrecipe/_editor/_bbox.py +0 -978
  218. figrecipe/_editor/_hitmap.py +0 -937
  219. figrecipe/_editor/_templates/_scripts.py +0 -2778
  220. figrecipe/_editor/_templates/_styles.py +0 -1326
  221. figrecipe/_reproducer.py +0 -975
  222. figrecipe-0.6.0.dist-info/METADATA +0 -394
  223. figrecipe-0.6.0.dist-info/RECORD +0 -90
  224. /figrecipe/_dev/demo_plotters/{plot_bar.py → bar_categorical/plot_bar.py} +0 -0
  225. /figrecipe/_dev/demo_plotters/{plot_barh.py → bar_categorical/plot_barh.py} +0 -0
  226. /figrecipe/_dev/demo_plotters/{plot_contour.py → contour_surface/plot_contour.py} +0 -0
  227. /figrecipe/_dev/demo_plotters/{plot_contourf.py → contour_surface/plot_contourf.py} +0 -0
  228. /figrecipe/_dev/demo_plotters/{plot_tricontour.py → contour_surface/plot_tricontour.py} +0 -0
  229. /figrecipe/_dev/demo_plotters/{plot_tricontourf.py → contour_surface/plot_tricontourf.py} +0 -0
  230. /figrecipe/_dev/demo_plotters/{plot_tripcolor.py → contour_surface/plot_tripcolor.py} +0 -0
  231. /figrecipe/_dev/demo_plotters/{plot_triplot.py → contour_surface/plot_triplot.py} +0 -0
  232. /figrecipe/_dev/demo_plotters/{plot_boxplot.py → distribution/plot_boxplot.py} +0 -0
  233. /figrecipe/_dev/demo_plotters/{plot_ecdf.py → distribution/plot_ecdf.py} +0 -0
  234. /figrecipe/_dev/demo_plotters/{plot_hist.py → distribution/plot_hist.py} +0 -0
  235. /figrecipe/_dev/demo_plotters/{plot_hist2d.py → distribution/plot_hist2d.py} +0 -0
  236. /figrecipe/_dev/demo_plotters/{plot_violinplot.py → distribution/plot_violinplot.py} +0 -0
  237. /figrecipe/_dev/demo_plotters/{plot_hexbin.py → image_matrix/plot_hexbin.py} +0 -0
  238. /figrecipe/_dev/demo_plotters/{plot_imshow.py → image_matrix/plot_imshow.py} +0 -0
  239. /figrecipe/_dev/demo_plotters/{plot_matshow.py → image_matrix/plot_matshow.py} +0 -0
  240. /figrecipe/_dev/demo_plotters/{plot_pcolor.py → image_matrix/plot_pcolor.py} +0 -0
  241. /figrecipe/_dev/demo_plotters/{plot_pcolormesh.py → image_matrix/plot_pcolormesh.py} +0 -0
  242. /figrecipe/_dev/demo_plotters/{plot_spy.py → image_matrix/plot_spy.py} +0 -0
  243. /figrecipe/_dev/demo_plotters/{plot_errorbar.py → line_curve/plot_errorbar.py} +0 -0
  244. /figrecipe/_dev/demo_plotters/{plot_fill.py → line_curve/plot_fill.py} +0 -0
  245. /figrecipe/_dev/demo_plotters/{plot_fill_between.py → line_curve/plot_fill_between.py} +0 -0
  246. /figrecipe/_dev/demo_plotters/{plot_fill_betweenx.py → line_curve/plot_fill_betweenx.py} +0 -0
  247. /figrecipe/_dev/demo_plotters/{plot_stackplot.py → line_curve/plot_stackplot.py} +0 -0
  248. /figrecipe/_dev/demo_plotters/{plot_stairs.py → line_curve/plot_stairs.py} +0 -0
  249. /figrecipe/_dev/demo_plotters/{plot_step.py → line_curve/plot_step.py} +0 -0
  250. /figrecipe/_dev/demo_plotters/{plot_scatter.py → scatter_points/plot_scatter.py} +0 -0
  251. /figrecipe/_dev/demo_plotters/{plot_eventplot.py → special/plot_eventplot.py} +0 -0
  252. /figrecipe/_dev/demo_plotters/{plot_loglog.py → special/plot_loglog.py} +0 -0
  253. /figrecipe/_dev/demo_plotters/{plot_semilogx.py → special/plot_semilogx.py} +0 -0
  254. /figrecipe/_dev/demo_plotters/{plot_semilogy.py → special/plot_semilogy.py} +0 -0
  255. /figrecipe/_dev/demo_plotters/{plot_stem.py → special/plot_stem.py} +0 -0
  256. /figrecipe/_dev/demo_plotters/{plot_acorr.py → spectral_signal/plot_acorr.py} +0 -0
  257. /figrecipe/_dev/demo_plotters/{plot_angle_spectrum.py → spectral_signal/plot_angle_spectrum.py} +0 -0
  258. /figrecipe/_dev/demo_plotters/{plot_cohere.py → spectral_signal/plot_cohere.py} +0 -0
  259. /figrecipe/_dev/demo_plotters/{plot_csd.py → spectral_signal/plot_csd.py} +0 -0
  260. /figrecipe/_dev/demo_plotters/{plot_magnitude_spectrum.py → spectral_signal/plot_magnitude_spectrum.py} +0 -0
  261. /figrecipe/_dev/demo_plotters/{plot_phase_spectrum.py → spectral_signal/plot_phase_spectrum.py} +0 -0
  262. /figrecipe/_dev/demo_plotters/{plot_psd.py → spectral_signal/plot_psd.py} +0 -0
  263. /figrecipe/_dev/demo_plotters/{plot_specgram.py → spectral_signal/plot_specgram.py} +0 -0
  264. /figrecipe/_dev/demo_plotters/{plot_xcorr.py → spectral_signal/plot_xcorr.py} +0 -0
  265. /figrecipe/_dev/demo_plotters/{plot_barbs.py → vector_flow/plot_barbs.py} +0 -0
  266. /figrecipe/_dev/demo_plotters/{plot_quiver.py → vector_flow/plot_quiver.py} +0 -0
  267. /figrecipe/_dev/demo_plotters/{plot_streamplot.py → vector_flow/plot_streamplot.py} +0 -0
  268. {figrecipe-0.6.0.dist-info → figrecipe-0.9.0.dist-info}/WHEEL +0 -0
  269. {figrecipe-0.6.0.dist-info → figrecipe-0.9.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,466 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """Text, legend, spine, and figure text bbox extraction."""
4
+
5
+ from ._elements import get_text_bbox, get_tick_labels_bbox
6
+ from ._transforms import transform_bbox
7
+
8
+
9
+ def extract_text_elements(
10
+ ax,
11
+ ax_idx,
12
+ fig,
13
+ renderer,
14
+ tight_bbox,
15
+ img_width,
16
+ img_height,
17
+ scale_x,
18
+ scale_y,
19
+ pad_inches,
20
+ saved_height_inches,
21
+ bboxes,
22
+ ):
23
+ """Extract bboxes for text elements (title, labels, ticks)."""
24
+ # Title
25
+ title = ax.get_title()
26
+ if title:
27
+ bbox = get_text_bbox(
28
+ ax.title,
29
+ fig,
30
+ renderer,
31
+ tight_bbox,
32
+ img_width,
33
+ img_height,
34
+ scale_x,
35
+ scale_y,
36
+ pad_inches,
37
+ saved_height_inches,
38
+ )
39
+ if bbox:
40
+ bboxes[f"ax{ax_idx}_title"] = {
41
+ **bbox,
42
+ "type": "title",
43
+ "label": "title",
44
+ "ax_index": ax_idx,
45
+ "text": title,
46
+ }
47
+
48
+ # X label
49
+ xlabel = ax.get_xlabel()
50
+ if xlabel:
51
+ bbox = get_text_bbox(
52
+ ax.xaxis.label,
53
+ fig,
54
+ renderer,
55
+ tight_bbox,
56
+ img_width,
57
+ img_height,
58
+ scale_x,
59
+ scale_y,
60
+ pad_inches,
61
+ saved_height_inches,
62
+ )
63
+ if bbox:
64
+ bboxes[f"ax{ax_idx}_xlabel"] = {
65
+ **bbox,
66
+ "type": "xlabel",
67
+ "label": "xlabel",
68
+ "ax_index": ax_idx,
69
+ "text": xlabel,
70
+ }
71
+
72
+ # X tick labels
73
+ xtick_bbox = get_tick_labels_bbox(
74
+ ax.xaxis,
75
+ "x",
76
+ fig,
77
+ renderer,
78
+ tight_bbox,
79
+ img_width,
80
+ img_height,
81
+ scale_x,
82
+ scale_y,
83
+ pad_inches,
84
+ saved_height_inches,
85
+ )
86
+ if xtick_bbox:
87
+ bboxes[f"ax{ax_idx}_xticks"] = {
88
+ **xtick_bbox,
89
+ "type": "xticks",
90
+ "label": "x tick labels",
91
+ "ax_index": ax_idx,
92
+ }
93
+
94
+ # Y label
95
+ ylabel = ax.get_ylabel()
96
+ if ylabel:
97
+ bbox = get_text_bbox(
98
+ ax.yaxis.label,
99
+ fig,
100
+ renderer,
101
+ tight_bbox,
102
+ img_width,
103
+ img_height,
104
+ scale_x,
105
+ scale_y,
106
+ pad_inches,
107
+ saved_height_inches,
108
+ )
109
+ if bbox:
110
+ bboxes[f"ax{ax_idx}_ylabel"] = {
111
+ **bbox,
112
+ "type": "ylabel",
113
+ "label": "ylabel",
114
+ "ax_index": ax_idx,
115
+ "text": ylabel,
116
+ }
117
+
118
+ # Y tick labels
119
+ ytick_bbox = get_tick_labels_bbox(
120
+ ax.yaxis,
121
+ "y",
122
+ fig,
123
+ renderer,
124
+ tight_bbox,
125
+ img_width,
126
+ img_height,
127
+ scale_x,
128
+ scale_y,
129
+ pad_inches,
130
+ saved_height_inches,
131
+ )
132
+ if ytick_bbox:
133
+ bboxes[f"ax{ax_idx}_yticks"] = {
134
+ **ytick_bbox,
135
+ "type": "yticks",
136
+ "label": "y tick labels",
137
+ "ax_index": ax_idx,
138
+ }
139
+
140
+
141
+ def extract_legend(
142
+ ax,
143
+ ax_idx,
144
+ fig,
145
+ renderer,
146
+ tight_bbox,
147
+ img_width,
148
+ img_height,
149
+ scale_x,
150
+ scale_y,
151
+ pad_inches,
152
+ saved_height_inches,
153
+ bboxes,
154
+ ):
155
+ """Extract bbox for legend."""
156
+ legend = ax.get_legend()
157
+ if legend is not None and legend.get_visible():
158
+ try:
159
+ legend_bbox = legend.get_window_extent(renderer)
160
+ if legend_bbox is not None:
161
+ bbox = transform_bbox(
162
+ legend_bbox,
163
+ fig,
164
+ tight_bbox,
165
+ img_width,
166
+ img_height,
167
+ scale_x,
168
+ scale_y,
169
+ pad_inches,
170
+ saved_height_inches,
171
+ )
172
+ if bbox:
173
+ bboxes[f"ax{ax_idx}_legend"] = {
174
+ **bbox,
175
+ "type": "legend",
176
+ "label": "legend",
177
+ "ax_index": ax_idx,
178
+ }
179
+ except Exception:
180
+ pass
181
+
182
+
183
+ def extract_spines(
184
+ ax,
185
+ ax_idx,
186
+ fig,
187
+ renderer,
188
+ tight_bbox,
189
+ img_width,
190
+ img_height,
191
+ scale_x,
192
+ scale_y,
193
+ pad_inches,
194
+ saved_height_inches,
195
+ bboxes,
196
+ ):
197
+ """Extract bboxes for spines with padding for easier clicking."""
198
+ spine_min_size = 8
199
+ for spine_name, spine in ax.spines.items():
200
+ if spine.get_visible():
201
+ try:
202
+ spine_bbox = spine.get_window_extent(renderer)
203
+ if spine_bbox is not None:
204
+ bbox = transform_bbox(
205
+ spine_bbox,
206
+ fig,
207
+ tight_bbox,
208
+ img_width,
209
+ img_height,
210
+ scale_x,
211
+ scale_y,
212
+ pad_inches,
213
+ saved_height_inches,
214
+ )
215
+ if bbox:
216
+ if bbox["width"] < spine_min_size:
217
+ expand = (spine_min_size - bbox["width"]) / 2
218
+ bbox["x"] -= expand
219
+ bbox["width"] = spine_min_size
220
+ if bbox["height"] < spine_min_size:
221
+ expand = (spine_min_size - bbox["height"]) / 2
222
+ bbox["y"] -= expand
223
+ bbox["height"] = spine_min_size
224
+ bboxes[f"ax{ax_idx}_spine_{spine_name}"] = {
225
+ **bbox,
226
+ "type": "spine",
227
+ "label": spine_name,
228
+ "ax_index": ax_idx,
229
+ }
230
+ except Exception:
231
+ pass
232
+
233
+
234
+ def extract_figure_text(
235
+ fig,
236
+ renderer,
237
+ tight_bbox,
238
+ img_width,
239
+ img_height,
240
+ scale_x,
241
+ scale_y,
242
+ pad_inches,
243
+ saved_height_inches,
244
+ bboxes,
245
+ ):
246
+ """Extract bboxes for figure-level text (suptitle, supxlabel, supylabel)."""
247
+ # Suptitle
248
+ if hasattr(fig, "_suptitle") and fig._suptitle is not None:
249
+ suptitle_obj = fig._suptitle
250
+ if suptitle_obj.get_text():
251
+ try:
252
+ suptitle_extent = suptitle_obj.get_window_extent(renderer)
253
+ if suptitle_extent is not None:
254
+ bbox = transform_bbox(
255
+ suptitle_extent,
256
+ fig,
257
+ tight_bbox,
258
+ img_width,
259
+ img_height,
260
+ scale_x,
261
+ scale_y,
262
+ pad_inches,
263
+ saved_height_inches,
264
+ )
265
+ if bbox:
266
+ bboxes["fig_suptitle"] = {
267
+ **bbox,
268
+ "type": "suptitle",
269
+ "label": "suptitle",
270
+ "ax_index": -1,
271
+ "text": suptitle_obj.get_text(),
272
+ }
273
+ except Exception:
274
+ pass
275
+
276
+ # Supxlabel
277
+ if hasattr(fig, "_supxlabel") and fig._supxlabel is not None:
278
+ supxlabel_obj = fig._supxlabel
279
+ if supxlabel_obj.get_text():
280
+ try:
281
+ supxlabel_extent = supxlabel_obj.get_window_extent(renderer)
282
+ if supxlabel_extent is not None:
283
+ bbox = transform_bbox(
284
+ supxlabel_extent,
285
+ fig,
286
+ tight_bbox,
287
+ img_width,
288
+ img_height,
289
+ scale_x,
290
+ scale_y,
291
+ pad_inches,
292
+ saved_height_inches,
293
+ )
294
+ if bbox:
295
+ bboxes["fig_supxlabel"] = {
296
+ **bbox,
297
+ "type": "supxlabel",
298
+ "label": "supxlabel",
299
+ "ax_index": -1,
300
+ "text": supxlabel_obj.get_text(),
301
+ }
302
+ except Exception:
303
+ pass
304
+
305
+ # Supylabel
306
+ if hasattr(fig, "_supylabel") and fig._supylabel is not None:
307
+ supylabel_obj = fig._supylabel
308
+ if supylabel_obj.get_text():
309
+ try:
310
+ supylabel_extent = supylabel_obj.get_window_extent(renderer)
311
+ if supylabel_extent is not None:
312
+ bbox = transform_bbox(
313
+ supylabel_extent,
314
+ fig,
315
+ tight_bbox,
316
+ img_width,
317
+ img_height,
318
+ scale_x,
319
+ scale_y,
320
+ pad_inches,
321
+ saved_height_inches,
322
+ )
323
+ if bbox:
324
+ bboxes["fig_supylabel"] = {
325
+ **bbox,
326
+ "type": "supylabel",
327
+ "label": "supylabel",
328
+ "ax_index": -1,
329
+ "text": supylabel_obj.get_text(),
330
+ }
331
+ except Exception:
332
+ pass
333
+
334
+
335
+ def extract_annotations(
336
+ ax,
337
+ ax_idx,
338
+ fig,
339
+ renderer,
340
+ tight_bbox,
341
+ img_width,
342
+ img_height,
343
+ scale_x,
344
+ scale_y,
345
+ pad_inches,
346
+ saved_height_inches,
347
+ bboxes,
348
+ ):
349
+ """Extract bboxes for decorative elements (ax.text, ax.annotate, panel labels).
350
+
351
+ These are cosmetic elements that can be repositioned without affecting data.
352
+ """
353
+ import string
354
+
355
+ panel_label_chars = set(string.ascii_uppercase)
356
+
357
+ # Extract ax.texts (includes panel labels and other text annotations)
358
+ for i, text_obj in enumerate(ax.texts):
359
+ text_content = text_obj.get_text().strip()
360
+ if not text_content:
361
+ continue
362
+
363
+ try:
364
+ bbox = get_text_bbox(
365
+ text_obj,
366
+ fig,
367
+ renderer,
368
+ tight_bbox,
369
+ img_width,
370
+ img_height,
371
+ scale_x,
372
+ scale_y,
373
+ pad_inches,
374
+ saved_height_inches,
375
+ )
376
+ if bbox:
377
+ # Determine if this is a panel label
378
+ is_panel_label = (
379
+ text_content in panel_label_chars
380
+ and text_obj.get_transform() == ax.transAxes
381
+ )
382
+
383
+ # Get position
384
+ pos = text_obj.get_position()
385
+
386
+ # Calculate axes-relative position (0-1)
387
+ if text_obj.get_transform() == ax.transAxes:
388
+ # Already in axes coordinates
389
+ rel_x, rel_y = pos[0], pos[1]
390
+ else:
391
+ # Convert from data coordinates to axes
392
+ xlim = ax.get_xlim()
393
+ ylim = ax.get_ylim()
394
+ rel_x = (
395
+ (pos[0] - xlim[0]) / (xlim[1] - xlim[0])
396
+ if xlim[1] != xlim[0]
397
+ else 0.5
398
+ )
399
+ rel_y = (
400
+ (pos[1] - ylim[0]) / (ylim[1] - ylim[0])
401
+ if ylim[1] != ylim[0]
402
+ else 0.5
403
+ )
404
+
405
+ if is_panel_label:
406
+ key = f"ax{ax_idx}_panel_label"
407
+ elem_type = "panel_label"
408
+ else:
409
+ key = f"ax{ax_idx}_text_{i}"
410
+ elem_type = "text"
411
+
412
+ bboxes[key] = {
413
+ **bbox,
414
+ "type": elem_type,
415
+ "label": elem_type,
416
+ "ax_index": ax_idx,
417
+ "text": text_content,
418
+ "text_index": i,
419
+ "pos_x": pos[0],
420
+ "pos_y": pos[1],
421
+ "rel_x": rel_x,
422
+ "rel_y": rel_y,
423
+ }
424
+ except Exception:
425
+ pass
426
+
427
+ # Extract ax.patches that are arrows (FancyArrowPatch)
428
+ from matplotlib.patches import FancyArrowPatch
429
+
430
+ for i, patch in enumerate(ax.patches):
431
+ if isinstance(patch, FancyArrowPatch):
432
+ try:
433
+ patch_bbox = patch.get_window_extent(renderer)
434
+ if patch_bbox is not None:
435
+ bbox = transform_bbox(
436
+ patch_bbox,
437
+ fig,
438
+ tight_bbox,
439
+ img_width,
440
+ img_height,
441
+ scale_x,
442
+ scale_y,
443
+ pad_inches,
444
+ saved_height_inches,
445
+ )
446
+ if bbox:
447
+ bboxes[f"ax{ax_idx}_arrow_{i}"] = {
448
+ **bbox,
449
+ "type": "arrow",
450
+ "label": "arrow",
451
+ "ax_index": ax_idx,
452
+ "arrow_index": i,
453
+ }
454
+ except Exception:
455
+ pass
456
+
457
+
458
+ __all__ = [
459
+ "extract_text_elements",
460
+ "extract_legend",
461
+ "extract_spines",
462
+ "extract_figure_text",
463
+ "extract_annotations",
464
+ ]
465
+
466
+ # EOF
@@ -0,0 +1,173 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ Line bbox extraction for line plots.
5
+
6
+ This module handles bbox extraction for line elements,
7
+ including data point sampling for hit detection.
8
+ """
9
+
10
+ from typing import Any, Dict, Optional
11
+
12
+ from matplotlib.axes import Axes
13
+ from matplotlib.figure import Figure
14
+ from matplotlib.transforms import Bbox
15
+
16
+ from ._transforms import display_to_image, transform_bbox
17
+
18
+
19
+ def get_line_bbox(
20
+ line,
21
+ ax: Axes,
22
+ fig: Figure,
23
+ renderer,
24
+ tight_bbox: Bbox,
25
+ img_width: int,
26
+ img_height: int,
27
+ scale_x: float,
28
+ scale_y: float,
29
+ pad_inches: float,
30
+ saved_height_inches: float,
31
+ include_points: bool = True,
32
+ ) -> Optional[Dict[str, Any]]:
33
+ """Get bbox and points for a line."""
34
+ try:
35
+ # Get window extent
36
+ window_extent = line.get_window_extent(renderer)
37
+ if window_extent is None:
38
+ return None
39
+
40
+ bbox = transform_bbox(
41
+ window_extent,
42
+ fig,
43
+ tight_bbox,
44
+ img_width,
45
+ img_height,
46
+ scale_x,
47
+ scale_y,
48
+ pad_inches,
49
+ saved_height_inches,
50
+ )
51
+ if bbox is None:
52
+ return None
53
+
54
+ # Add path points for proximity detection
55
+ if include_points:
56
+ xdata = line.get_xdata()
57
+ ydata = line.get_ydata()
58
+
59
+ if len(xdata) > 0 and len(ydata) > 0:
60
+ # Transform data coords to image pixels
61
+ transform = ax.transData
62
+ points = []
63
+
64
+ # Downsample if too many points
65
+ max_points = 100
66
+ step = max(1, len(xdata) // max_points)
67
+
68
+ for i in range(0, len(xdata), step):
69
+ try:
70
+ display_coords = transform.transform((xdata[i], ydata[i]))
71
+ img_coords = display_to_image(
72
+ display_coords[0],
73
+ display_coords[1],
74
+ fig,
75
+ tight_bbox,
76
+ img_width,
77
+ img_height,
78
+ scale_x,
79
+ scale_y,
80
+ pad_inches,
81
+ saved_height_inches,
82
+ )
83
+ if img_coords:
84
+ points.append(img_coords)
85
+ except Exception:
86
+ continue
87
+
88
+ if points:
89
+ bbox["points"] = points
90
+
91
+ return bbox
92
+
93
+ except Exception:
94
+ return None
95
+
96
+
97
+ def get_quiver_bbox(
98
+ quiver,
99
+ ax: Axes,
100
+ fig: Figure,
101
+ renderer,
102
+ tight_bbox: Bbox,
103
+ img_width: int,
104
+ img_height: int,
105
+ scale_x: float,
106
+ scale_y: float,
107
+ pad_inches: float,
108
+ saved_height_inches: float,
109
+ ) -> Optional[Dict[str, Any]]:
110
+ """Get bbox for a quiver plot from its data points."""
111
+ try:
112
+ # Get X, Y positions from quiver
113
+ # Quiver stores positions in X, Y arrays
114
+ X = quiver.X
115
+ Y = quiver.Y
116
+
117
+ if X is None or Y is None or len(X) == 0:
118
+ return None
119
+
120
+ # Flatten if needed
121
+ import numpy as np
122
+
123
+ X_flat = np.asarray(X).flatten()
124
+ Y_flat = np.asarray(Y).flatten()
125
+
126
+ if len(X_flat) == 0 or len(Y_flat) == 0:
127
+ return None
128
+
129
+ transform = ax.transData
130
+ points = []
131
+
132
+ # Limit to reasonable number of points
133
+ max_points = 100
134
+ step = max(1, len(X_flat) // max_points)
135
+
136
+ for i in range(0, len(X_flat), step):
137
+ try:
138
+ display_coords = transform.transform((X_flat[i], Y_flat[i]))
139
+ img_coords = display_to_image(
140
+ display_coords[0],
141
+ display_coords[1],
142
+ fig,
143
+ tight_bbox,
144
+ img_width,
145
+ img_height,
146
+ scale_x,
147
+ scale_y,
148
+ pad_inches,
149
+ saved_height_inches,
150
+ )
151
+ if img_coords:
152
+ points.append(img_coords)
153
+ except Exception:
154
+ continue
155
+
156
+ if not points:
157
+ return None
158
+
159
+ # Calculate bbox from points
160
+ xs = [p[0] for p in points]
161
+ ys = [p[1] for p in points]
162
+ padding = 15 # pixels - slightly larger for quiver arrows
163
+ return {
164
+ "x": float(min(xs) - padding),
165
+ "y": float(min(ys) - padding),
166
+ "width": float(max(xs) - min(xs) + 2 * padding),
167
+ "height": float(max(ys) - min(ys) + 2 * padding),
168
+ }
169
+ except Exception:
170
+ return None
171
+
172
+
173
+ __all__ = ["get_line_bbox", "get_quiver_bbox"]