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,3136 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
# File: ./src/scitex/vis/editor/flask_editor/templates/scripts.py
|
|
4
|
+
"""JavaScript for the Flask editor UI."""
|
|
5
|
+
|
|
6
|
+
JS_SCRIPTS = """
|
|
7
|
+
let overrides = {{ overrides|safe }};
|
|
8
|
+
let traces = overrides.traces || [];
|
|
9
|
+
let elementBboxes = {};
|
|
10
|
+
let imgSize = {width: 0, height: 0};
|
|
11
|
+
let hoveredElement = null;
|
|
12
|
+
let selectedElement = null;
|
|
13
|
+
let debugMode = false; // Debug mode to show all hit areas
|
|
14
|
+
let isShowingOriginalPreview = false; // True when showing existing SVG/PNG from bundle
|
|
15
|
+
let originalBboxes = null; // Store original bboxes from /preview
|
|
16
|
+
let originalImgSize = null; // Store original img size from /preview
|
|
17
|
+
|
|
18
|
+
// Schema v0.3 metadata for axes-local coordinate transforms
|
|
19
|
+
let schemaMeta = null;
|
|
20
|
+
|
|
21
|
+
// Multi-panel state
|
|
22
|
+
let panelData = null; // Panel info from /preview
|
|
23
|
+
let currentPanelIndex = 0;
|
|
24
|
+
let showingPanelGrid = false;
|
|
25
|
+
let panelBboxesCache = {}; // Cache bboxes per panel {panelName: {bboxes, imgSize}}
|
|
26
|
+
let activePanelCard = null; // Currently active panel card for hover/click
|
|
27
|
+
let panelHoveredElement = null; // Hovered element in panel grid
|
|
28
|
+
let panelDebugMode = false; // Show hit regions in panel grid
|
|
29
|
+
|
|
30
|
+
// Cycle selection state for overlapping elements
|
|
31
|
+
let elementsAtCursor = []; // All elements at current cursor position
|
|
32
|
+
let currentCycleIndex = 0; // Current index in cycle
|
|
33
|
+
|
|
34
|
+
// Unit system state (default: mm)
|
|
35
|
+
let dimensionUnit = 'mm';
|
|
36
|
+
const MM_TO_INCH = 1 / 25.4;
|
|
37
|
+
const INCH_TO_MM = 25.4;
|
|
38
|
+
|
|
39
|
+
// Dark mode detection
|
|
40
|
+
function isDarkMode() {
|
|
41
|
+
return document.documentElement.getAttribute('data-theme') === 'dark';
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Hitmap-based element detection
|
|
45
|
+
let hitmapCanvas = null;
|
|
46
|
+
let hitmapCtx = null;
|
|
47
|
+
let hitmapColorMap = {}; // Maps RGB string -> element info
|
|
48
|
+
let hitmapLoaded = false;
|
|
49
|
+
let hitmapImgSize = {width: 0, height: 0};
|
|
50
|
+
|
|
51
|
+
// Load hitmap for pixel-based element detection
|
|
52
|
+
async function loadHitmap() {
|
|
53
|
+
try {
|
|
54
|
+
const resp = await fetch('/hitmap');
|
|
55
|
+
const data = await resp.json();
|
|
56
|
+
|
|
57
|
+
if (data.error) {
|
|
58
|
+
console.log('Hitmap not available:', data.error);
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Create hidden canvas for hitmap sampling
|
|
63
|
+
hitmapCanvas = document.createElement('canvas');
|
|
64
|
+
hitmapCtx = hitmapCanvas.getContext('2d', { willReadFrequently: true });
|
|
65
|
+
|
|
66
|
+
// Load hitmap image
|
|
67
|
+
const img = new Image();
|
|
68
|
+
img.onload = () => {
|
|
69
|
+
hitmapCanvas.width = img.width;
|
|
70
|
+
hitmapCanvas.height = img.height;
|
|
71
|
+
hitmapImgSize = {width: img.width, height: img.height};
|
|
72
|
+
hitmapCtx.drawImage(img, 0, 0);
|
|
73
|
+
hitmapLoaded = true;
|
|
74
|
+
console.log('Hitmap loaded:', img.width, 'x', img.height);
|
|
75
|
+
};
|
|
76
|
+
img.src = 'data:image/png;base64,' + data.image;
|
|
77
|
+
|
|
78
|
+
// Build color map from response
|
|
79
|
+
if (data.color_map) {
|
|
80
|
+
for (const [key, info] of Object.entries(data.color_map)) {
|
|
81
|
+
if (info.rgb) {
|
|
82
|
+
const rgbKey = `${info.rgb[0]},${info.rgb[1]},${info.rgb[2]}`;
|
|
83
|
+
hitmapColorMap[rgbKey] = {
|
|
84
|
+
id: info.id,
|
|
85
|
+
type: info.type,
|
|
86
|
+
label: info.label,
|
|
87
|
+
axes_index: info.axes_index,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
console.log('Hitmap color map:', Object.keys(hitmapColorMap).length, 'elements');
|
|
92
|
+
}
|
|
93
|
+
} catch (e) {
|
|
94
|
+
console.error('Error loading hitmap:', e);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Get element at position using hitmap
|
|
99
|
+
function getElementFromHitmap(imgX, imgY) {
|
|
100
|
+
if (!hitmapLoaded || !hitmapCtx) return null;
|
|
101
|
+
|
|
102
|
+
// Scale coordinates from display image to hitmap
|
|
103
|
+
const scaleX = hitmapImgSize.width / imgSize.width;
|
|
104
|
+
const scaleY = hitmapImgSize.height / imgSize.height;
|
|
105
|
+
const hitmapX = Math.floor(imgX * scaleX);
|
|
106
|
+
const hitmapY = Math.floor(imgY * scaleY);
|
|
107
|
+
|
|
108
|
+
// Bounds check
|
|
109
|
+
if (hitmapX < 0 || hitmapX >= hitmapImgSize.width ||
|
|
110
|
+
hitmapY < 0 || hitmapY >= hitmapImgSize.height) {
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Sample pixel from hitmap
|
|
115
|
+
const pixel = hitmapCtx.getImageData(hitmapX, hitmapY, 1, 1).data;
|
|
116
|
+
const r = pixel[0], g = pixel[1], b = pixel[2], a = pixel[3];
|
|
117
|
+
|
|
118
|
+
// Skip transparent or white pixels
|
|
119
|
+
if (a < 128 || (r > 250 && g > 250 && b > 250)) {
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Look up in color map
|
|
124
|
+
const rgbKey = `${r},${g},${b}`;
|
|
125
|
+
const element = hitmapColorMap[rgbKey];
|
|
126
|
+
|
|
127
|
+
if (element) {
|
|
128
|
+
return `trace_${element.id}`; // Return element key for selection
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Hover system - client-side hit testing
|
|
135
|
+
function initHoverSystem() {
|
|
136
|
+
const container = document.getElementById('preview-container');
|
|
137
|
+
const img = document.getElementById('preview-img');
|
|
138
|
+
|
|
139
|
+
img.addEventListener('mousemove', (e) => {
|
|
140
|
+
if (imgSize.width === 0 || imgSize.height === 0) return;
|
|
141
|
+
|
|
142
|
+
const rect = img.getBoundingClientRect();
|
|
143
|
+
const x = e.clientX - rect.left;
|
|
144
|
+
const y = e.clientY - rect.top;
|
|
145
|
+
|
|
146
|
+
const scaleX = imgSize.width / rect.width;
|
|
147
|
+
const scaleY = imgSize.height / rect.height;
|
|
148
|
+
const imgX = x * scaleX;
|
|
149
|
+
const imgY = y * scaleY;
|
|
150
|
+
|
|
151
|
+
const element = findElementAt(imgX, imgY);
|
|
152
|
+
if (element !== hoveredElement) {
|
|
153
|
+
hoveredElement = element;
|
|
154
|
+
updateOverlay();
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
img.addEventListener('mouseleave', () => {
|
|
159
|
+
hoveredElement = null;
|
|
160
|
+
updateOverlay();
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
img.addEventListener('click', (e) => {
|
|
164
|
+
const rect = img.getBoundingClientRect();
|
|
165
|
+
const x = e.clientX - rect.left;
|
|
166
|
+
const y = e.clientY - rect.top;
|
|
167
|
+
const scaleX = imgSize.width / rect.width;
|
|
168
|
+
const scaleY = imgSize.height / rect.height;
|
|
169
|
+
const imgX = x * scaleX;
|
|
170
|
+
const imgY = y * scaleY;
|
|
171
|
+
|
|
172
|
+
// Alt+click or find all overlapping elements
|
|
173
|
+
if (e.altKey) {
|
|
174
|
+
// Cycle through overlapping elements
|
|
175
|
+
const allElements = findAllElementsAt(imgX, imgY);
|
|
176
|
+
if (allElements.length > 0) {
|
|
177
|
+
// If cursor moved to different location, reset cycle
|
|
178
|
+
if (JSON.stringify(allElements) !== JSON.stringify(elementsAtCursor)) {
|
|
179
|
+
elementsAtCursor = allElements;
|
|
180
|
+
currentCycleIndex = 0;
|
|
181
|
+
} else {
|
|
182
|
+
// Cycle to next element
|
|
183
|
+
currentCycleIndex = (currentCycleIndex + 1) % elementsAtCursor.length;
|
|
184
|
+
}
|
|
185
|
+
selectedElement = elementsAtCursor[currentCycleIndex];
|
|
186
|
+
updateOverlay();
|
|
187
|
+
scrollToSection(selectedElement);
|
|
188
|
+
|
|
189
|
+
// Show cycle indicator in status
|
|
190
|
+
const total = elementsAtCursor.length;
|
|
191
|
+
const current = currentCycleIndex + 1;
|
|
192
|
+
console.log(`Cycle selection: ${current}/${total} - ${selectedElement}`);
|
|
193
|
+
}
|
|
194
|
+
} else if (hoveredElement) {
|
|
195
|
+
// Normal click - select hovered element
|
|
196
|
+
selectedElement = hoveredElement;
|
|
197
|
+
elementsAtCursor = []; // Reset cycle
|
|
198
|
+
currentCycleIndex = 0;
|
|
199
|
+
updateOverlay();
|
|
200
|
+
scrollToSection(selectedElement);
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
// Right-click for cycle selection menu
|
|
205
|
+
img.addEventListener('contextmenu', (e) => {
|
|
206
|
+
e.preventDefault();
|
|
207
|
+
|
|
208
|
+
const rect = img.getBoundingClientRect();
|
|
209
|
+
const x = e.clientX - rect.left;
|
|
210
|
+
const y = e.clientY - rect.top;
|
|
211
|
+
const scaleX = imgSize.width / rect.width;
|
|
212
|
+
const scaleY = imgSize.height / rect.height;
|
|
213
|
+
const imgX = x * scaleX;
|
|
214
|
+
const imgY = y * scaleY;
|
|
215
|
+
|
|
216
|
+
const allElements = findAllElementsAt(imgX, imgY);
|
|
217
|
+
if (allElements.length > 1) {
|
|
218
|
+
// Cycle to next element
|
|
219
|
+
if (JSON.stringify(allElements) !== JSON.stringify(elementsAtCursor)) {
|
|
220
|
+
elementsAtCursor = allElements;
|
|
221
|
+
currentCycleIndex = 0;
|
|
222
|
+
} else {
|
|
223
|
+
currentCycleIndex = (currentCycleIndex + 1) % elementsAtCursor.length;
|
|
224
|
+
}
|
|
225
|
+
selectedElement = elementsAtCursor[currentCycleIndex];
|
|
226
|
+
updateOverlay();
|
|
227
|
+
scrollToSection(selectedElement);
|
|
228
|
+
|
|
229
|
+
const total = elementsAtCursor.length;
|
|
230
|
+
const current = currentCycleIndex + 1;
|
|
231
|
+
console.log(`Right-click cycle: ${current}/${total} - ${selectedElement}`);
|
|
232
|
+
} else if (allElements.length === 1) {
|
|
233
|
+
selectedElement = allElements[0];
|
|
234
|
+
updateOverlay();
|
|
235
|
+
scrollToSection(selectedElement);
|
|
236
|
+
}
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
img.addEventListener('load', () => {
|
|
240
|
+
updateOverlay();
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Convert axes-local pixel coordinates to image coordinates
|
|
245
|
+
function axesLocalToImage(axLocalX, axLocalY, axesBbox) {
|
|
246
|
+
// axesBbox has: x, y, width, height in figure pixel coordinates
|
|
247
|
+
// The local editor uses tight layout which shifts coordinates
|
|
248
|
+
// For now we use the existing image coordinates from bboxes
|
|
249
|
+
return [axLocalX + axesBbox.x, axLocalY + axesBbox.y];
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Get geometry_px points converted to image coordinates
|
|
253
|
+
function getGeometryPoints(bbox) {
|
|
254
|
+
const geom = bbox.geometry_px;
|
|
255
|
+
if (!geom) return null;
|
|
256
|
+
|
|
257
|
+
// For scatter: use points array directly
|
|
258
|
+
if (geom.points && geom.points.length > 0) {
|
|
259
|
+
return {
|
|
260
|
+
type: 'scatter',
|
|
261
|
+
points: geom.points,
|
|
262
|
+
hitRadius: geom.hit_radius_px || 5
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// For lines: use path_simplified
|
|
267
|
+
if (geom.path_simplified && geom.path_simplified.length > 0) {
|
|
268
|
+
return {
|
|
269
|
+
type: 'line',
|
|
270
|
+
points: geom.path_simplified,
|
|
271
|
+
linewidth: geom.linewidth_px || 1
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// For fills/polygons: use polygon
|
|
276
|
+
if (geom.polygon && geom.polygon.length > 0) {
|
|
277
|
+
return {
|
|
278
|
+
type: 'polygon',
|
|
279
|
+
points: geom.polygon
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
return null;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function findElementAt(x, y) {
|
|
287
|
+
// Multi-panel aware hit detection with specificity hierarchy:
|
|
288
|
+
// 1. Data elements with legacy points - proximity detection (correct saved-image coords)
|
|
289
|
+
// 2. Small elements (labels, ticks, legends, bars, fills)
|
|
290
|
+
// 3. Panel bboxes - lowest priority (fallback)
|
|
291
|
+
// Note: geometry_px (v0.3) uses axes-local coords which need coordinate transformation
|
|
292
|
+
|
|
293
|
+
const PROXIMITY_THRESHOLD = 15;
|
|
294
|
+
const SCATTER_THRESHOLD = 20; // Larger threshold for scatter points
|
|
295
|
+
|
|
296
|
+
// First: Check for data elements using legacy points (in saved-image coordinates)
|
|
297
|
+
let closestDataElement = null;
|
|
298
|
+
let minDistance = Infinity;
|
|
299
|
+
|
|
300
|
+
for (const [name, bbox] of Object.entries(elementBboxes)) {
|
|
301
|
+
if (name === '_meta') continue; // Skip metadata entry
|
|
302
|
+
|
|
303
|
+
// Prioritize legacy points array (already in correct saved-image coordinates)
|
|
304
|
+
if (bbox.points && bbox.points.length > 0) {
|
|
305
|
+
// Check if cursor is within general bbox area first
|
|
306
|
+
if (x >= bbox.x0 - SCATTER_THRESHOLD && x <= bbox.x1 + SCATTER_THRESHOLD &&
|
|
307
|
+
y >= bbox.y0 - SCATTER_THRESHOLD && y <= bbox.y1 + SCATTER_THRESHOLD) {
|
|
308
|
+
|
|
309
|
+
const elementType = bbox.element_type || 'line';
|
|
310
|
+
let dist;
|
|
311
|
+
|
|
312
|
+
if (elementType === 'scatter') {
|
|
313
|
+
// For scatter, find distance to nearest point
|
|
314
|
+
dist = distanceToNearestPoint(x, y, bbox.points);
|
|
315
|
+
} else {
|
|
316
|
+
// For lines, find distance to line segments
|
|
317
|
+
dist = distanceToLine(x, y, bbox.points);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
if (dist < minDistance) {
|
|
321
|
+
minDistance = dist;
|
|
322
|
+
closestDataElement = name;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// Use appropriate threshold based on element type
|
|
329
|
+
if (closestDataElement) {
|
|
330
|
+
const bbox = elementBboxes[closestDataElement];
|
|
331
|
+
const threshold = (bbox.element_type === 'scatter') ? SCATTER_THRESHOLD : PROXIMITY_THRESHOLD;
|
|
332
|
+
if (minDistance <= threshold) {
|
|
333
|
+
return closestDataElement;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// Second: Collect all bbox matches, excluding panels and data elements with points
|
|
338
|
+
const elementMatches = [];
|
|
339
|
+
const panelMatches = [];
|
|
340
|
+
|
|
341
|
+
for (const [name, bbox] of Object.entries(elementBboxes)) {
|
|
342
|
+
if (x >= bbox.x0 && x <= bbox.x1 && y >= bbox.y0 && y <= bbox.y1) {
|
|
343
|
+
const area = (bbox.x1 - bbox.x0) * (bbox.y1 - bbox.y0);
|
|
344
|
+
const isPanel = bbox.is_panel || name.endsWith('_panel');
|
|
345
|
+
const hasPoints = bbox.points && bbox.points.length > 0;
|
|
346
|
+
|
|
347
|
+
if (hasPoints) {
|
|
348
|
+
// Already handled above with proximity
|
|
349
|
+
continue;
|
|
350
|
+
} else if (isPanel) {
|
|
351
|
+
panelMatches.push({name, area, bbox});
|
|
352
|
+
} else {
|
|
353
|
+
elementMatches.push({name, area, bbox});
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// Return smallest non-panel element if any
|
|
359
|
+
if (elementMatches.length > 0) {
|
|
360
|
+
elementMatches.sort((a, b) => a.area - b.area);
|
|
361
|
+
return elementMatches[0].name;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// Fallback to panel selection (useful for multi-panel figures)
|
|
365
|
+
if (panelMatches.length > 0) {
|
|
366
|
+
panelMatches.sort((a, b) => a.area - b.area);
|
|
367
|
+
return panelMatches[0].name;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
return null;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
function distanceToNearestPoint(px, py, points) {
|
|
374
|
+
// Find distance to nearest point in scatter
|
|
375
|
+
if (!Array.isArray(points) || points.length === 0) return Infinity;
|
|
376
|
+
let minDist = Infinity;
|
|
377
|
+
for (const pt of points) {
|
|
378
|
+
if (!Array.isArray(pt) || pt.length < 2) continue;
|
|
379
|
+
const [x, y] = pt;
|
|
380
|
+
const dist = Math.sqrt((px - x) ** 2 + (py - y) ** 2);
|
|
381
|
+
if (dist < minDist) minDist = dist;
|
|
382
|
+
}
|
|
383
|
+
return minDist;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function distanceToLine(px, py, points) {
|
|
387
|
+
if (!Array.isArray(points) || points.length < 2) return Infinity;
|
|
388
|
+
let minDist = Infinity;
|
|
389
|
+
for (let i = 0; i < points.length - 1; i++) {
|
|
390
|
+
const pt1 = points[i];
|
|
391
|
+
const pt2 = points[i + 1];
|
|
392
|
+
if (!Array.isArray(pt1) || pt1.length < 2) continue;
|
|
393
|
+
if (!Array.isArray(pt2) || pt2.length < 2) continue;
|
|
394
|
+
const [x1, y1] = pt1;
|
|
395
|
+
const [x2, y2] = pt2;
|
|
396
|
+
const dist = distanceToSegment(px, py, x1, y1, x2, y2);
|
|
397
|
+
if (dist < minDist) minDist = dist;
|
|
398
|
+
}
|
|
399
|
+
return minDist;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
function distanceToSegment(px, py, x1, y1, x2, y2) {
|
|
403
|
+
const dx = x2 - x1;
|
|
404
|
+
const dy = y2 - y1;
|
|
405
|
+
const lenSq = dx * dx + dy * dy;
|
|
406
|
+
|
|
407
|
+
if (lenSq === 0) {
|
|
408
|
+
return Math.sqrt((px - x1) ** 2 + (py - y1) ** 2);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
let t = ((px - x1) * dx + (py - y1) * dy) / lenSq;
|
|
412
|
+
t = Math.max(0, Math.min(1, t));
|
|
413
|
+
|
|
414
|
+
const projX = x1 + t * dx;
|
|
415
|
+
const projY = y1 + t * dy;
|
|
416
|
+
|
|
417
|
+
return Math.sqrt((px - projX) ** 2 + (py - projY) ** 2);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// Point-in-polygon test using ray casting algorithm
|
|
421
|
+
function pointInPolygon(px, py, polygon) {
|
|
422
|
+
if (!Array.isArray(polygon) || polygon.length < 3) return false;
|
|
423
|
+
|
|
424
|
+
let inside = false;
|
|
425
|
+
for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i++) {
|
|
426
|
+
const ptI = polygon[i];
|
|
427
|
+
const ptJ = polygon[j];
|
|
428
|
+
if (!Array.isArray(ptI) || ptI.length < 2) continue;
|
|
429
|
+
if (!Array.isArray(ptJ) || ptJ.length < 2) continue;
|
|
430
|
+
const [xi, yi] = ptI;
|
|
431
|
+
const [xj, yj] = ptJ;
|
|
432
|
+
|
|
433
|
+
if (((yi > py) !== (yj > py)) &&
|
|
434
|
+
(px < (xj - xi) * (py - yi) / (yj - yi) + xi)) {
|
|
435
|
+
inside = !inside;
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
return inside;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
function findAllElementsAt(x, y) {
|
|
442
|
+
// Find all elements at cursor position (for cycle selection)
|
|
443
|
+
// Returns array sorted by specificity (most specific first)
|
|
444
|
+
const PROXIMITY_THRESHOLD = 15;
|
|
445
|
+
const SCATTER_THRESHOLD = 20;
|
|
446
|
+
|
|
447
|
+
const results = [];
|
|
448
|
+
|
|
449
|
+
for (const [name, bbox] of Object.entries(elementBboxes)) {
|
|
450
|
+
let match = false;
|
|
451
|
+
let distance = Infinity;
|
|
452
|
+
let priority = 0; // Lower = more specific
|
|
453
|
+
|
|
454
|
+
const hasPoints = bbox.points && bbox.points.length > 0;
|
|
455
|
+
const elementType = bbox.element_type || '';
|
|
456
|
+
const isPanel = bbox.is_panel || name.endsWith('_panel');
|
|
457
|
+
|
|
458
|
+
// Check data elements with points (lines, scatter)
|
|
459
|
+
if (hasPoints) {
|
|
460
|
+
if (x >= bbox.x0 - SCATTER_THRESHOLD && x <= bbox.x1 + SCATTER_THRESHOLD &&
|
|
461
|
+
y >= bbox.y0 - SCATTER_THRESHOLD && y <= bbox.y1 + SCATTER_THRESHOLD) {
|
|
462
|
+
|
|
463
|
+
if (elementType === 'scatter') {
|
|
464
|
+
distance = distanceToNearestPoint(x, y, bbox.points);
|
|
465
|
+
if (distance <= SCATTER_THRESHOLD) {
|
|
466
|
+
match = true;
|
|
467
|
+
priority = 1; // Scatter points = high priority
|
|
468
|
+
}
|
|
469
|
+
} else {
|
|
470
|
+
distance = distanceToLine(x, y, bbox.points);
|
|
471
|
+
if (distance <= PROXIMITY_THRESHOLD) {
|
|
472
|
+
match = true;
|
|
473
|
+
priority = 2; // Lines = high priority
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
// Check bbox containment
|
|
480
|
+
if (x >= bbox.x0 && x <= bbox.x1 && y >= bbox.y0 && y <= bbox.y1) {
|
|
481
|
+
const area = (bbox.x1 - bbox.x0) * (bbox.y1 - bbox.y0);
|
|
482
|
+
|
|
483
|
+
if (!match) {
|
|
484
|
+
match = true;
|
|
485
|
+
distance = 0;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
if (isPanel) {
|
|
489
|
+
priority = 100; // Panels = lowest priority
|
|
490
|
+
} else if (!hasPoints) {
|
|
491
|
+
// Small elements like labels, ticks - use area for priority
|
|
492
|
+
priority = 10 + Math.min(area / 10000, 50);
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
if (match) {
|
|
497
|
+
results.push({ name, distance, priority, bbox });
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// Sort by priority (lower first), then by distance
|
|
502
|
+
results.sort((a, b) => {
|
|
503
|
+
if (a.priority !== b.priority) return a.priority - b.priority;
|
|
504
|
+
return a.distance - b.distance;
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
return results.map(r => r.name);
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
function drawTracePath(bbox, scaleX, scaleY, type) {
|
|
511
|
+
if (!Array.isArray(bbox.points) || bbox.points.length < 2) return '';
|
|
512
|
+
|
|
513
|
+
const points = bbox.points.filter(pt => Array.isArray(pt) && pt.length >= 2);
|
|
514
|
+
if (points.length < 2) return '';
|
|
515
|
+
|
|
516
|
+
let pathD = `M ${points[0][0] * scaleX} ${points[0][1] * scaleY}`;
|
|
517
|
+
for (let i = 1; i < points.length; i++) {
|
|
518
|
+
pathD += ` L ${points[i][0] * scaleX} ${points[i][1] * scaleY}`;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
const className = type === 'hover' ? 'hover-path' : 'selected-path';
|
|
522
|
+
const labelX = points[0][0] * scaleX;
|
|
523
|
+
const labelY = points[0][1] * scaleY - 8;
|
|
524
|
+
const labelClass = type === 'hover' ? 'hover-label' : 'selected-label';
|
|
525
|
+
|
|
526
|
+
return `<path class="${className}" d="${pathD}"/>` +
|
|
527
|
+
`<text class="${labelClass}" x="${labelX}" y="${labelY}">${bbox.label || ''}</text>`;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
function drawScatterPoints(bbox, scaleX, scaleY, type) {
|
|
531
|
+
// Draw scatter points as circles
|
|
532
|
+
if (!Array.isArray(bbox.points) || bbox.points.length === 0) return '';
|
|
533
|
+
|
|
534
|
+
const className = type === 'hover' ? 'hover-scatter' : 'selected-scatter';
|
|
535
|
+
const labelClass = type === 'hover' ? 'hover-label' : 'selected-label';
|
|
536
|
+
const radius = 4;
|
|
537
|
+
|
|
538
|
+
let svg = '';
|
|
539
|
+
for (const pt of bbox.points) {
|
|
540
|
+
if (!Array.isArray(pt) || pt.length < 2) continue;
|
|
541
|
+
const [x, y] = pt;
|
|
542
|
+
svg += `<circle class="${className}" cx="${x * scaleX}" cy="${y * scaleY}" r="${radius}"/>`;
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
// Add label near first point
|
|
546
|
+
const validPoints = bbox.points.filter(pt => Array.isArray(pt) && pt.length >= 2);
|
|
547
|
+
if (validPoints.length > 0) {
|
|
548
|
+
const labelX = validPoints[0][0] * scaleX;
|
|
549
|
+
const labelY = validPoints[0][1] * scaleY - 10;
|
|
550
|
+
svg += `<text class="${labelClass}" x="${labelX}" y="${labelY}">${bbox.label || ''}</text>`;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
return svg;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
function updateOverlay() {
|
|
557
|
+
const overlay = document.getElementById('hover-overlay');
|
|
558
|
+
// Find the visible preview element (SVG wrapper or img)
|
|
559
|
+
const svgWrapper = document.getElementById('preview-svg-wrapper');
|
|
560
|
+
const imgEl = document.getElementById('preview-img');
|
|
561
|
+
|
|
562
|
+
let targetEl = null;
|
|
563
|
+
if (svgWrapper) {
|
|
564
|
+
targetEl = svgWrapper.querySelector('svg') || svgWrapper;
|
|
565
|
+
} else if (imgEl && imgEl.offsetWidth > 0) {
|
|
566
|
+
targetEl = imgEl;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
if (!targetEl) {
|
|
570
|
+
console.log('updateOverlay: No visible target element');
|
|
571
|
+
return;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
const rect = targetEl.getBoundingClientRect();
|
|
575
|
+
if (rect.width === 0 || rect.height === 0) {
|
|
576
|
+
console.log('updateOverlay: Target element has zero dimensions');
|
|
577
|
+
return;
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
// Guard against zero imgSize (can cause Infinity scale)
|
|
581
|
+
if (!imgSize || !imgSize.width || !imgSize.height || imgSize.width === 0 || imgSize.height === 0) {
|
|
582
|
+
console.log('updateOverlay: imgSize not set or zero', imgSize);
|
|
583
|
+
return;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
overlay.setAttribute('width', rect.width);
|
|
587
|
+
overlay.setAttribute('height', rect.height);
|
|
588
|
+
overlay.style.width = rect.width + 'px';
|
|
589
|
+
overlay.style.height = rect.height + 'px';
|
|
590
|
+
|
|
591
|
+
// Position overlay over the target element
|
|
592
|
+
const containerRect = document.getElementById('preview-container').getBoundingClientRect();
|
|
593
|
+
overlay.style.left = (rect.left - containerRect.left) + 'px';
|
|
594
|
+
overlay.style.top = (rect.top - containerRect.top) + 'px';
|
|
595
|
+
|
|
596
|
+
const scaleX = rect.width / imgSize.width;
|
|
597
|
+
const scaleY = rect.height / imgSize.height;
|
|
598
|
+
|
|
599
|
+
console.log('updateOverlay: rect=', rect.width, 'x', rect.height, 'imgSize=', imgSize, 'scale=', scaleX, scaleY);
|
|
600
|
+
|
|
601
|
+
let svg = '';
|
|
602
|
+
|
|
603
|
+
// Debug mode: draw ALL bboxes
|
|
604
|
+
if (debugMode) {
|
|
605
|
+
svg += drawDebugBboxes(scaleX, scaleY);
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
function drawElement(elementName, type) {
|
|
609
|
+
const bbox = elementBboxes[elementName];
|
|
610
|
+
if (!bbox) return '';
|
|
611
|
+
|
|
612
|
+
const elementType = bbox.element_type || '';
|
|
613
|
+
const hasPoints = bbox.points && bbox.points.length > 0;
|
|
614
|
+
|
|
615
|
+
// Lines - draw as path
|
|
616
|
+
if ((elementType === 'line' || elementName.includes('trace_')) && hasPoints) {
|
|
617
|
+
return drawTracePath(bbox, scaleX, scaleY, type);
|
|
618
|
+
}
|
|
619
|
+
// Scatter - draw as circles
|
|
620
|
+
else if (elementType === 'scatter' && hasPoints) {
|
|
621
|
+
return drawScatterPoints(bbox, scaleX, scaleY, type);
|
|
622
|
+
}
|
|
623
|
+
// Default - draw bbox rectangle
|
|
624
|
+
else {
|
|
625
|
+
const rectClass = type === 'hover' ? 'hover-rect' : 'selected-rect';
|
|
626
|
+
const labelClass = type === 'hover' ? 'hover-label' : 'selected-label';
|
|
627
|
+
const x = bbox.x0 * scaleX - 2;
|
|
628
|
+
const y = bbox.y0 * scaleY - 2;
|
|
629
|
+
const w = (bbox.x1 - bbox.x0) * scaleX + 4;
|
|
630
|
+
const h = (bbox.y1 - bbox.y0) * scaleY + 4;
|
|
631
|
+
return `<rect class="${rectClass}" x="${x}" y="${y}" width="${w}" height="${h}" rx="2"/>` +
|
|
632
|
+
`<text class="${labelClass}" x="${x}" y="${y - 4}">${bbox.label || elementName}</text>`;
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
if (hoveredElement && hoveredElement !== selectedElement) {
|
|
637
|
+
svg += drawElement(hoveredElement, 'hover');
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
if (selectedElement) {
|
|
641
|
+
svg += drawElement(selectedElement, 'selected');
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
overlay.innerHTML = svg;
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
// Draw all bboxes for debugging
|
|
648
|
+
function drawDebugBboxes(scaleX, scaleY) {
|
|
649
|
+
let svg = '';
|
|
650
|
+
let count = 0;
|
|
651
|
+
|
|
652
|
+
console.log('=== DEBUG BBOXES ===');
|
|
653
|
+
console.log('imgSize:', imgSize);
|
|
654
|
+
console.log('scale:', scaleX, scaleY);
|
|
655
|
+
|
|
656
|
+
for (const [name, bbox] of Object.entries(elementBboxes)) {
|
|
657
|
+
if (name === '_meta') continue;
|
|
658
|
+
|
|
659
|
+
count++;
|
|
660
|
+
const hasPoints = bbox.points && bbox.points.length > 0;
|
|
661
|
+
const elementType = bbox.element_type || '';
|
|
662
|
+
|
|
663
|
+
// Choose color based on element type
|
|
664
|
+
let rectClass = 'debug-rect';
|
|
665
|
+
if (name.includes('trace_') || elementType === 'line') {
|
|
666
|
+
rectClass = 'debug-rect-trace';
|
|
667
|
+
} else if (name.includes('legend')) {
|
|
668
|
+
rectClass = 'debug-rect-legend';
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
// Draw bbox rectangle
|
|
672
|
+
const x = bbox.x0 * scaleX;
|
|
673
|
+
const y = bbox.y0 * scaleY;
|
|
674
|
+
const w = (bbox.x1 - bbox.x0) * scaleX;
|
|
675
|
+
const h = (bbox.y1 - bbox.y0) * scaleY;
|
|
676
|
+
|
|
677
|
+
svg += `<rect class="${rectClass}" x="${x}" y="${y}" width="${w}" height="${h}"/>`;
|
|
678
|
+
svg += `<text class="debug-label" x="${x + 2}" y="${y + 10}">${name}</text>`;
|
|
679
|
+
|
|
680
|
+
// Draw path points if available
|
|
681
|
+
if (hasPoints && bbox.points.length > 1) {
|
|
682
|
+
let pathD = `M ${bbox.points[0][0] * scaleX} ${bbox.points[0][1] * scaleY}`;
|
|
683
|
+
for (let i = 1; i < bbox.points.length; i++) {
|
|
684
|
+
const pt = bbox.points[i];
|
|
685
|
+
if (pt && pt.length >= 2) {
|
|
686
|
+
pathD += ` L ${pt[0] * scaleX} ${pt[1] * scaleY}`;
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
svg += `<path class="debug-path" d="${pathD}"/>`;
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
console.log(` ${name}: (${bbox.x0?.toFixed(1)}, ${bbox.y0?.toFixed(1)}) - (${bbox.x1?.toFixed(1)}, ${bbox.y1?.toFixed(1)}), points: ${bbox.points?.length || 0}`);
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
console.log(`Total elements: ${count}`);
|
|
696
|
+
return svg;
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
// Toggle debug mode
|
|
700
|
+
function toggleDebugMode() {
|
|
701
|
+
debugMode = !debugMode;
|
|
702
|
+
const btn = document.getElementById('debug-toggle-btn');
|
|
703
|
+
if (btn) {
|
|
704
|
+
btn.classList.toggle('active', debugMode);
|
|
705
|
+
btn.textContent = debugMode ? 'Hide Hit Areas' : 'Show Hit Areas';
|
|
706
|
+
}
|
|
707
|
+
updateOverlay();
|
|
708
|
+
console.log('Debug mode:', debugMode ? 'ON' : 'OFF');
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
function expandSection(sectionId) {
|
|
712
|
+
console.log('expandSection called with:', sectionId);
|
|
713
|
+
let foundSection = null;
|
|
714
|
+
document.querySelectorAll('.section').forEach(section => {
|
|
715
|
+
const header = section.querySelector('.section-header');
|
|
716
|
+
const content = section.querySelector('.section-content');
|
|
717
|
+
if (section.id === sectionId) {
|
|
718
|
+
foundSection = section;
|
|
719
|
+
console.log('expandSection: Found section', sectionId, 'header:', header, 'content:', content);
|
|
720
|
+
header?.classList.remove('collapsed');
|
|
721
|
+
content?.classList.remove('collapsed');
|
|
722
|
+
} else if (header?.classList.contains('section-toggle')) {
|
|
723
|
+
header?.classList.add('collapsed');
|
|
724
|
+
content?.classList.add('collapsed');
|
|
725
|
+
}
|
|
726
|
+
});
|
|
727
|
+
if (!foundSection) {
|
|
728
|
+
console.warn('expandSection: Section not found:', sectionId);
|
|
729
|
+
} else {
|
|
730
|
+
// Scroll the section into view
|
|
731
|
+
setTimeout(() => {
|
|
732
|
+
foundSection.scrollIntoView({behavior: 'smooth', block: 'start'});
|
|
733
|
+
}, 50);
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
function scrollToSection(elementName) {
|
|
738
|
+
console.log('scrollToSection called with:', elementName);
|
|
739
|
+
|
|
740
|
+
// Map element names to their corresponding sections
|
|
741
|
+
// Elements with a mapping here will NOT show the "Selected" panel
|
|
742
|
+
const elementToSection = {
|
|
743
|
+
'title': 'section-labels',
|
|
744
|
+
'xlabel': 'section-labels',
|
|
745
|
+
'ylabel': 'section-labels',
|
|
746
|
+
'caption': 'section-labels',
|
|
747
|
+
'xaxis': 'section-ticks',
|
|
748
|
+
'yaxis': 'section-ticks',
|
|
749
|
+
'xaxis_ticks': 'section-ticks',
|
|
750
|
+
'yaxis_ticks': 'section-ticks',
|
|
751
|
+
'xaxis_spine': 'section-ticks',
|
|
752
|
+
'yaxis_spine': 'section-ticks',
|
|
753
|
+
'legend': 'section-legend'
|
|
754
|
+
};
|
|
755
|
+
|
|
756
|
+
const fieldMap = {
|
|
757
|
+
'title': 'title',
|
|
758
|
+
'xlabel': 'xlabel',
|
|
759
|
+
'ylabel': 'ylabel',
|
|
760
|
+
'caption': 'caption',
|
|
761
|
+
'xaxis': 'xmin',
|
|
762
|
+
'yaxis': 'ymin',
|
|
763
|
+
'xaxis_ticks': 'x_tick_fontsize',
|
|
764
|
+
'yaxis_ticks': 'y_tick_fontsize',
|
|
765
|
+
'xaxis_spine': 'axis_width',
|
|
766
|
+
'yaxis_spine': 'axis_width',
|
|
767
|
+
'legend': 'legend_visible'
|
|
768
|
+
};
|
|
769
|
+
|
|
770
|
+
if (elementName.startsWith('trace_')) {
|
|
771
|
+
expandSection('section-traces');
|
|
772
|
+
const traceIdx = elementBboxes[elementName]?.trace_idx;
|
|
773
|
+
if (traceIdx !== undefined) {
|
|
774
|
+
const traceItems = document.querySelectorAll('.trace-item');
|
|
775
|
+
if (traceItems[traceIdx]) {
|
|
776
|
+
setTimeout(() => {
|
|
777
|
+
// Scroll into view and highlight the trace item
|
|
778
|
+
traceItems[traceIdx].scrollIntoView({behavior: 'smooth', block: 'center'});
|
|
779
|
+
// Add temporary highlight effect
|
|
780
|
+
traceItems[traceIdx].classList.add('trace-item-highlight');
|
|
781
|
+
setTimeout(() => {
|
|
782
|
+
traceItems[traceIdx].classList.remove('trace-item-highlight');
|
|
783
|
+
}, 1500);
|
|
784
|
+
}, 100);
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
return;
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
// Extract base element name from prefixed names like "ax_00_yaxis_spine" or "ax0_title"
|
|
791
|
+
let baseElementName = elementName;
|
|
792
|
+
const match = elementName.match(/ax_?\d+_(.+)/);
|
|
793
|
+
if (match) {
|
|
794
|
+
baseElementName = match[1];
|
|
795
|
+
}
|
|
796
|
+
console.log('scrollToSection: baseElementName=', baseElementName, 'from', elementName);
|
|
797
|
+
|
|
798
|
+
const sectionId = elementToSection[baseElementName];
|
|
799
|
+
const fieldId = fieldMap[baseElementName];
|
|
800
|
+
console.log('scrollToSection: sectionId=', sectionId, 'fieldId=', fieldId);
|
|
801
|
+
|
|
802
|
+
if (sectionId) {
|
|
803
|
+
console.log('scrollToSection: expanding section', sectionId);
|
|
804
|
+
// Element has a corresponding section - expand it, don't show "Selected" panel
|
|
805
|
+
expandSection(sectionId);
|
|
806
|
+
|
|
807
|
+
if (fieldId) {
|
|
808
|
+
const field = document.getElementById(fieldId);
|
|
809
|
+
if (field) {
|
|
810
|
+
setTimeout(() => {
|
|
811
|
+
field.scrollIntoView({behavior: 'smooth', block: 'center'});
|
|
812
|
+
field.focus();
|
|
813
|
+
}, 100);
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
// Hide the "Selected" panel since we're using existing section
|
|
818
|
+
const selectedSection = document.getElementById('section-selected');
|
|
819
|
+
if (selectedSection) {
|
|
820
|
+
selectedSection.style.display = 'none';
|
|
821
|
+
}
|
|
822
|
+
} else {
|
|
823
|
+
// No corresponding section - show the "Selected" panel for this element
|
|
824
|
+
showSelectedElementPanel(elementName);
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
// Field to element synchronization - highlight element when field is focused
|
|
829
|
+
function setupFieldToElementSync() {
|
|
830
|
+
// Map field IDs to element names
|
|
831
|
+
const fieldToElement = {
|
|
832
|
+
// Title, Labels & Caption section
|
|
833
|
+
'title': 'title',
|
|
834
|
+
'title_fontsize': 'title',
|
|
835
|
+
'show_title': 'title',
|
|
836
|
+
'xlabel': 'xlabel',
|
|
837
|
+
'ylabel': 'ylabel',
|
|
838
|
+
'caption': 'caption',
|
|
839
|
+
'caption_fontsize': 'caption',
|
|
840
|
+
'show_caption': 'caption',
|
|
841
|
+
// Axis & Ticks section
|
|
842
|
+
'xmin': 'xaxis',
|
|
843
|
+
'xmax': 'xaxis',
|
|
844
|
+
'ymin': 'yaxis',
|
|
845
|
+
'ymax': 'yaxis',
|
|
846
|
+
'x_n_ticks': 'xaxis_ticks',
|
|
847
|
+
'hide_x_ticks': 'xaxis_ticks',
|
|
848
|
+
'x_tick_fontsize': 'xaxis_ticks',
|
|
849
|
+
'x_tick_direction': 'xaxis_ticks',
|
|
850
|
+
'x_tick_length': 'xaxis_ticks',
|
|
851
|
+
'x_tick_width': 'xaxis_ticks',
|
|
852
|
+
'y_n_ticks': 'yaxis_ticks',
|
|
853
|
+
'hide_y_ticks': 'yaxis_ticks',
|
|
854
|
+
'y_tick_fontsize': 'yaxis_ticks',
|
|
855
|
+
'y_tick_direction': 'yaxis_ticks',
|
|
856
|
+
'y_tick_length': 'yaxis_ticks',
|
|
857
|
+
'y_tick_width': 'yaxis_ticks',
|
|
858
|
+
// Legend section
|
|
859
|
+
'legend_visible': 'legend',
|
|
860
|
+
'legend_loc': 'legend',
|
|
861
|
+
'legend_frameon': 'legend',
|
|
862
|
+
'legend_fontsize': 'legend',
|
|
863
|
+
'legend_ncols': 'legend',
|
|
864
|
+
'legend_x': 'legend',
|
|
865
|
+
'legend_y': 'legend'
|
|
866
|
+
};
|
|
867
|
+
|
|
868
|
+
// Add focus listeners to all mapped fields
|
|
869
|
+
Object.entries(fieldToElement).forEach(([fieldId, elementName]) => {
|
|
870
|
+
const field = document.getElementById(fieldId);
|
|
871
|
+
if (field) {
|
|
872
|
+
field.addEventListener('focus', () => {
|
|
873
|
+
// Find the element in bboxes - for multi-panel, check ax_00 first
|
|
874
|
+
let targetElement = null;
|
|
875
|
+
if (elementBboxes[elementName]) {
|
|
876
|
+
targetElement = elementName;
|
|
877
|
+
} else {
|
|
878
|
+
// Try to find with axis prefix (e.g., ax_00_title)
|
|
879
|
+
for (const key of Object.keys(elementBboxes)) {
|
|
880
|
+
if (key.endsWith('_' + elementName) || key === elementName) {
|
|
881
|
+
targetElement = key;
|
|
882
|
+
break;
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
if (targetElement) {
|
|
888
|
+
selectedElement = targetElement;
|
|
889
|
+
updateOverlay();
|
|
890
|
+
setStatus(`Highlighting: ${targetElement}`, false);
|
|
891
|
+
}
|
|
892
|
+
});
|
|
893
|
+
|
|
894
|
+
// Also handle mouseenter for hover feedback
|
|
895
|
+
field.addEventListener('mouseenter', () => {
|
|
896
|
+
let targetElement = null;
|
|
897
|
+
if (elementBboxes[elementName]) {
|
|
898
|
+
targetElement = elementName;
|
|
899
|
+
} else {
|
|
900
|
+
for (const key of Object.keys(elementBboxes)) {
|
|
901
|
+
if (key.endsWith('_' + elementName) || key === elementName) {
|
|
902
|
+
targetElement = key;
|
|
903
|
+
break;
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
if (targetElement && targetElement !== selectedElement) {
|
|
909
|
+
hoveredElement = targetElement;
|
|
910
|
+
updateOverlay();
|
|
911
|
+
}
|
|
912
|
+
});
|
|
913
|
+
|
|
914
|
+
field.addEventListener('mouseleave', () => {
|
|
915
|
+
if (hoveredElement && hoveredElement !== selectedElement) {
|
|
916
|
+
hoveredElement = null;
|
|
917
|
+
updateOverlay();
|
|
918
|
+
}
|
|
919
|
+
});
|
|
920
|
+
}
|
|
921
|
+
});
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
// Selected element panel management
|
|
925
|
+
function showSelectedElementPanel(elementName) {
|
|
926
|
+
const section = document.getElementById('section-selected');
|
|
927
|
+
const titleEl = document.getElementById('selected-element-title');
|
|
928
|
+
const typeBadge = document.getElementById('element-type-badge');
|
|
929
|
+
const axisInfo = document.getElementById('element-axis-info');
|
|
930
|
+
|
|
931
|
+
// Hide all property sections first
|
|
932
|
+
document.querySelectorAll('.element-props').forEach(el => el.style.display = 'none');
|
|
933
|
+
|
|
934
|
+
if (!elementName) {
|
|
935
|
+
section.style.display = 'none';
|
|
936
|
+
return;
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
section.style.display = 'block';
|
|
940
|
+
|
|
941
|
+
// Parse element name to extract type and info
|
|
942
|
+
const elementInfo = parseElementName(elementName);
|
|
943
|
+
const bbox = elementBboxes[elementName] || {};
|
|
944
|
+
|
|
945
|
+
// Update title
|
|
946
|
+
titleEl.textContent = `Selected: ${elementInfo.displayName}`;
|
|
947
|
+
|
|
948
|
+
// Update type badge
|
|
949
|
+
typeBadge.className = `element-type-badge ${elementInfo.type}`;
|
|
950
|
+
typeBadge.textContent = elementInfo.type;
|
|
951
|
+
|
|
952
|
+
// Update axis info
|
|
953
|
+
if (elementInfo.axisId) {
|
|
954
|
+
const row = elementInfo.axisId.match(/ax_(\\d)(\\d)/);
|
|
955
|
+
if (row) {
|
|
956
|
+
axisInfo.textContent = `Panel: Row ${parseInt(row[1])+1}, Col ${parseInt(row[2])+1}`;
|
|
957
|
+
} else {
|
|
958
|
+
axisInfo.textContent = `Axis: ${elementInfo.axisId}`;
|
|
959
|
+
}
|
|
960
|
+
} else {
|
|
961
|
+
axisInfo.textContent = '';
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
// Show appropriate property panel and populate with current values
|
|
965
|
+
showPropertiesForElement(elementInfo, bbox);
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
function parseElementName(name) {
|
|
969
|
+
// Parse names like: ax_00_scatter_0, ax_11_trace_1, ax_01_xlabel, trace_0, xlabel, ax_00_xaxis, etc.
|
|
970
|
+
const result = {
|
|
971
|
+
original: name,
|
|
972
|
+
type: 'unknown',
|
|
973
|
+
displayName: name,
|
|
974
|
+
axisId: null,
|
|
975
|
+
index: null
|
|
976
|
+
};
|
|
977
|
+
|
|
978
|
+
// Check for axis prefix (ax_XX_)
|
|
979
|
+
const axisMatch = name.match(/^(ax_\\d+)_(.+)$/);
|
|
980
|
+
if (axisMatch) {
|
|
981
|
+
result.axisId = axisMatch[1];
|
|
982
|
+
name = axisMatch[2]; // Rest of the name
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
// Determine element type
|
|
986
|
+
if (name.includes('scatter')) {
|
|
987
|
+
result.type = 'scatter';
|
|
988
|
+
const idx = name.match(/scatter_(\\d+)/);
|
|
989
|
+
result.index = idx ? parseInt(idx[1]) : 0;
|
|
990
|
+
result.displayName = `Scatter ${result.index + 1}`;
|
|
991
|
+
} else if (name.includes('trace')) {
|
|
992
|
+
result.type = 'trace';
|
|
993
|
+
const idx = name.match(/trace_(\\d+)/);
|
|
994
|
+
result.index = idx ? parseInt(idx[1]) : 0;
|
|
995
|
+
result.displayName = `Line ${result.index + 1}`;
|
|
996
|
+
} else if (name.includes('fill')) {
|
|
997
|
+
result.type = 'fill';
|
|
998
|
+
const idx = name.match(/fill_(\\d+)/);
|
|
999
|
+
result.index = idx ? parseInt(idx[1]) : 0;
|
|
1000
|
+
result.displayName = `Fill Area ${result.index + 1}`;
|
|
1001
|
+
} else if (name.includes('bar')) {
|
|
1002
|
+
result.type = 'bar';
|
|
1003
|
+
const idx = name.match(/bar_(\\d+)/);
|
|
1004
|
+
result.index = idx ? parseInt(idx[1]) : 0;
|
|
1005
|
+
result.displayName = `Bar ${result.index + 1}`;
|
|
1006
|
+
} else if (name === 'xlabel' || name === 'ylabel' || name === 'title') {
|
|
1007
|
+
result.type = 'label';
|
|
1008
|
+
result.displayName = name.charAt(0).toUpperCase() + name.slice(1);
|
|
1009
|
+
} else if (name === 'legend') {
|
|
1010
|
+
result.type = 'legend';
|
|
1011
|
+
result.displayName = 'Legend';
|
|
1012
|
+
} else if (name === 'xaxis') {
|
|
1013
|
+
result.type = 'xaxis';
|
|
1014
|
+
result.displayName = 'X-Axis';
|
|
1015
|
+
} else if (name === 'yaxis') {
|
|
1016
|
+
result.type = 'yaxis';
|
|
1017
|
+
result.displayName = 'Y-Axis';
|
|
1018
|
+
} else if (name.includes('panel')) {
|
|
1019
|
+
result.type = 'panel';
|
|
1020
|
+
result.displayName = 'Panel';
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
return result;
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
function showPropertiesForElement(elementInfo, bbox) {
|
|
1027
|
+
const type = elementInfo.type;
|
|
1028
|
+
|
|
1029
|
+
if (type === 'trace') {
|
|
1030
|
+
const props = document.getElementById('selected-trace-props');
|
|
1031
|
+
props.style.display = 'block';
|
|
1032
|
+
|
|
1033
|
+
// Get values from: 1) element_overrides, 2) traces array (pltz metadata), 3) bbox data
|
|
1034
|
+
const traceOverrides = getTraceOverrides(elementInfo);
|
|
1035
|
+
const traceIdx = elementInfo.index || 0;
|
|
1036
|
+
const traceFromMeta = traces[traceIdx] || {};
|
|
1037
|
+
|
|
1038
|
+
// Label: prefer user override, then pltz metadata, then bbox label
|
|
1039
|
+
const label = traceOverrides.label || traceFromMeta.label || bbox.label?.replace(/.*:\s*/, '') || '';
|
|
1040
|
+
const color = traceOverrides.color || traceFromMeta.color || '#1f77b4';
|
|
1041
|
+
const linewidth = traceOverrides.linewidth || traceFromMeta.linewidth || 1.0;
|
|
1042
|
+
const linestyle = traceOverrides.linestyle || traceFromMeta.linestyle || '-';
|
|
1043
|
+
const marker = traceOverrides.marker || traceFromMeta.marker || '';
|
|
1044
|
+
const markersize = traceOverrides.markersize || traceFromMeta.markersize || 4;
|
|
1045
|
+
const alpha = traceOverrides.alpha || traceFromMeta.alpha || 1;
|
|
1046
|
+
|
|
1047
|
+
document.getElementById('sel-trace-label').value = label;
|
|
1048
|
+
document.getElementById('sel-trace-color').value = color;
|
|
1049
|
+
document.getElementById('sel-trace-color-text').value = color;
|
|
1050
|
+
document.getElementById('sel-trace-linewidth').value = linewidth;
|
|
1051
|
+
document.getElementById('sel-trace-linestyle').value = linestyle;
|
|
1052
|
+
document.getElementById('sel-trace-marker').value = marker;
|
|
1053
|
+
document.getElementById('sel-trace-markersize').value = markersize;
|
|
1054
|
+
document.getElementById('sel-trace-alpha').value = alpha;
|
|
1055
|
+
} else if (type === 'scatter') {
|
|
1056
|
+
const props = document.getElementById('selected-scatter-props');
|
|
1057
|
+
props.style.display = 'block';
|
|
1058
|
+
|
|
1059
|
+
const scatterOverrides = getScatterOverrides(elementInfo);
|
|
1060
|
+
if (scatterOverrides) {
|
|
1061
|
+
document.getElementById('sel-scatter-color').value = scatterOverrides.color || '#1f77b4';
|
|
1062
|
+
document.getElementById('sel-scatter-color-text').value = scatterOverrides.color || '#1f77b4';
|
|
1063
|
+
document.getElementById('sel-scatter-size').value = scatterOverrides.size || 20;
|
|
1064
|
+
document.getElementById('sel-scatter-marker').value = scatterOverrides.marker || 'o';
|
|
1065
|
+
document.getElementById('sel-scatter-alpha').value = scatterOverrides.alpha || 0.7;
|
|
1066
|
+
document.getElementById('sel-scatter-edgecolor').value = scatterOverrides.edgecolor || '#000000';
|
|
1067
|
+
document.getElementById('sel-scatter-edgecolor-text').value = scatterOverrides.edgecolor || '#000000';
|
|
1068
|
+
}
|
|
1069
|
+
} else if (type === 'fill') {
|
|
1070
|
+
const props = document.getElementById('selected-fill-props');
|
|
1071
|
+
props.style.display = 'block';
|
|
1072
|
+
|
|
1073
|
+
const fillOverrides = getFillOverrides(elementInfo);
|
|
1074
|
+
if (fillOverrides) {
|
|
1075
|
+
document.getElementById('sel-fill-color').value = fillOverrides.color || '#1f77b4';
|
|
1076
|
+
document.getElementById('sel-fill-color-text').value = fillOverrides.color || '#1f77b4';
|
|
1077
|
+
document.getElementById('sel-fill-alpha').value = fillOverrides.alpha || 0.3;
|
|
1078
|
+
}
|
|
1079
|
+
} else if (type === 'bar') {
|
|
1080
|
+
const props = document.getElementById('selected-bar-props');
|
|
1081
|
+
props.style.display = 'block';
|
|
1082
|
+
} else if (type === 'label') {
|
|
1083
|
+
const props = document.getElementById('selected-label-props');
|
|
1084
|
+
props.style.display = 'block';
|
|
1085
|
+
|
|
1086
|
+
// Get label text from global overrides
|
|
1087
|
+
const labelName = elementInfo.displayName.toLowerCase();
|
|
1088
|
+
document.getElementById('sel-label-text').value = overrides[labelName] || '';
|
|
1089
|
+
document.getElementById('sel-label-fontsize').value = overrides.axis_fontsize || 7;
|
|
1090
|
+
} else if (type === 'panel') {
|
|
1091
|
+
const props = document.getElementById('selected-panel-props');
|
|
1092
|
+
props.style.display = 'block';
|
|
1093
|
+
|
|
1094
|
+
// Load existing panel overrides, fall back to actual bbox values
|
|
1095
|
+
const panelOverrides = getPanelOverrides(elementInfo);
|
|
1096
|
+
const panelBbox = elementBboxes[selectedElement] || {};
|
|
1097
|
+
document.getElementById('sel-panel-title').value = panelOverrides.title || panelBbox.title || '';
|
|
1098
|
+
document.getElementById('sel-panel-xlabel').value = panelOverrides.xlabel || panelBbox.xlabel || '';
|
|
1099
|
+
document.getElementById('sel-panel-ylabel').value = panelOverrides.ylabel || panelBbox.ylabel || '';
|
|
1100
|
+
} else if (type === 'legend') {
|
|
1101
|
+
// For legend, expand the legend section instead
|
|
1102
|
+
expandSection('section-legend');
|
|
1103
|
+
} else if (type === 'xaxis') {
|
|
1104
|
+
const props = document.getElementById('selected-xaxis-props');
|
|
1105
|
+
props.style.display = 'block';
|
|
1106
|
+
|
|
1107
|
+
// Load existing xaxis overrides
|
|
1108
|
+
const xaxisOverrides = getAxisOverrides(elementInfo, 'xaxis');
|
|
1109
|
+
document.getElementById('sel-xaxis-fontsize').value = xaxisOverrides.tick_fontsize || overrides.tick_fontsize || 7;
|
|
1110
|
+
document.getElementById('sel-xaxis-label-fontsize').value = xaxisOverrides.label_fontsize || overrides.axis_fontsize || 7;
|
|
1111
|
+
document.getElementById('sel-xaxis-direction').value = xaxisOverrides.tick_direction || overrides.tick_direction || 'out';
|
|
1112
|
+
document.getElementById('sel-xaxis-nticks').value = xaxisOverrides.n_ticks || overrides.x_n_ticks || 4;
|
|
1113
|
+
document.getElementById('sel-xaxis-hide-ticks').checked = xaxisOverrides.hide_ticks || false;
|
|
1114
|
+
document.getElementById('sel-xaxis-hide-label').checked = xaxisOverrides.hide_label || false;
|
|
1115
|
+
document.getElementById('sel-xaxis-hide-spine').checked = xaxisOverrides.hide_spine || false;
|
|
1116
|
+
} else if (type === 'yaxis') {
|
|
1117
|
+
const props = document.getElementById('selected-yaxis-props');
|
|
1118
|
+
props.style.display = 'block';
|
|
1119
|
+
|
|
1120
|
+
// Load existing yaxis overrides
|
|
1121
|
+
const yaxisOverrides = getAxisOverrides(elementInfo, 'yaxis');
|
|
1122
|
+
document.getElementById('sel-yaxis-fontsize').value = yaxisOverrides.tick_fontsize || overrides.tick_fontsize || 7;
|
|
1123
|
+
document.getElementById('sel-yaxis-label-fontsize').value = yaxisOverrides.label_fontsize || overrides.axis_fontsize || 7;
|
|
1124
|
+
document.getElementById('sel-yaxis-direction').value = yaxisOverrides.tick_direction || overrides.tick_direction || 'out';
|
|
1125
|
+
document.getElementById('sel-yaxis-nticks').value = yaxisOverrides.n_ticks || overrides.y_n_ticks || 4;
|
|
1126
|
+
document.getElementById('sel-yaxis-hide-ticks').checked = yaxisOverrides.hide_ticks || false;
|
|
1127
|
+
document.getElementById('sel-yaxis-hide-label').checked = yaxisOverrides.hide_label || false;
|
|
1128
|
+
document.getElementById('sel-yaxis-hide-spine').checked = yaxisOverrides.hide_spine || false;
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
// Show statistics for data elements (trace, scatter, fill, bar)
|
|
1132
|
+
if (['trace', 'scatter', 'fill', 'bar'].includes(type)) {
|
|
1133
|
+
showElementStatistics(bbox);
|
|
1134
|
+
} else {
|
|
1135
|
+
hideElementStatistics();
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
function showElementStatistics(bbox) {
|
|
1140
|
+
const statsDiv = document.getElementById('selected-stats');
|
|
1141
|
+
if (!bbox || !bbox.points || bbox.points.length === 0) {
|
|
1142
|
+
statsDiv.style.display = 'none';
|
|
1143
|
+
return;
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
statsDiv.style.display = 'block';
|
|
1147
|
+
|
|
1148
|
+
// Extract Y values from points (format: [[x,y], [x,y], ...])
|
|
1149
|
+
const yValues = bbox.points.map(pt => pt[1]).filter(v => isFinite(v));
|
|
1150
|
+
const xValues = bbox.points.map(pt => pt[0]).filter(v => isFinite(v));
|
|
1151
|
+
|
|
1152
|
+
if (yValues.length === 0) {
|
|
1153
|
+
statsDiv.style.display = 'none';
|
|
1154
|
+
return;
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
// Calculate statistics
|
|
1158
|
+
const n = yValues.length;
|
|
1159
|
+
const mean = yValues.reduce((a, b) => a + b, 0) / n;
|
|
1160
|
+
const variance = yValues.reduce((sum, v) => sum + Math.pow(v - mean, 2), 0) / n;
|
|
1161
|
+
const std = Math.sqrt(variance);
|
|
1162
|
+
const min = Math.min(...yValues);
|
|
1163
|
+
const max = Math.max(...yValues);
|
|
1164
|
+
const range = max - min;
|
|
1165
|
+
|
|
1166
|
+
// Format numbers appropriately
|
|
1167
|
+
const fmt = (v) => {
|
|
1168
|
+
if (Math.abs(v) < 0.01 || Math.abs(v) >= 10000) {
|
|
1169
|
+
return v.toExponential(2);
|
|
1170
|
+
}
|
|
1171
|
+
return v.toFixed(2);
|
|
1172
|
+
};
|
|
1173
|
+
|
|
1174
|
+
// Update display
|
|
1175
|
+
document.getElementById('stat-n').textContent = n;
|
|
1176
|
+
document.getElementById('stat-mean').textContent = fmt(mean);
|
|
1177
|
+
document.getElementById('stat-std').textContent = fmt(std);
|
|
1178
|
+
document.getElementById('stat-min').textContent = fmt(min);
|
|
1179
|
+
document.getElementById('stat-max').textContent = fmt(max);
|
|
1180
|
+
document.getElementById('stat-range').textContent = fmt(range);
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
function hideElementStatistics() {
|
|
1184
|
+
document.getElementById('selected-stats').style.display = 'none';
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
function getTraceOverrides(elementInfo) {
|
|
1188
|
+
// Initialize element overrides storage if not exists
|
|
1189
|
+
if (!overrides.element_overrides) {
|
|
1190
|
+
overrides.element_overrides = {};
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1193
|
+
const key = elementInfo.original;
|
|
1194
|
+
if (!overrides.element_overrides[key]) {
|
|
1195
|
+
// Try to get from traces array
|
|
1196
|
+
if (traces[elementInfo.index]) {
|
|
1197
|
+
overrides.element_overrides[key] = { ...traces[elementInfo.index] };
|
|
1198
|
+
} else {
|
|
1199
|
+
overrides.element_overrides[key] = {};
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1202
|
+
return overrides.element_overrides[key];
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
function getScatterOverrides(elementInfo) {
|
|
1206
|
+
if (!overrides.element_overrides) {
|
|
1207
|
+
overrides.element_overrides = {};
|
|
1208
|
+
}
|
|
1209
|
+
const key = elementInfo.original;
|
|
1210
|
+
if (!overrides.element_overrides[key]) {
|
|
1211
|
+
overrides.element_overrides[key] = {};
|
|
1212
|
+
}
|
|
1213
|
+
return overrides.element_overrides[key];
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
function getFillOverrides(elementInfo) {
|
|
1217
|
+
if (!overrides.element_overrides) {
|
|
1218
|
+
overrides.element_overrides = {};
|
|
1219
|
+
}
|
|
1220
|
+
const key = elementInfo.original;
|
|
1221
|
+
if (!overrides.element_overrides[key]) {
|
|
1222
|
+
overrides.element_overrides[key] = {};
|
|
1223
|
+
}
|
|
1224
|
+
return overrides.element_overrides[key];
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
function getPanelOverrides(elementInfo) {
|
|
1228
|
+
if (!overrides.element_overrides) {
|
|
1229
|
+
overrides.element_overrides = {};
|
|
1230
|
+
}
|
|
1231
|
+
const key = elementInfo.original;
|
|
1232
|
+
if (!overrides.element_overrides[key]) {
|
|
1233
|
+
overrides.element_overrides[key] = {};
|
|
1234
|
+
}
|
|
1235
|
+
return overrides.element_overrides[key];
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
function getAxisOverrides(elementInfo, axisType) {
|
|
1239
|
+
// Get overrides for xaxis or yaxis element
|
|
1240
|
+
if (!overrides.element_overrides) {
|
|
1241
|
+
overrides.element_overrides = {};
|
|
1242
|
+
}
|
|
1243
|
+
const key = elementInfo.original;
|
|
1244
|
+
if (!overrides.element_overrides[key]) {
|
|
1245
|
+
overrides.element_overrides[key] = {};
|
|
1246
|
+
}
|
|
1247
|
+
return overrides.element_overrides[key];
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
function applySelectedElementChanges() {
|
|
1251
|
+
if (!selectedElement) return;
|
|
1252
|
+
|
|
1253
|
+
const elementInfo = parseElementName(selectedElement);
|
|
1254
|
+
const type = elementInfo.type;
|
|
1255
|
+
|
|
1256
|
+
if (!overrides.element_overrides) {
|
|
1257
|
+
overrides.element_overrides = {};
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
if (type === 'trace') {
|
|
1261
|
+
overrides.element_overrides[selectedElement] = {
|
|
1262
|
+
label: document.getElementById('sel-trace-label').value,
|
|
1263
|
+
color: document.getElementById('sel-trace-color').value,
|
|
1264
|
+
linewidth: parseFloat(document.getElementById('sel-trace-linewidth').value),
|
|
1265
|
+
linestyle: document.getElementById('sel-trace-linestyle').value,
|
|
1266
|
+
marker: document.getElementById('sel-trace-marker').value,
|
|
1267
|
+
markersize: parseFloat(document.getElementById('sel-trace-markersize').value),
|
|
1268
|
+
alpha: parseFloat(document.getElementById('sel-trace-alpha').value)
|
|
1269
|
+
};
|
|
1270
|
+
} else if (type === 'scatter') {
|
|
1271
|
+
overrides.element_overrides[selectedElement] = {
|
|
1272
|
+
color: document.getElementById('sel-scatter-color').value,
|
|
1273
|
+
size: parseFloat(document.getElementById('sel-scatter-size').value),
|
|
1274
|
+
marker: document.getElementById('sel-scatter-marker').value,
|
|
1275
|
+
alpha: parseFloat(document.getElementById('sel-scatter-alpha').value),
|
|
1276
|
+
edgecolor: document.getElementById('sel-scatter-edgecolor').value
|
|
1277
|
+
};
|
|
1278
|
+
} else if (type === 'fill') {
|
|
1279
|
+
overrides.element_overrides[selectedElement] = {
|
|
1280
|
+
color: document.getElementById('sel-fill-color').value,
|
|
1281
|
+
alpha: parseFloat(document.getElementById('sel-fill-alpha').value)
|
|
1282
|
+
};
|
|
1283
|
+
} else if (type === 'label') {
|
|
1284
|
+
const labelName = elementInfo.displayName.toLowerCase();
|
|
1285
|
+
overrides[labelName] = document.getElementById('sel-label-text').value;
|
|
1286
|
+
overrides.axis_fontsize = parseFloat(document.getElementById('sel-label-fontsize').value);
|
|
1287
|
+
} else if (type === 'bar') {
|
|
1288
|
+
overrides.element_overrides[selectedElement] = {
|
|
1289
|
+
facecolor: document.getElementById('sel-bar-facecolor').value,
|
|
1290
|
+
edgecolor: document.getElementById('sel-bar-edgecolor').value,
|
|
1291
|
+
alpha: parseFloat(document.getElementById('sel-bar-alpha').value)
|
|
1292
|
+
};
|
|
1293
|
+
} else if (type === 'panel') {
|
|
1294
|
+
// Panel-specific overrides (per-axis) including title, xlabel, ylabel
|
|
1295
|
+
overrides.element_overrides[selectedElement] = {
|
|
1296
|
+
title: document.getElementById('sel-panel-title').value,
|
|
1297
|
+
xlabel: document.getElementById('sel-panel-xlabel').value,
|
|
1298
|
+
ylabel: document.getElementById('sel-panel-ylabel').value,
|
|
1299
|
+
facecolor: document.getElementById('sel-panel-facecolor').value,
|
|
1300
|
+
transparent: document.getElementById('sel-panel-transparent').checked,
|
|
1301
|
+
grid: document.getElementById('sel-panel-grid').checked
|
|
1302
|
+
};
|
|
1303
|
+
} else if (type === 'xaxis') {
|
|
1304
|
+
// X-Axis specific overrides
|
|
1305
|
+
overrides.element_overrides[selectedElement] = {
|
|
1306
|
+
tick_fontsize: parseFloat(document.getElementById('sel-xaxis-fontsize').value),
|
|
1307
|
+
label_fontsize: parseFloat(document.getElementById('sel-xaxis-label-fontsize').value),
|
|
1308
|
+
tick_direction: document.getElementById('sel-xaxis-direction').value,
|
|
1309
|
+
n_ticks: parseInt(document.getElementById('sel-xaxis-nticks').value),
|
|
1310
|
+
hide_ticks: document.getElementById('sel-xaxis-hide-ticks').checked,
|
|
1311
|
+
hide_label: document.getElementById('sel-xaxis-hide-label').checked,
|
|
1312
|
+
hide_spine: document.getElementById('sel-xaxis-hide-spine').checked
|
|
1313
|
+
};
|
|
1314
|
+
} else if (type === 'yaxis') {
|
|
1315
|
+
// Y-Axis specific overrides
|
|
1316
|
+
overrides.element_overrides[selectedElement] = {
|
|
1317
|
+
tick_fontsize: parseFloat(document.getElementById('sel-yaxis-fontsize').value),
|
|
1318
|
+
label_fontsize: parseFloat(document.getElementById('sel-yaxis-label-fontsize').value),
|
|
1319
|
+
tick_direction: document.getElementById('sel-yaxis-direction').value,
|
|
1320
|
+
n_ticks: parseInt(document.getElementById('sel-yaxis-nticks').value),
|
|
1321
|
+
hide_ticks: document.getElementById('sel-yaxis-hide-ticks').checked,
|
|
1322
|
+
hide_label: document.getElementById('sel-yaxis-hide-label').checked,
|
|
1323
|
+
hide_spine: document.getElementById('sel-yaxis-hide-spine').checked
|
|
1324
|
+
};
|
|
1325
|
+
}
|
|
1326
|
+
|
|
1327
|
+
// Trigger update
|
|
1328
|
+
updatePreview();
|
|
1329
|
+
document.getElementById('status').textContent = `Applied changes to ${elementInfo.displayName}`;
|
|
1330
|
+
}
|
|
1331
|
+
|
|
1332
|
+
// Sync color inputs
|
|
1333
|
+
function setupColorSync(colorId, textId) {
|
|
1334
|
+
const colorInput = document.getElementById(colorId);
|
|
1335
|
+
const textInput = document.getElementById(textId);
|
|
1336
|
+
if (colorInput && textInput) {
|
|
1337
|
+
colorInput.addEventListener('input', () => {
|
|
1338
|
+
textInput.value = colorInput.value;
|
|
1339
|
+
});
|
|
1340
|
+
textInput.addEventListener('input', () => {
|
|
1341
|
+
if (/^#[0-9A-Fa-f]{6}$/.test(textInput.value)) {
|
|
1342
|
+
colorInput.value = textInput.value;
|
|
1343
|
+
}
|
|
1344
|
+
});
|
|
1345
|
+
}
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
// Theme management
|
|
1349
|
+
function toggleTheme() {
|
|
1350
|
+
const html = document.documentElement;
|
|
1351
|
+
const current = html.getAttribute('data-theme');
|
|
1352
|
+
const next = current === 'dark' ? 'light' : 'dark';
|
|
1353
|
+
html.setAttribute('data-theme', next);
|
|
1354
|
+
document.getElementById('theme-icon').innerHTML = next === 'dark' ? '☾' : '☼';
|
|
1355
|
+
localStorage.setItem('scitex-editor-theme', next);
|
|
1356
|
+
}
|
|
1357
|
+
|
|
1358
|
+
// Collapsible sections
|
|
1359
|
+
function toggleSection(header) {
|
|
1360
|
+
header.classList.toggle('collapsed');
|
|
1361
|
+
const content = header.nextElementSibling;
|
|
1362
|
+
content.classList.toggle('collapsed');
|
|
1363
|
+
}
|
|
1364
|
+
|
|
1365
|
+
function switchAxisTab(axis) {
|
|
1366
|
+
// Update tab buttons
|
|
1367
|
+
document.querySelectorAll('.axis-tab').forEach(tab => {
|
|
1368
|
+
tab.classList.remove('active');
|
|
1369
|
+
});
|
|
1370
|
+
document.getElementById('axis-tab-' + axis).classList.add('active');
|
|
1371
|
+
|
|
1372
|
+
// Update panels
|
|
1373
|
+
document.querySelectorAll('.axis-panel').forEach(panel => {
|
|
1374
|
+
panel.style.display = 'none';
|
|
1375
|
+
});
|
|
1376
|
+
document.getElementById('axis-panel-' + axis).style.display = 'block';
|
|
1377
|
+
}
|
|
1378
|
+
|
|
1379
|
+
function toggleCustomLegendPosition() {
|
|
1380
|
+
const legendLoc = document.getElementById('legend_loc').value;
|
|
1381
|
+
const customCoordsDiv = document.getElementById('custom-legend-coords');
|
|
1382
|
+
customCoordsDiv.style.display = legendLoc === 'custom' ? 'flex' : 'none';
|
|
1383
|
+
}
|
|
1384
|
+
|
|
1385
|
+
// Dimension unit toggle
|
|
1386
|
+
function setDimensionUnit(unit) {
|
|
1387
|
+
if (unit === dimensionUnit) return;
|
|
1388
|
+
|
|
1389
|
+
const widthInput = document.getElementById('fig_width');
|
|
1390
|
+
const heightInput = document.getElementById('fig_height');
|
|
1391
|
+
const widthLabel = document.getElementById('fig_width_label');
|
|
1392
|
+
const heightLabel = document.getElementById('fig_height_label');
|
|
1393
|
+
const mmBtn = document.getElementById('unit-mm');
|
|
1394
|
+
const inchBtn = document.getElementById('unit-inch');
|
|
1395
|
+
|
|
1396
|
+
// Get current values
|
|
1397
|
+
let width = parseFloat(widthInput.value) || 0;
|
|
1398
|
+
let height = parseFloat(heightInput.value) || 0;
|
|
1399
|
+
|
|
1400
|
+
// Convert values
|
|
1401
|
+
if (unit === 'mm' && dimensionUnit === 'inch') {
|
|
1402
|
+
// inch to mm
|
|
1403
|
+
width = Math.round(width * INCH_TO_MM * 10) / 10;
|
|
1404
|
+
height = Math.round(height * INCH_TO_MM * 10) / 10;
|
|
1405
|
+
widthInput.min = 10;
|
|
1406
|
+
widthInput.max = 300;
|
|
1407
|
+
widthInput.step = 1;
|
|
1408
|
+
heightInput.min = 10;
|
|
1409
|
+
heightInput.max = 300;
|
|
1410
|
+
heightInput.step = 1;
|
|
1411
|
+
} else if (unit === 'inch' && dimensionUnit === 'mm') {
|
|
1412
|
+
// mm to inch
|
|
1413
|
+
width = Math.round(width * MM_TO_INCH * 100) / 100;
|
|
1414
|
+
height = Math.round(height * MM_TO_INCH * 100) / 100;
|
|
1415
|
+
widthInput.min = 0.5;
|
|
1416
|
+
widthInput.max = 12;
|
|
1417
|
+
widthInput.step = 0.05;
|
|
1418
|
+
heightInput.min = 0.5;
|
|
1419
|
+
heightInput.max = 12;
|
|
1420
|
+
heightInput.step = 0.05;
|
|
1421
|
+
}
|
|
1422
|
+
|
|
1423
|
+
// Update values and labels
|
|
1424
|
+
widthInput.value = width;
|
|
1425
|
+
heightInput.value = height;
|
|
1426
|
+
widthLabel.textContent = `Width (${unit})`;
|
|
1427
|
+
heightLabel.textContent = `Height (${unit})`;
|
|
1428
|
+
|
|
1429
|
+
// Update button states
|
|
1430
|
+
if (unit === 'mm') {
|
|
1431
|
+
mmBtn.classList.add('active');
|
|
1432
|
+
inchBtn.classList.remove('active');
|
|
1433
|
+
} else {
|
|
1434
|
+
mmBtn.classList.remove('active');
|
|
1435
|
+
inchBtn.classList.add('active');
|
|
1436
|
+
}
|
|
1437
|
+
|
|
1438
|
+
dimensionUnit = unit;
|
|
1439
|
+
}
|
|
1440
|
+
|
|
1441
|
+
// Background type management
|
|
1442
|
+
let backgroundType = 'transparent';
|
|
1443
|
+
let initializingBackground = true; // Flag to prevent updates during init
|
|
1444
|
+
|
|
1445
|
+
function setBackgroundType(type) {
|
|
1446
|
+
backgroundType = type;
|
|
1447
|
+
|
|
1448
|
+
// Update hidden inputs for collectOverrides
|
|
1449
|
+
const facecolorInput = document.getElementById('facecolor');
|
|
1450
|
+
const transparentInput = document.getElementById('transparent');
|
|
1451
|
+
|
|
1452
|
+
if (type === 'white') {
|
|
1453
|
+
facecolorInput.value = '#ffffff';
|
|
1454
|
+
transparentInput.value = 'false';
|
|
1455
|
+
} else if (type === 'black') {
|
|
1456
|
+
facecolorInput.value = '#000000';
|
|
1457
|
+
transparentInput.value = 'false';
|
|
1458
|
+
} else {
|
|
1459
|
+
// transparent
|
|
1460
|
+
facecolorInput.value = '#ffffff';
|
|
1461
|
+
transparentInput.value = 'true';
|
|
1462
|
+
}
|
|
1463
|
+
|
|
1464
|
+
// Update button states
|
|
1465
|
+
document.querySelectorAll('.bg-btn').forEach(btn => btn.classList.remove('active'));
|
|
1466
|
+
document.getElementById(`bg-${type}`).classList.add('active');
|
|
1467
|
+
|
|
1468
|
+
// Trigger update only after initialization
|
|
1469
|
+
if (!initializingBackground) {
|
|
1470
|
+
scheduleUpdate();
|
|
1471
|
+
}
|
|
1472
|
+
}
|
|
1473
|
+
|
|
1474
|
+
// Get figure dimensions in inches (for matplotlib)
|
|
1475
|
+
function getFigSizeInches() {
|
|
1476
|
+
let width = parseFloat(document.getElementById('fig_width').value) || 80;
|
|
1477
|
+
let height = parseFloat(document.getElementById('fig_height').value) || 68;
|
|
1478
|
+
|
|
1479
|
+
if (dimensionUnit === 'mm') {
|
|
1480
|
+
width = width * MM_TO_INCH;
|
|
1481
|
+
height = height * MM_TO_INCH;
|
|
1482
|
+
}
|
|
1483
|
+
|
|
1484
|
+
return [Math.round(width * 100) / 100, Math.round(height * 100) / 100];
|
|
1485
|
+
}
|
|
1486
|
+
|
|
1487
|
+
// Initialize fields
|
|
1488
|
+
document.addEventListener('DOMContentLoaded', () => {
|
|
1489
|
+
// Load saved theme
|
|
1490
|
+
const savedTheme = localStorage.getItem('scitex-editor-theme');
|
|
1491
|
+
if (savedTheme) {
|
|
1492
|
+
document.documentElement.setAttribute('data-theme', savedTheme);
|
|
1493
|
+
document.getElementById('theme-icon').innerHTML = savedTheme === 'dark' ? '☾' : '☼';
|
|
1494
|
+
}
|
|
1495
|
+
|
|
1496
|
+
// Labels - Title
|
|
1497
|
+
if (overrides.title) document.getElementById('title').value = overrides.title;
|
|
1498
|
+
document.getElementById('show_title').checked = overrides.show_title !== false;
|
|
1499
|
+
document.getElementById('title_fontsize').value = overrides.title_fontsize || 8;
|
|
1500
|
+
// Labels - Caption
|
|
1501
|
+
if (overrides.caption) document.getElementById('caption').value = overrides.caption;
|
|
1502
|
+
document.getElementById('show_caption').checked = overrides.show_caption || false;
|
|
1503
|
+
document.getElementById('caption_fontsize').value = overrides.caption_fontsize || 7;
|
|
1504
|
+
// Labels - Axis
|
|
1505
|
+
if (overrides.xlabel) document.getElementById('xlabel').value = overrides.xlabel;
|
|
1506
|
+
if (overrides.ylabel) document.getElementById('ylabel').value = overrides.ylabel;
|
|
1507
|
+
|
|
1508
|
+
// Axis limits
|
|
1509
|
+
if (overrides.xlim) {
|
|
1510
|
+
document.getElementById('xmin').value = overrides.xlim[0];
|
|
1511
|
+
document.getElementById('xmax').value = overrides.xlim[1];
|
|
1512
|
+
}
|
|
1513
|
+
if (overrides.ylim) {
|
|
1514
|
+
document.getElementById('ymin').value = overrides.ylim[0];
|
|
1515
|
+
document.getElementById('ymax').value = overrides.ylim[1];
|
|
1516
|
+
}
|
|
1517
|
+
|
|
1518
|
+
// Traces
|
|
1519
|
+
updateTracesList();
|
|
1520
|
+
|
|
1521
|
+
// Legend
|
|
1522
|
+
document.getElementById('legend_visible').checked = overrides.legend_visible !== false;
|
|
1523
|
+
document.getElementById('legend_loc').value = overrides.legend_loc || 'best';
|
|
1524
|
+
document.getElementById('legend_frameon').checked = overrides.legend_frameon || false;
|
|
1525
|
+
document.getElementById('legend_fontsize').value = overrides.legend_fontsize || 6;
|
|
1526
|
+
document.getElementById('legend_ncols').value = overrides.legend_ncols || 1;
|
|
1527
|
+
document.getElementById('legend_x').value = overrides.legend_x !== undefined ? overrides.legend_x : 0.95;
|
|
1528
|
+
document.getElementById('legend_y').value = overrides.legend_y !== undefined ? overrides.legend_y : 0.95;
|
|
1529
|
+
toggleCustomLegendPosition();
|
|
1530
|
+
|
|
1531
|
+
// Axis and Ticks - X Axis (Bottom)
|
|
1532
|
+
document.getElementById('x_n_ticks').value = overrides.x_n_ticks || overrides.n_ticks || 4;
|
|
1533
|
+
document.getElementById('hide_x_ticks').checked = overrides.hide_x_ticks || false;
|
|
1534
|
+
document.getElementById('x_tick_fontsize').value = overrides.x_tick_fontsize || overrides.tick_fontsize || 7;
|
|
1535
|
+
document.getElementById('x_tick_direction').value = overrides.x_tick_direction || overrides.tick_direction || 'out';
|
|
1536
|
+
document.getElementById('x_tick_length').value = overrides.x_tick_length || overrides.tick_length || 0.8;
|
|
1537
|
+
document.getElementById('x_tick_width').value = overrides.x_tick_width || overrides.tick_width || 0.2;
|
|
1538
|
+
// X Axis (Top)
|
|
1539
|
+
document.getElementById('show_x_top').checked = overrides.show_x_top || false;
|
|
1540
|
+
document.getElementById('x_top_mirror').checked = overrides.x_top_mirror || false;
|
|
1541
|
+
// Y Axis (Left)
|
|
1542
|
+
document.getElementById('y_n_ticks').value = overrides.y_n_ticks || overrides.n_ticks || 4;
|
|
1543
|
+
document.getElementById('hide_y_ticks').checked = overrides.hide_y_ticks || false;
|
|
1544
|
+
document.getElementById('y_tick_fontsize').value = overrides.y_tick_fontsize || overrides.tick_fontsize || 7;
|
|
1545
|
+
document.getElementById('y_tick_direction').value = overrides.y_tick_direction || overrides.tick_direction || 'out';
|
|
1546
|
+
document.getElementById('y_tick_length').value = overrides.y_tick_length || overrides.tick_length || 0.8;
|
|
1547
|
+
document.getElementById('y_tick_width').value = overrides.y_tick_width || overrides.tick_width || 0.2;
|
|
1548
|
+
// Y Axis (Right)
|
|
1549
|
+
document.getElementById('show_y_right').checked = overrides.show_y_right || false;
|
|
1550
|
+
document.getElementById('y_right_mirror').checked = overrides.y_right_mirror || false;
|
|
1551
|
+
// Spines
|
|
1552
|
+
document.getElementById('hide_bottom_spine').checked = overrides.hide_bottom_spine || false;
|
|
1553
|
+
document.getElementById('hide_left_spine').checked = overrides.hide_left_spine || false;
|
|
1554
|
+
// Z Axis (3D)
|
|
1555
|
+
document.getElementById('hide_z_ticks').checked = overrides.hide_z_ticks || false;
|
|
1556
|
+
document.getElementById('z_n_ticks').value = overrides.z_n_ticks || 4;
|
|
1557
|
+
document.getElementById('z_tick_fontsize').value = overrides.z_tick_fontsize || 7;
|
|
1558
|
+
document.getElementById('z_tick_direction').value = overrides.z_tick_direction || 'out';
|
|
1559
|
+
|
|
1560
|
+
// Style
|
|
1561
|
+
document.getElementById('grid').checked = overrides.grid || false;
|
|
1562
|
+
document.getElementById('hide_top_spine').checked = overrides.hide_top_spine !== false;
|
|
1563
|
+
document.getElementById('hide_right_spine').checked = overrides.hide_right_spine !== false;
|
|
1564
|
+
document.getElementById('axis_width').value = overrides.axis_width || 0.2;
|
|
1565
|
+
document.getElementById('axis_fontsize').value = overrides.axis_fontsize || 7;
|
|
1566
|
+
// Initialize background type from overrides
|
|
1567
|
+
const isTransparent = overrides.transparent !== false;
|
|
1568
|
+
const facecolor = overrides.facecolor || '#ffffff';
|
|
1569
|
+
document.getElementById('facecolor').value = facecolor;
|
|
1570
|
+
|
|
1571
|
+
if (isTransparent) {
|
|
1572
|
+
setBackgroundType('transparent');
|
|
1573
|
+
} else if (facecolor === '#000000') {
|
|
1574
|
+
setBackgroundType('black');
|
|
1575
|
+
} else {
|
|
1576
|
+
setBackgroundType('white');
|
|
1577
|
+
}
|
|
1578
|
+
|
|
1579
|
+
// Dimensions (convert from inches in metadata to mm by default)
|
|
1580
|
+
if (overrides.fig_size) {
|
|
1581
|
+
// fig_size is in inches in the JSON - convert to mm for default display
|
|
1582
|
+
const widthMm = Math.round(overrides.fig_size[0] * INCH_TO_MM);
|
|
1583
|
+
const heightMm = Math.round(overrides.fig_size[1] * INCH_TO_MM);
|
|
1584
|
+
document.getElementById('fig_width').value = widthMm;
|
|
1585
|
+
document.getElementById('fig_height').value = heightMm;
|
|
1586
|
+
}
|
|
1587
|
+
document.getElementById('dpi').value = overrides.dpi || 300;
|
|
1588
|
+
// Default unit is mm, which is already set in HTML and JS state
|
|
1589
|
+
|
|
1590
|
+
// Note: facecolor is now managed by background toggle buttons (white/transparent/black)
|
|
1591
|
+
// No text input sync needed
|
|
1592
|
+
|
|
1593
|
+
updateAnnotationsList();
|
|
1594
|
+
// NOTE: Don't call updatePreview() here - we want to use existing PNG/SVG
|
|
1595
|
+
// loadInitialPreview() will load the original file and then start auto-update
|
|
1596
|
+
initHoverSystem();
|
|
1597
|
+
refreshStats(); // Load statistical test results
|
|
1598
|
+
|
|
1599
|
+
// Setup color sync for selected element property inputs
|
|
1600
|
+
setupColorSync('sel-trace-color', 'sel-trace-color-text');
|
|
1601
|
+
setupColorSync('sel-scatter-color', 'sel-scatter-color-text');
|
|
1602
|
+
setupColorSync('sel-scatter-edgecolor', 'sel-scatter-edgecolor-text');
|
|
1603
|
+
setupColorSync('sel-fill-color', 'sel-fill-color-text');
|
|
1604
|
+
setupColorSync('sel-bar-facecolor', 'sel-bar-facecolor-text');
|
|
1605
|
+
setupColorSync('sel-bar-edgecolor', 'sel-bar-edgecolor-text');
|
|
1606
|
+
setupColorSync('sel-panel-facecolor', 'sel-panel-facecolor-text');
|
|
1607
|
+
|
|
1608
|
+
// Mark initialization complete - now background changes will trigger updates
|
|
1609
|
+
initializingBackground = false;
|
|
1610
|
+
|
|
1611
|
+
// Setup field-to-element synchronization (highlight element when field is focused)
|
|
1612
|
+
setupFieldToElementSync();
|
|
1613
|
+
|
|
1614
|
+
// Load initial preview from existing PNG/SVG (no re-render)
|
|
1615
|
+
loadInitialPreview();
|
|
1616
|
+
|
|
1617
|
+
// Add resize handler to update overlay when window/image size changes
|
|
1618
|
+
window.addEventListener('resize', () => {
|
|
1619
|
+
updateOverlay();
|
|
1620
|
+
});
|
|
1621
|
+
|
|
1622
|
+
// Use ResizeObserver to detect when the preview container changes size
|
|
1623
|
+
const previewContainer = document.getElementById('preview-container');
|
|
1624
|
+
if (previewContainer && typeof ResizeObserver !== 'undefined') {
|
|
1625
|
+
const resizeObserver = new ResizeObserver(() => {
|
|
1626
|
+
updateOverlay();
|
|
1627
|
+
});
|
|
1628
|
+
resizeObserver.observe(previewContainer);
|
|
1629
|
+
}
|
|
1630
|
+
});
|
|
1631
|
+
|
|
1632
|
+
// =============================================================================
|
|
1633
|
+
// Loading Helpers
|
|
1634
|
+
// =============================================================================
|
|
1635
|
+
function showLoading() {
|
|
1636
|
+
const overlay = document.getElementById('loading-overlay');
|
|
1637
|
+
if (overlay) overlay.style.display = 'flex';
|
|
1638
|
+
}
|
|
1639
|
+
|
|
1640
|
+
function hideLoading() {
|
|
1641
|
+
const overlay = document.getElementById('loading-overlay');
|
|
1642
|
+
if (overlay) overlay.style.display = 'none';
|
|
1643
|
+
}
|
|
1644
|
+
|
|
1645
|
+
// Update form controls from overrides (used when switching panels)
|
|
1646
|
+
function updateControlsFromOverrides() {
|
|
1647
|
+
console.log('updateControlsFromOverrides called');
|
|
1648
|
+
console.log('overrides.traces:', overrides.traces);
|
|
1649
|
+
console.log('traces variable:', traces);
|
|
1650
|
+
|
|
1651
|
+
// Update title - try both id and name selectors
|
|
1652
|
+
const titleInput = document.getElementById('title') || document.querySelector('input[name="title"]');
|
|
1653
|
+
if (titleInput) {
|
|
1654
|
+
// Try overrides.title first, then axes[0].title
|
|
1655
|
+
let title = overrides.title;
|
|
1656
|
+
if (title === undefined && overrides.axes && overrides.axes[0]) {
|
|
1657
|
+
title = overrides.axes[0].title || '';
|
|
1658
|
+
}
|
|
1659
|
+
titleInput.value = title || '';
|
|
1660
|
+
}
|
|
1661
|
+
|
|
1662
|
+
// Update figure size
|
|
1663
|
+
const widthInput = document.getElementById('fig-width');
|
|
1664
|
+
const heightInput = document.getElementById('fig-height');
|
|
1665
|
+
if (widthInput && overrides.figure_width !== undefined) {
|
|
1666
|
+
widthInput.value = overrides.figure_width;
|
|
1667
|
+
}
|
|
1668
|
+
if (heightInput && overrides.figure_height !== undefined) {
|
|
1669
|
+
heightInput.value = overrides.figure_height;
|
|
1670
|
+
}
|
|
1671
|
+
|
|
1672
|
+
// Update xlabel, ylabel - try overrides first, then axes[0]
|
|
1673
|
+
const xlabelInput = document.getElementById('xlabel');
|
|
1674
|
+
const ylabelInput = document.getElementById('ylabel');
|
|
1675
|
+
let xlabel = overrides.xlabel;
|
|
1676
|
+
let ylabel = overrides.ylabel;
|
|
1677
|
+
if (overrides.axes && overrides.axes[0]) {
|
|
1678
|
+
xlabel = xlabel || overrides.axes[0].xlabel || '';
|
|
1679
|
+
ylabel = ylabel || overrides.axes[0].ylabel || '';
|
|
1680
|
+
}
|
|
1681
|
+
if (xlabelInput) xlabelInput.value = xlabel || '';
|
|
1682
|
+
if (ylabelInput) ylabelInput.value = ylabel || '';
|
|
1683
|
+
|
|
1684
|
+
// Update traces list
|
|
1685
|
+
updateTracesList();
|
|
1686
|
+
}
|
|
1687
|
+
|
|
1688
|
+
// Load preview from existing file without re-rendering
|
|
1689
|
+
async function loadInitialPreview() {
|
|
1690
|
+
setStatus('Loading preview...', false);
|
|
1691
|
+
try {
|
|
1692
|
+
const darkMode = isDarkMode();
|
|
1693
|
+
const resp = await fetch(`/preview?dark_mode=${darkMode}`);
|
|
1694
|
+
const data = await resp.json();
|
|
1695
|
+
|
|
1696
|
+
console.log('=== PREVIEW DATA RECEIVED ===');
|
|
1697
|
+
console.log('format:', data.format);
|
|
1698
|
+
console.log('img_size:', data.img_size);
|
|
1699
|
+
console.log('bboxes keys:', Object.keys(data.bboxes || {}));
|
|
1700
|
+
console.log('bboxes:', JSON.stringify(data.bboxes, null, 2));
|
|
1701
|
+
|
|
1702
|
+
const previewContainer = document.getElementById('preview-container');
|
|
1703
|
+
const img = document.getElementById('preview-img');
|
|
1704
|
+
|
|
1705
|
+
if (data.format === 'svg' && data.svg) {
|
|
1706
|
+
// Handle SVG: replace img with inline SVG
|
|
1707
|
+
const svgWrapper = document.createElement('div');
|
|
1708
|
+
svgWrapper.id = 'preview-svg-wrapper';
|
|
1709
|
+
svgWrapper.innerHTML = data.svg;
|
|
1710
|
+
svgWrapper.style.width = '100%';
|
|
1711
|
+
svgWrapper.style.maxHeight = '70vh';
|
|
1712
|
+
|
|
1713
|
+
// Find the SVG element and set styles
|
|
1714
|
+
const svgEl = svgWrapper.querySelector('svg');
|
|
1715
|
+
if (svgEl) {
|
|
1716
|
+
svgEl.style.width = '100%';
|
|
1717
|
+
svgEl.style.height = 'auto';
|
|
1718
|
+
svgEl.style.maxHeight = '70vh';
|
|
1719
|
+
svgEl.id = 'preview-img'; // Keep same ID for event handlers
|
|
1720
|
+
}
|
|
1721
|
+
|
|
1722
|
+
img.style.display = 'none';
|
|
1723
|
+
const existingWrapper = document.getElementById('preview-svg-wrapper');
|
|
1724
|
+
if (existingWrapper) existingWrapper.remove();
|
|
1725
|
+
previewContainer.appendChild(svgWrapper);
|
|
1726
|
+
} else if (data.image) {
|
|
1727
|
+
// Handle PNG: show as base64 image
|
|
1728
|
+
img.src = 'data:image/png;base64,' + data.image;
|
|
1729
|
+
img.style.display = 'block';
|
|
1730
|
+
const existingWrapper = document.getElementById('preview-svg-wrapper');
|
|
1731
|
+
if (existingWrapper) existingWrapper.remove();
|
|
1732
|
+
}
|
|
1733
|
+
|
|
1734
|
+
if (data.bboxes) {
|
|
1735
|
+
elementBboxes = data.bboxes;
|
|
1736
|
+
originalBboxes = JSON.parse(JSON.stringify(data.bboxes)); // Deep copy
|
|
1737
|
+
if (data.bboxes._meta) {
|
|
1738
|
+
schemaMeta = data.bboxes._meta;
|
|
1739
|
+
}
|
|
1740
|
+
console.log('Loaded bboxes:', Object.keys(elementBboxes).filter(k => k !== '_meta'));
|
|
1741
|
+
}
|
|
1742
|
+
if (data.img_size) {
|
|
1743
|
+
imgSize = data.img_size;
|
|
1744
|
+
originalImgSize = {...data.img_size}; // Copy
|
|
1745
|
+
console.log('Loaded imgSize:', imgSize);
|
|
1746
|
+
}
|
|
1747
|
+
|
|
1748
|
+
isShowingOriginalPreview = true;
|
|
1749
|
+
updateOverlay();
|
|
1750
|
+
setStatus('Preview loaded', false);
|
|
1751
|
+
|
|
1752
|
+
// Initialize hover system for the SVG if needed
|
|
1753
|
+
if (data.format === 'svg') {
|
|
1754
|
+
const svgWrapper = document.getElementById('preview-svg-wrapper');
|
|
1755
|
+
if (svgWrapper) {
|
|
1756
|
+
initHoverSystemForElement(svgWrapper.querySelector('svg'));
|
|
1757
|
+
}
|
|
1758
|
+
}
|
|
1759
|
+
|
|
1760
|
+
// Draw debug bboxes if debug mode is on
|
|
1761
|
+
if (debugMode) {
|
|
1762
|
+
drawDebugBboxes();
|
|
1763
|
+
}
|
|
1764
|
+
|
|
1765
|
+
// Handle multi-panel figz bundles
|
|
1766
|
+
if (data.panel_info && data.panel_info.panels) {
|
|
1767
|
+
panelData = data.panel_info;
|
|
1768
|
+
currentPanelIndex = data.panel_info.current_index || 0;
|
|
1769
|
+
console.log('Multi-panel figz detected:', panelData.panels.length, 'panels');
|
|
1770
|
+
loadPanelGrid();
|
|
1771
|
+
}
|
|
1772
|
+
|
|
1773
|
+
// Start auto-update AFTER initial preview is loaded
|
|
1774
|
+
setAutoUpdateInterval();
|
|
1775
|
+
} catch (e) {
|
|
1776
|
+
setStatus('Error loading preview: ' + e.message, true);
|
|
1777
|
+
console.error('Preview load error:', e);
|
|
1778
|
+
// Start auto-update even on error so the editor works
|
|
1779
|
+
setAutoUpdateInterval();
|
|
1780
|
+
}
|
|
1781
|
+
}
|
|
1782
|
+
|
|
1783
|
+
// =============================================================================
|
|
1784
|
+
// Multi-Panel Navigation
|
|
1785
|
+
// =============================================================================
|
|
1786
|
+
async function loadPanelGrid() {
|
|
1787
|
+
if (!panelData || panelData.panels.length <= 1) {
|
|
1788
|
+
// Not a multi-panel bundle or only one panel
|
|
1789
|
+
document.getElementById('panel-grid-section').style.display = 'none';
|
|
1790
|
+
document.getElementById('preview-header').style.display = 'none';
|
|
1791
|
+
return;
|
|
1792
|
+
}
|
|
1793
|
+
|
|
1794
|
+
console.log('Loading panel canvas for', panelData.panels.length, 'panels');
|
|
1795
|
+
|
|
1796
|
+
// Show panel header
|
|
1797
|
+
document.getElementById('preview-header').style.display = 'flex';
|
|
1798
|
+
|
|
1799
|
+
// Fetch all panel images with bboxes
|
|
1800
|
+
try {
|
|
1801
|
+
const resp = await fetch('/panels');
|
|
1802
|
+
const data = await resp.json();
|
|
1803
|
+
|
|
1804
|
+
if (data.error) {
|
|
1805
|
+
console.error('Panel canvas error:', data.error);
|
|
1806
|
+
return;
|
|
1807
|
+
}
|
|
1808
|
+
|
|
1809
|
+
const canvasEl = document.getElementById('panel-canvas');
|
|
1810
|
+
canvasEl.innerHTML = '';
|
|
1811
|
+
|
|
1812
|
+
// Calculate layout - arrange panels in a grid-like canvas
|
|
1813
|
+
const numPanels = data.panels.length;
|
|
1814
|
+
const cols = Math.ceil(Math.sqrt(numPanels));
|
|
1815
|
+
const baseWidth = 220;
|
|
1816
|
+
const baseHeight = 180;
|
|
1817
|
+
const padding = 15;
|
|
1818
|
+
|
|
1819
|
+
data.panels.forEach((panel, idx) => {
|
|
1820
|
+
// Store bboxes and imgSize in cache for interactive hover/click
|
|
1821
|
+
if (panel.bboxes && panel.img_size) {
|
|
1822
|
+
panelBboxesCache[panel.name] = {
|
|
1823
|
+
bboxes: panel.bboxes,
|
|
1824
|
+
imgSize: panel.img_size
|
|
1825
|
+
};
|
|
1826
|
+
console.log(`Panel ${panel.name}: ${Object.keys(panel.bboxes).filter(k => k !== '_meta').length} bboxes, img: ${panel.img_size.width}x${panel.img_size.height}`);
|
|
1827
|
+
} else {
|
|
1828
|
+
console.warn(`Panel ${panel.name}: missing bboxes or img_size`, {bboxes: !!panel.bboxes, img_size: !!panel.img_size});
|
|
1829
|
+
}
|
|
1830
|
+
|
|
1831
|
+
// Calculate position
|
|
1832
|
+
const col = idx % cols;
|
|
1833
|
+
const row = Math.floor(idx / cols);
|
|
1834
|
+
if (!panelPositions[panel.name]) {
|
|
1835
|
+
panelPositions[panel.name] = {
|
|
1836
|
+
x: padding + col * (baseWidth + padding),
|
|
1837
|
+
y: padding + row * (baseHeight + padding),
|
|
1838
|
+
width: baseWidth,
|
|
1839
|
+
height: baseHeight,
|
|
1840
|
+
};
|
|
1841
|
+
}
|
|
1842
|
+
const pos = panelPositions[panel.name];
|
|
1843
|
+
|
|
1844
|
+
const item = document.createElement('div');
|
|
1845
|
+
item.className = 'panel-canvas-item' + (idx === currentPanelIndex ? ' active' : '');
|
|
1846
|
+
item.dataset.panelIndex = idx;
|
|
1847
|
+
item.dataset.panelName = panel.name;
|
|
1848
|
+
item.style.left = pos.x + 'px';
|
|
1849
|
+
item.style.top = pos.y + 'px';
|
|
1850
|
+
item.style.width = pos.width + 'px';
|
|
1851
|
+
item.style.height = pos.height + 'px';
|
|
1852
|
+
|
|
1853
|
+
if (panel.image) {
|
|
1854
|
+
item.innerHTML = `
|
|
1855
|
+
<span class="panel-canvas-label">Panel ${panel.name}</span>
|
|
1856
|
+
<div class="panel-card-container">
|
|
1857
|
+
<img src="data:image/png;base64,${panel.image}" alt="Panel ${panel.name}">
|
|
1858
|
+
<svg class="panel-card-overlay" id="panel-overlay-${idx}"></svg>
|
|
1859
|
+
</div>
|
|
1860
|
+
<div class="panel-canvas-resize"></div>
|
|
1861
|
+
`;
|
|
1862
|
+
} else {
|
|
1863
|
+
item.innerHTML = `
|
|
1864
|
+
<span class="panel-canvas-label">Panel ${panel.name}</span>
|
|
1865
|
+
<div style="padding: 20px; color: var(--text-muted);">No preview</div>
|
|
1866
|
+
`;
|
|
1867
|
+
}
|
|
1868
|
+
|
|
1869
|
+
// Add interactive event handlers
|
|
1870
|
+
initCanvasItemInteraction(item, idx, panel.name);
|
|
1871
|
+
|
|
1872
|
+
canvasEl.appendChild(item);
|
|
1873
|
+
});
|
|
1874
|
+
|
|
1875
|
+
// Update canvas height to fit all panels
|
|
1876
|
+
const maxY = Math.max(...Object.values(panelPositions).map(p => p.y + p.height)) + padding;
|
|
1877
|
+
canvasEl.style.minHeight = Math.max(400, maxY) + 'px';
|
|
1878
|
+
|
|
1879
|
+
// Update panel indicator
|
|
1880
|
+
updatePanelIndicator();
|
|
1881
|
+
|
|
1882
|
+
// Show canvas for multi-panel figures
|
|
1883
|
+
if (data.panels.length > 1) {
|
|
1884
|
+
showingPanelGrid = true;
|
|
1885
|
+
document.getElementById('panel-grid-section').style.display = 'block';
|
|
1886
|
+
// Hide single-panel preview for multi-panel bundles
|
|
1887
|
+
const previewWrapper = document.querySelector('.preview-wrapper');
|
|
1888
|
+
if (previewWrapper) {
|
|
1889
|
+
previewWrapper.style.display = 'none';
|
|
1890
|
+
}
|
|
1891
|
+
}
|
|
1892
|
+
} catch (e) {
|
|
1893
|
+
console.error('Error loading panels:', e);
|
|
1894
|
+
}
|
|
1895
|
+
}
|
|
1896
|
+
|
|
1897
|
+
// Initialize interactive handlers for canvas panel items
|
|
1898
|
+
function initCanvasItemInteraction(item, panelIdx, panelName) {
|
|
1899
|
+
const container = item.querySelector('.panel-card-container');
|
|
1900
|
+
if (!container) return;
|
|
1901
|
+
|
|
1902
|
+
const img = container.querySelector('img');
|
|
1903
|
+
const overlay = container.querySelector('svg');
|
|
1904
|
+
if (!img || !overlay) return;
|
|
1905
|
+
|
|
1906
|
+
// Wait for image to load to get dimensions
|
|
1907
|
+
img.addEventListener('load', () => {
|
|
1908
|
+
overlay.setAttribute('width', img.offsetWidth);
|
|
1909
|
+
overlay.setAttribute('height', img.offsetHeight);
|
|
1910
|
+
overlay.style.width = img.offsetWidth + 'px';
|
|
1911
|
+
overlay.style.height = img.offsetHeight + 'px';
|
|
1912
|
+
});
|
|
1913
|
+
|
|
1914
|
+
// Mousemove for hover detection
|
|
1915
|
+
container.addEventListener('mousemove', (e) => {
|
|
1916
|
+
const panelCache = panelBboxesCache[panelName];
|
|
1917
|
+
if (!panelCache) return;
|
|
1918
|
+
|
|
1919
|
+
const rect = img.getBoundingClientRect();
|
|
1920
|
+
const x = e.clientX - rect.left;
|
|
1921
|
+
const y = e.clientY - rect.top;
|
|
1922
|
+
|
|
1923
|
+
const scaleX = panelCache.imgSize.width / rect.width;
|
|
1924
|
+
const scaleY = panelCache.imgSize.height / rect.height;
|
|
1925
|
+
const imgX = x * scaleX;
|
|
1926
|
+
const imgY = y * scaleY;
|
|
1927
|
+
|
|
1928
|
+
const element = findElementInPanelAt(imgX, imgY, panelCache.bboxes);
|
|
1929
|
+
if (element !== panelHoveredElement || activePanelCard !== item) {
|
|
1930
|
+
panelHoveredElement = element;
|
|
1931
|
+
activePanelCard = item;
|
|
1932
|
+
updatePanelOverlay(overlay, panelCache.bboxes, panelCache.imgSize, rect.width, rect.height, panelHoveredElement, null);
|
|
1933
|
+
}
|
|
1934
|
+
});
|
|
1935
|
+
|
|
1936
|
+
// Mouseleave to clear hover
|
|
1937
|
+
container.addEventListener('mouseleave', () => {
|
|
1938
|
+
panelHoveredElement = null;
|
|
1939
|
+
if (activePanelCard === item) {
|
|
1940
|
+
updatePanelOverlay(overlay, {}, {width: 0, height: 0}, 0, 0, null, null);
|
|
1941
|
+
}
|
|
1942
|
+
});
|
|
1943
|
+
|
|
1944
|
+
// Click to select element
|
|
1945
|
+
container.addEventListener('click', (e) => {
|
|
1946
|
+
e.stopPropagation();
|
|
1947
|
+
|
|
1948
|
+
// Recalculate element at click position (in case hover didn't detect it)
|
|
1949
|
+
const panelCache = panelBboxesCache[panelName];
|
|
1950
|
+
let clickedElement = panelHoveredElement;
|
|
1951
|
+
|
|
1952
|
+
if (panelCache && img) {
|
|
1953
|
+
const rect = img.getBoundingClientRect();
|
|
1954
|
+
const x = e.clientX - rect.left;
|
|
1955
|
+
const y = e.clientY - rect.top;
|
|
1956
|
+
|
|
1957
|
+
const scaleX = panelCache.imgSize.width / rect.width;
|
|
1958
|
+
const scaleY = panelCache.imgSize.height / rect.height;
|
|
1959
|
+
const imgX = x * scaleX;
|
|
1960
|
+
const imgY = y * scaleY;
|
|
1961
|
+
|
|
1962
|
+
clickedElement = findElementInPanelAt(imgX, imgY, panelCache.bboxes);
|
|
1963
|
+
console.log(`Click at (${imgX.toFixed(0)}, ${imgY.toFixed(0)}) -> element: ${clickedElement}`);
|
|
1964
|
+
}
|
|
1965
|
+
|
|
1966
|
+
if (clickedElement) {
|
|
1967
|
+
document.querySelectorAll('.panel-canvas-item').forEach((c, i) => {
|
|
1968
|
+
c.classList.toggle('active', i === panelIdx);
|
|
1969
|
+
});
|
|
1970
|
+
|
|
1971
|
+
// If already on this panel, just update selection without server call
|
|
1972
|
+
if (currentPanelIndex === panelIdx && panelCache) {
|
|
1973
|
+
console.log(`Same panel (${panelIdx}), updating selection to: ${clickedElement}`);
|
|
1974
|
+
selectedElement = clickedElement;
|
|
1975
|
+
// Sync elementBboxes with panel cache bboxes
|
|
1976
|
+
elementBboxes = panelCache.bboxes || {};
|
|
1977
|
+
imgSize = panelCache.imgSize || imgSize;
|
|
1978
|
+
console.log('elementBboxes keys:', Object.keys(elementBboxes));
|
|
1979
|
+
updateOverlay();
|
|
1980
|
+
console.log('Calling scrollToSection with:', selectedElement);
|
|
1981
|
+
scrollToSection(selectedElement);
|
|
1982
|
+
setStatus(`Selected: ${clickedElement}`, false);
|
|
1983
|
+
} else {
|
|
1984
|
+
currentPanelIndex = panelIdx;
|
|
1985
|
+
loadPanelForEditing(panelIdx, panelName, clickedElement);
|
|
1986
|
+
}
|
|
1987
|
+
} else {
|
|
1988
|
+
console.log(`No element found, selecting panel ${panelName}`);
|
|
1989
|
+
selectPanel(panelIdx);
|
|
1990
|
+
}
|
|
1991
|
+
});
|
|
1992
|
+
|
|
1993
|
+
// Drag support for repositioning
|
|
1994
|
+
item.addEventListener('mousedown', (e) => {
|
|
1995
|
+
if (e.target.classList.contains('panel-canvas-resize')) {
|
|
1996
|
+
startResize(e, item, panelName);
|
|
1997
|
+
} else if (!e.target.closest('.panel-card-container')) {
|
|
1998
|
+
startDrag(e, item, panelName);
|
|
1999
|
+
}
|
|
2000
|
+
});
|
|
2001
|
+
}
|
|
2002
|
+
|
|
2003
|
+
// Initialize interactive hover/click handlers for a panel card
|
|
2004
|
+
function initPanelCardInteraction(card, panelIdx, panelName) {
|
|
2005
|
+
const container = card.querySelector('.panel-card-container');
|
|
2006
|
+
if (!container) return;
|
|
2007
|
+
|
|
2008
|
+
const img = container.querySelector('img');
|
|
2009
|
+
const overlay = container.querySelector('svg');
|
|
2010
|
+
if (!img || !overlay) return;
|
|
2011
|
+
|
|
2012
|
+
// Wait for image to load to get dimensions
|
|
2013
|
+
img.addEventListener('load', () => {
|
|
2014
|
+
overlay.setAttribute('width', img.offsetWidth);
|
|
2015
|
+
overlay.setAttribute('height', img.offsetHeight);
|
|
2016
|
+
overlay.style.width = img.offsetWidth + 'px';
|
|
2017
|
+
overlay.style.height = img.offsetHeight + 'px';
|
|
2018
|
+
});
|
|
2019
|
+
|
|
2020
|
+
// Mousemove for hover detection
|
|
2021
|
+
container.addEventListener('mousemove', (e) => {
|
|
2022
|
+
const panelCache = panelBboxesCache[panelName];
|
|
2023
|
+
if (!panelCache) return;
|
|
2024
|
+
|
|
2025
|
+
const rect = img.getBoundingClientRect();
|
|
2026
|
+
const x = e.clientX - rect.left;
|
|
2027
|
+
const y = e.clientY - rect.top;
|
|
2028
|
+
|
|
2029
|
+
const scaleX = panelCache.imgSize.width / rect.width;
|
|
2030
|
+
const scaleY = panelCache.imgSize.height / rect.height;
|
|
2031
|
+
const imgX = x * scaleX;
|
|
2032
|
+
const imgY = y * scaleY;
|
|
2033
|
+
|
|
2034
|
+
// Find element at cursor using panel's bboxes
|
|
2035
|
+
const element = findElementInPanelAt(imgX, imgY, panelCache.bboxes);
|
|
2036
|
+
if (element !== panelHoveredElement || activePanelCard !== card) {
|
|
2037
|
+
panelHoveredElement = element;
|
|
2038
|
+
activePanelCard = card;
|
|
2039
|
+
updatePanelOverlay(overlay, panelCache.bboxes, panelCache.imgSize, rect.width, rect.height, panelHoveredElement, null);
|
|
2040
|
+
}
|
|
2041
|
+
});
|
|
2042
|
+
|
|
2043
|
+
// Mouseleave to clear hover
|
|
2044
|
+
container.addEventListener('mouseleave', () => {
|
|
2045
|
+
panelHoveredElement = null;
|
|
2046
|
+
if (activePanelCard === card) {
|
|
2047
|
+
updatePanelOverlay(overlay, {}, {width: 0, height: 0}, 0, 0, null, null);
|
|
2048
|
+
}
|
|
2049
|
+
});
|
|
2050
|
+
|
|
2051
|
+
// Click to select element
|
|
2052
|
+
container.addEventListener('click', (e) => {
|
|
2053
|
+
e.stopPropagation(); // Prevent card click from triggering selectPanel
|
|
2054
|
+
|
|
2055
|
+
if (panelHoveredElement) {
|
|
2056
|
+
// Set this panel as current and select the element
|
|
2057
|
+
currentPanelIndex = panelIdx;
|
|
2058
|
+
|
|
2059
|
+
// Update active state in grid
|
|
2060
|
+
document.querySelectorAll('.panel-card').forEach((c, i) => {
|
|
2061
|
+
c.classList.toggle('active', i === panelIdx);
|
|
2062
|
+
});
|
|
2063
|
+
|
|
2064
|
+
// Load this panel's data into the main editor
|
|
2065
|
+
loadPanelForEditing(panelIdx, panelName, panelHoveredElement);
|
|
2066
|
+
} else {
|
|
2067
|
+
// No element hovered, select the panel itself
|
|
2068
|
+
selectPanel(panelIdx);
|
|
2069
|
+
}
|
|
2070
|
+
});
|
|
2071
|
+
}
|
|
2072
|
+
|
|
2073
|
+
// Find element at position within a panel's bboxes
|
|
2074
|
+
function findElementInPanelAt(x, y, bboxes) {
|
|
2075
|
+
const PROXIMITY_THRESHOLD = 15;
|
|
2076
|
+
const SCATTER_THRESHOLD = 20;
|
|
2077
|
+
|
|
2078
|
+
let closestDataElement = null;
|
|
2079
|
+
let minDistance = Infinity;
|
|
2080
|
+
|
|
2081
|
+
for (const [name, bbox] of Object.entries(bboxes)) {
|
|
2082
|
+
if (name === '_meta') continue;
|
|
2083
|
+
|
|
2084
|
+
// Check data elements with points
|
|
2085
|
+
if (bbox.points && bbox.points.length > 0) {
|
|
2086
|
+
if (x >= bbox.x0 - SCATTER_THRESHOLD && x <= bbox.x1 + SCATTER_THRESHOLD &&
|
|
2087
|
+
y >= bbox.y0 - SCATTER_THRESHOLD && y <= bbox.y1 + SCATTER_THRESHOLD) {
|
|
2088
|
+
|
|
2089
|
+
const elementType = bbox.element_type || 'line';
|
|
2090
|
+
let dist;
|
|
2091
|
+
|
|
2092
|
+
if (elementType === 'scatter') {
|
|
2093
|
+
dist = distanceToNearestPoint(x, y, bbox.points);
|
|
2094
|
+
} else {
|
|
2095
|
+
dist = distanceToLine(x, y, bbox.points);
|
|
2096
|
+
}
|
|
2097
|
+
|
|
2098
|
+
if (dist < minDistance) {
|
|
2099
|
+
minDistance = dist;
|
|
2100
|
+
closestDataElement = name;
|
|
2101
|
+
}
|
|
2102
|
+
}
|
|
2103
|
+
}
|
|
2104
|
+
}
|
|
2105
|
+
|
|
2106
|
+
if (closestDataElement) {
|
|
2107
|
+
const bbox = bboxes[closestDataElement];
|
|
2108
|
+
const threshold = (bbox.element_type === 'scatter') ? SCATTER_THRESHOLD : PROXIMITY_THRESHOLD;
|
|
2109
|
+
if (minDistance <= threshold) {
|
|
2110
|
+
return closestDataElement;
|
|
2111
|
+
}
|
|
2112
|
+
}
|
|
2113
|
+
|
|
2114
|
+
// Check bbox containment for other elements
|
|
2115
|
+
const elementMatches = [];
|
|
2116
|
+
const panelMatches = [];
|
|
2117
|
+
|
|
2118
|
+
for (const [name, bbox] of Object.entries(bboxes)) {
|
|
2119
|
+
if (name === '_meta') continue;
|
|
2120
|
+
if (x >= bbox.x0 && x <= bbox.x1 && y >= bbox.y0 && y <= bbox.y1) {
|
|
2121
|
+
const area = (bbox.x1 - bbox.x0) * (bbox.y1 - bbox.y0);
|
|
2122
|
+
const isPanel = bbox.is_panel || name.endsWith('_panel');
|
|
2123
|
+
const hasPoints = bbox.points && bbox.points.length > 0;
|
|
2124
|
+
|
|
2125
|
+
if (hasPoints) continue;
|
|
2126
|
+
else if (isPanel) panelMatches.push({name, area, bbox});
|
|
2127
|
+
else elementMatches.push({name, area, bbox});
|
|
2128
|
+
}
|
|
2129
|
+
}
|
|
2130
|
+
|
|
2131
|
+
if (elementMatches.length > 0) {
|
|
2132
|
+
elementMatches.sort((a, b) => a.area - b.area);
|
|
2133
|
+
return elementMatches[0].name;
|
|
2134
|
+
}
|
|
2135
|
+
|
|
2136
|
+
if (panelMatches.length > 0) {
|
|
2137
|
+
panelMatches.sort((a, b) => a.area - b.area);
|
|
2138
|
+
return panelMatches[0].name;
|
|
2139
|
+
}
|
|
2140
|
+
|
|
2141
|
+
return null;
|
|
2142
|
+
}
|
|
2143
|
+
|
|
2144
|
+
// Toggle debug mode for panel grid
|
|
2145
|
+
function togglePanelDebugMode() {
|
|
2146
|
+
panelDebugMode = !panelDebugMode;
|
|
2147
|
+
const btn = document.getElementById('panel-debug-btn');
|
|
2148
|
+
if (btn) {
|
|
2149
|
+
btn.classList.toggle('active', panelDebugMode);
|
|
2150
|
+
btn.textContent = panelDebugMode ? 'Hide Hit Regions' : 'Show Hit Regions';
|
|
2151
|
+
}
|
|
2152
|
+
console.log('Panel debug mode:', panelDebugMode ? 'ON' : 'OFF');
|
|
2153
|
+
|
|
2154
|
+
// Redraw all panel overlays
|
|
2155
|
+
redrawAllPanelOverlays();
|
|
2156
|
+
}
|
|
2157
|
+
|
|
2158
|
+
// Redraw all panel overlays (useful for debug mode toggle)
|
|
2159
|
+
function redrawAllPanelOverlays() {
|
|
2160
|
+
document.querySelectorAll('.panel-canvas-item').forEach((item) => {
|
|
2161
|
+
const panelName = item.dataset.panelName;
|
|
2162
|
+
const panelCache = panelBboxesCache[panelName];
|
|
2163
|
+
if (!panelCache) {
|
|
2164
|
+
console.log('No cache for panel:', panelName);
|
|
2165
|
+
return;
|
|
2166
|
+
}
|
|
2167
|
+
|
|
2168
|
+
const container = item.querySelector('.panel-card-container');
|
|
2169
|
+
if (!container) return;
|
|
2170
|
+
|
|
2171
|
+
const img = container.querySelector('img');
|
|
2172
|
+
const overlay = container.querySelector('svg');
|
|
2173
|
+
if (!img || !overlay) return;
|
|
2174
|
+
|
|
2175
|
+
const rect = img.getBoundingClientRect();
|
|
2176
|
+
console.log(`Redraw panel ${panelName}: rect=${rect.width}x${rect.height}, bboxes=${Object.keys(panelCache.bboxes).length}`);
|
|
2177
|
+
if (rect.width > 0 && rect.height > 0) {
|
|
2178
|
+
updatePanelOverlay(overlay, panelCache.bboxes, panelCache.imgSize, rect.width, rect.height, null, null);
|
|
2179
|
+
}
|
|
2180
|
+
});
|
|
2181
|
+
}
|
|
2182
|
+
|
|
2183
|
+
// Update SVG overlay for a panel card
|
|
2184
|
+
function updatePanelOverlay(overlay, bboxes, imgSizePanel, displayWidth, displayHeight, hovered, selected) {
|
|
2185
|
+
if (!overlay || displayWidth === 0 || displayHeight === 0 || !imgSizePanel || imgSizePanel.width === 0) {
|
|
2186
|
+
if (overlay) overlay.innerHTML = '';
|
|
2187
|
+
return;
|
|
2188
|
+
}
|
|
2189
|
+
|
|
2190
|
+
overlay.setAttribute('width', displayWidth);
|
|
2191
|
+
overlay.setAttribute('height', displayHeight);
|
|
2192
|
+
|
|
2193
|
+
const scaleX = displayWidth / imgSizePanel.width;
|
|
2194
|
+
const scaleY = displayHeight / imgSizePanel.height;
|
|
2195
|
+
|
|
2196
|
+
let svg = '';
|
|
2197
|
+
|
|
2198
|
+
// Debug mode: draw all bboxes
|
|
2199
|
+
if (panelDebugMode && bboxes) {
|
|
2200
|
+
svg += drawPanelDebugBboxes(bboxes, scaleX, scaleY);
|
|
2201
|
+
}
|
|
2202
|
+
|
|
2203
|
+
function drawPanelElement(elementName, type) {
|
|
2204
|
+
const bbox = bboxes[elementName];
|
|
2205
|
+
if (!bbox) return '';
|
|
2206
|
+
|
|
2207
|
+
const elementType = bbox.element_type || '';
|
|
2208
|
+
const hasPoints = bbox.points && bbox.points.length > 0;
|
|
2209
|
+
|
|
2210
|
+
// Lines - draw as path
|
|
2211
|
+
if ((elementType === 'line' || elementName.includes('trace_')) && hasPoints) {
|
|
2212
|
+
if (bbox.points.length < 2) return '';
|
|
2213
|
+
const points = bbox.points.filter(pt => Array.isArray(pt) && pt.length >= 2);
|
|
2214
|
+
if (points.length < 2) return '';
|
|
2215
|
+
|
|
2216
|
+
let pathD = `M ${points[0][0] * scaleX} ${points[0][1] * scaleY}`;
|
|
2217
|
+
for (let i = 1; i < points.length; i++) {
|
|
2218
|
+
pathD += ` L ${points[i][0] * scaleX} ${points[i][1] * scaleY}`;
|
|
2219
|
+
}
|
|
2220
|
+
|
|
2221
|
+
const className = type === 'hover' ? 'hover-path' : 'selected-path';
|
|
2222
|
+
return `<path class="${className}" d="${pathD}"/>`;
|
|
2223
|
+
}
|
|
2224
|
+
// Scatter - draw as circles
|
|
2225
|
+
else if (elementType === 'scatter' && hasPoints) {
|
|
2226
|
+
const className = type === 'hover' ? 'hover-scatter' : 'selected-scatter';
|
|
2227
|
+
let result = '';
|
|
2228
|
+
for (const pt of bbox.points) {
|
|
2229
|
+
if (!Array.isArray(pt) || pt.length < 2) continue;
|
|
2230
|
+
result += `<circle class="${className}" cx="${pt[0] * scaleX}" cy="${pt[1] * scaleY}" r="3"/>`;
|
|
2231
|
+
}
|
|
2232
|
+
return result;
|
|
2233
|
+
}
|
|
2234
|
+
// Default - draw bbox rectangle
|
|
2235
|
+
else {
|
|
2236
|
+
const rectClass = type === 'hover' ? 'hover-rect' : 'selected-rect';
|
|
2237
|
+
const x = bbox.x0 * scaleX - 1;
|
|
2238
|
+
const y = bbox.y0 * scaleY - 1;
|
|
2239
|
+
const w = (bbox.x1 - bbox.x0) * scaleX + 2;
|
|
2240
|
+
const h = (bbox.y1 - bbox.y0) * scaleY + 2;
|
|
2241
|
+
return `<rect class="${rectClass}" x="${x}" y="${y}" width="${w}" height="${h}" rx="2"/>`;
|
|
2242
|
+
}
|
|
2243
|
+
}
|
|
2244
|
+
|
|
2245
|
+
if (hovered && hovered !== selected) {
|
|
2246
|
+
svg += drawPanelElement(hovered, 'hover');
|
|
2247
|
+
}
|
|
2248
|
+
|
|
2249
|
+
if (selected) {
|
|
2250
|
+
svg += drawPanelElement(selected, 'selected');
|
|
2251
|
+
}
|
|
2252
|
+
|
|
2253
|
+
overlay.innerHTML = svg;
|
|
2254
|
+
}
|
|
2255
|
+
|
|
2256
|
+
// Draw all bboxes for a panel in debug mode
|
|
2257
|
+
function drawPanelDebugBboxes(bboxes, scaleX, scaleY) {
|
|
2258
|
+
let svg = '';
|
|
2259
|
+
let count = 0;
|
|
2260
|
+
|
|
2261
|
+
for (const [name, bbox] of Object.entries(bboxes)) {
|
|
2262
|
+
if (name === '_meta') continue;
|
|
2263
|
+
if (bbox.x0 === undefined || bbox.y0 === undefined) continue;
|
|
2264
|
+
|
|
2265
|
+
count++;
|
|
2266
|
+
const hasPoints = bbox.points && bbox.points.length > 0;
|
|
2267
|
+
const elementType = bbox.element_type || '';
|
|
2268
|
+
|
|
2269
|
+
// Choose color based on element type
|
|
2270
|
+
let rectClass = 'debug-rect';
|
|
2271
|
+
if (name.includes('trace_') || elementType === 'line') {
|
|
2272
|
+
rectClass = 'debug-rect-trace';
|
|
2273
|
+
} else if (name.includes('legend')) {
|
|
2274
|
+
rectClass = 'debug-rect-legend';
|
|
2275
|
+
} else if (elementType === 'scatter') {
|
|
2276
|
+
rectClass = 'debug-rect-trace';
|
|
2277
|
+
}
|
|
2278
|
+
|
|
2279
|
+
// Draw bbox rectangle
|
|
2280
|
+
const x = bbox.x0 * scaleX;
|
|
2281
|
+
const y = bbox.y0 * scaleY;
|
|
2282
|
+
const w = (bbox.x1 - bbox.x0) * scaleX;
|
|
2283
|
+
const h = (bbox.y1 - bbox.y0) * scaleY;
|
|
2284
|
+
|
|
2285
|
+
svg += `<rect class="${rectClass}" x="${x}" y="${y}" width="${w}" height="${h}"/>`;
|
|
2286
|
+
|
|
2287
|
+
// Draw short label (truncated for small panels)
|
|
2288
|
+
const shortName = name.length > 10 ? name.substring(0, 8) + '..' : name;
|
|
2289
|
+
svg += `<text class="debug-label" x="${x + 1}" y="${y + 8}" style="font-size: 6px;">${shortName}</text>`;
|
|
2290
|
+
|
|
2291
|
+
// Draw path points if available
|
|
2292
|
+
if (hasPoints && bbox.points.length > 1) {
|
|
2293
|
+
let pathD = `M ${bbox.points[0][0] * scaleX} ${bbox.points[0][1] * scaleY}`;
|
|
2294
|
+
for (let i = 1; i < bbox.points.length; i++) {
|
|
2295
|
+
const pt = bbox.points[i];
|
|
2296
|
+
if (pt && pt.length >= 2) {
|
|
2297
|
+
pathD += ` L ${pt[0] * scaleX} ${pt[1] * scaleY}`;
|
|
2298
|
+
}
|
|
2299
|
+
}
|
|
2300
|
+
svg += `<path class="debug-path" d="${pathD}"/>`;
|
|
2301
|
+
}
|
|
2302
|
+
}
|
|
2303
|
+
|
|
2304
|
+
console.log(`Panel debug: ${count} elements with bboxes`);
|
|
2305
|
+
return svg;
|
|
2306
|
+
}
|
|
2307
|
+
|
|
2308
|
+
// Load panel data and switch to it for editing with a pre-selected element
|
|
2309
|
+
async function loadPanelForEditing(panelIdx, panelName, elementToSelect) {
|
|
2310
|
+
showLoading();
|
|
2311
|
+
setStatus(`Loading Panel ${panelName}...`, false);
|
|
2312
|
+
|
|
2313
|
+
try {
|
|
2314
|
+
const resp = await fetch(`/switch_panel/${panelIdx}`);
|
|
2315
|
+
const data = await resp.json();
|
|
2316
|
+
|
|
2317
|
+
if (data.error) {
|
|
2318
|
+
console.error('switch_panel error:', data.error);
|
|
2319
|
+
if (data.traceback) {
|
|
2320
|
+
console.error('Traceback:', data.traceback);
|
|
2321
|
+
}
|
|
2322
|
+
setStatus('Error: ' + data.error, true);
|
|
2323
|
+
hideLoading();
|
|
2324
|
+
return;
|
|
2325
|
+
}
|
|
2326
|
+
|
|
2327
|
+
// Update panel state
|
|
2328
|
+
currentPanelIndex = panelIdx;
|
|
2329
|
+
panelData.current_index = panelIdx;
|
|
2330
|
+
updatePanelIndicator();
|
|
2331
|
+
|
|
2332
|
+
// Update preview image
|
|
2333
|
+
const img = document.getElementById('preview-img');
|
|
2334
|
+
img.src = 'data:image/png;base64,' + data.image;
|
|
2335
|
+
|
|
2336
|
+
// Update bboxes and overlays
|
|
2337
|
+
elementBboxes = data.bboxes || {};
|
|
2338
|
+
if (data.img_size) {
|
|
2339
|
+
imgSize = data.img_size;
|
|
2340
|
+
}
|
|
2341
|
+
|
|
2342
|
+
// Update overrides
|
|
2343
|
+
if (data.overrides) {
|
|
2344
|
+
overrides = data.overrides;
|
|
2345
|
+
traces = overrides.traces || [];
|
|
2346
|
+
updateControlsFromOverrides();
|
|
2347
|
+
}
|
|
2348
|
+
|
|
2349
|
+
// Select the element that was clicked
|
|
2350
|
+
selectedElement = elementToSelect;
|
|
2351
|
+
updateOverlay();
|
|
2352
|
+
|
|
2353
|
+
// Scroll to section and show properties
|
|
2354
|
+
scrollToSection(selectedElement);
|
|
2355
|
+
|
|
2356
|
+
// Show single-panel preview when element selected
|
|
2357
|
+
const previewWrapper = document.querySelector('.preview-wrapper');
|
|
2358
|
+
if (previewWrapper) {
|
|
2359
|
+
previewWrapper.style.display = 'block';
|
|
2360
|
+
}
|
|
2361
|
+
|
|
2362
|
+
// Update panel path display in right panel header
|
|
2363
|
+
const panelPathEl = document.getElementById('panel-path-display');
|
|
2364
|
+
if (panelPathEl) {
|
|
2365
|
+
panelPathEl.textContent = `Panel: ${panelName}.pltz.d/spec.json`;
|
|
2366
|
+
}
|
|
2367
|
+
|
|
2368
|
+
setStatus(`Selected: ${elementToSelect} in Panel ${panelName}`, false);
|
|
2369
|
+
} catch (e) {
|
|
2370
|
+
setStatus('Error: ' + e.message, true);
|
|
2371
|
+
console.error('Panel load error:', e);
|
|
2372
|
+
} finally {
|
|
2373
|
+
hideLoading();
|
|
2374
|
+
}
|
|
2375
|
+
}
|
|
2376
|
+
|
|
2377
|
+
function togglePanelGrid() {
|
|
2378
|
+
showingPanelGrid = !showingPanelGrid;
|
|
2379
|
+
const gridSection = document.getElementById('panel-grid-section');
|
|
2380
|
+
const showBtn = document.getElementById('show-grid-btn');
|
|
2381
|
+
|
|
2382
|
+
if (showingPanelGrid) {
|
|
2383
|
+
gridSection.style.display = 'block';
|
|
2384
|
+
showBtn.textContent = 'Hide All';
|
|
2385
|
+
} else {
|
|
2386
|
+
gridSection.style.display = 'none';
|
|
2387
|
+
showBtn.textContent = 'Show All';
|
|
2388
|
+
}
|
|
2389
|
+
}
|
|
2390
|
+
|
|
2391
|
+
async function selectPanel(idx) {
|
|
2392
|
+
if (!panelData || idx < 0 || idx >= panelData.panels.length) return;
|
|
2393
|
+
|
|
2394
|
+
// Show loading state
|
|
2395
|
+
showLoading();
|
|
2396
|
+
setStatus('Switching panel...', false);
|
|
2397
|
+
|
|
2398
|
+
try {
|
|
2399
|
+
const resp = await fetch(`/switch_panel/${idx}`);
|
|
2400
|
+
const data = await resp.json();
|
|
2401
|
+
|
|
2402
|
+
if (data.error) {
|
|
2403
|
+
setStatus('Error: ' + data.error, true);
|
|
2404
|
+
hideLoading();
|
|
2405
|
+
return;
|
|
2406
|
+
}
|
|
2407
|
+
|
|
2408
|
+
// Update panel state
|
|
2409
|
+
currentPanelIndex = idx;
|
|
2410
|
+
panelData.current_index = idx;
|
|
2411
|
+
updatePanelIndicator();
|
|
2412
|
+
|
|
2413
|
+
// Update active state in grid
|
|
2414
|
+
document.querySelectorAll('.panel-card').forEach((card, i) => {
|
|
2415
|
+
card.classList.toggle('active', i === idx);
|
|
2416
|
+
});
|
|
2417
|
+
|
|
2418
|
+
// Update preview image
|
|
2419
|
+
const img = document.getElementById('preview-img');
|
|
2420
|
+
img.src = 'data:image/png;base64,' + data.image;
|
|
2421
|
+
|
|
2422
|
+
// Update bboxes and overlays
|
|
2423
|
+
elementBboxes = data.bboxes || {};
|
|
2424
|
+
if (data.img_size) {
|
|
2425
|
+
imgSize = data.img_size;
|
|
2426
|
+
}
|
|
2427
|
+
|
|
2428
|
+
// Update overrides
|
|
2429
|
+
if (data.overrides) {
|
|
2430
|
+
overrides = data.overrides;
|
|
2431
|
+
traces = overrides.traces || [];
|
|
2432
|
+
updateControlsFromOverrides();
|
|
2433
|
+
}
|
|
2434
|
+
|
|
2435
|
+
updateOverlay();
|
|
2436
|
+
if (debugMode) {
|
|
2437
|
+
drawDebugBboxes();
|
|
2438
|
+
}
|
|
2439
|
+
|
|
2440
|
+
// Update panel path display in right panel header
|
|
2441
|
+
const panelPathEl = document.getElementById('panel-path-display');
|
|
2442
|
+
if (panelPathEl && data.panel_name) {
|
|
2443
|
+
panelPathEl.textContent = `Panel: ${data.panel_name}/spec.json`;
|
|
2444
|
+
}
|
|
2445
|
+
|
|
2446
|
+
setStatus(`Switched to Panel ${data.panel_name.replace('.pltz.d', '')}`, false);
|
|
2447
|
+
} catch (e) {
|
|
2448
|
+
setStatus('Error switching panel: ' + e.message, true);
|
|
2449
|
+
console.error('Panel switch error:', e);
|
|
2450
|
+
} finally {
|
|
2451
|
+
hideLoading();
|
|
2452
|
+
}
|
|
2453
|
+
}
|
|
2454
|
+
|
|
2455
|
+
function prevPanel() {
|
|
2456
|
+
if (panelData && currentPanelIndex > 0) {
|
|
2457
|
+
selectPanel(currentPanelIndex - 1);
|
|
2458
|
+
}
|
|
2459
|
+
}
|
|
2460
|
+
|
|
2461
|
+
function nextPanel() {
|
|
2462
|
+
if (panelData && currentPanelIndex < panelData.panels.length - 1) {
|
|
2463
|
+
selectPanel(currentPanelIndex + 1);
|
|
2464
|
+
}
|
|
2465
|
+
}
|
|
2466
|
+
|
|
2467
|
+
function updatePanelIndicator() {
|
|
2468
|
+
if (!panelData) return;
|
|
2469
|
+
|
|
2470
|
+
const total = panelData.panels.length;
|
|
2471
|
+
const current = currentPanelIndex + 1;
|
|
2472
|
+
const panelName = panelData.panels[currentPanelIndex];
|
|
2473
|
+
|
|
2474
|
+
document.getElementById('panel-indicator').textContent = `${current} / ${total}`;
|
|
2475
|
+
document.getElementById('current-panel-name').textContent = `Panel ${panelName.replace('.pltz.d', '')}`;
|
|
2476
|
+
|
|
2477
|
+
// Update prev/next button states
|
|
2478
|
+
document.getElementById('prev-panel-btn').disabled = currentPanelIndex === 0;
|
|
2479
|
+
document.getElementById('next-panel-btn').disabled = currentPanelIndex === total - 1;
|
|
2480
|
+
}
|
|
2481
|
+
|
|
2482
|
+
// =============================================================================
|
|
2483
|
+
// Canvas Mode (Draggable Panel Layout)
|
|
2484
|
+
// =============================================================================
|
|
2485
|
+
let canvasMode = 'grid'; // 'grid' or 'canvas'
|
|
2486
|
+
let panelPositions = {}; // Store panel positions {name: {x, y, width, height}}
|
|
2487
|
+
let draggedPanel = null;
|
|
2488
|
+
let dragOffset = {x: 0, y: 0};
|
|
2489
|
+
|
|
2490
|
+
function setCanvasMode(mode) {
|
|
2491
|
+
canvasMode = mode;
|
|
2492
|
+
const gridEl = document.getElementById('panel-grid');
|
|
2493
|
+
const canvasEl = document.getElementById('panel-canvas');
|
|
2494
|
+
const gridBtn = document.getElementById('view-grid-btn');
|
|
2495
|
+
const canvasBtn = document.getElementById('view-canvas-btn');
|
|
2496
|
+
|
|
2497
|
+
if (mode === 'grid') {
|
|
2498
|
+
gridEl.style.display = 'grid';
|
|
2499
|
+
canvasEl.style.display = 'none';
|
|
2500
|
+
gridBtn.classList.remove('btn-secondary');
|
|
2501
|
+
gridBtn.classList.add('btn-primary');
|
|
2502
|
+
canvasBtn.classList.add('btn-secondary');
|
|
2503
|
+
canvasBtn.classList.remove('btn-primary');
|
|
2504
|
+
} else {
|
|
2505
|
+
gridEl.style.display = 'none';
|
|
2506
|
+
canvasEl.style.display = 'block';
|
|
2507
|
+
canvasBtn.classList.remove('btn-secondary');
|
|
2508
|
+
canvasBtn.classList.add('btn-primary');
|
|
2509
|
+
gridBtn.classList.add('btn-secondary');
|
|
2510
|
+
gridBtn.classList.remove('btn-primary');
|
|
2511
|
+
renderPanelCanvas();
|
|
2512
|
+
}
|
|
2513
|
+
}
|
|
2514
|
+
|
|
2515
|
+
async function renderPanelCanvas() {
|
|
2516
|
+
const canvasEl = document.getElementById('panel-canvas');
|
|
2517
|
+
if (!panelData || !canvasEl) return;
|
|
2518
|
+
|
|
2519
|
+
// Fetch panels if not cached
|
|
2520
|
+
try {
|
|
2521
|
+
const resp = await fetch('/panels');
|
|
2522
|
+
const data = await resp.json();
|
|
2523
|
+
if (data.error) return;
|
|
2524
|
+
|
|
2525
|
+
canvasEl.innerHTML = '';
|
|
2526
|
+
|
|
2527
|
+
// Calculate canvas size based on number of panels
|
|
2528
|
+
const numPanels = data.panels.length;
|
|
2529
|
+
const cols = Math.ceil(Math.sqrt(numPanels));
|
|
2530
|
+
const baseWidth = 200;
|
|
2531
|
+
const baseHeight = 150;
|
|
2532
|
+
const padding = 20;
|
|
2533
|
+
|
|
2534
|
+
data.panels.forEach((panel, idx) => {
|
|
2535
|
+
const name = panel.name;
|
|
2536
|
+
|
|
2537
|
+
// Initialize position if not set
|
|
2538
|
+
if (!panelPositions[name]) {
|
|
2539
|
+
const col = idx % cols;
|
|
2540
|
+
const row = Math.floor(idx / cols);
|
|
2541
|
+
panelPositions[name] = {
|
|
2542
|
+
x: padding + col * (baseWidth + padding),
|
|
2543
|
+
y: padding + row * (baseHeight + padding),
|
|
2544
|
+
width: baseWidth,
|
|
2545
|
+
height: baseHeight,
|
|
2546
|
+
};
|
|
2547
|
+
}
|
|
2548
|
+
|
|
2549
|
+
const pos = panelPositions[name];
|
|
2550
|
+
const item = document.createElement('div');
|
|
2551
|
+
item.className = 'panel-canvas-item' + (idx === currentPanelIndex ? ' active' : '');
|
|
2552
|
+
item.dataset.panelIndex = idx;
|
|
2553
|
+
item.dataset.panelName = name;
|
|
2554
|
+
item.style.left = pos.x + 'px';
|
|
2555
|
+
item.style.top = pos.y + 'px';
|
|
2556
|
+
item.style.width = pos.width + 'px';
|
|
2557
|
+
item.style.height = pos.height + 'px';
|
|
2558
|
+
|
|
2559
|
+
item.innerHTML = `
|
|
2560
|
+
<span class="panel-canvas-label">Panel ${name}</span>
|
|
2561
|
+
${panel.image ? `<img src="data:image/png;base64,${panel.image}" alt="Panel ${name}">` : '<div style="padding: 20px; color: var(--text-muted);">No preview</div>'}
|
|
2562
|
+
<div class="panel-canvas-resize"></div>
|
|
2563
|
+
`;
|
|
2564
|
+
|
|
2565
|
+
// Double-click to edit
|
|
2566
|
+
item.addEventListener('dblclick', () => selectPanel(idx));
|
|
2567
|
+
|
|
2568
|
+
// Drag start
|
|
2569
|
+
item.addEventListener('mousedown', (e) => {
|
|
2570
|
+
if (e.target.classList.contains('panel-canvas-resize')) {
|
|
2571
|
+
startResize(e, item, name);
|
|
2572
|
+
} else {
|
|
2573
|
+
startDrag(e, item, name);
|
|
2574
|
+
}
|
|
2575
|
+
});
|
|
2576
|
+
|
|
2577
|
+
canvasEl.appendChild(item);
|
|
2578
|
+
});
|
|
2579
|
+
|
|
2580
|
+
// Update canvas height to fit all panels
|
|
2581
|
+
const maxY = Math.max(...Object.values(panelPositions).map(p => p.y + p.height)) + padding;
|
|
2582
|
+
canvasEl.style.minHeight = Math.max(400, maxY) + 'px';
|
|
2583
|
+
|
|
2584
|
+
} catch (e) {
|
|
2585
|
+
console.error('Error rendering canvas:', e);
|
|
2586
|
+
}
|
|
2587
|
+
}
|
|
2588
|
+
|
|
2589
|
+
function startDrag(e, item, name) {
|
|
2590
|
+
e.preventDefault();
|
|
2591
|
+
draggedPanel = {item, name};
|
|
2592
|
+
dragOffset.x = e.clientX - item.offsetLeft;
|
|
2593
|
+
dragOffset.y = e.clientY - item.offsetTop;
|
|
2594
|
+
item.classList.add('dragging');
|
|
2595
|
+
|
|
2596
|
+
document.addEventListener('mousemove', onDrag);
|
|
2597
|
+
document.addEventListener('mouseup', stopDrag);
|
|
2598
|
+
}
|
|
2599
|
+
|
|
2600
|
+
function onDrag(e) {
|
|
2601
|
+
if (!draggedPanel) return;
|
|
2602
|
+
const canvasEl = document.getElementById('panel-canvas');
|
|
2603
|
+
const rect = canvasEl.getBoundingClientRect();
|
|
2604
|
+
|
|
2605
|
+
let newX = e.clientX - dragOffset.x;
|
|
2606
|
+
let newY = e.clientY - dragOffset.y;
|
|
2607
|
+
|
|
2608
|
+
// Constrain to canvas bounds
|
|
2609
|
+
newX = Math.max(0, Math.min(newX, canvasEl.offsetWidth - draggedPanel.item.offsetWidth));
|
|
2610
|
+
newY = Math.max(0, newY);
|
|
2611
|
+
|
|
2612
|
+
draggedPanel.item.style.left = newX + 'px';
|
|
2613
|
+
draggedPanel.item.style.top = newY + 'px';
|
|
2614
|
+
|
|
2615
|
+
panelPositions[draggedPanel.name].x = newX;
|
|
2616
|
+
panelPositions[draggedPanel.name].y = newY;
|
|
2617
|
+
}
|
|
2618
|
+
|
|
2619
|
+
function stopDrag() {
|
|
2620
|
+
if (draggedPanel) {
|
|
2621
|
+
draggedPanel.item.classList.remove('dragging');
|
|
2622
|
+
draggedPanel = null;
|
|
2623
|
+
}
|
|
2624
|
+
document.removeEventListener('mousemove', onDrag);
|
|
2625
|
+
document.removeEventListener('mouseup', stopDrag);
|
|
2626
|
+
}
|
|
2627
|
+
|
|
2628
|
+
let resizingPanel = null;
|
|
2629
|
+
|
|
2630
|
+
function startResize(e, item, name) {
|
|
2631
|
+
e.preventDefault();
|
|
2632
|
+
e.stopPropagation();
|
|
2633
|
+
resizingPanel = {item, name, startX: e.clientX, startY: e.clientY, startW: item.offsetWidth, startH: item.offsetHeight};
|
|
2634
|
+
|
|
2635
|
+
document.addEventListener('mousemove', onResize);
|
|
2636
|
+
document.addEventListener('mouseup', stopResize);
|
|
2637
|
+
}
|
|
2638
|
+
|
|
2639
|
+
function onResize(e) {
|
|
2640
|
+
if (!resizingPanel) return;
|
|
2641
|
+
const newW = Math.max(100, resizingPanel.startW + (e.clientX - resizingPanel.startX));
|
|
2642
|
+
const newH = Math.max(80, resizingPanel.startH + (e.clientY - resizingPanel.startY));
|
|
2643
|
+
|
|
2644
|
+
resizingPanel.item.style.width = newW + 'px';
|
|
2645
|
+
resizingPanel.item.style.height = newH + 'px';
|
|
2646
|
+
|
|
2647
|
+
panelPositions[resizingPanel.name].width = newW;
|
|
2648
|
+
panelPositions[resizingPanel.name].height = newH;
|
|
2649
|
+
}
|
|
2650
|
+
|
|
2651
|
+
function stopResize() {
|
|
2652
|
+
resizingPanel = null;
|
|
2653
|
+
document.removeEventListener('mousemove', onResize);
|
|
2654
|
+
document.removeEventListener('mouseup', stopResize);
|
|
2655
|
+
}
|
|
2656
|
+
|
|
2657
|
+
// Initialize hover system for a specific element (img or svg)
|
|
2658
|
+
function initHoverSystemForElement(el) {
|
|
2659
|
+
if (!el) return;
|
|
2660
|
+
|
|
2661
|
+
el.addEventListener('mousemove', (e) => {
|
|
2662
|
+
if (imgSize.width === 0 || imgSize.height === 0) return;
|
|
2663
|
+
|
|
2664
|
+
const rect = el.getBoundingClientRect();
|
|
2665
|
+
const x = e.clientX - rect.left;
|
|
2666
|
+
const y = e.clientY - rect.top;
|
|
2667
|
+
|
|
2668
|
+
const scaleX = imgSize.width / rect.width;
|
|
2669
|
+
const scaleY = imgSize.height / rect.height;
|
|
2670
|
+
const imgX = x * scaleX;
|
|
2671
|
+
const imgY = y * scaleY;
|
|
2672
|
+
|
|
2673
|
+
const element = findElementAt(imgX, imgY);
|
|
2674
|
+
if (element !== hoveredElement) {
|
|
2675
|
+
hoveredElement = element;
|
|
2676
|
+
updateOverlay();
|
|
2677
|
+
}
|
|
2678
|
+
});
|
|
2679
|
+
|
|
2680
|
+
el.addEventListener('mouseleave', () => {
|
|
2681
|
+
hoveredElement = null;
|
|
2682
|
+
updateOverlay();
|
|
2683
|
+
});
|
|
2684
|
+
|
|
2685
|
+
el.addEventListener('click', (e) => {
|
|
2686
|
+
if (hoveredElement) {
|
|
2687
|
+
selectedElement = hoveredElement;
|
|
2688
|
+
updateOverlay();
|
|
2689
|
+
scrollToSection(selectedElement);
|
|
2690
|
+
}
|
|
2691
|
+
});
|
|
2692
|
+
}
|
|
2693
|
+
|
|
2694
|
+
// Traces list management
|
|
2695
|
+
function updateTracesList() {
|
|
2696
|
+
const list = document.getElementById('traces-list');
|
|
2697
|
+
if (!traces || traces.length === 0) {
|
|
2698
|
+
list.innerHTML = '<div style="padding: 10px; color: var(--text-muted); font-size: 0.85em;">No traces found in metadata</div>';
|
|
2699
|
+
return;
|
|
2700
|
+
}
|
|
2701
|
+
|
|
2702
|
+
list.innerHTML = traces.map((t, i) => `
|
|
2703
|
+
<div class="trace-item">
|
|
2704
|
+
<input type="color" class="trace-color" value="${t.color || '#1f77b4'}"
|
|
2705
|
+
onchange="updateTraceColor(${i}, this.value)">
|
|
2706
|
+
<span class="trace-label">${t.label || t.id || 'Trace ' + (i+1)}</span>
|
|
2707
|
+
<div class="trace-style">
|
|
2708
|
+
<select onchange="updateTraceStyle(${i}, this.value)">
|
|
2709
|
+
<option value="-" ${t.linestyle === '-' ? 'selected' : ''}>Solid</option>
|
|
2710
|
+
<option value="--" ${t.linestyle === '--' ? 'selected' : ''}>Dashed</option>
|
|
2711
|
+
<option value=":" ${t.linestyle === ':' ? 'selected' : ''}>Dotted</option>
|
|
2712
|
+
<option value="-." ${t.linestyle === '-.' ? 'selected' : ''}>Dash-dot</option>
|
|
2713
|
+
</select>
|
|
2714
|
+
</div>
|
|
2715
|
+
</div>
|
|
2716
|
+
`).join('');
|
|
2717
|
+
}
|
|
2718
|
+
|
|
2719
|
+
function updateTraceColor(idx, color) {
|
|
2720
|
+
if (traces[idx]) {
|
|
2721
|
+
traces[idx].color = color;
|
|
2722
|
+
scheduleUpdate();
|
|
2723
|
+
}
|
|
2724
|
+
}
|
|
2725
|
+
|
|
2726
|
+
function updateTraceStyle(idx, style) {
|
|
2727
|
+
if (traces[idx]) {
|
|
2728
|
+
traces[idx].linestyle = style;
|
|
2729
|
+
scheduleUpdate();
|
|
2730
|
+
}
|
|
2731
|
+
}
|
|
2732
|
+
|
|
2733
|
+
function collectOverrides() {
|
|
2734
|
+
const o = {};
|
|
2735
|
+
|
|
2736
|
+
// Labels - Title
|
|
2737
|
+
const title = document.getElementById('title').value;
|
|
2738
|
+
if (title) o.title = title;
|
|
2739
|
+
o.show_title = document.getElementById('show_title').checked;
|
|
2740
|
+
o.title_fontsize = parseInt(document.getElementById('title_fontsize').value) || 8;
|
|
2741
|
+
// Labels - Caption
|
|
2742
|
+
const caption = document.getElementById('caption').value;
|
|
2743
|
+
if (caption) o.caption = caption;
|
|
2744
|
+
o.show_caption = document.getElementById('show_caption').checked;
|
|
2745
|
+
o.caption_fontsize = parseInt(document.getElementById('caption_fontsize').value) || 7;
|
|
2746
|
+
// Labels - Axis
|
|
2747
|
+
const xlabel = document.getElementById('xlabel').value;
|
|
2748
|
+
const ylabel = document.getElementById('ylabel').value;
|
|
2749
|
+
if (xlabel) o.xlabel = xlabel;
|
|
2750
|
+
if (ylabel) o.ylabel = ylabel;
|
|
2751
|
+
|
|
2752
|
+
// Axis limits
|
|
2753
|
+
const xmin = document.getElementById('xmin').value;
|
|
2754
|
+
const xmax = document.getElementById('xmax').value;
|
|
2755
|
+
if (xmin !== '' && xmax !== '') o.xlim = [parseFloat(xmin), parseFloat(xmax)];
|
|
2756
|
+
|
|
2757
|
+
const ymin = document.getElementById('ymin').value;
|
|
2758
|
+
const ymax = document.getElementById('ymax').value;
|
|
2759
|
+
if (ymin !== '' && ymax !== '') o.ylim = [parseFloat(ymin), parseFloat(ymax)];
|
|
2760
|
+
|
|
2761
|
+
// Traces
|
|
2762
|
+
o.traces = traces;
|
|
2763
|
+
|
|
2764
|
+
// Legend
|
|
2765
|
+
o.legend_visible = document.getElementById('legend_visible').checked;
|
|
2766
|
+
o.legend_loc = document.getElementById('legend_loc').value;
|
|
2767
|
+
o.legend_frameon = document.getElementById('legend_frameon').checked;
|
|
2768
|
+
o.legend_fontsize = parseInt(document.getElementById('legend_fontsize').value) || 6;
|
|
2769
|
+
o.legend_ncols = parseInt(document.getElementById('legend_ncols').value) || 1;
|
|
2770
|
+
o.legend_x = parseFloat(document.getElementById('legend_x').value) || 0.95;
|
|
2771
|
+
o.legend_y = parseFloat(document.getElementById('legend_y').value) || 0.95;
|
|
2772
|
+
|
|
2773
|
+
// Axis and Ticks - X Axis (Bottom)
|
|
2774
|
+
o.x_n_ticks = parseInt(document.getElementById('x_n_ticks').value) || 4;
|
|
2775
|
+
o.hide_x_ticks = document.getElementById('hide_x_ticks').checked;
|
|
2776
|
+
o.x_tick_fontsize = parseInt(document.getElementById('x_tick_fontsize').value) || 7;
|
|
2777
|
+
o.x_tick_direction = document.getElementById('x_tick_direction').value;
|
|
2778
|
+
o.x_tick_length = parseFloat(document.getElementById('x_tick_length').value) || 0.8;
|
|
2779
|
+
o.x_tick_width = parseFloat(document.getElementById('x_tick_width').value) || 0.2;
|
|
2780
|
+
// X Axis (Top)
|
|
2781
|
+
o.show_x_top = document.getElementById('show_x_top').checked;
|
|
2782
|
+
o.x_top_mirror = document.getElementById('x_top_mirror').checked;
|
|
2783
|
+
// Y Axis (Left)
|
|
2784
|
+
o.y_n_ticks = parseInt(document.getElementById('y_n_ticks').value) || 4;
|
|
2785
|
+
o.hide_y_ticks = document.getElementById('hide_y_ticks').checked;
|
|
2786
|
+
o.y_tick_fontsize = parseInt(document.getElementById('y_tick_fontsize').value) || 7;
|
|
2787
|
+
o.y_tick_direction = document.getElementById('y_tick_direction').value;
|
|
2788
|
+
o.y_tick_length = parseFloat(document.getElementById('y_tick_length').value) || 0.8;
|
|
2789
|
+
o.y_tick_width = parseFloat(document.getElementById('y_tick_width').value) || 0.2;
|
|
2790
|
+
// Y Axis (Right)
|
|
2791
|
+
o.show_y_right = document.getElementById('show_y_right').checked;
|
|
2792
|
+
o.y_right_mirror = document.getElementById('y_right_mirror').checked;
|
|
2793
|
+
// Spines
|
|
2794
|
+
o.hide_bottom_spine = document.getElementById('hide_bottom_spine').checked;
|
|
2795
|
+
o.hide_left_spine = document.getElementById('hide_left_spine').checked;
|
|
2796
|
+
// Z Axis (3D)
|
|
2797
|
+
o.hide_z_ticks = document.getElementById('hide_z_ticks').checked;
|
|
2798
|
+
o.z_n_ticks = parseInt(document.getElementById('z_n_ticks').value) || 4;
|
|
2799
|
+
o.z_tick_fontsize = parseInt(document.getElementById('z_tick_fontsize').value) || 7;
|
|
2800
|
+
o.z_tick_direction = document.getElementById('z_tick_direction').value;
|
|
2801
|
+
|
|
2802
|
+
// Style
|
|
2803
|
+
o.grid = document.getElementById('grid').checked;
|
|
2804
|
+
o.hide_top_spine = document.getElementById('hide_top_spine').checked;
|
|
2805
|
+
o.hide_right_spine = document.getElementById('hide_right_spine').checked;
|
|
2806
|
+
o.axis_width = parseFloat(document.getElementById('axis_width').value) || 0.2;
|
|
2807
|
+
o.axis_fontsize = parseInt(document.getElementById('axis_fontsize').value) || 7;
|
|
2808
|
+
o.facecolor = document.getElementById('facecolor').value;
|
|
2809
|
+
o.transparent = document.getElementById('transparent').value === 'true';
|
|
2810
|
+
|
|
2811
|
+
// Dimensions (always in inches for matplotlib)
|
|
2812
|
+
o.fig_size = getFigSizeInches();
|
|
2813
|
+
o.dpi = parseInt(document.getElementById('dpi').value) || 300;
|
|
2814
|
+
|
|
2815
|
+
// Annotations
|
|
2816
|
+
o.annotations = overrides.annotations || [];
|
|
2817
|
+
|
|
2818
|
+
// Element-specific overrides (per-element styles)
|
|
2819
|
+
if (overrides.element_overrides) {
|
|
2820
|
+
o.element_overrides = overrides.element_overrides;
|
|
2821
|
+
}
|
|
2822
|
+
|
|
2823
|
+
return o;
|
|
2824
|
+
}
|
|
2825
|
+
|
|
2826
|
+
async function updatePreview(forceUpdate = false) {
|
|
2827
|
+
// Skip auto-update if showing original preview (user hasn't explicitly requested update)
|
|
2828
|
+
if (isShowingOriginalPreview && !forceUpdate) {
|
|
2829
|
+
console.log('Skipping auto-update: showing original preview');
|
|
2830
|
+
return;
|
|
2831
|
+
}
|
|
2832
|
+
|
|
2833
|
+
setStatus('Updating...', false);
|
|
2834
|
+
overrides = collectOverrides();
|
|
2835
|
+
|
|
2836
|
+
// Preserve current selection to restore after update
|
|
2837
|
+
const previousSelection = selectedElement;
|
|
2838
|
+
|
|
2839
|
+
try {
|
|
2840
|
+
const darkMode = isDarkMode();
|
|
2841
|
+
const resp = await fetch('/update', {
|
|
2842
|
+
method: 'POST',
|
|
2843
|
+
headers: {'Content-Type': 'application/json'},
|
|
2844
|
+
body: JSON.stringify({overrides, dark_mode: darkMode})
|
|
2845
|
+
});
|
|
2846
|
+
const data = await resp.json();
|
|
2847
|
+
|
|
2848
|
+
// Remove SVG wrapper if exists, show img element for re-rendered preview
|
|
2849
|
+
const existingSvgWrapper = document.getElementById('preview-svg-wrapper');
|
|
2850
|
+
if (existingSvgWrapper) {
|
|
2851
|
+
existingSvgWrapper.remove();
|
|
2852
|
+
}
|
|
2853
|
+
const imgEl = document.getElementById('preview-img');
|
|
2854
|
+
imgEl.style.display = 'block';
|
|
2855
|
+
imgEl.src = 'data:image/png;base64,' + data.image;
|
|
2856
|
+
|
|
2857
|
+
// Mark that we're no longer showing original preview
|
|
2858
|
+
isShowingOriginalPreview = false;
|
|
2859
|
+
|
|
2860
|
+
if (data.bboxes) {
|
|
2861
|
+
elementBboxes = data.bboxes;
|
|
2862
|
+
// Store schema v0.3 metadata if available
|
|
2863
|
+
if (data.bboxes._meta) {
|
|
2864
|
+
schemaMeta = data.bboxes._meta;
|
|
2865
|
+
console.log('Schema v0.3 geometry available:', schemaMeta.schema_version);
|
|
2866
|
+
}
|
|
2867
|
+
}
|
|
2868
|
+
if (data.img_size) {
|
|
2869
|
+
imgSize = data.img_size;
|
|
2870
|
+
}
|
|
2871
|
+
|
|
2872
|
+
// Restore selection if the element still exists in the new bboxes
|
|
2873
|
+
if (previousSelection && elementBboxes[previousSelection]) {
|
|
2874
|
+
selectedElement = previousSelection;
|
|
2875
|
+
} else {
|
|
2876
|
+
selectedElement = null;
|
|
2877
|
+
}
|
|
2878
|
+
hoveredElement = null;
|
|
2879
|
+
updateOverlay();
|
|
2880
|
+
|
|
2881
|
+
setStatus('Preview updated', false);
|
|
2882
|
+
} catch (e) {
|
|
2883
|
+
setStatus('Error: ' + e.message, true);
|
|
2884
|
+
}
|
|
2885
|
+
}
|
|
2886
|
+
|
|
2887
|
+
// Restore original preview (SVG/PNG from bundle)
|
|
2888
|
+
async function restoreOriginalPreview() {
|
|
2889
|
+
if (!originalBboxes || !originalImgSize) {
|
|
2890
|
+
console.log('No original preview to restore');
|
|
2891
|
+
return;
|
|
2892
|
+
}
|
|
2893
|
+
|
|
2894
|
+
setStatus('Restoring original preview...', false);
|
|
2895
|
+
await loadInitialPreview();
|
|
2896
|
+
}
|
|
2897
|
+
|
|
2898
|
+
async function saveManual() {
|
|
2899
|
+
setStatus('Saving...', false);
|
|
2900
|
+
try {
|
|
2901
|
+
const resp = await fetch('/save', {
|
|
2902
|
+
method: 'POST',
|
|
2903
|
+
headers: {'Content-Type': 'application/json'}
|
|
2904
|
+
});
|
|
2905
|
+
const data = await resp.json();
|
|
2906
|
+
if (data.status === 'saved') {
|
|
2907
|
+
setStatus('Saved: ' + data.path.split('/').pop(), false);
|
|
2908
|
+
} else {
|
|
2909
|
+
setStatus('Error: ' + data.message, true);
|
|
2910
|
+
}
|
|
2911
|
+
} catch (e) {
|
|
2912
|
+
setStatus('Error: ' + e.message, true);
|
|
2913
|
+
}
|
|
2914
|
+
}
|
|
2915
|
+
|
|
2916
|
+
function resetOverrides() {
|
|
2917
|
+
if (confirm('Reset all changes to original values?')) {
|
|
2918
|
+
location.reload();
|
|
2919
|
+
}
|
|
2920
|
+
}
|
|
2921
|
+
|
|
2922
|
+
function addAnnotation() {
|
|
2923
|
+
const text = document.getElementById('annot-text').value;
|
|
2924
|
+
if (!text) return;
|
|
2925
|
+
const x = parseFloat(document.getElementById('annot-x').value) || 0.5;
|
|
2926
|
+
const y = parseFloat(document.getElementById('annot-y').value) || 0.5;
|
|
2927
|
+
const size = parseInt(document.getElementById('annot-size').value) || 8;
|
|
2928
|
+
if (!overrides.annotations) overrides.annotations = [];
|
|
2929
|
+
overrides.annotations.push({type: 'text', text, x, y, fontsize: size});
|
|
2930
|
+
document.getElementById('annot-text').value = '';
|
|
2931
|
+
updateAnnotationsList();
|
|
2932
|
+
updatePreview();
|
|
2933
|
+
}
|
|
2934
|
+
|
|
2935
|
+
function removeAnnotation(idx) {
|
|
2936
|
+
overrides.annotations.splice(idx, 1);
|
|
2937
|
+
updateAnnotationsList();
|
|
2938
|
+
updatePreview();
|
|
2939
|
+
}
|
|
2940
|
+
|
|
2941
|
+
function updateAnnotationsList() {
|
|
2942
|
+
const list = document.getElementById('annotations-list');
|
|
2943
|
+
const annotations = overrides.annotations || [];
|
|
2944
|
+
if (annotations.length === 0) {
|
|
2945
|
+
list.innerHTML = '';
|
|
2946
|
+
return;
|
|
2947
|
+
}
|
|
2948
|
+
list.innerHTML = annotations.map((a, i) =>
|
|
2949
|
+
`<div class="annotation-item">
|
|
2950
|
+
<span>${a.text.substring(0, 25)}${a.text.length > 25 ? '...' : ''} (${a.x.toFixed(2)}, ${a.y.toFixed(2)})</span>
|
|
2951
|
+
<button onclick="removeAnnotation(${i})">Remove</button>
|
|
2952
|
+
</div>`
|
|
2953
|
+
).join('');
|
|
2954
|
+
}
|
|
2955
|
+
|
|
2956
|
+
// =============================================================================
|
|
2957
|
+
// Statistics Display
|
|
2958
|
+
// =============================================================================
|
|
2959
|
+
async function refreshStats() {
|
|
2960
|
+
const container = document.getElementById('stats-container');
|
|
2961
|
+
container.innerHTML = '<div class="stats-loading">Loading statistics...</div>';
|
|
2962
|
+
|
|
2963
|
+
try {
|
|
2964
|
+
const resp = await fetch('/stats');
|
|
2965
|
+
const data = await resp.json();
|
|
2966
|
+
|
|
2967
|
+
if (!data.has_stats) {
|
|
2968
|
+
container.innerHTML = '<div class="stats-empty">No statistical tests in this figure</div>';
|
|
2969
|
+
return;
|
|
2970
|
+
}
|
|
2971
|
+
|
|
2972
|
+
let html = '';
|
|
2973
|
+
|
|
2974
|
+
// Show summary if available
|
|
2975
|
+
if (data.stats_summary) {
|
|
2976
|
+
const summary = data.stats_summary;
|
|
2977
|
+
html += `
|
|
2978
|
+
<div class="stats-summary-header">
|
|
2979
|
+
${summary.test_type.replace('_', '-')}
|
|
2980
|
+
<span class="stats-correction-badge">${summary.correction_method}</span>
|
|
2981
|
+
</div>
|
|
2982
|
+
<div class="stats-summary-body">
|
|
2983
|
+
<div class="stats-row">
|
|
2984
|
+
<span class="stats-label">Comparisons:</span>
|
|
2985
|
+
<span class="stats-value">${summary.n_comparisons}</span>
|
|
2986
|
+
</div>
|
|
2987
|
+
<div class="stats-row">
|
|
2988
|
+
<span class="stats-label">α (original):</span>
|
|
2989
|
+
<span class="stats-value">${summary.alpha}</span>
|
|
2990
|
+
</div>
|
|
2991
|
+
<div class="stats-row">
|
|
2992
|
+
<span class="stats-label">α (corrected):</span>
|
|
2993
|
+
<span class="stats-value">${summary.corrected_alpha.toFixed(4)}</span>
|
|
2994
|
+
</div>
|
|
2995
|
+
</div>
|
|
2996
|
+
`;
|
|
2997
|
+
}
|
|
2998
|
+
|
|
2999
|
+
// Show individual test results
|
|
3000
|
+
data.stats.forEach((stat, idx) => {
|
|
3001
|
+
const sigClass = getSigClass(stat.stars);
|
|
3002
|
+
const samples = stat.samples || {};
|
|
3003
|
+
const correction = stat.correction || {};
|
|
3004
|
+
|
|
3005
|
+
html += `
|
|
3006
|
+
<div class="stats-card">
|
|
3007
|
+
<div class="stats-card-header">
|
|
3008
|
+
<span class="stats-card-title">
|
|
3009
|
+
${samples.group1?.name || 'Group 1'} vs ${samples.group2?.name || 'Group 2'}
|
|
3010
|
+
</span>
|
|
3011
|
+
<span class="stats-significance ${sigClass}">${stat.stars}</span>
|
|
3012
|
+
</div>
|
|
3013
|
+
<div class="stats-row">
|
|
3014
|
+
<span class="stats-label">${stat.statistic?.name || 'Stat'}:</span>
|
|
3015
|
+
<span class="stats-value">${(stat.statistic?.value || 0).toFixed(3)}</span>
|
|
3016
|
+
</div>
|
|
3017
|
+
<div class="stats-row">
|
|
3018
|
+
<span class="stats-label">p (raw):</span>
|
|
3019
|
+
<span class="stats-value">${stat.p_value.toFixed(4)}</span>
|
|
3020
|
+
</div>
|
|
3021
|
+
${correction.corrected_p ? `
|
|
3022
|
+
<div class="stats-row">
|
|
3023
|
+
<span class="stats-label">p (corrected):</span>
|
|
3024
|
+
<span class="stats-value">${correction.corrected_p.toFixed(4)}</span>
|
|
3025
|
+
</div>` : ''}
|
|
3026
|
+
<div class="stats-groups">
|
|
3027
|
+
${samples.group1 ? renderGroupStats(samples.group1) : ''}
|
|
3028
|
+
${samples.group2 ? renderGroupStats(samples.group2) : ''}
|
|
3029
|
+
</div>
|
|
3030
|
+
</div>
|
|
3031
|
+
`;
|
|
3032
|
+
});
|
|
3033
|
+
|
|
3034
|
+
container.innerHTML = html;
|
|
3035
|
+
} catch (e) {
|
|
3036
|
+
container.innerHTML = `<div class="stats-empty">Error loading stats: ${e.message}</div>`;
|
|
3037
|
+
}
|
|
3038
|
+
}
|
|
3039
|
+
|
|
3040
|
+
function getSigClass(stars) {
|
|
3041
|
+
if (stars === '***') return 'sig-high';
|
|
3042
|
+
if (stars === '**') return 'sig-medium';
|
|
3043
|
+
if (stars === '*') return 'sig-low';
|
|
3044
|
+
return 'sig-ns';
|
|
3045
|
+
}
|
|
3046
|
+
|
|
3047
|
+
function renderGroupStats(group) {
|
|
3048
|
+
return `
|
|
3049
|
+
<div class="stats-group">
|
|
3050
|
+
<div class="stats-group-name">${group.name || 'Group'}</div>
|
|
3051
|
+
<div>n = ${group.n}</div>
|
|
3052
|
+
<div>μ = ${group.mean?.toFixed(2) || '-'}</div>
|
|
3053
|
+
<div>σ = ${group.std?.toFixed(2) || '-'}</div>
|
|
3054
|
+
</div>
|
|
3055
|
+
`;
|
|
3056
|
+
}
|
|
3057
|
+
|
|
3058
|
+
function setStatus(msg, isError = false) {
|
|
3059
|
+
const el = document.getElementById('status');
|
|
3060
|
+
const loadingOverlay = document.getElementById('loading-overlay');
|
|
3061
|
+
|
|
3062
|
+
// Show/hide spinner for loading states
|
|
3063
|
+
if (msg === 'Updating...' || msg === 'Loading preview...') {
|
|
3064
|
+
loadingOverlay.style.display = 'flex';
|
|
3065
|
+
el.textContent = ''; // Clear status text during loading
|
|
3066
|
+
} else {
|
|
3067
|
+
loadingOverlay.style.display = 'none';
|
|
3068
|
+
el.textContent = msg;
|
|
3069
|
+
}
|
|
3070
|
+
el.classList.toggle('error', isError);
|
|
3071
|
+
}
|
|
3072
|
+
|
|
3073
|
+
// Debounced auto-update
|
|
3074
|
+
let updateTimer = null;
|
|
3075
|
+
const DEBOUNCE_DELAY = 500;
|
|
3076
|
+
|
|
3077
|
+
function scheduleUpdate() {
|
|
3078
|
+
if (updateTimer) clearTimeout(updateTimer);
|
|
3079
|
+
updateTimer = setTimeout(() => {
|
|
3080
|
+
updatePreview();
|
|
3081
|
+
}, DEBOUNCE_DELAY);
|
|
3082
|
+
}
|
|
3083
|
+
|
|
3084
|
+
// Auto-update on input changes
|
|
3085
|
+
document.querySelectorAll('input[type="text"], input[type="number"], textarea').forEach(el => {
|
|
3086
|
+
el.addEventListener('input', scheduleUpdate);
|
|
3087
|
+
el.addEventListener('keypress', (e) => {
|
|
3088
|
+
if (e.key === 'Enter' && el.tagName !== 'TEXTAREA') {
|
|
3089
|
+
if (updateTimer) clearTimeout(updateTimer);
|
|
3090
|
+
updatePreview();
|
|
3091
|
+
}
|
|
3092
|
+
});
|
|
3093
|
+
});
|
|
3094
|
+
|
|
3095
|
+
document.querySelectorAll('input[type="checkbox"], select').forEach(el => {
|
|
3096
|
+
el.addEventListener('change', () => {
|
|
3097
|
+
if (updateTimer) clearTimeout(updateTimer);
|
|
3098
|
+
updatePreview();
|
|
3099
|
+
});
|
|
3100
|
+
});
|
|
3101
|
+
|
|
3102
|
+
document.querySelectorAll('input[type="color"]').forEach(el => {
|
|
3103
|
+
el.addEventListener('change', () => {
|
|
3104
|
+
if (updateTimer) clearTimeout(updateTimer);
|
|
3105
|
+
updatePreview();
|
|
3106
|
+
});
|
|
3107
|
+
});
|
|
3108
|
+
|
|
3109
|
+
// Ctrl+S keyboard shortcut to save
|
|
3110
|
+
document.addEventListener('keydown', (e) => {
|
|
3111
|
+
if ((e.ctrlKey || e.metaKey) && e.key === 's') {
|
|
3112
|
+
e.preventDefault();
|
|
3113
|
+
saveManual();
|
|
3114
|
+
}
|
|
3115
|
+
});
|
|
3116
|
+
|
|
3117
|
+
// Auto-update interval system
|
|
3118
|
+
let autoUpdateIntervalId = null;
|
|
3119
|
+
|
|
3120
|
+
function setAutoUpdateInterval() {
|
|
3121
|
+
if (autoUpdateIntervalId) {
|
|
3122
|
+
clearInterval(autoUpdateIntervalId);
|
|
3123
|
+
autoUpdateIntervalId = null;
|
|
3124
|
+
}
|
|
3125
|
+
|
|
3126
|
+
const intervalMs = parseInt(document.getElementById('auto_update_interval').value);
|
|
3127
|
+
if (intervalMs > 0) {
|
|
3128
|
+
autoUpdateIntervalId = setInterval(() => {
|
|
3129
|
+
updatePreview();
|
|
3130
|
+
}, intervalMs);
|
|
3131
|
+
}
|
|
3132
|
+
}
|
|
3133
|
+
"""
|
|
3134
|
+
|
|
3135
|
+
|
|
3136
|
+
# EOF
|