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.
- scitex/__init__.py +6 -2
- scitex/__version__.py +1 -1
- scitex/audio/README.md +52 -0
- scitex/audio/__init__.py +384 -0
- scitex/audio/__main__.py +129 -0
- scitex/audio/_tts.py +334 -0
- scitex/audio/engines/__init__.py +44 -0
- scitex/audio/engines/base.py +275 -0
- scitex/audio/engines/elevenlabs_engine.py +143 -0
- scitex/audio/engines/gtts_engine.py +162 -0
- scitex/audio/engines/pyttsx3_engine.py +131 -0
- scitex/audio/mcp_server.py +757 -0
- scitex/bridge/_helpers.py +1 -1
- scitex/bridge/_plt_vis.py +1 -1
- scitex/bridge/_stats_vis.py +1 -1
- scitex/dev/plt/__init__.py +272 -0
- scitex/dev/plt/plot_mpl_axhline.py +28 -0
- scitex/dev/plt/plot_mpl_axhspan.py +28 -0
- scitex/dev/plt/plot_mpl_axvline.py +28 -0
- scitex/dev/plt/plot_mpl_axvspan.py +28 -0
- scitex/dev/plt/plot_mpl_bar.py +29 -0
- scitex/dev/plt/plot_mpl_barh.py +29 -0
- scitex/dev/plt/plot_mpl_boxplot.py +28 -0
- scitex/dev/plt/plot_mpl_contour.py +31 -0
- scitex/dev/plt/plot_mpl_contourf.py +31 -0
- scitex/dev/plt/plot_mpl_errorbar.py +30 -0
- scitex/dev/plt/plot_mpl_eventplot.py +28 -0
- scitex/dev/plt/plot_mpl_fill.py +30 -0
- scitex/dev/plt/plot_mpl_fill_between.py +31 -0
- scitex/dev/plt/plot_mpl_hexbin.py +28 -0
- scitex/dev/plt/plot_mpl_hist.py +28 -0
- scitex/dev/plt/plot_mpl_hist2d.py +28 -0
- scitex/dev/plt/plot_mpl_imshow.py +29 -0
- scitex/dev/plt/plot_mpl_pcolormesh.py +31 -0
- scitex/dev/plt/plot_mpl_pie.py +29 -0
- scitex/dev/plt/plot_mpl_plot.py +29 -0
- scitex/dev/plt/plot_mpl_quiver.py +31 -0
- scitex/dev/plt/plot_mpl_scatter.py +28 -0
- scitex/dev/plt/plot_mpl_stackplot.py +31 -0
- scitex/dev/plt/plot_mpl_stem.py +29 -0
- scitex/dev/plt/plot_mpl_step.py +29 -0
- scitex/dev/plt/plot_mpl_violinplot.py +28 -0
- scitex/dev/plt/plot_sns_barplot.py +29 -0
- scitex/dev/plt/plot_sns_boxplot.py +29 -0
- scitex/dev/plt/plot_sns_heatmap.py +28 -0
- scitex/dev/plt/plot_sns_histplot.py +29 -0
- scitex/dev/plt/plot_sns_kdeplot.py +29 -0
- scitex/dev/plt/plot_sns_lineplot.py +31 -0
- scitex/dev/plt/plot_sns_scatterplot.py +29 -0
- scitex/dev/plt/plot_sns_stripplot.py +29 -0
- scitex/dev/plt/plot_sns_swarmplot.py +29 -0
- scitex/dev/plt/plot_sns_violinplot.py +29 -0
- scitex/dev/plt/plot_stx_bar.py +29 -0
- scitex/dev/plt/plot_stx_barh.py +29 -0
- scitex/dev/plt/plot_stx_box.py +28 -0
- scitex/dev/plt/plot_stx_boxplot.py +28 -0
- scitex/dev/plt/plot_stx_conf_mat.py +28 -0
- scitex/dev/plt/plot_stx_contour.py +31 -0
- scitex/dev/plt/plot_stx_ecdf.py +28 -0
- scitex/dev/plt/plot_stx_errorbar.py +30 -0
- scitex/dev/plt/plot_stx_fill_between.py +31 -0
- scitex/dev/plt/plot_stx_fillv.py +28 -0
- scitex/dev/plt/plot_stx_heatmap.py +28 -0
- scitex/dev/plt/plot_stx_image.py +28 -0
- scitex/dev/plt/plot_stx_imshow.py +28 -0
- scitex/dev/plt/plot_stx_joyplot.py +28 -0
- scitex/dev/plt/plot_stx_kde.py +28 -0
- scitex/dev/plt/plot_stx_line.py +28 -0
- scitex/dev/plt/plot_stx_mean_ci.py +28 -0
- scitex/dev/plt/plot_stx_mean_std.py +28 -0
- scitex/dev/plt/plot_stx_median_iqr.py +28 -0
- scitex/dev/plt/plot_stx_raster.py +28 -0
- scitex/dev/plt/plot_stx_rectangle.py +28 -0
- scitex/dev/plt/plot_stx_scatter.py +29 -0
- scitex/dev/plt/plot_stx_shaded_line.py +29 -0
- scitex/dev/plt/plot_stx_violin.py +28 -0
- scitex/dev/plt/plot_stx_violinplot.py +28 -0
- scitex/diagram/README.md +197 -0
- scitex/diagram/__init__.py +48 -0
- scitex/diagram/_compile.py +312 -0
- scitex/diagram/_diagram.py +355 -0
- scitex/diagram/_presets.py +173 -0
- scitex/diagram/_schema.py +182 -0
- scitex/diagram/_split.py +278 -0
- scitex/fig/__init__.py +352 -0
- scitex/{vis → fig}/backend/_parser.py +1 -1
- scitex/{vis → fig}/canvas.py +1 -1
- scitex/{vis → fig}/editor/__init__.py +5 -2
- scitex/{vis → fig}/editor/_dearpygui_editor.py +1 -1
- scitex/{vis → fig}/editor/_defaults.py +70 -5
- scitex/{vis → fig}/editor/_mpl_editor.py +1 -1
- scitex/{vis → fig}/editor/_qt_editor.py +182 -2
- scitex/{vis → fig}/editor/_tkinter_editor.py +1 -1
- scitex/fig/editor/edit/__init__.py +50 -0
- scitex/fig/editor/edit/backend_detector.py +109 -0
- scitex/fig/editor/edit/bundle_resolver.py +240 -0
- scitex/fig/editor/edit/editor_launcher.py +239 -0
- scitex/fig/editor/edit/manual_handler.py +53 -0
- scitex/fig/editor/edit/panel_loader.py +232 -0
- scitex/fig/editor/edit/path_resolver.py +67 -0
- scitex/fig/editor/flask_editor/_bbox.py +1299 -0
- scitex/fig/editor/flask_editor/_core.py +1429 -0
- scitex/{vis → fig}/editor/flask_editor/_plotter.py +38 -4
- scitex/fig/editor/flask_editor/_renderer.py +813 -0
- scitex/fig/editor/flask_editor/static/css/base/reset.css +41 -0
- scitex/fig/editor/flask_editor/static/css/base/typography.css +16 -0
- scitex/fig/editor/flask_editor/static/css/base/variables.css +85 -0
- scitex/fig/editor/flask_editor/static/css/components/buttons.css +217 -0
- scitex/fig/editor/flask_editor/static/css/components/context-menu.css +93 -0
- scitex/fig/editor/flask_editor/static/css/components/dropdown.css +57 -0
- scitex/fig/editor/flask_editor/static/css/components/forms.css +112 -0
- scitex/fig/editor/flask_editor/static/css/components/modal.css +59 -0
- scitex/fig/editor/flask_editor/static/css/components/sections.css +212 -0
- scitex/fig/editor/flask_editor/static/css/features/canvas.css +176 -0
- scitex/fig/editor/flask_editor/static/css/features/element-inspector.css +190 -0
- scitex/fig/editor/flask_editor/static/css/features/loading.css +59 -0
- scitex/fig/editor/flask_editor/static/css/features/overlay.css +45 -0
- scitex/fig/editor/flask_editor/static/css/features/panel-grid.css +95 -0
- scitex/fig/editor/flask_editor/static/css/features/selection.css +101 -0
- scitex/fig/editor/flask_editor/static/css/features/statistics.css +138 -0
- scitex/fig/editor/flask_editor/static/css/index.css +31 -0
- scitex/fig/editor/flask_editor/static/css/layout/container.css +7 -0
- scitex/fig/editor/flask_editor/static/css/layout/controls.css +56 -0
- scitex/fig/editor/flask_editor/static/css/layout/preview.css +78 -0
- scitex/fig/editor/flask_editor/static/js/alignment/axis.js +314 -0
- scitex/fig/editor/flask_editor/static/js/alignment/basic.js +107 -0
- scitex/fig/editor/flask_editor/static/js/alignment/distribute.js +54 -0
- scitex/fig/editor/flask_editor/static/js/canvas/canvas.js +172 -0
- scitex/fig/editor/flask_editor/static/js/canvas/dragging.js +258 -0
- scitex/fig/editor/flask_editor/static/js/canvas/resize.js +48 -0
- scitex/fig/editor/flask_editor/static/js/canvas/selection.js +71 -0
- scitex/fig/editor/flask_editor/static/js/core/api.js +288 -0
- scitex/fig/editor/flask_editor/static/js/core/state.js +143 -0
- scitex/fig/editor/flask_editor/static/js/core/utils.js +245 -0
- scitex/fig/editor/flask_editor/static/js/dev/element-inspector.js +992 -0
- scitex/fig/editor/flask_editor/static/js/editor/bbox.js +339 -0
- scitex/fig/editor/flask_editor/static/js/editor/element-drag.js +286 -0
- scitex/fig/editor/flask_editor/static/js/editor/overlay.js +371 -0
- scitex/fig/editor/flask_editor/static/js/editor/preview.js +293 -0
- scitex/fig/editor/flask_editor/static/js/main.js +426 -0
- scitex/fig/editor/flask_editor/static/js/shortcuts/context-menu.js +152 -0
- scitex/fig/editor/flask_editor/static/js/shortcuts/keyboard.js +265 -0
- scitex/fig/editor/flask_editor/static/js/ui/controls.js +184 -0
- scitex/fig/editor/flask_editor/static/js/ui/download.js +57 -0
- scitex/fig/editor/flask_editor/static/js/ui/help.js +100 -0
- scitex/fig/editor/flask_editor/static/js/ui/theme.js +34 -0
- scitex/fig/editor/flask_editor/templates/__init__.py +123 -0
- scitex/fig/editor/flask_editor/templates/_html.py +852 -0
- scitex/fig/editor/flask_editor/templates/_scripts.py +4933 -0
- scitex/fig/editor/flask_editor/templates/_styles.py +1658 -0
- scitex/{vis → fig}/io/__init__.py +13 -1
- scitex/fig/io/_bundle.py +1058 -0
- scitex/{vis → fig}/io/_canvas.py +1 -1
- scitex/{vis → fig}/io/_data.py +1 -1
- scitex/{vis → fig}/io/_export.py +1 -1
- scitex/{vis → fig}/io/_load.py +1 -1
- scitex/{vis → fig}/io/_panel.py +1 -1
- scitex/{vis → fig}/io/_save.py +1 -1
- scitex/{vis → fig}/model/__init__.py +1 -1
- scitex/{vis → fig}/model/_annotations.py +1 -1
- scitex/{vis → fig}/model/_axes.py +1 -1
- scitex/{vis → fig}/model/_figure.py +1 -1
- scitex/{vis → fig}/model/_guides.py +1 -1
- scitex/{vis → fig}/model/_plot.py +1 -1
- scitex/{vis → fig}/model/_styles.py +1 -1
- scitex/{vis → fig}/utils/__init__.py +1 -1
- scitex/io/__init__.py +22 -26
- scitex/io/_bundle.py +493 -0
- scitex/io/_flush.py +5 -2
- scitex/io/_load.py +98 -0
- scitex/io/_load_modules/_H5Explorer.py +5 -2
- scitex/io/_load_modules/_canvas.py +2 -2
- scitex/io/_load_modules/_image.py +3 -4
- scitex/io/_load_modules/_txt.py +4 -2
- scitex/io/_metadata.py +34 -324
- scitex/io/_metadata_modules/__init__.py +46 -0
- scitex/io/_metadata_modules/_embed.py +70 -0
- scitex/io/_metadata_modules/_read.py +64 -0
- scitex/io/_metadata_modules/_utils.py +79 -0
- scitex/io/_metadata_modules/embed_metadata_jpeg.py +74 -0
- scitex/io/_metadata_modules/embed_metadata_pdf.py +53 -0
- scitex/io/_metadata_modules/embed_metadata_png.py +26 -0
- scitex/io/_metadata_modules/embed_metadata_svg.py +62 -0
- scitex/io/_metadata_modules/read_metadata_jpeg.py +57 -0
- scitex/io/_metadata_modules/read_metadata_pdf.py +51 -0
- scitex/io/_metadata_modules/read_metadata_png.py +39 -0
- scitex/io/_metadata_modules/read_metadata_svg.py +44 -0
- scitex/io/_qr_utils.py +5 -3
- scitex/io/_save.py +548 -30
- scitex/io/_save_modules/_canvas.py +3 -3
- scitex/io/_save_modules/_image.py +5 -9
- scitex/io/_save_modules/_tex.py +7 -4
- scitex/io/_zip_bundle.py +439 -0
- scitex/io/utils/h5_to_zarr.py +11 -9
- scitex/msword/__init__.py +255 -0
- scitex/msword/profiles.py +357 -0
- scitex/msword/reader.py +753 -0
- scitex/msword/utils.py +289 -0
- scitex/msword/writer.py +362 -0
- scitex/plt/__init__.py +5 -2
- scitex/plt/_subplots/_AxesWrapper.py +6 -6
- scitex/plt/_subplots/_AxisWrapper.py +15 -9
- scitex/plt/_subplots/_AxisWrapperMixins/_AdjustmentMixin/__init__.py +36 -0
- scitex/plt/_subplots/_AxisWrapperMixins/_AdjustmentMixin/_labels.py +264 -0
- scitex/plt/_subplots/_AxisWrapperMixins/_AdjustmentMixin/_metadata.py +213 -0
- scitex/plt/_subplots/_AxisWrapperMixins/_AdjustmentMixin/_visual.py +128 -0
- scitex/plt/_subplots/_AxisWrapperMixins/_MatplotlibPlotMixin/__init__.py +59 -0
- scitex/plt/_subplots/_AxisWrapperMixins/_MatplotlibPlotMixin/_base.py +34 -0
- scitex/plt/_subplots/_AxisWrapperMixins/_MatplotlibPlotMixin/_scientific.py +593 -0
- scitex/plt/_subplots/_AxisWrapperMixins/_MatplotlibPlotMixin/_statistical.py +654 -0
- scitex/plt/_subplots/_AxisWrapperMixins/_MatplotlibPlotMixin/_stx_aliases.py +527 -0
- scitex/plt/_subplots/_AxisWrapperMixins/_RawMatplotlibMixin.py +321 -0
- scitex/plt/_subplots/_AxisWrapperMixins/_SeabornMixin/__init__.py +33 -0
- scitex/plt/_subplots/_AxisWrapperMixins/_SeabornMixin/_base.py +152 -0
- scitex/plt/_subplots/_AxisWrapperMixins/_SeabornMixin/_wrappers.py +600 -0
- scitex/plt/_subplots/_AxisWrapperMixins/__init__.py +79 -5
- scitex/plt/_subplots/_FigWrapper.py +6 -6
- scitex/plt/_subplots/_SubplotsWrapper.py +28 -18
- scitex/plt/_subplots/_export_as_csv.py +35 -5
- scitex/plt/_subplots/_export_as_csv_formatters/__init__.py +8 -0
- scitex/plt/_subplots/_export_as_csv_formatters/_format_annotate.py +10 -21
- scitex/plt/_subplots/_export_as_csv_formatters/_format_eventplot.py +18 -7
- scitex/plt/_subplots/_export_as_csv_formatters/_format_imshow2d.py +28 -12
- scitex/plt/_subplots/_export_as_csv_formatters/_format_matshow.py +10 -4
- scitex/plt/_subplots/_export_as_csv_formatters/_format_plot_imshow.py +13 -1
- scitex/plt/_subplots/_export_as_csv_formatters/_format_plot_kde.py +12 -2
- scitex/plt/_subplots/_export_as_csv_formatters/_format_plot_scatter.py +10 -3
- scitex/plt/_subplots/_export_as_csv_formatters/_format_quiver.py +10 -4
- scitex/plt/_subplots/_export_as_csv_formatters/_format_sns_jointplot.py +18 -3
- scitex/plt/_subplots/_export_as_csv_formatters/_format_sns_lineplot.py +44 -36
- scitex/plt/_subplots/_export_as_csv_formatters/_format_sns_pairplot.py +14 -2
- scitex/plt/_subplots/_export_as_csv_formatters/_format_streamplot.py +11 -5
- scitex/plt/_subplots/_export_as_csv_formatters/_format_stx_bar.py +84 -0
- scitex/plt/_subplots/_export_as_csv_formatters/_format_stx_barh.py +85 -0
- scitex/plt/_subplots/_export_as_csv_formatters/_format_stx_conf_mat.py +14 -3
- scitex/plt/_subplots/_export_as_csv_formatters/_format_stx_contour.py +54 -0
- scitex/plt/_subplots/_export_as_csv_formatters/_format_stx_ecdf.py +14 -2
- scitex/plt/_subplots/_export_as_csv_formatters/_format_stx_errorbar.py +120 -0
- scitex/plt/_subplots/_export_as_csv_formatters/_format_stx_heatmap.py +16 -6
- scitex/plt/_subplots/_export_as_csv_formatters/_format_stx_image.py +29 -19
- scitex/plt/_subplots/_export_as_csv_formatters/_format_stx_imshow.py +63 -0
- scitex/plt/_subplots/_export_as_csv_formatters/_format_stx_joyplot.py +22 -5
- scitex/plt/_subplots/_export_as_csv_formatters/_format_stx_mean_ci.py +18 -14
- scitex/plt/_subplots/_export_as_csv_formatters/_format_stx_mean_std.py +18 -14
- scitex/plt/_subplots/_export_as_csv_formatters/_format_stx_median_iqr.py +18 -14
- scitex/plt/_subplots/_export_as_csv_formatters/_format_stx_raster.py +10 -2
- scitex/plt/_subplots/_export_as_csv_formatters/_format_stx_scatter.py +51 -0
- scitex/plt/_subplots/_export_as_csv_formatters/_format_stx_scatter_hist.py +18 -9
- scitex/plt/ax/_plot/_stx_ecdf.py +4 -2
- scitex/plt/gallery/_generate.py +421 -14
- scitex/plt/io/__init__.py +53 -0
- scitex/plt/io/_bundle.py +490 -0
- scitex/plt/io/_layered_bundle.py +1343 -0
- scitex/plt/styles/SCITEX_STYLE.yaml +26 -0
- scitex/plt/styles/__init__.py +14 -0
- scitex/plt/styles/presets.py +78 -0
- scitex/plt/utils/__init__.py +13 -1
- scitex/plt/utils/_collect_figure_metadata.py +10 -14
- scitex/plt/utils/_configure_mpl.py +6 -18
- scitex/plt/utils/_crop.py +32 -14
- scitex/plt/utils/_csv_column_naming.py +54 -0
- scitex/plt/utils/_figure_mm.py +116 -1
- scitex/plt/utils/_hitmap.py +1643 -0
- scitex/plt/utils/metadata/__init__.py +25 -0
- scitex/plt/utils/metadata/_core.py +9 -10
- scitex/plt/utils/metadata/_dimensions.py +6 -3
- scitex/plt/utils/metadata/_editable_export.py +405 -0
- scitex/plt/utils/metadata/_geometry_extraction.py +570 -0
- scitex/schema/__init__.py +109 -16
- scitex/schema/_canvas.py +1 -1
- scitex/schema/_plot.py +1015 -0
- scitex/schema/_stats.py +2 -2
- scitex/stats/__init__.py +117 -0
- scitex/stats/io/__init__.py +29 -0
- scitex/stats/io/_bundle.py +156 -0
- scitex/tex/__init__.py +4 -0
- scitex/tex/_export.py +890 -0
- {scitex-2.7.0.dist-info → scitex-2.8.1.dist-info}/METADATA +11 -1
- {scitex-2.7.0.dist-info → scitex-2.8.1.dist-info}/RECORD +294 -170
- scitex/io/memo.md +0 -2827
- scitex/plt/REQUESTS.md +0 -191
- scitex/plt/_subplots/TODO.md +0 -53
- scitex/plt/_subplots/_AxisWrapperMixins/_AdjustmentMixin.py +0 -559
- scitex/plt/_subplots/_AxisWrapperMixins/_MatplotlibPlotMixin.py +0 -1609
- scitex/plt/_subplots/_AxisWrapperMixins/_SeabornMixin.py +0 -447
- scitex/plt/templates/research-master/scitex/vis/gallery/area/fill_between.json +0 -110
- scitex/plt/templates/research-master/scitex/vis/gallery/area/fill_betweenx.json +0 -88
- scitex/plt/templates/research-master/scitex/vis/gallery/area/stx_fill_between.json +0 -103
- scitex/plt/templates/research-master/scitex/vis/gallery/area/stx_fillv.json +0 -106
- scitex/plt/templates/research-master/scitex/vis/gallery/categorical/bar.json +0 -92
- scitex/plt/templates/research-master/scitex/vis/gallery/categorical/barh.json +0 -92
- scitex/plt/templates/research-master/scitex/vis/gallery/categorical/boxplot.json +0 -92
- scitex/plt/templates/research-master/scitex/vis/gallery/categorical/stx_bar.json +0 -84
- scitex/plt/templates/research-master/scitex/vis/gallery/categorical/stx_barh.json +0 -84
- scitex/plt/templates/research-master/scitex/vis/gallery/categorical/stx_box.json +0 -83
- scitex/plt/templates/research-master/scitex/vis/gallery/categorical/stx_boxplot.json +0 -93
- scitex/plt/templates/research-master/scitex/vis/gallery/categorical/stx_violin.json +0 -91
- scitex/plt/templates/research-master/scitex/vis/gallery/categorical/stx_violinplot.json +0 -91
- scitex/plt/templates/research-master/scitex/vis/gallery/categorical/violinplot.json +0 -91
- scitex/plt/templates/research-master/scitex/vis/gallery/contour/contour.json +0 -97
- scitex/plt/templates/research-master/scitex/vis/gallery/contour/contourf.json +0 -98
- scitex/plt/templates/research-master/scitex/vis/gallery/contour/stx_contour.json +0 -84
- scitex/plt/templates/research-master/scitex/vis/gallery/distribution/hist.json +0 -101
- scitex/plt/templates/research-master/scitex/vis/gallery/distribution/hist2d.json +0 -96
- scitex/plt/templates/research-master/scitex/vis/gallery/distribution/stx_ecdf.json +0 -95
- scitex/plt/templates/research-master/scitex/vis/gallery/distribution/stx_joyplot.json +0 -95
- scitex/plt/templates/research-master/scitex/vis/gallery/distribution/stx_kde.json +0 -93
- scitex/plt/templates/research-master/scitex/vis/gallery/grid/imshow.json +0 -95
- scitex/plt/templates/research-master/scitex/vis/gallery/grid/matshow.json +0 -95
- scitex/plt/templates/research-master/scitex/vis/gallery/grid/stx_conf_mat.json +0 -83
- scitex/plt/templates/research-master/scitex/vis/gallery/grid/stx_heatmap.json +0 -92
- scitex/plt/templates/research-master/scitex/vis/gallery/grid/stx_image.json +0 -121
- scitex/plt/templates/research-master/scitex/vis/gallery/grid/stx_imshow.json +0 -84
- scitex/plt/templates/research-master/scitex/vis/gallery/line/plot.json +0 -110
- scitex/plt/templates/research-master/scitex/vis/gallery/line/step.json +0 -92
- scitex/plt/templates/research-master/scitex/vis/gallery/line/stx_line.json +0 -95
- scitex/plt/templates/research-master/scitex/vis/gallery/line/stx_shaded_line.json +0 -96
- scitex/plt/templates/research-master/scitex/vis/gallery/scatter/hexbin.json +0 -95
- scitex/plt/templates/research-master/scitex/vis/gallery/scatter/scatter.json +0 -95
- scitex/plt/templates/research-master/scitex/vis/gallery/scatter/stem.json +0 -92
- scitex/plt/templates/research-master/scitex/vis/gallery/scatter/stx_scatter.json +0 -84
- scitex/plt/templates/research-master/scitex/vis/gallery/special/pie.json +0 -94
- scitex/plt/templates/research-master/scitex/vis/gallery/special/stx_raster.json +0 -109
- scitex/plt/templates/research-master/scitex/vis/gallery/special/stx_rectangle.json +0 -108
- scitex/plt/templates/research-master/scitex/vis/gallery/statistical/errorbar.json +0 -93
- scitex/plt/templates/research-master/scitex/vis/gallery/statistical/stx_errorbar.json +0 -84
- scitex/plt/templates/research-master/scitex/vis/gallery/statistical/stx_mean_ci.json +0 -96
- scitex/plt/templates/research-master/scitex/vis/gallery/statistical/stx_mean_std.json +0 -96
- scitex/plt/templates/research-master/scitex/vis/gallery/statistical/stx_median_iqr.json +0 -96
- scitex/plt/templates/research-master/scitex/vis/gallery/vector/quiver.json +0 -99
- scitex/plt/templates/research-master/scitex/vis/gallery/vector/streamplot.json +0 -100
- scitex/vis/__init__.py +0 -177
- scitex/vis/editor/_edit.py +0 -390
- scitex/vis/editor/flask_editor/_bbox.py +0 -529
- scitex/vis/editor/flask_editor/_core.py +0 -168
- scitex/vis/editor/flask_editor/_renderer.py +0 -393
- scitex/vis/editor/flask_editor/templates/__init__.py +0 -33
- scitex/vis/editor/flask_editor/templates/_html.py +0 -513
- scitex/vis/editor/flask_editor/templates/_scripts.py +0 -1261
- scitex/vis/editor/flask_editor/templates/_styles.py +0 -739
- /scitex/{vis → fig}/README.md +0 -0
- /scitex/{vis → fig}/backend/__init__.py +0 -0
- /scitex/{vis → fig}/backend/_export.py +0 -0
- /scitex/{vis → fig}/backend/_render.py +0 -0
- /scitex/{vis → fig}/docs/CANVAS_ARCHITECTURE.md +0 -0
- /scitex/{vis → fig}/editor/_flask_editor.py +0 -0
- /scitex/{vis → fig}/editor/flask_editor/__init__.py +0 -0
- /scitex/{vis → fig}/editor/flask_editor/_utils.py +0 -0
- /scitex/{vis → fig}/io/_directory.py +0 -0
- /scitex/{vis → fig}/model/_plot_types.py +0 -0
- /scitex/{vis → fig}/utils/_defaults.py +0 -0
- /scitex/{vis → fig}/utils/_validate.py +0 -0
- {scitex-2.7.0.dist-info → scitex-2.8.1.dist-info}/WHEEL +0 -0
- {scitex-2.7.0.dist-info → scitex-2.8.1.dist-info}/entry_points.txt +0 -0
- {scitex-2.7.0.dist-info → scitex-2.8.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,1343 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
# Timestamp: "2025-12-14 (ywatanabe)"
|
|
4
|
+
# File: /home/ywatanabe/proj/scitex-code/src/scitex/plt/io/_layered_bundle.py
|
|
5
|
+
|
|
6
|
+
"""
|
|
7
|
+
Layered .pltz Bundle I/O - New schema with spec/style/geometry separation.
|
|
8
|
+
|
|
9
|
+
Bundle structure:
|
|
10
|
+
plot.pltz.d/
|
|
11
|
+
spec.json # Semantic: WHAT to plot (canonical)
|
|
12
|
+
style.json # Appearance: HOW it looks (canonical)
|
|
13
|
+
data.csv # Raw data (immutable)
|
|
14
|
+
exports/
|
|
15
|
+
overview.png # Preview image
|
|
16
|
+
overview.svg # Vector preview
|
|
17
|
+
hitmap.png # Hit testing image
|
|
18
|
+
cache/
|
|
19
|
+
geometry_px.json # Derived: WHERE in pixels (regenerable)
|
|
20
|
+
render_manifest.json # Render metadata (dpi, hashes)
|
|
21
|
+
|
|
22
|
+
Design Principles:
|
|
23
|
+
- spec.json + style.json = source of truth (edit these)
|
|
24
|
+
- cache/* = derived, can be deleted and regenerated
|
|
25
|
+
- Canonical units: ratio (0-1) for axes bbox, mm for panel size
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
import json
|
|
29
|
+
import hashlib
|
|
30
|
+
from pathlib import Path
|
|
31
|
+
from typing import Any, Dict, List, Optional, Tuple
|
|
32
|
+
from dataclasses import asdict
|
|
33
|
+
|
|
34
|
+
from scitex import logging
|
|
35
|
+
from scitex.plt.styles import get_default_dpi, get_preview_dpi
|
|
36
|
+
from scitex.schema import (
|
|
37
|
+
# Spec classes
|
|
38
|
+
PltzSpec, PltzTraceSpec, PltzAxesItem, PltzAxesLimits, PltzAxesLabels,
|
|
39
|
+
PltzDataSource, BboxRatio, BboxPx,
|
|
40
|
+
# Style classes
|
|
41
|
+
PltzStyle, PltzTheme, PltzFont, PltzSize, PltzTraceStyle, PltzLegendSpec,
|
|
42
|
+
# Geometry classes
|
|
43
|
+
PltzGeometry, PltzRenderedAxes, PltzRenderedArtist, PltzRenderManifest,
|
|
44
|
+
# Version constants
|
|
45
|
+
PLOT_SPEC_VERSION, PLOT_STYLE_VERSION, PLOT_GEOMETRY_VERSION,
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
logger = logging.getLogger()
|
|
49
|
+
|
|
50
|
+
__all__ = [
|
|
51
|
+
"save_layered_pltz_bundle",
|
|
52
|
+
"load_layered_pltz_bundle",
|
|
53
|
+
"merge_layered_bundle",
|
|
54
|
+
"is_layered_bundle",
|
|
55
|
+
]
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def is_layered_bundle(bundle_dir: Path) -> bool:
|
|
59
|
+
"""Check if a bundle uses the new layered format."""
|
|
60
|
+
return (bundle_dir / "spec.json").exists()
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def save_layered_pltz_bundle(
|
|
64
|
+
fig,
|
|
65
|
+
bundle_dir: Path,
|
|
66
|
+
basename: str = "plot",
|
|
67
|
+
dpi: Optional[int] = None,
|
|
68
|
+
csv_df=None,
|
|
69
|
+
) -> None:
|
|
70
|
+
"""
|
|
71
|
+
Save matplotlib figure as layered .pltz bundle.
|
|
72
|
+
|
|
73
|
+
Parameters
|
|
74
|
+
----------
|
|
75
|
+
fig : matplotlib.figure.Figure
|
|
76
|
+
The figure to save.
|
|
77
|
+
bundle_dir : Path
|
|
78
|
+
Output directory (e.g., plot.pltz.d).
|
|
79
|
+
basename : str
|
|
80
|
+
Base filename for exports.
|
|
81
|
+
dpi : int, optional
|
|
82
|
+
DPI for raster exports. If None, uses get_default_dpi() from config.
|
|
83
|
+
csv_df : DataFrame, optional
|
|
84
|
+
Data to embed as CSV.
|
|
85
|
+
"""
|
|
86
|
+
# Resolve DPI from config if not specified
|
|
87
|
+
if dpi is None:
|
|
88
|
+
dpi = get_default_dpi()
|
|
89
|
+
import numpy as np
|
|
90
|
+
import tempfile
|
|
91
|
+
import warnings
|
|
92
|
+
from PIL import Image as PILImage
|
|
93
|
+
|
|
94
|
+
bundle_dir = Path(bundle_dir)
|
|
95
|
+
bundle_dir.mkdir(parents=True, exist_ok=True)
|
|
96
|
+
|
|
97
|
+
# Create subdirectories
|
|
98
|
+
exports_dir = bundle_dir / "exports"
|
|
99
|
+
cache_dir = bundle_dir / "cache"
|
|
100
|
+
exports_dir.mkdir(exist_ok=True)
|
|
101
|
+
cache_dir.mkdir(exist_ok=True)
|
|
102
|
+
|
|
103
|
+
# Extract figure dimensions
|
|
104
|
+
fig_width_inch, fig_height_inch = fig.get_size_inches()
|
|
105
|
+
|
|
106
|
+
# === Build PltzSpec (semantic) ===
|
|
107
|
+
axes_items = []
|
|
108
|
+
traces = []
|
|
109
|
+
extracted_data = {}
|
|
110
|
+
|
|
111
|
+
for ax_idx, ax in enumerate(fig.axes):
|
|
112
|
+
bbox = ax.get_position()
|
|
113
|
+
ax_id = f"ax{ax_idx}"
|
|
114
|
+
|
|
115
|
+
# Create axes item
|
|
116
|
+
ax_item = PltzAxesItem(
|
|
117
|
+
id=ax_id,
|
|
118
|
+
bbox=BboxRatio(
|
|
119
|
+
x0=round(bbox.x0, 4),
|
|
120
|
+
y0=round(bbox.y0, 4),
|
|
121
|
+
width=round(bbox.width, 4),
|
|
122
|
+
height=round(bbox.height, 4),
|
|
123
|
+
space="panel",
|
|
124
|
+
),
|
|
125
|
+
limits=PltzAxesLimits(
|
|
126
|
+
x=list(ax.get_xlim()),
|
|
127
|
+
y=list(ax.get_ylim()),
|
|
128
|
+
),
|
|
129
|
+
labels=PltzAxesLabels(
|
|
130
|
+
xlabel=ax.get_xlabel() or None,
|
|
131
|
+
ylabel=ax.get_ylabel() or None,
|
|
132
|
+
title=ax.get_title() or None,
|
|
133
|
+
),
|
|
134
|
+
)
|
|
135
|
+
axes_items.append(ax_item)
|
|
136
|
+
|
|
137
|
+
# Extract traces from lines
|
|
138
|
+
for line_idx, line in enumerate(ax.get_lines()):
|
|
139
|
+
label = line.get_label()
|
|
140
|
+
if label is None or label.startswith('_'):
|
|
141
|
+
label = f'series_{line_idx}'
|
|
142
|
+
|
|
143
|
+
trace_id = f"{ax_id}-line-{line_idx}"
|
|
144
|
+
xdata, ydata = line.get_data()
|
|
145
|
+
|
|
146
|
+
if len(xdata) > 0:
|
|
147
|
+
x_col = f"{ax_id}_trace-{trace_id}_x"
|
|
148
|
+
y_col = f"{ax_id}_trace-{trace_id}_y"
|
|
149
|
+
extracted_data[x_col] = np.array(xdata)
|
|
150
|
+
extracted_data[y_col] = np.array(ydata)
|
|
151
|
+
|
|
152
|
+
trace = PltzTraceSpec(
|
|
153
|
+
id=trace_id,
|
|
154
|
+
type="line",
|
|
155
|
+
axes_index=ax_idx,
|
|
156
|
+
x_col=x_col,
|
|
157
|
+
y_col=y_col,
|
|
158
|
+
label=label,
|
|
159
|
+
)
|
|
160
|
+
traces.append(trace)
|
|
161
|
+
|
|
162
|
+
# Handle CSV data - prefer extracted data (captures all matplotlib artists)
|
|
163
|
+
columns = []
|
|
164
|
+
csv_hash = None
|
|
165
|
+
if extracted_data:
|
|
166
|
+
# Use extracted data from matplotlib artists (captures axhline, etc.)
|
|
167
|
+
import pandas as pd
|
|
168
|
+
max_len = max(len(v) for v in extracted_data.values())
|
|
169
|
+
padded = {}
|
|
170
|
+
for k, v in extracted_data.items():
|
|
171
|
+
# Convert to float for NaN padding compatibility
|
|
172
|
+
v_float = np.array(v, dtype=float)
|
|
173
|
+
if len(v_float) < max_len:
|
|
174
|
+
padded[k] = np.pad(v_float, (0, max_len - len(v_float)), constant_values=np.nan)
|
|
175
|
+
else:
|
|
176
|
+
padded[k] = v_float
|
|
177
|
+
csv_df = pd.DataFrame(padded)
|
|
178
|
+
columns = list(csv_df.columns)
|
|
179
|
+
csv_str = csv_df.to_csv(index=False)
|
|
180
|
+
csv_hash = f"sha256:{hashlib.sha256(csv_str.encode()).hexdigest()[:16]}"
|
|
181
|
+
elif csv_df is not None:
|
|
182
|
+
# Fallback to provided CSV if no extracted data
|
|
183
|
+
columns = list(csv_df.columns)
|
|
184
|
+
csv_str = csv_df.to_csv(index=False)
|
|
185
|
+
csv_hash = f"sha256:{hashlib.sha256(csv_str.encode()).hexdigest()[:16]}"
|
|
186
|
+
|
|
187
|
+
# Create spec
|
|
188
|
+
spec = PltzSpec(
|
|
189
|
+
plot_id=basename,
|
|
190
|
+
data=PltzDataSource(
|
|
191
|
+
csv=f"{basename}.csv",
|
|
192
|
+
format="wide",
|
|
193
|
+
hash=csv_hash,
|
|
194
|
+
),
|
|
195
|
+
axes=axes_items,
|
|
196
|
+
traces=traces,
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
# === Build PltzStyle (appearance) ===
|
|
200
|
+
# Detect theme from figure
|
|
201
|
+
theme_mode = "light"
|
|
202
|
+
if hasattr(fig, '_scitex_theme'):
|
|
203
|
+
theme_mode = fig._scitex_theme
|
|
204
|
+
|
|
205
|
+
trace_styles = []
|
|
206
|
+
for ax_idx, ax in enumerate(fig.axes):
|
|
207
|
+
for line_idx, line in enumerate(ax.get_lines()):
|
|
208
|
+
label = line.get_label()
|
|
209
|
+
if label and not label.startswith('_'):
|
|
210
|
+
# Get line color
|
|
211
|
+
import matplotlib.colors as mcolors
|
|
212
|
+
color = line.get_color()
|
|
213
|
+
if isinstance(color, (list, tuple)):
|
|
214
|
+
color = mcolors.to_hex(color)
|
|
215
|
+
|
|
216
|
+
trace_id = f"ax{ax_idx}-line-{line_idx}"
|
|
217
|
+
trace_styles.append(PltzTraceStyle(
|
|
218
|
+
trace_id=trace_id,
|
|
219
|
+
color=color,
|
|
220
|
+
linewidth=line.get_linewidth(),
|
|
221
|
+
alpha=line.get_alpha(),
|
|
222
|
+
))
|
|
223
|
+
|
|
224
|
+
# Extract legend configuration from first axes with legend
|
|
225
|
+
legend_spec = PltzLegendSpec(visible=True, location="best")
|
|
226
|
+
for ax in fig.axes:
|
|
227
|
+
legend = ax.get_legend()
|
|
228
|
+
if legend is not None:
|
|
229
|
+
# Extract legend location
|
|
230
|
+
# matplotlib legend._loc can be int or string
|
|
231
|
+
loc = legend._loc
|
|
232
|
+
loc_map = {
|
|
233
|
+
0: "best", 1: "upper right", 2: "upper left", 3: "lower left",
|
|
234
|
+
4: "lower right", 5: "right", 6: "center left", 7: "center right",
|
|
235
|
+
8: "lower center", 9: "upper center", 10: "center",
|
|
236
|
+
}
|
|
237
|
+
if isinstance(loc, int):
|
|
238
|
+
location = loc_map.get(loc, "best")
|
|
239
|
+
else:
|
|
240
|
+
location = str(loc) if loc else "best"
|
|
241
|
+
|
|
242
|
+
# If location is "best", determine actual position from rendered bbox
|
|
243
|
+
if location == "best":
|
|
244
|
+
try:
|
|
245
|
+
# Get the actual rendered position
|
|
246
|
+
bbox = legend.get_window_extent(fig.canvas.get_renderer())
|
|
247
|
+
ax_bbox = ax.get_position()
|
|
248
|
+
fig_width, fig_height = fig.get_size_inches() * fig.dpi
|
|
249
|
+
|
|
250
|
+
# Calculate legend center relative to axes
|
|
251
|
+
legend_center_x = (bbox.x0 + bbox.x1) / 2
|
|
252
|
+
legend_center_y = (bbox.y0 + bbox.y1) / 2
|
|
253
|
+
ax_center_x = (ax_bbox.x0 + ax_bbox.x1) / 2 * fig_width
|
|
254
|
+
ax_center_y = (ax_bbox.y0 + ax_bbox.y1) / 2 * fig_height
|
|
255
|
+
|
|
256
|
+
# Determine quadrant
|
|
257
|
+
is_right = legend_center_x > ax_center_x
|
|
258
|
+
is_upper = legend_center_y > ax_center_y
|
|
259
|
+
|
|
260
|
+
if is_upper and is_right:
|
|
261
|
+
location = "upper right"
|
|
262
|
+
elif is_upper and not is_right:
|
|
263
|
+
location = "upper left"
|
|
264
|
+
elif not is_upper and is_right:
|
|
265
|
+
location = "lower right"
|
|
266
|
+
else:
|
|
267
|
+
location = "lower left"
|
|
268
|
+
except Exception:
|
|
269
|
+
pass # Keep "best" if we can't determine
|
|
270
|
+
|
|
271
|
+
# Extract other legend properties
|
|
272
|
+
legend_spec = PltzLegendSpec(
|
|
273
|
+
visible=legend.get_visible(),
|
|
274
|
+
location=location,
|
|
275
|
+
frameon=legend.get_frame_on(),
|
|
276
|
+
fontsize=legend._fontsize if hasattr(legend, '_fontsize') else None,
|
|
277
|
+
ncols=legend._ncols if hasattr(legend, '_ncols') else 1,
|
|
278
|
+
title=legend.get_title().get_text() if legend.get_title() else None,
|
|
279
|
+
)
|
|
280
|
+
break # Use first legend found
|
|
281
|
+
|
|
282
|
+
style = PltzStyle(
|
|
283
|
+
theme=PltzTheme(
|
|
284
|
+
mode=theme_mode,
|
|
285
|
+
colors={
|
|
286
|
+
"background": "transparent",
|
|
287
|
+
"axes_bg": "white" if theme_mode == "light" else "transparent",
|
|
288
|
+
"text": "black" if theme_mode == "light" else "#e8e8e8",
|
|
289
|
+
"spine": "black" if theme_mode == "light" else "#e8e8e8",
|
|
290
|
+
"tick": "black" if theme_mode == "light" else "#e8e8e8",
|
|
291
|
+
},
|
|
292
|
+
),
|
|
293
|
+
size=PltzSize(
|
|
294
|
+
width_mm=round(fig_width_inch * 25.4, 1),
|
|
295
|
+
height_mm=round(fig_height_inch * 25.4, 1),
|
|
296
|
+
),
|
|
297
|
+
font=PltzFont(family="sans-serif", size_pt=8.0),
|
|
298
|
+
traces=trace_styles,
|
|
299
|
+
legend=legend_spec,
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
# === Save exports and track coordinate transformations ===
|
|
303
|
+
#
|
|
304
|
+
# Cropping Pipeline:
|
|
305
|
+
#
|
|
306
|
+
# ┌──────────────────────────────┐
|
|
307
|
+
# │ original_figure_size_px │ ← matplotlib figure canvas
|
|
308
|
+
# │ ┌────────────────────────┐ │ (where bbox_px are measured)
|
|
309
|
+
# │ │ tight_bbox │ │
|
|
310
|
+
# │ │ ┌──────────────────┐ │ │
|
|
311
|
+
# │ │ │ final_image_px │ │ │ ← exported PNG (what user sees)
|
|
312
|
+
# │ │ └──────────────────┘ │ │
|
|
313
|
+
# │ └────────────────────────┘ │
|
|
314
|
+
# └──────────────────────────────┘
|
|
315
|
+
#
|
|
316
|
+
# To convert: final_coord = original_coord - total_crop_offset
|
|
317
|
+
#
|
|
318
|
+
# IMPORTANT: Use fig.dpi for coordinate calculations, NOT export dpi
|
|
319
|
+
# extract_selectable_regions uses fig.dpi for all bbox calculations
|
|
320
|
+
fig_dpi = fig.dpi
|
|
321
|
+
display_fig_size_px = [
|
|
322
|
+
int(fig.get_figwidth() * fig_dpi),
|
|
323
|
+
int(fig.get_figheight() * fig_dpi),
|
|
324
|
+
]
|
|
325
|
+
|
|
326
|
+
# Get matplotlib's tight bounding box (what bbox_inches='tight' crops to)
|
|
327
|
+
# Note: get_tightbbox returns values in INCHES, not pixels
|
|
328
|
+
fig.canvas.draw() # Ensure renderer is ready
|
|
329
|
+
renderer = fig.canvas.get_renderer()
|
|
330
|
+
tight_bbox_inches = fig.get_tightbbox(renderer)
|
|
331
|
+
|
|
332
|
+
# Convert from inches to display pixels (using fig.dpi, NOT export dpi)
|
|
333
|
+
tight_bbox_display_px = {
|
|
334
|
+
"x0": tight_bbox_inches.x0 * fig_dpi,
|
|
335
|
+
"y0": tight_bbox_inches.y0 * fig_dpi,
|
|
336
|
+
"x1": tight_bbox_inches.x1 * fig_dpi,
|
|
337
|
+
"y1": tight_bbox_inches.y1 * fig_dpi,
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
# Convert from matplotlib display coords (y=0 at bottom)
|
|
341
|
+
# to image coords (y=0 at top)
|
|
342
|
+
tight_bbox_in_image_coords = {
|
|
343
|
+
"left": tight_bbox_display_px["x0"],
|
|
344
|
+
"upper": display_fig_size_px[1] - tight_bbox_display_px["y1"], # Flip y
|
|
345
|
+
"right": tight_bbox_display_px["x1"],
|
|
346
|
+
"lower": display_fig_size_px[1] - tight_bbox_display_px["y0"],
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
# Scale factor: export_dpi / fig_dpi (to scale from display coords to export PNG coords)
|
|
350
|
+
dpi_scale = dpi / fig_dpi
|
|
351
|
+
|
|
352
|
+
with tempfile.TemporaryDirectory() as tmp_dir:
|
|
353
|
+
tmp_path = Path(tmp_dir)
|
|
354
|
+
|
|
355
|
+
with warnings.catch_warnings():
|
|
356
|
+
warnings.filterwarnings('ignore', message='.*tight_layout.*')
|
|
357
|
+
|
|
358
|
+
# Save PNG at full figure size (crop function will handle tight cropping)
|
|
359
|
+
# This ensures coordinate transformations are accurate
|
|
360
|
+
png_path = exports_dir / f"{basename}.png"
|
|
361
|
+
fig.savefig(png_path, dpi=dpi, format='png', transparent=True)
|
|
362
|
+
|
|
363
|
+
# Save SVG with tight bbox (separate concern, no coord issues)
|
|
364
|
+
svg_path = exports_dir / f"{basename}.svg"
|
|
365
|
+
fig.savefig(svg_path, bbox_inches='tight', format='svg')
|
|
366
|
+
|
|
367
|
+
# Generate hitmap
|
|
368
|
+
from scitex.plt.utils._hitmap import (
|
|
369
|
+
apply_hitmap_colors, restore_original_colors,
|
|
370
|
+
extract_path_data, extract_selectable_regions,
|
|
371
|
+
HITMAP_BACKGROUND_COLOR, HITMAP_AXES_COLOR
|
|
372
|
+
)
|
|
373
|
+
|
|
374
|
+
original_props, color_map, groups = apply_hitmap_colors(fig)
|
|
375
|
+
|
|
376
|
+
# Store and set hitmap colors for hitmap generation
|
|
377
|
+
saved_fig_facecolor = fig.patch.get_facecolor()
|
|
378
|
+
saved_ax_facecolors = []
|
|
379
|
+
for ax in fig.axes:
|
|
380
|
+
saved_ax_facecolors.append(ax.get_facecolor())
|
|
381
|
+
ax.set_facecolor(HITMAP_BACKGROUND_COLOR)
|
|
382
|
+
for spine in ax.spines.values():
|
|
383
|
+
spine.set_color(HITMAP_AXES_COLOR)
|
|
384
|
+
|
|
385
|
+
fig.patch.set_facecolor(HITMAP_BACKGROUND_COLOR)
|
|
386
|
+
|
|
387
|
+
# Save hitmap at full figure size (will crop with same box as main PNG)
|
|
388
|
+
hitmap_path = exports_dir / f"{basename}_hitmap.png"
|
|
389
|
+
fig.savefig(hitmap_path, dpi=dpi, format='png',
|
|
390
|
+
facecolor=HITMAP_BACKGROUND_COLOR)
|
|
391
|
+
|
|
392
|
+
# Restore colors
|
|
393
|
+
restore_original_colors(original_props)
|
|
394
|
+
fig.patch.set_facecolor(saved_fig_facecolor)
|
|
395
|
+
for i, ax in enumerate(fig.axes):
|
|
396
|
+
ax.set_facecolor(saved_ax_facecolors[i])
|
|
397
|
+
|
|
398
|
+
# Apply additional margin cropping (removes transparent edges)
|
|
399
|
+
margin_crop_box = None
|
|
400
|
+
try:
|
|
401
|
+
from scitex.plt.utils._crop import crop
|
|
402
|
+
|
|
403
|
+
_, margin_crop_box = crop(
|
|
404
|
+
str(png_path), output_path=str(png_path),
|
|
405
|
+
overwrite=True, margin=12, verbose=False, return_offset=True,
|
|
406
|
+
)
|
|
407
|
+
|
|
408
|
+
crop(str(hitmap_path), output_path=str(hitmap_path),
|
|
409
|
+
overwrite=True, crop_box=(
|
|
410
|
+
margin_crop_box['left'],
|
|
411
|
+
margin_crop_box['upper'],
|
|
412
|
+
margin_crop_box['right'],
|
|
413
|
+
margin_crop_box['lower'],
|
|
414
|
+
), verbose=False)
|
|
415
|
+
except Exception as e:
|
|
416
|
+
logger.debug(f"Crop failed: {e}")
|
|
417
|
+
|
|
418
|
+
# === Coordinate transformation pipeline ===
|
|
419
|
+
#
|
|
420
|
+
# Strategy: Since we save at full figure size (no bbox_inches='tight'),
|
|
421
|
+
# the crop function's crop_box IS the total offset from original figure to final PNG.
|
|
422
|
+
#
|
|
423
|
+
# We extract coordinates at export DPI so they match the saved PNG directly.
|
|
424
|
+
|
|
425
|
+
# Temporarily set fig.dpi to export DPI for coordinate extraction
|
|
426
|
+
saved_fig_dpi = fig.dpi
|
|
427
|
+
fig.set_dpi(dpi)
|
|
428
|
+
fig.canvas.draw() # Redraw at new DPI
|
|
429
|
+
|
|
430
|
+
# crop_box is the total offset from full figure to final PNG
|
|
431
|
+
# (crop_box['left'], crop_box['upper']) = top-left corner of crop region
|
|
432
|
+
total_offset_left = margin_crop_box["left"] if margin_crop_box else 0
|
|
433
|
+
total_offset_upper = margin_crop_box["upper"] if margin_crop_box else 0
|
|
434
|
+
|
|
435
|
+
# === Build PltzGeometry (cache) ===
|
|
436
|
+
# Extract at export DPI so coords are in full figure space (matches saved PNG before crop)
|
|
437
|
+
path_data = extract_path_data(fig)
|
|
438
|
+
selectable_regions = extract_selectable_regions(fig)
|
|
439
|
+
|
|
440
|
+
# Restore original DPI
|
|
441
|
+
fig.set_dpi(saved_fig_dpi)
|
|
442
|
+
|
|
443
|
+
# Get final image size (what user sees)
|
|
444
|
+
with PILImage.open(png_path) as img:
|
|
445
|
+
final_image_size_px = list(img.size)
|
|
446
|
+
|
|
447
|
+
# Adjust coordinates: subtract total offset (both tight_bbox and margin_crop)
|
|
448
|
+
# No DPI scaling needed since we extracted at export DPI
|
|
449
|
+
selectable_regions = _adjust_coords_for_offset(
|
|
450
|
+
selectable_regions, total_offset_left, total_offset_upper
|
|
451
|
+
)
|
|
452
|
+
path_data = _adjust_path_data_for_offset(
|
|
453
|
+
path_data, total_offset_left, total_offset_upper
|
|
454
|
+
)
|
|
455
|
+
|
|
456
|
+
rendered_axes = []
|
|
457
|
+
for ax_idx, ax_data in enumerate(path_data.get("axes", [])):
|
|
458
|
+
bbox_data = ax_data.get("bbox_px", {})
|
|
459
|
+
rendered_axes.append(PltzRenderedAxes(
|
|
460
|
+
id=f"ax{ax_idx}",
|
|
461
|
+
xlim=ax_data.get("xlim", [0, 1]),
|
|
462
|
+
ylim=ax_data.get("ylim", [0, 1]),
|
|
463
|
+
bbox_px=BboxPx(
|
|
464
|
+
x0=bbox_data.get("x0", 0),
|
|
465
|
+
y0=bbox_data.get("y0", 0),
|
|
466
|
+
width=bbox_data.get("width", bbox_data.get("x1", 0) - bbox_data.get("x0", 0)),
|
|
467
|
+
height=bbox_data.get("height", bbox_data.get("y1", 0) - bbox_data.get("y0", 0)),
|
|
468
|
+
),
|
|
469
|
+
))
|
|
470
|
+
|
|
471
|
+
rendered_artists = []
|
|
472
|
+
for artist in path_data.get("artists", []):
|
|
473
|
+
bbox_data = artist.get("bbox_px", {})
|
|
474
|
+
rendered_artists.append(PltzRenderedArtist(
|
|
475
|
+
id=str(artist.get("id", "")),
|
|
476
|
+
type=artist.get("type", "unknown"),
|
|
477
|
+
axes_index=artist.get("axes_index", 0),
|
|
478
|
+
bbox_px=BboxPx(
|
|
479
|
+
x0=bbox_data.get("x0", 0),
|
|
480
|
+
y0=bbox_data.get("y0", 0),
|
|
481
|
+
width=bbox_data.get("width", bbox_data.get("x1", 0) - bbox_data.get("x0", 0)),
|
|
482
|
+
height=bbox_data.get("height", bbox_data.get("y1", 0) - bbox_data.get("y0", 0)),
|
|
483
|
+
) if bbox_data else None,
|
|
484
|
+
path_px=artist.get("path_px"),
|
|
485
|
+
))
|
|
486
|
+
|
|
487
|
+
geometry = PltzGeometry(
|
|
488
|
+
source_hash=csv_hash or "",
|
|
489
|
+
figure_px=final_image_size_px, # Final cropped image size
|
|
490
|
+
dpi=dpi, # Export DPI (stored for consumers)
|
|
491
|
+
axes=rendered_axes,
|
|
492
|
+
artists=rendered_artists,
|
|
493
|
+
hit_regions={
|
|
494
|
+
"strategy": "hybrid",
|
|
495
|
+
"hit_map": f"{basename}_hitmap.png",
|
|
496
|
+
"color_map": {str(k): v for k, v in color_map.items()},
|
|
497
|
+
"groups": groups,
|
|
498
|
+
# Store DPI info for consumers that need to retrieve from data
|
|
499
|
+
"fig_dpi": fig_dpi, # Original matplotlib fig.dpi
|
|
500
|
+
"export_dpi": dpi, # Export DPI used for PNG
|
|
501
|
+
"dpi_scale": dpi_scale, # export_dpi / fig_dpi
|
|
502
|
+
},
|
|
503
|
+
selectable_regions=selectable_regions,
|
|
504
|
+
# Note: crop_box is now None because all coordinates are already adjusted
|
|
505
|
+
# to final_image space (no further transformation needed by consumers)
|
|
506
|
+
crop_box=None,
|
|
507
|
+
)
|
|
508
|
+
|
|
509
|
+
# === Save all JSON files ===
|
|
510
|
+
# spec.json
|
|
511
|
+
spec_path = bundle_dir / "spec.json"
|
|
512
|
+
with open(spec_path, "w") as f:
|
|
513
|
+
json.dump({
|
|
514
|
+
"schema": {"name": "scitex.plt.spec", "version": PLOT_SPEC_VERSION},
|
|
515
|
+
**asdict(spec),
|
|
516
|
+
}, f, indent=2, default=str)
|
|
517
|
+
|
|
518
|
+
# style.json
|
|
519
|
+
style_path = bundle_dir / "style.json"
|
|
520
|
+
with open(style_path, "w") as f:
|
|
521
|
+
json.dump({
|
|
522
|
+
"schema": {"name": "scitex.plt.style", "version": PLOT_STYLE_VERSION},
|
|
523
|
+
**asdict(style),
|
|
524
|
+
}, f, indent=2, default=str)
|
|
525
|
+
|
|
526
|
+
# cache/geometry_px.json
|
|
527
|
+
geometry_path = cache_dir / "geometry_px.json"
|
|
528
|
+
with open(geometry_path, "w") as f:
|
|
529
|
+
json.dump({
|
|
530
|
+
"schema": {"name": "scitex.plt.geometry", "version": PLOT_GEOMETRY_VERSION},
|
|
531
|
+
"_comment": "CACHE - can be deleted and regenerated from spec + style",
|
|
532
|
+
**asdict(geometry),
|
|
533
|
+
}, f, indent=2, default=str)
|
|
534
|
+
|
|
535
|
+
# cache/render_manifest.json
|
|
536
|
+
spec_hash = hashlib.sha256(open(spec_path, "rb").read()).hexdigest()[:16]
|
|
537
|
+
style_hash = hashlib.sha256(open(style_path, "rb").read()).hexdigest()[:16]
|
|
538
|
+
manifest = PltzRenderManifest(
|
|
539
|
+
source_hash=f"{spec_hash}:{style_hash}",
|
|
540
|
+
panel_size_mm=[round(fig_width_inch * 25.4, 1), round(fig_height_inch * 25.4, 1)],
|
|
541
|
+
dpi=dpi,
|
|
542
|
+
render_px=final_image_size_px,
|
|
543
|
+
overview_png=f"exports/{basename}.png",
|
|
544
|
+
overview_svg=f"exports/{basename}.svg",
|
|
545
|
+
hitmap_png=f"exports/{basename}_hitmap.png",
|
|
546
|
+
)
|
|
547
|
+
manifest_path = cache_dir / "render_manifest.json"
|
|
548
|
+
with open(manifest_path, "w") as f:
|
|
549
|
+
json.dump({
|
|
550
|
+
"schema": {"name": "scitex.plt.render_manifest", "version": PLOT_GEOMETRY_VERSION},
|
|
551
|
+
**asdict(manifest),
|
|
552
|
+
}, f, indent=2, default=str)
|
|
553
|
+
|
|
554
|
+
# Save CSV
|
|
555
|
+
if csv_df is not None:
|
|
556
|
+
csv_path = bundle_dir / f"{basename}.csv"
|
|
557
|
+
csv_df.to_csv(csv_path, index=False)
|
|
558
|
+
|
|
559
|
+
# Generate overview showing main image and hitmap side by side
|
|
560
|
+
_generate_pltz_overview(exports_dir, basename)
|
|
561
|
+
|
|
562
|
+
# Generate dynamic README.md
|
|
563
|
+
_generate_pltz_readme(bundle_dir, basename, spec, style, geometry, manifest)
|
|
564
|
+
|
|
565
|
+
logger.debug(f"Saved layered pltz bundle: {bundle_dir}")
|
|
566
|
+
|
|
567
|
+
|
|
568
|
+
def _generate_pltz_overview(exports_dir: Path, basename: str) -> None:
|
|
569
|
+
"""Generate comprehensive overview with plot, hitmap, overlay, bboxes, and JSON info.
|
|
570
|
+
|
|
571
|
+
Args:
|
|
572
|
+
exports_dir: Path to exports directory.
|
|
573
|
+
basename: Base filename for the bundle.
|
|
574
|
+
"""
|
|
575
|
+
import matplotlib.pyplot as plt
|
|
576
|
+
import matplotlib.patches as patches
|
|
577
|
+
from PIL import Image
|
|
578
|
+
import warnings
|
|
579
|
+
import numpy as np
|
|
580
|
+
|
|
581
|
+
bundle_dir = exports_dir.parent
|
|
582
|
+
png_path = exports_dir / f"{basename}.png"
|
|
583
|
+
hitmap_path = exports_dir / f"{basename}_hitmap.png"
|
|
584
|
+
|
|
585
|
+
if not png_path.exists():
|
|
586
|
+
return
|
|
587
|
+
|
|
588
|
+
try:
|
|
589
|
+
main_img = Image.open(png_path)
|
|
590
|
+
img_width, img_height = main_img.size
|
|
591
|
+
has_hitmap = hitmap_path.exists()
|
|
592
|
+
|
|
593
|
+
# Load JSON files for displaying info
|
|
594
|
+
spec_data = {}
|
|
595
|
+
style_data = {}
|
|
596
|
+
geometry_data = {}
|
|
597
|
+
manifest_data = {}
|
|
598
|
+
|
|
599
|
+
spec_path = bundle_dir / "spec.json"
|
|
600
|
+
style_path = bundle_dir / "style.json"
|
|
601
|
+
geometry_path = bundle_dir / "cache" / "geometry_px.json"
|
|
602
|
+
manifest_path = bundle_dir / "cache" / "render_manifest.json"
|
|
603
|
+
|
|
604
|
+
if spec_path.exists():
|
|
605
|
+
with open(spec_path, "r") as f:
|
|
606
|
+
spec_data = json.load(f)
|
|
607
|
+
if style_path.exists():
|
|
608
|
+
with open(style_path, "r") as f:
|
|
609
|
+
style_data = json.load(f)
|
|
610
|
+
if geometry_path.exists():
|
|
611
|
+
with open(geometry_path, "r") as f:
|
|
612
|
+
geometry_data = json.load(f)
|
|
613
|
+
if manifest_path.exists():
|
|
614
|
+
with open(manifest_path, "r") as f:
|
|
615
|
+
manifest_data = json.load(f)
|
|
616
|
+
|
|
617
|
+
# Get DPI and panel size for mm scaler
|
|
618
|
+
dpi = manifest_data.get("dpi", get_default_dpi())
|
|
619
|
+
panel_size_mm = manifest_data.get("panel_size_mm", [80, 68])
|
|
620
|
+
|
|
621
|
+
# Create figure with 2 rows, 3 columns layout
|
|
622
|
+
# Row 1: Plot | Hitmap | Overlay
|
|
623
|
+
# Row 2: Bboxes | JSON Info | mm Scaler
|
|
624
|
+
fig = plt.figure(figsize=(18, 12), facecolor="white")
|
|
625
|
+
gs = fig.add_gridspec(2, 3, hspace=0.3, wspace=0.2)
|
|
626
|
+
|
|
627
|
+
# === Row 1: Images ===
|
|
628
|
+
# 1. Main Plot
|
|
629
|
+
ax_plot = fig.add_subplot(gs[0, 0])
|
|
630
|
+
ax_plot.set_title("Plot", fontweight="bold", fontsize=11)
|
|
631
|
+
ax_plot.imshow(main_img)
|
|
632
|
+
ax_plot.axis("off")
|
|
633
|
+
|
|
634
|
+
# 2. Hitmap with ID labels
|
|
635
|
+
ax_hitmap = fig.add_subplot(gs[0, 1])
|
|
636
|
+
ax_hitmap.set_title("Hit Regions", fontweight="bold", fontsize=11)
|
|
637
|
+
if has_hitmap:
|
|
638
|
+
hitmap_img = Image.open(hitmap_path)
|
|
639
|
+
ax_hitmap.imshow(hitmap_img)
|
|
640
|
+
|
|
641
|
+
# Add ID labels from hit_regions color_map
|
|
642
|
+
color_map = geometry_data.get("hit_regions", {}).get("color_map", {})
|
|
643
|
+
artists = geometry_data.get("artists", [])
|
|
644
|
+
|
|
645
|
+
# Note: bbox_px coordinates are already in final image space
|
|
646
|
+
for idx, artist in enumerate(artists):
|
|
647
|
+
bbox = artist.get("bbox_px", {})
|
|
648
|
+
if bbox:
|
|
649
|
+
# Get center of bbox for label placement
|
|
650
|
+
x0 = bbox.get("x0", 0)
|
|
651
|
+
y0 = bbox.get("y0", 0)
|
|
652
|
+
width = bbox.get("width", 0)
|
|
653
|
+
height = bbox.get("height", 0)
|
|
654
|
+
cx, cy = x0 + width / 2, y0 + height / 2
|
|
655
|
+
|
|
656
|
+
# Find label from color_map (color_map IDs are 1-indexed)
|
|
657
|
+
color_map_id = str(idx + 1)
|
|
658
|
+
label = f"artist_{idx}"
|
|
659
|
+
if color_map_id in color_map:
|
|
660
|
+
label = color_map[color_map_id].get("label", label)
|
|
661
|
+
|
|
662
|
+
ax_hitmap.text(cx, cy, label, fontsize=8, ha="center", va="center",
|
|
663
|
+
color="white", fontweight="bold",
|
|
664
|
+
bbox=dict(boxstyle="round,pad=0.2", facecolor="black", alpha=0.7))
|
|
665
|
+
else:
|
|
666
|
+
ax_hitmap.text(0.5, 0.5, "No hitmap", ha="center", va="center",
|
|
667
|
+
transform=ax_hitmap.transAxes)
|
|
668
|
+
ax_hitmap.axis("off")
|
|
669
|
+
|
|
670
|
+
# 3. Overlay (plot + hitmap with transparency)
|
|
671
|
+
ax_overlay = fig.add_subplot(gs[0, 2])
|
|
672
|
+
ax_overlay.set_title("Overlay (Plot + Hit)", fontweight="bold", fontsize=11)
|
|
673
|
+
ax_overlay.imshow(main_img)
|
|
674
|
+
if has_hitmap:
|
|
675
|
+
hitmap_img = Image.open(hitmap_path).convert("RGBA")
|
|
676
|
+
hitmap_array = np.array(hitmap_img)
|
|
677
|
+
# Create semi-transparent overlay
|
|
678
|
+
hitmap_array[:, :, 3] = (hitmap_array[:, :, 3] * 0.5).astype(np.uint8)
|
|
679
|
+
ax_overlay.imshow(hitmap_array, alpha=0.5)
|
|
680
|
+
ax_overlay.axis("off")
|
|
681
|
+
|
|
682
|
+
# === Row 2: Details ===
|
|
683
|
+
# 4. Bboxes visualization
|
|
684
|
+
ax_bboxes = fig.add_subplot(gs[1, 0])
|
|
685
|
+
ax_bboxes.set_title("Element Bboxes", fontweight="bold", fontsize=11)
|
|
686
|
+
ax_bboxes.imshow(main_img)
|
|
687
|
+
|
|
688
|
+
# Note: bbox_px coordinates are already in final image space
|
|
689
|
+
# (adjusted during save_layered_pltz_bundle), so no offset needed
|
|
690
|
+
|
|
691
|
+
# Draw bboxes from geometry
|
|
692
|
+
colors = ["red", "blue", "green", "orange", "purple", "cyan"]
|
|
693
|
+
selectable = geometry_data.get("selectable_regions", {})
|
|
694
|
+
|
|
695
|
+
for ax_idx, ax_region in enumerate(selectable.get("axes", [])):
|
|
696
|
+
color = colors[ax_idx % len(colors)]
|
|
697
|
+
|
|
698
|
+
# Title bbox
|
|
699
|
+
if "title" in ax_region:
|
|
700
|
+
bbox = ax_region["title"].get("bbox_px", [])
|
|
701
|
+
if len(bbox) == 4:
|
|
702
|
+
_draw_bbox(ax_bboxes, bbox, color, "title")
|
|
703
|
+
|
|
704
|
+
# xlabel bbox
|
|
705
|
+
if "xlabel" in ax_region:
|
|
706
|
+
bbox = ax_region["xlabel"].get("bbox_px", [])
|
|
707
|
+
if len(bbox) == 4:
|
|
708
|
+
_draw_bbox(ax_bboxes, bbox, color, "xlabel")
|
|
709
|
+
|
|
710
|
+
# ylabel bbox
|
|
711
|
+
if "ylabel" in ax_region:
|
|
712
|
+
bbox = ax_region["ylabel"].get("bbox_px", [])
|
|
713
|
+
if len(bbox) == 4:
|
|
714
|
+
_draw_bbox(ax_bboxes, bbox, color, "ylabel")
|
|
715
|
+
|
|
716
|
+
# xaxis spine
|
|
717
|
+
if "xaxis" in ax_region and "spine" in ax_region["xaxis"]:
|
|
718
|
+
bbox = ax_region["xaxis"]["spine"].get("bbox_px", [])
|
|
719
|
+
if len(bbox) == 4:
|
|
720
|
+
_draw_bbox(ax_bboxes, bbox, "gray", "xaxis", lw=1)
|
|
721
|
+
|
|
722
|
+
# yaxis spine
|
|
723
|
+
if "yaxis" in ax_region and "spine" in ax_region["yaxis"]:
|
|
724
|
+
bbox = ax_region["yaxis"]["spine"].get("bbox_px", [])
|
|
725
|
+
if len(bbox) == 4:
|
|
726
|
+
_draw_bbox(ax_bboxes, bbox, "gray", "yaxis", lw=1)
|
|
727
|
+
|
|
728
|
+
# legend bbox
|
|
729
|
+
if "legend" in ax_region:
|
|
730
|
+
bbox = ax_region["legend"].get("bbox_px", [])
|
|
731
|
+
if len(bbox) == 4:
|
|
732
|
+
_draw_bbox(ax_bboxes, bbox, "magenta", "legend")
|
|
733
|
+
|
|
734
|
+
ax_bboxes.axis("off")
|
|
735
|
+
|
|
736
|
+
# 5. JSON Info
|
|
737
|
+
ax_json = fig.add_subplot(gs[1, 1])
|
|
738
|
+
ax_json.set_title("Bundle Info (depth=2)", fontweight="bold", fontsize=11)
|
|
739
|
+
ax_json.axis("off")
|
|
740
|
+
|
|
741
|
+
# Format JSON summary with limited depth
|
|
742
|
+
json_text = _format_json_summary(
|
|
743
|
+
{"spec": spec_data, "style": style_data},
|
|
744
|
+
max_depth=2
|
|
745
|
+
)
|
|
746
|
+
ax_json.text(0.02, 0.98, json_text, transform=ax_json.transAxes,
|
|
747
|
+
fontsize=7, fontfamily="monospace", verticalalignment="top",
|
|
748
|
+
bbox=dict(boxstyle="round", facecolor="wheat", alpha=0.5))
|
|
749
|
+
|
|
750
|
+
# 6. mm Scaler
|
|
751
|
+
ax_scale = fig.add_subplot(gs[1, 2])
|
|
752
|
+
ax_scale.set_title("Size & Scale (mm)", fontweight="bold", fontsize=11)
|
|
753
|
+
|
|
754
|
+
# Show the image with mm scale bars
|
|
755
|
+
ax_scale.imshow(main_img, extent=[0, panel_size_mm[0], panel_size_mm[1], 0])
|
|
756
|
+
|
|
757
|
+
# Add grid lines every 10mm
|
|
758
|
+
for x in range(0, int(panel_size_mm[0]) + 1, 10):
|
|
759
|
+
ax_scale.axvline(x, color='gray', linewidth=0.5, alpha=0.5)
|
|
760
|
+
if x > 0:
|
|
761
|
+
ax_scale.text(x, -1, f"{x}", ha="center", fontsize=7)
|
|
762
|
+
for y in range(0, int(panel_size_mm[1]) + 1, 10):
|
|
763
|
+
ax_scale.axhline(y, color='gray', linewidth=0.5, alpha=0.5)
|
|
764
|
+
if y > 0:
|
|
765
|
+
ax_scale.text(-1, y, f"{y}", ha="right", va="center", fontsize=7)
|
|
766
|
+
|
|
767
|
+
ax_scale.set_xlabel("mm", fontsize=9)
|
|
768
|
+
ax_scale.set_ylabel("mm", fontsize=9)
|
|
769
|
+
ax_scale.set_xlim(-3, panel_size_mm[0] + 1)
|
|
770
|
+
ax_scale.set_ylim(panel_size_mm[1] + 1, -3)
|
|
771
|
+
|
|
772
|
+
# Add size text
|
|
773
|
+
size_text = f"Panel: {panel_size_mm[0]:.1f} × {panel_size_mm[1]:.1f} mm\nDPI: {dpi}\nPixels: {img_width} × {img_height}"
|
|
774
|
+
ax_scale.text(panel_size_mm[0] * 0.95, panel_size_mm[1] * 0.95, size_text,
|
|
775
|
+
ha="right", va="bottom", fontsize=8,
|
|
776
|
+
bbox=dict(boxstyle="round", facecolor="white", alpha=0.8))
|
|
777
|
+
|
|
778
|
+
fig.suptitle(f"Overview: {basename}", fontsize=14, fontweight="bold", y=0.98)
|
|
779
|
+
|
|
780
|
+
overview_path = exports_dir / f"{basename}_overview.png"
|
|
781
|
+
with warnings.catch_warnings():
|
|
782
|
+
warnings.filterwarnings("ignore", message=".*tight_layout.*")
|
|
783
|
+
fig.savefig(overview_path, dpi=get_preview_dpi(), bbox_inches="tight", facecolor="white")
|
|
784
|
+
plt.close(fig)
|
|
785
|
+
|
|
786
|
+
except Exception as e:
|
|
787
|
+
logger.debug(f"Could not generate pltz overview: {e}")
|
|
788
|
+
import traceback
|
|
789
|
+
logger.debug(traceback.format_exc())
|
|
790
|
+
|
|
791
|
+
|
|
792
|
+
def _generate_pltz_readme(
|
|
793
|
+
bundle_dir: Path,
|
|
794
|
+
basename: str,
|
|
795
|
+
spec: "PltzSpec",
|
|
796
|
+
style: "PltzStyle",
|
|
797
|
+
geometry: "PltzGeometry",
|
|
798
|
+
manifest: "PltzRenderManifest",
|
|
799
|
+
) -> None:
|
|
800
|
+
"""Generate a dynamic README.md describing the bundle.
|
|
801
|
+
|
|
802
|
+
Parameters
|
|
803
|
+
----------
|
|
804
|
+
bundle_dir : Path
|
|
805
|
+
Path to the bundle directory.
|
|
806
|
+
basename : str
|
|
807
|
+
Base filename for the bundle.
|
|
808
|
+
spec : PltzSpec
|
|
809
|
+
The plot specification.
|
|
810
|
+
style : PltzStyle
|
|
811
|
+
The plot style.
|
|
812
|
+
geometry : PltzGeometry
|
|
813
|
+
The rendered geometry.
|
|
814
|
+
manifest : PltzRenderManifest
|
|
815
|
+
The render manifest.
|
|
816
|
+
"""
|
|
817
|
+
from datetime import datetime
|
|
818
|
+
|
|
819
|
+
# Count elements
|
|
820
|
+
n_axes = len(spec.axes) if spec.axes else 0
|
|
821
|
+
n_traces = len(spec.traces) if spec.traces else 0
|
|
822
|
+
|
|
823
|
+
# Get size info
|
|
824
|
+
width_mm = style.size.width_mm if style.size else 0
|
|
825
|
+
height_mm = style.size.height_mm if style.size else 0
|
|
826
|
+
dpi = manifest.dpi
|
|
827
|
+
render_px = manifest.render_px
|
|
828
|
+
|
|
829
|
+
readme_content = f"""# {basename}.pltz.d
|
|
830
|
+
|
|
831
|
+
> SciTeX Layered Plot Bundle - Auto-generated README
|
|
832
|
+
|
|
833
|
+
## Overview
|
|
834
|
+
|
|
835
|
+

|
|
836
|
+
|
|
837
|
+
## Bundle Structure
|
|
838
|
+
|
|
839
|
+
```
|
|
840
|
+
{basename}.pltz.d/
|
|
841
|
+
├── spec.json # WHAT to plot (semantic, editable)
|
|
842
|
+
├── style.json # HOW it looks (appearance, editable)
|
|
843
|
+
├── {basename}.csv # Raw data (immutable)
|
|
844
|
+
├── exports/
|
|
845
|
+
│ ├── {basename}.png # Main plot image
|
|
846
|
+
│ ├── {basename}.svg # Vector version
|
|
847
|
+
│ ├── {basename}_hitmap.png # Hit detection image
|
|
848
|
+
│ └── {basename}_overview.png # Visual summary
|
|
849
|
+
├── cache/
|
|
850
|
+
│ ├── geometry_px.json # Pixel coordinates (regenerable)
|
|
851
|
+
│ └── render_manifest.json # Render metadata
|
|
852
|
+
└── README.md # This file
|
|
853
|
+
```
|
|
854
|
+
|
|
855
|
+
## Plot Information
|
|
856
|
+
|
|
857
|
+
| Property | Value |
|
|
858
|
+
|----------|-------|
|
|
859
|
+
| Plot ID | `{spec.plot_id}` |
|
|
860
|
+
| Axes | {n_axes} |
|
|
861
|
+
| Traces | {n_traces} |
|
|
862
|
+
| Size | {width_mm:.1f} × {height_mm:.1f} mm |
|
|
863
|
+
| DPI | {dpi} |
|
|
864
|
+
| Pixels | {render_px[0]} × {render_px[1]} |
|
|
865
|
+
| Theme | {style.theme.mode if style.theme else 'light'} |
|
|
866
|
+
|
|
867
|
+
## Coordinate System
|
|
868
|
+
|
|
869
|
+
The bundle uses a layered coordinate system:
|
|
870
|
+
|
|
871
|
+
1. **spec.json + style.json** = Source of truth (edit these)
|
|
872
|
+
2. **cache/** = Derived data (can be deleted and regenerated)
|
|
873
|
+
|
|
874
|
+
### Coordinate Transformation Pipeline
|
|
875
|
+
|
|
876
|
+
```
|
|
877
|
+
Original Figure (at export DPI)
|
|
878
|
+
│
|
|
879
|
+
▼ crop_box offset
|
|
880
|
+
┌─────────────────┐
|
|
881
|
+
│ Final PNG │ ← bbox_px coordinates are in this space
|
|
882
|
+
│ ({render_px[0]} × {render_px[1]}) │
|
|
883
|
+
└─────────────────┘
|
|
884
|
+
```
|
|
885
|
+
|
|
886
|
+
**Formula**: `final_coords = original_coords - crop_offset`
|
|
887
|
+
|
|
888
|
+
## Usage
|
|
889
|
+
|
|
890
|
+
### Python
|
|
891
|
+
|
|
892
|
+
```python
|
|
893
|
+
import scitex as stx
|
|
894
|
+
|
|
895
|
+
# Load the bundle
|
|
896
|
+
bundle = stx.plt.io.load_layered_pltz_bundle("{bundle_dir}")
|
|
897
|
+
|
|
898
|
+
# Access components
|
|
899
|
+
spec = bundle["spec"] # What to plot
|
|
900
|
+
style = bundle["style"] # How it looks
|
|
901
|
+
geometry = bundle["geometry"] # Where in pixels
|
|
902
|
+
```
|
|
903
|
+
|
|
904
|
+
### Editing
|
|
905
|
+
|
|
906
|
+
Edit `spec.json` to change:
|
|
907
|
+
- Axis labels, titles, limits
|
|
908
|
+
- Trace data columns
|
|
909
|
+
- Data source
|
|
910
|
+
|
|
911
|
+
Edit `style.json` to change:
|
|
912
|
+
- Colors, line widths
|
|
913
|
+
- Font sizes
|
|
914
|
+
- Theme (light/dark)
|
|
915
|
+
|
|
916
|
+
After editing, regenerate cache with:
|
|
917
|
+
```python
|
|
918
|
+
stx.plt.io.regenerate_cache("{bundle_dir}")
|
|
919
|
+
```
|
|
920
|
+
|
|
921
|
+
---
|
|
922
|
+
|
|
923
|
+
*Generated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}*
|
|
924
|
+
*Schema: scitex.plt v{PLOT_SPEC_VERSION}*
|
|
925
|
+
"""
|
|
926
|
+
|
|
927
|
+
readme_path = bundle_dir / "README.md"
|
|
928
|
+
with open(readme_path, "w") as f:
|
|
929
|
+
f.write(readme_content)
|
|
930
|
+
|
|
931
|
+
|
|
932
|
+
def _adjust_coords_for_offset(
|
|
933
|
+
selectable_regions: Dict[str, Any],
|
|
934
|
+
offset_left: float,
|
|
935
|
+
offset_upper: float,
|
|
936
|
+
) -> Dict[str, Any]:
|
|
937
|
+
"""Adjust bbox_px coordinates by subtracting offset.
|
|
938
|
+
|
|
939
|
+
Used when coordinates are already in PNG space (extracted at export DPI).
|
|
940
|
+
|
|
941
|
+
Parameters
|
|
942
|
+
----------
|
|
943
|
+
selectable_regions : dict
|
|
944
|
+
The selectable_regions dict (already in PNG coords).
|
|
945
|
+
offset_left : float
|
|
946
|
+
Total offset from left edge to subtract.
|
|
947
|
+
offset_upper : float
|
|
948
|
+
Total offset from top edge to subtract.
|
|
949
|
+
|
|
950
|
+
Returns
|
|
951
|
+
-------
|
|
952
|
+
dict
|
|
953
|
+
selectable_regions with adjusted coordinates.
|
|
954
|
+
"""
|
|
955
|
+
import copy
|
|
956
|
+
result = copy.deepcopy(selectable_regions)
|
|
957
|
+
|
|
958
|
+
def adjust_bbox(bbox: List[float]) -> List[float]:
|
|
959
|
+
"""Subtract offset from [x0, y0, x1, y1] bbox."""
|
|
960
|
+
return [
|
|
961
|
+
bbox[0] - offset_left,
|
|
962
|
+
bbox[1] - offset_upper,
|
|
963
|
+
bbox[2] - offset_left,
|
|
964
|
+
bbox[3] - offset_upper,
|
|
965
|
+
]
|
|
966
|
+
|
|
967
|
+
for ax_region in result.get("axes", []):
|
|
968
|
+
# Adjust title, xlabel, ylabel
|
|
969
|
+
for key in ["title", "xlabel", "ylabel"]:
|
|
970
|
+
if key in ax_region and "bbox_px" in ax_region[key]:
|
|
971
|
+
ax_region[key]["bbox_px"] = adjust_bbox(ax_region[key]["bbox_px"])
|
|
972
|
+
|
|
973
|
+
# Adjust xaxis elements
|
|
974
|
+
if "xaxis" in ax_region:
|
|
975
|
+
xaxis = ax_region["xaxis"]
|
|
976
|
+
if xaxis.get("spine") and "bbox_px" in xaxis["spine"]:
|
|
977
|
+
xaxis["spine"]["bbox_px"] = adjust_bbox(xaxis["spine"]["bbox_px"])
|
|
978
|
+
for tick in xaxis.get("ticks", []):
|
|
979
|
+
if "bbox_px" in tick:
|
|
980
|
+
tick["bbox_px"] = adjust_bbox(tick["bbox_px"])
|
|
981
|
+
for label in xaxis.get("ticklabels", []):
|
|
982
|
+
if "bbox_px" in label:
|
|
983
|
+
label["bbox_px"] = adjust_bbox(label["bbox_px"])
|
|
984
|
+
|
|
985
|
+
# Adjust yaxis elements
|
|
986
|
+
if "yaxis" in ax_region:
|
|
987
|
+
yaxis = ax_region["yaxis"]
|
|
988
|
+
if yaxis.get("spine") and "bbox_px" in yaxis["spine"]:
|
|
989
|
+
yaxis["spine"]["bbox_px"] = adjust_bbox(yaxis["spine"]["bbox_px"])
|
|
990
|
+
for tick in yaxis.get("ticks", []):
|
|
991
|
+
if "bbox_px" in tick:
|
|
992
|
+
tick["bbox_px"] = adjust_bbox(tick["bbox_px"])
|
|
993
|
+
for label in yaxis.get("ticklabels", []):
|
|
994
|
+
if "bbox_px" in label:
|
|
995
|
+
label["bbox_px"] = adjust_bbox(label["bbox_px"])
|
|
996
|
+
|
|
997
|
+
# Adjust legend
|
|
998
|
+
if "legend" in ax_region:
|
|
999
|
+
legend = ax_region["legend"]
|
|
1000
|
+
if "bbox_px" in legend:
|
|
1001
|
+
legend["bbox_px"] = adjust_bbox(legend["bbox_px"])
|
|
1002
|
+
for entry in legend.get("entries", []):
|
|
1003
|
+
if "bbox_px" in entry:
|
|
1004
|
+
entry["bbox_px"] = adjust_bbox(entry["bbox_px"])
|
|
1005
|
+
|
|
1006
|
+
return result
|
|
1007
|
+
|
|
1008
|
+
|
|
1009
|
+
def _adjust_path_data_for_offset(
|
|
1010
|
+
path_data: Dict[str, Any],
|
|
1011
|
+
offset_left: float,
|
|
1012
|
+
offset_upper: float,
|
|
1013
|
+
) -> Dict[str, Any]:
|
|
1014
|
+
"""Adjust path_data coordinates by subtracting offset.
|
|
1015
|
+
|
|
1016
|
+
Used when coordinates are already in PNG space (extracted at export DPI).
|
|
1017
|
+
|
|
1018
|
+
Parameters
|
|
1019
|
+
----------
|
|
1020
|
+
path_data : dict
|
|
1021
|
+
The path_data dict (already in PNG coords).
|
|
1022
|
+
offset_left : float
|
|
1023
|
+
Total offset from left edge to subtract.
|
|
1024
|
+
offset_upper : float
|
|
1025
|
+
Total offset from top edge to subtract.
|
|
1026
|
+
|
|
1027
|
+
Returns
|
|
1028
|
+
-------
|
|
1029
|
+
dict
|
|
1030
|
+
path_data with adjusted coordinates.
|
|
1031
|
+
"""
|
|
1032
|
+
import copy
|
|
1033
|
+
result = copy.deepcopy(path_data)
|
|
1034
|
+
|
|
1035
|
+
# Adjust axes bbox_px
|
|
1036
|
+
for ax in result.get("axes", []):
|
|
1037
|
+
if "bbox_px" in ax:
|
|
1038
|
+
bbox = ax["bbox_px"]
|
|
1039
|
+
if isinstance(bbox, dict):
|
|
1040
|
+
bbox["x0"] = bbox.get("x0", 0) - offset_left
|
|
1041
|
+
bbox["y0"] = bbox.get("y0", 0) - offset_upper
|
|
1042
|
+
if "x1" in bbox:
|
|
1043
|
+
bbox["x1"] = bbox["x1"] - offset_left
|
|
1044
|
+
if "y1" in bbox:
|
|
1045
|
+
bbox["y1"] = bbox["y1"] - offset_upper
|
|
1046
|
+
|
|
1047
|
+
# Adjust artists
|
|
1048
|
+
for artist in result.get("artists", []):
|
|
1049
|
+
if "bbox_px" in artist and artist["bbox_px"]:
|
|
1050
|
+
bbox = artist["bbox_px"]
|
|
1051
|
+
if isinstance(bbox, dict):
|
|
1052
|
+
bbox["x0"] = bbox.get("x0", 0) - offset_left
|
|
1053
|
+
bbox["y0"] = bbox.get("y0", 0) - offset_upper
|
|
1054
|
+
if "x1" in bbox:
|
|
1055
|
+
bbox["x1"] = bbox["x1"] - offset_left
|
|
1056
|
+
if "y1" in bbox:
|
|
1057
|
+
bbox["y1"] = bbox["y1"] - offset_upper
|
|
1058
|
+
|
|
1059
|
+
# Adjust path_px points
|
|
1060
|
+
if "path_px" in artist and artist["path_px"]:
|
|
1061
|
+
artist["path_px"] = [
|
|
1062
|
+
[pt[0] - offset_left, pt[1] - offset_upper]
|
|
1063
|
+
for pt in artist["path_px"]
|
|
1064
|
+
if len(pt) >= 2
|
|
1065
|
+
]
|
|
1066
|
+
|
|
1067
|
+
return result
|
|
1068
|
+
|
|
1069
|
+
|
|
1070
|
+
def _adjust_path_data_for_crop(
|
|
1071
|
+
path_data: Dict[str, Any],
|
|
1072
|
+
offset_left: float,
|
|
1073
|
+
offset_upper: float,
|
|
1074
|
+
) -> Dict[str, Any]:
|
|
1075
|
+
"""Adjust path_data coordinates by subtracting crop offset.
|
|
1076
|
+
|
|
1077
|
+
Parameters
|
|
1078
|
+
----------
|
|
1079
|
+
path_data : dict
|
|
1080
|
+
The path_data dict from extract_path_data.
|
|
1081
|
+
offset_left : float
|
|
1082
|
+
Total offset from left edge.
|
|
1083
|
+
offset_upper : float
|
|
1084
|
+
Total offset from top edge.
|
|
1085
|
+
|
|
1086
|
+
Returns
|
|
1087
|
+
-------
|
|
1088
|
+
dict
|
|
1089
|
+
path_data with adjusted coordinates.
|
|
1090
|
+
"""
|
|
1091
|
+
import copy
|
|
1092
|
+
result = copy.deepcopy(path_data)
|
|
1093
|
+
|
|
1094
|
+
# Adjust axes bbox_px
|
|
1095
|
+
for ax in result.get("axes", []):
|
|
1096
|
+
if "bbox_px" in ax:
|
|
1097
|
+
bbox = ax["bbox_px"]
|
|
1098
|
+
if isinstance(bbox, dict):
|
|
1099
|
+
bbox["x0"] = bbox.get("x0", 0) - offset_left
|
|
1100
|
+
bbox["y0"] = bbox.get("y0", 0) - offset_upper
|
|
1101
|
+
# x1, y1 if present
|
|
1102
|
+
if "x1" in bbox:
|
|
1103
|
+
bbox["x1"] = bbox["x1"] - offset_left
|
|
1104
|
+
if "y1" in bbox:
|
|
1105
|
+
bbox["y1"] = bbox["y1"] - offset_upper
|
|
1106
|
+
|
|
1107
|
+
# Adjust artists
|
|
1108
|
+
for artist in result.get("artists", []):
|
|
1109
|
+
if "bbox_px" in artist and artist["bbox_px"]:
|
|
1110
|
+
bbox = artist["bbox_px"]
|
|
1111
|
+
if isinstance(bbox, dict):
|
|
1112
|
+
bbox["x0"] = bbox.get("x0", 0) - offset_left
|
|
1113
|
+
bbox["y0"] = bbox.get("y0", 0) - offset_upper
|
|
1114
|
+
if "x1" in bbox:
|
|
1115
|
+
bbox["x1"] = bbox["x1"] - offset_left
|
|
1116
|
+
if "y1" in bbox:
|
|
1117
|
+
bbox["y1"] = bbox["y1"] - offset_upper
|
|
1118
|
+
|
|
1119
|
+
# Adjust path_px points
|
|
1120
|
+
if "path_px" in artist and artist["path_px"]:
|
|
1121
|
+
artist["path_px"] = [
|
|
1122
|
+
[pt[0] - offset_left, pt[1] - offset_upper]
|
|
1123
|
+
for pt in artist["path_px"]
|
|
1124
|
+
if len(pt) >= 2
|
|
1125
|
+
]
|
|
1126
|
+
|
|
1127
|
+
return result
|
|
1128
|
+
|
|
1129
|
+
|
|
1130
|
+
def _draw_bbox(ax, bbox: List, color: str, label: str, lw: float = 2) -> None:
|
|
1131
|
+
"""Draw a bounding box on an axes with label inside."""
|
|
1132
|
+
import matplotlib.patches as patches
|
|
1133
|
+
|
|
1134
|
+
x0, y0, x1, y1 = bbox
|
|
1135
|
+
width = x1 - x0
|
|
1136
|
+
height = y1 - y0
|
|
1137
|
+
rect = patches.Rectangle((x0, y0), width, height,
|
|
1138
|
+
linewidth=lw, edgecolor=color, facecolor='none')
|
|
1139
|
+
ax.add_patch(rect)
|
|
1140
|
+
# Place label at top-left corner inside the box with background
|
|
1141
|
+
ax.text(x0 + 2, y0 + 2, label, fontsize=6, color="white", va='top', ha='left',
|
|
1142
|
+
fontweight='bold', bbox=dict(boxstyle="round,pad=0.1", facecolor=color, alpha=0.8))
|
|
1143
|
+
|
|
1144
|
+
|
|
1145
|
+
def _format_json_summary(data: Dict, max_depth: int = 2, current_depth: int = 0) -> str:
|
|
1146
|
+
"""Format JSON data as summary text with limited depth."""
|
|
1147
|
+
lines = []
|
|
1148
|
+
|
|
1149
|
+
def _format_value(key: str, value, depth: int, prefix: str = "") -> None:
|
|
1150
|
+
indent = " " * depth
|
|
1151
|
+
if depth >= max_depth:
|
|
1152
|
+
if isinstance(value, dict):
|
|
1153
|
+
lines.append(f"{prefix}{indent}{key}: {{...}} ({len(value)} keys)")
|
|
1154
|
+
elif isinstance(value, list):
|
|
1155
|
+
lines.append(f"{prefix}{indent}{key}: [...] ({len(value)} items)")
|
|
1156
|
+
else:
|
|
1157
|
+
val_str = str(value)[:30]
|
|
1158
|
+
if len(str(value)) > 30:
|
|
1159
|
+
val_str += "..."
|
|
1160
|
+
lines.append(f"{prefix}{indent}{key}: {val_str}")
|
|
1161
|
+
elif isinstance(value, dict):
|
|
1162
|
+
lines.append(f"{prefix}{indent}{key}:")
|
|
1163
|
+
for k, v in list(value.items())[:8]: # Limit items
|
|
1164
|
+
_format_value(k, v, depth + 1, prefix)
|
|
1165
|
+
if len(value) > 8:
|
|
1166
|
+
lines.append(f"{prefix}{indent} ... ({len(value) - 8} more)")
|
|
1167
|
+
elif isinstance(value, list):
|
|
1168
|
+
if len(value) > 0 and isinstance(value[0], dict):
|
|
1169
|
+
lines.append(f"{prefix}{indent}{key}: [{len(value)} items]")
|
|
1170
|
+
else:
|
|
1171
|
+
val_str = str(value)[:50]
|
|
1172
|
+
if len(str(value)) > 50:
|
|
1173
|
+
val_str += "..."
|
|
1174
|
+
lines.append(f"{prefix}{indent}{key}: {val_str}")
|
|
1175
|
+
else:
|
|
1176
|
+
val_str = str(value)[:40]
|
|
1177
|
+
if len(str(value)) > 40:
|
|
1178
|
+
val_str += "..."
|
|
1179
|
+
lines.append(f"{prefix}{indent}{key}: {val_str}")
|
|
1180
|
+
|
|
1181
|
+
for key, value in data.items():
|
|
1182
|
+
_format_value(key, value, current_depth)
|
|
1183
|
+
|
|
1184
|
+
return "\n".join(lines[:40]) # Limit total lines
|
|
1185
|
+
|
|
1186
|
+
|
|
1187
|
+
def load_layered_pltz_bundle(bundle_dir: Path) -> Dict[str, Any]:
|
|
1188
|
+
"""
|
|
1189
|
+
Load layered .pltz bundle and return merged spec for editor.
|
|
1190
|
+
|
|
1191
|
+
Parameters
|
|
1192
|
+
----------
|
|
1193
|
+
bundle_dir : Path
|
|
1194
|
+
Path to .pltz.d bundle.
|
|
1195
|
+
|
|
1196
|
+
Returns
|
|
1197
|
+
-------
|
|
1198
|
+
dict
|
|
1199
|
+
Merged bundle data compatible with editor.
|
|
1200
|
+
"""
|
|
1201
|
+
bundle_dir = Path(bundle_dir)
|
|
1202
|
+
|
|
1203
|
+
result = {
|
|
1204
|
+
"spec": None,
|
|
1205
|
+
"style": None,
|
|
1206
|
+
"geometry": None,
|
|
1207
|
+
"merged": None, # Combined for backward compatibility
|
|
1208
|
+
"basename": "plot",
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
# Load spec.json
|
|
1212
|
+
spec_path = bundle_dir / "spec.json"
|
|
1213
|
+
if spec_path.exists():
|
|
1214
|
+
with open(spec_path, "r") as f:
|
|
1215
|
+
result["spec"] = json.load(f)
|
|
1216
|
+
result["basename"] = result["spec"].get("plot_id", "plot")
|
|
1217
|
+
|
|
1218
|
+
# Load style.json
|
|
1219
|
+
style_path = bundle_dir / "style.json"
|
|
1220
|
+
if style_path.exists():
|
|
1221
|
+
with open(style_path, "r") as f:
|
|
1222
|
+
result["style"] = json.load(f)
|
|
1223
|
+
|
|
1224
|
+
# Load geometry from cache
|
|
1225
|
+
geometry_path = bundle_dir / "cache" / "geometry_px.json"
|
|
1226
|
+
if geometry_path.exists():
|
|
1227
|
+
with open(geometry_path, "r") as f:
|
|
1228
|
+
result["geometry"] = json.load(f)
|
|
1229
|
+
|
|
1230
|
+
# Create merged view for backward compatibility with editor
|
|
1231
|
+
result["merged"] = merge_layered_bundle(
|
|
1232
|
+
result["spec"], result["style"], result["geometry"]
|
|
1233
|
+
)
|
|
1234
|
+
|
|
1235
|
+
return result
|
|
1236
|
+
|
|
1237
|
+
|
|
1238
|
+
def merge_layered_bundle(
|
|
1239
|
+
spec: Optional[Dict],
|
|
1240
|
+
style: Optional[Dict],
|
|
1241
|
+
geometry: Optional[Dict],
|
|
1242
|
+
) -> Dict[str, Any]:
|
|
1243
|
+
"""
|
|
1244
|
+
Merge spec/style/geometry into old-format compatible dict for editor.
|
|
1245
|
+
|
|
1246
|
+
This provides backward compatibility with editors expecting the old format.
|
|
1247
|
+
"""
|
|
1248
|
+
if spec is None:
|
|
1249
|
+
return {}
|
|
1250
|
+
|
|
1251
|
+
merged = {
|
|
1252
|
+
"schema": {"name": "scitex.plt.plot", "version": "2.0.0"},
|
|
1253
|
+
"backend": "mpl",
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1256
|
+
# Merge data section
|
|
1257
|
+
if "data" in spec:
|
|
1258
|
+
merged["data"] = {
|
|
1259
|
+
"source": spec["data"].get("csv", "data.csv"),
|
|
1260
|
+
"path": spec["data"].get("csv", "data.csv"),
|
|
1261
|
+
"hash": spec["data"].get("hash"),
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
# Merge size from style
|
|
1265
|
+
if style and "size" in style:
|
|
1266
|
+
merged["size"] = {
|
|
1267
|
+
"width_mm": style["size"].get("width_mm", 80),
|
|
1268
|
+
"height_mm": style["size"].get("height_mm", 68),
|
|
1269
|
+
"dpi": geometry.get("dpi", get_default_dpi()) if geometry else get_default_dpi(),
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
# Merge axes from spec + style + geometry
|
|
1273
|
+
merged["axes"] = []
|
|
1274
|
+
for ax_spec in spec.get("axes", []):
|
|
1275
|
+
ax_merged = {
|
|
1276
|
+
"id": ax_spec.get("id"),
|
|
1277
|
+
"xlabel": ax_spec.get("labels", {}).get("xlabel"),
|
|
1278
|
+
"ylabel": ax_spec.get("labels", {}).get("ylabel"),
|
|
1279
|
+
"title": ax_spec.get("labels", {}).get("title"),
|
|
1280
|
+
"xlim": ax_spec.get("limits", {}).get("x"),
|
|
1281
|
+
"ylim": ax_spec.get("limits", {}).get("y"),
|
|
1282
|
+
"bbox": ax_spec.get("bbox", {}),
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1285
|
+
# Add geometry bbox_px if available
|
|
1286
|
+
if geometry:
|
|
1287
|
+
for ax_geom in geometry.get("axes", []):
|
|
1288
|
+
if ax_geom.get("id") == ax_spec.get("id"):
|
|
1289
|
+
ax_merged["bbox_px"] = ax_geom.get("bbox_px", {})
|
|
1290
|
+
break
|
|
1291
|
+
|
|
1292
|
+
merged["axes"].append(ax_merged)
|
|
1293
|
+
|
|
1294
|
+
# Merge traces with styles
|
|
1295
|
+
merged["traces"] = []
|
|
1296
|
+
# Build lookup for trace styles by trace_id
|
|
1297
|
+
trace_style_map = {}
|
|
1298
|
+
if style and "traces" in style:
|
|
1299
|
+
for ts in style.get("traces", []):
|
|
1300
|
+
if isinstance(ts, dict):
|
|
1301
|
+
trace_style_map[ts.get("trace_id", "")] = ts
|
|
1302
|
+
|
|
1303
|
+
for trace in spec.get("traces", []):
|
|
1304
|
+
trace_merged = dict(trace)
|
|
1305
|
+
# Add style if available
|
|
1306
|
+
trace_id = trace.get("id", "")
|
|
1307
|
+
if trace_id in trace_style_map:
|
|
1308
|
+
trace_merged.update(trace_style_map[trace_id])
|
|
1309
|
+
merged["traces"].append(trace_merged)
|
|
1310
|
+
|
|
1311
|
+
# Merge theme from style
|
|
1312
|
+
if style and "theme" in style:
|
|
1313
|
+
merged["theme"] = style["theme"]
|
|
1314
|
+
|
|
1315
|
+
# Merge legend from style (for editor compatibility)
|
|
1316
|
+
if style and "legend" in style:
|
|
1317
|
+
legend_style = style["legend"]
|
|
1318
|
+
merged["legend"] = {
|
|
1319
|
+
"visible": legend_style.get("visible", True),
|
|
1320
|
+
# Use "location" key but also provide "loc" for compatibility
|
|
1321
|
+
"loc": legend_style.get("location", "best"),
|
|
1322
|
+
"location": legend_style.get("location", "best"),
|
|
1323
|
+
"frameon": legend_style.get("frameon", False),
|
|
1324
|
+
"fontsize": legend_style.get("fontsize"),
|
|
1325
|
+
"ncols": legend_style.get("ncols", 1),
|
|
1326
|
+
"title": legend_style.get("title"),
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1329
|
+
# Merge hit_regions, selectable_regions, and figure_px from geometry
|
|
1330
|
+
if geometry:
|
|
1331
|
+
if "hit_regions" in geometry:
|
|
1332
|
+
merged["hit_regions"] = geometry["hit_regions"]
|
|
1333
|
+
if "selectable_regions" in geometry:
|
|
1334
|
+
merged["selectable_regions"] = geometry["selectable_regions"]
|
|
1335
|
+
if "figure_px" in geometry:
|
|
1336
|
+
merged["figure_px"] = geometry["figure_px"]
|
|
1337
|
+
if "artists" in geometry:
|
|
1338
|
+
merged["artists"] = geometry["artists"]
|
|
1339
|
+
|
|
1340
|
+
return merged
|
|
1341
|
+
|
|
1342
|
+
|
|
1343
|
+
# EOF
|