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,813 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
# File: ./src/scitex/vis/editor/flask_editor/renderer.py
|
|
4
|
+
"""Figure rendering for Flask editor - supports single and multi-axis figures."""
|
|
5
|
+
|
|
6
|
+
from typing import Dict, Any, Tuple, Optional, List
|
|
7
|
+
import base64
|
|
8
|
+
import io
|
|
9
|
+
|
|
10
|
+
import matplotlib
|
|
11
|
+
|
|
12
|
+
matplotlib.use("Agg")
|
|
13
|
+
import matplotlib.pyplot as plt
|
|
14
|
+
from matplotlib.ticker import MaxNLocator
|
|
15
|
+
from PIL import Image
|
|
16
|
+
import numpy as np
|
|
17
|
+
|
|
18
|
+
from ._plotter import plot_from_csv, plot_from_recipe
|
|
19
|
+
from ._bbox import extract_bboxes, extract_bboxes_multi
|
|
20
|
+
from scitex.plt.styles import get_default_dpi
|
|
21
|
+
|
|
22
|
+
# mm to pt conversion factor
|
|
23
|
+
MM_TO_PT = 2.83465
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def render_preview_with_bboxes(
|
|
27
|
+
csv_data, overrides: Dict[str, Any], axis_fontsize: int = 7,
|
|
28
|
+
metadata: Optional[Dict[str, Any]] = None,
|
|
29
|
+
dark_mode: bool = False,
|
|
30
|
+
) -> Tuple[str, Dict[str, Any], Dict[str, int]]:
|
|
31
|
+
"""Render figure and return base64 PNG along with element bounding boxes.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
csv_data: DataFrame containing CSV data
|
|
35
|
+
overrides: Dictionary with override settings
|
|
36
|
+
axis_fontsize: Default font size for axis labels
|
|
37
|
+
metadata: Optional JSON metadata (new schema with axes dict)
|
|
38
|
+
dark_mode: Whether to render with dark mode colors (light text/spines)
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
tuple: (base64_image_data, bboxes_dict, image_size)
|
|
42
|
+
"""
|
|
43
|
+
# Check if this is a multi-axis figure (new schema)
|
|
44
|
+
if metadata and "axes" in metadata and isinstance(metadata.get("axes"), dict):
|
|
45
|
+
return render_multi_axis_preview(csv_data, overrides, metadata, dark_mode)
|
|
46
|
+
|
|
47
|
+
# Fall back to single-axis rendering
|
|
48
|
+
return render_single_axis_preview(csv_data, overrides, axis_fontsize, dark_mode)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def render_single_axis_preview(
|
|
52
|
+
csv_data, overrides: Dict[str, Any], axis_fontsize: int = 7,
|
|
53
|
+
dark_mode: bool = False,
|
|
54
|
+
) -> Tuple[str, Dict[str, Any], Dict[str, int]]:
|
|
55
|
+
"""Render single-axis figure (legacy mode)."""
|
|
56
|
+
o = overrides
|
|
57
|
+
|
|
58
|
+
# Dimensions
|
|
59
|
+
dpi = o.get("dpi", get_default_dpi())
|
|
60
|
+
fig_size = o.get("fig_size", [3.15, 2.68])
|
|
61
|
+
|
|
62
|
+
# Font sizes
|
|
63
|
+
axis_fontsize = o.get("axis_fontsize", 7)
|
|
64
|
+
tick_fontsize = o.get("tick_fontsize", 7)
|
|
65
|
+
title_fontsize = o.get("title_fontsize", 8)
|
|
66
|
+
|
|
67
|
+
# Line/axis thickness
|
|
68
|
+
linewidth_pt = o.get("linewidth", 0.57)
|
|
69
|
+
axis_width_pt = o.get("axis_width", 0.2) * MM_TO_PT
|
|
70
|
+
tick_length_pt = o.get("tick_length", 0.8) * MM_TO_PT
|
|
71
|
+
tick_width_pt = o.get("tick_width", 0.2) * MM_TO_PT
|
|
72
|
+
tick_direction = o.get("tick_direction", "out")
|
|
73
|
+
x_n_ticks = o.get("x_n_ticks", o.get("n_ticks", 4))
|
|
74
|
+
y_n_ticks = o.get("y_n_ticks", o.get("n_ticks", 4))
|
|
75
|
+
hide_x_ticks = o.get("hide_x_ticks", False)
|
|
76
|
+
hide_y_ticks = o.get("hide_y_ticks", False)
|
|
77
|
+
|
|
78
|
+
transparent = o.get("transparent", True)
|
|
79
|
+
|
|
80
|
+
# Create figure
|
|
81
|
+
fig, ax = plt.subplots(figsize=fig_size, dpi=dpi)
|
|
82
|
+
_apply_background(fig, ax, o, transparent)
|
|
83
|
+
|
|
84
|
+
# Plot from CSV data
|
|
85
|
+
if csv_data is not None:
|
|
86
|
+
plot_from_csv(ax, csv_data, overrides, linewidth=linewidth_pt)
|
|
87
|
+
else:
|
|
88
|
+
ax.text(
|
|
89
|
+
0.5,
|
|
90
|
+
0.5,
|
|
91
|
+
"No plot data available\n(CSV not found)",
|
|
92
|
+
ha="center",
|
|
93
|
+
va="center",
|
|
94
|
+
transform=ax.transAxes,
|
|
95
|
+
fontsize=axis_fontsize,
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
# Apply labels
|
|
99
|
+
_apply_labels(ax, o, title_fontsize, axis_fontsize)
|
|
100
|
+
|
|
101
|
+
# Tick styling
|
|
102
|
+
_apply_tick_styling(
|
|
103
|
+
ax,
|
|
104
|
+
tick_fontsize,
|
|
105
|
+
tick_length_pt,
|
|
106
|
+
tick_width_pt,
|
|
107
|
+
tick_direction,
|
|
108
|
+
x_n_ticks,
|
|
109
|
+
y_n_ticks,
|
|
110
|
+
hide_x_ticks,
|
|
111
|
+
hide_y_ticks,
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
# Apply grid, limits, spines
|
|
115
|
+
_apply_style(ax, o, axis_width_pt)
|
|
116
|
+
|
|
117
|
+
# Apply annotations
|
|
118
|
+
_apply_annotations(ax, o, axis_fontsize)
|
|
119
|
+
|
|
120
|
+
# Apply caption (below figure)
|
|
121
|
+
caption_artist = _apply_caption(fig, o)
|
|
122
|
+
|
|
123
|
+
# Apply dark mode styling if requested
|
|
124
|
+
if dark_mode:
|
|
125
|
+
_apply_dark_theme(ax)
|
|
126
|
+
# Also style caption if present
|
|
127
|
+
if caption_artist:
|
|
128
|
+
caption_artist.set_color(DARK_THEME_TEXT_COLOR)
|
|
129
|
+
|
|
130
|
+
fig.tight_layout()
|
|
131
|
+
|
|
132
|
+
# Get element bounding boxes BEFORE saving (need renderer)
|
|
133
|
+
fig.canvas.draw()
|
|
134
|
+
renderer = fig.canvas.get_renderer()
|
|
135
|
+
|
|
136
|
+
# Save to buffer first to get actual image size
|
|
137
|
+
buf = io.BytesIO()
|
|
138
|
+
fig.savefig(
|
|
139
|
+
buf, format="png", dpi=dpi, bbox_inches="tight", transparent=transparent
|
|
140
|
+
)
|
|
141
|
+
buf.seek(0)
|
|
142
|
+
|
|
143
|
+
# Get actual saved image dimensions
|
|
144
|
+
img = Image.open(buf)
|
|
145
|
+
img_width, img_height = img.size
|
|
146
|
+
buf.seek(0)
|
|
147
|
+
|
|
148
|
+
# Get bboxes
|
|
149
|
+
bboxes = extract_bboxes(fig, ax, renderer, img_width, img_height)
|
|
150
|
+
|
|
151
|
+
img_data = base64.b64encode(buf.read()).decode("utf-8")
|
|
152
|
+
plt.close(fig)
|
|
153
|
+
|
|
154
|
+
return img_data, bboxes, {"width": img_width, "height": img_height}
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def render_multi_axis_preview(
|
|
158
|
+
csv_data, overrides: Dict[str, Any], metadata: Dict[str, Any],
|
|
159
|
+
dark_mode: bool = False,
|
|
160
|
+
) -> Tuple[str, Dict[str, Any], Dict[str, int]]:
|
|
161
|
+
"""Render multi-axis figure from new schema (scitex.plt.figure.recipe).
|
|
162
|
+
|
|
163
|
+
Args:
|
|
164
|
+
csv_data: DataFrame containing CSV data
|
|
165
|
+
overrides: Dictionary with override settings
|
|
166
|
+
metadata: JSON metadata with axes dict
|
|
167
|
+
dark_mode: Whether to render with dark mode colors
|
|
168
|
+
|
|
169
|
+
Returns:
|
|
170
|
+
tuple: (base64_image_data, bboxes_dict, image_size)
|
|
171
|
+
"""
|
|
172
|
+
o = overrides
|
|
173
|
+
axes_spec = metadata.get("axes", {})
|
|
174
|
+
fig_spec = metadata.get("figure", {})
|
|
175
|
+
|
|
176
|
+
# Get grid dimensions from axes positions
|
|
177
|
+
nrows, ncols = _get_grid_dimensions(axes_spec)
|
|
178
|
+
|
|
179
|
+
# Figure dimensions
|
|
180
|
+
dpi = fig_spec.get("dpi", o.get("dpi", get_default_dpi()))
|
|
181
|
+
size_mm = fig_spec.get("size_mm", [176, 106])
|
|
182
|
+
# Convert mm to inches (1 inch = 25.4 mm)
|
|
183
|
+
fig_size = (size_mm[0] / 25.4, size_mm[1] / 25.4)
|
|
184
|
+
|
|
185
|
+
# Font sizes (from overrides)
|
|
186
|
+
axis_fontsize = o.get("axis_fontsize", 7)
|
|
187
|
+
tick_fontsize = o.get("tick_fontsize", 7)
|
|
188
|
+
title_fontsize = o.get("title_fontsize", 8)
|
|
189
|
+
|
|
190
|
+
# Line/axis thickness
|
|
191
|
+
linewidth_pt = o.get("linewidth", 0.57)
|
|
192
|
+
axis_width_pt = o.get("axis_width", 0.2) * MM_TO_PT
|
|
193
|
+
tick_length_pt = o.get("tick_length", 0.8) * MM_TO_PT
|
|
194
|
+
tick_width_pt = o.get("tick_width", 0.2) * MM_TO_PT
|
|
195
|
+
tick_direction = o.get("tick_direction", "out")
|
|
196
|
+
x_n_ticks = o.get("x_n_ticks", o.get("n_ticks", 4))
|
|
197
|
+
y_n_ticks = o.get("y_n_ticks", o.get("n_ticks", 4))
|
|
198
|
+
|
|
199
|
+
transparent = o.get("transparent", True)
|
|
200
|
+
|
|
201
|
+
# Create multi-axis figure
|
|
202
|
+
fig, axes_array = plt.subplots(nrows, ncols, figsize=fig_size, dpi=dpi)
|
|
203
|
+
|
|
204
|
+
# Handle 1D or 2D array
|
|
205
|
+
if nrows == 1 and ncols == 1:
|
|
206
|
+
axes_array = np.array([[axes_array]])
|
|
207
|
+
elif nrows == 1:
|
|
208
|
+
axes_array = axes_array.reshape(1, -1)
|
|
209
|
+
elif ncols == 1:
|
|
210
|
+
axes_array = axes_array.reshape(-1, 1)
|
|
211
|
+
|
|
212
|
+
# Apply background to figure
|
|
213
|
+
if transparent:
|
|
214
|
+
fig.patch.set_facecolor("none")
|
|
215
|
+
elif o.get("facecolor"):
|
|
216
|
+
fig.patch.set_facecolor(o["facecolor"])
|
|
217
|
+
|
|
218
|
+
# Build mapping from axis ID to row/col
|
|
219
|
+
ax_to_rowcol = _build_ax_to_rowcol_map(axes_spec)
|
|
220
|
+
|
|
221
|
+
# Map axes by their ID
|
|
222
|
+
axes_map = {}
|
|
223
|
+
for ax_id, ax_spec in axes_spec.items():
|
|
224
|
+
row, col = ax_to_rowcol.get(ax_id, (0, 0))
|
|
225
|
+
ax = axes_array[row, col]
|
|
226
|
+
axes_map[ax_id] = ax
|
|
227
|
+
|
|
228
|
+
# Apply background
|
|
229
|
+
if transparent:
|
|
230
|
+
ax.patch.set_facecolor("none")
|
|
231
|
+
elif o.get("facecolor"):
|
|
232
|
+
ax.patch.set_facecolor(o["facecolor"])
|
|
233
|
+
|
|
234
|
+
# Plot data - check which schema format
|
|
235
|
+
if csv_data is not None:
|
|
236
|
+
calls = ax_spec.get("calls", [])
|
|
237
|
+
if calls:
|
|
238
|
+
# Recipe schema with explicit calls
|
|
239
|
+
plot_from_recipe(ax, csv_data, ax_spec, overrides, linewidth_pt, ax_id=ax_id)
|
|
240
|
+
else:
|
|
241
|
+
# Editable schema - plot from CSV column names
|
|
242
|
+
csv_row, csv_col = row, col # Use computed row/col for CSV lookup
|
|
243
|
+
_plot_from_editable_csv(ax, csv_data, ax_id, csv_row, csv_col, overrides, linewidth_pt, metadata)
|
|
244
|
+
|
|
245
|
+
# Get panel-specific overrides (e.g., ax_00_panel)
|
|
246
|
+
panel_key = f"{ax_id}_panel"
|
|
247
|
+
element_overrides = o.get("element_overrides", {})
|
|
248
|
+
panel_overrides = element_overrides.get(panel_key, {})
|
|
249
|
+
|
|
250
|
+
# Apply axis labels from spec, with panel overrides taking precedence
|
|
251
|
+
xaxis = ax_spec.get("xaxis", {})
|
|
252
|
+
yaxis = ax_spec.get("yaxis", {})
|
|
253
|
+
|
|
254
|
+
# Panel title (from overrides or spec)
|
|
255
|
+
panel_title = panel_overrides.get("title")
|
|
256
|
+
if panel_title:
|
|
257
|
+
ax.set_title(panel_title, fontsize=title_fontsize)
|
|
258
|
+
|
|
259
|
+
# X/Y labels (panel overrides take precedence over spec)
|
|
260
|
+
xlabel = panel_overrides.get("xlabel") or xaxis.get("label")
|
|
261
|
+
ylabel = panel_overrides.get("ylabel") or yaxis.get("label")
|
|
262
|
+
|
|
263
|
+
if xlabel:
|
|
264
|
+
ax.set_xlabel(xlabel, fontsize=axis_fontsize)
|
|
265
|
+
if ylabel:
|
|
266
|
+
ax.set_ylabel(ylabel, fontsize=axis_fontsize)
|
|
267
|
+
|
|
268
|
+
# Apply axis limits
|
|
269
|
+
if xaxis.get("lim"):
|
|
270
|
+
ax.set_xlim(xaxis["lim"])
|
|
271
|
+
if yaxis.get("lim"):
|
|
272
|
+
ax.set_ylim(yaxis["lim"])
|
|
273
|
+
|
|
274
|
+
# Tick styling
|
|
275
|
+
_apply_tick_styling(
|
|
276
|
+
ax,
|
|
277
|
+
tick_fontsize,
|
|
278
|
+
tick_length_pt,
|
|
279
|
+
tick_width_pt,
|
|
280
|
+
tick_direction,
|
|
281
|
+
x_n_ticks,
|
|
282
|
+
y_n_ticks,
|
|
283
|
+
False, # hide_x_ticks
|
|
284
|
+
False, # hide_y_ticks
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
# Apply spines
|
|
288
|
+
if o.get("hide_top_spine", True):
|
|
289
|
+
ax.spines["top"].set_visible(False)
|
|
290
|
+
if o.get("hide_right_spine", True):
|
|
291
|
+
ax.spines["right"].set_visible(False)
|
|
292
|
+
for spine in ax.spines.values():
|
|
293
|
+
spine.set_linewidth(axis_width_pt)
|
|
294
|
+
|
|
295
|
+
# Apply dark mode to this axis
|
|
296
|
+
if dark_mode:
|
|
297
|
+
_apply_dark_theme(ax)
|
|
298
|
+
|
|
299
|
+
# Apply caption (below figure) - use global overrides
|
|
300
|
+
caption_artist = _apply_caption(fig, o)
|
|
301
|
+
if dark_mode and caption_artist:
|
|
302
|
+
caption_artist.set_color(DARK_THEME_TEXT_COLOR)
|
|
303
|
+
|
|
304
|
+
fig.tight_layout()
|
|
305
|
+
|
|
306
|
+
# Get element bounding boxes
|
|
307
|
+
fig.canvas.draw()
|
|
308
|
+
renderer = fig.canvas.get_renderer()
|
|
309
|
+
|
|
310
|
+
# Save to buffer
|
|
311
|
+
buf = io.BytesIO()
|
|
312
|
+
fig.savefig(
|
|
313
|
+
buf, format="png", dpi=dpi, bbox_inches="tight", transparent=transparent
|
|
314
|
+
)
|
|
315
|
+
buf.seek(0)
|
|
316
|
+
|
|
317
|
+
# Get actual saved image dimensions
|
|
318
|
+
img = Image.open(buf)
|
|
319
|
+
img_width, img_height = img.size
|
|
320
|
+
buf.seek(0)
|
|
321
|
+
|
|
322
|
+
# Get bboxes for all axes
|
|
323
|
+
bboxes = extract_bboxes_multi(fig, axes_map, renderer, img_width, img_height)
|
|
324
|
+
|
|
325
|
+
img_data = base64.b64encode(buf.read()).decode("utf-8")
|
|
326
|
+
plt.close(fig)
|
|
327
|
+
|
|
328
|
+
return img_data, bboxes, {"width": img_width, "height": img_height}
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
def _get_grid_dimensions(axes_spec: Dict[str, Any]) -> Tuple[int, int]:
|
|
332
|
+
"""Get grid dimensions from axes specifications.
|
|
333
|
+
|
|
334
|
+
Handles both:
|
|
335
|
+
- New recipe schema with grid_position
|
|
336
|
+
- Editable schema - infer grid from positions (y-position groups = rows)
|
|
337
|
+
"""
|
|
338
|
+
# Check if any axis has grid_position
|
|
339
|
+
has_grid_pos = any(ax_spec.get("grid_position") for ax_spec in axes_spec.values())
|
|
340
|
+
|
|
341
|
+
if has_grid_pos:
|
|
342
|
+
max_row = 0
|
|
343
|
+
max_col = 0
|
|
344
|
+
for ax_id, ax_spec in axes_spec.items():
|
|
345
|
+
pos = ax_spec.get("grid_position", {})
|
|
346
|
+
max_row = max(max_row, pos.get("row", 0))
|
|
347
|
+
max_col = max(max_col, pos.get("col", 0))
|
|
348
|
+
return max_row + 1, max_col + 1
|
|
349
|
+
|
|
350
|
+
# Editable schema: infer grid from positions
|
|
351
|
+
# Group by y-position to determine rows
|
|
352
|
+
if not axes_spec:
|
|
353
|
+
return 1, 1
|
|
354
|
+
|
|
355
|
+
positions = []
|
|
356
|
+
for ax_id, ax_spec in axes_spec.items():
|
|
357
|
+
pos = ax_spec.get("position", [0, 0, 0, 0])
|
|
358
|
+
if len(pos) >= 2:
|
|
359
|
+
positions.append((ax_id, pos[0], pos[1])) # (id, x, y)
|
|
360
|
+
|
|
361
|
+
if not positions:
|
|
362
|
+
return 1, 1
|
|
363
|
+
|
|
364
|
+
# Cluster by y-position (tolerance for floating point)
|
|
365
|
+
y_values = sorted(set(round(p[2], 2) for p in positions), reverse=True)
|
|
366
|
+
n_rows = len(y_values)
|
|
367
|
+
|
|
368
|
+
# Count columns in first row
|
|
369
|
+
first_row_y = y_values[0]
|
|
370
|
+
n_cols = sum(1 for p in positions if abs(p[2] - first_row_y) < 0.1)
|
|
371
|
+
|
|
372
|
+
return n_rows, n_cols
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
def _build_ax_to_rowcol_map(axes_spec: Dict[str, Any]) -> Dict[str, Tuple[int, int]]:
|
|
376
|
+
"""Build mapping from axis ID to (row, col) based on positions."""
|
|
377
|
+
if not axes_spec:
|
|
378
|
+
return {}
|
|
379
|
+
|
|
380
|
+
# Check if any axis has grid_position
|
|
381
|
+
has_grid_pos = any(ax_spec.get("grid_position") for ax_spec in axes_spec.values())
|
|
382
|
+
|
|
383
|
+
if has_grid_pos:
|
|
384
|
+
result = {}
|
|
385
|
+
for ax_id, ax_spec in axes_spec.items():
|
|
386
|
+
pos = ax_spec.get("grid_position", {})
|
|
387
|
+
result[ax_id] = (pos.get("row", 0), pos.get("col", 0))
|
|
388
|
+
return result
|
|
389
|
+
|
|
390
|
+
# Editable schema: compute from positions
|
|
391
|
+
positions = []
|
|
392
|
+
for ax_id, ax_spec in axes_spec.items():
|
|
393
|
+
pos = ax_spec.get("position", [0, 0, 0, 0])
|
|
394
|
+
if len(pos) >= 2:
|
|
395
|
+
positions.append((ax_id, pos[0], pos[1])) # (id, x, y)
|
|
396
|
+
|
|
397
|
+
if not positions:
|
|
398
|
+
return {ax_id: (0, 0) for ax_id in axes_spec}
|
|
399
|
+
|
|
400
|
+
# Get unique y-values (rows) - higher y = lower row number (top of figure)
|
|
401
|
+
y_values = sorted(set(round(p[2], 2) for p in positions), reverse=True)
|
|
402
|
+
y_to_row = {y: i for i, y in enumerate(y_values)}
|
|
403
|
+
|
|
404
|
+
# For each row, sort by x to get column
|
|
405
|
+
result = {}
|
|
406
|
+
for row_idx, row_y in enumerate(y_values):
|
|
407
|
+
row_axes = [(ax_id, x) for ax_id, x, y in positions if abs(y - row_y) < 0.1]
|
|
408
|
+
row_axes.sort(key=lambda t: t[1]) # Sort by x
|
|
409
|
+
for col_idx, (ax_id, _) in enumerate(row_axes):
|
|
410
|
+
result[ax_id] = (row_idx, col_idx)
|
|
411
|
+
|
|
412
|
+
return result
|
|
413
|
+
|
|
414
|
+
|
|
415
|
+
def _get_row_col_from_ax_id(ax_id: str, ax_map: Optional[Dict[str, Tuple[int, int]]] = None) -> Tuple[int, int]:
|
|
416
|
+
"""Extract row and col from axis ID using the mapping."""
|
|
417
|
+
if ax_map and ax_id in ax_map:
|
|
418
|
+
return ax_map[ax_id]
|
|
419
|
+
# Fallback: try parsing ax_XY format
|
|
420
|
+
import re
|
|
421
|
+
match = re.match(r'ax_(\d)(\d)', ax_id)
|
|
422
|
+
if match:
|
|
423
|
+
return int(match.group(1)), int(match.group(2))
|
|
424
|
+
return 0, 0
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
def _plot_from_editable_csv(
|
|
428
|
+
ax, csv_data, ax_id: str, row: int, col: int,
|
|
429
|
+
overrides: Dict[str, Any], linewidth: float,
|
|
430
|
+
metadata: Optional[Dict[str, Any]] = None,
|
|
431
|
+
):
|
|
432
|
+
"""Plot data from editable schema CSV format.
|
|
433
|
+
|
|
434
|
+
CSV columns follow pattern: ax-row-X-col-Y_trace-id-NAME_variable-VAR
|
|
435
|
+
"""
|
|
436
|
+
import re
|
|
437
|
+
df = csv_data
|
|
438
|
+
elements = metadata.get("elements", {}) if metadata else {}
|
|
439
|
+
|
|
440
|
+
# Find columns for this axis (by row/col)
|
|
441
|
+
pattern = f"ax-row-{row}-col-{col}_"
|
|
442
|
+
ax_cols = [c for c in df.columns if c.startswith(pattern)]
|
|
443
|
+
|
|
444
|
+
if not ax_cols:
|
|
445
|
+
return
|
|
446
|
+
|
|
447
|
+
# Group columns by trace-id
|
|
448
|
+
traces = {}
|
|
449
|
+
for col_name in ax_cols:
|
|
450
|
+
# Parse: ax-row-X-col-Y_trace-id-NAME_variable-VAR
|
|
451
|
+
match = re.match(rf'{pattern}trace-id-(.+?)_variable-(.+)', col_name)
|
|
452
|
+
if match:
|
|
453
|
+
trace_id = match.group(1)
|
|
454
|
+
var_name = match.group(2)
|
|
455
|
+
if trace_id not in traces:
|
|
456
|
+
traces[trace_id] = {}
|
|
457
|
+
traces[trace_id][var_name] = col_name
|
|
458
|
+
|
|
459
|
+
# Get element overrides from overrides dict
|
|
460
|
+
element_overrides = overrides.get("element_overrides", {})
|
|
461
|
+
|
|
462
|
+
# Plot each trace
|
|
463
|
+
trace_idx = 0
|
|
464
|
+
for trace_id, vars_dict in traces.items():
|
|
465
|
+
# Find element info from metadata
|
|
466
|
+
element_key = f"{ax_id}_line_{trace_idx:02d}"
|
|
467
|
+
element_info = elements.get(element_key, {})
|
|
468
|
+
label = element_info.get("label", trace_id)
|
|
469
|
+
|
|
470
|
+
# Get user overrides for this trace
|
|
471
|
+
override_key = f"{ax_id}_trace_{trace_idx}"
|
|
472
|
+
trace_overrides = element_overrides.get(override_key, {})
|
|
473
|
+
|
|
474
|
+
# Determine plot type based on variables present
|
|
475
|
+
# Check more specific patterns first, then fall back to x/y
|
|
476
|
+
|
|
477
|
+
if 'y_lower' in vars_dict and 'y_middle' in vars_dict and 'y_upper' in vars_dict:
|
|
478
|
+
# Shaded line plot (fill_between + line)
|
|
479
|
+
x_col = vars_dict.get('x')
|
|
480
|
+
if x_col:
|
|
481
|
+
x = df[x_col].dropna().values
|
|
482
|
+
y_lower = df[vars_dict['y_lower']].dropna().values
|
|
483
|
+
y_middle = df[vars_dict['y_middle']].dropna().values
|
|
484
|
+
y_upper = df[vars_dict['y_upper']].dropna().values
|
|
485
|
+
|
|
486
|
+
min_len = min(len(x), len(y_lower), len(y_middle), len(y_upper))
|
|
487
|
+
if min_len > 0:
|
|
488
|
+
ax.fill_between(x[:min_len], y_lower[:min_len], y_upper[:min_len], alpha=0.3)
|
|
489
|
+
ax.plot(x[:min_len], y_middle[:min_len], linewidth=linewidth)
|
|
490
|
+
trace_idx += 1
|
|
491
|
+
|
|
492
|
+
elif 'row' in vars_dict and 'col' in vars_dict and 'value' in vars_dict:
|
|
493
|
+
# Heatmap / imshow
|
|
494
|
+
rows_data = df[vars_dict['row']].dropna().values
|
|
495
|
+
cols_data = df[vars_dict['col']].dropna().values
|
|
496
|
+
values = df[vars_dict['value']].dropna().values
|
|
497
|
+
if len(rows_data) > 0:
|
|
498
|
+
n_rows = int(rows_data.max()) + 1
|
|
499
|
+
n_cols = int(cols_data.max()) + 1
|
|
500
|
+
data = np.zeros((n_rows, n_cols))
|
|
501
|
+
for r, c, v in zip(rows_data.astype(int), cols_data.astype(int), values):
|
|
502
|
+
data[r, c] = v
|
|
503
|
+
ax.imshow(data, aspect='auto', origin='lower')
|
|
504
|
+
|
|
505
|
+
elif 'y1' in vars_dict and 'y2' in vars_dict:
|
|
506
|
+
# fill_between (CI band)
|
|
507
|
+
x_col = vars_dict.get('x')
|
|
508
|
+
if x_col:
|
|
509
|
+
x = df[x_col].dropna().values
|
|
510
|
+
y1 = df[vars_dict['y1']].dropna().values
|
|
511
|
+
y2 = df[vars_dict['y2']].dropna().values
|
|
512
|
+
min_len = min(len(x), len(y1), len(y2))
|
|
513
|
+
if min_len > 0:
|
|
514
|
+
ax.fill_between(x[:min_len], y1[:min_len], y2[:min_len], alpha=0.3)
|
|
515
|
+
|
|
516
|
+
elif 'yerr' in vars_dict and 'y' in vars_dict:
|
|
517
|
+
# Error bars with bar chart
|
|
518
|
+
x_col = vars_dict.get('x')
|
|
519
|
+
y_col = vars_dict.get('y')
|
|
520
|
+
if x_col and y_col:
|
|
521
|
+
x = df[x_col].dropna().values
|
|
522
|
+
y = df[y_col].dropna().values
|
|
523
|
+
yerr = df[vars_dict['yerr']].dropna().values
|
|
524
|
+
min_len = min(len(x), len(y), len(yerr))
|
|
525
|
+
if min_len > 0:
|
|
526
|
+
ax.bar(x[:min_len], y[:min_len], yerr=yerr[:min_len])
|
|
527
|
+
|
|
528
|
+
elif 'group' in vars_dict and 'value' in vars_dict:
|
|
529
|
+
# Violin/strip plot - plot as scatter for now
|
|
530
|
+
groups = df[vars_dict['group']].dropna().values
|
|
531
|
+
values = df[vars_dict['value']].dropna().values
|
|
532
|
+
if len(groups) > 0 and len(values) > 0:
|
|
533
|
+
min_len = min(len(groups), len(values))
|
|
534
|
+
# Convert string groups to numeric positions
|
|
535
|
+
unique_groups = list(dict.fromkeys(groups[:min_len])) # Preserve order
|
|
536
|
+
group_to_x = {g: i for i, g in enumerate(unique_groups)}
|
|
537
|
+
x_positions = np.array([group_to_x.get(g, 0) for g in groups[:min_len]])
|
|
538
|
+
# Add jitter for strip plot effect
|
|
539
|
+
jitter = np.random.uniform(-0.1, 0.1, min_len)
|
|
540
|
+
ax.scatter(x_positions + jitter, values[:min_len], alpha=0.6, s=20)
|
|
541
|
+
# Set tick labels
|
|
542
|
+
ax.set_xticks(range(len(unique_groups)))
|
|
543
|
+
ax.set_xticklabels(unique_groups, fontsize=6)
|
|
544
|
+
|
|
545
|
+
elif 'width' in vars_dict and 'height' in vars_dict:
|
|
546
|
+
# Rectangle - skip for now
|
|
547
|
+
pass
|
|
548
|
+
|
|
549
|
+
elif 'type' in vars_dict:
|
|
550
|
+
# Skip type-only entries (like stim markers)
|
|
551
|
+
pass
|
|
552
|
+
|
|
553
|
+
elif 'content' in vars_dict:
|
|
554
|
+
# Text annotation - skip for preview
|
|
555
|
+
pass
|
|
556
|
+
|
|
557
|
+
elif 'x' in vars_dict and 'y' in vars_dict:
|
|
558
|
+
# Default: line or scatter plot
|
|
559
|
+
x_col = vars_dict['x']
|
|
560
|
+
y_col = vars_dict['y']
|
|
561
|
+
x = df[x_col].dropna().values
|
|
562
|
+
y = df[y_col].dropna().values
|
|
563
|
+
if len(x) > 0 and len(y) > 0:
|
|
564
|
+
min_len = min(len(x), len(y))
|
|
565
|
+
# Apply overrides
|
|
566
|
+
color = trace_overrides.get("color")
|
|
567
|
+
lw = trace_overrides.get("linewidth", linewidth)
|
|
568
|
+
ls = trace_overrides.get("linestyle", "-")
|
|
569
|
+
marker = trace_overrides.get("marker")
|
|
570
|
+
alpha = trace_overrides.get("alpha", 1.0)
|
|
571
|
+
|
|
572
|
+
kwargs = {"linewidth": lw, "linestyle": ls, "alpha": alpha}
|
|
573
|
+
if color:
|
|
574
|
+
kwargs["color"] = color
|
|
575
|
+
if marker:
|
|
576
|
+
kwargs["marker"] = marker
|
|
577
|
+
if label and label != trace_id:
|
|
578
|
+
kwargs["label"] = label
|
|
579
|
+
|
|
580
|
+
# Check if this looks like scatter data (trace-id contains 'scatter' or 'strip')
|
|
581
|
+
if 'scatter' in trace_id.lower() or 'strip' in trace_id.lower():
|
|
582
|
+
scatter_kwargs = {"alpha": alpha, "s": 20}
|
|
583
|
+
if color:
|
|
584
|
+
scatter_kwargs["c"] = color
|
|
585
|
+
ax.scatter(x[:min_len], y[:min_len], **scatter_kwargs)
|
|
586
|
+
else:
|
|
587
|
+
ax.plot(x[:min_len], y[:min_len], **kwargs)
|
|
588
|
+
trace_idx += 1
|
|
589
|
+
|
|
590
|
+
|
|
591
|
+
def _apply_background(fig, ax, o, transparent):
|
|
592
|
+
"""Apply background settings to figure."""
|
|
593
|
+
if transparent:
|
|
594
|
+
fig.patch.set_facecolor("none")
|
|
595
|
+
ax.patch.set_facecolor("none")
|
|
596
|
+
elif o.get("facecolor"):
|
|
597
|
+
fig.patch.set_facecolor(o["facecolor"])
|
|
598
|
+
ax.patch.set_facecolor(o["facecolor"])
|
|
599
|
+
|
|
600
|
+
|
|
601
|
+
def _apply_labels(ax, o, title_fontsize, axis_fontsize):
|
|
602
|
+
"""Apply title and axis labels."""
|
|
603
|
+
# Show title only if enabled (default True)
|
|
604
|
+
if o.get("show_title", True) and o.get("title"):
|
|
605
|
+
ax.set_title(o["title"], fontsize=title_fontsize)
|
|
606
|
+
if o.get("xlabel"):
|
|
607
|
+
ax.set_xlabel(o["xlabel"], fontsize=axis_fontsize)
|
|
608
|
+
if o.get("ylabel"):
|
|
609
|
+
ax.set_ylabel(o["ylabel"], fontsize=axis_fontsize)
|
|
610
|
+
|
|
611
|
+
|
|
612
|
+
def _apply_caption(fig, o, caption_fontsize=7):
|
|
613
|
+
"""Apply caption below the figure."""
|
|
614
|
+
if not o.get("show_caption", False) or not o.get("caption"):
|
|
615
|
+
return None
|
|
616
|
+
|
|
617
|
+
caption_text = o.get("caption", "")
|
|
618
|
+
fontsize = o.get("caption_fontsize", caption_fontsize)
|
|
619
|
+
|
|
620
|
+
# Place caption below the figure
|
|
621
|
+
# Using fig.text with y position slightly below 0
|
|
622
|
+
caption_artist = fig.text(
|
|
623
|
+
0.5, -0.02, # Centered, below the figure
|
|
624
|
+
caption_text,
|
|
625
|
+
ha="center",
|
|
626
|
+
va="top",
|
|
627
|
+
fontsize=fontsize,
|
|
628
|
+
wrap=True,
|
|
629
|
+
transform=fig.transFigure,
|
|
630
|
+
)
|
|
631
|
+
return caption_artist
|
|
632
|
+
|
|
633
|
+
|
|
634
|
+
def _apply_tick_styling(
|
|
635
|
+
ax,
|
|
636
|
+
tick_fontsize,
|
|
637
|
+
tick_length_pt,
|
|
638
|
+
tick_width_pt,
|
|
639
|
+
tick_direction,
|
|
640
|
+
x_n_ticks,
|
|
641
|
+
y_n_ticks,
|
|
642
|
+
hide_x_ticks,
|
|
643
|
+
hide_y_ticks,
|
|
644
|
+
):
|
|
645
|
+
"""Apply tick styling to axes."""
|
|
646
|
+
ax.tick_params(
|
|
647
|
+
axis="both",
|
|
648
|
+
labelsize=tick_fontsize,
|
|
649
|
+
length=tick_length_pt,
|
|
650
|
+
width=tick_width_pt,
|
|
651
|
+
direction=tick_direction,
|
|
652
|
+
)
|
|
653
|
+
|
|
654
|
+
if hide_x_ticks:
|
|
655
|
+
ax.xaxis.set_ticks([])
|
|
656
|
+
ax.xaxis.set_ticklabels([])
|
|
657
|
+
else:
|
|
658
|
+
ax.xaxis.set_major_locator(MaxNLocator(nbins=x_n_ticks))
|
|
659
|
+
if hide_y_ticks:
|
|
660
|
+
ax.yaxis.set_ticks([])
|
|
661
|
+
ax.yaxis.set_ticklabels([])
|
|
662
|
+
else:
|
|
663
|
+
ax.yaxis.set_major_locator(MaxNLocator(nbins=y_n_ticks))
|
|
664
|
+
|
|
665
|
+
|
|
666
|
+
def _apply_style(ax, o, axis_width_pt):
|
|
667
|
+
"""Apply grid, axis limits, and spine settings."""
|
|
668
|
+
if o.get("grid"):
|
|
669
|
+
ax.grid(True, linewidth=axis_width_pt, alpha=0.3)
|
|
670
|
+
|
|
671
|
+
if o.get("xlim"):
|
|
672
|
+
ax.set_xlim(o["xlim"])
|
|
673
|
+
if o.get("ylim"):
|
|
674
|
+
ax.set_ylim(o["ylim"])
|
|
675
|
+
|
|
676
|
+
if o.get("hide_top_spine", True):
|
|
677
|
+
ax.spines["top"].set_visible(False)
|
|
678
|
+
if o.get("hide_right_spine", True):
|
|
679
|
+
ax.spines["right"].set_visible(False)
|
|
680
|
+
|
|
681
|
+
for spine in ax.spines.values():
|
|
682
|
+
spine.set_linewidth(axis_width_pt)
|
|
683
|
+
|
|
684
|
+
|
|
685
|
+
def _apply_annotations(ax, o, axis_fontsize):
|
|
686
|
+
"""Apply text annotations to figure."""
|
|
687
|
+
for annot in o.get("annotations", []):
|
|
688
|
+
if annot.get("type") == "text":
|
|
689
|
+
ax.text(
|
|
690
|
+
annot.get("x", 0.5),
|
|
691
|
+
annot.get("y", 0.5),
|
|
692
|
+
annot.get("text", ""),
|
|
693
|
+
transform=ax.transAxes,
|
|
694
|
+
fontsize=annot.get("fontsize", axis_fontsize),
|
|
695
|
+
)
|
|
696
|
+
|
|
697
|
+
|
|
698
|
+
def render_panel_preview(
|
|
699
|
+
panel_dir,
|
|
700
|
+
dark_mode: bool = False,
|
|
701
|
+
) -> Tuple[Optional[str], Optional[Dict[str, Any]], Optional[Dict[str, int]]]:
|
|
702
|
+
"""Render a panel from its pltz bundle directory with dark mode support.
|
|
703
|
+
|
|
704
|
+
Args:
|
|
705
|
+
panel_dir: Path to the .pltz.d panel directory
|
|
706
|
+
dark_mode: Whether to render with dark mode colors
|
|
707
|
+
|
|
708
|
+
Returns:
|
|
709
|
+
tuple: (base64_image_data, bboxes_dict, image_size) or (None, None, None) on error
|
|
710
|
+
"""
|
|
711
|
+
from pathlib import Path
|
|
712
|
+
import json
|
|
713
|
+
import pandas as pd
|
|
714
|
+
|
|
715
|
+
panel_dir = Path(panel_dir)
|
|
716
|
+
|
|
717
|
+
try:
|
|
718
|
+
# Load spec.json
|
|
719
|
+
spec_path = panel_dir / "spec.json"
|
|
720
|
+
if not spec_path.exists():
|
|
721
|
+
# Try legacy format
|
|
722
|
+
for f in panel_dir.glob("*.json"):
|
|
723
|
+
if f.name != "style.json":
|
|
724
|
+
spec_path = f
|
|
725
|
+
break
|
|
726
|
+
|
|
727
|
+
if not spec_path.exists():
|
|
728
|
+
return None, None, None
|
|
729
|
+
|
|
730
|
+
with open(spec_path, "r") as f:
|
|
731
|
+
metadata = json.load(f)
|
|
732
|
+
|
|
733
|
+
# Load CSV data
|
|
734
|
+
csv_data = None
|
|
735
|
+
csv_path = panel_dir / "data.csv"
|
|
736
|
+
if not csv_path.exists():
|
|
737
|
+
for f in panel_dir.glob("*.csv"):
|
|
738
|
+
csv_path = f
|
|
739
|
+
break
|
|
740
|
+
|
|
741
|
+
if csv_path.exists():
|
|
742
|
+
csv_data = pd.read_csv(csv_path)
|
|
743
|
+
|
|
744
|
+
# Load style.json for overrides
|
|
745
|
+
style_path = panel_dir / "style.json"
|
|
746
|
+
overrides = {}
|
|
747
|
+
if style_path.exists():
|
|
748
|
+
with open(style_path, "r") as f:
|
|
749
|
+
style = json.load(f)
|
|
750
|
+
# Convert style to overrides format
|
|
751
|
+
size = style.get("size", {})
|
|
752
|
+
if size:
|
|
753
|
+
width_mm = size.get("width_mm", 80)
|
|
754
|
+
height_mm = size.get("height_mm", 68)
|
|
755
|
+
overrides["fig_size"] = [width_mm / 25.4, height_mm / 25.4]
|
|
756
|
+
overrides["transparent"] = True
|
|
757
|
+
|
|
758
|
+
# Render with dark mode
|
|
759
|
+
return render_preview_with_bboxes(
|
|
760
|
+
csv_data, overrides,
|
|
761
|
+
metadata=metadata,
|
|
762
|
+
dark_mode=dark_mode,
|
|
763
|
+
)
|
|
764
|
+
|
|
765
|
+
except Exception as e:
|
|
766
|
+
import traceback
|
|
767
|
+
print(f"Error rendering panel {panel_dir}: {e}")
|
|
768
|
+
traceback.print_exc()
|
|
769
|
+
return None, None, None
|
|
770
|
+
|
|
771
|
+
|
|
772
|
+
# Dark mode theme colors
|
|
773
|
+
DARK_THEME_TEXT_COLOR = "#e8e8e8" # Light gray for visibility on dark background
|
|
774
|
+
DARK_THEME_SPINE_COLOR = "#e8e8e8"
|
|
775
|
+
DARK_THEME_TICK_COLOR = "#e8e8e8"
|
|
776
|
+
|
|
777
|
+
|
|
778
|
+
def _apply_dark_theme(ax):
|
|
779
|
+
"""Apply dark mode colors to axes for visibility on dark backgrounds.
|
|
780
|
+
|
|
781
|
+
Changes title, labels, tick labels, spines, and legend text to light colors.
|
|
782
|
+
"""
|
|
783
|
+
# Title
|
|
784
|
+
title = ax.get_title()
|
|
785
|
+
if title:
|
|
786
|
+
ax.title.set_color(DARK_THEME_TEXT_COLOR)
|
|
787
|
+
|
|
788
|
+
# Axis labels
|
|
789
|
+
ax.xaxis.label.set_color(DARK_THEME_TEXT_COLOR)
|
|
790
|
+
ax.yaxis.label.set_color(DARK_THEME_TEXT_COLOR)
|
|
791
|
+
|
|
792
|
+
# Tick labels
|
|
793
|
+
ax.tick_params(axis="both", colors=DARK_THEME_TICK_COLOR, labelcolor=DARK_THEME_TEXT_COLOR)
|
|
794
|
+
|
|
795
|
+
# Spines
|
|
796
|
+
for spine in ax.spines.values():
|
|
797
|
+
spine.set_color(DARK_THEME_SPINE_COLOR)
|
|
798
|
+
|
|
799
|
+
# Legend (if exists)
|
|
800
|
+
legend = ax.get_legend()
|
|
801
|
+
if legend:
|
|
802
|
+
for text in legend.get_texts():
|
|
803
|
+
text.set_color(DARK_THEME_TEXT_COLOR)
|
|
804
|
+
# Legend title
|
|
805
|
+
legend_title = legend.get_title()
|
|
806
|
+
if legend_title:
|
|
807
|
+
legend_title.set_color(DARK_THEME_TEXT_COLOR)
|
|
808
|
+
# Legend frame (make transparent or dark)
|
|
809
|
+
legend.get_frame().set_facecolor("none")
|
|
810
|
+
legend.get_frame().set_edgecolor(DARK_THEME_SPINE_COLOR)
|
|
811
|
+
|
|
812
|
+
|
|
813
|
+
# EOF
|