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,535 @@
|
|
|
1
|
+
"""Session State Management for Streamlit GUI.
|
|
2
|
+
|
|
3
|
+
This module provides centralized session state management to persist data,
|
|
4
|
+
plots, and analysis results across page navigation in the Streamlit multi-page app.
|
|
5
|
+
|
|
6
|
+
Functions
|
|
7
|
+
---------
|
|
8
|
+
initialize_session_state()
|
|
9
|
+
Initialize all session state variables with defaults
|
|
10
|
+
add_dataset(name, data, metadata=None)
|
|
11
|
+
Add a dataset to session state
|
|
12
|
+
get_current_dataset()
|
|
13
|
+
Retrieve the currently selected dataset
|
|
14
|
+
get_dataset(name)
|
|
15
|
+
Retrieve a specific dataset by name
|
|
16
|
+
delete_dataset(name)
|
|
17
|
+
Remove a dataset from session state
|
|
18
|
+
add_plot_to_history(plot_data)
|
|
19
|
+
Add a generated plot to history
|
|
20
|
+
clear_plot_history()
|
|
21
|
+
Remove all plots from history
|
|
22
|
+
add_analysis_result(result_data)
|
|
23
|
+
Add analysis result to session state
|
|
24
|
+
save_session_to_file(filepath)
|
|
25
|
+
Persist session state to JSON file
|
|
26
|
+
load_session_from_file(filepath)
|
|
27
|
+
Restore session state from JSON file
|
|
28
|
+
clear_session()
|
|
29
|
+
Reset all session state to defaults
|
|
30
|
+
|
|
31
|
+
Examples
|
|
32
|
+
--------
|
|
33
|
+
>>> import streamlit as st
|
|
34
|
+
>>> from eagleplot.utils.session_state import initialize_session_state, add_dataset
|
|
35
|
+
>>> initialize_session_state()
|
|
36
|
+
>>> data = np.array([1, 2, 3, 4, 5])
|
|
37
|
+
>>> add_dataset('test_data.npy', data)
|
|
38
|
+
>>> dataset = get_current_dataset()
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
import io
|
|
42
|
+
import streamlit as st
|
|
43
|
+
import json
|
|
44
|
+
import base64
|
|
45
|
+
from datetime import datetime
|
|
46
|
+
from pathlib import Path
|
|
47
|
+
from typing import Any, Dict, List, Optional
|
|
48
|
+
import numpy as np
|
|
49
|
+
import pandas as pd
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def initialize_session_state():
|
|
53
|
+
"""Initialize all session state variables with default values.
|
|
54
|
+
|
|
55
|
+
This should be called once at the start of the app (in gui.py).
|
|
56
|
+
Creates the following state variables:
|
|
57
|
+
- datasets: Dict[str, Any] - Loaded datasets keyed by filename
|
|
58
|
+
- current_dataset: str | None - Name of currently selected dataset
|
|
59
|
+
- plot_history: List[Dict] - History of generated plots
|
|
60
|
+
- analysis_results: List[Dict] - Results from mathematical operations
|
|
61
|
+
- plot_config: Dict - Current plot configuration settings
|
|
62
|
+
- export_queue: List[Dict] - Items queued for export
|
|
63
|
+
"""
|
|
64
|
+
if "datasets" not in st.session_state:
|
|
65
|
+
st.session_state.datasets = {}
|
|
66
|
+
|
|
67
|
+
if "dataset_metadata" not in st.session_state:
|
|
68
|
+
st.session_state.dataset_metadata = {}
|
|
69
|
+
|
|
70
|
+
if "current_dataset" not in st.session_state:
|
|
71
|
+
st.session_state.current_dataset = None
|
|
72
|
+
|
|
73
|
+
if "plot_history" not in st.session_state:
|
|
74
|
+
st.session_state.plot_history = []
|
|
75
|
+
|
|
76
|
+
if "analysis_results" not in st.session_state:
|
|
77
|
+
st.session_state.analysis_results = []
|
|
78
|
+
|
|
79
|
+
if "plot_config" not in st.session_state:
|
|
80
|
+
st.session_state.plot_config = {
|
|
81
|
+
"default_figsize": (8, 6),
|
|
82
|
+
"default_dpi": 100,
|
|
83
|
+
"default_style": "default",
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if "export_queue" not in st.session_state:
|
|
87
|
+
st.session_state.export_queue = []
|
|
88
|
+
|
|
89
|
+
if "mol_vib_data" not in st.session_state:
|
|
90
|
+
st.session_state.mol_vib_data = None
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def add_dataset(name: str, data: Any, metadata: Optional[Dict] = None):
|
|
94
|
+
"""Add a dataset to session state.
|
|
95
|
+
|
|
96
|
+
Parameters
|
|
97
|
+
----------
|
|
98
|
+
name : str
|
|
99
|
+
Dataset name (usually filename)
|
|
100
|
+
data : Any
|
|
101
|
+
Dataset (DataFrame, ndarray, dict, etc.)
|
|
102
|
+
metadata : dict, optional
|
|
103
|
+
Additional metadata (file size, upload time, etc.)
|
|
104
|
+
"""
|
|
105
|
+
st.session_state.datasets[name] = data
|
|
106
|
+
|
|
107
|
+
# Store metadata
|
|
108
|
+
if metadata is None:
|
|
109
|
+
metadata = {}
|
|
110
|
+
|
|
111
|
+
metadata["added_time"] = datetime.now().isoformat()
|
|
112
|
+
metadata["data_type"] = type(data).__name__
|
|
113
|
+
|
|
114
|
+
# Add shape/size info
|
|
115
|
+
if isinstance(data, pd.DataFrame):
|
|
116
|
+
metadata["shape"] = data.shape
|
|
117
|
+
metadata["columns"] = list(data.columns)
|
|
118
|
+
metadata["dtypes"] = {col: str(dtype) for col, dtype in data.dtypes.items()}
|
|
119
|
+
elif isinstance(data, np.ndarray):
|
|
120
|
+
metadata["shape"] = data.shape
|
|
121
|
+
metadata["dtype"] = str(data.dtype)
|
|
122
|
+
elif isinstance(data, (list, tuple)):
|
|
123
|
+
metadata["length"] = len(data)
|
|
124
|
+
|
|
125
|
+
st.session_state.dataset_metadata[name] = metadata
|
|
126
|
+
|
|
127
|
+
# Set as current dataset if it's the first one
|
|
128
|
+
if st.session_state.current_dataset is None:
|
|
129
|
+
st.session_state.current_dataset = name
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def get_current_dataset() -> Optional[Any]:
|
|
133
|
+
"""Retrieve the currently selected dataset.
|
|
134
|
+
|
|
135
|
+
Returns
|
|
136
|
+
-------
|
|
137
|
+
data : Any or None
|
|
138
|
+
Current dataset, or None if no dataset is selected
|
|
139
|
+
"""
|
|
140
|
+
if st.session_state.current_dataset is None:
|
|
141
|
+
return None
|
|
142
|
+
|
|
143
|
+
return st.session_state.datasets.get(st.session_state.current_dataset)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def get_dataset(name: str) -> Optional[Any]:
|
|
147
|
+
"""Retrieve a specific dataset by name.
|
|
148
|
+
|
|
149
|
+
Parameters
|
|
150
|
+
----------
|
|
151
|
+
name : str
|
|
152
|
+
Dataset name
|
|
153
|
+
|
|
154
|
+
Returns
|
|
155
|
+
-------
|
|
156
|
+
data : Any or None
|
|
157
|
+
Dataset if found, None otherwise
|
|
158
|
+
"""
|
|
159
|
+
return st.session_state.datasets.get(name)
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def delete_dataset(name: str) -> bool:
|
|
163
|
+
"""Remove a dataset from session state.
|
|
164
|
+
|
|
165
|
+
Parameters
|
|
166
|
+
----------
|
|
167
|
+
name : str
|
|
168
|
+
Dataset name to delete
|
|
169
|
+
|
|
170
|
+
Returns
|
|
171
|
+
-------
|
|
172
|
+
success : bool
|
|
173
|
+
True if dataset was deleted, False if not found
|
|
174
|
+
"""
|
|
175
|
+
if name in st.session_state.datasets:
|
|
176
|
+
del st.session_state.datasets[name]
|
|
177
|
+
del st.session_state.dataset_metadata[name]
|
|
178
|
+
|
|
179
|
+
# Update current dataset if we deleted it
|
|
180
|
+
if st.session_state.current_dataset == name:
|
|
181
|
+
if st.session_state.datasets:
|
|
182
|
+
st.session_state.current_dataset = list(st.session_state.datasets.keys())[0]
|
|
183
|
+
else:
|
|
184
|
+
st.session_state.current_dataset = None
|
|
185
|
+
|
|
186
|
+
return True
|
|
187
|
+
|
|
188
|
+
return False
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
_MAX_PLOT_HISTORY = 50
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def add_plot_to_history(plot_data: Dict):
|
|
195
|
+
"""Add a generated plot to history (capped at ``_MAX_PLOT_HISTORY`` entries).
|
|
196
|
+
|
|
197
|
+
Parameters
|
|
198
|
+
----------
|
|
199
|
+
plot_data : dict
|
|
200
|
+
Plot information with keys:
|
|
201
|
+
- type: Plot type (e.g., 'histogram', 'scatter')
|
|
202
|
+
- dataset: Dataset name used
|
|
203
|
+
- config: Plot configuration dict
|
|
204
|
+
- timestamp: When plot was created
|
|
205
|
+
- figure: Matplotlib/Plotly figure object (optional)
|
|
206
|
+
|
|
207
|
+
Notes
|
|
208
|
+
-----
|
|
209
|
+
Figure objects are stripped from older entries once the history exceeds
|
|
210
|
+
five entries to prevent unbounded memory growth.
|
|
211
|
+
"""
|
|
212
|
+
if "timestamp" not in plot_data:
|
|
213
|
+
plot_data["timestamp"] = datetime.now().isoformat()
|
|
214
|
+
|
|
215
|
+
st.session_state.plot_history.append(plot_data)
|
|
216
|
+
|
|
217
|
+
# Drop figure objects from all but the 5 most recent entries.
|
|
218
|
+
history = st.session_state.plot_history
|
|
219
|
+
for entry in history[:-5]:
|
|
220
|
+
entry.pop("figure", None)
|
|
221
|
+
|
|
222
|
+
# Rolling cap — evict oldest entries when limit is exceeded.
|
|
223
|
+
if len(history) > _MAX_PLOT_HISTORY:
|
|
224
|
+
st.session_state.plot_history = history[-_MAX_PLOT_HISTORY:]
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def clear_plot_history():
|
|
228
|
+
"""Remove all plots from history."""
|
|
229
|
+
st.session_state.plot_history = []
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def add_analysis_result(result_data: Dict):
|
|
233
|
+
"""Add analysis result to session state.
|
|
234
|
+
|
|
235
|
+
Parameters
|
|
236
|
+
----------
|
|
237
|
+
result_data : dict
|
|
238
|
+
Analysis result with keys:
|
|
239
|
+
- type: Analysis type (e.g., 'statistics', 'curve_fit')
|
|
240
|
+
- dataset: Dataset name used
|
|
241
|
+
- results: Analysis output dict
|
|
242
|
+
- timestamp: When analysis was performed
|
|
243
|
+
"""
|
|
244
|
+
if "timestamp" not in result_data:
|
|
245
|
+
result_data["timestamp"] = datetime.now().isoformat()
|
|
246
|
+
|
|
247
|
+
st.session_state.analysis_results.append(result_data)
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def _serialize_data(data: Any) -> Any:
|
|
251
|
+
"""Serialize data for JSON storage.
|
|
252
|
+
|
|
253
|
+
Only types with a safe, self-describing encoding are serialized:
|
|
254
|
+
DataFrames (via ``to_json``), numeric/boolean ndarrays (via an explicit
|
|
255
|
+
dtype + shape + raw-buffer triple), JSON scalars, and lists/dicts of the
|
|
256
|
+
above. Anything else is replaced by an ``unsupported`` placeholder rather
|
|
257
|
+
than being pickled — see Notes.
|
|
258
|
+
|
|
259
|
+
Parameters
|
|
260
|
+
----------
|
|
261
|
+
data : Any
|
|
262
|
+
Data to serialize
|
|
263
|
+
|
|
264
|
+
Returns
|
|
265
|
+
-------
|
|
266
|
+
serialized : Any
|
|
267
|
+
Serialized representation — a dict for tagged types, otherwise the
|
|
268
|
+
value itself.
|
|
269
|
+
|
|
270
|
+
Notes
|
|
271
|
+
-----
|
|
272
|
+
Session files are written and re-read through the GUI's Export page, which
|
|
273
|
+
accepts uploads. Any ``pickle``-based encoding would therefore make
|
|
274
|
+
"open a session file" equivalent to "execute arbitrary code", so pickle is
|
|
275
|
+
deliberately not used here. Object-dtype arrays are also refused, since
|
|
276
|
+
their contents cannot be represented without pickling.
|
|
277
|
+
"""
|
|
278
|
+
if isinstance(data, pd.DataFrame):
|
|
279
|
+
return {"__type__": "DataFrame", "__data__": data.to_json(orient="split")}
|
|
280
|
+
elif isinstance(data, np.ndarray):
|
|
281
|
+
if data.dtype.hasobject:
|
|
282
|
+
return {
|
|
283
|
+
"__type__": "unsupported",
|
|
284
|
+
"__class__": f"numpy.ndarray[{data.dtype}]",
|
|
285
|
+
"__reason__": "object-dtype arrays cannot be serialized safely",
|
|
286
|
+
}
|
|
287
|
+
return {
|
|
288
|
+
"__type__": "ndarray",
|
|
289
|
+
"__dtype__": data.dtype.str,
|
|
290
|
+
"__shape__": list(data.shape),
|
|
291
|
+
"__data__": base64.b64encode(np.ascontiguousarray(data).tobytes()).decode("utf-8"),
|
|
292
|
+
}
|
|
293
|
+
elif isinstance(data, (int, float, str, bool, type(None))):
|
|
294
|
+
return data
|
|
295
|
+
elif isinstance(data, (list, tuple)):
|
|
296
|
+
return [_serialize_data(item) for item in data]
|
|
297
|
+
elif isinstance(data, dict):
|
|
298
|
+
return {key: _serialize_data(value) for key, value in data.items()}
|
|
299
|
+
else:
|
|
300
|
+
return {
|
|
301
|
+
"__type__": "unsupported",
|
|
302
|
+
"__class__": type(data).__name__,
|
|
303
|
+
"__reason__": "no safe JSON encoding for this type",
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
def _decode_ndarray(data: Dict) -> np.ndarray:
|
|
308
|
+
"""Rebuild an ndarray from a serialized dtype/shape/buffer triple.
|
|
309
|
+
|
|
310
|
+
Parameters
|
|
311
|
+
----------
|
|
312
|
+
data : dict
|
|
313
|
+
Mapping with ``__dtype__``, ``__shape__``, and ``__data__`` keys as
|
|
314
|
+
written by :func:`_serialize_data`.
|
|
315
|
+
|
|
316
|
+
Returns
|
|
317
|
+
-------
|
|
318
|
+
numpy.ndarray
|
|
319
|
+
A writable array with the recorded dtype and shape.
|
|
320
|
+
|
|
321
|
+
Raises
|
|
322
|
+
------
|
|
323
|
+
ValueError
|
|
324
|
+
If the dtype is absent, is an object dtype, or if the decoded buffer
|
|
325
|
+
length does not match ``dtype.itemsize * prod(shape)``.
|
|
326
|
+
"""
|
|
327
|
+
dtype_str = data.get("__dtype__")
|
|
328
|
+
if not dtype_str:
|
|
329
|
+
raise ValueError("ndarray entry is missing '__dtype__'")
|
|
330
|
+
|
|
331
|
+
dtype = np.dtype(dtype_str)
|
|
332
|
+
if dtype.hasobject:
|
|
333
|
+
raise ValueError(f"refusing to decode object dtype {dtype!r}")
|
|
334
|
+
|
|
335
|
+
shape = tuple(int(n) for n in data.get("__shape__", ()))
|
|
336
|
+
if any(n < 0 for n in shape):
|
|
337
|
+
raise ValueError(f"invalid shape {shape}")
|
|
338
|
+
|
|
339
|
+
buf = base64.b64decode(data["__data__"])
|
|
340
|
+
# np.frombuffer validates that the buffer is a whole number of items, but
|
|
341
|
+
# not that it matches `shape`; check explicitly so a truncated or padded
|
|
342
|
+
# file fails here rather than producing a silently wrong array.
|
|
343
|
+
expected = dtype.itemsize * int(np.prod(shape, dtype=np.int64)) if shape else dtype.itemsize
|
|
344
|
+
if len(buf) != expected:
|
|
345
|
+
raise ValueError(
|
|
346
|
+
f"ndarray buffer is {len(buf)} bytes; expected {expected} "
|
|
347
|
+
f"for dtype {dtype.str} and shape {shape}"
|
|
348
|
+
)
|
|
349
|
+
|
|
350
|
+
# frombuffer returns a read-only view onto `buf`; copy so callers can write.
|
|
351
|
+
return np.frombuffer(buf, dtype=dtype).reshape(shape).copy()
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
def _deserialize_data(data: Any, skipped: Optional[List[str]] = None) -> Any:
|
|
355
|
+
"""Deserialize data from JSON storage.
|
|
356
|
+
|
|
357
|
+
Parameters
|
|
358
|
+
----------
|
|
359
|
+
data : Any
|
|
360
|
+
Serialized data
|
|
361
|
+
skipped : list of str, optional
|
|
362
|
+
If given, a human-readable note is appended for every entry that could
|
|
363
|
+
not be restored. Callers use this to tell the user what was dropped.
|
|
364
|
+
|
|
365
|
+
Returns
|
|
366
|
+
-------
|
|
367
|
+
deserialized : Any
|
|
368
|
+
Deserialized data, or ``None`` for entries that could not be restored.
|
|
369
|
+
|
|
370
|
+
Notes
|
|
371
|
+
-----
|
|
372
|
+
Entries written by Plottle 2.0.1 and earlier used a pickle-based encoding
|
|
373
|
+
(``__type__`` of ``"pickled"``, and ``"ndarray"`` without a ``__dtype__``).
|
|
374
|
+
Those are refused rather than unpickled — see :func:`_serialize_data`.
|
|
375
|
+
"""
|
|
376
|
+
if isinstance(data, dict):
|
|
377
|
+
kind = data.get("__type__")
|
|
378
|
+
if kind is None:
|
|
379
|
+
return {key: _deserialize_data(value, skipped) for key, value in data.items()}
|
|
380
|
+
|
|
381
|
+
if kind == "DataFrame":
|
|
382
|
+
return pd.read_json(io.StringIO(data["__data__"]), orient="split")
|
|
383
|
+
|
|
384
|
+
if kind == "ndarray":
|
|
385
|
+
if "__dtype__" not in data:
|
|
386
|
+
if skipped is not None:
|
|
387
|
+
skipped.append(
|
|
388
|
+
"an array saved by Plottle 2.0.1 or earlier "
|
|
389
|
+
"(pickle-encoded; refused for safety)"
|
|
390
|
+
)
|
|
391
|
+
return None
|
|
392
|
+
try:
|
|
393
|
+
return _decode_ndarray(data)
|
|
394
|
+
except (ValueError, TypeError) as exc:
|
|
395
|
+
if skipped is not None:
|
|
396
|
+
skipped.append(f"a corrupt array entry ({exc})")
|
|
397
|
+
return None
|
|
398
|
+
|
|
399
|
+
if kind == "pickled":
|
|
400
|
+
if skipped is not None:
|
|
401
|
+
skipped.append(
|
|
402
|
+
f"a pickled {data.get('__class__', 'object')} saved by Plottle 2.0.1 "
|
|
403
|
+
"or earlier (refused for safety)"
|
|
404
|
+
)
|
|
405
|
+
return None
|
|
406
|
+
|
|
407
|
+
if kind == "unsupported":
|
|
408
|
+
if skipped is not None:
|
|
409
|
+
skipped.append(
|
|
410
|
+
f"{data.get('__class__', 'an object')} "
|
|
411
|
+
f"({data.get('__reason__', 'unsupported type')})"
|
|
412
|
+
)
|
|
413
|
+
return None
|
|
414
|
+
|
|
415
|
+
if skipped is not None:
|
|
416
|
+
skipped.append(f"an entry of unrecognized type {kind!r}")
|
|
417
|
+
return None
|
|
418
|
+
|
|
419
|
+
elif isinstance(data, list):
|
|
420
|
+
return [_deserialize_data(item, skipped) for item in data]
|
|
421
|
+
else:
|
|
422
|
+
return data
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
def save_session_to_file(filepath: str):
|
|
426
|
+
"""Persist session state to JSON file.
|
|
427
|
+
|
|
428
|
+
Parameters
|
|
429
|
+
----------
|
|
430
|
+
filepath : str
|
|
431
|
+
Path to save session file
|
|
432
|
+
|
|
433
|
+
Notes
|
|
434
|
+
-----
|
|
435
|
+
This saves datasets, plot history, and analysis results.
|
|
436
|
+
Matplotlib figure objects are NOT saved (too large).
|
|
437
|
+
"""
|
|
438
|
+
session_data = {
|
|
439
|
+
"version": "1.0",
|
|
440
|
+
"saved_at": datetime.now().isoformat(),
|
|
441
|
+
"datasets": {},
|
|
442
|
+
"dataset_metadata": st.session_state.dataset_metadata,
|
|
443
|
+
"current_dataset": st.session_state.current_dataset,
|
|
444
|
+
"plot_history": [
|
|
445
|
+
{k: v for k, v in plot.items() if k != "figure"}
|
|
446
|
+
for plot in st.session_state.plot_history
|
|
447
|
+
],
|
|
448
|
+
"analysis_results": st.session_state.analysis_results,
|
|
449
|
+
"plot_config": st.session_state.plot_config,
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
# Serialize datasets
|
|
453
|
+
for name, data in st.session_state.datasets.items():
|
|
454
|
+
session_data["datasets"][name] = _serialize_data(data)
|
|
455
|
+
|
|
456
|
+
filepath = Path(filepath)
|
|
457
|
+
with open(filepath, "w") as f:
|
|
458
|
+
json.dump(session_data, f, indent=2)
|
|
459
|
+
|
|
460
|
+
|
|
461
|
+
def load_session_from_file(filepath: str) -> List[str]:
|
|
462
|
+
"""Restore session state from JSON file.
|
|
463
|
+
|
|
464
|
+
Parameters
|
|
465
|
+
----------
|
|
466
|
+
filepath : str
|
|
467
|
+
Path to session file
|
|
468
|
+
|
|
469
|
+
Returns
|
|
470
|
+
-------
|
|
471
|
+
skipped : list of str
|
|
472
|
+
One human-readable note per dataset that could not be restored — for
|
|
473
|
+
example entries saved by Plottle 2.0.1 or earlier, which used a
|
|
474
|
+
pickle-based encoding that is refused for safety. Empty when the whole
|
|
475
|
+
session was restored. Callers should surface this to the user.
|
|
476
|
+
"""
|
|
477
|
+
filepath = Path(filepath)
|
|
478
|
+
with open(filepath, "r") as f:
|
|
479
|
+
session_data = json.load(f)
|
|
480
|
+
|
|
481
|
+
# Deserialize datasets
|
|
482
|
+
skipped: List[str] = []
|
|
483
|
+
st.session_state.datasets = {}
|
|
484
|
+
for name, data in session_data["datasets"].items():
|
|
485
|
+
notes: List[str] = []
|
|
486
|
+
restored = _deserialize_data(data, notes)
|
|
487
|
+
if restored is None and notes:
|
|
488
|
+
skipped.extend(f"{name}: {note}" for note in notes)
|
|
489
|
+
continue
|
|
490
|
+
skipped.extend(f"{name}: {note}" for note in notes)
|
|
491
|
+
st.session_state.datasets[name] = restored
|
|
492
|
+
|
|
493
|
+
st.session_state.dataset_metadata = session_data["dataset_metadata"]
|
|
494
|
+
st.session_state.current_dataset = session_data["current_dataset"]
|
|
495
|
+
st.session_state.plot_history = session_data["plot_history"]
|
|
496
|
+
st.session_state.analysis_results = session_data["analysis_results"]
|
|
497
|
+
st.session_state.plot_config = session_data["plot_config"]
|
|
498
|
+
|
|
499
|
+
# A dataset that was dropped must not stay selected.
|
|
500
|
+
if st.session_state.current_dataset not in st.session_state.datasets:
|
|
501
|
+
st.session_state.current_dataset = next(iter(st.session_state.datasets), None)
|
|
502
|
+
|
|
503
|
+
return skipped
|
|
504
|
+
|
|
505
|
+
|
|
506
|
+
def clear_session():
|
|
507
|
+
"""Reset all session state to defaults."""
|
|
508
|
+
st.session_state.datasets = {}
|
|
509
|
+
st.session_state.dataset_metadata = {}
|
|
510
|
+
st.session_state.current_dataset = None
|
|
511
|
+
st.session_state.plot_history = []
|
|
512
|
+
st.session_state.analysis_results = []
|
|
513
|
+
st.session_state.plot_config = {
|
|
514
|
+
"default_figsize": (8, 6),
|
|
515
|
+
"default_dpi": 100,
|
|
516
|
+
"default_style": "default",
|
|
517
|
+
}
|
|
518
|
+
st.session_state.export_queue = []
|
|
519
|
+
|
|
520
|
+
|
|
521
|
+
def get_session_summary() -> Dict:
|
|
522
|
+
"""Get a summary of current session state.
|
|
523
|
+
|
|
524
|
+
Returns
|
|
525
|
+
-------
|
|
526
|
+
summary : dict
|
|
527
|
+
Summary with counts and current selections
|
|
528
|
+
"""
|
|
529
|
+
return {
|
|
530
|
+
"num_datasets": len(st.session_state.datasets),
|
|
531
|
+
"current_dataset": st.session_state.current_dataset,
|
|
532
|
+
"num_plots": len(st.session_state.plot_history),
|
|
533
|
+
"num_analyses": len(st.session_state.analysis_results),
|
|
534
|
+
"dataset_names": list(st.session_state.datasets.keys()),
|
|
535
|
+
}
|