scitex 2.7.0__py3-none-any.whl → 2.7.3__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/fig/__init__.py +352 -0
- scitex/{vis → fig}/backend/_parser.py +1 -1
- scitex/{vis → fig}/canvas.py +1 -1
- scitex/{vis → fig}/editor/_defaults.py +70 -5
- scitex/fig/editor/_edit.py +751 -0
- scitex/{vis → fig}/editor/_qt_editor.py +181 -1
- scitex/fig/editor/flask_editor/_bbox.py +1276 -0
- scitex/fig/editor/flask_editor/_core.py +624 -0
- scitex/{vis → fig}/editor/flask_editor/_plotter.py +38 -4
- scitex/fig/editor/flask_editor/_renderer.py +739 -0
- scitex/{vis → fig}/editor/flask_editor/templates/__init__.py +1 -1
- scitex/fig/editor/flask_editor/templates/_html.py +834 -0
- scitex/fig/editor/flask_editor/templates/_scripts.py +3136 -0
- scitex/{vis → fig}/editor/flask_editor/templates/_styles.py +625 -18
- scitex/{vis → fig}/io/__init__.py +13 -1
- scitex/fig/io/_bundle.py +973 -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 +10 -26
- scitex/io/_bundle.py +434 -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/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.7.3.dist-info}/METADATA +11 -1
- {scitex-2.7.0.dist-info → scitex-2.7.3.dist-info}/RECORD +238 -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/_html.py +0 -513
- scitex/vis/editor/flask_editor/templates/_scripts.py +0 -1261
- /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/__init__.py +0 -0
- /scitex/{vis → fig}/editor/_dearpygui_editor.py +0 -0
- /scitex/{vis → fig}/editor/_flask_editor.py +0 -0
- /scitex/{vis → fig}/editor/_mpl_editor.py +0 -0
- /scitex/{vis → fig}/editor/_tkinter_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.7.3.dist-info}/WHEEL +0 -0
- {scitex-2.7.0.dist-info → scitex-2.7.3.dist-info}/entry_points.txt +0 -0
- {scitex-2.7.0.dist-info → scitex-2.7.3.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,624 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
# File: ./src/scitex/vis/editor/flask_editor/core.py
|
|
4
|
+
"""Core WebEditor class for Flask-based figure editing."""
|
|
5
|
+
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Dict, Any, Optional
|
|
8
|
+
import base64
|
|
9
|
+
import copy
|
|
10
|
+
import json
|
|
11
|
+
import threading
|
|
12
|
+
import webbrowser
|
|
13
|
+
|
|
14
|
+
from ._utils import find_available_port, kill_process_on_port, check_port_available
|
|
15
|
+
from .templates import build_html_template
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class WebEditor:
|
|
19
|
+
"""
|
|
20
|
+
Browser-based figure editor using Flask.
|
|
21
|
+
|
|
22
|
+
Features:
|
|
23
|
+
- Displays existing PNG from pltz bundle (no re-rendering)
|
|
24
|
+
- Hitmap-based element selection for precise clicking
|
|
25
|
+
- Property editors with sliders and color pickers
|
|
26
|
+
- Save to .manual.json
|
|
27
|
+
- SciTeX style defaults pre-filled
|
|
28
|
+
- Auto-finds available port if default is in use
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
def __init__(
|
|
32
|
+
self,
|
|
33
|
+
json_path: Path,
|
|
34
|
+
metadata: Dict[str, Any],
|
|
35
|
+
csv_data: Optional[Any] = None,
|
|
36
|
+
png_path: Optional[Path] = None,
|
|
37
|
+
hitmap_path: Optional[Path] = None,
|
|
38
|
+
manual_overrides: Optional[Dict[str, Any]] = None,
|
|
39
|
+
port: int = 5050,
|
|
40
|
+
panel_info: Optional[Dict[str, Any]] = None,
|
|
41
|
+
):
|
|
42
|
+
self.json_path = Path(json_path)
|
|
43
|
+
self.metadata = metadata
|
|
44
|
+
self.csv_data = csv_data
|
|
45
|
+
self.png_path = Path(png_path) if png_path else None
|
|
46
|
+
self.hitmap_path = Path(hitmap_path) if hitmap_path else None
|
|
47
|
+
self.manual_overrides = manual_overrides or {}
|
|
48
|
+
self._requested_port = port
|
|
49
|
+
self.port = port
|
|
50
|
+
self.panel_info = panel_info # For multi-panel figz bundles
|
|
51
|
+
|
|
52
|
+
# Extract hit_regions from metadata for color-based element detection
|
|
53
|
+
self.hit_regions = metadata.get("hit_regions", {})
|
|
54
|
+
self.color_map = self.hit_regions.get("color_map", {})
|
|
55
|
+
|
|
56
|
+
# Get SciTeX defaults and merge with metadata
|
|
57
|
+
from .._defaults import get_scitex_defaults, extract_defaults_from_metadata
|
|
58
|
+
|
|
59
|
+
self.scitex_defaults = get_scitex_defaults()
|
|
60
|
+
self.metadata_defaults = extract_defaults_from_metadata(metadata)
|
|
61
|
+
|
|
62
|
+
# Start with defaults, then overlay manual overrides
|
|
63
|
+
self.current_overrides = copy.deepcopy(self.scitex_defaults)
|
|
64
|
+
self.current_overrides.update(self.metadata_defaults)
|
|
65
|
+
self.current_overrides.update(self.manual_overrides)
|
|
66
|
+
|
|
67
|
+
# Track initial state to detect modifications
|
|
68
|
+
self._initial_overrides = copy.deepcopy(self.current_overrides)
|
|
69
|
+
self._user_modified = False
|
|
70
|
+
|
|
71
|
+
def run(self):
|
|
72
|
+
"""Launch the web editor."""
|
|
73
|
+
try:
|
|
74
|
+
from flask import Flask, render_template_string, request, jsonify
|
|
75
|
+
except ImportError:
|
|
76
|
+
raise ImportError(
|
|
77
|
+
"Flask is required for web editor. Install: pip install flask"
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
# Handle port conflicts - always use port 5050
|
|
81
|
+
import time
|
|
82
|
+
max_retries = 3
|
|
83
|
+
for attempt in range(max_retries):
|
|
84
|
+
if check_port_available(self._requested_port):
|
|
85
|
+
self.port = self._requested_port
|
|
86
|
+
break
|
|
87
|
+
print(f"Port {self._requested_port} in use. Freeing... (attempt {attempt + 1}/{max_retries})")
|
|
88
|
+
kill_process_on_port(self._requested_port)
|
|
89
|
+
time.sleep(1.0) # Wait for port release
|
|
90
|
+
else:
|
|
91
|
+
# After retries, use requested port anyway (Flask will error if unavailable)
|
|
92
|
+
print(f"Warning: Port {self._requested_port} may still be in use")
|
|
93
|
+
self.port = self._requested_port
|
|
94
|
+
|
|
95
|
+
app = Flask(__name__)
|
|
96
|
+
editor = self
|
|
97
|
+
|
|
98
|
+
@app.route("/")
|
|
99
|
+
def index():
|
|
100
|
+
# Rebuild template each time for hot reload support
|
|
101
|
+
html_template = build_html_template()
|
|
102
|
+
|
|
103
|
+
# Extract figz and panel paths for display
|
|
104
|
+
json_path_str = str(editor.json_path.resolve())
|
|
105
|
+
figz_path = ""
|
|
106
|
+
panel_path = ""
|
|
107
|
+
|
|
108
|
+
# Check if this is inside a figz bundle
|
|
109
|
+
if '.figz.d/' in json_path_str:
|
|
110
|
+
parts = json_path_str.split('.figz.d/')
|
|
111
|
+
figz_path = parts[0] + '.figz.d'
|
|
112
|
+
panel_path = parts[1] if len(parts) > 1 else ""
|
|
113
|
+
elif '.pltz.d/' in json_path_str:
|
|
114
|
+
parts = json_path_str.split('.pltz.d/')
|
|
115
|
+
figz_path = parts[0] + '.pltz.d'
|
|
116
|
+
panel_path = parts[1] if len(parts) > 1 else ""
|
|
117
|
+
else:
|
|
118
|
+
figz_path = json_path_str
|
|
119
|
+
|
|
120
|
+
return render_template_string(
|
|
121
|
+
html_template,
|
|
122
|
+
filename=figz_path,
|
|
123
|
+
panel_path=panel_path,
|
|
124
|
+
overrides=json.dumps(editor.current_overrides),
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
@app.route("/preview")
|
|
128
|
+
def preview():
|
|
129
|
+
"""Render figure preview with current overrides (same logic as /update)."""
|
|
130
|
+
from ._renderer import render_preview_with_bboxes
|
|
131
|
+
|
|
132
|
+
# Always use renderer for consistency between initial and updated views
|
|
133
|
+
dark_mode = request.args.get("dark_mode", "false").lower() == "true"
|
|
134
|
+
img_data, bboxes, img_size = render_preview_with_bboxes(
|
|
135
|
+
editor.csv_data, editor.current_overrides,
|
|
136
|
+
metadata=editor.metadata,
|
|
137
|
+
dark_mode=dark_mode,
|
|
138
|
+
)
|
|
139
|
+
return jsonify({
|
|
140
|
+
"image": img_data,
|
|
141
|
+
"bboxes": bboxes,
|
|
142
|
+
"img_size": img_size,
|
|
143
|
+
"has_hitmap": editor.hitmap_path is not None and editor.hitmap_path.exists(),
|
|
144
|
+
"format": "png",
|
|
145
|
+
"panel_info": editor.panel_info,
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
@app.route("/panels")
|
|
149
|
+
def panels():
|
|
150
|
+
"""Return all panel images with bboxes for interactive grid view (figz bundles only)."""
|
|
151
|
+
from PIL import Image
|
|
152
|
+
from ._bbox import extract_bboxes_from_metadata, extract_bboxes_from_geometry_px
|
|
153
|
+
|
|
154
|
+
if not editor.panel_info:
|
|
155
|
+
return jsonify({"error": "Not a multi-panel figz bundle"}), 400
|
|
156
|
+
|
|
157
|
+
figz_dir = Path(editor.panel_info["figz_dir"])
|
|
158
|
+
panel_names = editor.panel_info["panels"]
|
|
159
|
+
panel_images = []
|
|
160
|
+
|
|
161
|
+
for panel_name in panel_names:
|
|
162
|
+
panel_dir = figz_dir / panel_name
|
|
163
|
+
panel_data = {"name": panel_name.replace(".pltz.d", ""), "image": None, "bboxes": None, "img_size": None}
|
|
164
|
+
|
|
165
|
+
# Find PNG in exports/ or root
|
|
166
|
+
png_path = None
|
|
167
|
+
exports_dir = panel_dir / "exports"
|
|
168
|
+
if exports_dir.exists():
|
|
169
|
+
for f in exports_dir.glob("*.png"):
|
|
170
|
+
if "_hitmap" not in f.name and "_overview" not in f.name:
|
|
171
|
+
png_path = f
|
|
172
|
+
break
|
|
173
|
+
if not png_path:
|
|
174
|
+
for f in panel_dir.glob("*.png"):
|
|
175
|
+
if "_hitmap" not in f.name and "_overview" not in f.name:
|
|
176
|
+
png_path = f
|
|
177
|
+
break
|
|
178
|
+
|
|
179
|
+
if png_path and png_path.exists():
|
|
180
|
+
with open(png_path, "rb") as f:
|
|
181
|
+
panel_data["image"] = base64.b64encode(f.read()).decode("utf-8")
|
|
182
|
+
img = Image.open(png_path)
|
|
183
|
+
panel_data["width"], panel_data["height"] = img.size
|
|
184
|
+
panel_data["img_size"] = {"width": img.size[0], "height": img.size[1]}
|
|
185
|
+
img.close()
|
|
186
|
+
|
|
187
|
+
# Try to load geometry_px.json from cache (has precise pixel coordinates)
|
|
188
|
+
geometry_path = panel_dir / "cache" / "geometry_px.json"
|
|
189
|
+
if geometry_path.exists():
|
|
190
|
+
import json
|
|
191
|
+
with open(geometry_path) as f:
|
|
192
|
+
geometry_data = json.load(f)
|
|
193
|
+
panel_data["bboxes"] = extract_bboxes_from_geometry_px(
|
|
194
|
+
geometry_data,
|
|
195
|
+
panel_data["img_size"]["width"],
|
|
196
|
+
panel_data["img_size"]["height"]
|
|
197
|
+
)
|
|
198
|
+
else:
|
|
199
|
+
# Fall back to spec.json extraction
|
|
200
|
+
spec_path = panel_dir / "spec.json"
|
|
201
|
+
if spec_path.exists():
|
|
202
|
+
import json
|
|
203
|
+
with open(spec_path) as f:
|
|
204
|
+
panel_metadata = json.load(f)
|
|
205
|
+
panel_data["bboxes"] = extract_bboxes_from_metadata(
|
|
206
|
+
panel_metadata,
|
|
207
|
+
panel_data["img_size"]["width"],
|
|
208
|
+
panel_data["img_size"]["height"]
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
panel_images.append(panel_data)
|
|
212
|
+
|
|
213
|
+
return jsonify({
|
|
214
|
+
"panels": panel_images,
|
|
215
|
+
"count": len(panel_images),
|
|
216
|
+
})
|
|
217
|
+
|
|
218
|
+
@app.route("/switch_panel/<int:panel_index>")
|
|
219
|
+
def switch_panel(panel_index):
|
|
220
|
+
"""Switch to a different panel in the figz bundle.
|
|
221
|
+
|
|
222
|
+
Loads the actual PNG from the panel's exports folder instead of re-rendering.
|
|
223
|
+
"""
|
|
224
|
+
from PIL import Image
|
|
225
|
+
from .._edit import _load_panel_data
|
|
226
|
+
from ._bbox import extract_bboxes_from_metadata, extract_bboxes_from_geometry_px
|
|
227
|
+
|
|
228
|
+
if not editor.panel_info:
|
|
229
|
+
return jsonify({"error": "Not a multi-panel figz bundle"}), 400
|
|
230
|
+
|
|
231
|
+
panels = editor.panel_info["panels"]
|
|
232
|
+
if panel_index < 0 or panel_index >= len(panels):
|
|
233
|
+
return jsonify({"error": f"Invalid panel index: {panel_index}"}), 400
|
|
234
|
+
|
|
235
|
+
figz_dir = Path(editor.panel_info["figz_dir"])
|
|
236
|
+
panel_name = panels[panel_index]
|
|
237
|
+
panel_dir = figz_dir / panel_name
|
|
238
|
+
|
|
239
|
+
# Load the panel's data
|
|
240
|
+
try:
|
|
241
|
+
panel_data = _load_panel_data(panel_dir)
|
|
242
|
+
if not panel_data:
|
|
243
|
+
return jsonify({"error": f"Could not load panel: {panel_name}"}), 400
|
|
244
|
+
|
|
245
|
+
# Update editor state
|
|
246
|
+
editor.json_path = panel_data["json_path"]
|
|
247
|
+
editor.metadata = panel_data["metadata"]
|
|
248
|
+
editor.csv_data = panel_data.get("csv_data")
|
|
249
|
+
editor.png_path = panel_data.get("png_path")
|
|
250
|
+
editor.hitmap_path = panel_data.get("hitmap_path")
|
|
251
|
+
editor.panel_info["current_index"] = panel_index
|
|
252
|
+
|
|
253
|
+
# Re-extract defaults from new metadata
|
|
254
|
+
from .._defaults import get_scitex_defaults, extract_defaults_from_metadata
|
|
255
|
+
editor.scitex_defaults = get_scitex_defaults()
|
|
256
|
+
editor.metadata_defaults = extract_defaults_from_metadata(editor.metadata)
|
|
257
|
+
editor.current_overrides = copy.deepcopy(editor.scitex_defaults)
|
|
258
|
+
editor.current_overrides.update(editor.metadata_defaults)
|
|
259
|
+
editor.current_overrides.update(editor.manual_overrides)
|
|
260
|
+
|
|
261
|
+
# Load actual PNG from panel instead of re-rendering
|
|
262
|
+
img_data = None
|
|
263
|
+
img_size = {"width": 0, "height": 0}
|
|
264
|
+
png_path = panel_data.get("png_path")
|
|
265
|
+
|
|
266
|
+
if png_path and png_path.exists():
|
|
267
|
+
with open(png_path, "rb") as f:
|
|
268
|
+
img_data = base64.b64encode(f.read()).decode("utf-8")
|
|
269
|
+
img = Image.open(png_path)
|
|
270
|
+
img_size = {"width": img.size[0], "height": img.size[1]}
|
|
271
|
+
img.close()
|
|
272
|
+
else:
|
|
273
|
+
# Fallback: look for any PNG in exports/
|
|
274
|
+
exports_dir = panel_dir / "exports"
|
|
275
|
+
if exports_dir.exists():
|
|
276
|
+
for f in exports_dir.glob("*.png"):
|
|
277
|
+
if "_hitmap" not in f.name and "_overview" not in f.name:
|
|
278
|
+
with open(f, "rb") as pf:
|
|
279
|
+
img_data = base64.b64encode(pf.read()).decode("utf-8")
|
|
280
|
+
img = Image.open(f)
|
|
281
|
+
img_size = {"width": img.size[0], "height": img.size[1]}
|
|
282
|
+
img.close()
|
|
283
|
+
break
|
|
284
|
+
|
|
285
|
+
if not img_data:
|
|
286
|
+
return jsonify({"error": f"No PNG found for panel: {panel_name}"}), 400
|
|
287
|
+
|
|
288
|
+
# Extract bboxes - prefer geometry_px.json for precise coordinates
|
|
289
|
+
bboxes = {}
|
|
290
|
+
geometry_path = panel_dir / "cache" / "geometry_px.json"
|
|
291
|
+
if geometry_path.exists():
|
|
292
|
+
with open(geometry_path) as f:
|
|
293
|
+
geometry_data = json.load(f)
|
|
294
|
+
bboxes = extract_bboxes_from_geometry_px(
|
|
295
|
+
geometry_data,
|
|
296
|
+
img_size["width"],
|
|
297
|
+
img_size["height"],
|
|
298
|
+
)
|
|
299
|
+
else:
|
|
300
|
+
# Fall back to metadata extraction
|
|
301
|
+
bboxes = extract_bboxes_from_metadata(
|
|
302
|
+
editor.metadata,
|
|
303
|
+
img_size["width"],
|
|
304
|
+
img_size["height"],
|
|
305
|
+
)
|
|
306
|
+
|
|
307
|
+
return jsonify({
|
|
308
|
+
"success": True,
|
|
309
|
+
"panel_name": panel_name,
|
|
310
|
+
"panel_index": panel_index,
|
|
311
|
+
"image": img_data,
|
|
312
|
+
"bboxes": bboxes,
|
|
313
|
+
"img_size": img_size,
|
|
314
|
+
"overrides": editor.current_overrides,
|
|
315
|
+
})
|
|
316
|
+
except Exception as e:
|
|
317
|
+
import traceback
|
|
318
|
+
return jsonify({
|
|
319
|
+
"error": f"Failed to switch panel: {str(e)}",
|
|
320
|
+
"traceback": traceback.format_exc(),
|
|
321
|
+
}), 500
|
|
322
|
+
|
|
323
|
+
@app.route("/hitmap")
|
|
324
|
+
def hitmap():
|
|
325
|
+
"""Return hitmap PNG for element detection."""
|
|
326
|
+
if editor.hitmap_path and editor.hitmap_path.exists():
|
|
327
|
+
with open(editor.hitmap_path, "rb") as f:
|
|
328
|
+
img_data = base64.b64encode(f.read()).decode("utf-8")
|
|
329
|
+
return jsonify({
|
|
330
|
+
"image": img_data,
|
|
331
|
+
"color_map": editor.color_map,
|
|
332
|
+
})
|
|
333
|
+
return jsonify({"error": "No hitmap available"}), 404
|
|
334
|
+
|
|
335
|
+
@app.route("/color_map")
|
|
336
|
+
def color_map():
|
|
337
|
+
"""Return color map for hitmap element identification."""
|
|
338
|
+
return jsonify({
|
|
339
|
+
"color_map": editor.color_map,
|
|
340
|
+
"hit_regions": editor.hit_regions,
|
|
341
|
+
})
|
|
342
|
+
|
|
343
|
+
@app.route("/update", methods=["POST"])
|
|
344
|
+
def update():
|
|
345
|
+
"""Update overrides and re-render with updated properties."""
|
|
346
|
+
from ._renderer import render_preview_with_bboxes
|
|
347
|
+
|
|
348
|
+
data = request.json
|
|
349
|
+
editor.current_overrides.update(data.get("overrides", {}))
|
|
350
|
+
editor._user_modified = True
|
|
351
|
+
|
|
352
|
+
# Check if dark mode is requested from POST data
|
|
353
|
+
dark_mode = data.get("dark_mode", False)
|
|
354
|
+
|
|
355
|
+
# Re-render the figure with updated overrides
|
|
356
|
+
img_data, bboxes, img_size = render_preview_with_bboxes(
|
|
357
|
+
editor.csv_data, editor.current_overrides,
|
|
358
|
+
metadata=editor.metadata,
|
|
359
|
+
dark_mode=dark_mode,
|
|
360
|
+
)
|
|
361
|
+
return jsonify({
|
|
362
|
+
"image": img_data,
|
|
363
|
+
"bboxes": bboxes,
|
|
364
|
+
"img_size": img_size,
|
|
365
|
+
"status": "updated",
|
|
366
|
+
})
|
|
367
|
+
|
|
368
|
+
@app.route("/save", methods=["POST"])
|
|
369
|
+
def save():
|
|
370
|
+
"""Save to .manual.json."""
|
|
371
|
+
from .._edit import save_manual_overrides
|
|
372
|
+
|
|
373
|
+
try:
|
|
374
|
+
manual_path = save_manual_overrides(
|
|
375
|
+
editor.json_path, editor.current_overrides
|
|
376
|
+
)
|
|
377
|
+
return jsonify({"status": "saved", "path": str(manual_path)})
|
|
378
|
+
except Exception as e:
|
|
379
|
+
return jsonify({"status": "error", "message": str(e)}), 500
|
|
380
|
+
|
|
381
|
+
@app.route("/shutdown", methods=["POST"])
|
|
382
|
+
def shutdown():
|
|
383
|
+
"""Shutdown the server."""
|
|
384
|
+
func = request.environ.get("werkzeug.server.shutdown")
|
|
385
|
+
if func is None:
|
|
386
|
+
raise RuntimeError("Not running with Werkzeug Server")
|
|
387
|
+
func()
|
|
388
|
+
return jsonify({"status": "shutdown"})
|
|
389
|
+
|
|
390
|
+
@app.route("/stats")
|
|
391
|
+
def stats():
|
|
392
|
+
"""Return statistical test results from figure metadata."""
|
|
393
|
+
stats_data = editor.metadata.get("stats", [])
|
|
394
|
+
stats_summary = editor.metadata.get("stats_summary", None)
|
|
395
|
+
return jsonify({
|
|
396
|
+
"stats": stats_data,
|
|
397
|
+
"stats_summary": stats_summary,
|
|
398
|
+
"has_stats": len(stats_data) > 0,
|
|
399
|
+
})
|
|
400
|
+
|
|
401
|
+
# Open browser after short delay
|
|
402
|
+
def open_browser():
|
|
403
|
+
import time
|
|
404
|
+
|
|
405
|
+
time.sleep(0.5)
|
|
406
|
+
webbrowser.open(f"http://127.0.0.1:{self.port}")
|
|
407
|
+
|
|
408
|
+
threading.Thread(target=open_browser, daemon=True).start()
|
|
409
|
+
|
|
410
|
+
print(f"Starting SciTeX Figure Editor at http://127.0.0.1:{self.port}")
|
|
411
|
+
print("Press Ctrl+C to stop")
|
|
412
|
+
|
|
413
|
+
# Note: use_reloader=False because the reloader re-runs the entire script
|
|
414
|
+
# which causes infinite loops when the demo generates figures
|
|
415
|
+
# Templates are rebuilt on each page refresh anyway
|
|
416
|
+
app.run(host="127.0.0.1", port=self.port, debug=False, use_reloader=False)
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
def _extract_bboxes_from_metadata(
|
|
420
|
+
metadata: Dict[str, Any],
|
|
421
|
+
display_width: Optional[float] = None,
|
|
422
|
+
display_height: Optional[float] = None
|
|
423
|
+
) -> Dict[str, Any]:
|
|
424
|
+
"""Extract element bounding boxes from pltz metadata.
|
|
425
|
+
|
|
426
|
+
Builds bboxes from selectable_regions in the metadata for click detection.
|
|
427
|
+
This allows the editor to highlight elements when clicked.
|
|
428
|
+
|
|
429
|
+
Coordinate system (new layered format):
|
|
430
|
+
- selectable_regions bbox_px: Already in final image space (figure_px)
|
|
431
|
+
- Display size: Actual displayed image size (PNG pixels or SVG viewBox)
|
|
432
|
+
- Scale = display_size / figure_px (usually 1:1, but may differ for scaled display)
|
|
433
|
+
|
|
434
|
+
Parameters
|
|
435
|
+
----------
|
|
436
|
+
metadata : dict
|
|
437
|
+
The pltz JSON metadata containing selectable_regions
|
|
438
|
+
display_width : float, optional
|
|
439
|
+
Actual display image width (from PNG size or SVG viewBox)
|
|
440
|
+
display_height : float, optional
|
|
441
|
+
Actual display image height (from PNG size or SVG viewBox)
|
|
442
|
+
|
|
443
|
+
Returns
|
|
444
|
+
-------
|
|
445
|
+
dict
|
|
446
|
+
Mapping of element IDs to their bounding box coordinates (in display pixels)
|
|
447
|
+
"""
|
|
448
|
+
bboxes = {}
|
|
449
|
+
selectable = metadata.get("selectable_regions", {})
|
|
450
|
+
|
|
451
|
+
# Figure dimensions from new layered format (bbox_px are in this space)
|
|
452
|
+
figure_px = metadata.get("figure_px", [])
|
|
453
|
+
if isinstance(figure_px, list) and len(figure_px) >= 2:
|
|
454
|
+
fig_width = figure_px[0]
|
|
455
|
+
fig_height = figure_px[1]
|
|
456
|
+
else:
|
|
457
|
+
# Fallback for old format: try hit_regions.path_data.figure
|
|
458
|
+
hit_regions = metadata.get("hit_regions", {})
|
|
459
|
+
path_data = hit_regions.get("path_data", {})
|
|
460
|
+
orig_fig = path_data.get("figure", {})
|
|
461
|
+
fig_width = orig_fig.get("width_px", 944)
|
|
462
|
+
fig_height = orig_fig.get("height_px", 803)
|
|
463
|
+
|
|
464
|
+
# Use actual display dimensions if provided, else use figure_px
|
|
465
|
+
if display_width is None:
|
|
466
|
+
display_width = fig_width
|
|
467
|
+
if display_height is None:
|
|
468
|
+
display_height = fig_height
|
|
469
|
+
|
|
470
|
+
# Scale factor: display / figure_px
|
|
471
|
+
# Usually 1:1 since display is the same PNG, but may differ for scaled display
|
|
472
|
+
scale_x = display_width / fig_width if fig_width > 0 else 1
|
|
473
|
+
scale_y = display_height / fig_height if fig_height > 0 else 1
|
|
474
|
+
|
|
475
|
+
# Helper to convert coords to display pixels
|
|
476
|
+
def to_display_bbox(bbox, is_list=True):
|
|
477
|
+
"""Convert bbox to display pixels (apply scaling if display != figure_px).
|
|
478
|
+
|
|
479
|
+
Parameters
|
|
480
|
+
----------
|
|
481
|
+
bbox : list or dict
|
|
482
|
+
Bbox coordinates [x0, y0, x1, y1] or dict with keys
|
|
483
|
+
is_list : bool
|
|
484
|
+
Whether bbox is a list (True) or dict (False)
|
|
485
|
+
"""
|
|
486
|
+
if is_list:
|
|
487
|
+
x0, y0, x1, y1 = bbox[0], bbox[1], bbox[2], bbox[3]
|
|
488
|
+
else:
|
|
489
|
+
x0 = bbox.get("x0", 0)
|
|
490
|
+
y0 = bbox.get("y0", 0)
|
|
491
|
+
x1 = bbox.get("x1", bbox.get("x0", 0) + bbox.get("width", 0))
|
|
492
|
+
y1 = bbox.get("y1", bbox.get("y0", 0) + bbox.get("height", 0))
|
|
493
|
+
|
|
494
|
+
# Scale to display coords (usually 1:1)
|
|
495
|
+
disp_x0 = x0 * scale_x
|
|
496
|
+
disp_x1 = x1 * scale_x
|
|
497
|
+
disp_y0 = y0 * scale_y
|
|
498
|
+
disp_y1 = y1 * scale_y
|
|
499
|
+
|
|
500
|
+
return {
|
|
501
|
+
"x0": disp_x0,
|
|
502
|
+
"y0": disp_y0,
|
|
503
|
+
"x1": disp_x1,
|
|
504
|
+
"y1": disp_y1,
|
|
505
|
+
"x": disp_x0,
|
|
506
|
+
"y": disp_y0,
|
|
507
|
+
"width": disp_x1 - disp_x0,
|
|
508
|
+
"height": disp_y1 - disp_y0,
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
# Extract from selectable_regions.axes
|
|
512
|
+
axes_regions = selectable.get("axes", [])
|
|
513
|
+
for ax_idx, ax in enumerate(axes_regions):
|
|
514
|
+
ax_key = f"ax_{ax_idx:02d}"
|
|
515
|
+
|
|
516
|
+
# Title
|
|
517
|
+
title = ax.get("title", {})
|
|
518
|
+
if title and "bbox_px" in title:
|
|
519
|
+
bbox_disp = to_display_bbox(title["bbox_px"])
|
|
520
|
+
bboxes[f"{ax_key}_title"] = {
|
|
521
|
+
**bbox_disp,
|
|
522
|
+
"type": "title",
|
|
523
|
+
"text": title.get("text", ""),
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
# X label
|
|
527
|
+
xlabel = ax.get("xlabel", {})
|
|
528
|
+
if xlabel and "bbox_px" in xlabel:
|
|
529
|
+
bbox_disp = to_display_bbox(xlabel["bbox_px"])
|
|
530
|
+
bboxes[f"{ax_key}_xlabel"] = {
|
|
531
|
+
**bbox_disp,
|
|
532
|
+
"type": "xlabel",
|
|
533
|
+
"text": xlabel.get("text", ""),
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
# Y label
|
|
537
|
+
ylabel = ax.get("ylabel", {})
|
|
538
|
+
if ylabel and "bbox_px" in ylabel:
|
|
539
|
+
bbox_disp = to_display_bbox(ylabel["bbox_px"])
|
|
540
|
+
bboxes[f"{ax_key}_ylabel"] = {
|
|
541
|
+
**bbox_disp,
|
|
542
|
+
"type": "ylabel",
|
|
543
|
+
"text": ylabel.get("text", ""),
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
# Legend
|
|
547
|
+
legend = ax.get("legend", {})
|
|
548
|
+
if legend and "bbox_px" in legend:
|
|
549
|
+
bbox_disp = to_display_bbox(legend["bbox_px"])
|
|
550
|
+
bboxes[f"{ax_key}_legend"] = {
|
|
551
|
+
**bbox_disp,
|
|
552
|
+
"type": "legend",
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
# X-axis spine
|
|
556
|
+
xaxis = ax.get("xaxis", {})
|
|
557
|
+
if xaxis:
|
|
558
|
+
spine = xaxis.get("spine", {})
|
|
559
|
+
if spine and "bbox_px" in spine:
|
|
560
|
+
bbox_disp = to_display_bbox(spine["bbox_px"])
|
|
561
|
+
bboxes[f"{ax_key}_xaxis_spine"] = {
|
|
562
|
+
**bbox_disp,
|
|
563
|
+
"type": "xaxis",
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
# Y-axis spine
|
|
567
|
+
yaxis = ax.get("yaxis", {})
|
|
568
|
+
if yaxis:
|
|
569
|
+
spine = yaxis.get("spine", {})
|
|
570
|
+
if spine and "bbox_px" in spine:
|
|
571
|
+
bbox_disp = to_display_bbox(spine["bbox_px"])
|
|
572
|
+
bboxes[f"{ax_key}_yaxis_spine"] = {
|
|
573
|
+
**bbox_disp,
|
|
574
|
+
"type": "yaxis",
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
# Extract traces from artists (top-level in new format, or hit_regions.path_data in old)
|
|
578
|
+
artists = metadata.get("artists", [])
|
|
579
|
+
if not artists:
|
|
580
|
+
# Fallback for old format
|
|
581
|
+
hit_regions = metadata.get("hit_regions", {})
|
|
582
|
+
path_data = hit_regions.get("path_data", {})
|
|
583
|
+
artists = path_data.get("artists", [])
|
|
584
|
+
|
|
585
|
+
for artist in artists:
|
|
586
|
+
artist_id = artist.get("id", 0)
|
|
587
|
+
artist_type = artist.get("type", "line")
|
|
588
|
+
bbox_px = artist.get("bbox_px", {})
|
|
589
|
+
if bbox_px:
|
|
590
|
+
bbox_disp = to_display_bbox(bbox_px, is_list=False)
|
|
591
|
+
trace_entry = {
|
|
592
|
+
**bbox_disp,
|
|
593
|
+
"type": artist_type,
|
|
594
|
+
"label": artist.get("label", f"Trace {artist_id}"),
|
|
595
|
+
"element_type": artist_type,
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
# Include scaled path points for line proximity detection
|
|
599
|
+
path_px = artist.get("path_px", [])
|
|
600
|
+
if path_px:
|
|
601
|
+
scaled_points = [
|
|
602
|
+
[pt[0] * scale_x, pt[1] * scale_y]
|
|
603
|
+
for pt in path_px if len(pt) >= 2
|
|
604
|
+
]
|
|
605
|
+
trace_entry["points"] = scaled_points
|
|
606
|
+
|
|
607
|
+
bboxes[f"trace_{artist_id}"] = trace_entry
|
|
608
|
+
|
|
609
|
+
# Add metadata for JavaScript to understand the coordinate system
|
|
610
|
+
bboxes["_meta"] = {
|
|
611
|
+
"display_width": display_width,
|
|
612
|
+
"display_height": display_height,
|
|
613
|
+
"figure_px_width": fig_width,
|
|
614
|
+
"figure_px_height": fig_height,
|
|
615
|
+
"scale_x": scale_x,
|
|
616
|
+
"scale_y": scale_y,
|
|
617
|
+
# Note: With new layered format, bbox_px are already in final image space
|
|
618
|
+
# so scale is typically 1:1 (unless display is resized)
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
return bboxes
|
|
622
|
+
|
|
623
|
+
|
|
624
|
+
# EOF
|
|
@@ -206,6 +206,8 @@ def plot_from_recipe(
|
|
|
206
206
|
_render_imshow(ax, df, data_ref, kwargs)
|
|
207
207
|
elif method == "contour":
|
|
208
208
|
_render_contour(ax, df, data_ref, kwargs)
|
|
209
|
+
elif method == "contourf":
|
|
210
|
+
_render_contourf(ax, df, data_ref, kwargs)
|
|
209
211
|
elif method in ("stx_shaded_line", "stx_fillv", "stx_violin",
|
|
210
212
|
"stx_box", "stx_rectangle", "stx_raster"):
|
|
211
213
|
_render_stx_method(ax, df, method, data_ref, kwargs)
|
|
@@ -393,6 +395,26 @@ def _render_contour(ax, df, data_ref, kwargs):
|
|
|
393
395
|
ax.contour(X, Y, Z, **kwargs)
|
|
394
396
|
|
|
395
397
|
|
|
398
|
+
def _render_contourf(ax, df, data_ref, kwargs):
|
|
399
|
+
"""Render filled contour plot."""
|
|
400
|
+
x_col = data_ref.get("x", "")
|
|
401
|
+
y_col = data_ref.get("y", "")
|
|
402
|
+
z_col = data_ref.get("z", "")
|
|
403
|
+
|
|
404
|
+
x = _get_column_data(df, x_col)
|
|
405
|
+
y = _get_column_data(df, y_col)
|
|
406
|
+
z = _get_column_data(df, z_col)
|
|
407
|
+
|
|
408
|
+
if x is not None and y is not None and z is not None:
|
|
409
|
+
# Assume data is on a grid - reconstruct
|
|
410
|
+
n = int(np.sqrt(len(x)))
|
|
411
|
+
if n * n == len(x):
|
|
412
|
+
X = x.reshape(n, n)
|
|
413
|
+
Y = y.reshape(n, n)
|
|
414
|
+
Z = z.reshape(n, n)
|
|
415
|
+
ax.contourf(X, Y, Z, **kwargs)
|
|
416
|
+
|
|
417
|
+
|
|
396
418
|
def _render_stx_method(ax, df, method, data_ref, kwargs):
|
|
397
419
|
"""Render scitex-specific methods (shaded_line, fillv, etc.)."""
|
|
398
420
|
# These are custom methods - for now, skip or implement basic versions
|
|
@@ -452,7 +474,18 @@ def _render_generic(ax, df, method, data_ref, kwargs, linewidth):
|
|
|
452
474
|
y = _get_column_data(df, y_col)
|
|
453
475
|
|
|
454
476
|
if x is not None and y is not None and len(x) == len(y):
|
|
455
|
-
ax.plot(
|
|
477
|
+
# Filter out kwargs that are not valid for ax.plot()
|
|
478
|
+
invalid_plot_kwargs = {
|
|
479
|
+
'levels', 'extend', 'origin', 'extent', 'aspect',
|
|
480
|
+
'norm', 'vmin', 'vmax', 'interpolation', 'filternorm',
|
|
481
|
+
'filterrad', 'resample', 'bins', 'range', 'density',
|
|
482
|
+
'weights', 'cumulative', 'bottom', 'histtype', 'align',
|
|
483
|
+
'orientation', 'rwidth', 'log', 'stacked', 'data',
|
|
484
|
+
'width', 'height', 'edgecolors', 's', 'c', 'facecolors',
|
|
485
|
+
}
|
|
486
|
+
filtered_kwargs = {k: v for k, v in kwargs.items()
|
|
487
|
+
if k not in invalid_plot_kwargs}
|
|
488
|
+
ax.plot(x, y, linewidth=linewidth, **filtered_kwargs)
|
|
456
489
|
|
|
457
490
|
|
|
458
491
|
def _plot_with_traces(
|
|
@@ -469,11 +502,12 @@ def _plot_with_traces(
|
|
|
469
502
|
):
|
|
470
503
|
"""Plot using trace information from overrides."""
|
|
471
504
|
for trace in traces:
|
|
505
|
+
# Support both old format (csv_columns.x/y) and new format (x_col/y_col)
|
|
472
506
|
csv_cols = trace.get("csv_columns", {})
|
|
473
|
-
x_col = csv_cols.get("x")
|
|
474
|
-
y_col = csv_cols.get("y")
|
|
507
|
+
x_col = csv_cols.get("x") or trace.get("x_col")
|
|
508
|
+
y_col = csv_cols.get("y") or trace.get("y_col")
|
|
475
509
|
|
|
476
|
-
if x_col in df.columns and y_col in df.columns:
|
|
510
|
+
if x_col and y_col and x_col in df.columns and y_col in df.columns:
|
|
477
511
|
ax.plot(
|
|
478
512
|
df[x_col],
|
|
479
513
|
df[y_col],
|