scitex 2.7.0__py3-none-any.whl → 2.8.1__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 (355) hide show
  1. scitex/__init__.py +6 -2
  2. scitex/__version__.py +1 -1
  3. scitex/audio/README.md +52 -0
  4. scitex/audio/__init__.py +384 -0
  5. scitex/audio/__main__.py +129 -0
  6. scitex/audio/_tts.py +334 -0
  7. scitex/audio/engines/__init__.py +44 -0
  8. scitex/audio/engines/base.py +275 -0
  9. scitex/audio/engines/elevenlabs_engine.py +143 -0
  10. scitex/audio/engines/gtts_engine.py +162 -0
  11. scitex/audio/engines/pyttsx3_engine.py +131 -0
  12. scitex/audio/mcp_server.py +757 -0
  13. scitex/bridge/_helpers.py +1 -1
  14. scitex/bridge/_plt_vis.py +1 -1
  15. scitex/bridge/_stats_vis.py +1 -1
  16. scitex/dev/plt/__init__.py +272 -0
  17. scitex/dev/plt/plot_mpl_axhline.py +28 -0
  18. scitex/dev/plt/plot_mpl_axhspan.py +28 -0
  19. scitex/dev/plt/plot_mpl_axvline.py +28 -0
  20. scitex/dev/plt/plot_mpl_axvspan.py +28 -0
  21. scitex/dev/plt/plot_mpl_bar.py +29 -0
  22. scitex/dev/plt/plot_mpl_barh.py +29 -0
  23. scitex/dev/plt/plot_mpl_boxplot.py +28 -0
  24. scitex/dev/plt/plot_mpl_contour.py +31 -0
  25. scitex/dev/plt/plot_mpl_contourf.py +31 -0
  26. scitex/dev/plt/plot_mpl_errorbar.py +30 -0
  27. scitex/dev/plt/plot_mpl_eventplot.py +28 -0
  28. scitex/dev/plt/plot_mpl_fill.py +30 -0
  29. scitex/dev/plt/plot_mpl_fill_between.py +31 -0
  30. scitex/dev/plt/plot_mpl_hexbin.py +28 -0
  31. scitex/dev/plt/plot_mpl_hist.py +28 -0
  32. scitex/dev/plt/plot_mpl_hist2d.py +28 -0
  33. scitex/dev/plt/plot_mpl_imshow.py +29 -0
  34. scitex/dev/plt/plot_mpl_pcolormesh.py +31 -0
  35. scitex/dev/plt/plot_mpl_pie.py +29 -0
  36. scitex/dev/plt/plot_mpl_plot.py +29 -0
  37. scitex/dev/plt/plot_mpl_quiver.py +31 -0
  38. scitex/dev/plt/plot_mpl_scatter.py +28 -0
  39. scitex/dev/plt/plot_mpl_stackplot.py +31 -0
  40. scitex/dev/plt/plot_mpl_stem.py +29 -0
  41. scitex/dev/plt/plot_mpl_step.py +29 -0
  42. scitex/dev/plt/plot_mpl_violinplot.py +28 -0
  43. scitex/dev/plt/plot_sns_barplot.py +29 -0
  44. scitex/dev/plt/plot_sns_boxplot.py +29 -0
  45. scitex/dev/plt/plot_sns_heatmap.py +28 -0
  46. scitex/dev/plt/plot_sns_histplot.py +29 -0
  47. scitex/dev/plt/plot_sns_kdeplot.py +29 -0
  48. scitex/dev/plt/plot_sns_lineplot.py +31 -0
  49. scitex/dev/plt/plot_sns_scatterplot.py +29 -0
  50. scitex/dev/plt/plot_sns_stripplot.py +29 -0
  51. scitex/dev/plt/plot_sns_swarmplot.py +29 -0
  52. scitex/dev/plt/plot_sns_violinplot.py +29 -0
  53. scitex/dev/plt/plot_stx_bar.py +29 -0
  54. scitex/dev/plt/plot_stx_barh.py +29 -0
  55. scitex/dev/plt/plot_stx_box.py +28 -0
  56. scitex/dev/plt/plot_stx_boxplot.py +28 -0
  57. scitex/dev/plt/plot_stx_conf_mat.py +28 -0
  58. scitex/dev/plt/plot_stx_contour.py +31 -0
  59. scitex/dev/plt/plot_stx_ecdf.py +28 -0
  60. scitex/dev/plt/plot_stx_errorbar.py +30 -0
  61. scitex/dev/plt/plot_stx_fill_between.py +31 -0
  62. scitex/dev/plt/plot_stx_fillv.py +28 -0
  63. scitex/dev/plt/plot_stx_heatmap.py +28 -0
  64. scitex/dev/plt/plot_stx_image.py +28 -0
  65. scitex/dev/plt/plot_stx_imshow.py +28 -0
  66. scitex/dev/plt/plot_stx_joyplot.py +28 -0
  67. scitex/dev/plt/plot_stx_kde.py +28 -0
  68. scitex/dev/plt/plot_stx_line.py +28 -0
  69. scitex/dev/plt/plot_stx_mean_ci.py +28 -0
  70. scitex/dev/plt/plot_stx_mean_std.py +28 -0
  71. scitex/dev/plt/plot_stx_median_iqr.py +28 -0
  72. scitex/dev/plt/plot_stx_raster.py +28 -0
  73. scitex/dev/plt/plot_stx_rectangle.py +28 -0
  74. scitex/dev/plt/plot_stx_scatter.py +29 -0
  75. scitex/dev/plt/plot_stx_shaded_line.py +29 -0
  76. scitex/dev/plt/plot_stx_violin.py +28 -0
  77. scitex/dev/plt/plot_stx_violinplot.py +28 -0
  78. scitex/diagram/README.md +197 -0
  79. scitex/diagram/__init__.py +48 -0
  80. scitex/diagram/_compile.py +312 -0
  81. scitex/diagram/_diagram.py +355 -0
  82. scitex/diagram/_presets.py +173 -0
  83. scitex/diagram/_schema.py +182 -0
  84. scitex/diagram/_split.py +278 -0
  85. scitex/fig/__init__.py +352 -0
  86. scitex/{vis → fig}/backend/_parser.py +1 -1
  87. scitex/{vis → fig}/canvas.py +1 -1
  88. scitex/{vis → fig}/editor/__init__.py +5 -2
  89. scitex/{vis → fig}/editor/_dearpygui_editor.py +1 -1
  90. scitex/{vis → fig}/editor/_defaults.py +70 -5
  91. scitex/{vis → fig}/editor/_mpl_editor.py +1 -1
  92. scitex/{vis → fig}/editor/_qt_editor.py +182 -2
  93. scitex/{vis → fig}/editor/_tkinter_editor.py +1 -1
  94. scitex/fig/editor/edit/__init__.py +50 -0
  95. scitex/fig/editor/edit/backend_detector.py +109 -0
  96. scitex/fig/editor/edit/bundle_resolver.py +240 -0
  97. scitex/fig/editor/edit/editor_launcher.py +239 -0
  98. scitex/fig/editor/edit/manual_handler.py +53 -0
  99. scitex/fig/editor/edit/panel_loader.py +232 -0
  100. scitex/fig/editor/edit/path_resolver.py +67 -0
  101. scitex/fig/editor/flask_editor/_bbox.py +1299 -0
  102. scitex/fig/editor/flask_editor/_core.py +1429 -0
  103. scitex/{vis → fig}/editor/flask_editor/_plotter.py +38 -4
  104. scitex/fig/editor/flask_editor/_renderer.py +813 -0
  105. scitex/fig/editor/flask_editor/static/css/base/reset.css +41 -0
  106. scitex/fig/editor/flask_editor/static/css/base/typography.css +16 -0
  107. scitex/fig/editor/flask_editor/static/css/base/variables.css +85 -0
  108. scitex/fig/editor/flask_editor/static/css/components/buttons.css +217 -0
  109. scitex/fig/editor/flask_editor/static/css/components/context-menu.css +93 -0
  110. scitex/fig/editor/flask_editor/static/css/components/dropdown.css +57 -0
  111. scitex/fig/editor/flask_editor/static/css/components/forms.css +112 -0
  112. scitex/fig/editor/flask_editor/static/css/components/modal.css +59 -0
  113. scitex/fig/editor/flask_editor/static/css/components/sections.css +212 -0
  114. scitex/fig/editor/flask_editor/static/css/features/canvas.css +176 -0
  115. scitex/fig/editor/flask_editor/static/css/features/element-inspector.css +190 -0
  116. scitex/fig/editor/flask_editor/static/css/features/loading.css +59 -0
  117. scitex/fig/editor/flask_editor/static/css/features/overlay.css +45 -0
  118. scitex/fig/editor/flask_editor/static/css/features/panel-grid.css +95 -0
  119. scitex/fig/editor/flask_editor/static/css/features/selection.css +101 -0
  120. scitex/fig/editor/flask_editor/static/css/features/statistics.css +138 -0
  121. scitex/fig/editor/flask_editor/static/css/index.css +31 -0
  122. scitex/fig/editor/flask_editor/static/css/layout/container.css +7 -0
  123. scitex/fig/editor/flask_editor/static/css/layout/controls.css +56 -0
  124. scitex/fig/editor/flask_editor/static/css/layout/preview.css +78 -0
  125. scitex/fig/editor/flask_editor/static/js/alignment/axis.js +314 -0
  126. scitex/fig/editor/flask_editor/static/js/alignment/basic.js +107 -0
  127. scitex/fig/editor/flask_editor/static/js/alignment/distribute.js +54 -0
  128. scitex/fig/editor/flask_editor/static/js/canvas/canvas.js +172 -0
  129. scitex/fig/editor/flask_editor/static/js/canvas/dragging.js +258 -0
  130. scitex/fig/editor/flask_editor/static/js/canvas/resize.js +48 -0
  131. scitex/fig/editor/flask_editor/static/js/canvas/selection.js +71 -0
  132. scitex/fig/editor/flask_editor/static/js/core/api.js +288 -0
  133. scitex/fig/editor/flask_editor/static/js/core/state.js +143 -0
  134. scitex/fig/editor/flask_editor/static/js/core/utils.js +245 -0
  135. scitex/fig/editor/flask_editor/static/js/dev/element-inspector.js +992 -0
  136. scitex/fig/editor/flask_editor/static/js/editor/bbox.js +339 -0
  137. scitex/fig/editor/flask_editor/static/js/editor/element-drag.js +286 -0
  138. scitex/fig/editor/flask_editor/static/js/editor/overlay.js +371 -0
  139. scitex/fig/editor/flask_editor/static/js/editor/preview.js +293 -0
  140. scitex/fig/editor/flask_editor/static/js/main.js +426 -0
  141. scitex/fig/editor/flask_editor/static/js/shortcuts/context-menu.js +152 -0
  142. scitex/fig/editor/flask_editor/static/js/shortcuts/keyboard.js +265 -0
  143. scitex/fig/editor/flask_editor/static/js/ui/controls.js +184 -0
  144. scitex/fig/editor/flask_editor/static/js/ui/download.js +57 -0
  145. scitex/fig/editor/flask_editor/static/js/ui/help.js +100 -0
  146. scitex/fig/editor/flask_editor/static/js/ui/theme.js +34 -0
  147. scitex/fig/editor/flask_editor/templates/__init__.py +123 -0
  148. scitex/fig/editor/flask_editor/templates/_html.py +852 -0
  149. scitex/fig/editor/flask_editor/templates/_scripts.py +4933 -0
  150. scitex/fig/editor/flask_editor/templates/_styles.py +1658 -0
  151. scitex/{vis → fig}/io/__init__.py +13 -1
  152. scitex/fig/io/_bundle.py +1058 -0
  153. scitex/{vis → fig}/io/_canvas.py +1 -1
  154. scitex/{vis → fig}/io/_data.py +1 -1
  155. scitex/{vis → fig}/io/_export.py +1 -1
  156. scitex/{vis → fig}/io/_load.py +1 -1
  157. scitex/{vis → fig}/io/_panel.py +1 -1
  158. scitex/{vis → fig}/io/_save.py +1 -1
  159. scitex/{vis → fig}/model/__init__.py +1 -1
  160. scitex/{vis → fig}/model/_annotations.py +1 -1
  161. scitex/{vis → fig}/model/_axes.py +1 -1
  162. scitex/{vis → fig}/model/_figure.py +1 -1
  163. scitex/{vis → fig}/model/_guides.py +1 -1
  164. scitex/{vis → fig}/model/_plot.py +1 -1
  165. scitex/{vis → fig}/model/_styles.py +1 -1
  166. scitex/{vis → fig}/utils/__init__.py +1 -1
  167. scitex/io/__init__.py +22 -26
  168. scitex/io/_bundle.py +493 -0
  169. scitex/io/_flush.py +5 -2
  170. scitex/io/_load.py +98 -0
  171. scitex/io/_load_modules/_H5Explorer.py +5 -2
  172. scitex/io/_load_modules/_canvas.py +2 -2
  173. scitex/io/_load_modules/_image.py +3 -4
  174. scitex/io/_load_modules/_txt.py +4 -2
  175. scitex/io/_metadata.py +34 -324
  176. scitex/io/_metadata_modules/__init__.py +46 -0
  177. scitex/io/_metadata_modules/_embed.py +70 -0
  178. scitex/io/_metadata_modules/_read.py +64 -0
  179. scitex/io/_metadata_modules/_utils.py +79 -0
  180. scitex/io/_metadata_modules/embed_metadata_jpeg.py +74 -0
  181. scitex/io/_metadata_modules/embed_metadata_pdf.py +53 -0
  182. scitex/io/_metadata_modules/embed_metadata_png.py +26 -0
  183. scitex/io/_metadata_modules/embed_metadata_svg.py +62 -0
  184. scitex/io/_metadata_modules/read_metadata_jpeg.py +57 -0
  185. scitex/io/_metadata_modules/read_metadata_pdf.py +51 -0
  186. scitex/io/_metadata_modules/read_metadata_png.py +39 -0
  187. scitex/io/_metadata_modules/read_metadata_svg.py +44 -0
  188. scitex/io/_qr_utils.py +5 -3
  189. scitex/io/_save.py +548 -30
  190. scitex/io/_save_modules/_canvas.py +3 -3
  191. scitex/io/_save_modules/_image.py +5 -9
  192. scitex/io/_save_modules/_tex.py +7 -4
  193. scitex/io/_zip_bundle.py +439 -0
  194. scitex/io/utils/h5_to_zarr.py +11 -9
  195. scitex/msword/__init__.py +255 -0
  196. scitex/msword/profiles.py +357 -0
  197. scitex/msword/reader.py +753 -0
  198. scitex/msword/utils.py +289 -0
  199. scitex/msword/writer.py +362 -0
  200. scitex/plt/__init__.py +5 -2
  201. scitex/plt/_subplots/_AxesWrapper.py +6 -6
  202. scitex/plt/_subplots/_AxisWrapper.py +15 -9
  203. scitex/plt/_subplots/_AxisWrapperMixins/_AdjustmentMixin/__init__.py +36 -0
  204. scitex/plt/_subplots/_AxisWrapperMixins/_AdjustmentMixin/_labels.py +264 -0
  205. scitex/plt/_subplots/_AxisWrapperMixins/_AdjustmentMixin/_metadata.py +213 -0
  206. scitex/plt/_subplots/_AxisWrapperMixins/_AdjustmentMixin/_visual.py +128 -0
  207. scitex/plt/_subplots/_AxisWrapperMixins/_MatplotlibPlotMixin/__init__.py +59 -0
  208. scitex/plt/_subplots/_AxisWrapperMixins/_MatplotlibPlotMixin/_base.py +34 -0
  209. scitex/plt/_subplots/_AxisWrapperMixins/_MatplotlibPlotMixin/_scientific.py +593 -0
  210. scitex/plt/_subplots/_AxisWrapperMixins/_MatplotlibPlotMixin/_statistical.py +654 -0
  211. scitex/plt/_subplots/_AxisWrapperMixins/_MatplotlibPlotMixin/_stx_aliases.py +527 -0
  212. scitex/plt/_subplots/_AxisWrapperMixins/_RawMatplotlibMixin.py +321 -0
  213. scitex/plt/_subplots/_AxisWrapperMixins/_SeabornMixin/__init__.py +33 -0
  214. scitex/plt/_subplots/_AxisWrapperMixins/_SeabornMixin/_base.py +152 -0
  215. scitex/plt/_subplots/_AxisWrapperMixins/_SeabornMixin/_wrappers.py +600 -0
  216. scitex/plt/_subplots/_AxisWrapperMixins/__init__.py +79 -5
  217. scitex/plt/_subplots/_FigWrapper.py +6 -6
  218. scitex/plt/_subplots/_SubplotsWrapper.py +28 -18
  219. scitex/plt/_subplots/_export_as_csv.py +35 -5
  220. scitex/plt/_subplots/_export_as_csv_formatters/__init__.py +8 -0
  221. scitex/plt/_subplots/_export_as_csv_formatters/_format_annotate.py +10 -21
  222. scitex/plt/_subplots/_export_as_csv_formatters/_format_eventplot.py +18 -7
  223. scitex/plt/_subplots/_export_as_csv_formatters/_format_imshow2d.py +28 -12
  224. scitex/plt/_subplots/_export_as_csv_formatters/_format_matshow.py +10 -4
  225. scitex/plt/_subplots/_export_as_csv_formatters/_format_plot_imshow.py +13 -1
  226. scitex/plt/_subplots/_export_as_csv_formatters/_format_plot_kde.py +12 -2
  227. scitex/plt/_subplots/_export_as_csv_formatters/_format_plot_scatter.py +10 -3
  228. scitex/plt/_subplots/_export_as_csv_formatters/_format_quiver.py +10 -4
  229. scitex/plt/_subplots/_export_as_csv_formatters/_format_sns_jointplot.py +18 -3
  230. scitex/plt/_subplots/_export_as_csv_formatters/_format_sns_lineplot.py +44 -36
  231. scitex/plt/_subplots/_export_as_csv_formatters/_format_sns_pairplot.py +14 -2
  232. scitex/plt/_subplots/_export_as_csv_formatters/_format_streamplot.py +11 -5
  233. scitex/plt/_subplots/_export_as_csv_formatters/_format_stx_bar.py +84 -0
  234. scitex/plt/_subplots/_export_as_csv_formatters/_format_stx_barh.py +85 -0
  235. scitex/plt/_subplots/_export_as_csv_formatters/_format_stx_conf_mat.py +14 -3
  236. scitex/plt/_subplots/_export_as_csv_formatters/_format_stx_contour.py +54 -0
  237. scitex/plt/_subplots/_export_as_csv_formatters/_format_stx_ecdf.py +14 -2
  238. scitex/plt/_subplots/_export_as_csv_formatters/_format_stx_errorbar.py +120 -0
  239. scitex/plt/_subplots/_export_as_csv_formatters/_format_stx_heatmap.py +16 -6
  240. scitex/plt/_subplots/_export_as_csv_formatters/_format_stx_image.py +29 -19
  241. scitex/plt/_subplots/_export_as_csv_formatters/_format_stx_imshow.py +63 -0
  242. scitex/plt/_subplots/_export_as_csv_formatters/_format_stx_joyplot.py +22 -5
  243. scitex/plt/_subplots/_export_as_csv_formatters/_format_stx_mean_ci.py +18 -14
  244. scitex/plt/_subplots/_export_as_csv_formatters/_format_stx_mean_std.py +18 -14
  245. scitex/plt/_subplots/_export_as_csv_formatters/_format_stx_median_iqr.py +18 -14
  246. scitex/plt/_subplots/_export_as_csv_formatters/_format_stx_raster.py +10 -2
  247. scitex/plt/_subplots/_export_as_csv_formatters/_format_stx_scatter.py +51 -0
  248. scitex/plt/_subplots/_export_as_csv_formatters/_format_stx_scatter_hist.py +18 -9
  249. scitex/plt/ax/_plot/_stx_ecdf.py +4 -2
  250. scitex/plt/gallery/_generate.py +421 -14
  251. scitex/plt/io/__init__.py +53 -0
  252. scitex/plt/io/_bundle.py +490 -0
  253. scitex/plt/io/_layered_bundle.py +1343 -0
  254. scitex/plt/styles/SCITEX_STYLE.yaml +26 -0
  255. scitex/plt/styles/__init__.py +14 -0
  256. scitex/plt/styles/presets.py +78 -0
  257. scitex/plt/utils/__init__.py +13 -1
  258. scitex/plt/utils/_collect_figure_metadata.py +10 -14
  259. scitex/plt/utils/_configure_mpl.py +6 -18
  260. scitex/plt/utils/_crop.py +32 -14
  261. scitex/plt/utils/_csv_column_naming.py +54 -0
  262. scitex/plt/utils/_figure_mm.py +116 -1
  263. scitex/plt/utils/_hitmap.py +1643 -0
  264. scitex/plt/utils/metadata/__init__.py +25 -0
  265. scitex/plt/utils/metadata/_core.py +9 -10
  266. scitex/plt/utils/metadata/_dimensions.py +6 -3
  267. scitex/plt/utils/metadata/_editable_export.py +405 -0
  268. scitex/plt/utils/metadata/_geometry_extraction.py +570 -0
  269. scitex/schema/__init__.py +109 -16
  270. scitex/schema/_canvas.py +1 -1
  271. scitex/schema/_plot.py +1015 -0
  272. scitex/schema/_stats.py +2 -2
  273. scitex/stats/__init__.py +117 -0
  274. scitex/stats/io/__init__.py +29 -0
  275. scitex/stats/io/_bundle.py +156 -0
  276. scitex/tex/__init__.py +4 -0
  277. scitex/tex/_export.py +890 -0
  278. {scitex-2.7.0.dist-info → scitex-2.8.1.dist-info}/METADATA +11 -1
  279. {scitex-2.7.0.dist-info → scitex-2.8.1.dist-info}/RECORD +294 -170
  280. scitex/io/memo.md +0 -2827
  281. scitex/plt/REQUESTS.md +0 -191
  282. scitex/plt/_subplots/TODO.md +0 -53
  283. scitex/plt/_subplots/_AxisWrapperMixins/_AdjustmentMixin.py +0 -559
  284. scitex/plt/_subplots/_AxisWrapperMixins/_MatplotlibPlotMixin.py +0 -1609
  285. scitex/plt/_subplots/_AxisWrapperMixins/_SeabornMixin.py +0 -447
  286. scitex/plt/templates/research-master/scitex/vis/gallery/area/fill_between.json +0 -110
  287. scitex/plt/templates/research-master/scitex/vis/gallery/area/fill_betweenx.json +0 -88
  288. scitex/plt/templates/research-master/scitex/vis/gallery/area/stx_fill_between.json +0 -103
  289. scitex/plt/templates/research-master/scitex/vis/gallery/area/stx_fillv.json +0 -106
  290. scitex/plt/templates/research-master/scitex/vis/gallery/categorical/bar.json +0 -92
  291. scitex/plt/templates/research-master/scitex/vis/gallery/categorical/barh.json +0 -92
  292. scitex/plt/templates/research-master/scitex/vis/gallery/categorical/boxplot.json +0 -92
  293. scitex/plt/templates/research-master/scitex/vis/gallery/categorical/stx_bar.json +0 -84
  294. scitex/plt/templates/research-master/scitex/vis/gallery/categorical/stx_barh.json +0 -84
  295. scitex/plt/templates/research-master/scitex/vis/gallery/categorical/stx_box.json +0 -83
  296. scitex/plt/templates/research-master/scitex/vis/gallery/categorical/stx_boxplot.json +0 -93
  297. scitex/plt/templates/research-master/scitex/vis/gallery/categorical/stx_violin.json +0 -91
  298. scitex/plt/templates/research-master/scitex/vis/gallery/categorical/stx_violinplot.json +0 -91
  299. scitex/plt/templates/research-master/scitex/vis/gallery/categorical/violinplot.json +0 -91
  300. scitex/plt/templates/research-master/scitex/vis/gallery/contour/contour.json +0 -97
  301. scitex/plt/templates/research-master/scitex/vis/gallery/contour/contourf.json +0 -98
  302. scitex/plt/templates/research-master/scitex/vis/gallery/contour/stx_contour.json +0 -84
  303. scitex/plt/templates/research-master/scitex/vis/gallery/distribution/hist.json +0 -101
  304. scitex/plt/templates/research-master/scitex/vis/gallery/distribution/hist2d.json +0 -96
  305. scitex/plt/templates/research-master/scitex/vis/gallery/distribution/stx_ecdf.json +0 -95
  306. scitex/plt/templates/research-master/scitex/vis/gallery/distribution/stx_joyplot.json +0 -95
  307. scitex/plt/templates/research-master/scitex/vis/gallery/distribution/stx_kde.json +0 -93
  308. scitex/plt/templates/research-master/scitex/vis/gallery/grid/imshow.json +0 -95
  309. scitex/plt/templates/research-master/scitex/vis/gallery/grid/matshow.json +0 -95
  310. scitex/plt/templates/research-master/scitex/vis/gallery/grid/stx_conf_mat.json +0 -83
  311. scitex/plt/templates/research-master/scitex/vis/gallery/grid/stx_heatmap.json +0 -92
  312. scitex/plt/templates/research-master/scitex/vis/gallery/grid/stx_image.json +0 -121
  313. scitex/plt/templates/research-master/scitex/vis/gallery/grid/stx_imshow.json +0 -84
  314. scitex/plt/templates/research-master/scitex/vis/gallery/line/plot.json +0 -110
  315. scitex/plt/templates/research-master/scitex/vis/gallery/line/step.json +0 -92
  316. scitex/plt/templates/research-master/scitex/vis/gallery/line/stx_line.json +0 -95
  317. scitex/plt/templates/research-master/scitex/vis/gallery/line/stx_shaded_line.json +0 -96
  318. scitex/plt/templates/research-master/scitex/vis/gallery/scatter/hexbin.json +0 -95
  319. scitex/plt/templates/research-master/scitex/vis/gallery/scatter/scatter.json +0 -95
  320. scitex/plt/templates/research-master/scitex/vis/gallery/scatter/stem.json +0 -92
  321. scitex/plt/templates/research-master/scitex/vis/gallery/scatter/stx_scatter.json +0 -84
  322. scitex/plt/templates/research-master/scitex/vis/gallery/special/pie.json +0 -94
  323. scitex/plt/templates/research-master/scitex/vis/gallery/special/stx_raster.json +0 -109
  324. scitex/plt/templates/research-master/scitex/vis/gallery/special/stx_rectangle.json +0 -108
  325. scitex/plt/templates/research-master/scitex/vis/gallery/statistical/errorbar.json +0 -93
  326. scitex/plt/templates/research-master/scitex/vis/gallery/statistical/stx_errorbar.json +0 -84
  327. scitex/plt/templates/research-master/scitex/vis/gallery/statistical/stx_mean_ci.json +0 -96
  328. scitex/plt/templates/research-master/scitex/vis/gallery/statistical/stx_mean_std.json +0 -96
  329. scitex/plt/templates/research-master/scitex/vis/gallery/statistical/stx_median_iqr.json +0 -96
  330. scitex/plt/templates/research-master/scitex/vis/gallery/vector/quiver.json +0 -99
  331. scitex/plt/templates/research-master/scitex/vis/gallery/vector/streamplot.json +0 -100
  332. scitex/vis/__init__.py +0 -177
  333. scitex/vis/editor/_edit.py +0 -390
  334. scitex/vis/editor/flask_editor/_bbox.py +0 -529
  335. scitex/vis/editor/flask_editor/_core.py +0 -168
  336. scitex/vis/editor/flask_editor/_renderer.py +0 -393
  337. scitex/vis/editor/flask_editor/templates/__init__.py +0 -33
  338. scitex/vis/editor/flask_editor/templates/_html.py +0 -513
  339. scitex/vis/editor/flask_editor/templates/_scripts.py +0 -1261
  340. scitex/vis/editor/flask_editor/templates/_styles.py +0 -739
  341. /scitex/{vis → fig}/README.md +0 -0
  342. /scitex/{vis → fig}/backend/__init__.py +0 -0
  343. /scitex/{vis → fig}/backend/_export.py +0 -0
  344. /scitex/{vis → fig}/backend/_render.py +0 -0
  345. /scitex/{vis → fig}/docs/CANVAS_ARCHITECTURE.md +0 -0
  346. /scitex/{vis → fig}/editor/_flask_editor.py +0 -0
  347. /scitex/{vis → fig}/editor/flask_editor/__init__.py +0 -0
  348. /scitex/{vis → fig}/editor/flask_editor/_utils.py +0 -0
  349. /scitex/{vis → fig}/io/_directory.py +0 -0
  350. /scitex/{vis → fig}/model/_plot_types.py +0 -0
  351. /scitex/{vis → fig}/utils/_defaults.py +0 -0
  352. /scitex/{vis → fig}/utils/_validate.py +0 -0
  353. {scitex-2.7.0.dist-info → scitex-2.8.1.dist-info}/WHEEL +0 -0
  354. {scitex-2.7.0.dist-info → scitex-2.8.1.dist-info}/entry_points.txt +0 -0
  355. {scitex-2.7.0.dist-info → scitex-2.8.1.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,1299 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ # File: ./src/scitex/vis/editor/flask_editor/bbox.py
4
+ """Bounding box extraction for figure elements.
5
+
6
+ Updated to integrate Schema v0.3 geometry extraction for shape-based hit testing.
7
+ """
8
+
9
+ from typing import Dict, Any
10
+
11
+ # Try to import schema v0.3 geometry extraction
12
+ try:
13
+ from scitex.plt.utils.metadata._geometry_extraction import (
14
+ extract_axes_bbox_px,
15
+ extract_line_geometry,
16
+ extract_scatter_geometry,
17
+ extract_polygon_geometry,
18
+ extract_bar_group_geometry,
19
+ )
20
+ GEOMETRY_V03_AVAILABLE = True
21
+ except ImportError:
22
+ GEOMETRY_V03_AVAILABLE = False
23
+
24
+
25
+ def extract_bboxes(
26
+ fig, ax, renderer, img_width: int, img_height: int
27
+ ) -> Dict[str, Any]:
28
+ """Extract bounding boxes for all figure elements (single-axis)."""
29
+ from matplotlib.transforms import Bbox
30
+
31
+ # Get figure tight bbox in inches
32
+ fig_bbox = fig.get_tightbbox(renderer)
33
+ tight_x0 = fig_bbox.x0
34
+ tight_y0 = fig_bbox.y0
35
+ tight_width = fig_bbox.width
36
+ tight_height = fig_bbox.height
37
+
38
+ # bbox_inches='tight' adds pad_inches (default 0.1) around the tight bbox
39
+ pad_inches = 0.1
40
+ saved_width_inches = tight_width + 2 * pad_inches
41
+ saved_height_inches = tight_height + 2 * pad_inches
42
+
43
+ # Scale factors for converting inches to pixels
44
+ scale_x = img_width / saved_width_inches
45
+ scale_y = img_height / saved_height_inches
46
+
47
+ bboxes = {}
48
+
49
+ def get_element_bbox(element, name):
50
+ """Get element bbox in image pixel coordinates."""
51
+ try:
52
+ bbox = element.get_window_extent(renderer)
53
+
54
+ elem_x0_inches = bbox.x0 / fig.dpi
55
+ elem_x1_inches = bbox.x1 / fig.dpi
56
+ elem_y0_inches = bbox.y0 / fig.dpi
57
+ elem_y1_inches = bbox.y1 / fig.dpi
58
+
59
+ x0_rel = elem_x0_inches - tight_x0 + pad_inches
60
+ x1_rel = elem_x1_inches - tight_x0 + pad_inches
61
+ y0_rel = saved_height_inches - (elem_y1_inches - tight_y0 + pad_inches)
62
+ y1_rel = saved_height_inches - (elem_y0_inches - tight_y0 + pad_inches)
63
+
64
+ bboxes[name] = {
65
+ "x0": max(0, int(x0_rel * scale_x)),
66
+ "y0": max(0, int(y0_rel * scale_y)),
67
+ "x1": min(img_width, int(x1_rel * scale_x)),
68
+ "y1": min(img_height, int(y1_rel * scale_y)),
69
+ "label": name.replace("_", " ").title(),
70
+ }
71
+ except Exception as e:
72
+ print(f"Error getting bbox for {name}: {e}")
73
+
74
+ def bbox_to_img_coords(bbox):
75
+ """Convert matplotlib bbox to image pixel coordinates."""
76
+ x0_inches = bbox.x0 / fig.dpi
77
+ y0_inches = bbox.y0 / fig.dpi
78
+ x1_inches = bbox.x1 / fig.dpi
79
+ y1_inches = bbox.y1 / fig.dpi
80
+ x0_rel = x0_inches - tight_x0 + pad_inches
81
+ y0_rel = y0_inches - tight_y0 + pad_inches
82
+ x1_rel = x1_inches - tight_x0 + pad_inches
83
+ y1_rel = y1_inches - tight_y0 + pad_inches
84
+ return {
85
+ "x0": int(x0_rel * scale_x),
86
+ "y0": int((saved_height_inches - y1_rel) * scale_y),
87
+ "x1": int(x1_rel * scale_x),
88
+ "y1": int((saved_height_inches - y0_rel) * scale_y),
89
+ }
90
+
91
+ # Get bboxes for title, labels
92
+ # Use ax_00_ prefix for consistency with geometry_px.json format
93
+ ax_prefix = "ax_00_"
94
+ if ax.title.get_text():
95
+ get_element_bbox(ax.title, f"{ax_prefix}title")
96
+ if ax.xaxis.label.get_text():
97
+ get_element_bbox(ax.xaxis.label, f"{ax_prefix}xlabel")
98
+ if ax.yaxis.label.get_text():
99
+ get_element_bbox(ax.yaxis.label, f"{ax_prefix}ylabel")
100
+
101
+ # Get axis bboxes
102
+ _extract_axis_bboxes(ax, renderer, bboxes, bbox_to_img_coords, Bbox, ax_prefix)
103
+
104
+ # Get legend bbox
105
+ legend = ax.get_legend()
106
+ if legend:
107
+ get_element_bbox(legend, f"{ax_prefix}legend")
108
+
109
+ # Get caption bbox (figure-level text)
110
+ for text_artist in fig.texts:
111
+ # Check if this is a caption (positioned below figure)
112
+ text_content = text_artist.get_text()
113
+ if text_content:
114
+ pos = text_artist.get_position()
115
+ # Caption is typically centered horizontally (x ~= 0.5) and below figure (y < 0.1)
116
+ if pos[1] < 0.1:
117
+ get_element_bbox(text_artist, f"{ax_prefix}caption")
118
+ break # Only one caption expected
119
+
120
+ # Get trace (line) bboxes
121
+ _extract_trace_bboxes(
122
+ ax,
123
+ fig,
124
+ renderer,
125
+ bboxes,
126
+ get_element_bbox,
127
+ tight_x0,
128
+ tight_y0,
129
+ saved_height_inches,
130
+ scale_x,
131
+ scale_y,
132
+ pad_inches,
133
+ )
134
+
135
+ # Add schema v0.3 metadata if available
136
+ if GEOMETRY_V03_AVAILABLE:
137
+ axes_bbox = extract_axes_bbox_px(ax, fig)
138
+ bboxes["_meta"] = {
139
+ "schema_version": "0.3.0",
140
+ "axes_bbox_px": axes_bbox,
141
+ "geometry_available": True,
142
+ }
143
+
144
+ return bboxes
145
+
146
+
147
+ def extract_bboxes_multi(
148
+ fig, axes_map: Dict[str, Any], renderer, img_width: int, img_height: int
149
+ ) -> Dict[str, Any]:
150
+ """Extract bounding boxes for all elements in a multi-axis figure.
151
+
152
+ Args:
153
+ fig: Matplotlib figure
154
+ axes_map: Dict mapping axis IDs (e.g., 'ax_00') to matplotlib Axes objects
155
+ renderer: Matplotlib renderer
156
+ img_width: Image width in pixels
157
+ img_height: Image height in pixels
158
+
159
+ Returns:
160
+ Dict with bboxes keyed by "{ax_id}_{element_type}" (e.g., "ax_00_xlabel")
161
+ """
162
+ from matplotlib.transforms import Bbox
163
+
164
+ # Get figure tight bbox in inches
165
+ fig_bbox = fig.get_tightbbox(renderer)
166
+ tight_x0 = fig_bbox.x0
167
+ tight_y0 = fig_bbox.y0
168
+ tight_width = fig_bbox.width
169
+ tight_height = fig_bbox.height
170
+
171
+ # bbox_inches='tight' adds pad_inches (default 0.1) around the tight bbox
172
+ pad_inches = 0.1
173
+ saved_width_inches = tight_width + 2 * pad_inches
174
+ saved_height_inches = tight_height + 2 * pad_inches
175
+
176
+ # Scale factors for converting inches to pixels
177
+ scale_x = img_width / saved_width_inches
178
+ scale_y = img_height / saved_height_inches
179
+
180
+ bboxes = {}
181
+
182
+ def get_element_bbox(element, name, ax_id, current_ax=None):
183
+ """Get element bbox in image pixel coordinates."""
184
+ import numpy as np
185
+ full_name = f"{ax_id}_{name}"
186
+ try:
187
+ bbox = element.get_window_extent(renderer)
188
+
189
+ # Check for invalid bbox (infinity or NaN)
190
+ if not (np.isfinite(bbox.x0) and np.isfinite(bbox.x1) and
191
+ np.isfinite(bbox.y0) and np.isfinite(bbox.y1)):
192
+ # Try to get bbox from data for scatter/collection elements
193
+ if hasattr(element, 'get_offsets') and current_ax is not None:
194
+ offsets = element.get_offsets()
195
+ if len(offsets) > 0 and np.isfinite(offsets).all():
196
+ # Use axis transform to get display coordinates
197
+ display_coords = current_ax.transData.transform(offsets)
198
+ x0 = display_coords[:, 0].min()
199
+ x1 = display_coords[:, 0].max()
200
+ y0 = display_coords[:, 1].min()
201
+ y1 = display_coords[:, 1].max()
202
+ if np.isfinite([x0, x1, y0, y1]).all():
203
+ from matplotlib.transforms import Bbox
204
+ bbox = Bbox.from_extents(x0, y0, x1, y1)
205
+ else:
206
+ return # Skip this element
207
+ else:
208
+ return # Skip this element
209
+ else:
210
+ return # Skip this element
211
+
212
+ elem_x0_inches = bbox.x0 / fig.dpi
213
+ elem_x1_inches = bbox.x1 / fig.dpi
214
+ elem_y0_inches = bbox.y0 / fig.dpi
215
+ elem_y1_inches = bbox.y1 / fig.dpi
216
+
217
+ x0_rel = elem_x0_inches - tight_x0 + pad_inches
218
+ x1_rel = elem_x1_inches - tight_x0 + pad_inches
219
+ y0_rel = saved_height_inches - (elem_y1_inches - tight_y0 + pad_inches)
220
+ y1_rel = saved_height_inches - (elem_y0_inches - tight_y0 + pad_inches)
221
+
222
+ # Clamp values to avoid overflow
223
+ x0_px = max(0, min(img_width, int(x0_rel * scale_x)))
224
+ y0_px = max(0, min(img_height, int(y0_rel * scale_y)))
225
+ x1_px = max(0, min(img_width, int(x1_rel * scale_x)))
226
+ y1_px = max(0, min(img_height, int(y1_rel * scale_y)))
227
+
228
+ bboxes[full_name] = {
229
+ "x0": x0_px,
230
+ "y0": y0_px,
231
+ "x1": x1_px,
232
+ "y1": y1_px,
233
+ "label": f"{ax_id}: {name.replace('_', ' ').title()}",
234
+ "ax_id": ax_id,
235
+ }
236
+ except Exception as e:
237
+ print(f"Error getting bbox for {full_name}: {e}")
238
+
239
+ def bbox_to_img_coords(bbox):
240
+ """Convert matplotlib bbox to image pixel coordinates."""
241
+ x0_inches = bbox.x0 / fig.dpi
242
+ y0_inches = bbox.y0 / fig.dpi
243
+ x1_inches = bbox.x1 / fig.dpi
244
+ y1_inches = bbox.y1 / fig.dpi
245
+ x0_rel = x0_inches - tight_x0 + pad_inches
246
+ y0_rel = y0_inches - tight_y0 + pad_inches
247
+ x1_rel = x1_inches - tight_x0 + pad_inches
248
+ y1_rel = y1_inches - tight_y0 + pad_inches
249
+ return {
250
+ "x0": int(x0_rel * scale_x),
251
+ "y0": int((saved_height_inches - y1_rel) * scale_y),
252
+ "x1": int(x1_rel * scale_x),
253
+ "y1": int((saved_height_inches - y0_rel) * scale_y),
254
+ }
255
+
256
+ # Extract bboxes for each axis
257
+ for ax_id, ax in axes_map.items():
258
+ # Get axes bounding box (the entire panel area)
259
+ try:
260
+ ax_bbox = ax.get_window_extent(renderer)
261
+ coords = bbox_to_img_coords(ax_bbox)
262
+ # Extract actual title/labels from the axes
263
+ title_text = ax.title.get_text() if ax.title else ""
264
+ xlabel_text = ax.xaxis.label.get_text() if ax.xaxis.label else ""
265
+ ylabel_text = ax.yaxis.label.get_text() if ax.yaxis.label else ""
266
+ bboxes[f"{ax_id}_panel"] = {
267
+ **coords,
268
+ "label": f"Panel {ax_id}",
269
+ "ax_id": ax_id,
270
+ "is_panel": True,
271
+ "title": title_text,
272
+ "xlabel": xlabel_text,
273
+ "ylabel": ylabel_text,
274
+ }
275
+ except Exception as e:
276
+ print(f"Error getting panel bbox for {ax_id}: {e}")
277
+
278
+ # Get bboxes for title, labels
279
+ if ax.title.get_text():
280
+ get_element_bbox(ax.title, "title", ax_id, ax)
281
+ if ax.xaxis.label.get_text():
282
+ get_element_bbox(ax.xaxis.label, "xlabel", ax_id, ax)
283
+ if ax.yaxis.label.get_text():
284
+ get_element_bbox(ax.yaxis.label, "ylabel", ax_id, ax)
285
+
286
+ # Get X-axis bbox (spine + ticks + ticklabels)
287
+ _extract_axis_bboxes_for_axis(ax, ax_id, renderer, bboxes, bbox_to_img_coords, Bbox)
288
+
289
+ # Get legend bbox
290
+ legend = ax.get_legend()
291
+ if legend:
292
+ get_element_bbox(legend, "legend", ax_id, ax)
293
+ # Add element_type for drag detection
294
+ if f"{ax_id}_legend" in bboxes:
295
+ bboxes[f"{ax_id}_legend"]["element_type"] = "legend"
296
+ bboxes[f"{ax_id}_legend"]["draggable"] = True
297
+
298
+ # Get panel letter (text annotations like A, B, C)
299
+ import re
300
+ panel_letter_pattern = re.compile(r'^[A-Z]\.?$|^\([A-Za-z]\)$')
301
+ for idx, text_artist in enumerate(ax.texts):
302
+ text_content = text_artist.get_text().strip()
303
+ if text_content and panel_letter_pattern.match(text_content):
304
+ name = f"panel_letter_{text_content.replace('.', '').replace('(', '').replace(')', '')}"
305
+ get_element_bbox(text_artist, name, ax_id, ax)
306
+ full_name = f"{ax_id}_{name}"
307
+ if full_name in bboxes:
308
+ bboxes[full_name]["element_type"] = "panel_letter"
309
+ bboxes[full_name]["draggable"] = True
310
+ bboxes[full_name]["text"] = text_content
311
+ # Get position in axes coordinates (0-1)
312
+ pos = text_artist.get_position()
313
+ transform = text_artist.get_transform()
314
+ if transform == ax.transAxes:
315
+ bboxes[full_name]["axes_position"] = {"x": pos[0], "y": pos[1]}
316
+
317
+ # Get trace (line) bboxes
318
+ _extract_trace_bboxes_for_axis(
319
+ ax,
320
+ ax_id,
321
+ fig,
322
+ renderer,
323
+ bboxes,
324
+ get_element_bbox,
325
+ tight_x0,
326
+ tight_y0,
327
+ saved_height_inches,
328
+ scale_x,
329
+ scale_y,
330
+ pad_inches,
331
+ )
332
+
333
+ # Get caption bbox (figure-level text)
334
+ # This is outside the per-axis loop since caption is a figure-level element
335
+ for text_artist in fig.texts:
336
+ text_content = text_artist.get_text()
337
+ if text_content:
338
+ pos = text_artist.get_position()
339
+ # Caption is typically centered horizontally (x ~= 0.5) and below figure (y < 0.1)
340
+ if pos[1] < 0.1:
341
+ try:
342
+ bbox = text_artist.get_window_extent(renderer)
343
+ x0_inches = bbox.x0 / fig.dpi
344
+ x1_inches = bbox.x1 / fig.dpi
345
+ y0_inches = bbox.y0 / fig.dpi
346
+ y1_inches = bbox.y1 / fig.dpi
347
+ x0_rel = x0_inches - tight_x0 + pad_inches
348
+ x1_rel = x1_inches - tight_x0 + pad_inches
349
+ y0_rel = saved_height_inches - (y1_inches - tight_y0 + pad_inches)
350
+ y1_rel = saved_height_inches - (y0_inches - tight_y0 + pad_inches)
351
+ bboxes["caption"] = {
352
+ "x0": max(0, int(x0_rel * scale_x)),
353
+ "y0": max(0, int(y0_rel * scale_y)),
354
+ "x1": min(img_width, int(x1_rel * scale_x)),
355
+ "y1": min(img_height, int(y1_rel * scale_y)),
356
+ "label": "Caption",
357
+ }
358
+ except Exception as e:
359
+ print(f"Error getting caption bbox: {e}")
360
+ break # Only one caption expected
361
+
362
+ # Add schema v0.3 metadata if available
363
+ if GEOMETRY_V03_AVAILABLE:
364
+ axes_bboxes = {}
365
+ for ax_id, ax in axes_map.items():
366
+ axes_bboxes[ax_id] = extract_axes_bbox_px(ax, fig)
367
+ bboxes["_meta"] = {
368
+ "schema_version": "0.3.0",
369
+ "axes": axes_bboxes,
370
+ "geometry_available": True,
371
+ }
372
+
373
+ return bboxes
374
+
375
+
376
+ def _extract_trace_bboxes_for_axis(
377
+ ax,
378
+ ax_id,
379
+ fig,
380
+ renderer,
381
+ bboxes,
382
+ get_element_bbox,
383
+ tight_x0,
384
+ tight_y0,
385
+ saved_height_inches,
386
+ scale_x,
387
+ scale_y,
388
+ pad_inches,
389
+ ):
390
+ """Extract bboxes for all data elements in a specific axis.
391
+
392
+ Handles:
393
+ - Lines (plot, errorbar lines)
394
+ - Scatter points (PathCollection)
395
+ - Fill areas (PolyCollection from fill_between)
396
+ - Bars (Rectangle patches)
397
+ """
398
+ import numpy as np
399
+
400
+ def coords_to_img_points(data_coords):
401
+ """Convert data coordinates to image pixel coordinates."""
402
+ if len(data_coords) == 0:
403
+ return []
404
+ transform = ax.transData
405
+ points_display = transform.transform(data_coords)
406
+ points_img = []
407
+ for px, py in points_display:
408
+ # Skip invalid points (NaN, infinity)
409
+ if not np.isfinite(px) or not np.isfinite(py):
410
+ continue
411
+ px_inches = px / fig.dpi
412
+ py_inches = py / fig.dpi
413
+ x_rel = px_inches - tight_x0 + pad_inches
414
+ y_rel = saved_height_inches - (py_inches - tight_y0 + pad_inches)
415
+ # Clamp to reasonable bounds to avoid overflow
416
+ x_img = max(-10000, min(10000, int(x_rel * scale_x)))
417
+ y_img = max(-10000, min(10000, int(y_rel * scale_y)))
418
+ points_img.append([x_img, y_img])
419
+ # Downsample if too many
420
+ if len(points_img) > 100:
421
+ step = len(points_img) // 100
422
+ points_img = points_img[::step]
423
+ return points_img
424
+
425
+ # 1. Extract lines (plot, errorbar lines, etc.)
426
+ line_idx = 0
427
+ for line in ax.get_lines():
428
+ try:
429
+ label = line.get_label()
430
+ # Include unlabeled lines but mark them appropriately
431
+ if label is None or label.startswith("_"):
432
+ label = None # Will use generic name
433
+
434
+ trace_name = f"trace_{line_idx}"
435
+ full_name = f"{ax_id}_{trace_name}"
436
+ get_element_bbox(line, trace_name, ax_id, ax)
437
+
438
+ if full_name in bboxes:
439
+ bboxes[full_name]["label"] = f"{ax_id}: {label or f'Line {line_idx}'}"
440
+ bboxes[full_name]["trace_idx"] = line_idx
441
+ bboxes[full_name]["element_type"] = "line"
442
+
443
+ xdata, ydata = line.get_xdata(), line.get_ydata()
444
+ if len(xdata) > 0:
445
+ bboxes[full_name]["points"] = coords_to_img_points(
446
+ list(zip(xdata, ydata))
447
+ )
448
+
449
+ # Add schema v0.3 geometry_px if available
450
+ if GEOMETRY_V03_AVAILABLE:
451
+ try:
452
+ geom = extract_line_geometry(line, ax, fig)
453
+ bboxes[full_name]["geometry_px"] = geom
454
+ except Exception:
455
+ pass # Fall back to legacy points
456
+
457
+ line_idx += 1
458
+ except Exception as e:
459
+ print(f"Error getting line bbox for {ax_id}: {e}")
460
+
461
+ # 2. Extract collections (scatter, fill_between, etc.)
462
+ coll_idx = 0
463
+ for coll in ax.collections:
464
+ try:
465
+ label = coll.get_label()
466
+ if label is None or label.startswith("_"):
467
+ # Still extract unlabeled collections but with generic name
468
+ label = None
469
+
470
+ coll_type = type(coll).__name__
471
+ if coll_type == "PathCollection":
472
+ # Scatter points
473
+ element_name = f"scatter_{coll_idx}"
474
+ full_name = f"{ax_id}_{element_name}"
475
+ get_element_bbox(coll, element_name, ax_id, ax)
476
+
477
+ if full_name in bboxes:
478
+ bboxes[full_name]["label"] = f"{ax_id}: {label or f'Scatter {coll_idx}'}"
479
+ bboxes[full_name]["element_type"] = "scatter"
480
+
481
+ # Get scatter point positions
482
+ offsets = coll.get_offsets()
483
+ if len(offsets) > 0:
484
+ bboxes[full_name]["points"] = coords_to_img_points(offsets)
485
+
486
+ # Add schema v0.3 geometry_px if available
487
+ if GEOMETRY_V03_AVAILABLE:
488
+ try:
489
+ geom = extract_scatter_geometry(coll, ax, fig)
490
+ bboxes[full_name]["geometry_px"] = geom
491
+ except Exception:
492
+ pass # Fall back to legacy points
493
+
494
+ elif coll_type in ("PolyCollection", "FillBetweenPolyCollection"):
495
+ # Fill areas (fill_between, etc.)
496
+ element_name = f"fill_{coll_idx}"
497
+ full_name = f"{ax_id}_{element_name}"
498
+ get_element_bbox(coll, element_name, ax_id, ax)
499
+
500
+ if full_name in bboxes:
501
+ bboxes[full_name]["label"] = f"{ax_id}: {label or f'Fill {coll_idx}'}"
502
+ bboxes[full_name]["element_type"] = "fill"
503
+
504
+ # Add schema v0.3 geometry_px if available
505
+ if GEOMETRY_V03_AVAILABLE:
506
+ try:
507
+ geom = extract_polygon_geometry(coll, ax, fig)
508
+ bboxes[full_name]["geometry_px"] = geom
509
+ except Exception:
510
+ pass
511
+
512
+ coll_idx += 1
513
+ except Exception as e:
514
+ print(f"Error getting collection bbox for {ax_id}: {e}")
515
+
516
+ # 3. Extract patches (bars, rectangles, etc.)
517
+ patch_idx = 0
518
+ for patch in ax.patches:
519
+ try:
520
+ label = patch.get_label()
521
+ patch_type = type(patch).__name__
522
+
523
+ if patch_type == "Rectangle":
524
+ # Bar chart bars
525
+ element_name = f"bar_{patch_idx}"
526
+ full_name = f"{ax_id}_{element_name}"
527
+ get_element_bbox(patch, element_name, ax_id, ax)
528
+
529
+ if full_name in bboxes:
530
+ bboxes[full_name]["label"] = f"{ax_id}: {label or f'Bar {patch_idx}'}"
531
+ bboxes[full_name]["element_type"] = "bar"
532
+
533
+ patch_idx += 1
534
+ except Exception as e:
535
+ print(f"Error getting patch bbox for {ax_id}: {e}")
536
+
537
+
538
+ def _extract_axis_bboxes_for_axis(ax, ax_id, renderer, bboxes, bbox_to_img_coords, Bbox):
539
+ """Extract X and Y axis bboxes for a specific axis (multi-axis version)."""
540
+ try:
541
+ # X-axis: combine spine and tick labels into one bbox
542
+ x_axis_bboxes = []
543
+ for ticklabel in ax.xaxis.get_ticklabels():
544
+ if ticklabel.get_visible():
545
+ try:
546
+ tb = ticklabel.get_window_extent(renderer)
547
+ if tb.width > 0:
548
+ x_axis_bboxes.append(tb)
549
+ except Exception:
550
+ pass
551
+ for tick in ax.xaxis.get_major_ticks():
552
+ if tick.tick1line.get_visible():
553
+ try:
554
+ tb = tick.tick1line.get_window_extent(renderer)
555
+ if tb.width > 0 or tb.height > 0:
556
+ x_axis_bboxes.append(tb)
557
+ except Exception:
558
+ pass
559
+ spine_bbox = ax.spines["bottom"].get_window_extent(renderer)
560
+ if spine_bbox.width > 0:
561
+ if x_axis_bboxes:
562
+ tick_union = Bbox.union(x_axis_bboxes)
563
+ constrained_spine = Bbox.from_extents(
564
+ tick_union.x0, spine_bbox.y0, tick_union.x1, spine_bbox.y1
565
+ )
566
+ x_axis_bboxes.append(constrained_spine)
567
+ else:
568
+ x_axis_bboxes.append(spine_bbox)
569
+ if x_axis_bboxes:
570
+ combined = Bbox.union(x_axis_bboxes)
571
+ bboxes[f"{ax_id}_xaxis"] = bbox_to_img_coords(combined)
572
+ bboxes[f"{ax_id}_xaxis"]["label"] = f"{ax_id}: X Axis"
573
+ bboxes[f"{ax_id}_xaxis"]["ax_id"] = ax_id
574
+ bboxes[f"{ax_id}_xaxis"]["element_type"] = "xaxis"
575
+
576
+ # Y-axis: combine spine and tick labels into one bbox
577
+ y_axis_bboxes = []
578
+ for ticklabel in ax.yaxis.get_ticklabels():
579
+ if ticklabel.get_visible():
580
+ try:
581
+ tb = ticklabel.get_window_extent(renderer)
582
+ if tb.width > 0:
583
+ y_axis_bboxes.append(tb)
584
+ except Exception:
585
+ pass
586
+ for tick in ax.yaxis.get_major_ticks():
587
+ if tick.tick1line.get_visible():
588
+ try:
589
+ tb = tick.tick1line.get_window_extent(renderer)
590
+ if tb.width > 0 or tb.height > 0:
591
+ y_axis_bboxes.append(tb)
592
+ except Exception:
593
+ pass
594
+ spine_bbox = ax.spines["left"].get_window_extent(renderer)
595
+ if spine_bbox.height > 0:
596
+ if y_axis_bboxes:
597
+ tick_union = Bbox.union(y_axis_bboxes)
598
+ constrained_spine = Bbox.from_extents(
599
+ spine_bbox.x0, tick_union.y0, spine_bbox.x1, tick_union.y1
600
+ )
601
+ y_axis_bboxes.append(constrained_spine)
602
+ else:
603
+ y_axis_bboxes.append(spine_bbox)
604
+ if y_axis_bboxes:
605
+ combined = Bbox.union(y_axis_bboxes)
606
+ padded = Bbox.from_extents(
607
+ combined.x0 - 10, combined.y0 - 5, combined.x1 + 5, combined.y1 + 5
608
+ )
609
+ bboxes[f"{ax_id}_yaxis"] = bbox_to_img_coords(padded)
610
+ bboxes[f"{ax_id}_yaxis"]["label"] = f"{ax_id}: Y Axis"
611
+ bboxes[f"{ax_id}_yaxis"]["ax_id"] = ax_id
612
+ bboxes[f"{ax_id}_yaxis"]["element_type"] = "yaxis"
613
+
614
+ except Exception as e:
615
+ print(f"Error getting axis bboxes for {ax_id}: {e}")
616
+
617
+
618
+ def _extract_axis_bboxes(ax, renderer, bboxes, bbox_to_img_coords, Bbox, ax_prefix=""):
619
+ """Extract bboxes for X and Y axis elements.
620
+
621
+ Args:
622
+ ax: Matplotlib axis.
623
+ renderer: Figure renderer.
624
+ bboxes: Dict to store bboxes.
625
+ bbox_to_img_coords: Coordinate conversion function.
626
+ Bbox: Matplotlib Bbox class.
627
+ ax_prefix: Prefix for bbox names (e.g., "ax_00_").
628
+ """
629
+ try:
630
+ # X-axis: combine spine and tick labels into one bbox
631
+ x_axis_bboxes = []
632
+ for ticklabel in ax.xaxis.get_ticklabels():
633
+ if ticklabel.get_visible():
634
+ try:
635
+ tb = ticklabel.get_window_extent(renderer)
636
+ if tb.width > 0:
637
+ x_axis_bboxes.append(tb)
638
+ except Exception:
639
+ pass
640
+ for tick in ax.xaxis.get_major_ticks():
641
+ if tick.tick1line.get_visible():
642
+ try:
643
+ tb = tick.tick1line.get_window_extent(renderer)
644
+ if tb.width > 0 or tb.height > 0:
645
+ x_axis_bboxes.append(tb)
646
+ except Exception:
647
+ pass
648
+ spine_bbox = ax.spines["bottom"].get_window_extent(renderer)
649
+ if spine_bbox.width > 0:
650
+ if x_axis_bboxes:
651
+ tick_union = Bbox.union(x_axis_bboxes)
652
+ constrained_spine = Bbox.from_extents(
653
+ tick_union.x0, spine_bbox.y0, tick_union.x1, spine_bbox.y1
654
+ )
655
+ x_axis_bboxes.append(constrained_spine)
656
+ else:
657
+ x_axis_bboxes.append(spine_bbox)
658
+ if x_axis_bboxes:
659
+ combined = Bbox.union(x_axis_bboxes)
660
+ bboxes[f"{ax_prefix}xaxis_spine"] = bbox_to_img_coords(combined)
661
+ bboxes[f"{ax_prefix}xaxis_spine"]["label"] = "X Spine & Ticks"
662
+
663
+ # Y-axis: combine spine and tick labels into one bbox
664
+ y_axis_bboxes = []
665
+ for ticklabel in ax.yaxis.get_ticklabels():
666
+ if ticklabel.get_visible():
667
+ try:
668
+ tb = ticklabel.get_window_extent(renderer)
669
+ if tb.width > 0:
670
+ y_axis_bboxes.append(tb)
671
+ except Exception:
672
+ pass
673
+ for tick in ax.yaxis.get_major_ticks():
674
+ if tick.tick1line.get_visible():
675
+ try:
676
+ tb = tick.tick1line.get_window_extent(renderer)
677
+ if tb.width > 0 or tb.height > 0:
678
+ y_axis_bboxes.append(tb)
679
+ except Exception:
680
+ pass
681
+ spine_bbox = ax.spines["left"].get_window_extent(renderer)
682
+ if spine_bbox.height > 0:
683
+ if y_axis_bboxes:
684
+ tick_union = Bbox.union(y_axis_bboxes)
685
+ constrained_spine = Bbox.from_extents(
686
+ spine_bbox.x0, tick_union.y0, spine_bbox.x1, tick_union.y1
687
+ )
688
+ y_axis_bboxes.append(constrained_spine)
689
+ else:
690
+ y_axis_bboxes.append(spine_bbox)
691
+ if y_axis_bboxes:
692
+ combined = Bbox.union(y_axis_bboxes)
693
+ padded = Bbox.from_extents(
694
+ combined.x0 - 10, combined.y0 - 5, combined.x1 + 5, combined.y1 + 5
695
+ )
696
+ bboxes[f"{ax_prefix}yaxis_spine"] = bbox_to_img_coords(padded)
697
+ bboxes[f"{ax_prefix}yaxis_spine"]["label"] = "Y Spine & Ticks"
698
+
699
+ except Exception as e:
700
+ print(f"Error getting axis bboxes: {e}")
701
+
702
+
703
+ def _extract_trace_bboxes(
704
+ ax,
705
+ fig,
706
+ renderer,
707
+ bboxes,
708
+ get_element_bbox,
709
+ tight_x0,
710
+ tight_y0,
711
+ saved_height_inches,
712
+ scale_x,
713
+ scale_y,
714
+ pad_inches,
715
+ ):
716
+ """Extract bboxes for all data elements (lines, scatter, fill) with proximity detection."""
717
+ import numpy as np
718
+
719
+ def coords_to_img_points(data_coords):
720
+ """Convert data coordinates to image pixel coordinates."""
721
+ if len(data_coords) == 0:
722
+ return []
723
+ transform = ax.transData
724
+ points_display = transform.transform(data_coords)
725
+ points_img = []
726
+ for px, py in points_display:
727
+ if not np.isfinite(px) or not np.isfinite(py):
728
+ continue
729
+ px_inches = px / fig.dpi
730
+ py_inches = py / fig.dpi
731
+ x_rel = px_inches - tight_x0 + pad_inches
732
+ y_rel = saved_height_inches - (py_inches - tight_y0 + pad_inches)
733
+ x_img = max(-10000, min(10000, int(x_rel * scale_x)))
734
+ y_img = max(-10000, min(10000, int(y_rel * scale_y)))
735
+ points_img.append([x_img, y_img])
736
+ if len(points_img) > 100:
737
+ step = len(points_img) // 100
738
+ points_img = points_img[::step]
739
+ return points_img
740
+
741
+ # 1. Extract lines
742
+ for idx, line in enumerate(ax.get_lines()):
743
+ try:
744
+ label = line.get_label()
745
+ # Include unlabeled lines but mark them appropriately
746
+ if label is None or label.startswith("_"):
747
+ label = None # Will use generic name
748
+ get_element_bbox(line, f"trace_{idx}")
749
+ if f"trace_{idx}" in bboxes:
750
+ bboxes[f"trace_{idx}"]["label"] = label or f"Trace {idx}"
751
+ bboxes[f"trace_{idx}"]["trace_idx"] = idx
752
+ bboxes[f"trace_{idx}"]["element_type"] = "line"
753
+
754
+ xdata, ydata = line.get_xdata(), line.get_ydata()
755
+ if len(xdata) > 0:
756
+ bboxes[f"trace_{idx}"]["points"] = coords_to_img_points(
757
+ list(zip(xdata, ydata))
758
+ )
759
+
760
+ # Add schema v0.3 geometry_px if available
761
+ if GEOMETRY_V03_AVAILABLE:
762
+ try:
763
+ geom = extract_line_geometry(line, ax, fig)
764
+ bboxes[f"trace_{idx}"]["geometry_px"] = geom
765
+ except Exception:
766
+ pass
767
+ except Exception as e:
768
+ print(f"Error getting trace bbox: {e}")
769
+
770
+ # 2. Extract collections (scatter, fill_between)
771
+ coll_idx = 0
772
+ for coll in ax.collections:
773
+ try:
774
+ label = coll.get_label()
775
+ if label is None or label.startswith("_"):
776
+ label = None
777
+
778
+ coll_type = type(coll).__name__
779
+ if coll_type == "PathCollection":
780
+ # Scatter points
781
+ elem_key = f"scatter_{coll_idx}"
782
+ get_element_bbox(coll, elem_key)
783
+
784
+ # Initialize entry if bbox extraction failed but we have data
785
+ offsets = coll.get_offsets()
786
+ if elem_key not in bboxes and len(offsets) > 0:
787
+ # Create bbox from data coordinates as fallback
788
+ points_img = coords_to_img_points(offsets)
789
+ if points_img:
790
+ xs = [p[0] for p in points_img]
791
+ ys = [p[1] for p in points_img]
792
+ bboxes[elem_key] = {
793
+ "x0": min(xs) - 10,
794
+ "y0": min(ys) - 10,
795
+ "x1": max(xs) + 10,
796
+ "y1": max(ys) + 10,
797
+ }
798
+
799
+ if elem_key in bboxes:
800
+ bboxes[elem_key]["label"] = label or f"Scatter {coll_idx}"
801
+ bboxes[elem_key]["element_type"] = "scatter"
802
+
803
+ if len(offsets) > 0:
804
+ bboxes[elem_key]["points"] = coords_to_img_points(offsets)
805
+
806
+ # Add schema v0.3 geometry_px if available
807
+ if GEOMETRY_V03_AVAILABLE:
808
+ try:
809
+ geom = extract_scatter_geometry(coll, ax, fig)
810
+ bboxes[elem_key]["geometry_px"] = geom
811
+ except Exception:
812
+ pass
813
+
814
+ elif coll_type in ("PolyCollection", "FillBetweenPolyCollection"):
815
+ # Fill areas
816
+ get_element_bbox(coll, f"fill_{coll_idx}")
817
+ if f"fill_{coll_idx}" in bboxes:
818
+ bboxes[f"fill_{coll_idx}"]["label"] = label or f"Fill {coll_idx}"
819
+ bboxes[f"fill_{coll_idx}"]["element_type"] = "fill"
820
+
821
+ # Add schema v0.3 geometry_px if available
822
+ if GEOMETRY_V03_AVAILABLE:
823
+ try:
824
+ geom = extract_polygon_geometry(coll, ax, fig)
825
+ bboxes[f"fill_{coll_idx}"]["geometry_px"] = geom
826
+ except Exception:
827
+ pass
828
+
829
+ coll_idx += 1
830
+ except Exception as e:
831
+ print(f"Error getting collection bbox: {e}")
832
+
833
+
834
+ def extract_bboxes_from_metadata(
835
+ metadata: Dict[str, Any],
836
+ img_width: int,
837
+ img_height: int,
838
+ ) -> Dict[str, Any]:
839
+ """Extract bounding boxes from pre-computed metadata (without re-rendering).
840
+
841
+ This is used when loading actual PNGs from bundles instead of re-rendering.
842
+ Extracts bbox info from:
843
+ - hit_regions (if available from v0.3 schema)
844
+ - elements dict
845
+ - axes positions
846
+
847
+ Args:
848
+ metadata: JSON metadata from spec.json or panel JSON
849
+ img_width: Image width in pixels
850
+ img_height: Image height in pixels
851
+
852
+ Returns:
853
+ Dict with bboxes keyed by element name
854
+ """
855
+ bboxes = {}
856
+
857
+ # Check for pre-computed hit_regions (v0.3 schema)
858
+ hit_regions = metadata.get("hit_regions", {})
859
+ if hit_regions:
860
+ color_map = hit_regions.get("color_map", {})
861
+ for element_name, color in color_map.items():
862
+ # We don't have exact coords from color map, but we can create placeholder
863
+ bboxes[element_name] = {
864
+ "label": element_name.replace("_", " ").title(),
865
+ "element_type": _guess_element_type(element_name),
866
+ }
867
+
868
+ # Check for geometry_px in cache (v0.3 layered bundle)
869
+ geometry_px = metadata.get("geometry_px", {})
870
+ if geometry_px:
871
+ for element_name, geom in geometry_px.items():
872
+ if isinstance(geom, dict) and "bbox" in geom:
873
+ bbox = geom["bbox"]
874
+ bboxes[element_name] = {
875
+ "x0": bbox.get("x0", 0),
876
+ "y0": bbox.get("y0", 0),
877
+ "x1": bbox.get("x1", img_width),
878
+ "y1": bbox.get("y1", img_height),
879
+ "label": element_name.replace("_", " ").title(),
880
+ "element_type": _guess_element_type(element_name),
881
+ }
882
+ if "points" in geom:
883
+ bboxes[element_name]["points"] = geom["points"]
884
+
885
+ # Extract from elements dict if present
886
+ elements = metadata.get("elements", {})
887
+ if not isinstance(elements, dict):
888
+ elements = {}
889
+ for element_name, element_info in elements.items():
890
+ if not isinstance(element_info, dict):
891
+ continue
892
+ if element_name not in bboxes:
893
+ bboxes[element_name] = {
894
+ "label": element_info.get("label", element_name.replace("_", " ").title()),
895
+ "element_type": element_info.get("type", _guess_element_type(element_name)),
896
+ }
897
+
898
+ # Extract from axes (handle both dict and list formats)
899
+ axes = metadata.get("axes", [])
900
+ if isinstance(axes, list):
901
+ axes_list = axes
902
+ elif isinstance(axes, dict):
903
+ axes_list = list(axes.values())
904
+ else:
905
+ axes_list = []
906
+
907
+ for i, ax_spec in enumerate(axes_list):
908
+ if not isinstance(ax_spec, dict):
909
+ continue
910
+
911
+ ax_id = ax_spec.get("id", f"ax{i}")
912
+
913
+ # Panel bbox - check for "bbox" field (new format) or "position" (old format)
914
+ bbox_spec = ax_spec.get("bbox", {})
915
+ pos = ax_spec.get("position", [])
916
+
917
+ if bbox_spec and isinstance(bbox_spec, dict):
918
+ # New format: bbox with x0, y0, width, height in panel fraction
919
+ x0_frac = bbox_spec.get("x0", 0)
920
+ y0_frac = bbox_spec.get("y0", 0)
921
+ w_frac = bbox_spec.get("width", 1)
922
+ h_frac = bbox_spec.get("height", 1)
923
+ x0 = int(x0_frac * img_width)
924
+ y0 = int(y0_frac * img_height)
925
+ x1 = int((x0_frac + w_frac) * img_width)
926
+ y1 = int((y0_frac + h_frac) * img_height)
927
+ bboxes[f"{ax_id}_panel"] = {
928
+ "x0": x0,
929
+ "y0": y0,
930
+ "x1": x1,
931
+ "y1": y1,
932
+ "label": f"Panel {ax_id}",
933
+ "ax_id": ax_id,
934
+ "is_panel": True,
935
+ }
936
+ elif len(pos) >= 4:
937
+ # Old format: position is in figure fraction [x0, y0, width, height]
938
+ x0 = int(pos[0] * img_width)
939
+ y0 = int((1 - pos[1] - pos[3]) * img_height) # Flip Y
940
+ x1 = int((pos[0] + pos[2]) * img_width)
941
+ y1 = int((1 - pos[1]) * img_height) # Flip Y
942
+ bboxes[f"{ax_id}_panel"] = {
943
+ "x0": x0,
944
+ "y0": y0,
945
+ "x1": x1,
946
+ "y1": y1,
947
+ "label": f"Panel {ax_id}",
948
+ "ax_id": ax_id,
949
+ "is_panel": True,
950
+ }
951
+
952
+ # Title/labels from labels dict (new format) or xaxis/yaxis (old format)
953
+ labels = ax_spec.get("labels", {})
954
+ xaxis = ax_spec.get("xaxis", {})
955
+ yaxis = ax_spec.get("yaxis", {})
956
+
957
+ xlabel = labels.get("xlabel") or (xaxis.get("label") if isinstance(xaxis, dict) else None)
958
+ ylabel = labels.get("ylabel") or (yaxis.get("label") if isinstance(yaxis, dict) else None)
959
+ title = labels.get("title")
960
+
961
+ if xlabel:
962
+ bboxes[f"{ax_id}_xlabel"] = {
963
+ "label": f"{ax_id}: {xlabel}",
964
+ "element_type": "xlabel",
965
+ "ax_id": ax_id,
966
+ }
967
+ if ylabel:
968
+ bboxes[f"{ax_id}_ylabel"] = {
969
+ "label": f"{ax_id}: {ylabel}",
970
+ "element_type": "ylabel",
971
+ "ax_id": ax_id,
972
+ }
973
+ if title:
974
+ bboxes[f"{ax_id}_title"] = {
975
+ "label": title,
976
+ "element_type": "title",
977
+ "ax_id": ax_id,
978
+ }
979
+
980
+ # Extract from traces array (pltz spec format)
981
+ traces = metadata.get("traces", [])
982
+ if isinstance(traces, list):
983
+ for i, trace in enumerate(traces):
984
+ if not isinstance(trace, dict):
985
+ continue
986
+ trace_id = trace.get("id", f"trace_{i}")
987
+ trace_type = trace.get("type", "line")
988
+ trace_label = trace.get("label", f"Trace {i}")
989
+ ax_idx = trace.get("axes_index", 0)
990
+
991
+ # Use axes bbox as fallback for trace bbox
992
+ ax_panel_key = None
993
+ for key in bboxes:
994
+ if key.endswith("_panel") and bboxes[key].get("ax_id", "").endswith(str(ax_idx)):
995
+ ax_panel_key = key
996
+ break
997
+ if not ax_panel_key:
998
+ # Find any panel bbox
999
+ for key in bboxes:
1000
+ if key.endswith("_panel"):
1001
+ ax_panel_key = key
1002
+ break
1003
+
1004
+ trace_bbox = {
1005
+ "label": trace_label,
1006
+ "element_type": trace_type,
1007
+ "trace_idx": i,
1008
+ "ax_id": f"ax{ax_idx}",
1009
+ }
1010
+
1011
+ # Copy panel bbox coordinates if available
1012
+ if ax_panel_key and ax_panel_key in bboxes:
1013
+ panel = bboxes[ax_panel_key]
1014
+ trace_bbox["x0"] = panel.get("x0", 0)
1015
+ trace_bbox["y0"] = panel.get("y0", 0)
1016
+ trace_bbox["x1"] = panel.get("x1", img_width)
1017
+ trace_bbox["y1"] = panel.get("y1", img_height)
1018
+
1019
+ bboxes[f"trace_{i}"] = trace_bbox
1020
+
1021
+ # If no bboxes found, return minimal set
1022
+ if not bboxes:
1023
+ bboxes["panel"] = {
1024
+ "x0": 0,
1025
+ "y0": 0,
1026
+ "x1": img_width,
1027
+ "y1": img_height,
1028
+ "label": "Panel",
1029
+ "is_panel": True,
1030
+ }
1031
+
1032
+ return bboxes
1033
+
1034
+
1035
+ def _guess_element_type(name: str) -> str:
1036
+ """Guess element type from element name."""
1037
+ name_lower = name.lower()
1038
+ if "line" in name_lower or "trace" in name_lower:
1039
+ return "line"
1040
+ elif "scatter" in name_lower:
1041
+ return "scatter"
1042
+ elif "bar" in name_lower:
1043
+ return "bar"
1044
+ elif "fill" in name_lower:
1045
+ return "fill"
1046
+ elif "xlabel" in name_lower:
1047
+ return "xlabel"
1048
+ elif "ylabel" in name_lower:
1049
+ return "ylabel"
1050
+ elif "title" in name_lower:
1051
+ return "title"
1052
+ elif "legend" in name_lower:
1053
+ return "legend"
1054
+ elif "xaxis" in name_lower:
1055
+ return "xaxis"
1056
+ elif "yaxis" in name_lower:
1057
+ return "yaxis"
1058
+ elif "panel" in name_lower:
1059
+ return "panel"
1060
+ return "unknown"
1061
+
1062
+
1063
+ def extract_bboxes_from_geometry_px(
1064
+ geometry_data: Dict[str, Any],
1065
+ img_width: int,
1066
+ img_height: int,
1067
+ ) -> Dict[str, Any]:
1068
+ """Extract bounding boxes from geometry_px.json (cached pixel coordinates).
1069
+
1070
+ This provides precise pixel coordinates for interactive element selection.
1071
+
1072
+ Args:
1073
+ geometry_data: JSON data from geometry_px.json
1074
+ img_width: Actual image width in pixels
1075
+ img_height: Actual image height in pixels
1076
+
1077
+ Returns:
1078
+ Dict with bboxes keyed by element name
1079
+ """
1080
+ bboxes = {}
1081
+
1082
+ # Get figure dimensions from geometry to calculate scale
1083
+ figure_px = geometry_data.get("figure_px", [img_width, img_height])
1084
+ if isinstance(figure_px, list) and len(figure_px) >= 2:
1085
+ geom_width, geom_height = figure_px[0], figure_px[1]
1086
+ else:
1087
+ geom_width, geom_height = img_width, img_height
1088
+
1089
+ # Scale factor if image size differs from geometry
1090
+ scale_x = img_width / geom_width if geom_width > 0 else 1
1091
+ scale_y = img_height / geom_height if geom_height > 0 else 1
1092
+
1093
+ # Extract axes bboxes
1094
+ axes = geometry_data.get("axes", [])
1095
+ for i, ax in enumerate(axes):
1096
+ if not isinstance(ax, dict):
1097
+ continue
1098
+ ax_id = ax.get("id", f"ax{i}")
1099
+ bbox_px = ax.get("bbox_px", {})
1100
+ if bbox_px:
1101
+ x0 = float(bbox_px.get("x0", 0)) * scale_x
1102
+ y0 = float(bbox_px.get("y0", 0)) * scale_y
1103
+ w = float(bbox_px.get("width", 0)) * scale_x
1104
+ h = float(bbox_px.get("height", 0)) * scale_y
1105
+ bboxes[f"{ax_id}_panel"] = {
1106
+ "x0": int(x0),
1107
+ "y0": int(y0),
1108
+ "x1": int(x0 + w),
1109
+ "y1": int(y0 + h),
1110
+ "label": f"Axes {ax_id}",
1111
+ "ax_id": ax_id,
1112
+ "is_panel": True,
1113
+ }
1114
+
1115
+ # Helper to safely convert to int (handle inf/nan)
1116
+ def safe_int(val, default=0, max_val=10000):
1117
+ import math
1118
+ if val is None or math.isinf(val) or math.isnan(val):
1119
+ return default
1120
+ return max(0, min(int(val), max_val))
1121
+
1122
+ # Extract artists (lines, scatter, bars, etc.)
1123
+ artists = geometry_data.get("artists", [])
1124
+ for i, artist in enumerate(artists):
1125
+ if not isinstance(artist, dict):
1126
+ continue
1127
+
1128
+ artist_id = artist.get("id", str(i))
1129
+ artist_type = artist.get("type", "unknown")
1130
+ artist_label = artist.get("label") or f"{artist_type}_{i}"
1131
+ axes_index = artist.get("axes_index", 0)
1132
+
1133
+ # Get bbox_px
1134
+ bbox_px = artist.get("bbox_px", {})
1135
+ if bbox_px:
1136
+ x0 = float(bbox_px.get("x0", 0)) * scale_x
1137
+ y0 = float(bbox_px.get("y0", 0)) * scale_y
1138
+ w = float(bbox_px.get("width", 0)) * scale_x
1139
+ h = float(bbox_px.get("height", 0)) * scale_y
1140
+
1141
+ artist_bbox = {
1142
+ "x0": safe_int(x0, 0, img_width),
1143
+ "y0": safe_int(y0, 0, img_height),
1144
+ "x1": safe_int(x0 + w, img_width, img_width),
1145
+ "y1": safe_int(y0 + h, img_height, img_height),
1146
+ "label": artist_label,
1147
+ "element_type": artist_type,
1148
+ "trace_idx": i,
1149
+ "ax_id": f"ax{axes_index}",
1150
+ }
1151
+
1152
+ # Get path_px for lines (for precise hover detection)
1153
+ path_px = artist.get("path_px", [])
1154
+ if path_px and len(path_px) > 0:
1155
+ import math
1156
+ # Scale points to actual image coordinates, filter out inf/nan
1157
+ scaled_points = []
1158
+ for pt in path_px:
1159
+ if isinstance(pt, (list, tuple)) and len(pt) >= 2:
1160
+ px, py = pt[0] * scale_x, pt[1] * scale_y
1161
+ if not (math.isinf(px) or math.isinf(py) or math.isnan(px) or math.isnan(py)):
1162
+ scaled_points.append([px, py])
1163
+ if scaled_points:
1164
+ artist_bbox["points"] = scaled_points
1165
+
1166
+ # Get scatter points if available
1167
+ scatter_px = artist.get("scatter_px", [])
1168
+ if scatter_px and len(scatter_px) > 0:
1169
+ import math
1170
+ scaled_points = []
1171
+ for pt in scatter_px:
1172
+ if isinstance(pt, (list, tuple)) and len(pt) >= 2:
1173
+ px, py = pt[0] * scale_x, pt[1] * scale_y
1174
+ if not (math.isinf(px) or math.isinf(py) or math.isnan(px) or math.isnan(py)):
1175
+ scaled_points.append([px, py])
1176
+ if scaled_points:
1177
+ artist_bbox["points"] = scaled_points
1178
+ artist_bbox["element_type"] = "scatter"
1179
+
1180
+ bboxes[f"trace_{i}"] = artist_bbox
1181
+
1182
+ # Extract from selectable_regions (title, xlabel, ylabel, xaxis, yaxis)
1183
+ selectable = geometry_data.get("selectable_regions", {})
1184
+ sel_axes = selectable.get("axes", [])
1185
+ for ax_data in sel_axes:
1186
+ if not isinstance(ax_data, dict):
1187
+ continue
1188
+ ax_idx = ax_data.get("index", 0)
1189
+ ax_id = f"ax{ax_idx}"
1190
+
1191
+ # Title
1192
+ title_data = ax_data.get("title", {})
1193
+ if title_data and "bbox_px" in title_data:
1194
+ bbox = title_data["bbox_px"]
1195
+ if isinstance(bbox, list) and len(bbox) >= 4:
1196
+ bboxes[f"{ax_id}_title"] = {
1197
+ "x0": safe_int(bbox[0] * scale_x, 0, img_width),
1198
+ "y0": safe_int(bbox[1] * scale_y, 0, img_height),
1199
+ "x1": safe_int(bbox[2] * scale_x, img_width, img_width),
1200
+ "y1": safe_int(bbox[3] * scale_y, img_height, img_height),
1201
+ "label": title_data.get("text", "Title"),
1202
+ "element_type": "title",
1203
+ "ax_id": ax_id,
1204
+ }
1205
+
1206
+ # X Label
1207
+ xlabel_data = ax_data.get("xlabel", {})
1208
+ if xlabel_data and "bbox_px" in xlabel_data:
1209
+ bbox = xlabel_data["bbox_px"]
1210
+ if isinstance(bbox, list) and len(bbox) >= 4:
1211
+ bboxes[f"{ax_id}_xlabel"] = {
1212
+ "x0": safe_int(bbox[0] * scale_x, 0, img_width),
1213
+ "y0": safe_int(bbox[1] * scale_y, 0, img_height),
1214
+ "x1": safe_int(bbox[2] * scale_x, img_width, img_width),
1215
+ "y1": safe_int(bbox[3] * scale_y, img_height, img_height),
1216
+ "label": xlabel_data.get("text", "X Label"),
1217
+ "element_type": "xlabel",
1218
+ "ax_id": ax_id,
1219
+ }
1220
+
1221
+ # Y Label
1222
+ ylabel_data = ax_data.get("ylabel", {})
1223
+ if ylabel_data and "bbox_px" in ylabel_data:
1224
+ bbox = ylabel_data["bbox_px"]
1225
+ if isinstance(bbox, list) and len(bbox) >= 4:
1226
+ bboxes[f"{ax_id}_ylabel"] = {
1227
+ "x0": safe_int(bbox[0] * scale_x, 0, img_width),
1228
+ "y0": safe_int(bbox[1] * scale_y, 0, img_height),
1229
+ "x1": safe_int(bbox[2] * scale_x, img_width, img_width),
1230
+ "y1": safe_int(bbox[3] * scale_y, img_height, img_height),
1231
+ "label": ylabel_data.get("text", "Y Label"),
1232
+ "element_type": "ylabel",
1233
+ "ax_id": ax_id,
1234
+ }
1235
+
1236
+ # X Axis spine
1237
+ xaxis_data = ax_data.get("xaxis", {})
1238
+ if xaxis_data:
1239
+ spine = xaxis_data.get("spine", {})
1240
+ if spine and "bbox_px" in spine:
1241
+ bbox = spine["bbox_px"]
1242
+ if isinstance(bbox, list) and len(bbox) >= 4:
1243
+ bboxes[f"{ax_id}_xaxis"] = {
1244
+ "x0": safe_int(bbox[0] * scale_x, 0, img_width),
1245
+ "y0": safe_int(bbox[1] * scale_y, 0, img_height),
1246
+ "x1": safe_int(bbox[2] * scale_x, img_width, img_width),
1247
+ "y1": safe_int(bbox[3] * scale_y, img_height, img_height),
1248
+ "label": "X Axis",
1249
+ "element_type": "xaxis",
1250
+ "ax_id": ax_id,
1251
+ }
1252
+
1253
+ # Y Axis spine
1254
+ yaxis_data = ax_data.get("yaxis", {})
1255
+ if yaxis_data:
1256
+ spine = yaxis_data.get("spine", {})
1257
+ if spine and "bbox_px" in spine:
1258
+ bbox = spine["bbox_px"]
1259
+ if isinstance(bbox, list) and len(bbox) >= 4:
1260
+ bboxes[f"{ax_id}_yaxis"] = {
1261
+ "x0": safe_int(bbox[0] * scale_x, 0, img_width),
1262
+ "y0": safe_int(bbox[1] * scale_y, 0, img_height),
1263
+ "x1": safe_int(bbox[2] * scale_x, img_width, img_width),
1264
+ "y1": safe_int(bbox[3] * scale_y, img_height, img_height),
1265
+ "label": "Y Axis",
1266
+ "element_type": "yaxis",
1267
+ "ax_id": ax_id,
1268
+ }
1269
+
1270
+ # Legend
1271
+ legend_data = ax_data.get("legend", {})
1272
+ if legend_data and "bbox_px" in legend_data:
1273
+ bbox = legend_data["bbox_px"]
1274
+ if isinstance(bbox, list) and len(bbox) >= 4:
1275
+ bboxes[f"{ax_id}_legend"] = {
1276
+ "x0": safe_int(bbox[0] * scale_x, 0, img_width),
1277
+ "y0": safe_int(bbox[1] * scale_y, 0, img_height),
1278
+ "x1": safe_int(bbox[2] * scale_x, img_width, img_width),
1279
+ "y1": safe_int(bbox[3] * scale_y, img_height, img_height),
1280
+ "label": "Legend",
1281
+ "element_type": "legend",
1282
+ "ax_id": ax_id,
1283
+ }
1284
+
1285
+ # If no bboxes found, return minimal set
1286
+ if not bboxes:
1287
+ bboxes["panel"] = {
1288
+ "x0": 0,
1289
+ "y0": 0,
1290
+ "x1": img_width,
1291
+ "y1": img_height,
1292
+ "label": "Panel",
1293
+ "is_panel": True,
1294
+ }
1295
+
1296
+ return bboxes
1297
+
1298
+
1299
+ # EOF