eagleplot 3.0.0__tar.gz → 3.1.0__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.
Files changed (115) hide show
  1. {eagleplot-3.0.0 → eagleplot-3.1.0}/LICENSE +21 -21
  2. {eagleplot-3.0.0 → eagleplot-3.1.0}/PKG-INFO +6 -1
  3. {eagleplot-3.0.0 → eagleplot-3.1.0}/README.md +4 -0
  4. {eagleplot-3.0.0 → eagleplot-3.1.0}/eagleplot/Home.py +91 -29
  5. {eagleplot-3.0.0 → eagleplot-3.1.0}/eagleplot/__init__.py +1 -1
  6. {eagleplot-3.0.0 → eagleplot-3.1.0}/eagleplot/io.py +21 -8
  7. {eagleplot-3.0.0 → eagleplot-3.1.0}/eagleplot/pages/10_Molecular_Viz.py +40 -14
  8. {eagleplot-3.0.0 → eagleplot-3.1.0}/eagleplot/pages/11_Batch_Analysis.py +52 -17
  9. {eagleplot-3.0.0 → eagleplot-3.1.0}/eagleplot/pages/13_Settings.py +85 -77
  10. {eagleplot-3.0.0 → eagleplot-3.1.0}/eagleplot/pages/1_Data_Upload.py +10 -3
  11. {eagleplot-3.0.0 → eagleplot-3.1.0}/eagleplot/pages/2_Quick_Plot.py +415 -387
  12. {eagleplot-3.0.0 → eagleplot-3.1.0}/eagleplot/pages/3_Analysis_Tools.py +1274 -1162
  13. eagleplot-3.1.0/eagleplot/pages/4_Multi_Plot_Dashboard.py +803 -0
  14. {eagleplot-3.0.0 → eagleplot-3.1.0}/eagleplot/pages/5_Advanced_Plotting.py +73 -28
  15. {eagleplot-3.0.0 → eagleplot-3.1.0}/eagleplot/pages/6_Export_Results.py +130 -53
  16. {eagleplot-3.0.0 → eagleplot-3.1.0}/eagleplot/pages/7_Gallery.py +3 -1
  17. {eagleplot-3.0.0 → eagleplot-3.1.0}/eagleplot/pages/9_Spectroscopy.py +20 -11
  18. {eagleplot-3.0.0 → eagleplot-3.1.0}/eagleplot/utils/access.py +16 -2
  19. eagleplot-3.1.0/eagleplot/utils/figures.py +198 -0
  20. {eagleplot-3.0.0 → eagleplot-3.1.0}/eagleplot/utils/plot_config.py +1 -1
  21. eagleplot-3.1.0/eagleplot/utils/runtime.py +128 -0
  22. {eagleplot-3.0.0 → eagleplot-3.1.0}/eagleplot/utils/theming.py +3 -0
  23. {eagleplot-3.0.0 → eagleplot-3.1.0}/eagleplot/utils/user_settings.py +11 -2
  24. {eagleplot-3.0.0 → eagleplot-3.1.0}/eagleplot.egg-info/PKG-INFO +6 -1
  25. {eagleplot-3.0.0 → eagleplot-3.1.0}/eagleplot.egg-info/SOURCES.txt +11 -0
  26. {eagleplot-3.0.0 → eagleplot-3.1.0}/eagleplot.egg-info/requires.txt +1 -0
  27. {eagleplot-3.0.0 → eagleplot-3.1.0}/pyproject.toml +1 -0
  28. eagleplot-3.1.0/tests/test_advanced_plotting_page.py +179 -0
  29. eagleplot-3.1.0/tests/test_analysis_tools_page.py +229 -0
  30. eagleplot-3.1.0/tests/test_batch_page.py +64 -0
  31. eagleplot-3.1.0/tests/test_dashboard_page.py +267 -0
  32. {eagleplot-3.0.0 → eagleplot-3.1.0}/tests/test_docs_site.py +15 -0
  33. eagleplot-3.1.0/tests/test_export_page.py +115 -0
  34. eagleplot-3.1.0/tests/test_figures.py +238 -0
  35. eagleplot-3.1.0/tests/test_home_page.py +50 -0
  36. eagleplot-3.1.0/tests/test_quick_plot.py +97 -0
  37. eagleplot-3.1.0/tests/test_runtime.py +251 -0
  38. eagleplot-3.0.0/eagleplot/pages/4_Multi_Plot_Dashboard.py +0 -791
  39. {eagleplot-3.0.0 → eagleplot-3.1.0}/eagleplot/annotations.py +0 -0
  40. {eagleplot-3.0.0 → eagleplot-3.1.0}/eagleplot/assets/logo.ico +0 -0
  41. {eagleplot-3.0.0 → eagleplot-3.1.0}/eagleplot/assets/logo.png +0 -0
  42. {eagleplot-3.0.0 → eagleplot-3.1.0}/eagleplot/assets/nccu-horiz-logo.png +0 -0
  43. {eagleplot-3.0.0 → eagleplot-3.1.0}/eagleplot/assets/nccu-wings.png +0 -0
  44. {eagleplot-3.0.0 → eagleplot-3.1.0}/eagleplot/batch.py +0 -0
  45. {eagleplot-3.0.0 → eagleplot-3.1.0}/eagleplot/cli.py +0 -0
  46. {eagleplot-3.0.0 → eagleplot-3.1.0}/eagleplot/data_tools.py +0 -0
  47. {eagleplot-3.0.0 → eagleplot-3.1.0}/eagleplot/example-data/Artificial/bimodal_distribution.csv +0 -0
  48. {eagleplot-3.0.0 → eagleplot-3.1.0}/eagleplot/example-data/Artificial/correlation_matrix.npy +0 -0
  49. {eagleplot-3.0.0 → eagleplot-3.1.0}/eagleplot/example-data/Artificial/gaussian_surface.npy +0 -0
  50. {eagleplot-3.0.0 → eagleplot-3.1.0}/eagleplot/example-data/Artificial/grouped_categorical.csv +0 -0
  51. {eagleplot-3.0.0 → eagleplot-3.1.0}/eagleplot/example-data/Artificial/ir_spectrum.csv +0 -0
  52. {eagleplot-3.0.0 → eagleplot-3.1.0}/eagleplot/example-data/Artificial/molecular_properties.csv +0 -0
  53. {eagleplot-3.0.0 → eagleplot-3.1.0}/eagleplot/example-data/Artificial/normal_distribution.csv +0 -0
  54. {eagleplot-3.0.0 → eagleplot-3.1.0}/eagleplot/example-data/Artificial/reaction_kinetics.csv +0 -0
  55. {eagleplot-3.0.0 → eagleplot-3.1.0}/eagleplot/example-data/Artificial/scatter_correlation.csv +0 -0
  56. {eagleplot-3.0.0 → eagleplot-3.1.0}/eagleplot/example-data/Artificial/sine_cosine_waves.csv +0 -0
  57. {eagleplot-3.0.0 → eagleplot-3.1.0}/eagleplot/gallery/box_plot.png +0 -0
  58. {eagleplot-3.0.0 → eagleplot-3.1.0}/eagleplot/gallery/contour_plot.png +0 -0
  59. {eagleplot-3.0.0 → eagleplot-3.1.0}/eagleplot/gallery/distribution_plot.png +0 -0
  60. {eagleplot-3.0.0 → eagleplot-3.1.0}/eagleplot/gallery/heatmap.png +0 -0
  61. {eagleplot-3.0.0 → eagleplot-3.1.0}/eagleplot/gallery/histogram.png +0 -0
  62. {eagleplot-3.0.0 → eagleplot-3.1.0}/eagleplot/gallery/interactive_3d_surface.png +0 -0
  63. {eagleplot-3.0.0 → eagleplot-3.1.0}/eagleplot/gallery/interactive_heatmap.png +0 -0
  64. {eagleplot-3.0.0 → eagleplot-3.1.0}/eagleplot/gallery/interactive_histogram.png +0 -0
  65. {eagleplot-3.0.0 → eagleplot-3.1.0}/eagleplot/gallery/interactive_line.png +0 -0
  66. {eagleplot-3.0.0 → eagleplot-3.1.0}/eagleplot/gallery/interactive_scatter.png +0 -0
  67. {eagleplot-3.0.0 → eagleplot-3.1.0}/eagleplot/gallery/line_plot.png +0 -0
  68. {eagleplot-3.0.0 → eagleplot-3.1.0}/eagleplot/gallery/manifest.json +0 -0
  69. {eagleplot-3.0.0 → eagleplot-3.1.0}/eagleplot/gallery/regression_plot.png +0 -0
  70. {eagleplot-3.0.0 → eagleplot-3.1.0}/eagleplot/gallery/scatter_plot.png +0 -0
  71. {eagleplot-3.0.0 → eagleplot-3.1.0}/eagleplot/math.py +0 -0
  72. {eagleplot-3.0.0 → eagleplot-3.1.0}/eagleplot/molecular/__init__.py +0 -0
  73. {eagleplot-3.0.0 → eagleplot-3.1.0}/eagleplot/molecular/atom_data.py +0 -0
  74. {eagleplot-3.0.0 → eagleplot-3.1.0}/eagleplot/molecular/parsers.py +0 -0
  75. {eagleplot-3.0.0 → eagleplot-3.1.0}/eagleplot/nist.py +0 -0
  76. {eagleplot-3.0.0 → eagleplot-3.1.0}/eagleplot/pages/12_Help.py +0 -0
  77. {eagleplot-3.0.0 → eagleplot-3.1.0}/eagleplot/pages/8_Data_Tools.py +0 -0
  78. {eagleplot-3.0.0 → eagleplot-3.1.0}/eagleplot/pages/__init__.py +0 -0
  79. {eagleplot-3.0.0 → eagleplot-3.1.0}/eagleplot/peaks.py +0 -0
  80. {eagleplot-3.0.0 → eagleplot-3.1.0}/eagleplot/plotting.py +0 -0
  81. {eagleplot-3.0.0 → eagleplot-3.1.0}/eagleplot/plugin_loader.py +0 -0
  82. {eagleplot-3.0.0 → eagleplot-3.1.0}/eagleplot/report.py +0 -0
  83. {eagleplot-3.0.0 → eagleplot-3.1.0}/eagleplot/signal.py +0 -0
  84. {eagleplot-3.0.0 → eagleplot-3.1.0}/eagleplot/spectroscopy.py +0 -0
  85. {eagleplot-3.0.0 → eagleplot-3.1.0}/eagleplot/utils/__init__.py +0 -0
  86. {eagleplot-3.0.0 → eagleplot-3.1.0}/eagleplot/utils/data_preview.py +0 -0
  87. {eagleplot-3.0.0 → eagleplot-3.1.0}/eagleplot/utils/session_state.py +0 -0
  88. {eagleplot-3.0.0 → eagleplot-3.1.0}/eagleplot.egg-info/dependency_links.txt +0 -0
  89. {eagleplot-3.0.0 → eagleplot-3.1.0}/eagleplot.egg-info/entry_points.txt +0 -0
  90. {eagleplot-3.0.0 → eagleplot-3.1.0}/eagleplot.egg-info/top_level.txt +0 -0
  91. {eagleplot-3.0.0 → eagleplot-3.1.0}/setup.cfg +0 -0
  92. {eagleplot-3.0.0 → eagleplot-3.1.0}/tests/test_annotations.py +0 -0
  93. {eagleplot-3.0.0 → eagleplot-3.1.0}/tests/test_batch.py +0 -0
  94. {eagleplot-3.0.0 → eagleplot-3.1.0}/tests/test_cli.py +0 -0
  95. {eagleplot-3.0.0 → eagleplot-3.1.0}/tests/test_data_tools.py +0 -0
  96. {eagleplot-3.0.0 → eagleplot-3.1.0}/tests/test_docs_consistency.py +0 -0
  97. {eagleplot-3.0.0 → eagleplot-3.1.0}/tests/test_gui_pages.py +0 -0
  98. {eagleplot-3.0.0 → eagleplot-3.1.0}/tests/test_gui_paths.py +0 -0
  99. {eagleplot-3.0.0 → eagleplot-3.1.0}/tests/test_gui_safety.py +0 -0
  100. {eagleplot-3.0.0 → eagleplot-3.1.0}/tests/test_integration.py +0 -0
  101. {eagleplot-3.0.0 → eagleplot-3.1.0}/tests/test_io.py +0 -0
  102. {eagleplot-3.0.0 → eagleplot-3.1.0}/tests/test_math.py +0 -0
  103. {eagleplot-3.0.0 → eagleplot-3.1.0}/tests/test_molecular_parsers.py +0 -0
  104. {eagleplot-3.0.0 → eagleplot-3.1.0}/tests/test_nist.py +0 -0
  105. {eagleplot-3.0.0 → eagleplot-3.1.0}/tests/test_peaks.py +0 -0
  106. {eagleplot-3.0.0 → eagleplot-3.1.0}/tests/test_plotting.py +0 -0
  107. {eagleplot-3.0.0 → eagleplot-3.1.0}/tests/test_plotting_advanced.py +0 -0
  108. {eagleplot-3.0.0 → eagleplot-3.1.0}/tests/test_plugin_loader.py +0 -0
  109. {eagleplot-3.0.0 → eagleplot-3.1.0}/tests/test_rename_compat.py +0 -0
  110. {eagleplot-3.0.0 → eagleplot-3.1.0}/tests/test_repo_hygiene.py +0 -0
  111. {eagleplot-3.0.0 → eagleplot-3.1.0}/tests/test_report.py +0 -0
  112. {eagleplot-3.0.0 → eagleplot-3.1.0}/tests/test_signal.py +0 -0
  113. {eagleplot-3.0.0 → eagleplot-3.1.0}/tests/test_spectroscopy.py +0 -0
  114. {eagleplot-3.0.0 → eagleplot-3.1.0}/tests/test_theming.py +0 -0
  115. {eagleplot-3.0.0 → eagleplot-3.1.0}/tests/test_utils.py +0 -0
@@ -1,21 +1,21 @@
1
- MIT License
2
-
3
- Copyright (c) 2026 Jonathan D. Schultz, North Carolina Central University
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Jonathan D. Schultz, North Carolina Central University
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: eagleplot
3
- Version: 3.0.0
3
+ Version: 3.1.0
4
4
  Summary: A general-purpose Python toolkit for scientific data visualization and analysis
5
5
  Author-email: "Jonathan D. Schultz" <jschult3@nccu.edu>
6
6
  License-Expression: MIT
@@ -37,6 +37,7 @@ Provides-Extra: formats
37
37
  Requires-Dist: h5py>=3.0.0; extra == "formats"
38
38
  Requires-Dist: xarray>=0.20.0; extra == "formats"
39
39
  Requires-Dist: netcdf4>=1.6.0; extra == "formats"
40
+ Requires-Dist: jcamp>=1.2; extra == "formats"
40
41
  Provides-Extra: nist
41
42
  Requires-Dist: requests>=2.28.0; extra == "nist"
42
43
  Provides-Extra: docs
@@ -62,6 +63,10 @@ Dynamic: license-file
62
63
  [![PyPI](https://img.shields.io/pypi/v/eagleplot)](https://pypi.org/project/eagleplot/)
63
64
  [![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/The-Schultz-Lab/eagleplot/main)
64
65
 
66
+ **[Open EaglePlot in your browser](https://the-schultz-lab.github.io/eagleplot/app/)**: nothing
67
+ to install, and your data never leaves your computer. Good for laptops where you can't
68
+ install software. To install it instead, see [Quick Start](#quick-start).
69
+
65
70
  A general-purpose Python toolkit for scientific data visualization and analysis, developed for research and teaching in computational science at North Carolina Central University.
66
71
 
67
72
  > **EaglePlot was called Plottle until version 3.0.** If you used Plottle, change
@@ -4,6 +4,10 @@
4
4
  [![PyPI](https://img.shields.io/pypi/v/eagleplot)](https://pypi.org/project/eagleplot/)
5
5
  [![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/The-Schultz-Lab/eagleplot/main)
6
6
 
7
+ **[Open EaglePlot in your browser](https://the-schultz-lab.github.io/eagleplot/app/)**: nothing
8
+ to install, and your data never leaves your computer. Good for laptops where you can't
9
+ install software. To install it instead, see [Quick Start](#quick-start).
10
+
7
11
  A general-purpose Python toolkit for scientific data visualization and analysis, developed for research and teaching in computational science at North Carolina Central University.
8
12
 
9
13
  > **EaglePlot was called Plottle until version 3.0.** If you used Plottle, change
@@ -9,15 +9,15 @@ Navigation Structure
9
9
  Home
10
10
  Data Upload
11
11
  Plot
12
- Basic — Quick Plot (27 plot types, Matplotlib/Seaborn/Plotly)
13
- Multiplot — Multi-Plot Dashboard (grid layouts, axis sharing)
12
+ Quick Plot — 27 plot types, Matplotlib/Seaborn/Plotly
13
+ Dashboard — Multi-Plot Dashboard (grid layouts, axis sharing)
14
14
  Advanced
15
15
  ↳ Advanced Plotting — Seaborn statistical + Plotly interactive
16
16
  ↳ Spectroscopy — IR/Raman, NMR, UV-Vis, Mass Spec + NIST
17
17
  ↳ Molecular Viz — 3D structure and vibrational modes
18
18
  Analyze
19
- Single — Analysis Tools (stats, curve fit, signal, peaks, …)
20
- Batch — Batch Analysis + workflow presets
19
+ Analysis Tools — stats, curve fit, signal, peaks, …
20
+ Batch Analysis — the same across several datasets, + workflow presets
21
21
  Export
22
22
  Gallery
23
23
  Help
@@ -47,6 +47,7 @@ _NCCU_HORIZ = _ASSETS_DIR / "nccu-horiz-logo.png"
47
47
  _NCCU_WINGS = _ASSETS_DIR / "nccu-wings.png"
48
48
 
49
49
  from eagleplot import __version__ # noqa: E402
50
+ from eagleplot.utils import runtime # noqa: E402
50
51
  from eagleplot.utils import initialize_session_state, get_session_summary # noqa: E402
51
52
  from eagleplot.utils.theming import app_css # noqa: E402
52
53
 
@@ -62,7 +63,8 @@ st.set_page_config(
62
63
  page_title="EaglePlot",
63
64
  page_icon=_page_icon,
64
65
  layout="wide",
65
- initial_sidebar_state="expanded",
66
+ # "auto": open on wide screens, closed on phones (where it would cover the page).
67
+ initial_sidebar_state="auto",
66
68
  menu_items={
67
69
  "About": (
68
70
  f"**EaglePlot** v{__version__}\n\n"
@@ -87,6 +89,56 @@ initialize_session_state()
87
89
  # ── Home page content ─────────────────────────────────────────────────────────
88
90
 
89
91
 
92
+ # Examples offered on the first screen: one click loads the data and opens Quick Plot.
93
+ _EXAMPLE_DIR = Path(__file__).resolve().parent / "example-data" / "Artificial"
94
+ _STARTERS = [
95
+ ("sine_cosine_waves.csv", "Sine & cosine waves", "Line plot of three series"),
96
+ ("ir_spectrum.csv", "IR spectrum", "A spectrum from 4000 to 400 cm⁻¹"),
97
+ ("scatter_correlation.csv", "Scatter correlation", "Yield vs reaction time"),
98
+ ]
99
+
100
+
101
+ def _get_started() -> None:
102
+ """First screen for a session without data: one-click examples, or upload."""
103
+ st.markdown("### Get started")
104
+ st.caption("Try EaglePlot with an example dataset, or bring your own data.")
105
+ cols = st.columns(len(_STARTERS))
106
+ for col, (fname, title, desc) in zip(cols, _STARTERS):
107
+ path = _EXAMPLE_DIR / fname
108
+ if not path.exists():
109
+ continue
110
+ with col, st.container(border=True):
111
+ st.markdown(f"**{title}**")
112
+ st.caption(desc)
113
+ if st.button(
114
+ "Plot this example",
115
+ key=f"home_try_{fname}",
116
+ type="primary" if fname == _STARTERS[0][0] else "secondary",
117
+ width="stretch",
118
+ ):
119
+ from eagleplot.io import load_data
120
+ from eagleplot.utils import add_dataset
121
+
122
+ add_dataset(
123
+ fname,
124
+ load_data(path),
125
+ metadata={"source": "example", "file_type": path.suffix},
126
+ )
127
+ st.switch_page(basic_pg)
128
+ with st.container(border=True):
129
+ _up_col, _ex_col = st.columns(2)
130
+ with _up_col:
131
+ st.markdown("**Your own data**")
132
+ st.caption("CSV, Excel, JSON, NumPy, spectra (JCAMP-DX) and more.")
133
+ if st.button("Upload a file", key="home_upload", width="stretch"):
134
+ st.switch_page(data_pg)
135
+ with _ex_col:
136
+ st.markdown("**More examples**")
137
+ st.caption("Distributions, kinetics, molecular properties, 2-D arrays.")
138
+ if st.button("Browse all examples", key="home_examples", width="stretch"):
139
+ st.switch_page(data_pg)
140
+
141
+
90
142
  def _home_page() -> None:
91
143
  """Render the Dashboard / Help landing page."""
92
144
  tab_dash, tab_help = st.tabs(["Dashboard", "Help"])
@@ -117,29 +169,32 @@ def _home_page() -> None:
117
169
 
118
170
  st.divider()
119
171
 
120
- # ── Session metrics ───────────────────────────────────────────────────
121
172
  summary = get_session_summary()
122
- col1, col2, col3 = st.columns(3)
123
- col1.metric("Datasets", summary["num_datasets"])
124
- col2.metric("Plots", summary["num_plots"])
125
- col3.metric("Analyses", summary["num_analyses"])
126
-
127
- if summary["current_dataset"]:
128
- st.success(f"Active dataset: **{summary['current_dataset']}**")
129
- st.page_link(basic_pg, label="Go to Quick Plot →")
173
+ if summary["num_datasets"] == 0:
174
+ _get_started()
130
175
  else:
131
- st.info("No dataset loaded.")
132
- st.page_link(data_pg, label="Upload Data →")
176
+ # ── Session metrics ───────────────────────────────────────────────
177
+ col1, col2, col3 = st.columns(3)
178
+ col1.metric("Datasets", summary["num_datasets"])
179
+ col2.metric("Plots", summary["num_plots"])
180
+ col3.metric("Analyses", summary["num_analyses"])
181
+ if summary["current_dataset"]:
182
+ st.success(f"Active dataset: **{summary['current_dataset']}**")
183
+ _go_col, _more_col, _ = st.columns([1, 1, 2])
184
+ with _go_col:
185
+ st.page_link(basic_pg, label="Go to Quick Plot →")
186
+ with _more_col:
187
+ st.page_link(data_pg, label="Load more data →")
133
188
 
134
189
  with tab_help:
135
190
  st.markdown("## Quick Start")
136
191
  st.markdown("""
137
192
  1. **Upload Data** — Go to *Data Upload* to load a CSV, Excel, NumPy, or other file.
138
193
  Or load a built-in example dataset.
139
- 2. **Plot** — *Plot → Basic* lets you choose from 27 plot types and configure them
140
- interactively.
141
- 3. **Analyse** — *Analyze → Single* has curve fitting, statistics, signal processing,
142
- peak analysis, and more.
194
+ 2. **Plot** — *Plot → Quick Plot* lets you choose from 27 plot types and configure
195
+ them interactively.
196
+ 3. **Analyse** — *Analyze → Analysis Tools* has curve fitting, statistics, signal
197
+ processing, peak analysis, and more.
143
198
  4. **Export** — *Export* saves plots as PNG/PDF/SVG and data in multiple formats.
144
199
  """)
145
200
 
@@ -207,12 +262,12 @@ data_pg = st.Page(
207
262
  # Plot group
208
263
  basic_pg = st.Page(
209
264
  str(_PAGES_DIR / "2_Quick_Plot.py"),
210
- title="Basic",
265
+ title="Quick Plot",
211
266
  url_path="plot-basic",
212
267
  )
213
268
  multi_pg = st.Page(
214
269
  str(_PAGES_DIR / "4_Multi_Plot_Dashboard.py"),
215
- title="Multiplot",
270
+ title="Dashboard",
216
271
  url_path="plot-multiplot",
217
272
  )
218
273
  adv_plt_pg = st.Page(
@@ -234,12 +289,12 @@ molvis_pg = st.Page(
234
289
  # Analyze group
235
290
  single_pg = st.Page(
236
291
  str(_PAGES_DIR / "3_Analysis_Tools.py"),
237
- title="Single",
292
+ title="Analysis Tools",
238
293
  url_path="analyze-single",
239
294
  )
240
295
  batch_pg = st.Page(
241
296
  str(_PAGES_DIR / "11_Batch_Analysis.py"),
242
- title="Batch",
297
+ title="Batch Analysis",
243
298
  url_path="analyze-batch",
244
299
  )
245
300
 
@@ -313,16 +368,16 @@ with st.sidebar:
313
368
  st.page_link(data_pg, label="Data Upload")
314
369
 
315
370
  with st.expander("**Plot**", expanded=pg in _PLOT_PAGES):
316
- st.page_link(basic_pg, label="Basic")
317
- st.page_link(multi_pg, label="Multiplot")
371
+ st.page_link(basic_pg, label="Quick Plot")
372
+ st.page_link(multi_pg, label="Dashboard")
318
373
  st.markdown(_ADV_HEADER, unsafe_allow_html=True)
319
374
  st.page_link(adv_plt_pg, label="↳ Advanced Plotting")
320
375
  st.page_link(spec_pg, label="↳ Spectroscopy")
321
376
  st.page_link(molvis_pg, label="↳ Molecular Viz")
322
377
 
323
378
  with st.expander("**Analyze**", expanded=pg in _ANALYZE_PAGES):
324
- st.page_link(single_pg, label="Single")
325
- st.page_link(batch_pg, label="Batch")
379
+ st.page_link(single_pg, label="Analysis Tools")
380
+ st.page_link(batch_pg, label="Batch Analysis")
326
381
  st.page_link(datatools_pg, label="Data Tools")
327
382
 
328
383
  st.divider()
@@ -332,7 +387,7 @@ with st.sidebar:
332
387
  st.page_link(settings_pg, label="Settings")
333
388
 
334
389
  st.divider()
335
- with st.expander("About", expanded=True):
390
+ with st.expander("About", expanded=False):
336
391
  if _NCCU_HORIZ.exists():
337
392
  _horiz_b64 = base64.b64encode(_NCCU_HORIZ.read_bytes()).decode()
338
393
  st.markdown(
@@ -355,6 +410,13 @@ with st.sidebar:
355
410
  "science at North Carolina Central University."
356
411
  )
357
412
  st.markdown("[NCCU Schultz Lab on GitHub](https://github.com/The-Schultz-Lab)")
413
+ if runtime.IS_PYODIDE and not runtime.IS_DESKTOP:
414
+ st.caption(
415
+ f"EaglePlot v{__version__}, running in your browser: your data stays on "
416
+ "this computer. To load whole folders, fetch spectra from NIST or work "
417
+ "faster, [install EaglePlot]"
418
+ "(https://the-schultz-lab.github.io/eagleplot/#install)."
419
+ )
358
420
 
359
421
 
360
422
  # ── Execute the active page ───────────────────────────────────────────────────
@@ -14,6 +14,6 @@ gui : Interactive GUI components
14
14
  # `[tool.setuptools.dynamic]`, and cli.py / Home.py / docs/conf.py import it, so
15
15
  # the number cannot drift between the package, the CLI, and the GUI.
16
16
  # See audit A-26.
17
- __version__ = "3.0.0"
17
+ __version__ = "3.1.0"
18
18
 
19
19
  __all__ = ["io", "math", "plotting", "gui", "__version__"]
@@ -389,15 +389,28 @@ def load_jcamp(filepath: Union[str, Path]) -> pd.DataFrame:
389
389
  1/CM
390
390
  """
391
391
  try:
392
- from jcamp import JCAMP_reader as _jcamp_read
392
+ import jcamp as _jcamp
393
393
  except ImportError:
394
- try:
395
- from jcamp import jcamp_read as _jcamp_read # type: ignore[no-redef]
396
- except ImportError:
397
- raise ImportError(
398
- "The `jcamp` package is required to load JCAMP-DX files. "
399
- "Install it with: pip install jcamp"
400
- )
394
+ raise ImportError(
395
+ "The `jcamp` package is required to load JCAMP-DX files. "
396
+ "Install it with: pip install jcamp"
397
+ )
398
+ # The reader has been renamed over jcamp's history: readfile (1.3+),
399
+ # jcamp_readfile (1.2), JCAMP_reader (older). Only looking for the old names
400
+ # made every .jdx upload fail with jcamp 1.3 installed.
401
+ _jcamp_read = next(
402
+ (
403
+ getattr(_jcamp, name)
404
+ for name in ("readfile", "jcamp_readfile", "JCAMP_reader")
405
+ if hasattr(_jcamp, name)
406
+ ),
407
+ None,
408
+ )
409
+ if _jcamp_read is None:
410
+ raise ImportError(
411
+ f"Unsupported jcamp version {getattr(_jcamp, '__version__', '?')}: no "
412
+ "JCAMP-DX file reader found. Install it with: pip install --upgrade jcamp"
413
+ )
401
414
 
402
415
  filepath = Path(filepath)
403
416
  if not filepath.exists():
@@ -17,6 +17,7 @@ import streamlit as st
17
17
 
18
18
  sys.path.insert(0, str(Path(__file__).parent.parent.parent))
19
19
 
20
+ from eagleplot.utils.figures import LAZY_DOWNLOAD # noqa: E402
20
21
  from eagleplot.utils.session_state import initialize_session_state
21
22
  from eagleplot.molecular import (
22
23
  VibrationalData,
@@ -172,8 +173,37 @@ st.subheader("Export")
172
173
 
173
174
  col1, col2 = st.columns(2)
174
175
 
176
+
177
+ def _freq_table_csv(modes) -> bytes:
178
+ return (
179
+ pd.DataFrame(
180
+ {
181
+ "mode": [m.mode_number for m in modes],
182
+ "frequency_cm-1": [m.frequency for m in modes],
183
+ "ir_intensity_km_per_mol": [
184
+ (m.ir_intensity if m.ir_intensity is not None else float("nan")) for m in modes
185
+ ],
186
+ "is_imaginary": [m.is_imaginary for m in modes],
187
+ }
188
+ )
189
+ .to_csv(index=False)
190
+ .encode()
191
+ )
192
+
193
+
194
+ # On Streamlit >= 1.52 each export is one click: the file is built when Download is
195
+ # pressed. Older Streamlit keeps the two-step "prepare, then download" flow, so no
196
+ # export is built on every rerun there either (roadmap D1.2).
175
197
  with col1:
176
- if st.button("Export as interactive HTML", key="mol_export_html"):
198
+ if LAZY_DOWNLOAD:
199
+ st.download_button(
200
+ "Download interactive HTML",
201
+ data=lambda f=fig: f.to_html(full_html=True, include_plotlyjs="cdn"),
202
+ file_name=f"{Path(uploaded.name).stem}_mol.html",
203
+ mime="text/html",
204
+ key="mol_dl_html",
205
+ )
206
+ elif st.button("Export as interactive HTML", key="mol_export_html"):
177
207
  try:
178
208
  html_str = fig.to_html(full_html=True, include_plotlyjs="cdn")
179
209
  st.download_button(
@@ -187,22 +217,18 @@ with col1:
187
217
  st.error(str(exc))
188
218
 
189
219
  with col2:
190
- if vib_data.modes and st.button("Download frequency table (CSV)", key="mol_dl_csv"):
191
- freq_df_dl = pd.DataFrame(
192
- {
193
- "mode": [m.mode_number for m in vib_data.modes],
194
- "frequency_cm-1": [m.frequency for m in vib_data.modes],
195
- "ir_intensity_km_per_mol": [
196
- (m.ir_intensity if m.ir_intensity is not None else float("nan"))
197
- for m in vib_data.modes
198
- ],
199
- "is_imaginary": [m.is_imaginary for m in vib_data.modes],
200
- }
220
+ if vib_data.modes and LAZY_DOWNLOAD:
221
+ st.download_button(
222
+ "Download frequency table (CSV)",
223
+ data=lambda modes=list(vib_data.modes): _freq_table_csv(modes),
224
+ file_name=f"{Path(uploaded.name).stem}_frequencies.csv",
225
+ mime="text/csv",
226
+ key="mol_dl_csv_btn",
201
227
  )
202
- csv_bytes = freq_df_dl.to_csv(index=False).encode()
228
+ elif vib_data.modes and st.button("Download frequency table (CSV)", key="mol_dl_csv"):
203
229
  st.download_button(
204
230
  "Download CSV",
205
- data=csv_bytes,
231
+ data=_freq_table_csv(vib_data.modes),
206
232
  file_name=f"{Path(uploaded.name).stem}_frequencies.csv",
207
233
  mime="text/csv",
208
234
  key="mol_dl_csv_btn",
@@ -16,7 +16,9 @@ import pandas as pd
16
16
 
17
17
  sys.path.insert(0, str(Path(__file__).parent.parent.parent))
18
18
 
19
+ from eagleplot.utils.figures import download_payload # noqa: E402
19
20
  from eagleplot.utils.session_state import initialize_session_state
21
+ from eagleplot.utils import runtime
20
22
  from eagleplot.batch import (
21
23
  batch_statistics,
22
24
  batch_curve_fit,
@@ -55,10 +57,31 @@ _RESULT_OUTPUT_COLS = [
55
57
  ]
56
58
 
57
59
 
58
- def _csv_download(df: pd.DataFrame, filename: str, label: str = "Download CSV") -> None:
59
- """Render a CSV download button for *df*."""
60
- csv_bytes = df.to_csv(index=False).encode("utf-8")
61
- st.download_button(label, data=csv_bytes, file_name=filename, mime="text/csv")
60
+ def _show_result(state_key: str, filename: str) -> None:
61
+ """Show the last result of a batch tool, with a CSV download.
62
+
63
+ Results live in session state, so they stay on screen after any later interaction,
64
+ including clicking Download (which reruns the page). They used to exist only on the
65
+ rerun right after Run. The CSV is built on click, or once per result (roadmap D1.2).
66
+ """
67
+ saved = st.session_state.get(state_key)
68
+ if not saved:
69
+ return
70
+ result_df, names = saved["df"], saved["datasets"]
71
+ st.success(f"Results for {len(names)} dataset(s): {', '.join(names)}")
72
+ st.dataframe(result_df, width="stretch")
73
+ st.download_button(
74
+ "Download CSV",
75
+ data=download_payload(
76
+ lambda df=result_df: df.to_csv(index=False).encode("utf-8"),
77
+ cache_name=f"_{state_key}_csv",
78
+ version=id(result_df),
79
+ key="csv",
80
+ ),
81
+ file_name=filename,
82
+ mime="text/csv",
83
+ key=f"{state_key}_dl",
84
+ )
62
85
 
63
86
 
64
87
  # ── top-level tabs ─────────────────────────────────────────────────────────────
@@ -120,11 +143,13 @@ with tab_analysis:
120
143
  cols_arg = col_filter if col_filter else None
121
144
  try:
122
145
  result_df = batch_statistics(selected_datasets, columns=cols_arg)
123
- st.success(f"Statistics computed for {len(selected_names)} dataset(s).")
124
- st.dataframe(result_df, width="stretch")
125
- _csv_download(result_df, "batch_statistics.csv")
146
+ st.session_state["ba_stats_result"] = {
147
+ "df": result_df,
148
+ "datasets": list(selected_names),
149
+ }
126
150
  except Exception as exc:
127
151
  st.error(f"Error: {exc}")
152
+ _show_result("ba_stats_result", "batch_statistics.csv")
128
153
 
129
154
  # ── Curve Fit ─────────────────────────────────────────────────────────────
130
155
  with sub_fit:
@@ -164,11 +189,13 @@ with tab_analysis:
164
189
  fit_type=fit_type,
165
190
  degree=fit_degree,
166
191
  )
167
- st.success(f"Curve fit complete for {len(selected_names)} dataset(s).")
168
- st.dataframe(result_df, width="stretch")
169
- _csv_download(result_df, "batch_curve_fit.csv")
192
+ st.session_state["ba_fit_result"] = {
193
+ "df": result_df,
194
+ "datasets": list(selected_names),
195
+ }
170
196
  except Exception as exc:
171
197
  st.error(f"Error: {exc}")
198
+ _show_result("ba_fit_result", "batch_curve_fit.csv")
172
199
 
173
200
  # ── Peak Analysis ─────────────────────────────────────────────────────────
174
201
  with sub_peaks:
@@ -229,11 +256,13 @@ with tab_analysis:
229
256
  prominence=pa_prominence,
230
257
  distance=pa_distance,
231
258
  )
232
- st.success(f"Peak analysis complete for {len(selected_names)} dataset(s).")
233
- st.dataframe(result_df, width="stretch")
234
- _csv_download(result_df, "batch_peak_analysis.csv")
259
+ st.session_state["ba_pa_result"] = {
260
+ "df": result_df,
261
+ "datasets": list(selected_names),
262
+ }
235
263
  except Exception as exc:
236
264
  st.error(f"Error: {exc}")
265
+ _show_result("ba_pa_result", "batch_peak_analysis.csv")
237
266
 
238
267
 
239
268
  # ══════════════════════════════════════════════════════════════════════════════
@@ -241,10 +270,16 @@ with tab_analysis:
241
270
  # ══════════════════════════════════════════════════════════════════════════════
242
271
  with tab_workflows:
243
272
  st.subheader("Workflow Presets")
244
- st.info(
245
- "Workflows are saved in **config.json** and persist across app restarts. "
246
- "A workflow is a named list of analysis steps stored as JSON."
247
- )
273
+ if runtime.capabilities().persistent_storage:
274
+ st.info(
275
+ "Workflows are saved in **config.json** and persist across app restarts. "
276
+ "A workflow is a named list of analysis steps stored as JSON."
277
+ )
278
+ else:
279
+ st.info(
280
+ "A workflow is a named list of analysis steps stored as JSON. In this version "
281
+ "of EaglePlot, saved workflows last only until you close or reload this tab."
282
+ )
248
283
 
249
284
  _TEMPLATE_STEPS = [
250
285
  {"operation": "batch_statistics", "params": {"columns": []}},