scitex 2.10.3__py3-none-any.whl → 2.11.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.
- scitex/__init__.py +1 -4
- scitex/__version__.py +1 -1
- scitex/_install_guide.py +14 -2
- scitex/bridge/_figrecipe.py +1 -1
- scitex/bridge/_helpers.py +1 -1
- scitex/bridge/_plt_vis.py +1 -1
- scitex/bridge/_stats_plt.py +1 -1
- scitex/bridge/_stats_vis.py +2 -2
- scitex/{fig → canvas}/__init__.py +84 -96
- scitex/{fig → canvas}/backend/_parser.py +1 -1
- scitex/{fig → canvas}/canvas.py +13 -14
- scitex/{fts/_fig/_editor → canvas/editor}/_defaults.py +2 -2
- scitex/{fig → canvas}/editor/edit/__init__.py +11 -14
- scitex/{fig → canvas}/editor/edit/bundle_resolver.py +56 -48
- scitex/{fig → canvas}/editor/edit/editor_launcher.py +79 -26
- scitex/{fts/_fig/_editor/_cui/_panel_loader.py → canvas/editor/edit/panel_loader.py} +8 -8
- scitex/{fts/_fig/_editor/_gui/_flask_editor → canvas/editor/flask_editor}/_bbox.py +2 -1
- scitex/{fts/_fig/_editor/_gui/_flask_editor → canvas/editor/flask_editor}/_core.py +84 -84
- scitex/{fts/_fig/_editor/_gui/_flask_editor → canvas/editor/flask_editor}/_renderer.py +7 -6
- scitex/{fts/_fig/_editor/_gui/_flask_editor → canvas/editor/flask_editor}/static/css/features/canvas.css +2 -2
- scitex/{fig → canvas}/editor/flask_editor/static/css/features/panel-grid.css +1 -1
- scitex/{fig → canvas}/editor/flask_editor/static/js/core/api.js +3 -4
- scitex/{fig → canvas}/editor/flask_editor/static/js/editor/preview.js +5 -5
- scitex/{fig → canvas}/editor/flask_editor/templates/_html.py +3 -3
- scitex/{fig → canvas}/editor/flask_editor/templates/_scripts.py +10 -10
- scitex/{fig → canvas}/editor/flask_editor/templates/_styles.py +3 -3
- scitex/{fig → canvas}/io/__init__.py +32 -38
- scitex/{fig → canvas}/io/_bundle.py +217 -154
- scitex/{fig → canvas}/io/_canvas.py +1 -1
- scitex/{fig → canvas}/io/_data.py +1 -1
- scitex/{fig → canvas}/io/_export.py +1 -1
- scitex/{fig → canvas}/io/_load.py +1 -1
- scitex/{fig → canvas}/io/_panel.py +1 -1
- scitex/{fig → canvas}/io/_save.py +1 -1
- scitex/{fig → canvas}/model/__init__.py +1 -1
- scitex/{fig → canvas}/model/_annotations.py +1 -1
- scitex/{fig → canvas}/model/_axes.py +1 -1
- scitex/{fig → canvas}/model/_figure.py +1 -1
- scitex/{fig → canvas}/model/_guides.py +1 -1
- scitex/{fig → canvas}/model/_plot.py +1 -1
- scitex/{fig → canvas}/model/_styles.py +1 -1
- scitex/{fig → canvas}/utils/__init__.py +1 -1
- scitex/cli/convert.py +10 -6
- scitex/diagram/README.md +7 -7
- scitex/io/__init__.py +7 -19
- scitex/io/_load.py +15 -19
- scitex/io/_load_modules/_canvas.py +2 -2
- scitex/io/_load_modules/_con.py +5 -5
- scitex/io/_load_modules/_eeg.py +16 -12
- scitex/io/_save.py +11 -16
- scitex/io/_save_modules/__init__.py +6 -10
- scitex/io/_save_modules/_canvas.py +3 -3
- scitex/io/_save_modules/_plot_bundle.py +112 -0
- scitex/io/_save_modules/{_pltz_stx.py → _plot_scitex.py} +7 -7
- scitex/io/_save_modules/_stx_bundle.py +16 -16
- scitex/io/bundle/README.md +89 -80
- scitex/{fts/_bundle/_FTS.py → io/bundle/_Bundle.py} +197 -95
- scitex/io/bundle/__init__.py +67 -35
- scitex/{fts/_bundle → io/bundle}/_children.py +32 -40
- scitex/io/bundle/_core.py +184 -97
- scitex/{fts/_bundle/_dataclasses/_Node.py → io/bundle/_dataclasses/_Spec.py} +29 -23
- scitex/{fts/_bundle/_dataclasses/_NodeRefs.py → io/bundle/_dataclasses/_SpecRefs.py} +6 -6
- scitex/{fts/_bundle → io/bundle}/_dataclasses/__init__.py +4 -4
- scitex/{fts/_bundle → io/bundle}/_loader.py +19 -19
- scitex/io/bundle/_manifest.py +99 -0
- scitex/{fts/_bundle → io/bundle}/_mpl_helpers.py +119 -28
- scitex/io/bundle/_nested.py +113 -100
- scitex/{fts/_bundle → io/bundle}/_saver.py +13 -14
- scitex/{fts/_bundle → io/bundle}/_storage.py +3 -3
- scitex/io/bundle/_types.py +41 -16
- scitex/{fts/_bundle → io/bundle}/_validation.py +20 -18
- scitex/io/bundle/_zip.py +21 -31
- scitex/{fts/_kinds → io/bundle/kinds}/_plot/_backend/_parser.py +1 -1
- scitex/{fts/_kinds → io/bundle/kinds}/_plot/_models/_Annotations.py +1 -1
- scitex/{fts/_kinds → io/bundle/kinds}/_plot/_models/_Axes.py +1 -1
- scitex/{fts/_kinds → io/bundle/kinds}/_plot/_models/_Figure.py +1 -1
- scitex/{fts/_fig → io/bundle/kinds/_plot}/_models/_Guides.py +1 -1
- scitex/{fts/_kinds → io/bundle/kinds}/_plot/_models/_Plot.py +1 -1
- scitex/{fts/_fig → io/bundle/kinds/_plot}/_models/_Styles.py +1 -1
- scitex/{fts/_kinds → io/bundle/kinds}/_plot/_utils/_plot_layout.py +1 -1
- scitex/{fts/_kinds → io/bundle/kinds}/_table/_latex/__init__.py +1 -1
- scitex/{fts/_kinds → io/bundle/kinds}/_table/_latex/_editor/_app.py +1 -1
- scitex/{fts/_tables → io/bundle/kinds/_table}/_latex/_export.py +1 -1
- scitex/{fts/_kinds → io/bundle/kinds}/_table/_latex/_figure_exporter.py +1 -1
- scitex/{fts/_kinds → io/bundle/kinds}/_table/_latex/_table_exporter.py +1 -1
- scitex/io/bundle/schemas/__init__.py +30 -0
- scitex/parallel/_run.py +5 -4
- scitex/path/_find.py +60 -83
- scitex/path/_get_module_path.py +23 -21
- scitex/path/_get_spath.py +6 -27
- scitex/path/_getsize.py +23 -9
- scitex/path/_increment_version.py +31 -38
- scitex/path/_mk_spath.py +26 -29
- scitex/path/_path.py +5 -12
- scitex/path/_split.py +27 -15
- scitex/path/_this_path.py +23 -9
- scitex/plt/_subplots/_AxisWrapperMixins/_MatplotlibPlotMixin/__init__.py +2 -1
- scitex/plt/_subplots/_AxisWrapperMixins/__init__.py +2 -2
- scitex/plt/gallery/_generate.py +76 -50
- scitex/plt/io/__init__.py +17 -19
- scitex/plt/io/_bundle.py +99 -52
- scitex/plt/io/_layered_bundle.py +303 -168
- scitex/plt/utils/_csv_column_naming.py +250 -118
- scitex/schema/__init__.py +69 -73
- scitex/schema/_canvas.py +1 -1
- scitex/schema/_stats.py +2 -2
- scitex/stats/__init__.py +30 -33
- scitex/stats/_schema.py +1 -1
- scitex/stats/io/__init__.py +10 -11
- scitex/stats/io/_bundle.py +16 -16
- {scitex-2.10.3.dist-info → scitex-2.11.0.dist-info}/METADATA +190 -73
- {scitex-2.10.3.dist-info → scitex-2.11.0.dist-info}/RECORD +237 -360
- scitex/fig/editor/_defaults.py +0 -300
- scitex/fig/editor/edit/panel_loader.py +0 -232
- scitex/fig/editor/flask_editor/_bbox.py +0 -1299
- scitex/fig/editor/flask_editor/_core.py +0 -1429
- scitex/fig/editor/flask_editor/_renderer.py +0 -813
- scitex/fig/editor/flask_editor/static/css/features/canvas.css +0 -176
- scitex/fts/README.md +0 -262
- scitex/fts/TODO.md +0 -66
- scitex/fts/__init__.py +0 -90
- scitex/fts/_bundle/README_IN_BUNDLE.md +0 -102
- scitex/fts/_bundle/__init__.py +0 -38
- scitex/fts/_bundle/_utils/__init__.py +0 -55
- scitex/fts/_bundle/_utils/_const.py +0 -26
- scitex/fts/_bundle/_utils/_errors.py +0 -73
- scitex/fts/_bundle/_utils/_generate.py +0 -21
- scitex/fts/_bundle/_utils/_types.py +0 -76
- scitex/fts/_bundle/_zipbundle.py +0 -165
- scitex/fts/_fig/__init__.py +0 -22
- scitex/fts/_fig/_backend/_parser.py +0 -188
- scitex/fts/_fig/_editor/__init__.py +0 -14
- scitex/fts/_fig/_editor/_cui/__init__.py +0 -33
- scitex/fts/_fig/_editor/_cui/_backend_detector.py +0 -39
- scitex/fts/_fig/_editor/_cui/_bundle_resolver.py +0 -366
- scitex/fts/_fig/_editor/_cui/_editor_launcher.py +0 -175
- scitex/fts/_fig/_editor/_cui/_manual_handler.py +0 -52
- scitex/fts/_fig/_editor/_cui/_path_resolver.py +0 -66
- scitex/fts/_fig/_editor/_gui/__init__.py +0 -11
- scitex/fts/_fig/_editor/_gui/_flask_editor/__init__.py +0 -20
- scitex/fts/_fig/_editor/_gui/_flask_editor/_plotter.py +0 -664
- scitex/fts/_fig/_editor/_gui/_flask_editor/_utils.py +0 -79
- scitex/fts/_fig/_editor/_gui/_flask_editor/static/css/base/reset.css +0 -41
- scitex/fts/_fig/_editor/_gui/_flask_editor/static/css/base/typography.css +0 -16
- scitex/fts/_fig/_editor/_gui/_flask_editor/static/css/base/variables.css +0 -85
- scitex/fts/_fig/_editor/_gui/_flask_editor/static/css/components/buttons.css +0 -217
- scitex/fts/_fig/_editor/_gui/_flask_editor/static/css/components/context-menu.css +0 -93
- scitex/fts/_fig/_editor/_gui/_flask_editor/static/css/components/dropdown.css +0 -57
- scitex/fts/_fig/_editor/_gui/_flask_editor/static/css/components/forms.css +0 -112
- scitex/fts/_fig/_editor/_gui/_flask_editor/static/css/components/modal.css +0 -59
- scitex/fts/_fig/_editor/_gui/_flask_editor/static/css/components/sections.css +0 -212
- scitex/fts/_fig/_editor/_gui/_flask_editor/static/css/features/element-inspector.css +0 -190
- scitex/fts/_fig/_editor/_gui/_flask_editor/static/css/features/loading.css +0 -59
- scitex/fts/_fig/_editor/_gui/_flask_editor/static/css/features/overlay.css +0 -45
- scitex/fts/_fig/_editor/_gui/_flask_editor/static/css/features/panel-grid.css +0 -95
- scitex/fts/_fig/_editor/_gui/_flask_editor/static/css/features/selection.css +0 -101
- scitex/fts/_fig/_editor/_gui/_flask_editor/static/css/features/statistics.css +0 -138
- scitex/fts/_fig/_editor/_gui/_flask_editor/static/css/index.css +0 -31
- scitex/fts/_fig/_editor/_gui/_flask_editor/static/css/layout/container.css +0 -7
- scitex/fts/_fig/_editor/_gui/_flask_editor/static/css/layout/controls.css +0 -56
- scitex/fts/_fig/_editor/_gui/_flask_editor/static/css/layout/preview.css +0 -78
- scitex/fts/_fig/_editor/_gui/_flask_editor/static/js/alignment/axis.js +0 -314
- scitex/fts/_fig/_editor/_gui/_flask_editor/static/js/alignment/basic.js +0 -107
- scitex/fts/_fig/_editor/_gui/_flask_editor/static/js/alignment/distribute.js +0 -54
- scitex/fts/_fig/_editor/_gui/_flask_editor/static/js/canvas/canvas.js +0 -172
- scitex/fts/_fig/_editor/_gui/_flask_editor/static/js/canvas/dragging.js +0 -258
- scitex/fts/_fig/_editor/_gui/_flask_editor/static/js/canvas/resize.js +0 -48
- scitex/fts/_fig/_editor/_gui/_flask_editor/static/js/canvas/selection.js +0 -71
- scitex/fts/_fig/_editor/_gui/_flask_editor/static/js/core/api.js +0 -288
- scitex/fts/_fig/_editor/_gui/_flask_editor/static/js/core/state.js +0 -143
- scitex/fts/_fig/_editor/_gui/_flask_editor/static/js/core/utils.js +0 -245
- scitex/fts/_fig/_editor/_gui/_flask_editor/static/js/dev/element-inspector.js +0 -992
- scitex/fts/_fig/_editor/_gui/_flask_editor/static/js/editor/bbox.js +0 -339
- scitex/fts/_fig/_editor/_gui/_flask_editor/static/js/editor/element-drag.js +0 -286
- scitex/fts/_fig/_editor/_gui/_flask_editor/static/js/editor/overlay.js +0 -371
- scitex/fts/_fig/_editor/_gui/_flask_editor/static/js/editor/preview.js +0 -293
- scitex/fts/_fig/_editor/_gui/_flask_editor/static/js/main.js +0 -426
- scitex/fts/_fig/_editor/_gui/_flask_editor/static/js/shortcuts/context-menu.js +0 -152
- scitex/fts/_fig/_editor/_gui/_flask_editor/static/js/shortcuts/keyboard.js +0 -265
- scitex/fts/_fig/_editor/_gui/_flask_editor/static/js/ui/controls.js +0 -184
- scitex/fts/_fig/_editor/_gui/_flask_editor/static/js/ui/download.js +0 -57
- scitex/fts/_fig/_editor/_gui/_flask_editor/static/js/ui/help.js +0 -100
- scitex/fts/_fig/_editor/_gui/_flask_editor/static/js/ui/theme.js +0 -34
- scitex/fts/_fig/_editor/_gui/_flask_editor/templates/__init__.py +0 -124
- scitex/fts/_fig/_editor/_gui/_flask_editor/templates/_html.py +0 -851
- scitex/fts/_fig/_editor/_gui/_flask_editor/templates/_scripts.py +0 -4932
- scitex/fts/_fig/_editor/_gui/_flask_editor/templates/_styles.py +0 -1657
- scitex/fts/_fig/_editor/_gui/_flask_editor.py +0 -36
- scitex/fts/_fig/_models/_Annotations.py +0 -115
- scitex/fts/_fig/_models/_Axes.py +0 -152
- scitex/fts/_fig/_models/_Figure.py +0 -138
- scitex/fts/_fig/_models/_Plot.py +0 -123
- scitex/fts/_fig/_utils/_plot_layout.py +0 -397
- scitex/fts/_kinds/_figure/_composite.py +0 -345
- scitex/fts/_kinds/_plot/_backend/__init__.py +0 -53
- scitex/fts/_kinds/_plot/_backend/_export.py +0 -165
- scitex/fts/_kinds/_plot/_backend/_render.py +0 -538
- scitex/fts/_kinds/_plot/_dataclasses/_ChannelEncoding.py +0 -46
- scitex/fts/_kinds/_plot/_dataclasses/_Encoding.py +0 -82
- scitex/fts/_kinds/_plot/_dataclasses/_Theme.py +0 -441
- scitex/fts/_kinds/_plot/_dataclasses/_TraceEncoding.py +0 -52
- scitex/fts/_kinds/_plot/_dataclasses/__init__.py +0 -47
- scitex/fts/_kinds/_plot/_models/_Guides.py +0 -104
- scitex/fts/_kinds/_plot/_models/_Styles.py +0 -245
- scitex/fts/_kinds/_plot/_models/__init__.py +0 -80
- scitex/fts/_kinds/_plot/_models/_plot_types/__init__.py +0 -156
- scitex/fts/_kinds/_plot/_models/_plot_types/_bar.py +0 -43
- scitex/fts/_kinds/_plot/_models/_plot_types/_box.py +0 -38
- scitex/fts/_kinds/_plot/_models/_plot_types/_distribution.py +0 -36
- scitex/fts/_kinds/_plot/_models/_plot_types/_errorbar.py +0 -60
- scitex/fts/_kinds/_plot/_models/_plot_types/_histogram.py +0 -30
- scitex/fts/_kinds/_plot/_models/_plot_types/_image.py +0 -61
- scitex/fts/_kinds/_plot/_models/_plot_types/_line.py +0 -57
- scitex/fts/_kinds/_plot/_models/_plot_types/_scatter.py +0 -30
- scitex/fts/_kinds/_plot/_models/_plot_types/_seaborn.py +0 -121
- scitex/fts/_kinds/_plot/_models/_plot_types/_violin.py +0 -36
- scitex/fts/_kinds/_plot/_utils/__init__.py +0 -129
- scitex/fts/_kinds/_plot/_utils/_auto_layout.py +0 -127
- scitex/fts/_kinds/_plot/_utils/_calc_bounds.py +0 -111
- scitex/fts/_kinds/_plot/_utils/_const_sizes.py +0 -48
- scitex/fts/_kinds/_plot/_utils/_convert_coords.py +0 -77
- scitex/fts/_kinds/_plot/_utils/_get_template.py +0 -178
- scitex/fts/_kinds/_plot/_utils/_normalize.py +0 -73
- scitex/fts/_kinds/_plot/_utils/_validate.py +0 -197
- scitex/fts/_kinds/_table/_latex/_export.py +0 -279
- scitex/fts/_stats/__init__.py +0 -48
- scitex/fts/_stats/_dataclasses/_Stats.py +0 -423
- scitex/fts/_stats/_dataclasses/__init__.py +0 -48
- scitex/fts/_tables/__init__.py +0 -65
- scitex/fts/_tables/_latex/__init__.py +0 -93
- scitex/fts/_tables/_latex/_editor/__init__.py +0 -11
- scitex/fts/_tables/_latex/_editor/_app.py +0 -725
- scitex/fts/_tables/_latex/_figure_exporter.py +0 -153
- scitex/fts/_tables/_latex/_stats_formatter.py +0 -274
- scitex/fts/_tables/_latex/_table_exporter.py +0 -362
- scitex/fts/_tables/_latex/_utils.py +0 -369
- scitex/fts/_tables/_latex/_validator.py +0 -445
- scitex/io/_save_modules/_pltz_bundle.py +0 -356
- /scitex/{fig → canvas}/README.md +0 -0
- /scitex/{fig → canvas}/backend/__init__.py +0 -0
- /scitex/{fig → canvas}/backend/_export.py +0 -0
- /scitex/{fig → canvas}/backend/_render.py +0 -0
- /scitex/{fig → canvas}/docs/CANVAS_ARCHITECTURE.md +0 -0
- /scitex/{fig → canvas}/editor/__init__.py +0 -0
- /scitex/{fig → canvas}/editor/_dearpygui_editor.py +0 -0
- /scitex/{fig → canvas}/editor/_flask_editor.py +0 -0
- /scitex/{fig → canvas}/editor/_mpl_editor.py +0 -0
- /scitex/{fig → canvas}/editor/_qt_editor.py +0 -0
- /scitex/{fig → canvas}/editor/_tkinter_editor.py +0 -0
- /scitex/{fig → canvas}/editor/edit/backend_detector.py +0 -0
- /scitex/{fig → canvas}/editor/edit/manual_handler.py +0 -0
- /scitex/{fig → canvas}/editor/edit/path_resolver.py +0 -0
- /scitex/{fig → canvas}/editor/flask_editor/__init__.py +0 -0
- /scitex/{fig → canvas}/editor/flask_editor/_plotter.py +0 -0
- /scitex/{fig → canvas}/editor/flask_editor/_utils.py +0 -0
- /scitex/{fig → canvas}/editor/flask_editor/static/css/base/reset.css +0 -0
- /scitex/{fig → canvas}/editor/flask_editor/static/css/base/typography.css +0 -0
- /scitex/{fig → canvas}/editor/flask_editor/static/css/base/variables.css +0 -0
- /scitex/{fig → canvas}/editor/flask_editor/static/css/components/buttons.css +0 -0
- /scitex/{fig → canvas}/editor/flask_editor/static/css/components/context-menu.css +0 -0
- /scitex/{fig → canvas}/editor/flask_editor/static/css/components/dropdown.css +0 -0
- /scitex/{fig → canvas}/editor/flask_editor/static/css/components/forms.css +0 -0
- /scitex/{fig → canvas}/editor/flask_editor/static/css/components/modal.css +0 -0
- /scitex/{fig → canvas}/editor/flask_editor/static/css/components/sections.css +0 -0
- /scitex/{fig → canvas}/editor/flask_editor/static/css/features/element-inspector.css +0 -0
- /scitex/{fig → canvas}/editor/flask_editor/static/css/features/loading.css +0 -0
- /scitex/{fig → canvas}/editor/flask_editor/static/css/features/overlay.css +0 -0
- /scitex/{fig → canvas}/editor/flask_editor/static/css/features/selection.css +0 -0
- /scitex/{fig → canvas}/editor/flask_editor/static/css/features/statistics.css +0 -0
- /scitex/{fig → canvas}/editor/flask_editor/static/css/index.css +0 -0
- /scitex/{fig → canvas}/editor/flask_editor/static/css/layout/container.css +0 -0
- /scitex/{fig → canvas}/editor/flask_editor/static/css/layout/controls.css +0 -0
- /scitex/{fig → canvas}/editor/flask_editor/static/css/layout/preview.css +0 -0
- /scitex/{fig → canvas}/editor/flask_editor/static/js/alignment/axis.js +0 -0
- /scitex/{fig → canvas}/editor/flask_editor/static/js/alignment/basic.js +0 -0
- /scitex/{fig → canvas}/editor/flask_editor/static/js/alignment/distribute.js +0 -0
- /scitex/{fig → canvas}/editor/flask_editor/static/js/canvas/canvas.js +0 -0
- /scitex/{fig → canvas}/editor/flask_editor/static/js/canvas/dragging.js +0 -0
- /scitex/{fig → canvas}/editor/flask_editor/static/js/canvas/resize.js +0 -0
- /scitex/{fig → canvas}/editor/flask_editor/static/js/canvas/selection.js +0 -0
- /scitex/{fig → canvas}/editor/flask_editor/static/js/core/state.js +0 -0
- /scitex/{fig → canvas}/editor/flask_editor/static/js/core/utils.js +0 -0
- /scitex/{fig → canvas}/editor/flask_editor/static/js/dev/element-inspector.js +0 -0
- /scitex/{fig → canvas}/editor/flask_editor/static/js/editor/bbox.js +0 -0
- /scitex/{fig → canvas}/editor/flask_editor/static/js/editor/element-drag.js +0 -0
- /scitex/{fig → canvas}/editor/flask_editor/static/js/editor/overlay.js +0 -0
- /scitex/{fig → canvas}/editor/flask_editor/static/js/main.js +0 -0
- /scitex/{fig → canvas}/editor/flask_editor/static/js/shortcuts/context-menu.js +0 -0
- /scitex/{fig → canvas}/editor/flask_editor/static/js/shortcuts/keyboard.js +0 -0
- /scitex/{fig → canvas}/editor/flask_editor/static/js/ui/controls.js +0 -0
- /scitex/{fig → canvas}/editor/flask_editor/static/js/ui/download.js +0 -0
- /scitex/{fig → canvas}/editor/flask_editor/static/js/ui/help.js +0 -0
- /scitex/{fig → canvas}/editor/flask_editor/static/js/ui/theme.js +0 -0
- /scitex/{fig → canvas}/editor/flask_editor/templates/__init__.py +0 -0
- /scitex/{fig → canvas}/io/_directory.py +0 -0
- /scitex/{fig → canvas}/model/_plot_types.py +0 -0
- /scitex/{fig → canvas}/utils/_defaults.py +0 -0
- /scitex/{fig → canvas}/utils/_validate.py +0 -0
- /scitex/{fts/_bundle → io/bundle}/_conversion/__init__.py +0 -0
- /scitex/{fts/_bundle → io/bundle}/_conversion/_bundle2dict.py +0 -0
- /scitex/{fts/_bundle → io/bundle}/_conversion/_dict2bundle.py +0 -0
- /scitex/{fts/_bundle → io/bundle}/_dataclasses/_Axes.py +0 -0
- /scitex/{fts/_bundle → io/bundle}/_dataclasses/_BBox.py +0 -0
- /scitex/{fts/_bundle → io/bundle}/_dataclasses/_ColumnDef.py +0 -0
- /scitex/{fts/_bundle → io/bundle}/_dataclasses/_DataFormat.py +0 -0
- /scitex/{fts/_bundle → io/bundle}/_dataclasses/_DataInfo.py +0 -0
- /scitex/{fts/_bundle → io/bundle}/_dataclasses/_DataSource.py +0 -0
- /scitex/{fts/_bundle → io/bundle}/_dataclasses/_SizeMM.py +0 -0
- /scitex/{fts/_bundle → io/bundle}/_extractors/__init__.py +0 -0
- /scitex/{fts/_bundle → io/bundle}/_extractors/_extract_bar.py +0 -0
- /scitex/{fts/_bundle → io/bundle}/_extractors/_extract_line.py +0 -0
- /scitex/{fts/_bundle → io/bundle}/_extractors/_extract_scatter.py +0 -0
- /scitex/{fts/_kinds → io/bundle/kinds}/__init__.py +0 -0
- /scitex/{fts/_kinds → io/bundle/kinds}/_figure/__init__.py +0 -0
- /scitex/{fts/_fig → io/bundle/kinds/_figure}/_composite.py +0 -0
- /scitex/{fts/_kinds → io/bundle/kinds}/_plot/__init__.py +0 -0
- /scitex/{fts/_fig → io/bundle/kinds/_plot}/_backend/__init__.py +0 -0
- /scitex/{fts/_fig → io/bundle/kinds/_plot}/_backend/_export.py +0 -0
- /scitex/{fts/_fig → io/bundle/kinds/_plot}/_backend/_render.py +0 -0
- /scitex/{fts/_fig → io/bundle/kinds/_plot}/_dataclasses/_ChannelEncoding.py +0 -0
- /scitex/{fts/_fig → io/bundle/kinds/_plot}/_dataclasses/_Encoding.py +0 -0
- /scitex/{fts/_fig → io/bundle/kinds/_plot}/_dataclasses/_Theme.py +0 -0
- /scitex/{fts/_fig → io/bundle/kinds/_plot}/_dataclasses/_TraceEncoding.py +0 -0
- /scitex/{fts/_fig → io/bundle/kinds/_plot}/_dataclasses/__init__.py +0 -0
- /scitex/{fts/_fig → io/bundle/kinds/_plot}/_models/__init__.py +0 -0
- /scitex/{fts/_fig → io/bundle/kinds/_plot}/_models/_plot_types/__init__.py +0 -0
- /scitex/{fts/_fig → io/bundle/kinds/_plot}/_models/_plot_types/_bar.py +0 -0
- /scitex/{fts/_fig → io/bundle/kinds/_plot}/_models/_plot_types/_box.py +0 -0
- /scitex/{fts/_fig → io/bundle/kinds/_plot}/_models/_plot_types/_distribution.py +0 -0
- /scitex/{fts/_fig → io/bundle/kinds/_plot}/_models/_plot_types/_errorbar.py +0 -0
- /scitex/{fts/_fig → io/bundle/kinds/_plot}/_models/_plot_types/_histogram.py +0 -0
- /scitex/{fts/_fig → io/bundle/kinds/_plot}/_models/_plot_types/_image.py +0 -0
- /scitex/{fts/_fig → io/bundle/kinds/_plot}/_models/_plot_types/_line.py +0 -0
- /scitex/{fts/_fig → io/bundle/kinds/_plot}/_models/_plot_types/_scatter.py +0 -0
- /scitex/{fts/_fig → io/bundle/kinds/_plot}/_models/_plot_types/_seaborn.py +0 -0
- /scitex/{fts/_fig → io/bundle/kinds/_plot}/_models/_plot_types/_violin.py +0 -0
- /scitex/{fts/_fig → io/bundle/kinds/_plot}/_utils/__init__.py +0 -0
- /scitex/{fts/_fig → io/bundle/kinds/_plot}/_utils/_auto_layout.py +0 -0
- /scitex/{fts/_fig → io/bundle/kinds/_plot}/_utils/_calc_bounds.py +0 -0
- /scitex/{fts/_fig → io/bundle/kinds/_plot}/_utils/_const_sizes.py +0 -0
- /scitex/{fts/_fig → io/bundle/kinds/_plot}/_utils/_convert_coords.py +0 -0
- /scitex/{fts/_fig → io/bundle/kinds/_plot}/_utils/_get_template.py +0 -0
- /scitex/{fts/_fig → io/bundle/kinds/_plot}/_utils/_normalize.py +0 -0
- /scitex/{fts/_fig → io/bundle/kinds/_plot}/_utils/_validate.py +0 -0
- /scitex/{fts/_kinds → io/bundle/kinds}/_shape/__init__.py +0 -0
- /scitex/{fts/_kinds → io/bundle/kinds}/_stats/__init__.py +0 -0
- /scitex/{fts/_kinds → io/bundle/kinds}/_stats/_dataclasses/_Stats.py +0 -0
- /scitex/{fts/_kinds → io/bundle/kinds}/_stats/_dataclasses/__init__.py +0 -0
- /scitex/{fts/_kinds → io/bundle/kinds}/_table/__init__.py +0 -0
- /scitex/{fts/_kinds → io/bundle/kinds}/_table/_latex/_editor/__init__.py +0 -0
- /scitex/{fts/_kinds → io/bundle/kinds}/_table/_latex/_stats_formatter.py +0 -0
- /scitex/{fts/_kinds → io/bundle/kinds}/_table/_latex/_utils.py +0 -0
- /scitex/{fts/_kinds → io/bundle/kinds}/_table/_latex/_validator.py +0 -0
- /scitex/{fts/_kinds → io/bundle/kinds}/_text/__init__.py +0 -0
- /scitex/{fts/_schemas → io/bundle/schemas}/data_info.schema.json +0 -0
- /scitex/{fts/_schemas → io/bundle/schemas}/encoding.schema.json +0 -0
- /scitex/{fts/_schemas → io/bundle/schemas}/node.schema.json +0 -0
- /scitex/{fts/_schemas → io/bundle/schemas}/render_manifest.schema.json +0 -0
- /scitex/{fts/_schemas → io/bundle/schemas}/stats.schema.json +0 -0
- /scitex/{fts/_schemas → io/bundle/schemas}/theme.schema.json +0 -0
- {scitex-2.10.3.dist-info → scitex-2.11.0.dist-info}/WHEEL +0 -0
- {scitex-2.10.3.dist-info → scitex-2.11.0.dist-info}/entry_points.txt +0 -0
- {scitex-2.10.3.dist-info → scitex-2.11.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,1299 +0,0 @@
|
|
|
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
|