scitex 2.10.3__py3-none-any.whl → 2.13.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- scitex/__init__.py +1 -4
- scitex/__main__.py +24 -5
- scitex/__version__.py +1 -1
- scitex/_install_guide.py +14 -2
- scitex/_optional_deps.py +33 -0
- scitex/ai/classification/reporters/_ClassificationReporter.py +1 -1
- scitex/ai/classification/timeseries/_TimeSeriesBlockingSplit.py +2 -2
- scitex/ai/classification/timeseries/_TimeSeriesCalendarSplit.py +2 -2
- scitex/ai/classification/timeseries/_TimeSeriesSlidingWindowSplit.py +2 -2
- scitex/ai/classification/timeseries/_TimeSeriesSlidingWindowSplit_v01-not-using-n_splits.py +2 -2
- scitex/ai/classification/timeseries/_TimeSeriesStratifiedSplit.py +2 -2
- scitex/ai/classification/timeseries/_normalize_timestamp.py +1 -1
- scitex/ai/metrics/_calc_seizure_prediction_metrics.py +1 -1
- scitex/ai/plt/_plot_feature_importance.py +1 -1
- scitex/ai/plt/_plot_learning_curve.py +1 -1
- scitex/ai/plt/_plot_optuna_study.py +1 -1
- scitex/ai/plt/_plot_pre_rec_curve.py +1 -1
- scitex/ai/plt/_plot_roc_curve.py +1 -1
- scitex/ai/plt/_stx_conf_mat.py +1 -1
- scitex/ai/training/_LearningCurveLogger.py +1 -1
- scitex/audio/mcp_server.py +38 -8
- scitex/bridge/_figrecipe.py +1 -1
- scitex/bridge/_helpers.py +1 -1
- scitex/bridge/_plt_vis.py +1 -1
- scitex/bridge/_stats_plt.py +1 -1
- scitex/bridge/_stats_vis.py +2 -2
- scitex/browser/automation/CookieHandler.py +1 -1
- scitex/browser/core/BrowserMixin.py +1 -1
- scitex/browser/core/ChromeProfileManager.py +1 -1
- scitex/browser/debugging/_browser_logger.py +1 -1
- scitex/browser/debugging/_highlight_element.py +1 -1
- scitex/browser/debugging/_show_grid.py +1 -1
- scitex/browser/interaction/click_center.py +1 -1
- scitex/browser/interaction/click_with_fallbacks.py +1 -1
- scitex/browser/interaction/close_popups.py +1 -1
- scitex/browser/interaction/fill_with_fallbacks.py +1 -1
- scitex/browser/pdf/click_download_for_chrome_pdf_viewer.py +1 -1
- scitex/browser/pdf/detect_chrome_pdf_viewer.py +1 -1
- scitex/browser/stealth/HumanBehavior.py +1 -1
- scitex/browser/stealth/StealthManager.py +1 -1
- scitex/{fig → canvas}/__init__.py +84 -96
- scitex/canvas/_mcp_handlers.py +372 -0
- scitex/canvas/_mcp_tool_schemas.py +219 -0
- scitex/{fig → canvas}/backend/_parser.py +1 -1
- scitex/{fig → canvas}/canvas.py +13 -14
- scitex/{fts/_fig/_editor → canvas/editor}/_defaults.py +2 -2
- scitex/{fig → canvas}/editor/edit/__init__.py +11 -14
- scitex/{fig → canvas}/editor/edit/bundle_resolver.py +56 -48
- scitex/{fig → canvas}/editor/edit/editor_launcher.py +79 -26
- scitex/{fts/_fig/_editor/_cui/_panel_loader.py → canvas/editor/edit/panel_loader.py} +8 -8
- scitex/{fts/_fig/_editor/_gui/_flask_editor → canvas/editor/flask_editor}/_bbox.py +2 -1
- scitex/{fts/_fig/_editor/_gui/_flask_editor → canvas/editor/flask_editor}/_core.py +84 -84
- scitex/{fts/_fig/_editor/_gui/_flask_editor → canvas/editor/flask_editor}/_renderer.py +7 -6
- scitex/{fts/_fig/_editor/_gui/_flask_editor → canvas/editor/flask_editor}/static/css/features/canvas.css +2 -2
- scitex/{fig → canvas}/editor/flask_editor/static/css/features/panel-grid.css +1 -1
- scitex/{fig → canvas}/editor/flask_editor/static/js/core/api.js +3 -4
- scitex/{fig → canvas}/editor/flask_editor/static/js/editor/preview.js +5 -5
- scitex/{fig → canvas}/editor/flask_editor/templates/_html.py +3 -3
- scitex/{fig → canvas}/editor/flask_editor/templates/_scripts.py +10 -10
- scitex/{fig → canvas}/editor/flask_editor/templates/_styles.py +3 -3
- scitex/{fig → canvas}/io/__init__.py +32 -38
- scitex/{fig → canvas}/io/_bundle.py +217 -154
- scitex/{fig → canvas}/io/_canvas.py +1 -1
- scitex/{fig → canvas}/io/_data.py +1 -1
- scitex/{fig → canvas}/io/_export.py +1 -1
- scitex/{fig → canvas}/io/_load.py +1 -1
- scitex/{fig → canvas}/io/_panel.py +1 -1
- scitex/{fig → canvas}/io/_save.py +1 -1
- scitex/canvas/mcp_server.py +151 -0
- scitex/{fig → canvas}/model/__init__.py +1 -1
- scitex/{fig → canvas}/model/_annotations.py +1 -1
- scitex/{fig → canvas}/model/_axes.py +1 -1
- scitex/{fig → canvas}/model/_figure.py +1 -1
- scitex/{fig → canvas}/model/_guides.py +1 -1
- scitex/{fig → canvas}/model/_plot.py +1 -1
- scitex/{fig → canvas}/model/_styles.py +1 -1
- scitex/{fig → canvas}/utils/__init__.py +1 -1
- scitex/capture/mcp_server.py +41 -12
- scitex/cli/audio.py +233 -0
- scitex/cli/capture.py +307 -0
- scitex/cli/convert.py +10 -6
- scitex/cli/main.py +27 -4
- scitex/cli/repro.py +233 -0
- scitex/cli/resource.py +240 -0
- scitex/cli/stats.py +325 -0
- scitex/cli/template.py +236 -0
- scitex/cli/tex.py +286 -0
- scitex/cli/web.py +11 -12
- scitex/dev/__init__.py +3 -0
- scitex/dev/_pyproject.py +405 -0
- scitex/dev/plt/__init__.py +2 -2
- scitex/dev/plt/mpl/get_dir_ax.py +1 -1
- scitex/dev/plt/mpl/get_signatures.py +1 -1
- scitex/dev/plt/mpl/get_signatures_details.py +1 -1
- scitex/diagram/README.md +7 -7
- scitex/diagram/_mcp_handlers.py +400 -0
- scitex/diagram/_mcp_tool_schemas.py +157 -0
- scitex/diagram/mcp_server.py +151 -0
- scitex/dsp/_demo_sig.py +51 -5
- scitex/dsp/_mne.py +13 -2
- scitex/dsp/_modulation_index.py +15 -3
- scitex/dsp/_pac.py +23 -5
- scitex/dsp/_psd.py +16 -4
- scitex/dsp/_resample.py +24 -4
- scitex/dsp/_transform.py +16 -3
- scitex/dsp/add_noise.py +15 -1
- scitex/dsp/norm.py +17 -2
- scitex/dsp/reference.py +17 -1
- scitex/dsp/utils/_differential_bandpass_filters.py +20 -2
- scitex/dsp/utils/_zero_pad.py +18 -4
- scitex/dt/_normalize_timestamp.py +1 -1
- scitex/git/_session.py +1 -1
- scitex/io/__init__.py +7 -19
- scitex/io/_load.py +15 -19
- scitex/io/_load_modules/_canvas.py +2 -2
- scitex/io/_load_modules/_con.py +17 -6
- scitex/io/_load_modules/_eeg.py +28 -13
- scitex/io/_load_modules/_optuna.py +21 -63
- scitex/io/_load_modules/_torch.py +11 -3
- scitex/io/_save.py +11 -16
- scitex/io/_save_modules/__init__.py +6 -10
- scitex/io/_save_modules/_canvas.py +3 -3
- scitex/io/_save_modules/_optuna_study_as_csv_and_pngs.py +13 -2
- scitex/io/_save_modules/_plot_bundle.py +112 -0
- scitex/io/_save_modules/{_pltz_stx.py → _plot_scitex.py} +7 -7
- scitex/io/_save_modules/_stx_bundle.py +16 -16
- scitex/io/_save_modules/_torch.py +11 -3
- scitex/io/bundle/README.md +89 -80
- scitex/{fts/_bundle/_FTS.py → io/bundle/_Bundle.py} +197 -95
- scitex/io/bundle/__init__.py +67 -35
- scitex/{fts/_bundle → io/bundle}/_children.py +32 -40
- scitex/io/bundle/_core.py +184 -97
- scitex/{fts/_bundle/_dataclasses/_Node.py → io/bundle/_dataclasses/_Spec.py} +29 -23
- scitex/{fts/_bundle/_dataclasses/_NodeRefs.py → io/bundle/_dataclasses/_SpecRefs.py} +6 -6
- scitex/{fts/_bundle → io/bundle}/_dataclasses/__init__.py +4 -4
- scitex/{fts/_bundle → io/bundle}/_loader.py +19 -19
- scitex/io/bundle/_manifest.py +99 -0
- scitex/{fts/_bundle → io/bundle}/_mpl_helpers.py +119 -28
- scitex/io/bundle/_nested.py +113 -100
- scitex/{fts/_bundle → io/bundle}/_saver.py +13 -14
- scitex/{fts/_bundle → io/bundle}/_storage.py +3 -3
- scitex/io/bundle/_types.py +41 -16
- scitex/{fts/_bundle → io/bundle}/_validation.py +20 -18
- scitex/io/bundle/_zip.py +21 -31
- scitex/{fts/_kinds → io/bundle/kinds}/_plot/_backend/_parser.py +1 -1
- scitex/{fts/_kinds → io/bundle/kinds}/_plot/_models/_Annotations.py +1 -1
- scitex/{fts/_kinds → io/bundle/kinds}/_plot/_models/_Axes.py +1 -1
- scitex/{fts/_kinds → io/bundle/kinds}/_plot/_models/_Figure.py +1 -1
- scitex/{fts/_fig → io/bundle/kinds/_plot}/_models/_Guides.py +1 -1
- scitex/{fts/_kinds → io/bundle/kinds}/_plot/_models/_Plot.py +1 -1
- scitex/{fts/_fig → io/bundle/kinds/_plot}/_models/_Styles.py +1 -1
- scitex/{fts/_kinds → io/bundle/kinds}/_plot/_utils/_plot_layout.py +1 -1
- scitex/{fts/_kinds → io/bundle/kinds}/_table/_latex/__init__.py +1 -1
- scitex/{fts/_kinds → io/bundle/kinds}/_table/_latex/_editor/_app.py +1 -1
- scitex/{fts/_tables → io/bundle/kinds/_table}/_latex/_export.py +1 -1
- scitex/{fts/_kinds → io/bundle/kinds}/_table/_latex/_figure_exporter.py +1 -1
- scitex/{fts/_kinds → io/bundle/kinds}/_table/_latex/_table_exporter.py +1 -1
- scitex/io/bundle/schemas/__init__.py +30 -0
- scitex/mcp_server.py +159 -0
- scitex/parallel/_run.py +5 -4
- scitex/path/_find.py +60 -83
- scitex/path/_get_module_path.py +23 -21
- scitex/path/_get_spath.py +6 -27
- scitex/path/_getsize.py +23 -9
- scitex/path/_increment_version.py +31 -38
- scitex/path/_mk_spath.py +26 -29
- scitex/path/_path.py +5 -12
- scitex/path/_split.py +27 -15
- scitex/path/_this_path.py +23 -9
- scitex/plt/_mcp_handlers.py +361 -0
- scitex/plt/_mcp_tool_schemas.py +169 -0
- scitex/plt/_subplots/_AxisWrapperMixins/_MatplotlibPlotMixin/__init__.py +2 -1
- scitex/plt/_subplots/_AxisWrapperMixins/__init__.py +2 -2
- scitex/plt/gallery/_generate.py +76 -50
- scitex/plt/io/__init__.py +17 -19
- scitex/plt/io/_bundle.py +99 -52
- scitex/plt/io/_layered_bundle.py +303 -168
- scitex/plt/mcp_server.py +205 -0
- scitex/plt/utils/_csv_column_naming.py +250 -118
- scitex/repro/README_RandomStateManager.md +3 -3
- scitex/repro/_RandomStateManager.py +14 -14
- scitex/repro/_gen_ID.py +1 -1
- scitex/repro/_gen_timestamp.py +1 -1
- scitex/repro/_hash_array.py +4 -4
- scitex/schema/__init__.py +69 -73
- scitex/schema/_canvas.py +1 -1
- scitex/schema/_stats.py +2 -2
- scitex/scholar/__main__.py +24 -2
- scitex/scholar/_mcp_handlers.py +685 -0
- scitex/scholar/_mcp_tool_schemas.py +339 -0
- scitex/scholar/docs/template.py +1 -1
- scitex/scholar/examples/07_storage_integration.py +1 -1
- scitex/scholar/impact_factor/jcr/ImpactFactorJCREngine.py +1 -1
- scitex/scholar/impact_factor/jcr/build_database.py +1 -1
- scitex/scholar/mcp_server.py +315 -0
- scitex/scholar/pdf_download/ScholarPDFDownloader.py +1 -1
- scitex/scholar/pipelines/ScholarPipelineBibTeX.py +1 -1
- scitex/scholar/pipelines/ScholarPipelineParallel.py +1 -1
- scitex/scholar/pipelines/ScholarPipelineSingle.py +1 -1
- scitex/scholar/storage/PaperIO.py +1 -1
- scitex/session/README.md +4 -4
- scitex/session/__init__.py +1 -1
- scitex/session/_decorator.py +9 -9
- scitex/session/_lifecycle.py +5 -5
- scitex/session/template.py +1 -1
- scitex/stats/__init__.py +30 -33
- scitex/stats/__main__.py +281 -0
- scitex/stats/_mcp_handlers.py +1191 -0
- scitex/stats/_mcp_tool_schemas.py +384 -0
- scitex/stats/_schema.py +1 -1
- scitex/stats/correct/_correct_bonferroni.py +1 -1
- scitex/stats/correct/_correct_fdr.py +1 -1
- scitex/stats/correct/_correct_fdr_.py +1 -1
- scitex/stats/correct/_correct_holm.py +1 -1
- scitex/stats/correct/_correct_sidak.py +1 -1
- scitex/stats/effect_sizes/_cliffs_delta.py +1 -1
- scitex/stats/effect_sizes/_cohens_d.py +1 -1
- scitex/stats/effect_sizes/_epsilon_squared.py +1 -1
- scitex/stats/effect_sizes/_eta_squared.py +1 -1
- scitex/stats/effect_sizes/_prob_superiority.py +1 -1
- scitex/stats/io/__init__.py +10 -11
- scitex/stats/io/_bundle.py +16 -16
- scitex/stats/mcp_server.py +405 -0
- scitex/stats/posthoc/_dunnett.py +1 -1
- scitex/stats/posthoc/_games_howell.py +1 -1
- scitex/stats/posthoc/_tukey_hsd.py +1 -1
- scitex/stats/power/_power.py +1 -1
- scitex/stats/utils/_effect_size.py +1 -1
- scitex/stats/utils/_formatters.py +1 -1
- scitex/stats/utils/_power.py +1 -1
- scitex/template/_mcp_handlers.py +259 -0
- scitex/template/_mcp_tool_schemas.py +112 -0
- scitex/template/mcp_server.py +186 -0
- scitex/utils/_verify_scitex_format.py +2 -2
- scitex/utils/template.py +1 -1
- scitex/web/__init__.py +12 -11
- scitex/web/_scraping.py +26 -265
- scitex/web/download_images.py +316 -0
- scitex/writer/Writer.py +1 -1
- scitex/writer/_clone_writer_project.py +1 -1
- scitex/writer/_validate_tree_structures.py +1 -1
- scitex/writer/dataclasses/config/_WriterConfig.py +1 -1
- scitex/writer/dataclasses/contents/_ManuscriptContents.py +1 -1
- scitex/writer/dataclasses/core/_Document.py +1 -1
- scitex/writer/dataclasses/core/_DocumentSection.py +1 -1
- scitex/writer/dataclasses/results/_CompilationResult.py +1 -1
- scitex/writer/dataclasses/results/_LaTeXIssue.py +1 -1
- scitex/writer/utils/.legacy_git_retry.py +7 -5
- scitex/writer/utils/_parse_latex_logs.py +1 -1
- scitex-2.13.0.dist-info/METADATA +1231 -0
- {scitex-2.10.3.dist-info → scitex-2.13.0.dist-info}/RECORD +376 -470
- scitex-2.13.0.dist-info/entry_points.txt +11 -0
- scitex/fig/editor/_defaults.py +0 -300
- scitex/fig/editor/edit/panel_loader.py +0 -232
- scitex/fig/editor/flask_editor/_bbox.py +0 -1299
- scitex/fig/editor/flask_editor/_core.py +0 -1429
- scitex/fig/editor/flask_editor/_renderer.py +0 -813
- scitex/fig/editor/flask_editor/static/css/features/canvas.css +0 -176
- scitex/fts/README.md +0 -262
- scitex/fts/TODO.md +0 -66
- scitex/fts/__init__.py +0 -90
- scitex/fts/_bundle/README_IN_BUNDLE.md +0 -102
- scitex/fts/_bundle/__init__.py +0 -38
- scitex/fts/_bundle/_utils/__init__.py +0 -55
- scitex/fts/_bundle/_utils/_const.py +0 -26
- scitex/fts/_bundle/_utils/_errors.py +0 -73
- scitex/fts/_bundle/_utils/_generate.py +0 -21
- scitex/fts/_bundle/_utils/_types.py +0 -76
- scitex/fts/_bundle/_zipbundle.py +0 -165
- scitex/fts/_fig/__init__.py +0 -22
- scitex/fts/_fig/_backend/_parser.py +0 -188
- scitex/fts/_fig/_editor/__init__.py +0 -14
- scitex/fts/_fig/_editor/_cui/__init__.py +0 -33
- scitex/fts/_fig/_editor/_cui/_backend_detector.py +0 -39
- scitex/fts/_fig/_editor/_cui/_bundle_resolver.py +0 -366
- scitex/fts/_fig/_editor/_cui/_editor_launcher.py +0 -175
- scitex/fts/_fig/_editor/_cui/_manual_handler.py +0 -52
- scitex/fts/_fig/_editor/_cui/_path_resolver.py +0 -66
- scitex/fts/_fig/_editor/_gui/__init__.py +0 -11
- scitex/fts/_fig/_editor/_gui/_flask_editor/__init__.py +0 -20
- scitex/fts/_fig/_editor/_gui/_flask_editor/_plotter.py +0 -664
- scitex/fts/_fig/_editor/_gui/_flask_editor/_utils.py +0 -79
- scitex/fts/_fig/_editor/_gui/_flask_editor/static/css/base/reset.css +0 -41
- scitex/fts/_fig/_editor/_gui/_flask_editor/static/css/base/typography.css +0 -16
- scitex/fts/_fig/_editor/_gui/_flask_editor/static/css/base/variables.css +0 -85
- scitex/fts/_fig/_editor/_gui/_flask_editor/static/css/components/buttons.css +0 -217
- scitex/fts/_fig/_editor/_gui/_flask_editor/static/css/components/context-menu.css +0 -93
- scitex/fts/_fig/_editor/_gui/_flask_editor/static/css/components/dropdown.css +0 -57
- scitex/fts/_fig/_editor/_gui/_flask_editor/static/css/components/forms.css +0 -112
- scitex/fts/_fig/_editor/_gui/_flask_editor/static/css/components/modal.css +0 -59
- scitex/fts/_fig/_editor/_gui/_flask_editor/static/css/components/sections.css +0 -212
- scitex/fts/_fig/_editor/_gui/_flask_editor/static/css/features/element-inspector.css +0 -190
- scitex/fts/_fig/_editor/_gui/_flask_editor/static/css/features/loading.css +0 -59
- scitex/fts/_fig/_editor/_gui/_flask_editor/static/css/features/overlay.css +0 -45
- scitex/fts/_fig/_editor/_gui/_flask_editor/static/css/features/panel-grid.css +0 -95
- scitex/fts/_fig/_editor/_gui/_flask_editor/static/css/features/selection.css +0 -101
- scitex/fts/_fig/_editor/_gui/_flask_editor/static/css/features/statistics.css +0 -138
- scitex/fts/_fig/_editor/_gui/_flask_editor/static/css/index.css +0 -31
- scitex/fts/_fig/_editor/_gui/_flask_editor/static/css/layout/container.css +0 -7
- scitex/fts/_fig/_editor/_gui/_flask_editor/static/css/layout/controls.css +0 -56
- scitex/fts/_fig/_editor/_gui/_flask_editor/static/css/layout/preview.css +0 -78
- scitex/fts/_fig/_editor/_gui/_flask_editor/static/js/alignment/axis.js +0 -314
- scitex/fts/_fig/_editor/_gui/_flask_editor/static/js/alignment/basic.js +0 -107
- scitex/fts/_fig/_editor/_gui/_flask_editor/static/js/alignment/distribute.js +0 -54
- scitex/fts/_fig/_editor/_gui/_flask_editor/static/js/canvas/canvas.js +0 -172
- scitex/fts/_fig/_editor/_gui/_flask_editor/static/js/canvas/dragging.js +0 -258
- scitex/fts/_fig/_editor/_gui/_flask_editor/static/js/canvas/resize.js +0 -48
- scitex/fts/_fig/_editor/_gui/_flask_editor/static/js/canvas/selection.js +0 -71
- scitex/fts/_fig/_editor/_gui/_flask_editor/static/js/core/api.js +0 -288
- scitex/fts/_fig/_editor/_gui/_flask_editor/static/js/core/state.js +0 -143
- scitex/fts/_fig/_editor/_gui/_flask_editor/static/js/core/utils.js +0 -245
- scitex/fts/_fig/_editor/_gui/_flask_editor/static/js/dev/element-inspector.js +0 -992
- scitex/fts/_fig/_editor/_gui/_flask_editor/static/js/editor/bbox.js +0 -339
- scitex/fts/_fig/_editor/_gui/_flask_editor/static/js/editor/element-drag.js +0 -286
- scitex/fts/_fig/_editor/_gui/_flask_editor/static/js/editor/overlay.js +0 -371
- scitex/fts/_fig/_editor/_gui/_flask_editor/static/js/editor/preview.js +0 -293
- scitex/fts/_fig/_editor/_gui/_flask_editor/static/js/main.js +0 -426
- scitex/fts/_fig/_editor/_gui/_flask_editor/static/js/shortcuts/context-menu.js +0 -152
- scitex/fts/_fig/_editor/_gui/_flask_editor/static/js/shortcuts/keyboard.js +0 -265
- scitex/fts/_fig/_editor/_gui/_flask_editor/static/js/ui/controls.js +0 -184
- scitex/fts/_fig/_editor/_gui/_flask_editor/static/js/ui/download.js +0 -57
- scitex/fts/_fig/_editor/_gui/_flask_editor/static/js/ui/help.js +0 -100
- scitex/fts/_fig/_editor/_gui/_flask_editor/static/js/ui/theme.js +0 -34
- scitex/fts/_fig/_editor/_gui/_flask_editor/templates/__init__.py +0 -124
- scitex/fts/_fig/_editor/_gui/_flask_editor/templates/_html.py +0 -851
- scitex/fts/_fig/_editor/_gui/_flask_editor/templates/_scripts.py +0 -4932
- scitex/fts/_fig/_editor/_gui/_flask_editor/templates/_styles.py +0 -1657
- scitex/fts/_fig/_editor/_gui/_flask_editor.py +0 -36
- scitex/fts/_fig/_models/_Annotations.py +0 -115
- scitex/fts/_fig/_models/_Axes.py +0 -152
- scitex/fts/_fig/_models/_Figure.py +0 -138
- scitex/fts/_fig/_models/_Plot.py +0 -123
- scitex/fts/_fig/_utils/_plot_layout.py +0 -397
- scitex/fts/_kinds/_figure/_composite.py +0 -345
- scitex/fts/_kinds/_plot/_backend/__init__.py +0 -53
- scitex/fts/_kinds/_plot/_backend/_export.py +0 -165
- scitex/fts/_kinds/_plot/_backend/_render.py +0 -538
- scitex/fts/_kinds/_plot/_dataclasses/_ChannelEncoding.py +0 -46
- scitex/fts/_kinds/_plot/_dataclasses/_Encoding.py +0 -82
- scitex/fts/_kinds/_plot/_dataclasses/_Theme.py +0 -441
- scitex/fts/_kinds/_plot/_dataclasses/_TraceEncoding.py +0 -52
- scitex/fts/_kinds/_plot/_dataclasses/__init__.py +0 -47
- scitex/fts/_kinds/_plot/_models/_Guides.py +0 -104
- scitex/fts/_kinds/_plot/_models/_Styles.py +0 -245
- scitex/fts/_kinds/_plot/_models/__init__.py +0 -80
- scitex/fts/_kinds/_plot/_models/_plot_types/__init__.py +0 -156
- scitex/fts/_kinds/_plot/_models/_plot_types/_bar.py +0 -43
- scitex/fts/_kinds/_plot/_models/_plot_types/_box.py +0 -38
- scitex/fts/_kinds/_plot/_models/_plot_types/_distribution.py +0 -36
- scitex/fts/_kinds/_plot/_models/_plot_types/_errorbar.py +0 -60
- scitex/fts/_kinds/_plot/_models/_plot_types/_histogram.py +0 -30
- scitex/fts/_kinds/_plot/_models/_plot_types/_image.py +0 -61
- scitex/fts/_kinds/_plot/_models/_plot_types/_line.py +0 -57
- scitex/fts/_kinds/_plot/_models/_plot_types/_scatter.py +0 -30
- scitex/fts/_kinds/_plot/_models/_plot_types/_seaborn.py +0 -121
- scitex/fts/_kinds/_plot/_models/_plot_types/_violin.py +0 -36
- scitex/fts/_kinds/_plot/_utils/__init__.py +0 -129
- scitex/fts/_kinds/_plot/_utils/_auto_layout.py +0 -127
- scitex/fts/_kinds/_plot/_utils/_calc_bounds.py +0 -111
- scitex/fts/_kinds/_plot/_utils/_const_sizes.py +0 -48
- scitex/fts/_kinds/_plot/_utils/_convert_coords.py +0 -77
- scitex/fts/_kinds/_plot/_utils/_get_template.py +0 -178
- scitex/fts/_kinds/_plot/_utils/_normalize.py +0 -73
- scitex/fts/_kinds/_plot/_utils/_validate.py +0 -197
- scitex/fts/_kinds/_table/_latex/_export.py +0 -279
- scitex/fts/_stats/__init__.py +0 -48
- scitex/fts/_stats/_dataclasses/_Stats.py +0 -423
- scitex/fts/_stats/_dataclasses/__init__.py +0 -48
- scitex/fts/_tables/__init__.py +0 -65
- scitex/fts/_tables/_latex/__init__.py +0 -93
- scitex/fts/_tables/_latex/_editor/__init__.py +0 -11
- scitex/fts/_tables/_latex/_editor/_app.py +0 -725
- scitex/fts/_tables/_latex/_figure_exporter.py +0 -153
- scitex/fts/_tables/_latex/_stats_formatter.py +0 -274
- scitex/fts/_tables/_latex/_table_exporter.py +0 -362
- scitex/fts/_tables/_latex/_utils.py +0 -369
- scitex/fts/_tables/_latex/_validator.py +0 -445
- scitex/io/_save_modules/_pltz_bundle.py +0 -356
- scitex-2.10.3.dist-info/METADATA +0 -952
- scitex-2.10.3.dist-info/entry_points.txt +0 -2
- /scitex/{fig → canvas}/README.md +0 -0
- /scitex/{fig → canvas}/backend/__init__.py +0 -0
- /scitex/{fig → canvas}/backend/_export.py +0 -0
- /scitex/{fig → canvas}/backend/_render.py +0 -0
- /scitex/{fig → canvas}/docs/CANVAS_ARCHITECTURE.md +0 -0
- /scitex/{fig → canvas}/editor/__init__.py +0 -0
- /scitex/{fig → canvas}/editor/_dearpygui_editor.py +0 -0
- /scitex/{fig → canvas}/editor/_flask_editor.py +0 -0
- /scitex/{fig → canvas}/editor/_mpl_editor.py +0 -0
- /scitex/{fig → canvas}/editor/_qt_editor.py +0 -0
- /scitex/{fig → canvas}/editor/_tkinter_editor.py +0 -0
- /scitex/{fig → canvas}/editor/edit/backend_detector.py +0 -0
- /scitex/{fig → canvas}/editor/edit/manual_handler.py +0 -0
- /scitex/{fig → canvas}/editor/edit/path_resolver.py +0 -0
- /scitex/{fig → canvas}/editor/flask_editor/__init__.py +0 -0
- /scitex/{fig → canvas}/editor/flask_editor/_plotter.py +0 -0
- /scitex/{fig → canvas}/editor/flask_editor/_utils.py +0 -0
- /scitex/{fig → canvas}/editor/flask_editor/static/css/base/reset.css +0 -0
- /scitex/{fig → canvas}/editor/flask_editor/static/css/base/typography.css +0 -0
- /scitex/{fig → canvas}/editor/flask_editor/static/css/base/variables.css +0 -0
- /scitex/{fig → canvas}/editor/flask_editor/static/css/components/buttons.css +0 -0
- /scitex/{fig → canvas}/editor/flask_editor/static/css/components/context-menu.css +0 -0
- /scitex/{fig → canvas}/editor/flask_editor/static/css/components/dropdown.css +0 -0
- /scitex/{fig → canvas}/editor/flask_editor/static/css/components/forms.css +0 -0
- /scitex/{fig → canvas}/editor/flask_editor/static/css/components/modal.css +0 -0
- /scitex/{fig → canvas}/editor/flask_editor/static/css/components/sections.css +0 -0
- /scitex/{fig → canvas}/editor/flask_editor/static/css/features/element-inspector.css +0 -0
- /scitex/{fig → canvas}/editor/flask_editor/static/css/features/loading.css +0 -0
- /scitex/{fig → canvas}/editor/flask_editor/static/css/features/overlay.css +0 -0
- /scitex/{fig → canvas}/editor/flask_editor/static/css/features/selection.css +0 -0
- /scitex/{fig → canvas}/editor/flask_editor/static/css/features/statistics.css +0 -0
- /scitex/{fig → canvas}/editor/flask_editor/static/css/index.css +0 -0
- /scitex/{fig → canvas}/editor/flask_editor/static/css/layout/container.css +0 -0
- /scitex/{fig → canvas}/editor/flask_editor/static/css/layout/controls.css +0 -0
- /scitex/{fig → canvas}/editor/flask_editor/static/css/layout/preview.css +0 -0
- /scitex/{fig → canvas}/editor/flask_editor/static/js/alignment/axis.js +0 -0
- /scitex/{fig → canvas}/editor/flask_editor/static/js/alignment/basic.js +0 -0
- /scitex/{fig → canvas}/editor/flask_editor/static/js/alignment/distribute.js +0 -0
- /scitex/{fig → canvas}/editor/flask_editor/static/js/canvas/canvas.js +0 -0
- /scitex/{fig → canvas}/editor/flask_editor/static/js/canvas/dragging.js +0 -0
- /scitex/{fig → canvas}/editor/flask_editor/static/js/canvas/resize.js +0 -0
- /scitex/{fig → canvas}/editor/flask_editor/static/js/canvas/selection.js +0 -0
- /scitex/{fig → canvas}/editor/flask_editor/static/js/core/state.js +0 -0
- /scitex/{fig → canvas}/editor/flask_editor/static/js/core/utils.js +0 -0
- /scitex/{fig → canvas}/editor/flask_editor/static/js/dev/element-inspector.js +0 -0
- /scitex/{fig → canvas}/editor/flask_editor/static/js/editor/bbox.js +0 -0
- /scitex/{fig → canvas}/editor/flask_editor/static/js/editor/element-drag.js +0 -0
- /scitex/{fig → canvas}/editor/flask_editor/static/js/editor/overlay.js +0 -0
- /scitex/{fig → canvas}/editor/flask_editor/static/js/main.js +0 -0
- /scitex/{fig → canvas}/editor/flask_editor/static/js/shortcuts/context-menu.js +0 -0
- /scitex/{fig → canvas}/editor/flask_editor/static/js/shortcuts/keyboard.js +0 -0
- /scitex/{fig → canvas}/editor/flask_editor/static/js/ui/controls.js +0 -0
- /scitex/{fig → canvas}/editor/flask_editor/static/js/ui/download.js +0 -0
- /scitex/{fig → canvas}/editor/flask_editor/static/js/ui/help.js +0 -0
- /scitex/{fig → canvas}/editor/flask_editor/static/js/ui/theme.js +0 -0
- /scitex/{fig → canvas}/editor/flask_editor/templates/__init__.py +0 -0
- /scitex/{fig → canvas}/io/_directory.py +0 -0
- /scitex/{fig → canvas}/model/_plot_types.py +0 -0
- /scitex/{fig → canvas}/utils/_defaults.py +0 -0
- /scitex/{fig → canvas}/utils/_validate.py +0 -0
- /scitex/{fts/_bundle → io/bundle}/_conversion/__init__.py +0 -0
- /scitex/{fts/_bundle → io/bundle}/_conversion/_bundle2dict.py +0 -0
- /scitex/{fts/_bundle → io/bundle}/_conversion/_dict2bundle.py +0 -0
- /scitex/{fts/_bundle → io/bundle}/_dataclasses/_Axes.py +0 -0
- /scitex/{fts/_bundle → io/bundle}/_dataclasses/_BBox.py +0 -0
- /scitex/{fts/_bundle → io/bundle}/_dataclasses/_ColumnDef.py +0 -0
- /scitex/{fts/_bundle → io/bundle}/_dataclasses/_DataFormat.py +0 -0
- /scitex/{fts/_bundle → io/bundle}/_dataclasses/_DataInfo.py +0 -0
- /scitex/{fts/_bundle → io/bundle}/_dataclasses/_DataSource.py +0 -0
- /scitex/{fts/_bundle → io/bundle}/_dataclasses/_SizeMM.py +0 -0
- /scitex/{fts/_bundle → io/bundle}/_extractors/__init__.py +0 -0
- /scitex/{fts/_bundle → io/bundle}/_extractors/_extract_bar.py +0 -0
- /scitex/{fts/_bundle → io/bundle}/_extractors/_extract_line.py +0 -0
- /scitex/{fts/_bundle → io/bundle}/_extractors/_extract_scatter.py +0 -0
- /scitex/{fts/_kinds → io/bundle/kinds}/__init__.py +0 -0
- /scitex/{fts/_kinds → io/bundle/kinds}/_figure/__init__.py +0 -0
- /scitex/{fts/_fig → io/bundle/kinds/_figure}/_composite.py +0 -0
- /scitex/{fts/_kinds → io/bundle/kinds}/_plot/__init__.py +0 -0
- /scitex/{fts/_fig → io/bundle/kinds/_plot}/_backend/__init__.py +0 -0
- /scitex/{fts/_fig → io/bundle/kinds/_plot}/_backend/_export.py +0 -0
- /scitex/{fts/_fig → io/bundle/kinds/_plot}/_backend/_render.py +0 -0
- /scitex/{fts/_fig → io/bundle/kinds/_plot}/_dataclasses/_ChannelEncoding.py +0 -0
- /scitex/{fts/_fig → io/bundle/kinds/_plot}/_dataclasses/_Encoding.py +0 -0
- /scitex/{fts/_fig → io/bundle/kinds/_plot}/_dataclasses/_Theme.py +0 -0
- /scitex/{fts/_fig → io/bundle/kinds/_plot}/_dataclasses/_TraceEncoding.py +0 -0
- /scitex/{fts/_fig → io/bundle/kinds/_plot}/_dataclasses/__init__.py +0 -0
- /scitex/{fts/_fig → io/bundle/kinds/_plot}/_models/__init__.py +0 -0
- /scitex/{fts/_fig → io/bundle/kinds/_plot}/_models/_plot_types/__init__.py +0 -0
- /scitex/{fts/_fig → io/bundle/kinds/_plot}/_models/_plot_types/_bar.py +0 -0
- /scitex/{fts/_fig → io/bundle/kinds/_plot}/_models/_plot_types/_box.py +0 -0
- /scitex/{fts/_fig → io/bundle/kinds/_plot}/_models/_plot_types/_distribution.py +0 -0
- /scitex/{fts/_fig → io/bundle/kinds/_plot}/_models/_plot_types/_errorbar.py +0 -0
- /scitex/{fts/_fig → io/bundle/kinds/_plot}/_models/_plot_types/_histogram.py +0 -0
- /scitex/{fts/_fig → io/bundle/kinds/_plot}/_models/_plot_types/_image.py +0 -0
- /scitex/{fts/_fig → io/bundle/kinds/_plot}/_models/_plot_types/_line.py +0 -0
- /scitex/{fts/_fig → io/bundle/kinds/_plot}/_models/_plot_types/_scatter.py +0 -0
- /scitex/{fts/_fig → io/bundle/kinds/_plot}/_models/_plot_types/_seaborn.py +0 -0
- /scitex/{fts/_fig → io/bundle/kinds/_plot}/_models/_plot_types/_violin.py +0 -0
- /scitex/{fts/_fig → io/bundle/kinds/_plot}/_utils/__init__.py +0 -0
- /scitex/{fts/_fig → io/bundle/kinds/_plot}/_utils/_auto_layout.py +0 -0
- /scitex/{fts/_fig → io/bundle/kinds/_plot}/_utils/_calc_bounds.py +0 -0
- /scitex/{fts/_fig → io/bundle/kinds/_plot}/_utils/_const_sizes.py +0 -0
- /scitex/{fts/_fig → io/bundle/kinds/_plot}/_utils/_convert_coords.py +0 -0
- /scitex/{fts/_fig → io/bundle/kinds/_plot}/_utils/_get_template.py +0 -0
- /scitex/{fts/_fig → io/bundle/kinds/_plot}/_utils/_normalize.py +0 -0
- /scitex/{fts/_fig → io/bundle/kinds/_plot}/_utils/_validate.py +0 -0
- /scitex/{fts/_kinds → io/bundle/kinds}/_shape/__init__.py +0 -0
- /scitex/{fts/_kinds → io/bundle/kinds}/_stats/__init__.py +0 -0
- /scitex/{fts/_kinds → io/bundle/kinds}/_stats/_dataclasses/_Stats.py +0 -0
- /scitex/{fts/_kinds → io/bundle/kinds}/_stats/_dataclasses/__init__.py +0 -0
- /scitex/{fts/_kinds → io/bundle/kinds}/_table/__init__.py +0 -0
- /scitex/{fts/_kinds → io/bundle/kinds}/_table/_latex/_editor/__init__.py +0 -0
- /scitex/{fts/_kinds → io/bundle/kinds}/_table/_latex/_stats_formatter.py +0 -0
- /scitex/{fts/_kinds → io/bundle/kinds}/_table/_latex/_utils.py +0 -0
- /scitex/{fts/_kinds → io/bundle/kinds}/_table/_latex/_validator.py +0 -0
- /scitex/{fts/_kinds → io/bundle/kinds}/_text/__init__.py +0 -0
- /scitex/{fts/_schemas → io/bundle/schemas}/data_info.schema.json +0 -0
- /scitex/{fts/_schemas → io/bundle/schemas}/encoding.schema.json +0 -0
- /scitex/{fts/_schemas → io/bundle/schemas}/node.schema.json +0 -0
- /scitex/{fts/_schemas → io/bundle/schemas}/render_manifest.schema.json +0 -0
- /scitex/{fts/_schemas → io/bundle/schemas}/stats.schema.json +0 -0
- /scitex/{fts/_schemas → io/bundle/schemas}/theme.schema.json +0 -0
- {scitex-2.10.3.dist-info → scitex-2.13.0.dist-info}/WHEEL +0 -0
- {scitex-2.10.3.dist-info → scitex-2.13.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,1429 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
# -*- coding: utf-8 -*-
|
|
3
|
-
# File: ./src/scitex/vis/editor/flask_editor/core.py
|
|
4
|
-
"""Core WebEditor class for Flask-based figure editing."""
|
|
5
|
-
|
|
6
|
-
from pathlib import Path
|
|
7
|
-
from typing import Dict, Any, Optional
|
|
8
|
-
import base64
|
|
9
|
-
import copy
|
|
10
|
-
import json
|
|
11
|
-
import threading
|
|
12
|
-
import webbrowser
|
|
13
|
-
|
|
14
|
-
from ._utils import find_available_port, kill_process_on_port, check_port_available
|
|
15
|
-
from .templates import build_html_template
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
class WebEditor:
|
|
19
|
-
"""
|
|
20
|
-
Browser-based figure editor using Flask.
|
|
21
|
-
|
|
22
|
-
Features:
|
|
23
|
-
- Displays existing PNG from pltz bundle (no re-rendering)
|
|
24
|
-
- Hitmap-based element selection for precise clicking
|
|
25
|
-
- Property editors with sliders and color pickers
|
|
26
|
-
- Save to .manual.json
|
|
27
|
-
- SciTeX style defaults pre-filled
|
|
28
|
-
- Auto-finds available port if default is in use
|
|
29
|
-
"""
|
|
30
|
-
|
|
31
|
-
def __init__(
|
|
32
|
-
self,
|
|
33
|
-
json_path: Path,
|
|
34
|
-
metadata: Dict[str, Any],
|
|
35
|
-
csv_data: Optional[Any] = None,
|
|
36
|
-
png_path: Optional[Path] = None,
|
|
37
|
-
hitmap_path: Optional[Path] = None,
|
|
38
|
-
manual_overrides: Optional[Dict[str, Any]] = None,
|
|
39
|
-
port: int = 5050,
|
|
40
|
-
panel_info: Optional[Dict[str, Any]] = None,
|
|
41
|
-
):
|
|
42
|
-
self.json_path = Path(json_path)
|
|
43
|
-
self.metadata = metadata
|
|
44
|
-
self.csv_data = csv_data
|
|
45
|
-
self.png_path = Path(png_path) if png_path else None
|
|
46
|
-
self.hitmap_path = Path(hitmap_path) if hitmap_path else None
|
|
47
|
-
self.manual_overrides = manual_overrides or {}
|
|
48
|
-
self._requested_port = port
|
|
49
|
-
self.port = port
|
|
50
|
-
self.panel_info = panel_info # For multi-panel figz bundles
|
|
51
|
-
|
|
52
|
-
# Extract hit_regions from metadata for color-based element detection
|
|
53
|
-
self.hit_regions = metadata.get("hit_regions", {})
|
|
54
|
-
self.color_map = self.hit_regions.get("color_map", {})
|
|
55
|
-
|
|
56
|
-
# Get SciTeX defaults and merge with metadata
|
|
57
|
-
from .._defaults import get_scitex_defaults, extract_defaults_from_metadata
|
|
58
|
-
|
|
59
|
-
self.scitex_defaults = get_scitex_defaults()
|
|
60
|
-
self.metadata_defaults = extract_defaults_from_metadata(metadata)
|
|
61
|
-
|
|
62
|
-
# Start with defaults, then overlay manual overrides
|
|
63
|
-
self.current_overrides = copy.deepcopy(self.scitex_defaults)
|
|
64
|
-
self.current_overrides.update(self.metadata_defaults)
|
|
65
|
-
self.current_overrides.update(self.manual_overrides)
|
|
66
|
-
|
|
67
|
-
# Track initial state to detect modifications
|
|
68
|
-
self._initial_overrides = copy.deepcopy(self.current_overrides)
|
|
69
|
-
self._user_modified = False
|
|
70
|
-
|
|
71
|
-
def run(self):
|
|
72
|
-
"""Launch the web editor."""
|
|
73
|
-
try:
|
|
74
|
-
from flask import Flask, render_template_string, request, jsonify
|
|
75
|
-
except ImportError:
|
|
76
|
-
raise ImportError(
|
|
77
|
-
"Flask is required for web editor. Install: pip install flask"
|
|
78
|
-
)
|
|
79
|
-
|
|
80
|
-
# Handle port conflicts - always use port 5050
|
|
81
|
-
import time
|
|
82
|
-
max_retries = 3
|
|
83
|
-
for attempt in range(max_retries):
|
|
84
|
-
if check_port_available(self._requested_port):
|
|
85
|
-
self.port = self._requested_port
|
|
86
|
-
break
|
|
87
|
-
print(f"Port {self._requested_port} in use. Freeing... (attempt {attempt + 1}/{max_retries})")
|
|
88
|
-
kill_process_on_port(self._requested_port)
|
|
89
|
-
time.sleep(1.0) # Wait for port release
|
|
90
|
-
else:
|
|
91
|
-
# After retries, use requested port anyway (Flask will error if unavailable)
|
|
92
|
-
print(f"Warning: Port {self._requested_port} may still be in use")
|
|
93
|
-
self.port = self._requested_port
|
|
94
|
-
|
|
95
|
-
# Configure Flask with static folder path
|
|
96
|
-
import os
|
|
97
|
-
static_folder = os.path.join(os.path.dirname(__file__), 'static')
|
|
98
|
-
app = Flask(__name__, static_folder=static_folder, static_url_path='/static')
|
|
99
|
-
editor = self
|
|
100
|
-
|
|
101
|
-
def _export_composed_figure(editor, formats=["png", "svg"], dpi=150):
|
|
102
|
-
"""Helper to compose and export figure to bundle."""
|
|
103
|
-
from scitex.io import ZipBundle
|
|
104
|
-
from PIL import Image
|
|
105
|
-
import numpy as np
|
|
106
|
-
import matplotlib
|
|
107
|
-
matplotlib.use('Agg')
|
|
108
|
-
import matplotlib.pyplot as plt
|
|
109
|
-
import json as json_module
|
|
110
|
-
import io
|
|
111
|
-
import zipfile
|
|
112
|
-
|
|
113
|
-
if not editor.panel_info:
|
|
114
|
-
return {"success": False, "error": "No panel info"}
|
|
115
|
-
|
|
116
|
-
bundle_path = editor.panel_info.get("bundle_path")
|
|
117
|
-
figz_dir = editor.panel_info.get("figz_dir")
|
|
118
|
-
|
|
119
|
-
if not bundle_path and not figz_dir:
|
|
120
|
-
return {"success": False, "error": "No bundle path"}
|
|
121
|
-
|
|
122
|
-
figure_name = Path(bundle_path).stem if bundle_path else (
|
|
123
|
-
Path(figz_dir).stem.replace(".figz.d", "") if figz_dir else "figure"
|
|
124
|
-
)
|
|
125
|
-
|
|
126
|
-
# Read spec.json for layout and layout.json for position overrides
|
|
127
|
-
spec = {}
|
|
128
|
-
layout_overrides = {}
|
|
129
|
-
if bundle_path:
|
|
130
|
-
try:
|
|
131
|
-
with ZipBundle(bundle_path, mode="r") as bundle:
|
|
132
|
-
spec = bundle.read_json("spec.json")
|
|
133
|
-
try:
|
|
134
|
-
layout_overrides = bundle.read_json("layout.json")
|
|
135
|
-
except:
|
|
136
|
-
pass
|
|
137
|
-
except:
|
|
138
|
-
pass
|
|
139
|
-
elif figz_dir:
|
|
140
|
-
spec_path = Path(figz_dir) / "spec.json"
|
|
141
|
-
if spec_path.exists():
|
|
142
|
-
with open(spec_path) as f:
|
|
143
|
-
spec = json_module.load(f)
|
|
144
|
-
layout_path = Path(figz_dir) / "layout.json"
|
|
145
|
-
if layout_path.exists():
|
|
146
|
-
with open(layout_path) as f:
|
|
147
|
-
layout_overrides = json_module.load(f)
|
|
148
|
-
|
|
149
|
-
# Also check in-memory layout overrides
|
|
150
|
-
if editor.panel_info and editor.panel_info.get("layout"):
|
|
151
|
-
layout_overrides = editor.panel_info.get("layout", {})
|
|
152
|
-
|
|
153
|
-
# Get figure dimensions
|
|
154
|
-
fig_width_mm = 180
|
|
155
|
-
fig_height_mm = 120
|
|
156
|
-
if "figure" in spec:
|
|
157
|
-
fig_info = spec.get("figure", {})
|
|
158
|
-
styles = fig_info.get("styles", {})
|
|
159
|
-
size = styles.get("size", {})
|
|
160
|
-
fig_width_mm = size.get("width_mm", 180)
|
|
161
|
-
fig_height_mm = size.get("height_mm", 120)
|
|
162
|
-
|
|
163
|
-
fig_width_in = fig_width_mm / 25.4
|
|
164
|
-
fig_height_in = fig_height_mm / 25.4
|
|
165
|
-
|
|
166
|
-
fig = plt.figure(figsize=(fig_width_in, fig_height_in), dpi=dpi, facecolor='white')
|
|
167
|
-
|
|
168
|
-
# Compose panels
|
|
169
|
-
panels_spec = spec.get("panels", [])
|
|
170
|
-
panel_paths = editor.panel_info.get("panel_paths", [])
|
|
171
|
-
panel_is_zip = editor.panel_info.get("panel_is_zip", [])
|
|
172
|
-
|
|
173
|
-
for panel_spec in panels_spec:
|
|
174
|
-
panel_id = panel_spec.get("id", "")
|
|
175
|
-
pos = panel_spec.get("position", {})
|
|
176
|
-
size = panel_spec.get("size", {})
|
|
177
|
-
|
|
178
|
-
# Skip overview/auxiliary panels (only compose main panels A-Z)
|
|
179
|
-
panel_id_lower = panel_id.lower()
|
|
180
|
-
if any(skip in panel_id_lower for skip in ['overview', 'thumb', 'preview', 'aux']):
|
|
181
|
-
continue
|
|
182
|
-
|
|
183
|
-
# Find panel path first (needed to check layout_overrides)
|
|
184
|
-
panel_path = None
|
|
185
|
-
is_zip = False
|
|
186
|
-
panel_name = None
|
|
187
|
-
for idx, pp in enumerate(panel_paths):
|
|
188
|
-
pp_name = Path(pp).stem.replace(".pltz", "")
|
|
189
|
-
if (pp_name == panel_id or
|
|
190
|
-
pp_name.startswith(f"panel_{panel_id}_") or
|
|
191
|
-
pp_name == f"panel_{panel_id}" or
|
|
192
|
-
f"_{panel_id}_" in pp_name):
|
|
193
|
-
panel_path = pp
|
|
194
|
-
panel_name = Path(pp).name # e.g., "panel_A_twinx.pltz"
|
|
195
|
-
is_zip = panel_is_zip[idx] if idx < len(panel_is_zip) else False
|
|
196
|
-
break
|
|
197
|
-
|
|
198
|
-
if not panel_path:
|
|
199
|
-
continue
|
|
200
|
-
|
|
201
|
-
# Check for layout overrides (from layout.json or in-memory)
|
|
202
|
-
override = layout_overrides.get(panel_name, {})
|
|
203
|
-
override_pos = override.get("position", {})
|
|
204
|
-
override_size = override.get("size", {})
|
|
205
|
-
|
|
206
|
-
# Use override positions if available, otherwise use spec
|
|
207
|
-
x_mm = override_pos.get("x_mm", pos.get("x_mm", 0))
|
|
208
|
-
y_mm = override_pos.get("y_mm", pos.get("y_mm", 0))
|
|
209
|
-
w_mm = override_size.get("width_mm", size.get("width_mm", 60))
|
|
210
|
-
h_mm = override_size.get("height_mm", size.get("height_mm", 40))
|
|
211
|
-
|
|
212
|
-
x_frac = x_mm / fig_width_mm
|
|
213
|
-
y_frac = 1 - (y_mm + h_mm) / fig_height_mm
|
|
214
|
-
w_frac = w_mm / fig_width_mm
|
|
215
|
-
h_frac = h_mm / fig_height_mm
|
|
216
|
-
|
|
217
|
-
# Load panel preview
|
|
218
|
-
try:
|
|
219
|
-
# Exclusion patterns for preview selection
|
|
220
|
-
exclude_patterns = ['hitmap', 'overview', 'thumb', 'preview']
|
|
221
|
-
|
|
222
|
-
if is_zip:
|
|
223
|
-
with ZipBundle(panel_path, mode="r") as pltz_bundle:
|
|
224
|
-
with zipfile.ZipFile(panel_path, 'r') as zf:
|
|
225
|
-
png_files = [n for n in zf.namelist()
|
|
226
|
-
if n.endswith('.png')
|
|
227
|
-
and 'exports/' in n
|
|
228
|
-
and not any(p in n.lower() for p in exclude_patterns)]
|
|
229
|
-
if png_files:
|
|
230
|
-
preview_path = png_files[0]
|
|
231
|
-
if '.pltz.d/' in preview_path:
|
|
232
|
-
preview_path = preview_path.split('.pltz.d/')[-1]
|
|
233
|
-
img_data = pltz_bundle.read_bytes(preview_path)
|
|
234
|
-
img = Image.open(io.BytesIO(img_data))
|
|
235
|
-
ax = fig.add_axes([x_frac, y_frac, w_frac, h_frac])
|
|
236
|
-
ax.imshow(np.array(img))
|
|
237
|
-
ax.axis('off')
|
|
238
|
-
else:
|
|
239
|
-
pltz_dir = Path(panel_path)
|
|
240
|
-
exports_dir = pltz_dir / "exports"
|
|
241
|
-
if exports_dir.exists():
|
|
242
|
-
for png_file in exports_dir.glob("*.png"):
|
|
243
|
-
name_lower = png_file.name.lower()
|
|
244
|
-
if not any(p in name_lower for p in exclude_patterns):
|
|
245
|
-
img = Image.open(png_file)
|
|
246
|
-
ax = fig.add_axes([x_frac, y_frac, w_frac, h_frac])
|
|
247
|
-
ax.imshow(np.array(img))
|
|
248
|
-
ax.axis('off')
|
|
249
|
-
break
|
|
250
|
-
except Exception as e:
|
|
251
|
-
print(f"Could not load panel {panel_id}: {e}")
|
|
252
|
-
|
|
253
|
-
# Draw panel letter
|
|
254
|
-
if panel_id and len(panel_id) <= 2: # Only for short IDs like A, B, C...
|
|
255
|
-
# Position letter at top-left corner of panel
|
|
256
|
-
letter_x = x_frac + 0.01
|
|
257
|
-
letter_y = y_frac + h_frac - 0.02
|
|
258
|
-
fig.text(letter_x, letter_y, panel_id,
|
|
259
|
-
fontsize=14, fontweight='bold', color='black',
|
|
260
|
-
ha='left', va='top',
|
|
261
|
-
transform=fig.transFigure,
|
|
262
|
-
bbox=dict(boxstyle='square,pad=0.1',
|
|
263
|
-
facecolor='white', edgecolor='none', alpha=0.8))
|
|
264
|
-
|
|
265
|
-
exported = {}
|
|
266
|
-
|
|
267
|
-
# Save to bundle
|
|
268
|
-
if bundle_path:
|
|
269
|
-
with ZipBundle(bundle_path, mode="a") as bundle:
|
|
270
|
-
for fmt in formats:
|
|
271
|
-
buf = io.BytesIO()
|
|
272
|
-
fig.savefig(buf, format=fmt, dpi=dpi, bbox_inches="tight",
|
|
273
|
-
facecolor="white", pad_inches=0.02)
|
|
274
|
-
buf.seek(0)
|
|
275
|
-
export_path = f"exports/{figure_name}.{fmt}"
|
|
276
|
-
bundle.write_bytes(export_path, buf.read())
|
|
277
|
-
exported[fmt] = export_path
|
|
278
|
-
|
|
279
|
-
plt.close(fig)
|
|
280
|
-
return {"success": True, "exported": exported}
|
|
281
|
-
|
|
282
|
-
@app.route("/")
|
|
283
|
-
def index():
|
|
284
|
-
# Rebuild template each time for hot reload support
|
|
285
|
-
html_template = build_html_template()
|
|
286
|
-
|
|
287
|
-
# Extract figz and panel paths for display
|
|
288
|
-
json_path_str = str(editor.json_path.resolve())
|
|
289
|
-
figz_path = ""
|
|
290
|
-
panel_path = ""
|
|
291
|
-
|
|
292
|
-
# Check if this is inside a figz bundle
|
|
293
|
-
if '.figz.d/' in json_path_str:
|
|
294
|
-
parts = json_path_str.split('.figz.d/')
|
|
295
|
-
figz_path = parts[0] + '.figz.d'
|
|
296
|
-
panel_path = parts[1] if len(parts) > 1 else ""
|
|
297
|
-
elif '.pltz.d/' in json_path_str:
|
|
298
|
-
parts = json_path_str.split('.pltz.d/')
|
|
299
|
-
figz_path = parts[0] + '.pltz.d'
|
|
300
|
-
panel_path = parts[1] if len(parts) > 1 else ""
|
|
301
|
-
else:
|
|
302
|
-
figz_path = json_path_str
|
|
303
|
-
|
|
304
|
-
return render_template_string(
|
|
305
|
-
html_template,
|
|
306
|
-
filename=figz_path,
|
|
307
|
-
panel_path=panel_path,
|
|
308
|
-
overrides=json.dumps(editor.current_overrides),
|
|
309
|
-
)
|
|
310
|
-
|
|
311
|
-
@app.route("/preview")
|
|
312
|
-
def preview():
|
|
313
|
-
"""Render figure preview with current overrides (same logic as /update)."""
|
|
314
|
-
from ._renderer import render_preview_with_bboxes
|
|
315
|
-
|
|
316
|
-
# Always use renderer for consistency between initial and updated views
|
|
317
|
-
dark_mode = request.args.get("dark_mode", "false").lower() == "true"
|
|
318
|
-
img_data, bboxes, img_size = render_preview_with_bboxes(
|
|
319
|
-
editor.csv_data, editor.current_overrides,
|
|
320
|
-
metadata=editor.metadata,
|
|
321
|
-
dark_mode=dark_mode,
|
|
322
|
-
)
|
|
323
|
-
return jsonify({
|
|
324
|
-
"image": img_data,
|
|
325
|
-
"bboxes": bboxes,
|
|
326
|
-
"img_size": img_size,
|
|
327
|
-
"has_hitmap": editor.hitmap_path is not None and editor.hitmap_path.exists(),
|
|
328
|
-
"format": "png",
|
|
329
|
-
"panel_info": editor.panel_info,
|
|
330
|
-
})
|
|
331
|
-
|
|
332
|
-
@app.route("/panels")
|
|
333
|
-
def panels():
|
|
334
|
-
"""Return all panel images with bboxes for interactive grid view (figz bundles only).
|
|
335
|
-
|
|
336
|
-
Uses smart load_panel_data helper for transparent zip/directory handling.
|
|
337
|
-
Returns layout info from figz spec.json for unified canvas positioning.
|
|
338
|
-
"""
|
|
339
|
-
from ._bbox import extract_bboxes_from_metadata, extract_bboxes_from_geometry_px
|
|
340
|
-
from ..edit import load_panel_data
|
|
341
|
-
import json as json_module
|
|
342
|
-
|
|
343
|
-
if not editor.panel_info:
|
|
344
|
-
return jsonify({"error": "Not a multi-panel figz bundle"}), 400
|
|
345
|
-
|
|
346
|
-
panel_names = editor.panel_info["panels"]
|
|
347
|
-
panel_paths = editor.panel_info.get("panel_paths", [])
|
|
348
|
-
panel_is_zip = editor.panel_info.get("panel_is_zip", [False] * len(panel_names))
|
|
349
|
-
figz_dir = Path(editor.panel_info["figz_dir"])
|
|
350
|
-
|
|
351
|
-
if not panel_paths:
|
|
352
|
-
panel_paths = [str(figz_dir / name) for name in panel_names]
|
|
353
|
-
|
|
354
|
-
# Load figz spec.json to get panel layout
|
|
355
|
-
figz_layout = {}
|
|
356
|
-
spec_path = figz_dir / "spec.json"
|
|
357
|
-
if spec_path.exists():
|
|
358
|
-
with open(spec_path) as f:
|
|
359
|
-
figz_spec = json_module.load(f)
|
|
360
|
-
for panel_spec in figz_spec.get("panels", []):
|
|
361
|
-
panel_id = panel_spec.get("id", "")
|
|
362
|
-
figz_layout[panel_id] = {
|
|
363
|
-
"position": panel_spec.get("position", {}),
|
|
364
|
-
"size": panel_spec.get("size", {}),
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
panel_images = []
|
|
368
|
-
|
|
369
|
-
for idx, panel_name in enumerate(panel_names):
|
|
370
|
-
panel_path = panel_paths[idx]
|
|
371
|
-
is_zip = panel_is_zip[idx] if idx < len(panel_is_zip) else None
|
|
372
|
-
display_name = panel_name.replace(".pltz.d", "").replace(".pltz", "")
|
|
373
|
-
|
|
374
|
-
# Use smart helper to load panel data
|
|
375
|
-
loaded = load_panel_data(panel_path, is_zip=is_zip)
|
|
376
|
-
|
|
377
|
-
panel_data = {"name": display_name, "image": None, "bboxes": None, "img_size": None}
|
|
378
|
-
|
|
379
|
-
# Add layout info from figz spec
|
|
380
|
-
if display_name in figz_layout:
|
|
381
|
-
panel_data["layout"] = figz_layout[display_name]
|
|
382
|
-
|
|
383
|
-
if loaded:
|
|
384
|
-
# Get image data
|
|
385
|
-
if loaded.get("is_zip"):
|
|
386
|
-
png_bytes = loaded.get("png_bytes")
|
|
387
|
-
if png_bytes:
|
|
388
|
-
panel_data["image"] = base64.b64encode(png_bytes).decode("utf-8")
|
|
389
|
-
else:
|
|
390
|
-
png_path = loaded.get("png_path")
|
|
391
|
-
if png_path and png_path.exists():
|
|
392
|
-
with open(png_path, "rb") as f:
|
|
393
|
-
panel_data["image"] = base64.b64encode(f.read()).decode("utf-8")
|
|
394
|
-
|
|
395
|
-
# Get image size
|
|
396
|
-
img_size = loaded.get("img_size")
|
|
397
|
-
if img_size:
|
|
398
|
-
panel_data["img_size"] = img_size
|
|
399
|
-
panel_data["width"] = img_size["width"]
|
|
400
|
-
panel_data["height"] = img_size["height"]
|
|
401
|
-
elif loaded.get("png_path"):
|
|
402
|
-
from PIL import Image
|
|
403
|
-
img = Image.open(loaded["png_path"])
|
|
404
|
-
panel_data["img_size"] = {"width": img.size[0], "height": img.size[1]}
|
|
405
|
-
panel_data["width"], panel_data["height"] = img.size
|
|
406
|
-
img.close()
|
|
407
|
-
|
|
408
|
-
# Extract bboxes - prefer geometry_px.json
|
|
409
|
-
if panel_data.get("img_size"):
|
|
410
|
-
geometry_data = loaded.get("geometry_data")
|
|
411
|
-
metadata = loaded.get("metadata", {})
|
|
412
|
-
|
|
413
|
-
if geometry_data:
|
|
414
|
-
panel_data["bboxes"] = extract_bboxes_from_geometry_px(
|
|
415
|
-
geometry_data,
|
|
416
|
-
panel_data["img_size"]["width"],
|
|
417
|
-
panel_data["img_size"]["height"]
|
|
418
|
-
)
|
|
419
|
-
elif metadata:
|
|
420
|
-
panel_data["bboxes"] = extract_bboxes_from_metadata(
|
|
421
|
-
metadata,
|
|
422
|
-
panel_data["img_size"]["width"],
|
|
423
|
-
panel_data["img_size"]["height"]
|
|
424
|
-
)
|
|
425
|
-
|
|
426
|
-
panel_images.append(panel_data)
|
|
427
|
-
|
|
428
|
-
return jsonify({
|
|
429
|
-
"panels": panel_images,
|
|
430
|
-
"count": len(panel_images),
|
|
431
|
-
"layout": figz_layout,
|
|
432
|
-
})
|
|
433
|
-
|
|
434
|
-
@app.route("/switch_panel/<int:panel_index>")
|
|
435
|
-
def switch_panel(panel_index):
|
|
436
|
-
"""Switch to a different panel in the figz bundle.
|
|
437
|
-
|
|
438
|
-
Uses smart load_panel_data helper for transparent zip/directory handling.
|
|
439
|
-
"""
|
|
440
|
-
from ._bbox import extract_bboxes_from_metadata, extract_bboxes_from_geometry_px
|
|
441
|
-
from ..edit import load_panel_data
|
|
442
|
-
|
|
443
|
-
if not editor.panel_info:
|
|
444
|
-
return jsonify({"error": "Not a multi-panel figz bundle"}), 400
|
|
445
|
-
|
|
446
|
-
panels = editor.panel_info["panels"]
|
|
447
|
-
panel_paths = editor.panel_info.get("panel_paths", [])
|
|
448
|
-
panel_is_zip = editor.panel_info.get("panel_is_zip", [False] * len(panels))
|
|
449
|
-
|
|
450
|
-
if panel_index < 0 or panel_index >= len(panels):
|
|
451
|
-
return jsonify({"error": f"Invalid panel index: {panel_index}"}), 400
|
|
452
|
-
|
|
453
|
-
panel_name = panels[panel_index]
|
|
454
|
-
panel_path = panel_paths[panel_index] if panel_paths else str(Path(editor.panel_info["figz_dir"]) / panel_name)
|
|
455
|
-
is_zip = panel_is_zip[panel_index] if panel_index < len(panel_is_zip) else None
|
|
456
|
-
|
|
457
|
-
try:
|
|
458
|
-
# Use smart helper to load panel data
|
|
459
|
-
loaded = load_panel_data(panel_path, is_zip=is_zip)
|
|
460
|
-
|
|
461
|
-
if not loaded:
|
|
462
|
-
return jsonify({"error": f"Could not load panel: {panel_name}"}), 400
|
|
463
|
-
|
|
464
|
-
# Get image data
|
|
465
|
-
img_data = None
|
|
466
|
-
if loaded.get("is_zip"):
|
|
467
|
-
png_bytes = loaded.get("png_bytes")
|
|
468
|
-
if png_bytes:
|
|
469
|
-
img_data = base64.b64encode(png_bytes).decode("utf-8")
|
|
470
|
-
else:
|
|
471
|
-
png_path = loaded.get("png_path")
|
|
472
|
-
if png_path and png_path.exists():
|
|
473
|
-
with open(png_path, "rb") as f:
|
|
474
|
-
img_data = base64.b64encode(f.read()).decode("utf-8")
|
|
475
|
-
|
|
476
|
-
if not img_data:
|
|
477
|
-
return jsonify({"error": f"No PNG found for panel: {panel_name}"}), 400
|
|
478
|
-
|
|
479
|
-
# Get image size
|
|
480
|
-
img_size = loaded.get("img_size", {"width": 0, "height": 0})
|
|
481
|
-
if not img_size and loaded.get("png_path"):
|
|
482
|
-
from PIL import Image
|
|
483
|
-
img = Image.open(loaded["png_path"])
|
|
484
|
-
img_size = {"width": img.size[0], "height": img.size[1]}
|
|
485
|
-
img.close()
|
|
486
|
-
|
|
487
|
-
# Extract bboxes - prefer geometry_px.json
|
|
488
|
-
bboxes = {}
|
|
489
|
-
geometry_data = loaded.get("geometry_data")
|
|
490
|
-
metadata = loaded.get("metadata", {})
|
|
491
|
-
|
|
492
|
-
if geometry_data and img_size:
|
|
493
|
-
bboxes = extract_bboxes_from_geometry_px(
|
|
494
|
-
geometry_data,
|
|
495
|
-
img_size["width"],
|
|
496
|
-
img_size["height"]
|
|
497
|
-
)
|
|
498
|
-
elif metadata and img_size:
|
|
499
|
-
bboxes = extract_bboxes_from_metadata(
|
|
500
|
-
metadata,
|
|
501
|
-
img_size["width"],
|
|
502
|
-
img_size["height"]
|
|
503
|
-
)
|
|
504
|
-
|
|
505
|
-
# Update editor state
|
|
506
|
-
editor.metadata = metadata
|
|
507
|
-
editor.panel_info["current_index"] = panel_index
|
|
508
|
-
|
|
509
|
-
# Re-extract defaults from new metadata
|
|
510
|
-
from .._defaults import get_scitex_defaults, extract_defaults_from_metadata
|
|
511
|
-
editor.scitex_defaults = get_scitex_defaults()
|
|
512
|
-
editor.metadata_defaults = extract_defaults_from_metadata(metadata)
|
|
513
|
-
editor.current_overrides = copy.deepcopy(editor.scitex_defaults)
|
|
514
|
-
editor.current_overrides.update(editor.metadata_defaults)
|
|
515
|
-
editor.current_overrides.update(editor.manual_overrides)
|
|
516
|
-
|
|
517
|
-
return jsonify({
|
|
518
|
-
"success": True,
|
|
519
|
-
"panel_name": panel_name,
|
|
520
|
-
"panel_index": panel_index,
|
|
521
|
-
"image": img_data,
|
|
522
|
-
"bboxes": bboxes,
|
|
523
|
-
"img_size": img_size,
|
|
524
|
-
"overrides": editor.current_overrides,
|
|
525
|
-
})
|
|
526
|
-
except Exception as e:
|
|
527
|
-
import traceback
|
|
528
|
-
return jsonify({
|
|
529
|
-
"error": f"Failed to switch panel: {str(e)}",
|
|
530
|
-
"traceback": traceback.format_exc(),
|
|
531
|
-
}), 500
|
|
532
|
-
|
|
533
|
-
@app.route("/hitmap")
|
|
534
|
-
def hitmap():
|
|
535
|
-
"""Return hitmap PNG for element detection."""
|
|
536
|
-
if editor.hitmap_path and editor.hitmap_path.exists():
|
|
537
|
-
with open(editor.hitmap_path, "rb") as f:
|
|
538
|
-
img_data = base64.b64encode(f.read()).decode("utf-8")
|
|
539
|
-
return jsonify({
|
|
540
|
-
"image": img_data,
|
|
541
|
-
"color_map": editor.color_map,
|
|
542
|
-
})
|
|
543
|
-
return jsonify({"error": "No hitmap available"}), 404
|
|
544
|
-
|
|
545
|
-
@app.route("/color_map")
|
|
546
|
-
def color_map():
|
|
547
|
-
"""Return color map for hitmap element identification."""
|
|
548
|
-
return jsonify({
|
|
549
|
-
"color_map": editor.color_map,
|
|
550
|
-
"hit_regions": editor.hit_regions,
|
|
551
|
-
})
|
|
552
|
-
|
|
553
|
-
@app.route("/update", methods=["POST"])
|
|
554
|
-
def update():
|
|
555
|
-
"""Update overrides and re-render with updated properties."""
|
|
556
|
-
from ._renderer import render_preview_with_bboxes
|
|
557
|
-
|
|
558
|
-
data = request.json
|
|
559
|
-
editor.current_overrides.update(data.get("overrides", {}))
|
|
560
|
-
editor._user_modified = True
|
|
561
|
-
|
|
562
|
-
# Check if dark mode is requested from POST data
|
|
563
|
-
dark_mode = data.get("dark_mode", False)
|
|
564
|
-
|
|
565
|
-
# Re-render the figure with updated overrides
|
|
566
|
-
img_data, bboxes, img_size = render_preview_with_bboxes(
|
|
567
|
-
editor.csv_data, editor.current_overrides,
|
|
568
|
-
metadata=editor.metadata,
|
|
569
|
-
dark_mode=dark_mode,
|
|
570
|
-
)
|
|
571
|
-
return jsonify({
|
|
572
|
-
"image": img_data,
|
|
573
|
-
"bboxes": bboxes,
|
|
574
|
-
"img_size": img_size,
|
|
575
|
-
"status": "updated",
|
|
576
|
-
})
|
|
577
|
-
|
|
578
|
-
@app.route("/save", methods=["POST"])
|
|
579
|
-
def save():
|
|
580
|
-
"""Save to .manual.json."""
|
|
581
|
-
from ..edit import save_manual_overrides
|
|
582
|
-
|
|
583
|
-
try:
|
|
584
|
-
manual_path = save_manual_overrides(
|
|
585
|
-
editor.json_path, editor.current_overrides
|
|
586
|
-
)
|
|
587
|
-
return jsonify({"status": "saved", "path": str(manual_path)})
|
|
588
|
-
except Exception as e:
|
|
589
|
-
return jsonify({"status": "error", "message": str(e)}), 500
|
|
590
|
-
|
|
591
|
-
@app.route("/save_layout", methods=["POST"])
|
|
592
|
-
def save_layout():
|
|
593
|
-
"""Save panel layout positions to figz bundle."""
|
|
594
|
-
try:
|
|
595
|
-
data = request.get_json()
|
|
596
|
-
layout = data.get("layout", {})
|
|
597
|
-
|
|
598
|
-
if not layout:
|
|
599
|
-
return jsonify({"success": False, "error": "No layout data provided"})
|
|
600
|
-
|
|
601
|
-
# Check if we have panel_info (figz bundle)
|
|
602
|
-
if not editor.panel_info:
|
|
603
|
-
return jsonify({"success": False, "error": "No panel info available (not a figz bundle)"})
|
|
604
|
-
|
|
605
|
-
bundle_path = editor.panel_info.get("bundle_path")
|
|
606
|
-
if not bundle_path:
|
|
607
|
-
return jsonify({"success": False, "error": "Bundle path not available"})
|
|
608
|
-
|
|
609
|
-
# Update layout in the figz bundle
|
|
610
|
-
from scitex.fig.io import ZipBundle
|
|
611
|
-
|
|
612
|
-
bundle = ZipBundle(bundle_path)
|
|
613
|
-
|
|
614
|
-
# Read existing layout or create new one
|
|
615
|
-
try:
|
|
616
|
-
existing_layout = bundle.read_json("layout.json")
|
|
617
|
-
except:
|
|
618
|
-
existing_layout = {}
|
|
619
|
-
|
|
620
|
-
# Update layout with new positions
|
|
621
|
-
for panel_name, pos in layout.items():
|
|
622
|
-
if panel_name not in existing_layout:
|
|
623
|
-
existing_layout[panel_name] = {}
|
|
624
|
-
if "position" not in existing_layout[panel_name]:
|
|
625
|
-
existing_layout[panel_name]["position"] = {}
|
|
626
|
-
if "size" not in existing_layout[panel_name]:
|
|
627
|
-
existing_layout[panel_name]["size"] = {}
|
|
628
|
-
|
|
629
|
-
# Update position
|
|
630
|
-
existing_layout[panel_name]["position"]["x_mm"] = pos.get("x_mm", 0)
|
|
631
|
-
existing_layout[panel_name]["position"]["y_mm"] = pos.get("y_mm", 0)
|
|
632
|
-
|
|
633
|
-
# Update size if provided
|
|
634
|
-
if "width_mm" in pos:
|
|
635
|
-
existing_layout[panel_name]["size"]["width_mm"] = pos["width_mm"]
|
|
636
|
-
if "height_mm" in pos:
|
|
637
|
-
existing_layout[panel_name]["size"]["height_mm"] = pos["height_mm"]
|
|
638
|
-
|
|
639
|
-
# Save updated layout
|
|
640
|
-
bundle.write_json("layout.json", existing_layout)
|
|
641
|
-
|
|
642
|
-
# Update in-memory panel_info
|
|
643
|
-
editor.panel_info["layout"] = existing_layout
|
|
644
|
-
|
|
645
|
-
# Auto-export composed figure to bundle
|
|
646
|
-
export_result = _export_composed_figure(editor, formats=["png", "svg"])
|
|
647
|
-
|
|
648
|
-
return jsonify({
|
|
649
|
-
"success": True,
|
|
650
|
-
"layout": existing_layout,
|
|
651
|
-
"exported": export_result.get("exported", {})
|
|
652
|
-
})
|
|
653
|
-
|
|
654
|
-
except Exception as e:
|
|
655
|
-
import traceback
|
|
656
|
-
return jsonify({
|
|
657
|
-
"success": False,
|
|
658
|
-
"error": str(e),
|
|
659
|
-
"traceback": traceback.format_exc()
|
|
660
|
-
})
|
|
661
|
-
|
|
662
|
-
@app.route("/save_element_position", methods=["POST"])
|
|
663
|
-
def save_element_position():
|
|
664
|
-
"""Save element position (legend/panel_letter) to figz bundle.
|
|
665
|
-
|
|
666
|
-
ONLY legends and panel letters can be repositioned to maintain
|
|
667
|
-
scientific rigor. Data elements are never moved.
|
|
668
|
-
"""
|
|
669
|
-
try:
|
|
670
|
-
data = request.get_json()
|
|
671
|
-
element = data.get("element", "")
|
|
672
|
-
panel = data.get("panel", "")
|
|
673
|
-
element_type = data.get("element_type", "")
|
|
674
|
-
position = data.get("position", {})
|
|
675
|
-
snap_name = data.get("snap_name")
|
|
676
|
-
|
|
677
|
-
# Validate element type (whitelist for scientific rigor)
|
|
678
|
-
ALLOWED_TYPES = ["legend", "panel_letter"]
|
|
679
|
-
if element_type not in ALLOWED_TYPES:
|
|
680
|
-
return jsonify({
|
|
681
|
-
"success": False,
|
|
682
|
-
"error": f"Element type '{element_type}' cannot be repositioned (scientific rigor)"
|
|
683
|
-
})
|
|
684
|
-
|
|
685
|
-
if not editor.panel_info:
|
|
686
|
-
return jsonify({"success": False, "error": "No panel info available"})
|
|
687
|
-
|
|
688
|
-
bundle_path = editor.panel_info.get("bundle_path")
|
|
689
|
-
if not bundle_path:
|
|
690
|
-
return jsonify({"success": False, "error": "Bundle path not available"})
|
|
691
|
-
|
|
692
|
-
from scitex.fig.io import ZipBundle
|
|
693
|
-
bundle = ZipBundle(bundle_path)
|
|
694
|
-
|
|
695
|
-
# Read or create style.json for element positions
|
|
696
|
-
try:
|
|
697
|
-
style = bundle.read_json("style.json")
|
|
698
|
-
except:
|
|
699
|
-
style = {}
|
|
700
|
-
|
|
701
|
-
# Initialize structure
|
|
702
|
-
if "elements" not in style:
|
|
703
|
-
style["elements"] = {}
|
|
704
|
-
if panel not in style["elements"]:
|
|
705
|
-
style["elements"][panel] = {}
|
|
706
|
-
|
|
707
|
-
# Save element position
|
|
708
|
-
style["elements"][panel][element] = {
|
|
709
|
-
"type": element_type,
|
|
710
|
-
"position": position,
|
|
711
|
-
"snap_name": snap_name,
|
|
712
|
-
}
|
|
713
|
-
|
|
714
|
-
# For legends, also update legend_location for matplotlib compatibility
|
|
715
|
-
if element_type == "legend" and snap_name:
|
|
716
|
-
# Convert snap name to matplotlib loc format
|
|
717
|
-
loc_map = {
|
|
718
|
-
"upper left": "upper left",
|
|
719
|
-
"upper center": "upper center",
|
|
720
|
-
"upper right": "upper right",
|
|
721
|
-
"center left": "center left",
|
|
722
|
-
"center": "center",
|
|
723
|
-
"center right": "center right",
|
|
724
|
-
"lower left": "lower left",
|
|
725
|
-
"lower center": "lower center",
|
|
726
|
-
"lower right": "lower right",
|
|
727
|
-
}
|
|
728
|
-
if snap_name in loc_map:
|
|
729
|
-
if "legend" not in style:
|
|
730
|
-
style["legend"] = {}
|
|
731
|
-
style["legend"]["location"] = loc_map[snap_name]
|
|
732
|
-
|
|
733
|
-
bundle.write_json("style.json", style)
|
|
734
|
-
|
|
735
|
-
return jsonify({
|
|
736
|
-
"success": True,
|
|
737
|
-
"element": element,
|
|
738
|
-
"position": position,
|
|
739
|
-
"snap_name": snap_name
|
|
740
|
-
})
|
|
741
|
-
|
|
742
|
-
except Exception as e:
|
|
743
|
-
import traceback
|
|
744
|
-
return jsonify({
|
|
745
|
-
"success": False,
|
|
746
|
-
"error": str(e),
|
|
747
|
-
"traceback": traceback.format_exc()
|
|
748
|
-
})
|
|
749
|
-
|
|
750
|
-
@app.route("/export", methods=["POST"])
|
|
751
|
-
def export_figure():
|
|
752
|
-
"""Export composed figure to various formats and update figz bundle."""
|
|
753
|
-
try:
|
|
754
|
-
data = request.get_json()
|
|
755
|
-
formats = data.get("formats", ["png", "svg"])
|
|
756
|
-
|
|
757
|
-
if not editor.panel_info:
|
|
758
|
-
return jsonify({"success": False, "error": "No panel info available"})
|
|
759
|
-
|
|
760
|
-
bundle_path = editor.panel_info.get("bundle_path")
|
|
761
|
-
if not bundle_path:
|
|
762
|
-
return jsonify({"success": False, "error": "Bundle path not available"})
|
|
763
|
-
|
|
764
|
-
from scitex.io import ZipBundle
|
|
765
|
-
from pathlib import Path
|
|
766
|
-
import io
|
|
767
|
-
import matplotlib
|
|
768
|
-
matplotlib.use('Agg')
|
|
769
|
-
import matplotlib.pyplot as plt
|
|
770
|
-
from PIL import Image
|
|
771
|
-
import numpy as np
|
|
772
|
-
|
|
773
|
-
figure_name = Path(bundle_path).stem
|
|
774
|
-
dpi = data.get("dpi", 150)
|
|
775
|
-
|
|
776
|
-
with ZipBundle(bundle_path, mode="a") as bundle:
|
|
777
|
-
# Read spec for figure size and panel positions
|
|
778
|
-
try:
|
|
779
|
-
spec = bundle.read_json("spec.json")
|
|
780
|
-
except:
|
|
781
|
-
spec = {}
|
|
782
|
-
|
|
783
|
-
# Get figure dimensions
|
|
784
|
-
fig_width_mm = 180
|
|
785
|
-
fig_height_mm = 120
|
|
786
|
-
if "figure" in spec:
|
|
787
|
-
fig_info = spec.get("figure", {})
|
|
788
|
-
styles = fig_info.get("styles", {})
|
|
789
|
-
size = styles.get("size", {})
|
|
790
|
-
fig_width_mm = size.get("width_mm", 180)
|
|
791
|
-
fig_height_mm = size.get("height_mm", 120)
|
|
792
|
-
|
|
793
|
-
# Convert mm to inches
|
|
794
|
-
fig_width_in = fig_width_mm / 25.4
|
|
795
|
-
fig_height_in = fig_height_mm / 25.4
|
|
796
|
-
|
|
797
|
-
# Create figure with white background
|
|
798
|
-
fig = plt.figure(figsize=(fig_width_in, fig_height_in), dpi=dpi, facecolor='white')
|
|
799
|
-
|
|
800
|
-
# Get panels from spec or editor.panel_info
|
|
801
|
-
panels_spec = spec.get("panels", [])
|
|
802
|
-
|
|
803
|
-
# Compose panels onto figure
|
|
804
|
-
for panel_spec in panels_spec:
|
|
805
|
-
panel_id = panel_spec.get("id", "")
|
|
806
|
-
pltz_name = panel_spec.get("plot", "")
|
|
807
|
-
|
|
808
|
-
# Get position and size from spec
|
|
809
|
-
pos = panel_spec.get("position", {})
|
|
810
|
-
size = panel_spec.get("size", {})
|
|
811
|
-
|
|
812
|
-
x_mm = pos.get("x_mm", 0)
|
|
813
|
-
y_mm = pos.get("y_mm", 0)
|
|
814
|
-
w_mm = size.get("width_mm", 60)
|
|
815
|
-
h_mm = size.get("height_mm", 40)
|
|
816
|
-
|
|
817
|
-
# Convert to figure coordinates (0-1)
|
|
818
|
-
x_frac = x_mm / fig_width_mm
|
|
819
|
-
y_frac = 1 - (y_mm + h_mm) / fig_height_mm # Flip Y
|
|
820
|
-
w_frac = w_mm / fig_width_mm
|
|
821
|
-
h_frac = h_mm / fig_height_mm
|
|
822
|
-
|
|
823
|
-
# Try to read panel image from pltz exports
|
|
824
|
-
img_loaded = False
|
|
825
|
-
for pltz_path in [f"{panel_id}.pltz", pltz_name.replace(".d", "")]:
|
|
826
|
-
if img_loaded:
|
|
827
|
-
break
|
|
828
|
-
try:
|
|
829
|
-
# Read pltz as nested bundle
|
|
830
|
-
pltz_bytes = bundle.read_bytes(pltz_path)
|
|
831
|
-
import tempfile
|
|
832
|
-
with tempfile.NamedTemporaryFile(suffix=".pltz", delete=False) as tmp:
|
|
833
|
-
tmp.write(pltz_bytes)
|
|
834
|
-
tmp_path = tmp.name
|
|
835
|
-
try:
|
|
836
|
-
with ZipBundle(tmp_path, mode="r") as pltz_bundle:
|
|
837
|
-
# Try various preview paths
|
|
838
|
-
for preview_path in ["exports/preview.png", "preview.png", f"exports/{panel_id}.png"]:
|
|
839
|
-
try:
|
|
840
|
-
img_data = pltz_bundle.read_bytes(preview_path)
|
|
841
|
-
img = Image.open(io.BytesIO(img_data))
|
|
842
|
-
img_array = np.array(img)
|
|
843
|
-
|
|
844
|
-
# Create axes and add image
|
|
845
|
-
ax = fig.add_axes([x_frac, y_frac, w_frac, h_frac])
|
|
846
|
-
ax.imshow(img_array)
|
|
847
|
-
ax.axis('off')
|
|
848
|
-
img_loaded = True
|
|
849
|
-
break
|
|
850
|
-
except:
|
|
851
|
-
continue
|
|
852
|
-
finally:
|
|
853
|
-
import os
|
|
854
|
-
os.unlink(tmp_path)
|
|
855
|
-
except Exception as e:
|
|
856
|
-
print(f"Could not load pltz {pltz_path}: {e}")
|
|
857
|
-
continue
|
|
858
|
-
|
|
859
|
-
exported = {}
|
|
860
|
-
|
|
861
|
-
for fmt in formats:
|
|
862
|
-
buf = io.BytesIO()
|
|
863
|
-
if fmt in ["png", "jpeg", "jpg"]:
|
|
864
|
-
fig.savefig(buf, format="png" if fmt == "png" else "jpeg",
|
|
865
|
-
dpi=dpi, bbox_inches="tight", facecolor="white",
|
|
866
|
-
pad_inches=0.02)
|
|
867
|
-
elif fmt == "svg":
|
|
868
|
-
fig.savefig(buf, format="svg", bbox_inches="tight", pad_inches=0.02)
|
|
869
|
-
elif fmt == "pdf":
|
|
870
|
-
fig.savefig(buf, format="pdf", bbox_inches="tight", pad_inches=0.02)
|
|
871
|
-
else:
|
|
872
|
-
continue
|
|
873
|
-
|
|
874
|
-
buf.seek(0)
|
|
875
|
-
content = buf.read()
|
|
876
|
-
|
|
877
|
-
# Save to exports/ directory in bundle
|
|
878
|
-
export_path = f"exports/{figure_name}.{fmt}"
|
|
879
|
-
bundle.write_bytes(export_path, content)
|
|
880
|
-
exported[fmt] = export_path
|
|
881
|
-
|
|
882
|
-
plt.close(fig)
|
|
883
|
-
|
|
884
|
-
return jsonify({
|
|
885
|
-
"success": True,
|
|
886
|
-
"exported": exported,
|
|
887
|
-
"bundle_path": str(bundle_path)
|
|
888
|
-
})
|
|
889
|
-
|
|
890
|
-
except Exception as e:
|
|
891
|
-
import traceback
|
|
892
|
-
return jsonify({
|
|
893
|
-
"success": False,
|
|
894
|
-
"error": str(e),
|
|
895
|
-
"traceback": traceback.format_exc()
|
|
896
|
-
})
|
|
897
|
-
|
|
898
|
-
@app.route("/download/<fmt>")
|
|
899
|
-
def download_figure(fmt):
|
|
900
|
-
"""Download figure in specified format."""
|
|
901
|
-
try:
|
|
902
|
-
from flask import send_file
|
|
903
|
-
import io
|
|
904
|
-
from pathlib import Path
|
|
905
|
-
|
|
906
|
-
mime_types = {
|
|
907
|
-
"png": "image/png",
|
|
908
|
-
"jpeg": "image/jpeg",
|
|
909
|
-
"jpg": "image/jpeg",
|
|
910
|
-
"svg": "image/svg+xml",
|
|
911
|
-
"pdf": "application/pdf",
|
|
912
|
-
}
|
|
913
|
-
|
|
914
|
-
if fmt not in mime_types:
|
|
915
|
-
return f"Unsupported format: {fmt}", 400
|
|
916
|
-
|
|
917
|
-
# For figz bundles, download the composed figure
|
|
918
|
-
if editor.panel_info:
|
|
919
|
-
bundle_path = editor.panel_info.get("bundle_path")
|
|
920
|
-
figz_dir = editor.panel_info.get("figz_dir")
|
|
921
|
-
figure_name = Path(bundle_path).stem if bundle_path else (
|
|
922
|
-
Path(figz_dir).stem.replace(".figz.d", "") if figz_dir else "figure"
|
|
923
|
-
)
|
|
924
|
-
|
|
925
|
-
if bundle_path or figz_dir:
|
|
926
|
-
from scitex.io import ZipBundle
|
|
927
|
-
from PIL import Image
|
|
928
|
-
import numpy as np
|
|
929
|
-
import matplotlib
|
|
930
|
-
matplotlib.use('Agg')
|
|
931
|
-
import matplotlib.pyplot as plt
|
|
932
|
-
import json as json_module
|
|
933
|
-
|
|
934
|
-
# Always compose on-demand to ensure current panel state
|
|
935
|
-
# (existing exports in bundle may be stale or blank)
|
|
936
|
-
|
|
937
|
-
# Read spec.json and layout.json for position overrides
|
|
938
|
-
spec = {}
|
|
939
|
-
layout_overrides = {}
|
|
940
|
-
if bundle_path:
|
|
941
|
-
try:
|
|
942
|
-
with ZipBundle(bundle_path, mode="r") as bundle:
|
|
943
|
-
spec = bundle.read_json("spec.json")
|
|
944
|
-
try:
|
|
945
|
-
layout_overrides = bundle.read_json("layout.json")
|
|
946
|
-
except:
|
|
947
|
-
pass
|
|
948
|
-
except:
|
|
949
|
-
pass
|
|
950
|
-
elif figz_dir:
|
|
951
|
-
spec_path = Path(figz_dir) / "spec.json"
|
|
952
|
-
if spec_path.exists():
|
|
953
|
-
with open(spec_path) as f:
|
|
954
|
-
spec = json_module.load(f)
|
|
955
|
-
layout_path = Path(figz_dir) / "layout.json"
|
|
956
|
-
if layout_path.exists():
|
|
957
|
-
with open(layout_path) as f:
|
|
958
|
-
layout_overrides = json_module.load(f)
|
|
959
|
-
|
|
960
|
-
# Also check in-memory layout overrides (most current)
|
|
961
|
-
if editor.panel_info and editor.panel_info.get("layout"):
|
|
962
|
-
layout_overrides = editor.panel_info.get("layout", {})
|
|
963
|
-
|
|
964
|
-
# Get figure dimensions
|
|
965
|
-
fig_width_mm = 180
|
|
966
|
-
fig_height_mm = 120
|
|
967
|
-
if "figure" in spec:
|
|
968
|
-
fig_info = spec.get("figure", {})
|
|
969
|
-
styles = fig_info.get("styles", {})
|
|
970
|
-
size = styles.get("size", {})
|
|
971
|
-
fig_width_mm = size.get("width_mm", 180)
|
|
972
|
-
fig_height_mm = size.get("height_mm", 120)
|
|
973
|
-
|
|
974
|
-
fig_width_in = fig_width_mm / 25.4
|
|
975
|
-
fig_height_in = fig_height_mm / 25.4
|
|
976
|
-
|
|
977
|
-
dpi = 150 if fmt in ["jpeg", "jpg"] else 300
|
|
978
|
-
fig = plt.figure(figsize=(fig_width_in, fig_height_in), dpi=dpi, facecolor='white')
|
|
979
|
-
|
|
980
|
-
# Compose panels
|
|
981
|
-
panels_spec = spec.get("panels", [])
|
|
982
|
-
panel_paths = editor.panel_info.get("panel_paths", [])
|
|
983
|
-
panel_is_zip = editor.panel_info.get("panel_is_zip", [])
|
|
984
|
-
|
|
985
|
-
for panel_spec in panels_spec:
|
|
986
|
-
panel_id = panel_spec.get("id", "")
|
|
987
|
-
pos = panel_spec.get("position", {})
|
|
988
|
-
size = panel_spec.get("size", {})
|
|
989
|
-
|
|
990
|
-
# Skip overview/auxiliary panels (only compose main panels A-Z)
|
|
991
|
-
panel_id_lower = panel_id.lower()
|
|
992
|
-
if any(skip in panel_id_lower for skip in ['overview', 'thumb', 'preview', 'aux']):
|
|
993
|
-
continue
|
|
994
|
-
|
|
995
|
-
# Find panel path first (needed to check layout_overrides)
|
|
996
|
-
panel_path = None
|
|
997
|
-
is_zip = False
|
|
998
|
-
panel_name = None
|
|
999
|
-
for idx, pp in enumerate(panel_paths):
|
|
1000
|
-
pp_name = Path(pp).stem.replace(".pltz", "")
|
|
1001
|
-
# Match exact name, or name contains panel_id pattern
|
|
1002
|
-
# e.g., "panel_A_twinx" matches panel_id "A"
|
|
1003
|
-
if (pp_name == panel_id or
|
|
1004
|
-
pp_name.startswith(f"panel_{panel_id}_") or
|
|
1005
|
-
pp_name.startswith(f"panel_{panel_id}.") or
|
|
1006
|
-
pp_name == f"panel_{panel_id}" or
|
|
1007
|
-
pp_name == panel_id or
|
|
1008
|
-
f"_{panel_id}_" in pp_name or
|
|
1009
|
-
pp_name.endswith(f"_{panel_id}")):
|
|
1010
|
-
panel_path = pp
|
|
1011
|
-
panel_name = Path(pp).name # e.g., "panel_A_twinx.pltz"
|
|
1012
|
-
is_zip = panel_is_zip[idx] if idx < len(panel_is_zip) else False
|
|
1013
|
-
break
|
|
1014
|
-
|
|
1015
|
-
if not panel_path:
|
|
1016
|
-
print(f"Could not find panel path for id={panel_id}, available: {[Path(p).stem for p in panel_paths]}")
|
|
1017
|
-
continue
|
|
1018
|
-
|
|
1019
|
-
# Check for layout overrides (from layout.json or in-memory)
|
|
1020
|
-
override = layout_overrides.get(panel_name, {})
|
|
1021
|
-
override_pos = override.get("position", {})
|
|
1022
|
-
override_size = override.get("size", {})
|
|
1023
|
-
|
|
1024
|
-
# Use override positions if available, otherwise use spec
|
|
1025
|
-
x_mm = override_pos.get("x_mm", pos.get("x_mm", 0))
|
|
1026
|
-
y_mm = override_pos.get("y_mm", pos.get("y_mm", 0))
|
|
1027
|
-
w_mm = override_size.get("width_mm", size.get("width_mm", 60))
|
|
1028
|
-
h_mm = override_size.get("height_mm", size.get("height_mm", 40))
|
|
1029
|
-
|
|
1030
|
-
x_frac = x_mm / fig_width_mm
|
|
1031
|
-
y_frac = 1 - (y_mm + h_mm) / fig_height_mm
|
|
1032
|
-
w_frac = w_mm / fig_width_mm
|
|
1033
|
-
h_frac = h_mm / fig_height_mm
|
|
1034
|
-
|
|
1035
|
-
# Load panel preview image
|
|
1036
|
-
try:
|
|
1037
|
-
img_loaded = False
|
|
1038
|
-
# Exclusion patterns for preview selection
|
|
1039
|
-
exclude_patterns = ['hitmap', 'overview', 'thumb', 'preview']
|
|
1040
|
-
|
|
1041
|
-
if is_zip:
|
|
1042
|
-
with ZipBundle(panel_path, mode="r") as pltz_bundle:
|
|
1043
|
-
# Find PNG in exports (exclude hitmap, overview, thumbnails)
|
|
1044
|
-
import zipfile
|
|
1045
|
-
with zipfile.ZipFile(panel_path, 'r') as zf:
|
|
1046
|
-
png_files = [n for n in zf.namelist()
|
|
1047
|
-
if n.endswith('.png')
|
|
1048
|
-
and 'exports/' in n
|
|
1049
|
-
and not any(p in n.lower() for p in exclude_patterns)]
|
|
1050
|
-
if png_files:
|
|
1051
|
-
# Use first matching PNG
|
|
1052
|
-
preview_path = png_files[0]
|
|
1053
|
-
# Extract the path relative to .d directory
|
|
1054
|
-
if '.pltz.d/' in preview_path:
|
|
1055
|
-
preview_path = preview_path.split('.pltz.d/')[-1]
|
|
1056
|
-
try:
|
|
1057
|
-
img_data = pltz_bundle.read_bytes(preview_path)
|
|
1058
|
-
img = Image.open(io.BytesIO(img_data))
|
|
1059
|
-
ax = fig.add_axes([x_frac, y_frac, w_frac, h_frac])
|
|
1060
|
-
ax.imshow(np.array(img))
|
|
1061
|
-
ax.axis('off')
|
|
1062
|
-
img_loaded = True
|
|
1063
|
-
except Exception as e:
|
|
1064
|
-
print(f"Could not read {preview_path}: {e}")
|
|
1065
|
-
else:
|
|
1066
|
-
# Directory-based pltz
|
|
1067
|
-
pltz_dir = Path(panel_path)
|
|
1068
|
-
exports_dir = pltz_dir / "exports"
|
|
1069
|
-
if exports_dir.exists():
|
|
1070
|
-
for png_file in exports_dir.glob("*.png"):
|
|
1071
|
-
name_lower = png_file.name.lower()
|
|
1072
|
-
if not any(p in name_lower for p in exclude_patterns):
|
|
1073
|
-
img = Image.open(png_file)
|
|
1074
|
-
ax = fig.add_axes([x_frac, y_frac, w_frac, h_frac])
|
|
1075
|
-
ax.imshow(np.array(img))
|
|
1076
|
-
ax.axis('off')
|
|
1077
|
-
img_loaded = True
|
|
1078
|
-
break
|
|
1079
|
-
if not img_loaded:
|
|
1080
|
-
print(f"No preview found for panel {panel_id}")
|
|
1081
|
-
except Exception as e:
|
|
1082
|
-
print(f"Could not load panel {panel_id}: {e}")
|
|
1083
|
-
|
|
1084
|
-
# Draw panel letter
|
|
1085
|
-
if panel_id and len(panel_id) <= 2: # Only for short IDs like A, B, C...
|
|
1086
|
-
# Position letter at top-left corner of panel
|
|
1087
|
-
letter_x = x_frac + 0.01
|
|
1088
|
-
letter_y = y_frac + h_frac - 0.02
|
|
1089
|
-
fig.text(letter_x, letter_y, panel_id,
|
|
1090
|
-
fontsize=14, fontweight='bold', color='black',
|
|
1091
|
-
ha='left', va='top',
|
|
1092
|
-
transform=fig.transFigure,
|
|
1093
|
-
bbox=dict(boxstyle='square,pad=0.1',
|
|
1094
|
-
facecolor='white', edgecolor='none', alpha=0.8))
|
|
1095
|
-
|
|
1096
|
-
buf = io.BytesIO()
|
|
1097
|
-
fig.savefig(buf, format=fmt if fmt != "jpg" else "jpeg",
|
|
1098
|
-
dpi=dpi, bbox_inches="tight", facecolor="white",
|
|
1099
|
-
pad_inches=0.02)
|
|
1100
|
-
plt.close(fig)
|
|
1101
|
-
buf.seek(0)
|
|
1102
|
-
|
|
1103
|
-
return send_file(
|
|
1104
|
-
buf,
|
|
1105
|
-
mimetype=mime_types[fmt],
|
|
1106
|
-
as_attachment=True,
|
|
1107
|
-
download_name=f"{figure_name}.{fmt}"
|
|
1108
|
-
)
|
|
1109
|
-
|
|
1110
|
-
# For single pltz files, render from csv_data
|
|
1111
|
-
from ._renderer import render_preview_with_bboxes
|
|
1112
|
-
import matplotlib
|
|
1113
|
-
matplotlib.use('Agg')
|
|
1114
|
-
import matplotlib.pyplot as plt
|
|
1115
|
-
|
|
1116
|
-
figure_name = "figure"
|
|
1117
|
-
if editor.json_path:
|
|
1118
|
-
figure_name = Path(editor.json_path).stem
|
|
1119
|
-
|
|
1120
|
-
img_data, _, _ = render_preview_with_bboxes(
|
|
1121
|
-
editor.csv_data, editor.current_overrides,
|
|
1122
|
-
metadata=editor.metadata,
|
|
1123
|
-
dark_mode=False,
|
|
1124
|
-
)
|
|
1125
|
-
|
|
1126
|
-
if fmt == "png":
|
|
1127
|
-
import base64
|
|
1128
|
-
content = base64.b64decode(img_data)
|
|
1129
|
-
buf = io.BytesIO(content)
|
|
1130
|
-
return send_file(
|
|
1131
|
-
buf,
|
|
1132
|
-
mimetype=mime_types[fmt],
|
|
1133
|
-
as_attachment=True,
|
|
1134
|
-
download_name=f"{figure_name}.{fmt}"
|
|
1135
|
-
)
|
|
1136
|
-
|
|
1137
|
-
# For other formats, re-render
|
|
1138
|
-
from ._plotter import plot_from_csv
|
|
1139
|
-
fig, ax = plt.subplots(figsize=(8, 6))
|
|
1140
|
-
plot_from_csv(ax, editor.csv_data, editor.current_overrides)
|
|
1141
|
-
|
|
1142
|
-
buf = io.BytesIO()
|
|
1143
|
-
dpi = 150 if fmt in ["jpeg", "jpg"] else 300
|
|
1144
|
-
fig.savefig(buf, format=fmt if fmt != "jpg" else "jpeg",
|
|
1145
|
-
dpi=dpi, bbox_inches="tight",
|
|
1146
|
-
facecolor="white" if fmt in ["jpeg", "jpg"] else None)
|
|
1147
|
-
plt.close(fig)
|
|
1148
|
-
buf.seek(0)
|
|
1149
|
-
|
|
1150
|
-
return send_file(
|
|
1151
|
-
buf,
|
|
1152
|
-
mimetype=mime_types[fmt],
|
|
1153
|
-
as_attachment=True,
|
|
1154
|
-
download_name=f"{figure_name}.{fmt}"
|
|
1155
|
-
)
|
|
1156
|
-
|
|
1157
|
-
except Exception as e:
|
|
1158
|
-
import traceback
|
|
1159
|
-
return f"Error: {str(e)}\n{traceback.format_exc()}", 500
|
|
1160
|
-
|
|
1161
|
-
@app.route("/download_figz")
|
|
1162
|
-
def download_figz():
|
|
1163
|
-
"""Download as figz bundle (re-editable format)."""
|
|
1164
|
-
try:
|
|
1165
|
-
if not editor.panel_info:
|
|
1166
|
-
return "No panel info available", 404
|
|
1167
|
-
|
|
1168
|
-
bundle_path = editor.panel_info.get("bundle_path")
|
|
1169
|
-
if not bundle_path:
|
|
1170
|
-
return "Bundle path not available", 404
|
|
1171
|
-
|
|
1172
|
-
from flask import send_file
|
|
1173
|
-
from pathlib import Path
|
|
1174
|
-
|
|
1175
|
-
# Send the figz file directly (it's already a pltz-compatible format)
|
|
1176
|
-
return send_file(
|
|
1177
|
-
bundle_path,
|
|
1178
|
-
mimetype="application/zip",
|
|
1179
|
-
as_attachment=True,
|
|
1180
|
-
download_name=Path(bundle_path).name
|
|
1181
|
-
)
|
|
1182
|
-
|
|
1183
|
-
except Exception as e:
|
|
1184
|
-
return str(e), 500
|
|
1185
|
-
|
|
1186
|
-
@app.route("/shutdown", methods=["POST"])
|
|
1187
|
-
def shutdown():
|
|
1188
|
-
"""Shutdown the server."""
|
|
1189
|
-
func = request.environ.get("werkzeug.server.shutdown")
|
|
1190
|
-
if func is None:
|
|
1191
|
-
raise RuntimeError("Not running with Werkzeug Server")
|
|
1192
|
-
func()
|
|
1193
|
-
return jsonify({"status": "shutdown"})
|
|
1194
|
-
|
|
1195
|
-
@app.route("/stats")
|
|
1196
|
-
def stats():
|
|
1197
|
-
"""Return statistical test results from figure metadata."""
|
|
1198
|
-
stats_data = editor.metadata.get("stats", [])
|
|
1199
|
-
stats_summary = editor.metadata.get("stats_summary", None)
|
|
1200
|
-
return jsonify({
|
|
1201
|
-
"stats": stats_data,
|
|
1202
|
-
"stats_summary": stats_summary,
|
|
1203
|
-
"has_stats": len(stats_data) > 0,
|
|
1204
|
-
})
|
|
1205
|
-
|
|
1206
|
-
# Open browser after short delay
|
|
1207
|
-
def open_browser():
|
|
1208
|
-
import time
|
|
1209
|
-
|
|
1210
|
-
time.sleep(0.5)
|
|
1211
|
-
webbrowser.open(f"http://127.0.0.1:{self.port}")
|
|
1212
|
-
|
|
1213
|
-
threading.Thread(target=open_browser, daemon=True).start()
|
|
1214
|
-
|
|
1215
|
-
print(f"Starting SciTeX Figure Editor at http://127.0.0.1:{self.port}")
|
|
1216
|
-
print("Press Ctrl+C to stop")
|
|
1217
|
-
|
|
1218
|
-
# Note: use_reloader=False because the reloader re-runs the entire script
|
|
1219
|
-
# which causes infinite loops when the demo generates figures
|
|
1220
|
-
# Templates are rebuilt on each page refresh anyway
|
|
1221
|
-
app.run(host="127.0.0.1", port=self.port, debug=False, use_reloader=False)
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
def _extract_bboxes_from_metadata(
|
|
1225
|
-
metadata: Dict[str, Any],
|
|
1226
|
-
display_width: Optional[float] = None,
|
|
1227
|
-
display_height: Optional[float] = None
|
|
1228
|
-
) -> Dict[str, Any]:
|
|
1229
|
-
"""Extract element bounding boxes from pltz metadata.
|
|
1230
|
-
|
|
1231
|
-
Builds bboxes from selectable_regions in the metadata for click detection.
|
|
1232
|
-
This allows the editor to highlight elements when clicked.
|
|
1233
|
-
|
|
1234
|
-
Coordinate system (new layered format):
|
|
1235
|
-
- selectable_regions bbox_px: Already in final image space (figure_px)
|
|
1236
|
-
- Display size: Actual displayed image size (PNG pixels or SVG viewBox)
|
|
1237
|
-
- Scale = display_size / figure_px (usually 1:1, but may differ for scaled display)
|
|
1238
|
-
|
|
1239
|
-
Parameters
|
|
1240
|
-
----------
|
|
1241
|
-
metadata : dict
|
|
1242
|
-
The pltz JSON metadata containing selectable_regions
|
|
1243
|
-
display_width : float, optional
|
|
1244
|
-
Actual display image width (from PNG size or SVG viewBox)
|
|
1245
|
-
display_height : float, optional
|
|
1246
|
-
Actual display image height (from PNG size or SVG viewBox)
|
|
1247
|
-
|
|
1248
|
-
Returns
|
|
1249
|
-
-------
|
|
1250
|
-
dict
|
|
1251
|
-
Mapping of element IDs to their bounding box coordinates (in display pixels)
|
|
1252
|
-
"""
|
|
1253
|
-
bboxes = {}
|
|
1254
|
-
selectable = metadata.get("selectable_regions", {})
|
|
1255
|
-
|
|
1256
|
-
# Figure dimensions from new layered format (bbox_px are in this space)
|
|
1257
|
-
figure_px = metadata.get("figure_px", [])
|
|
1258
|
-
if isinstance(figure_px, list) and len(figure_px) >= 2:
|
|
1259
|
-
fig_width = figure_px[0]
|
|
1260
|
-
fig_height = figure_px[1]
|
|
1261
|
-
else:
|
|
1262
|
-
# Fallback for old format: try hit_regions.path_data.figure
|
|
1263
|
-
hit_regions = metadata.get("hit_regions", {})
|
|
1264
|
-
path_data = hit_regions.get("path_data", {})
|
|
1265
|
-
orig_fig = path_data.get("figure", {})
|
|
1266
|
-
fig_width = orig_fig.get("width_px", 944)
|
|
1267
|
-
fig_height = orig_fig.get("height_px", 803)
|
|
1268
|
-
|
|
1269
|
-
# Use actual display dimensions if provided, else use figure_px
|
|
1270
|
-
if display_width is None:
|
|
1271
|
-
display_width = fig_width
|
|
1272
|
-
if display_height is None:
|
|
1273
|
-
display_height = fig_height
|
|
1274
|
-
|
|
1275
|
-
# Scale factor: display / figure_px
|
|
1276
|
-
# Usually 1:1 since display is the same PNG, but may differ for scaled display
|
|
1277
|
-
scale_x = display_width / fig_width if fig_width > 0 else 1
|
|
1278
|
-
scale_y = display_height / fig_height if fig_height > 0 else 1
|
|
1279
|
-
|
|
1280
|
-
# Helper to convert coords to display pixels
|
|
1281
|
-
def to_display_bbox(bbox, is_list=True):
|
|
1282
|
-
"""Convert bbox to display pixels (apply scaling if display != figure_px).
|
|
1283
|
-
|
|
1284
|
-
Parameters
|
|
1285
|
-
----------
|
|
1286
|
-
bbox : list or dict
|
|
1287
|
-
Bbox coordinates [x0, y0, x1, y1] or dict with keys
|
|
1288
|
-
is_list : bool
|
|
1289
|
-
Whether bbox is a list (True) or dict (False)
|
|
1290
|
-
"""
|
|
1291
|
-
if is_list:
|
|
1292
|
-
x0, y0, x1, y1 = bbox[0], bbox[1], bbox[2], bbox[3]
|
|
1293
|
-
else:
|
|
1294
|
-
x0 = bbox.get("x0", 0)
|
|
1295
|
-
y0 = bbox.get("y0", 0)
|
|
1296
|
-
x1 = bbox.get("x1", bbox.get("x0", 0) + bbox.get("width", 0))
|
|
1297
|
-
y1 = bbox.get("y1", bbox.get("y0", 0) + bbox.get("height", 0))
|
|
1298
|
-
|
|
1299
|
-
# Scale to display coords (usually 1:1)
|
|
1300
|
-
disp_x0 = x0 * scale_x
|
|
1301
|
-
disp_x1 = x1 * scale_x
|
|
1302
|
-
disp_y0 = y0 * scale_y
|
|
1303
|
-
disp_y1 = y1 * scale_y
|
|
1304
|
-
|
|
1305
|
-
return {
|
|
1306
|
-
"x0": disp_x0,
|
|
1307
|
-
"y0": disp_y0,
|
|
1308
|
-
"x1": disp_x1,
|
|
1309
|
-
"y1": disp_y1,
|
|
1310
|
-
"x": disp_x0,
|
|
1311
|
-
"y": disp_y0,
|
|
1312
|
-
"width": disp_x1 - disp_x0,
|
|
1313
|
-
"height": disp_y1 - disp_y0,
|
|
1314
|
-
}
|
|
1315
|
-
|
|
1316
|
-
# Extract from selectable_regions.axes
|
|
1317
|
-
axes_regions = selectable.get("axes", [])
|
|
1318
|
-
for ax_idx, ax in enumerate(axes_regions):
|
|
1319
|
-
ax_key = f"ax_{ax_idx:02d}"
|
|
1320
|
-
|
|
1321
|
-
# Title
|
|
1322
|
-
title = ax.get("title", {})
|
|
1323
|
-
if title and "bbox_px" in title:
|
|
1324
|
-
bbox_disp = to_display_bbox(title["bbox_px"])
|
|
1325
|
-
bboxes[f"{ax_key}_title"] = {
|
|
1326
|
-
**bbox_disp,
|
|
1327
|
-
"type": "title",
|
|
1328
|
-
"text": title.get("text", ""),
|
|
1329
|
-
}
|
|
1330
|
-
|
|
1331
|
-
# X label
|
|
1332
|
-
xlabel = ax.get("xlabel", {})
|
|
1333
|
-
if xlabel and "bbox_px" in xlabel:
|
|
1334
|
-
bbox_disp = to_display_bbox(xlabel["bbox_px"])
|
|
1335
|
-
bboxes[f"{ax_key}_xlabel"] = {
|
|
1336
|
-
**bbox_disp,
|
|
1337
|
-
"type": "xlabel",
|
|
1338
|
-
"text": xlabel.get("text", ""),
|
|
1339
|
-
}
|
|
1340
|
-
|
|
1341
|
-
# Y label
|
|
1342
|
-
ylabel = ax.get("ylabel", {})
|
|
1343
|
-
if ylabel and "bbox_px" in ylabel:
|
|
1344
|
-
bbox_disp = to_display_bbox(ylabel["bbox_px"])
|
|
1345
|
-
bboxes[f"{ax_key}_ylabel"] = {
|
|
1346
|
-
**bbox_disp,
|
|
1347
|
-
"type": "ylabel",
|
|
1348
|
-
"text": ylabel.get("text", ""),
|
|
1349
|
-
}
|
|
1350
|
-
|
|
1351
|
-
# Legend
|
|
1352
|
-
legend = ax.get("legend", {})
|
|
1353
|
-
if legend and "bbox_px" in legend:
|
|
1354
|
-
bbox_disp = to_display_bbox(legend["bbox_px"])
|
|
1355
|
-
bboxes[f"{ax_key}_legend"] = {
|
|
1356
|
-
**bbox_disp,
|
|
1357
|
-
"type": "legend",
|
|
1358
|
-
}
|
|
1359
|
-
|
|
1360
|
-
# X-axis spine
|
|
1361
|
-
xaxis = ax.get("xaxis", {})
|
|
1362
|
-
if xaxis:
|
|
1363
|
-
spine = xaxis.get("spine", {})
|
|
1364
|
-
if spine and "bbox_px" in spine:
|
|
1365
|
-
bbox_disp = to_display_bbox(spine["bbox_px"])
|
|
1366
|
-
bboxes[f"{ax_key}_xaxis_spine"] = {
|
|
1367
|
-
**bbox_disp,
|
|
1368
|
-
"type": "xaxis",
|
|
1369
|
-
}
|
|
1370
|
-
|
|
1371
|
-
# Y-axis spine
|
|
1372
|
-
yaxis = ax.get("yaxis", {})
|
|
1373
|
-
if yaxis:
|
|
1374
|
-
spine = yaxis.get("spine", {})
|
|
1375
|
-
if spine and "bbox_px" in spine:
|
|
1376
|
-
bbox_disp = to_display_bbox(spine["bbox_px"])
|
|
1377
|
-
bboxes[f"{ax_key}_yaxis_spine"] = {
|
|
1378
|
-
**bbox_disp,
|
|
1379
|
-
"type": "yaxis",
|
|
1380
|
-
}
|
|
1381
|
-
|
|
1382
|
-
# Extract traces from artists (top-level in new format, or hit_regions.path_data in old)
|
|
1383
|
-
artists = metadata.get("artists", [])
|
|
1384
|
-
if not artists:
|
|
1385
|
-
# Fallback for old format
|
|
1386
|
-
hit_regions = metadata.get("hit_regions", {})
|
|
1387
|
-
path_data = hit_regions.get("path_data", {})
|
|
1388
|
-
artists = path_data.get("artists", [])
|
|
1389
|
-
|
|
1390
|
-
for artist in artists:
|
|
1391
|
-
artist_id = artist.get("id", 0)
|
|
1392
|
-
artist_type = artist.get("type", "line")
|
|
1393
|
-
bbox_px = artist.get("bbox_px", {})
|
|
1394
|
-
if bbox_px:
|
|
1395
|
-
bbox_disp = to_display_bbox(bbox_px, is_list=False)
|
|
1396
|
-
trace_entry = {
|
|
1397
|
-
**bbox_disp,
|
|
1398
|
-
"type": artist_type,
|
|
1399
|
-
"label": artist.get("label", f"Trace {artist_id}"),
|
|
1400
|
-
"element_type": artist_type,
|
|
1401
|
-
}
|
|
1402
|
-
|
|
1403
|
-
# Include scaled path points for line proximity detection
|
|
1404
|
-
path_px = artist.get("path_px", [])
|
|
1405
|
-
if path_px:
|
|
1406
|
-
scaled_points = [
|
|
1407
|
-
[pt[0] * scale_x, pt[1] * scale_y]
|
|
1408
|
-
for pt in path_px if len(pt) >= 2
|
|
1409
|
-
]
|
|
1410
|
-
trace_entry["points"] = scaled_points
|
|
1411
|
-
|
|
1412
|
-
bboxes[f"trace_{artist_id}"] = trace_entry
|
|
1413
|
-
|
|
1414
|
-
# Add metadata for JavaScript to understand the coordinate system
|
|
1415
|
-
bboxes["_meta"] = {
|
|
1416
|
-
"display_width": display_width,
|
|
1417
|
-
"display_height": display_height,
|
|
1418
|
-
"figure_px_width": fig_width,
|
|
1419
|
-
"figure_px_height": fig_height,
|
|
1420
|
-
"scale_x": scale_x,
|
|
1421
|
-
"scale_y": scale_y,
|
|
1422
|
-
# Note: With new layered format, bbox_px are already in final image space
|
|
1423
|
-
# so scale is typically 1:1 (unless display is resized)
|
|
1424
|
-
}
|
|
1425
|
-
|
|
1426
|
-
return bboxes
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
# EOF
|