eagleplot 3.0.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.
- eagleplot/Home.py +362 -0
- eagleplot/__init__.py +19 -0
- eagleplot/annotations.py +215 -0
- eagleplot/assets/logo.ico +0 -0
- eagleplot/assets/logo.png +0 -0
- eagleplot/assets/nccu-horiz-logo.png +0 -0
- eagleplot/assets/nccu-wings.png +0 -0
- eagleplot/batch.py +446 -0
- eagleplot/cli.py +719 -0
- eagleplot/data_tools.py +849 -0
- eagleplot/example-data/Artificial/bimodal_distribution.csv +501 -0
- eagleplot/example-data/Artificial/correlation_matrix.npy +0 -0
- eagleplot/example-data/Artificial/gaussian_surface.npy +0 -0
- eagleplot/example-data/Artificial/grouped_categorical.csv +241 -0
- eagleplot/example-data/Artificial/ir_spectrum.csv +1802 -0
- eagleplot/example-data/Artificial/molecular_properties.csv +121 -0
- eagleplot/example-data/Artificial/normal_distribution.csv +401 -0
- eagleplot/example-data/Artificial/reaction_kinetics.csv +121 -0
- eagleplot/example-data/Artificial/scatter_correlation.csv +201 -0
- eagleplot/example-data/Artificial/sine_cosine_waves.csv +301 -0
- eagleplot/gallery/box_plot.png +0 -0
- eagleplot/gallery/contour_plot.png +0 -0
- eagleplot/gallery/distribution_plot.png +0 -0
- eagleplot/gallery/heatmap.png +0 -0
- eagleplot/gallery/histogram.png +0 -0
- eagleplot/gallery/interactive_3d_surface.png +0 -0
- eagleplot/gallery/interactive_heatmap.png +0 -0
- eagleplot/gallery/interactive_histogram.png +0 -0
- eagleplot/gallery/interactive_line.png +0 -0
- eagleplot/gallery/interactive_scatter.png +0 -0
- eagleplot/gallery/line_plot.png +0 -0
- eagleplot/gallery/manifest.json +196 -0
- eagleplot/gallery/regression_plot.png +0 -0
- eagleplot/gallery/scatter_plot.png +0 -0
- eagleplot/io.py +1008 -0
- eagleplot/math.py +1210 -0
- eagleplot/molecular/__init__.py +21 -0
- eagleplot/molecular/atom_data.py +287 -0
- eagleplot/molecular/parsers.py +747 -0
- eagleplot/nist.py +241 -0
- eagleplot/pages/10_Molecular_Viz.py +225 -0
- eagleplot/pages/11_Batch_Analysis.py +325 -0
- eagleplot/pages/12_Help.py +199 -0
- eagleplot/pages/13_Settings.py +388 -0
- eagleplot/pages/1_Data_Upload.py +521 -0
- eagleplot/pages/2_Quick_Plot.py +1693 -0
- eagleplot/pages/3_Analysis_Tools.py +2510 -0
- eagleplot/pages/4_Multi_Plot_Dashboard.py +791 -0
- eagleplot/pages/5_Advanced_Plotting.py +691 -0
- eagleplot/pages/6_Export_Results.py +442 -0
- eagleplot/pages/7_Gallery.py +201 -0
- eagleplot/pages/8_Data_Tools.py +434 -0
- eagleplot/pages/9_Spectroscopy.py +821 -0
- eagleplot/pages/__init__.py +8 -0
- eagleplot/peaks.py +513 -0
- eagleplot/plotting.py +2630 -0
- eagleplot/plugin_loader.py +208 -0
- eagleplot/report.py +448 -0
- eagleplot/signal.py +494 -0
- eagleplot/spectroscopy.py +830 -0
- eagleplot/utils/__init__.py +76 -0
- eagleplot/utils/access.py +77 -0
- eagleplot/utils/data_preview.py +352 -0
- eagleplot/utils/plot_config.py +2721 -0
- eagleplot/utils/session_state.py +535 -0
- eagleplot/utils/theming.py +312 -0
- eagleplot/utils/user_settings.py +294 -0
- eagleplot-3.0.0.dist-info/METADATA +304 -0
- eagleplot-3.0.0.dist-info/RECORD +73 -0
- eagleplot-3.0.0.dist-info/WHEEL +5 -0
- eagleplot-3.0.0.dist-info/entry_points.txt +2 -0
- eagleplot-3.0.0.dist-info/licenses/LICENSE +21 -0
- eagleplot-3.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,442 @@
|
|
|
1
|
+
"""Export Results Page.
|
|
2
|
+
|
|
3
|
+
Download plots, datasets, and analysis results generated during the session.
|
|
4
|
+
|
|
5
|
+
Sections
|
|
6
|
+
--------
|
|
7
|
+
1. Plot History — download each saved plot as PNG (150/300 dpi), SVG, PDF, or HTML
|
|
8
|
+
2. Dataset Export — download the current dataset as CSV or JSON
|
|
9
|
+
3. Analysis Export — download stored analysis results as JSON
|
|
10
|
+
4. Session Save/Load — persist the full session to a JSON file
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import io
|
|
14
|
+
import json
|
|
15
|
+
import tempfile
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
import sys
|
|
18
|
+
|
|
19
|
+
import streamlit as st
|
|
20
|
+
import numpy as np
|
|
21
|
+
import pandas as pd
|
|
22
|
+
|
|
23
|
+
sys.path.insert(0, str(Path(__file__).parent.parent.parent))
|
|
24
|
+
|
|
25
|
+
from eagleplot.utils import initialize_session_state
|
|
26
|
+
from eagleplot.utils.session_state import (
|
|
27
|
+
get_current_dataset,
|
|
28
|
+
get_session_summary,
|
|
29
|
+
save_session_to_file,
|
|
30
|
+
load_session_from_file,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
initialize_session_state()
|
|
34
|
+
|
|
35
|
+
st.title("Export Results")
|
|
36
|
+
st.caption("Download plots, datasets, and analysis results from the current session.")
|
|
37
|
+
|
|
38
|
+
# ── Helper: render a Matplotlib figure to bytes ───────────────────────────────
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _mpl_bytes(fig, fmt: str, dpi: int = 150) -> bytes:
|
|
42
|
+
"""Render a Matplotlib figure to bytes in the requested format."""
|
|
43
|
+
buf = io.BytesIO()
|
|
44
|
+
fig.savefig(buf, format=fmt, dpi=dpi, bbox_inches="tight")
|
|
45
|
+
buf.seek(0)
|
|
46
|
+
return buf.read()
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _plotly_html(fig) -> bytes:
|
|
50
|
+
"""Render a Plotly figure to HTML bytes."""
|
|
51
|
+
return fig.to_html(include_plotlyjs="cdn").encode("utf-8")
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _is_mpl(fig) -> bool:
|
|
55
|
+
try:
|
|
56
|
+
from matplotlib.figure import Figure
|
|
57
|
+
|
|
58
|
+
return isinstance(fig, Figure)
|
|
59
|
+
except ImportError:
|
|
60
|
+
return False
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _is_plotly(fig) -> bool:
|
|
64
|
+
try:
|
|
65
|
+
import plotly.graph_objects as go
|
|
66
|
+
|
|
67
|
+
return isinstance(fig, go.Figure)
|
|
68
|
+
except ImportError:
|
|
69
|
+
return False
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
# ══════════════════════════════════════════════════════════════════════════════
|
|
73
|
+
# Section 1 — Plot History
|
|
74
|
+
# ══════════════════════════════════════════════════════════════════════════════
|
|
75
|
+
st.markdown("## Plot History")
|
|
76
|
+
|
|
77
|
+
plot_history = st.session_state.get("plot_history", [])
|
|
78
|
+
|
|
79
|
+
if not plot_history:
|
|
80
|
+
st.info(
|
|
81
|
+
"No plots saved yet. Generate plots on the Quick Plot, "
|
|
82
|
+
"Analysis Tools, Dashboard, or Advanced Plotting pages."
|
|
83
|
+
)
|
|
84
|
+
else:
|
|
85
|
+
st.markdown(f"**{len(plot_history)} plot(s) in session.**")
|
|
86
|
+
|
|
87
|
+
for i, entry in enumerate(plot_history):
|
|
88
|
+
plot_type = entry.get("type", "plot")
|
|
89
|
+
dataset = entry.get("dataset", "—")
|
|
90
|
+
timestamp = entry.get("timestamp", "")[:19].replace("T", " ")
|
|
91
|
+
fig = entry.get("figure")
|
|
92
|
+
|
|
93
|
+
label = f"Plot {i + 1} — {plot_type} | dataset: {dataset} | {timestamp}"
|
|
94
|
+
|
|
95
|
+
with st.expander(label, expanded=(i == len(plot_history) - 1)):
|
|
96
|
+
if fig is None:
|
|
97
|
+
st.warning(
|
|
98
|
+
"Figure object not available for this entry. "
|
|
99
|
+
"Re-generate the plot to enable downloads."
|
|
100
|
+
)
|
|
101
|
+
continue
|
|
102
|
+
|
|
103
|
+
if _is_mpl(fig):
|
|
104
|
+
c1, c2, c3, c4 = st.columns(4)
|
|
105
|
+
with c1:
|
|
106
|
+
st.download_button(
|
|
107
|
+
"PNG 150 dpi",
|
|
108
|
+
data=_mpl_bytes(fig, "png", dpi=150),
|
|
109
|
+
file_name=f"plot_{i + 1}.png",
|
|
110
|
+
mime="image/png",
|
|
111
|
+
key=f"dl_png150_{i}",
|
|
112
|
+
)
|
|
113
|
+
with c2:
|
|
114
|
+
st.download_button(
|
|
115
|
+
"PNG 300 dpi",
|
|
116
|
+
data=_mpl_bytes(fig, "png", dpi=300),
|
|
117
|
+
file_name=f"plot_{i + 1}_hires.png",
|
|
118
|
+
mime="image/png",
|
|
119
|
+
key=f"dl_png300_{i}",
|
|
120
|
+
)
|
|
121
|
+
with c3:
|
|
122
|
+
st.download_button(
|
|
123
|
+
"SVG",
|
|
124
|
+
data=_mpl_bytes(fig, "svg"),
|
|
125
|
+
file_name=f"plot_{i + 1}.svg",
|
|
126
|
+
mime="image/svg+xml",
|
|
127
|
+
key=f"dl_svg_{i}",
|
|
128
|
+
)
|
|
129
|
+
with c4:
|
|
130
|
+
st.download_button(
|
|
131
|
+
"PDF",
|
|
132
|
+
data=_mpl_bytes(fig, "pdf"),
|
|
133
|
+
file_name=f"plot_{i + 1}.pdf",
|
|
134
|
+
mime="application/pdf",
|
|
135
|
+
key=f"dl_pdf_{i}",
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
elif _is_plotly(fig):
|
|
139
|
+
st.download_button(
|
|
140
|
+
"HTML (interactive)",
|
|
141
|
+
data=_plotly_html(fig),
|
|
142
|
+
file_name=f"plot_{i + 1}.html",
|
|
143
|
+
mime="text/html",
|
|
144
|
+
key=f"dl_html_{i}",
|
|
145
|
+
)
|
|
146
|
+
st.caption(
|
|
147
|
+
"Plotly figures export as self-contained HTML. "
|
|
148
|
+
"PNG export requires the `kaleido` package."
|
|
149
|
+
)
|
|
150
|
+
try:
|
|
151
|
+
import kaleido # noqa: F401
|
|
152
|
+
|
|
153
|
+
png_bytes = fig.to_image(format="png", scale=2)
|
|
154
|
+
st.download_button(
|
|
155
|
+
"PNG (2x)",
|
|
156
|
+
data=png_bytes,
|
|
157
|
+
file_name=f"plot_{i + 1}.png",
|
|
158
|
+
mime="image/png",
|
|
159
|
+
key=f"dl_plotly_png_{i}",
|
|
160
|
+
)
|
|
161
|
+
except ImportError:
|
|
162
|
+
pass
|
|
163
|
+
else:
|
|
164
|
+
st.warning(f"Unknown figure type: {type(fig).__name__}")
|
|
165
|
+
|
|
166
|
+
st.markdown("---")
|
|
167
|
+
|
|
168
|
+
# ══════════════════════════════════════════════════════════════════════════════
|
|
169
|
+
# Section 2 — Dataset Export
|
|
170
|
+
# ══════════════════════════════════════════════════════════════════════════════
|
|
171
|
+
st.markdown("## Dataset Export")
|
|
172
|
+
|
|
173
|
+
current_data = get_current_dataset()
|
|
174
|
+
current_name = st.session_state.get("current_dataset", None)
|
|
175
|
+
|
|
176
|
+
if current_data is None:
|
|
177
|
+
st.info("No dataset loaded. Upload data on the Data Upload page.")
|
|
178
|
+
else:
|
|
179
|
+
st.markdown(f"**Current dataset:** `{current_name}`")
|
|
180
|
+
|
|
181
|
+
if isinstance(current_data, pd.DataFrame):
|
|
182
|
+
c1, c2 = st.columns(2)
|
|
183
|
+
with c1:
|
|
184
|
+
csv_bytes = current_data.to_csv(index=False).encode("utf-8")
|
|
185
|
+
st.download_button(
|
|
186
|
+
"Download as CSV",
|
|
187
|
+
data=csv_bytes,
|
|
188
|
+
file_name=f"{Path(current_name).stem}_export.csv",
|
|
189
|
+
mime="text/csv",
|
|
190
|
+
key="dl_dataset_csv",
|
|
191
|
+
)
|
|
192
|
+
with c2:
|
|
193
|
+
json_bytes = current_data.to_json(orient="records", indent=2).encode("utf-8")
|
|
194
|
+
st.download_button(
|
|
195
|
+
"Download as JSON",
|
|
196
|
+
data=json_bytes,
|
|
197
|
+
file_name=f"{Path(current_name).stem}_export.json",
|
|
198
|
+
mime="application/json",
|
|
199
|
+
key="dl_dataset_json",
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
elif isinstance(current_data, np.ndarray):
|
|
203
|
+
buf = io.BytesIO()
|
|
204
|
+
np.save(buf, current_data)
|
|
205
|
+
buf.seek(0)
|
|
206
|
+
st.download_button(
|
|
207
|
+
"Download as .npy",
|
|
208
|
+
data=buf.read(),
|
|
209
|
+
file_name=f"{Path(current_name).stem}_export.npy",
|
|
210
|
+
mime="application/octet-stream",
|
|
211
|
+
key="dl_dataset_npy",
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
else:
|
|
215
|
+
import pickle as _pickle
|
|
216
|
+
|
|
217
|
+
pkl_bytes = _pickle.dumps(current_data)
|
|
218
|
+
st.download_button(
|
|
219
|
+
"Download as .pkl",
|
|
220
|
+
data=pkl_bytes,
|
|
221
|
+
file_name=f"{Path(current_name).stem}_export.pkl",
|
|
222
|
+
mime="application/octet-stream",
|
|
223
|
+
key="dl_dataset_pkl",
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
st.markdown("---")
|
|
227
|
+
|
|
228
|
+
# ══════════════════════════════════════════════════════════════════════════════
|
|
229
|
+
# Section 3 — Analysis Results Export
|
|
230
|
+
# ══════════════════════════════════════════════════════════════════════════════
|
|
231
|
+
st.markdown("## Analysis Results")
|
|
232
|
+
|
|
233
|
+
analysis_results = st.session_state.get("analysis_results", [])
|
|
234
|
+
|
|
235
|
+
if not analysis_results:
|
|
236
|
+
st.info("No analysis results stored yet. Run analyses on the Analysis Tools page.")
|
|
237
|
+
else:
|
|
238
|
+
st.markdown(f"**{len(analysis_results)} result(s) stored.**")
|
|
239
|
+
|
|
240
|
+
# Build a JSON-serializable list (strip non-serializable items like arrays)
|
|
241
|
+
def _make_serializable(obj):
|
|
242
|
+
if isinstance(obj, np.ndarray):
|
|
243
|
+
return obj.tolist()
|
|
244
|
+
if isinstance(obj, np.integer):
|
|
245
|
+
return int(obj)
|
|
246
|
+
if isinstance(obj, np.floating):
|
|
247
|
+
return float(obj)
|
|
248
|
+
if isinstance(obj, dict):
|
|
249
|
+
return {k: _make_serializable(v) for k, v in obj.items()}
|
|
250
|
+
if isinstance(obj, (list, tuple)):
|
|
251
|
+
return [_make_serializable(x) for x in obj]
|
|
252
|
+
return obj
|
|
253
|
+
|
|
254
|
+
serializable = _make_serializable(analysis_results)
|
|
255
|
+
results_json = json.dumps(serializable, indent=2).encode("utf-8")
|
|
256
|
+
|
|
257
|
+
st.download_button(
|
|
258
|
+
"Download all results as JSON",
|
|
259
|
+
data=results_json,
|
|
260
|
+
file_name="analysis_results.json",
|
|
261
|
+
mime="application/json",
|
|
262
|
+
key="dl_analysis_json",
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
with st.expander("Preview results"):
|
|
266
|
+
for j, res in enumerate(analysis_results):
|
|
267
|
+
r_type = res.get("type", "result")
|
|
268
|
+
r_ts = res.get("timestamp", "")[:19].replace("T", " ")
|
|
269
|
+
st.markdown(f"**{j + 1}. {r_type}** — {r_ts}")
|
|
270
|
+
|
|
271
|
+
st.markdown("---")
|
|
272
|
+
|
|
273
|
+
# ══════════════════════════════════════════════════════════════════════════════
|
|
274
|
+
# Section 4 — PDF Report
|
|
275
|
+
# ══════════════════════════════════════════════════════════════════════════════
|
|
276
|
+
st.markdown("## PDF Report")
|
|
277
|
+
st.markdown(
|
|
278
|
+
"Generate a PDF report containing all matplotlib plots, dataset previews, "
|
|
279
|
+
"and analysis results from the current session."
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
# Import report generator lazily to avoid hard dependency at module load
|
|
283
|
+
try:
|
|
284
|
+
from eagleplot.report import generate_pdf_report as _gen_pdf
|
|
285
|
+
|
|
286
|
+
_report_available = True
|
|
287
|
+
except ImportError:
|
|
288
|
+
_report_available = False
|
|
289
|
+
|
|
290
|
+
if not _report_available:
|
|
291
|
+
st.warning("PDF report module not available.")
|
|
292
|
+
else:
|
|
293
|
+
_report_title = st.text_input(
|
|
294
|
+
"Report title",
|
|
295
|
+
value="EaglePlot Analysis Report",
|
|
296
|
+
key="pdf_report_title",
|
|
297
|
+
)
|
|
298
|
+
_report_author = st.text_input(
|
|
299
|
+
"Author (optional)",
|
|
300
|
+
value="",
|
|
301
|
+
key="pdf_report_author",
|
|
302
|
+
)
|
|
303
|
+
_report_dpi = st.select_slider(
|
|
304
|
+
"Figure resolution (DPI)",
|
|
305
|
+
options=[72, 100, 150, 200, 300],
|
|
306
|
+
value=150,
|
|
307
|
+
key="pdf_report_dpi",
|
|
308
|
+
)
|
|
309
|
+
|
|
310
|
+
if st.button("Generate PDF Report", type="primary", key="pdf_report_btn"):
|
|
311
|
+
_df_datasets = {
|
|
312
|
+
k: v
|
|
313
|
+
for k, v in st.session_state.get("datasets", {}).items()
|
|
314
|
+
if isinstance(v, pd.DataFrame)
|
|
315
|
+
}
|
|
316
|
+
with st.spinner("Generating PDF..."):
|
|
317
|
+
with tempfile.NamedTemporaryFile(suffix=".pdf", delete=False) as _tmp:
|
|
318
|
+
_tmp_path = _tmp.name
|
|
319
|
+
try:
|
|
320
|
+
_gen_pdf(
|
|
321
|
+
filepath=_tmp_path,
|
|
322
|
+
title=_report_title,
|
|
323
|
+
plot_entries=st.session_state.get("plot_history", []),
|
|
324
|
+
datasets=_df_datasets,
|
|
325
|
+
analysis_results=st.session_state.get("analysis_results", []),
|
|
326
|
+
author=_report_author,
|
|
327
|
+
dpi=_report_dpi,
|
|
328
|
+
)
|
|
329
|
+
_pdf_bytes = Path(_tmp_path).read_bytes()
|
|
330
|
+
st.download_button(
|
|
331
|
+
"Download PDF Report",
|
|
332
|
+
data=_pdf_bytes,
|
|
333
|
+
file_name="eagleplot_report.pdf",
|
|
334
|
+
mime="application/pdf",
|
|
335
|
+
key="dl_pdf_report",
|
|
336
|
+
)
|
|
337
|
+
st.success("PDF ready for download.")
|
|
338
|
+
except Exception as _pdf_err:
|
|
339
|
+
st.error(f"Could not generate PDF: {_pdf_err}")
|
|
340
|
+
finally:
|
|
341
|
+
Path(_tmp_path).unlink(missing_ok=True)
|
|
342
|
+
|
|
343
|
+
st.markdown("---")
|
|
344
|
+
|
|
345
|
+
# ══════════════════════════════════════════════════════════════════════════════
|
|
346
|
+
# Section 5 — Session Save / Load
|
|
347
|
+
# ══════════════════════════════════════════════════════════════════════════════
|
|
348
|
+
st.markdown("## Session Save / Load")
|
|
349
|
+
st.markdown(
|
|
350
|
+
"Save the full session (datasets, plot history metadata, analysis results) "
|
|
351
|
+
"to a JSON file. Note: figure objects are **not** included — only metadata."
|
|
352
|
+
)
|
|
353
|
+
|
|
354
|
+
tab_save, tab_load = st.tabs(["Save session", "Load session"])
|
|
355
|
+
|
|
356
|
+
with tab_save:
|
|
357
|
+
summary = get_session_summary()
|
|
358
|
+
st.markdown(
|
|
359
|
+
f"- **Datasets:** {summary['num_datasets']} \n"
|
|
360
|
+
f"- **Plots in history:** {summary['num_plots']} \n"
|
|
361
|
+
f"- **Analysis results:** {summary['num_analyses']}"
|
|
362
|
+
)
|
|
363
|
+
|
|
364
|
+
# The download button used to be drawn inside `if st.button(...)`, so it existed
|
|
365
|
+
# for one rerun only: the next interaction anywhere on the page removed it. The
|
|
366
|
+
# generated bytes now live in session state and the download button is drawn on
|
|
367
|
+
# every rerun while they still match the session. Generation stays an explicit
|
|
368
|
+
# click because serialising large datasets on every rerun would be slow. A-32.
|
|
369
|
+
_session_fingerprint = (
|
|
370
|
+
tuple((name, id(obj)) for name, obj in st.session_state.datasets.items()),
|
|
371
|
+
summary["current_dataset"],
|
|
372
|
+
summary["num_plots"],
|
|
373
|
+
summary["num_analyses"],
|
|
374
|
+
)
|
|
375
|
+
_cached = st.session_state.get("_session_export")
|
|
376
|
+
if _cached is not None and _cached["fingerprint"] != _session_fingerprint:
|
|
377
|
+
_cached = None
|
|
378
|
+
st.session_state.pop("_session_export", None)
|
|
379
|
+
|
|
380
|
+
if st.button(
|
|
381
|
+
"Regenerate session file" if _cached else "Generate session file",
|
|
382
|
+
type="secondary" if _cached else "primary",
|
|
383
|
+
key="session_save_btn",
|
|
384
|
+
):
|
|
385
|
+
with tempfile.NamedTemporaryFile(suffix=".json", delete=False, mode="w") as tmp:
|
|
386
|
+
tmp_path = tmp.name
|
|
387
|
+
|
|
388
|
+
try:
|
|
389
|
+
save_session_to_file(tmp_path)
|
|
390
|
+
_cached = {
|
|
391
|
+
"fingerprint": _session_fingerprint,
|
|
392
|
+
"data": Path(tmp_path).read_bytes(),
|
|
393
|
+
}
|
|
394
|
+
st.session_state["_session_export"] = _cached
|
|
395
|
+
except Exception as exc:
|
|
396
|
+
st.error(f"Could not save session: {exc}")
|
|
397
|
+
finally:
|
|
398
|
+
Path(tmp_path).unlink(missing_ok=True)
|
|
399
|
+
|
|
400
|
+
if _cached is not None:
|
|
401
|
+
st.download_button(
|
|
402
|
+
"Download session.json",
|
|
403
|
+
data=_cached["data"],
|
|
404
|
+
file_name="eagleplot_session.json",
|
|
405
|
+
mime="application/json",
|
|
406
|
+
type="primary",
|
|
407
|
+
key="dl_session_json",
|
|
408
|
+
)
|
|
409
|
+
st.caption(f"{len(_cached['data']) / 1024:,.1f} KB, matches the current session.")
|
|
410
|
+
|
|
411
|
+
with tab_load:
|
|
412
|
+
uploaded = st.file_uploader(
|
|
413
|
+
"Upload a session file (.json)",
|
|
414
|
+
type=["json"],
|
|
415
|
+
key="session_upload",
|
|
416
|
+
)
|
|
417
|
+
if uploaded is not None:
|
|
418
|
+
if st.button("Restore session", type="primary", key="session_load_btn"):
|
|
419
|
+
with tempfile.NamedTemporaryFile(suffix=".json", delete=False, mode="wb") as tmp:
|
|
420
|
+
tmp.write(uploaded.read())
|
|
421
|
+
tmp_path = tmp.name
|
|
422
|
+
|
|
423
|
+
try:
|
|
424
|
+
skipped = load_session_from_file(tmp_path)
|
|
425
|
+
if skipped:
|
|
426
|
+
# Deliberately no st.rerun() here — a rerun would discard this
|
|
427
|
+
# warning before the user ever sees it. The data is already in
|
|
428
|
+
# session state, so navigating to another page picks it up.
|
|
429
|
+
st.warning(
|
|
430
|
+
"Session restored, but some entries could not be loaded:\n\n"
|
|
431
|
+
+ "\n".join(f"- {note}" for note in skipped)
|
|
432
|
+
+ "\n\nSessions saved by Plottle 2.0.1 or earlier stored arrays in a "
|
|
433
|
+
"format that is no longer accepted. Re-upload the original data files "
|
|
434
|
+
"and save the session again."
|
|
435
|
+
)
|
|
436
|
+
else:
|
|
437
|
+
st.success("Session restored. Navigate to other pages to use the loaded data.")
|
|
438
|
+
st.rerun()
|
|
439
|
+
except Exception as exc:
|
|
440
|
+
st.error(f"Could not load session: {exc}")
|
|
441
|
+
finally:
|
|
442
|
+
Path(tmp_path).unlink(missing_ok=True)
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
"""Gallery Page.
|
|
2
|
+
|
|
3
|
+
Displays pre-rendered example figures grouped by library (Matplotlib,
|
|
4
|
+
Seaborn, Plotly). Each card shows a thumbnail, title, description, and a
|
|
5
|
+
"Use this config" button that pre-fills Quick Plot with the figure's settings.
|
|
6
|
+
|
|
7
|
+
To populate the gallery run once from the repo root::
|
|
8
|
+
|
|
9
|
+
python generate_gallery.py
|
|
10
|
+
|
|
11
|
+
The script writes PNGs and ``docs/gallery/manifest.json``.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import json
|
|
17
|
+
import sys
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
|
|
20
|
+
import streamlit as st
|
|
21
|
+
|
|
22
|
+
_APP_ROOT = Path(__file__).resolve().parent.parent.parent
|
|
23
|
+
# Packaged alongside the code so the Gallery is populated after `pip install`
|
|
24
|
+
# rather than only in a source checkout -- see audit A-09.
|
|
25
|
+
_GALLERY_DIR = Path(__file__).resolve().parent.parent / "gallery"
|
|
26
|
+
_MANIFEST_PATH = _GALLERY_DIR / "manifest.json"
|
|
27
|
+
|
|
28
|
+
sys.path.insert(0, str(_APP_ROOT))
|
|
29
|
+
|
|
30
|
+
from eagleplot.utils import initialize_session_state
|
|
31
|
+
|
|
32
|
+
initialize_session_state()
|
|
33
|
+
|
|
34
|
+
# ── Equal-height cards ────────────────────────────────────────────────────────
|
|
35
|
+
st.markdown(
|
|
36
|
+
"""
|
|
37
|
+
<style>
|
|
38
|
+
[data-testid="stHorizontalBlock"] {
|
|
39
|
+
align-items: stretch;
|
|
40
|
+
}
|
|
41
|
+
[data-testid="stHorizontalBlock"] > [data-testid="column"] {
|
|
42
|
+
display: flex;
|
|
43
|
+
flex-direction: column;
|
|
44
|
+
}
|
|
45
|
+
[data-testid="stHorizontalBlock"] > [data-testid="column"]
|
|
46
|
+
> [data-testid="stVerticalBlock"] {
|
|
47
|
+
flex: 1;
|
|
48
|
+
display: flex;
|
|
49
|
+
flex-direction: column;
|
|
50
|
+
}
|
|
51
|
+
[data-testid="stHorizontalBlock"] > [data-testid="column"]
|
|
52
|
+
> [data-testid="stVerticalBlock"]
|
|
53
|
+
> [data-testid="stVerticalBlockBorderWrapper"] {
|
|
54
|
+
flex: 1;
|
|
55
|
+
display: flex;
|
|
56
|
+
flex-direction: column;
|
|
57
|
+
}
|
|
58
|
+
[data-testid="stVerticalBlockBorderWrapper"] > [data-testid="stVerticalBlock"] {
|
|
59
|
+
flex: 1;
|
|
60
|
+
display: flex;
|
|
61
|
+
flex-direction: column;
|
|
62
|
+
justify-content: space-between;
|
|
63
|
+
}
|
|
64
|
+
</style>
|
|
65
|
+
""",
|
|
66
|
+
unsafe_allow_html=True,
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
# ── Page header ───────────────────────────────────────────────────────────────
|
|
70
|
+
st.title("Gallery")
|
|
71
|
+
st.caption(
|
|
72
|
+
"Browse example figures. Click **Use this config** to pre-fill Quick Plot "
|
|
73
|
+
"with that figure's settings."
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
# ── Guard: manifest missing ───────────────────────────────────────────────────
|
|
77
|
+
if not _MANIFEST_PATH.exists():
|
|
78
|
+
st.warning(
|
|
79
|
+
"No gallery figures found. "
|
|
80
|
+
"Run **`python generate_gallery.py`** from the repo root to generate them."
|
|
81
|
+
)
|
|
82
|
+
st.code("python generate_gallery.py", language="bash")
|
|
83
|
+
st.stop()
|
|
84
|
+
|
|
85
|
+
# ── Load manifest ─────────────────────────────────────────────────────────────
|
|
86
|
+
with open(_MANIFEST_PATH, encoding="utf-8") as fh:
|
|
87
|
+
_manifest: list[dict] = json.load(fh)
|
|
88
|
+
|
|
89
|
+
# ── Group by library ──────────────────────────────────────────────────────────
|
|
90
|
+
_LIBRARY_ORDER = ["Matplotlib", "Seaborn", "Plotly"]
|
|
91
|
+
|
|
92
|
+
_by_library: dict[str, list[dict]] = {}
|
|
93
|
+
for entry in _manifest:
|
|
94
|
+
_by_library.setdefault(entry.get("library", "Other"), []).append(entry)
|
|
95
|
+
|
|
96
|
+
# Add any user-saved entries that live in the gallery dir but not in manifest
|
|
97
|
+
_user_entries: list[dict] = []
|
|
98
|
+
_manifest_keys = {e["key"] for e in _manifest}
|
|
99
|
+
for png in sorted(_GALLERY_DIR.glob("user_*.png")):
|
|
100
|
+
key = png.stem
|
|
101
|
+
meta_path = _GALLERY_DIR / f"{key}.json"
|
|
102
|
+
if meta_path.exists():
|
|
103
|
+
try:
|
|
104
|
+
with open(meta_path, encoding="utf-8") as fh:
|
|
105
|
+
meta = json.load(fh)
|
|
106
|
+
if key not in _manifest_keys:
|
|
107
|
+
_user_entries.append(meta)
|
|
108
|
+
except Exception:
|
|
109
|
+
pass
|
|
110
|
+
|
|
111
|
+
if _user_entries:
|
|
112
|
+
_by_library.setdefault("My Figures", _user_entries)
|
|
113
|
+
|
|
114
|
+
# ── Render sections ───────────────────────────────────────────────────────────
|
|
115
|
+
_lib_order = [lib for lib in _LIBRARY_ORDER if lib in _by_library]
|
|
116
|
+
_lib_order += [lib for lib in _by_library if lib not in _LIBRARY_ORDER]
|
|
117
|
+
|
|
118
|
+
# _DEFAULTS_KEY_MAP inverse — needed to restore settings into widget keys.
|
|
119
|
+
# Mirror of the map in 2_Quick_Plot.py.
|
|
120
|
+
_CFG_TO_WIDGET: dict[str, str] = {
|
|
121
|
+
"fontfamily": "_shared_fontfamily",
|
|
122
|
+
"fontcolor": "_shared_fontcolor",
|
|
123
|
+
"linewidth": "_shared_lw",
|
|
124
|
+
"grid": "_shared_grid",
|
|
125
|
+
"grid_linestyle": "_shared_grid_ls",
|
|
126
|
+
"grid_which": "_shared_grid_which",
|
|
127
|
+
"legend_frameon": "_shared_leg_frame",
|
|
128
|
+
"legend_framealpha": "_shared_leg_bg",
|
|
129
|
+
"legend_position": "_shared_leg_pos",
|
|
130
|
+
"color_palette": "_shared_palette",
|
|
131
|
+
"fontsize": "_shared_ply_fontsize",
|
|
132
|
+
"fontsize_label": "_shared_fs_label",
|
|
133
|
+
"fontsize_tick": "_shared_fs_tick",
|
|
134
|
+
"fontsize_title": "_shared_fs_title",
|
|
135
|
+
"fontsize_legend": "_shared_fs_legend",
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _apply_gallery_config(cfg: dict) -> None:
|
|
140
|
+
"""Write a gallery config dict into the shared Quick Plot widget keys."""
|
|
141
|
+
for cfg_key, widget_key in _CFG_TO_WIDGET.items():
|
|
142
|
+
if cfg_key in cfg:
|
|
143
|
+
# Attempt type coercion — manifest stores everything as strings.
|
|
144
|
+
val = cfg[cfg_key]
|
|
145
|
+
current = st.session_state.get(widget_key)
|
|
146
|
+
if isinstance(current, bool):
|
|
147
|
+
st.session_state[widget_key] = str(val).lower() in ("true", "1")
|
|
148
|
+
elif isinstance(current, (int, float)) or current is None:
|
|
149
|
+
try:
|
|
150
|
+
st.session_state[widget_key] = (
|
|
151
|
+
type(current)(val) if current is not None else val
|
|
152
|
+
)
|
|
153
|
+
except (ValueError, TypeError):
|
|
154
|
+
st.session_state[widget_key] = val
|
|
155
|
+
else:
|
|
156
|
+
st.session_state[widget_key] = val
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
for lib in _lib_order:
|
|
160
|
+
entries = _by_library[lib]
|
|
161
|
+
st.markdown(f"## {lib}")
|
|
162
|
+
|
|
163
|
+
# 3-column grid — new st.columns() per row so cards align across columns
|
|
164
|
+
for row_start in range(0, len(entries), 3):
|
|
165
|
+
row_entries = entries[row_start : row_start + 3]
|
|
166
|
+
cols = st.columns(3)
|
|
167
|
+
for col, entry in zip(cols, row_entries):
|
|
168
|
+
with col:
|
|
169
|
+
with st.container(border=True):
|
|
170
|
+
img_path = _GALLERY_DIR / entry.get("filename", f"{entry['key']}.png")
|
|
171
|
+
if img_path.exists():
|
|
172
|
+
st.image(str(img_path), width="stretch")
|
|
173
|
+
else:
|
|
174
|
+
st.caption("_(image not generated yet)_")
|
|
175
|
+
|
|
176
|
+
st.markdown(
|
|
177
|
+
f"<div style='min-height:5.5em'>"
|
|
178
|
+
f"<strong>{entry['title']}</strong><br>"
|
|
179
|
+
f"<span style='font-size:0.82rem;color:inherit'>"
|
|
180
|
+
f"{entry.get('description', '')}</span><br><br>"
|
|
181
|
+
f"<span style='font-size:0.8rem'>"
|
|
182
|
+
f"<em>Best for:</em> {entry.get('best_for', '—')}</span>"
|
|
183
|
+
f"</div>"
|
|
184
|
+
f"<p style='font-size:0.78rem;margin-top:0.3rem'>"
|
|
185
|
+
f"Dataset: <code>{entry.get('dataset', '—')}</code></p>",
|
|
186
|
+
unsafe_allow_html=True,
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
if st.button(
|
|
190
|
+
"Use this config",
|
|
191
|
+
key=f"gallery_apply_{entry['key']}",
|
|
192
|
+
width="stretch",
|
|
193
|
+
):
|
|
194
|
+
_apply_gallery_config(entry.get("config", {}))
|
|
195
|
+
st.session_state["qp_category"] = lib
|
|
196
|
+
st.success(
|
|
197
|
+
f"Config loaded. Go to **Quick Plot** and choose "
|
|
198
|
+
f"**{entry['title']}** to generate the figure."
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
st.markdown("---")
|