PyMieSimX 0.2.0__tar.gz → 0.2.2__tar.gz
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.
Potentially problematic release.
This version of PyMieSimX might be problematic. Click here for more details.
- {pymiesimx-0.2.0 → pymiesimx-0.2.2}/PKG-INFO +16 -1
- {pymiesimx-0.2.0 → pymiesimx-0.2.2}/PyMieSimX/gui/assets/dashboard.css +114 -38
- pymiesimx-0.2.2/PyMieSimX/gui/assets/simulation-sharing.js +36 -0
- {pymiesimx-0.2.0 → pymiesimx-0.2.2}/PyMieSimX/gui/callbacks.py +102 -25
- {pymiesimx-0.2.0 → pymiesimx-0.2.2}/PyMieSimX/gui/components/cards.py +8 -0
- {pymiesimx-0.2.0 → pymiesimx-0.2.2}/PyMieSimX/gui/interface.py +5 -2
- {pymiesimx-0.2.0 → pymiesimx-0.2.2}/PyMieSimX/gui/layout.py +1 -0
- {pymiesimx-0.2.0 → pymiesimx-0.2.2}/PyMieSimX/gui/pages/experiment/page.py +12 -3
- {pymiesimx-0.2.0 → pymiesimx-0.2.2}/PyMieSimX/gui/pages/home.py +13 -5
- {pymiesimx-0.2.0 → pymiesimx-0.2.2}/PyMieSimX/gui/pages/population.py +23 -13
- {pymiesimx-0.2.0 → pymiesimx-0.2.2}/PyMieSimX/gui/pages/single/page.py +13 -3
- pymiesimx-0.2.2/PyMieSimX/gui/seo.py +91 -0
- pymiesimx-0.2.2/PyMieSimX/gui/sharing.py +332 -0
- {pymiesimx-0.2.0 → pymiesimx-0.2.2}/PyMieSimX.egg-info/PKG-INFO +16 -1
- {pymiesimx-0.2.0 → pymiesimx-0.2.2}/PyMieSimX.egg-info/SOURCES.txt +5 -0
- {pymiesimx-0.2.0 → pymiesimx-0.2.2}/PyMieSimX.egg-info/scm_file_list.json +5 -0
- pymiesimx-0.2.2/PyMieSimX.egg-info/scm_version.json +8 -0
- {pymiesimx-0.2.0 → pymiesimx-0.2.2}/README.rst +15 -0
- {pymiesimx-0.2.0 → pymiesimx-0.2.2}/tests/browser/test_dashboard.py +287 -2
- pymiesimx-0.2.2/tests/gui/test_seo.py +67 -0
- pymiesimx-0.2.2/tests/gui/test_sharing.py +171 -0
- pymiesimx-0.2.0/PyMieSimX.egg-info/scm_version.json +0 -8
- {pymiesimx-0.2.0 → pymiesimx-0.2.2}/.coveragerc +0 -0
- {pymiesimx-0.2.0 → pymiesimx-0.2.2}/.flake8 +0 -0
- {pymiesimx-0.2.0 → pymiesimx-0.2.2}/.github/dependabot.yml +0 -0
- {pymiesimx-0.2.0 → pymiesimx-0.2.2}/.github/workflows/deploy_PyPi.yml +0 -0
- {pymiesimx-0.2.0 → pymiesimx-0.2.2}/.github/workflows/deploy_anaconda.yml +0 -0
- {pymiesimx-0.2.0 → pymiesimx-0.2.2}/.github/workflows/deploy_coverage.yml +0 -0
- {pymiesimx-0.2.0 → pymiesimx-0.2.2}/.github/workflows/quality.yml +0 -0
- {pymiesimx-0.2.0 → pymiesimx-0.2.2}/.github/workflows/tests.yml +0 -0
- {pymiesimx-0.2.0 → pymiesimx-0.2.2}/.gitignore +0 -0
- {pymiesimx-0.2.0 → pymiesimx-0.2.2}/.pre-commit-config.yaml +0 -0
- {pymiesimx-0.2.0 → pymiesimx-0.2.2}/AGENTS.md +0 -0
- {pymiesimx-0.2.0 → pymiesimx-0.2.2}/CONTRIBUTING.md +0 -0
- {pymiesimx-0.2.0 → pymiesimx-0.2.2}/Dockerfile +0 -0
- {pymiesimx-0.2.0 → pymiesimx-0.2.2}/LICENSE +0 -0
- {pymiesimx-0.2.0 → pymiesimx-0.2.2}/PyMieSimX/__init__.py +0 -0
- {pymiesimx-0.2.0 → pymiesimx-0.2.2}/PyMieSimX/__main__.py +0 -0
- {pymiesimx-0.2.0 → pymiesimx-0.2.2}/PyMieSimX/api.py +0 -0
- {pymiesimx-0.2.0 → pymiesimx-0.2.2}/PyMieSimX/application/__init__.py +0 -0
- {pymiesimx-0.2.0 → pymiesimx-0.2.2}/PyMieSimX/application/wsgi.py +0 -0
- {pymiesimx-0.2.0 → pymiesimx-0.2.2}/PyMieSimX/gui/__init__.py +0 -0
- {pymiesimx-0.2.0 → pymiesimx-0.2.2}/PyMieSimX/gui/admin.py +0 -0
- {pymiesimx-0.2.0 → pymiesimx-0.2.2}/PyMieSimX/gui/assets/card-state.js +0 -0
- {pymiesimx-0.2.0 → pymiesimx-0.2.2}/PyMieSimX/gui/assets/default-settings.json +0 -0
- {pymiesimx-0.2.0 → pymiesimx-0.2.2}/PyMieSimX/gui/assets/home-spf-radial.gif +0 -0
- {pymiesimx-0.2.0 → pymiesimx-0.2.2}/PyMieSimX/gui/assets/home-spf-radial.png +0 -0
- {pymiesimx-0.2.0 → pymiesimx-0.2.2}/PyMieSimX/gui/assets/logo.png +0 -0
- {pymiesimx-0.2.0 → pymiesimx-0.2.2}/PyMieSimX/gui/assets/materials-stock.json +0 -0
- {pymiesimx-0.2.0 → pymiesimx-0.2.2}/PyMieSimX/gui/assets/pymiesim-favicon.svg +0 -0
- {pymiesimx-0.2.0 → pymiesimx-0.2.2}/PyMieSimX/gui/assets/pymiesim-logo-dark.svg +0 -0
- {pymiesimx-0.2.0 → pymiesimx-0.2.2}/PyMieSimX/gui/assets/pymiesim-logo.svg +0 -0
- {pymiesimx-0.2.0 → pymiesimx-0.2.2}/PyMieSimX/gui/callback_helpers.py +0 -0
- {pymiesimx-0.2.0 → pymiesimx-0.2.2}/PyMieSimX/gui/components/__init__.py +0 -0
- {pymiesimx-0.2.0 → pymiesimx-0.2.2}/PyMieSimX/gui/computation.py +0 -0
- {pymiesimx-0.2.0 → pymiesimx-0.2.2}/PyMieSimX/gui/defaults.py +0 -0
- {pymiesimx-0.2.0 → pymiesimx-0.2.2}/PyMieSimX/gui/experiment_service.py +0 -0
- {pymiesimx-0.2.0 → pymiesimx-0.2.2}/PyMieSimX/gui/jobs.py +0 -0
- {pymiesimx-0.2.0 → pymiesimx-0.2.2}/PyMieSimX/gui/material_catalog.py +0 -0
- {pymiesimx-0.2.0 → pymiesimx-0.2.2}/PyMieSimX/gui/models.py +0 -0
- {pymiesimx-0.2.0 → pymiesimx-0.2.2}/PyMieSimX/gui/pages/__init__.py +0 -0
- {pymiesimx-0.2.0 → pymiesimx-0.2.2}/PyMieSimX/gui/pages/citation.py +0 -0
- {pymiesimx-0.2.0 → pymiesimx-0.2.2}/PyMieSimX/gui/pages/documentation.py +0 -0
- {pymiesimx-0.2.0 → pymiesimx-0.2.2}/PyMieSimX/gui/pages/experiment/__init__.py +0 -0
- {pymiesimx-0.2.0 → pymiesimx-0.2.2}/PyMieSimX/gui/pages/experiment/sections/__init__.py +0 -0
- {pymiesimx-0.2.0 → pymiesimx-0.2.2}/PyMieSimX/gui/pages/experiment/sections/configuration.py +0 -0
- {pymiesimx-0.2.0 → pymiesimx-0.2.2}/PyMieSimX/gui/pages/field_syntax.py +0 -0
- {pymiesimx-0.2.0 → pymiesimx-0.2.2}/PyMieSimX/gui/pages/install_local.py +0 -0
- {pymiesimx-0.2.0 → pymiesimx-0.2.2}/PyMieSimX/gui/pages/sellmeier.py +0 -0
- {pymiesimx-0.2.0 → pymiesimx-0.2.2}/PyMieSimX/gui/pages/settings.py +0 -0
- {pymiesimx-0.2.0 → pymiesimx-0.2.2}/PyMieSimX/gui/pages/single/__init__.py +0 -0
- {pymiesimx-0.2.0 → pymiesimx-0.2.2}/PyMieSimX/gui/pages/single/sections/__init__.py +0 -0
- {pymiesimx-0.2.0 → pymiesimx-0.2.2}/PyMieSimX/gui/pages/single/sections/representations.py +0 -0
- {pymiesimx-0.2.0 → pymiesimx-0.2.2}/PyMieSimX/gui/pages/single/sections/setup.py +0 -0
- {pymiesimx-0.2.0 → pymiesimx-0.2.2}/PyMieSimX/gui/parsing.py +0 -0
- {pymiesimx-0.2.0 → pymiesimx-0.2.2}/PyMieSimX/gui/plotting.py +0 -0
- {pymiesimx-0.2.0 → pymiesimx-0.2.2}/PyMieSimX/gui/population_service.py +0 -0
- {pymiesimx-0.2.0 → pymiesimx-0.2.2}/PyMieSimX/gui/schemas.py +0 -0
- {pymiesimx-0.2.0 → pymiesimx-0.2.2}/PyMieSimX/gui/services.py +0 -0
- {pymiesimx-0.2.0 → pymiesimx-0.2.2}/PyMieSimX/gui/single_service.py +0 -0
- {pymiesimx-0.2.0 → pymiesimx-0.2.2}/PyMieSimX/gui/usage_metrics.py +0 -0
- {pymiesimx-0.2.0 → pymiesimx-0.2.2}/PyMieSimX/gui/validation.py +0 -0
- {pymiesimx-0.2.0 → pymiesimx-0.2.2}/PyMieSimX.egg-info/dependency_links.txt +0 -0
- {pymiesimx-0.2.0 → pymiesimx-0.2.2}/PyMieSimX.egg-info/entry_points.txt +0 -0
- {pymiesimx-0.2.0 → pymiesimx-0.2.2}/PyMieSimX.egg-info/requires.txt +0 -0
- {pymiesimx-0.2.0 → pymiesimx-0.2.2}/PyMieSimX.egg-info/top_level.txt +0 -0
- {pymiesimx-0.2.0 → pymiesimx-0.2.2}/conda.recipe/meta.yaml +0 -0
- {pymiesimx-0.2.0 → pymiesimx-0.2.2}/makefile +0 -0
- {pymiesimx-0.2.0 → pymiesimx-0.2.2}/pyproject.toml +0 -0
- {pymiesimx-0.2.0 → pymiesimx-0.2.2}/pytest.ini +0 -0
- {pymiesimx-0.2.0 → pymiesimx-0.2.2}/setup.cfg +0 -0
- {pymiesimx-0.2.0 → pymiesimx-0.2.2}/tests/gui/cli.py +0 -0
- {pymiesimx-0.2.0 → pymiesimx-0.2.2}/tests/gui/test_admin.py +0 -0
- {pymiesimx-0.2.0 → pymiesimx-0.2.2}/tests/gui/test_api.py +0 -0
- {pymiesimx-0.2.0 → pymiesimx-0.2.2}/tests/gui/test_dash_callbacks.py +0 -0
- {pymiesimx-0.2.0 → pymiesimx-0.2.2}/tests/gui/test_home.py +0 -0
- {pymiesimx-0.2.0 → pymiesimx-0.2.2}/tests/gui/test_population.py +0 -0
- {pymiesimx-0.2.0 → pymiesimx-0.2.2}/tests/gui/test_services.py +0 -0
- {pymiesimx-0.2.0 → pymiesimx-0.2.2}/tests/gui/test_usage_metrics.py +0 -0
- {pymiesimx-0.2.0 → pymiesimx-0.2.2}/tools/generate_home_spf.py +0 -0
- {pymiesimx-0.2.0 → pymiesimx-0.2.2}/tools/next_release_version.py +0 -0
- {pymiesimx-0.2.0 → pymiesimx-0.2.2}/tools/release_tag.py +0 -0
- {pymiesimx-0.2.0 → pymiesimx-0.2.2}/tools/verify_conda_artifact.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: PyMieSimX
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.2
|
|
4
4
|
Summary: Graphical Dash interface for PyMieSim.
|
|
5
5
|
Author-email: Martin Poinsinet de Sivry-Houle <martin.poinsinet.de.sivry@gmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -50,6 +50,21 @@ optical setups, run parameter sweeps, explore individual particles, and download
|
|
|
50
50
|
your results as CSV files. The hosted service provides direct access to the
|
|
51
51
|
graphical interface; local installation is also available below.
|
|
52
52
|
|
|
53
|
+
Sharing simulations
|
|
54
|
+
-------------------
|
|
55
|
+
|
|
56
|
+
Use **Copy simulation link** in Single Scatterer, Parameter Scan, or Ensemble
|
|
57
|
+
Optics to share the current setup. Links preserve the input values, sweep
|
|
58
|
+
ranges, selected measures, and plot settings without requiring an account.
|
|
59
|
+
Opening a link restores the setup and automatically runs the simulation once
|
|
60
|
+
the inputs and plot settings are ready. Later edits can be calculated with
|
|
61
|
+
**Run**. If clipboard access is unavailable, use **Show simulation
|
|
62
|
+
link** and copy the displayed URL.
|
|
63
|
+
|
|
64
|
+
Links store the configuration rather than a result snapshot. Recalculation
|
|
65
|
+
uses the installed solver and optical material data, so results can change
|
|
66
|
+
after those dependencies are updated.
|
|
67
|
+
|
|
53
68
|
Ensemble Optics
|
|
54
69
|
---------------
|
|
55
70
|
|
|
@@ -499,9 +499,9 @@ html .home-metric-tile { font-size: .92rem !important; }
|
|
|
499
499
|
.version-widget-label { font-size: .72rem; font-weight: 700; letter-spacing: .08em; text-transform: uppercase; opacity: .7; }
|
|
500
500
|
.version-widget-value { font-size: .92rem; font-weight: 700; }
|
|
501
501
|
|
|
502
|
-
html .home-page-hero { padding:
|
|
503
|
-
html .home-page-hero h1 { margin: 0 0
|
|
504
|
-
html .home-page-hero .hero-text { max-width:
|
|
502
|
+
html .home-page-hero { padding: 30px !important; border-radius: 15px; border-left: 5px solid rgba(13, 110, 253, .75); }
|
|
503
|
+
html .home-page-hero h1 { margin: 0 0 12px; letter-spacing: -.045em; font-family: inherit !important; font-size: 2.55rem !important; font-weight: 800; line-height: 1.05; }
|
|
504
|
+
html .home-page-hero .hero-text { max-width: 460px; margin: 0; font-size: 1rem !important; line-height: 1.6; opacity: .7; }
|
|
505
505
|
|
|
506
506
|
html .home-metrics-card > .card-body {
|
|
507
507
|
display: grid;
|
|
@@ -3869,76 +3869,85 @@ html .single-workspace .representation-card {
|
|
|
3869
3869
|
display: flex;
|
|
3870
3870
|
align-items: center;
|
|
3871
3871
|
justify-content: space-between;
|
|
3872
|
-
gap:
|
|
3872
|
+
gap: 28px;
|
|
3873
3873
|
flex-wrap: wrap;
|
|
3874
3874
|
}
|
|
3875
3875
|
|
|
3876
3876
|
.home-hero-copy {
|
|
3877
|
-
flex: 1 1
|
|
3877
|
+
flex: 1 1 300px;
|
|
3878
3878
|
min-width: 0;
|
|
3879
3879
|
}
|
|
3880
3880
|
|
|
3881
3881
|
.home-hero-actions {
|
|
3882
3882
|
display: flex;
|
|
3883
3883
|
align-items: center;
|
|
3884
|
-
justify-content: flex-end;
|
|
3885
3884
|
flex-wrap: wrap;
|
|
3886
|
-
gap:
|
|
3885
|
+
gap: 26px;
|
|
3887
3886
|
margin-left: auto;
|
|
3888
3887
|
}
|
|
3889
3888
|
|
|
3890
3889
|
.home-visit-metric {
|
|
3891
3890
|
display: flex;
|
|
3892
3891
|
flex-direction: column;
|
|
3893
|
-
|
|
3894
|
-
|
|
3895
|
-
|
|
3896
|
-
border: 1px solid #cddcec;
|
|
3897
|
-
border-radius: 9px;
|
|
3898
|
-
background: #f3f7fc;
|
|
3899
|
-
color: #2f4358;
|
|
3900
|
-
}
|
|
3901
|
-
|
|
3902
|
-
.home-visit-label {
|
|
3903
|
-
font-size: .72rem;
|
|
3904
|
-
font-weight: 600;
|
|
3892
|
+
gap: 4px;
|
|
3893
|
+
padding-right: 26px;
|
|
3894
|
+
border-right: 1px solid #e5e7eb;
|
|
3905
3895
|
}
|
|
3906
3896
|
|
|
3907
3897
|
.home-visit-value {
|
|
3908
|
-
font-size: 1.
|
|
3898
|
+
font-size: 1.9rem;
|
|
3899
|
+
font-weight: 750;
|
|
3909
3900
|
line-height: 1.2;
|
|
3901
|
+
letter-spacing: -.04em;
|
|
3910
3902
|
font-variant-numeric: tabular-nums;
|
|
3911
3903
|
}
|
|
3912
3904
|
|
|
3905
|
+
.home-visit-label {
|
|
3906
|
+
font-size: .75rem;
|
|
3907
|
+
opacity: .6;
|
|
3908
|
+
}
|
|
3909
|
+
|
|
3913
3910
|
.home-support-actions {
|
|
3914
3911
|
display: flex;
|
|
3915
3912
|
flex-direction: column;
|
|
3916
|
-
gap:
|
|
3913
|
+
gap: 8px;
|
|
3917
3914
|
}
|
|
3918
3915
|
|
|
3919
3916
|
.home-support-button {
|
|
3920
3917
|
display: inline-flex;
|
|
3921
3918
|
align-items: center;
|
|
3922
|
-
gap:
|
|
3923
|
-
|
|
3924
|
-
padding:
|
|
3925
|
-
border: 1px solid #
|
|
3926
|
-
border-radius:
|
|
3927
|
-
background:
|
|
3928
|
-
color: #
|
|
3929
|
-
font-size: .
|
|
3930
|
-
font-weight:
|
|
3919
|
+
gap: 10px;
|
|
3920
|
+
min-height: 42px;
|
|
3921
|
+
padding: 8px 13px;
|
|
3922
|
+
border: 1px solid #e8d6ad;
|
|
3923
|
+
border-radius: 10px;
|
|
3924
|
+
background: #fcf5e7;
|
|
3925
|
+
color: #795520;
|
|
3926
|
+
font-size: .82rem;
|
|
3927
|
+
font-weight: 650;
|
|
3928
|
+
line-height: 1.4;
|
|
3931
3929
|
text-decoration: none;
|
|
3932
|
-
|
|
3933
|
-
box-shadow: 0 3px 9px rgba(151, 104, 23, .10);
|
|
3934
|
-
transition: background 150ms ease, box-shadow 150ms ease;
|
|
3930
|
+
transition: background 150ms ease, border-color 150ms ease, box-shadow 150ms ease;
|
|
3935
3931
|
}
|
|
3936
3932
|
|
|
3937
3933
|
.home-support-button:hover {
|
|
3938
|
-
background: #
|
|
3939
|
-
color: #
|
|
3934
|
+
background: #f8ecd2;
|
|
3935
|
+
border-color: #d7bd85;
|
|
3936
|
+
color: #674516;
|
|
3940
3937
|
text-decoration: none;
|
|
3941
|
-
box-shadow: 0
|
|
3938
|
+
box-shadow: 0 2px 6px rgba(112, 79, 25, .08);
|
|
3939
|
+
}
|
|
3940
|
+
|
|
3941
|
+
.home-star-button {
|
|
3942
|
+
background: #fff;
|
|
3943
|
+
border-color: #e5e7eb;
|
|
3944
|
+
color: #343b46;
|
|
3945
|
+
}
|
|
3946
|
+
|
|
3947
|
+
.home-star-button:hover {
|
|
3948
|
+
background: #f5f6f8;
|
|
3949
|
+
border-color: #cbd0d8;
|
|
3950
|
+
color: #343b46;
|
|
3942
3951
|
}
|
|
3943
3952
|
|
|
3944
3953
|
.home-support-button:focus-visible {
|
|
@@ -3947,8 +3956,47 @@ html .single-workspace .representation-card {
|
|
|
3947
3956
|
}
|
|
3948
3957
|
|
|
3949
3958
|
.home-support-icon {
|
|
3950
|
-
|
|
3951
|
-
|
|
3959
|
+
display: inline-flex;
|
|
3960
|
+
align-items: center;
|
|
3961
|
+
justify-content: center;
|
|
3962
|
+
width: 22px;
|
|
3963
|
+
font-size: 1.15rem;
|
|
3964
|
+
line-height: 1;
|
|
3965
|
+
}
|
|
3966
|
+
|
|
3967
|
+
.home-support-label {
|
|
3968
|
+
flex: 1;
|
|
3969
|
+
}
|
|
3970
|
+
|
|
3971
|
+
.home-support-arrow {
|
|
3972
|
+
font-size: 1rem;
|
|
3973
|
+
opacity: .45;
|
|
3974
|
+
}
|
|
3975
|
+
|
|
3976
|
+
@media (max-width: 575px) {
|
|
3977
|
+
html .home-page-hero {
|
|
3978
|
+
padding: 22px !important;
|
|
3979
|
+
gap: 22px;
|
|
3980
|
+
}
|
|
3981
|
+
|
|
3982
|
+
.home-hero-actions {
|
|
3983
|
+
justify-content: space-between;
|
|
3984
|
+
width: 100%;
|
|
3985
|
+
margin-left: 0;
|
|
3986
|
+
gap: 18px;
|
|
3987
|
+
}
|
|
3988
|
+
|
|
3989
|
+
.home-visit-metric {
|
|
3990
|
+
border-right: 0;
|
|
3991
|
+
padding-right: 0;
|
|
3992
|
+
flex-direction: row;
|
|
3993
|
+
align-items: baseline;
|
|
3994
|
+
gap: 10px;
|
|
3995
|
+
}
|
|
3996
|
+
|
|
3997
|
+
.home-support-actions {
|
|
3998
|
+
width: 100%;
|
|
3999
|
+
}
|
|
3952
4000
|
}
|
|
3953
4001
|
|
|
3954
4002
|
.admin-page {
|
|
@@ -4007,3 +4055,31 @@ html .single-workspace .representation-card {
|
|
|
4007
4055
|
.population-page .documentation-table td { overflow-wrap: anywhere; }
|
|
4008
4056
|
#population-status, #population-preview-error { margin-bottom: 12px; }
|
|
4009
4057
|
#population-results { overflow-x: auto; }
|
|
4058
|
+
.workspace-intro {
|
|
4059
|
+
margin: 0;
|
|
4060
|
+
color: var(--muted);
|
|
4061
|
+
font-size: .86rem;
|
|
4062
|
+
line-height: 1.6;
|
|
4063
|
+
}
|
|
4064
|
+
|
|
4065
|
+
.single-tab-content, .experiment-tab-content, .population-page { gap: 12px; }
|
|
4066
|
+
.page-hero.workspace-header {
|
|
4067
|
+
box-sizing: border-box;
|
|
4068
|
+
min-height: 76px;
|
|
4069
|
+
display: flex;
|
|
4070
|
+
flex-direction: column;
|
|
4071
|
+
justify-content: center;
|
|
4072
|
+
gap: 4px;
|
|
4073
|
+
}
|
|
4074
|
+
.workspace-header .hero-text { max-width: none; line-height: 1.6; min-height: 2lh; }
|
|
4075
|
+
@media (max-width: 700px) {
|
|
4076
|
+
.page-hero.workspace-header { min-height: 148px; }
|
|
4077
|
+
.workspace-header .eyebrow { min-height: 2lh; }
|
|
4078
|
+
.workspace-header .hero-text { min-height: 4lh; }
|
|
4079
|
+
}
|
|
4080
|
+
|
|
4081
|
+
.simulation-share-feedback { display: grid; gap: 8px; font-size: .86rem; color: var(--muted); }
|
|
4082
|
+
.simulation-share-actions { display: contents; }
|
|
4083
|
+
.simulation-share-feedback:empty { display: none; }
|
|
4084
|
+
.simulation-share-feedback .field-input { width: 100%; min-width: 0; }
|
|
4085
|
+
.simulation-link-notice { padding: 10px 14px; border: 1px solid var(--line); border-radius: 8px; color: var(--ink); }
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
window.dash_clientside = Object.assign({}, window.dash_clientside, {
|
|
2
|
+
simulation_sharing: {
|
|
3
|
+
auto_run: function(config, fieldIds, fieldValues, fieldClasses, settings) {
|
|
4
|
+
const unchanged = window.dash_clientside.no_update;
|
|
5
|
+
if (!config || config.started) return [unchanged, unchanged];
|
|
6
|
+
const values = Array.prototype.slice.call(arguments, 5);
|
|
7
|
+
const clicks = values.pop();
|
|
8
|
+
if (clicks) return [unchanged, Object.assign({}, config, {started: true})];
|
|
9
|
+
|
|
10
|
+
const actual = {};
|
|
11
|
+
config.control_names.forEach((name, index) => { actual[name] = values[index]; });
|
|
12
|
+
(fieldIds || []).forEach((id, index) => {
|
|
13
|
+
const ordered = {};
|
|
14
|
+
Object.keys(id).sort().forEach(key => { ordered[key] = id[key]; });
|
|
15
|
+
actual[JSON.stringify(ordered)] = fieldValues[index];
|
|
16
|
+
});
|
|
17
|
+
// Field classes depend on the existing validation callbacks; this
|
|
18
|
+
// also waits for dynamic fields and material-mode restoration.
|
|
19
|
+
if (!fieldClasses || fieldClasses.length < config.field_keys.length ||
|
|
20
|
+
!config.field_keys.every(key => Object.prototype.hasOwnProperty.call(actual, key))) {
|
|
21
|
+
return [unchanged, unchanged];
|
|
22
|
+
}
|
|
23
|
+
const same = (left, right) => JSON.stringify(left) === JSON.stringify(right);
|
|
24
|
+
if (!Object.entries(config.expected_controls).every(([key, value]) => same(actual[key], value))) {
|
|
25
|
+
return [unchanged, unchanged];
|
|
26
|
+
}
|
|
27
|
+
const plot = (settings || {})[config.workspace] || {};
|
|
28
|
+
if (!Object.entries(config.plot_settings).every(([key, value]) => same(plot[key], value))) {
|
|
29
|
+
return [unchanged, unchanged];
|
|
30
|
+
}
|
|
31
|
+
// Both outputs update together. Later edits and callback updates
|
|
32
|
+
// see the started flag and cannot launch a second automatic run.
|
|
33
|
+
return [1, Object.assign({}, config, {started: true})];
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
});
|
|
@@ -31,6 +31,7 @@ from PyMieSimX.gui.callback_helpers import (
|
|
|
31
31
|
from PyMieSimX.gui.jobs import experiment_jobs
|
|
32
32
|
from PyMieSimX.gui.validation import normalize_measures
|
|
33
33
|
from PyMieSimX.gui import usage_metrics
|
|
34
|
+
from PyMieSimX.gui.sharing import register_sharing_callbacks, read_simulation, shared_setup, shared_plot_settings, restore_controls
|
|
34
35
|
from PyMieSimX.gui.admin import build_admin_page, collect_admin_dashboard_data, dashboard_values, is_admin_access_granted
|
|
35
36
|
from PyMieSimX.gui.services import (
|
|
36
37
|
available_measures,
|
|
@@ -194,6 +195,21 @@ def register_callbacks(app: Dash, default_measure_options: list[str]) -> None:
|
|
|
194
195
|
def _route_pages(pathname: str | None, search: str | None, home_visits: int, experiment_runs: int, single_runs: int, theme_store: dict | None, plot_settings: dict | None):
|
|
195
196
|
"""Render only the selected route page inside the persistent shell."""
|
|
196
197
|
route = pathname or "/"
|
|
198
|
+
try:
|
|
199
|
+
setup = read_simulation(search, route)
|
|
200
|
+
share_message = "Shared setup loaded. This simulation runs automatically." if setup else None
|
|
201
|
+
except ValueError as error:
|
|
202
|
+
setup = None
|
|
203
|
+
share_message = str(error)
|
|
204
|
+
plot_settings = shared_plot_settings(plot_settings, setup)
|
|
205
|
+
|
|
206
|
+
def simulation_page(page):
|
|
207
|
+
from dash import html
|
|
208
|
+
restore_controls(page, setup)
|
|
209
|
+
if share_message:
|
|
210
|
+
page.children.insert(0, html.Div(share_message, role="status", className="simulation-link-notice"))
|
|
211
|
+
return build_page_with_footer(page)
|
|
212
|
+
|
|
197
213
|
active = {
|
|
198
214
|
"home": "sidebar-link",
|
|
199
215
|
"experiment": "sidebar-link",
|
|
@@ -233,13 +249,13 @@ def register_callbacks(app: Dash, default_measure_options: list[str]) -> None:
|
|
|
233
249
|
return build_page_with_footer(build_settings_page((theme_store or {}).get("theme", "light"), plot_settings or {})), *(active[key] for key in ("home", "experiment", "single", "documentation", "settings", "population")), home_visits
|
|
234
250
|
if route == "/single":
|
|
235
251
|
active["single"] += " active"
|
|
236
|
-
return
|
|
252
|
+
return simulation_page(build_single_page((plot_settings or {}).get("particle_explorer", {}), setup)), *(active[key] for key in ("home", "experiment", "single", "documentation", "settings", "population")), home_visits
|
|
237
253
|
if route == "/population":
|
|
238
254
|
active["population"] += " active"
|
|
239
|
-
return
|
|
255
|
+
return simulation_page(build_population_page()), *(active[key] for key in ("home", "experiment", "single", "documentation", "settings", "population")), home_visits
|
|
240
256
|
if route == "/experiment":
|
|
241
257
|
active["experiment"] += " active"
|
|
242
|
-
return
|
|
258
|
+
return simulation_page(build_experiment_page(default_measure_options, plot_settings or {}, setup)), *(active[key] for key in ("home", "experiment", "single", "documentation", "settings", "population")), home_visits
|
|
243
259
|
active["home"] += " active"
|
|
244
260
|
return build_page_with_footer(build_home_page(home_visits=home_visits)), *(active[key] for key in ("home", "experiment", "single", "documentation", "settings", "population")), home_visits
|
|
245
261
|
|
|
@@ -292,9 +308,12 @@ def register_callbacks(app: Dash, default_measure_options: list[str]) -> None:
|
|
|
292
308
|
Input("plot-experiment-legend", "value", allow_optional=True),
|
|
293
309
|
Input("plot-experiment-grid", "value", allow_optional=True),
|
|
294
310
|
State("plot-settings-store", "data"),
|
|
311
|
+
State("url", "search"),
|
|
312
|
+
State("url", "pathname"),
|
|
295
313
|
)
|
|
296
314
|
def _sync_plot_settings(*values):
|
|
297
|
-
|
|
315
|
+
search, path = values[-2:]
|
|
316
|
+
stored_settings = shared_plot_settings(values[-3], shared_setup(search, path))
|
|
298
317
|
particle_values = values[:9]
|
|
299
318
|
sweep_values = values[9:18]
|
|
300
319
|
single_local_values = values[18:24]
|
|
@@ -387,28 +406,43 @@ def register_callbacks(app: Dash, default_measure_options: list[str]) -> None:
|
|
|
387
406
|
|
|
388
407
|
return classes, errors
|
|
389
408
|
|
|
390
|
-
@app.callback(Output("source-fields", "children"), Input("source-type", "value"))
|
|
391
|
-
def _render_source_fields(source_type: str):
|
|
409
|
+
@app.callback(Output("source-fields", "children"), Input("source-type", "value"), State("url", "search"))
|
|
410
|
+
def _render_source_fields(source_type: str, search: str = ""):
|
|
392
411
|
LOGGER.debug("Rendering source fields for %s", source_type)
|
|
393
|
-
|
|
412
|
+
setup = shared_setup(search, "/experiment")
|
|
413
|
+
if setup and setup["controls"].get("source-type") != source_type:
|
|
414
|
+
setup = None
|
|
415
|
+
return restore_controls(render_fields("source", source_type), setup)
|
|
394
416
|
|
|
395
|
-
@app.callback(Output("scatterer-fields", "children"), Input("scatterer-type", "value"))
|
|
396
|
-
def _render_scatterer_fields(scatterer_type: str):
|
|
417
|
+
@app.callback(Output("scatterer-fields", "children"), Input("scatterer-type", "value"), State("url", "search"))
|
|
418
|
+
def _render_scatterer_fields(scatterer_type: str, search: str = ""):
|
|
397
419
|
LOGGER.debug("Rendering scatterer fields for %s", scatterer_type)
|
|
398
|
-
|
|
420
|
+
setup = shared_setup(search, "/experiment")
|
|
421
|
+
if setup and setup["controls"].get("scatterer-type") != scatterer_type:
|
|
422
|
+
setup = None
|
|
423
|
+
return restore_controls(render_fields("scatterer", scatterer_type), setup)
|
|
399
424
|
|
|
400
|
-
@app.callback(Output("detector-fields", "children"), Input("detector-type", "value"))
|
|
401
|
-
def _render_detector_fields(detector_type: str):
|
|
425
|
+
@app.callback(Output("detector-fields", "children"), Input("detector-type", "value"), State("url", "search"))
|
|
426
|
+
def _render_detector_fields(detector_type: str, search: str = ""):
|
|
402
427
|
LOGGER.debug("Rendering detector fields for %s", detector_type)
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
428
|
+
setup = shared_setup(search, "/experiment")
|
|
429
|
+
if setup and setup["controls"].get("detector-type") != detector_type:
|
|
430
|
+
setup = None
|
|
431
|
+
return restore_controls(render_fields("detector", detector_type), setup)
|
|
432
|
+
|
|
433
|
+
@app.callback(Output("single-source-fields", "children"), Input("single-source-type", "value"), State("url", "search"))
|
|
434
|
+
def _render_single_source_fields(source_type: str, search: str = ""):
|
|
435
|
+
setup = shared_setup(search, "/single")
|
|
436
|
+
if setup and setup["controls"].get("single-source-type") != source_type:
|
|
437
|
+
setup = None
|
|
438
|
+
return restore_controls(render_fields("single-source", source_type), setup)
|
|
439
|
+
|
|
440
|
+
@app.callback(Output("single-scatterer-fields", "children"), Input("single-scatterer-type", "value"), State("url", "search"))
|
|
441
|
+
def _render_single_scatterer_fields(scatterer_type: str, search: str = ""):
|
|
442
|
+
setup = shared_setup(search, "/single")
|
|
443
|
+
if setup and setup["controls"].get("single-scatterer-type") != scatterer_type:
|
|
444
|
+
setup = None
|
|
445
|
+
return restore_controls(render_fields("single-scatterer", scatterer_type), setup)
|
|
412
446
|
|
|
413
447
|
@app.callback(
|
|
414
448
|
Output({"kind": "material-toggle", "section": MATCH, "name": MATCH}, "className"),
|
|
@@ -478,6 +512,7 @@ def register_callbacks(app: Dash, default_measure_options: list[str]) -> None:
|
|
|
478
512
|
Input({"kind": "field", "section": "detector", "name": ALL}, "value"),
|
|
479
513
|
Input({"kind": "field", "section": "detector", "name": ALL}, "id"),
|
|
480
514
|
State("x-axis-select", "value"),
|
|
515
|
+
State("url", "search"),
|
|
481
516
|
)
|
|
482
517
|
def _update_x_axis_options(
|
|
483
518
|
source_type: str,
|
|
@@ -490,6 +525,7 @@ def register_callbacks(app: Dash, default_measure_options: list[str]) -> None:
|
|
|
490
525
|
detector_values: list[str],
|
|
491
526
|
detector_ids: list[dict[str, str]],
|
|
492
527
|
current_x_axis: str | None,
|
|
528
|
+
search: str = "",
|
|
493
529
|
):
|
|
494
530
|
LOGGER.debug(
|
|
495
531
|
"Updating x-axis options from form state source=%s scatterer=%s detector=%s current=%s",
|
|
@@ -509,6 +545,16 @@ def register_callbacks(app: Dash, default_measure_options: list[str]) -> None:
|
|
|
509
545
|
)
|
|
510
546
|
|
|
511
547
|
options = [{"label": field_name, "value": field_name} for field_name in variable_fields]
|
|
548
|
+
setup = shared_setup(search, "/experiment")
|
|
549
|
+
if setup:
|
|
550
|
+
desired_axis = setup["controls"].get("x-axis-select")
|
|
551
|
+
complete = (len(source_ids) == len(SOURCE_FIELDS[source_type])
|
|
552
|
+
and len(scatterer_ids) == len(SCATTERER_FIELDS[scatterer_type])
|
|
553
|
+
and len(detector_ids) == len(DETECTOR_FIELDS[detector_type]))
|
|
554
|
+
if current_x_axis is None or current_x_axis == desired_axis:
|
|
555
|
+
current_x_axis = desired_axis
|
|
556
|
+
if not complete:
|
|
557
|
+
return options, current_x_axis
|
|
512
558
|
value = current_x_axis if current_x_axis in variable_fields else (variable_fields[0] if variable_fields else None)
|
|
513
559
|
return options, value
|
|
514
560
|
|
|
@@ -835,12 +881,25 @@ def register_callbacks(app: Dash, default_measure_options: list[str]) -> None:
|
|
|
835
881
|
Input("single-representation", "value"),
|
|
836
882
|
Input("single-projection", "value"),
|
|
837
883
|
State("plot-settings-store", "data"),
|
|
884
|
+
State("url", "search"),
|
|
885
|
+
State("plot-single-x-scale", "value", allow_optional=True),
|
|
886
|
+
State("plot-single-y-scale", "value", allow_optional=True),
|
|
887
|
+
State("plot-single-font-size", "value", allow_optional=True),
|
|
888
|
+
State("plot-single-line-width", "value", allow_optional=True),
|
|
889
|
+
State("plot-single-legend", "value", allow_optional=True),
|
|
890
|
+
State("plot-single-grid", "value", allow_optional=True),
|
|
838
891
|
)
|
|
839
|
-
def _update_single_plot_options(representation: str, projection: str, plot_settings: dict | None):
|
|
892
|
+
def _update_single_plot_options(representation: str, projection: str, plot_settings: dict | None, search: str = "", *local_values):
|
|
893
|
+
setup = shared_setup(search, "/single")
|
|
894
|
+
plot_settings = shared_plot_settings(plot_settings, setup)
|
|
840
895
|
particle_settings = (plot_settings or {}).get("particle_explorer", plot_settings or {})
|
|
896
|
+
particle_settings = _merge_local_plot_values(particle_settings, local_values)
|
|
841
897
|
from PyMieSimX.gui.layout import _plot_options_card
|
|
842
898
|
|
|
843
|
-
|
|
899
|
+
card = _plot_options_card("single", particle_settings, representation, projection)
|
|
900
|
+
if setup:
|
|
901
|
+
restore_controls(card, {**setup, "controls": {}})
|
|
902
|
+
return card
|
|
844
903
|
|
|
845
904
|
@app.callback(
|
|
846
905
|
Output("experiment-plot-options-container", "children"),
|
|
@@ -848,22 +907,38 @@ def register_callbacks(app: Dash, default_measure_options: list[str]) -> None:
|
|
|
848
907
|
Input("experiment-result", "data"),
|
|
849
908
|
State("plot-settings-store", "data"),
|
|
850
909
|
State("plot-experiment-projection", "value", allow_optional=True),
|
|
910
|
+
State("url", "search"),
|
|
911
|
+
State("plot-experiment-x-scale", "value", allow_optional=True),
|
|
912
|
+
State("plot-experiment-y-scale", "value", allow_optional=True),
|
|
913
|
+
State("plot-experiment-font-size", "value", allow_optional=True),
|
|
914
|
+
State("plot-experiment-line-width", "value", allow_optional=True),
|
|
915
|
+
State("plot-experiment-legend", "value", allow_optional=True),
|
|
916
|
+
State("plot-experiment-grid", "value", allow_optional=True),
|
|
851
917
|
)
|
|
852
|
-
def _update_experiment_plot_options(x_axis: str | None, result: dict | None, plot_settings: dict | None, current_projection: str | None):
|
|
918
|
+
def _update_experiment_plot_options(x_axis: str | None, result: dict | None, plot_settings: dict | None, current_projection: str | None, search: str = "", *local_values):
|
|
919
|
+
setup = shared_setup(search, "/experiment")
|
|
920
|
+
plot_settings = shared_plot_settings(plot_settings, setup)
|
|
853
921
|
sweep_settings = (plot_settings or {}).get("parameter_sweep", plot_settings or {})
|
|
854
922
|
units = (result or {}).get("units", {})
|
|
855
923
|
xaxis_unit = units.get(x_axis)
|
|
856
924
|
if xaxis_unit is None and x_axis:
|
|
857
925
|
matching_units = [value for key, value in units.items() if key.rsplit(":", 1)[-1] == x_axis]
|
|
858
926
|
xaxis_unit = matching_units[0] if matching_units else None
|
|
927
|
+
if xaxis_unit is None and x_axis:
|
|
928
|
+
xaxis_unit = next((str(spec.unit) for group in (SOURCE_FIELDS, SCATTERER_FIELDS, DETECTOR_FIELDS)
|
|
929
|
+
for specs in group.values() for spec in specs if spec.name == x_axis and spec.unit is not None), None)
|
|
859
930
|
is_angle = _is_angular_unit(xaxis_unit)
|
|
860
931
|
options = [{"label": "Cartesian", "value": "cartesian"}]
|
|
861
932
|
if is_angle:
|
|
862
933
|
options.append({"label": "Polar", "value": "polar"})
|
|
863
934
|
projection = current_projection if is_angle and current_projection == "polar" else "cartesian"
|
|
935
|
+
sweep_settings = _merge_local_plot_values(sweep_settings, local_values)
|
|
864
936
|
from PyMieSimX.gui.layout import _plot_options_card
|
|
865
937
|
|
|
866
|
-
|
|
938
|
+
card = _plot_options_card("experiment", sweep_settings, projection=projection, projection_options=options)
|
|
939
|
+
if setup:
|
|
940
|
+
restore_controls(card, {**setup, "controls": {}})
|
|
941
|
+
return card
|
|
867
942
|
|
|
868
943
|
@app.callback(
|
|
869
944
|
Output("single-graph", "figure"),
|
|
@@ -894,6 +969,8 @@ def register_callbacks(app: Dash, default_measure_options: list[str]) -> None:
|
|
|
894
969
|
"""Keep Export CSV greyed out until Run has produced a result."""
|
|
895
970
|
return not result
|
|
896
971
|
|
|
972
|
+
register_sharing_callbacks(app)
|
|
973
|
+
|
|
897
974
|
|
|
898
975
|
def build_single_empty_figure(plot_settings: dict | None = None, theme: str = "light"):
|
|
899
976
|
"""Return the initial representation canvas."""
|
|
@@ -6,6 +6,14 @@ from collections.abc import Sequence
|
|
|
6
6
|
from dash import html
|
|
7
7
|
|
|
8
8
|
|
|
9
|
+
def workspace_header(title: str, description: str):
|
|
10
|
+
"""Build the same compact introduction card for each simulation workspace."""
|
|
11
|
+
return html.Section(className="page-hero workspace-header", children=[
|
|
12
|
+
html.P(title, className="eyebrow"),
|
|
13
|
+
html.P(description, className="hero-text workspace-intro"),
|
|
14
|
+
])
|
|
15
|
+
|
|
16
|
+
|
|
9
17
|
class Card:
|
|
10
18
|
"""Build a RosettaX-style card with an optional colored left accent."""
|
|
11
19
|
|
|
@@ -14,6 +14,7 @@ from PyMieSimX.gui.callbacks import register_callbacks
|
|
|
14
14
|
from PyMieSimX.gui.layout import create_layout
|
|
15
15
|
from PyMieSimX.gui.material_catalog import ensure_material_catalog
|
|
16
16
|
from PyMieSimX.gui.services import available_measures
|
|
17
|
+
from PyMieSimX.gui.seo import PAGE_METADATA, SearchDash, register_page_metadata
|
|
17
18
|
|
|
18
19
|
LOGGER = logging.getLogger(__name__)
|
|
19
20
|
GITHUB_RELEASES_URL = "https://github.com/MartinPdeS/PyMieSimX/releases"
|
|
@@ -36,9 +37,10 @@ def _initialize_material_catalog() -> None:
|
|
|
36
37
|
|
|
37
38
|
def create_dash_app() -> Dash:
|
|
38
39
|
"""Create and configure the experiment dashboard Dash application."""
|
|
39
|
-
application =
|
|
40
|
+
application = SearchDash(
|
|
40
41
|
__name__,
|
|
41
|
-
title="
|
|
42
|
+
title=PAGE_METADATA["/"]["title"],
|
|
43
|
+
update_title=None,
|
|
42
44
|
assets_folder=str(Path(__file__).with_name("assets")),
|
|
43
45
|
suppress_callback_exceptions=True,
|
|
44
46
|
)
|
|
@@ -51,6 +53,7 @@ def create_dash_app() -> Dash:
|
|
|
51
53
|
initial_measures = available_measures("SphereSet", "PhotodiodeSet")
|
|
52
54
|
application.layout = create_layout(initial_measures)
|
|
53
55
|
register_callbacks(application, initial_measures)
|
|
56
|
+
register_page_metadata(application)
|
|
54
57
|
LOGGER.debug("Dash application initialized with %d callbacks", len(application.callback_map))
|
|
55
58
|
return application
|
|
56
59
|
|
|
@@ -217,6 +217,7 @@ def create_layout(default_measure_options: list[str]):
|
|
|
217
217
|
className="app-shell",
|
|
218
218
|
children=[
|
|
219
219
|
dcc.Location(id="url", refresh=False),
|
|
220
|
+
dcc.Store(id="page-metadata"),
|
|
220
221
|
dcc.Store(id="experiment-result"),
|
|
221
222
|
dcc.Store(id="single-result"),
|
|
222
223
|
dcc.Download(id="csv-download"),
|
|
@@ -3,18 +3,25 @@
|
|
|
3
3
|
|
|
4
4
|
from dash import dcc, html
|
|
5
5
|
|
|
6
|
+
from PyMieSimX.gui.sharing import sharing_controls, sharing_feedback
|
|
7
|
+
from PyMieSimX.gui.components.cards import workspace_header
|
|
8
|
+
|
|
6
9
|
from PyMieSimX.gui.callback_helpers import status_banner
|
|
7
10
|
from PyMieSimX.gui.layout import PLOT_CONFIG, _plot_options_card, _x_axis_card, build_plot_options_sidebar, build_tabbed_sidebar
|
|
8
11
|
|
|
9
12
|
from .sections import build_detector_section, build_scatterer_section, build_source_section
|
|
10
13
|
|
|
11
14
|
|
|
12
|
-
def build_experiment_page(default_measure_options: list[str], plot_settings: dict | None = None):
|
|
15
|
+
def build_experiment_page(default_measure_options: list[str], plot_settings: dict | None = None, simulation: dict | None = None):
|
|
13
16
|
"""Build the isolated Parameter Scan workspace."""
|
|
14
17
|
settings = (plot_settings or {}).get("parameter_sweep", {})
|
|
18
|
+
projection = (simulation or {}).get("controls", {}).get("plot-experiment-projection", "cartesian")
|
|
19
|
+
projection_options = [{"label": "Cartesian", "value": "cartesian"}, {"label": "Polar", "value": "polar"}] if projection == "polar" else None
|
|
20
|
+
initial_plot_options = _plot_options_card("experiment", settings, projection=projection, projection_options=projection_options)
|
|
15
21
|
return html.Div(
|
|
16
22
|
className="tab-content-stack experiment-tab-content",
|
|
17
23
|
children=[
|
|
24
|
+
workspace_header("Compare scattering across a parameter sweep", "Run a Mie parameter sweep across source, scatterer, and detector settings. Compare scattering measures and export the results as CSV."),
|
|
18
25
|
html.Section(
|
|
19
26
|
id="configure",
|
|
20
27
|
className="workspace",
|
|
@@ -24,7 +31,6 @@ def build_experiment_page(default_measure_options: list[str], plot_settings: dic
|
|
|
24
31
|
html.Section(
|
|
25
32
|
className="result-column",
|
|
26
33
|
children=[
|
|
27
|
-
_x_axis_card(default_measure_options),
|
|
28
34
|
html.Div(
|
|
29
35
|
className="graph-toolbar",
|
|
30
36
|
children=[
|
|
@@ -42,6 +48,7 @@ def build_experiment_page(default_measure_options: list[str], plot_settings: dic
|
|
|
42
48
|
disabled=True,
|
|
43
49
|
className="toolbar-button toolbar-button-secondary",
|
|
44
50
|
),
|
|
51
|
+
sharing_controls("/experiment"),
|
|
45
52
|
],
|
|
46
53
|
),
|
|
47
54
|
html.Div(
|
|
@@ -49,6 +56,8 @@ def build_experiment_page(default_measure_options: list[str], plot_settings: dic
|
|
|
49
56
|
className="experiment-run-feedback",
|
|
50
57
|
children=status_banner("idle", "Ready. Click Run to start a sweep."),
|
|
51
58
|
),
|
|
59
|
+
sharing_feedback(),
|
|
60
|
+
_x_axis_card(default_measure_options),
|
|
52
61
|
html.Section(className="panel graph-panel", children=[dcc.Loading(id="result-graph-loading", type="circle", color="#4f8df7", custom_spinner=html.Div("Computing…", className="plot-computing-indicator"), delay_show=150, delay_hide=150, children=dcc.Graph(id="result-graph", config=PLOT_CONFIG))]),
|
|
53
62
|
],
|
|
54
63
|
),
|
|
@@ -61,7 +70,7 @@ def build_experiment_page(default_measure_options: list[str], plot_settings: dic
|
|
|
61
70
|
{"tab_id": "experiment-tab-source", "panel_id": "experiment-panel-source", "label": "Source", "color": "yellow", "content": build_source_section(), "active": False},
|
|
62
71
|
{"tab_id": "experiment-tab-scatterer", "panel_id": "experiment-panel-scatterer", "label": "Scatterer", "color": "blue", "content": build_scatterer_section(), "active": False},
|
|
63
72
|
{"tab_id": "experiment-tab-detector", "panel_id": "experiment-panel-detector", "label": "Detector", "color": "cyan", "content": build_detector_section(), "active": False},
|
|
64
|
-
{"tab_id": "experiment-tab-plot-options", "panel_id": "experiment-panel-plot-options", "label": "Plot options", "color": "purple", "content": build_plot_options_sidebar("experiment-plot-options-container",
|
|
73
|
+
{"tab_id": "experiment-tab-plot-options", "panel_id": "experiment-panel-plot-options", "label": "Plot options", "color": "purple", "content": build_plot_options_sidebar("experiment-plot-options-container", initial_plot_options), "active": False},
|
|
65
74
|
],
|
|
66
75
|
),
|
|
67
76
|
],
|