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,521 @@
|
|
|
1
|
+
"""Data Upload and Preview Page.
|
|
2
|
+
|
|
3
|
+
This page allows users to upload datasets in various formats, preview
|
|
4
|
+
the data, and view basic statistics. Uploaded datasets are stored in
|
|
5
|
+
session state and available throughout the app.
|
|
6
|
+
|
|
7
|
+
Supported Formats
|
|
8
|
+
-----------------
|
|
9
|
+
- NumPy arrays (.npy, .npz)
|
|
10
|
+
- CSV (.csv)
|
|
11
|
+
- Excel (.xlsx, .xls)
|
|
12
|
+
- TSV (.tsv)
|
|
13
|
+
- JSON (.json)
|
|
14
|
+
- Parquet (.parquet)
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
import sys
|
|
18
|
+
import streamlit as st
|
|
19
|
+
import tempfile
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
import traceback
|
|
22
|
+
import numpy as np
|
|
23
|
+
import pandas as pd
|
|
24
|
+
|
|
25
|
+
_APP_ROOT = Path(__file__).resolve().parent.parent.parent
|
|
26
|
+
# Example datasets ship inside the package so this tab works after
|
|
27
|
+
# `pip install` -- see audit A-09.
|
|
28
|
+
_EXAMPLE_DIR = Path(__file__).resolve().parent.parent / "example-data" / "Artificial"
|
|
29
|
+
|
|
30
|
+
# Metadata for each built-in example file
|
|
31
|
+
_EXAMPLES = {
|
|
32
|
+
"normal_distribution.csv": {
|
|
33
|
+
"label": "Normal Distribution",
|
|
34
|
+
"desc": "400 simulated temperature measurements (Gaussian, μ=298 K).",
|
|
35
|
+
"use": "Histogram · Distribution · Interactive Histogram",
|
|
36
|
+
},
|
|
37
|
+
"bimodal_distribution.csv": {
|
|
38
|
+
"label": "Bimodal Distribution",
|
|
39
|
+
"desc": "500 absorbance values drawn from two overlapping Gaussians.",
|
|
40
|
+
"use": "Distribution · Overlaid Distributions",
|
|
41
|
+
},
|
|
42
|
+
"sine_cosine_waves.csv": {
|
|
43
|
+
"label": "Sine & Cosine Waves",
|
|
44
|
+
"desc": "300-point sin, cos, and damped-sin series over 0–4π.",
|
|
45
|
+
"use": "Line Plot · Interactive Line",
|
|
46
|
+
},
|
|
47
|
+
"scatter_correlation.csv": {
|
|
48
|
+
"label": "Scatter Correlation",
|
|
49
|
+
"desc": "200 (reaction time, yield, temperature) points with linear trend.",
|
|
50
|
+
"use": "Scatter · Regression · Interactive Scatter",
|
|
51
|
+
},
|
|
52
|
+
"grouped_categorical.csv": {
|
|
53
|
+
"label": "Grouped Categorical",
|
|
54
|
+
"desc": "Catalyst × solvent yield data (4 catalysts × 3 solvents, 20 obs each).",
|
|
55
|
+
"use": "Box / Violin / Swarm · Grouped Categorical",
|
|
56
|
+
},
|
|
57
|
+
"molecular_properties.csv": {
|
|
58
|
+
"label": "Molecular Properties",
|
|
59
|
+
"desc": "120 molecules with MW, logP, TPSA, HBD, HBA, RotBonds, pIC50.",
|
|
60
|
+
"use": "Correlation Heatmap · Scatter · Regression",
|
|
61
|
+
},
|
|
62
|
+
"ir_spectrum.csv": {
|
|
63
|
+
"label": "IR Spectrum",
|
|
64
|
+
"desc": "Simulated IR transmittance spectrum from 4000–400 cm⁻¹.",
|
|
65
|
+
"use": "Line Plot · Interactive Line",
|
|
66
|
+
},
|
|
67
|
+
"reaction_kinetics.csv": {
|
|
68
|
+
"label": "Reaction Kinetics",
|
|
69
|
+
"desc": "A→B→C consecutive-reaction concentration profiles over 60 min.",
|
|
70
|
+
"use": "Line Plot · Interactive Line",
|
|
71
|
+
},
|
|
72
|
+
"gaussian_surface.npy": {
|
|
73
|
+
"label": "Gaussian Surface (2D array)",
|
|
74
|
+
"desc": "60×60 double-Gaussian potential energy surface.",
|
|
75
|
+
"use": "Contour Plot · 3D Surface · Heatmap · Interactive Heatmap",
|
|
76
|
+
},
|
|
77
|
+
"correlation_matrix.npy": {
|
|
78
|
+
"label": "Correlation Matrix (2D array)",
|
|
79
|
+
"desc": "10×10 exact correlation matrix derived from random data.",
|
|
80
|
+
"use": "Heatmap · Interactive Heatmap",
|
|
81
|
+
},
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
sys.path.insert(0, str(Path(__file__).parent.parent.parent))
|
|
85
|
+
|
|
86
|
+
from eagleplot.io import load_data # noqa: E402
|
|
87
|
+
from eagleplot.math import calculate_statistics, check_normality # noqa: E402
|
|
88
|
+
from eagleplot.utils import ( # noqa: E402
|
|
89
|
+
initialize_session_state,
|
|
90
|
+
add_dataset,
|
|
91
|
+
get_dataset,
|
|
92
|
+
delete_dataset,
|
|
93
|
+
get_session_summary,
|
|
94
|
+
display_dataset_card,
|
|
95
|
+
display_data_preview,
|
|
96
|
+
)
|
|
97
|
+
from eagleplot.batch import scan_directory, batch_load_files # noqa: E402
|
|
98
|
+
from eagleplot.io import downsample_for_preview # noqa: E402
|
|
99
|
+
from eagleplot.utils.access import folder_loading_allowed_here # noqa: E402
|
|
100
|
+
|
|
101
|
+
_LARGE_FILE_BYTES = 50 * 1024 * 1024 # 50 MB — warn above this
|
|
102
|
+
_PREVIEW_MAX_ROWS = 10_000 # downsample preview above this
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
@st.cache_data(show_spinner=False)
|
|
106
|
+
def _load_example_cached(path: str):
|
|
107
|
+
"""Load an example dataset from a fixed path, cached across reruns."""
|
|
108
|
+
return load_data(path)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
# Initialize session state
|
|
112
|
+
initialize_session_state()
|
|
113
|
+
|
|
114
|
+
# Page title
|
|
115
|
+
st.title("Data Upload")
|
|
116
|
+
st.caption("Load datasets from file, examples, or a folder.")
|
|
117
|
+
|
|
118
|
+
tab1, tab2, tab3 = st.tabs(["Upload File", "Example Datasets", "Batch Import"])
|
|
119
|
+
|
|
120
|
+
with tab1:
|
|
121
|
+
# NOTE: `pkl` is deliberately absent. Unpickling an uploaded file executes
|
|
122
|
+
# code contained in it, and a user clicking "Browse files" has not consented
|
|
123
|
+
# to that. `eagleplot.io.load_pickle()` remains available in the Python API,
|
|
124
|
+
# where the caller has chosen to trust the file. See TDEC-011 / audit A-16.
|
|
125
|
+
uploaded_file = st.file_uploader(
|
|
126
|
+
"Choose a data file",
|
|
127
|
+
type=[
|
|
128
|
+
"npy",
|
|
129
|
+
"npz",
|
|
130
|
+
"csv",
|
|
131
|
+
"xlsx",
|
|
132
|
+
"xls",
|
|
133
|
+
"tsv",
|
|
134
|
+
"json",
|
|
135
|
+
"parquet",
|
|
136
|
+
"jdx",
|
|
137
|
+
"dx",
|
|
138
|
+
"h5",
|
|
139
|
+
"hdf5",
|
|
140
|
+
"nc",
|
|
141
|
+
"cdf",
|
|
142
|
+
"spc",
|
|
143
|
+
"asc",
|
|
144
|
+
"mzml",
|
|
145
|
+
"mzxml",
|
|
146
|
+
],
|
|
147
|
+
help="Upload a data file in any supported format",
|
|
148
|
+
)
|
|
149
|
+
st.caption(
|
|
150
|
+
"Pickle (`.pkl`) files are not accepted here — loading one runs any code it "
|
|
151
|
+
"contains. Use `eagleplot.io.load_pickle()` from Python if you trust the file, "
|
|
152
|
+
"or re-save your data as CSV or Parquet."
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
if uploaded_file is not None:
|
|
156
|
+
try:
|
|
157
|
+
with st.spinner(f"Loading {uploaded_file.name}..."):
|
|
158
|
+
# Each upload gets its own temp directory, removed as soon as the
|
|
159
|
+
# file is parsed. A shared $TMP/eagleplot/ let two sessions uploading
|
|
160
|
+
# the same filename overwrite each other's data, and nothing was
|
|
161
|
+
# ever deleted. The browser-supplied name is reduced to its
|
|
162
|
+
# basename before it touches the filesystem. See audit A-14.
|
|
163
|
+
with tempfile.TemporaryDirectory(prefix="eagleplot-upload-") as temp_dir:
|
|
164
|
+
temp_path = Path(temp_dir) / Path(uploaded_file.name).name
|
|
165
|
+
temp_path.write_bytes(uploaded_file.getvalue())
|
|
166
|
+
data = load_data(str(temp_path))
|
|
167
|
+
# np.load() returns a lazy NpzFile that keeps the file open;
|
|
168
|
+
# read every array now, before the directory is removed.
|
|
169
|
+
if isinstance(data, np.lib.npyio.NpzFile):
|
|
170
|
+
with data:
|
|
171
|
+
data = {key: data[key] for key in data.files}
|
|
172
|
+
|
|
173
|
+
# Large-file warning: show a note and offer a downsampled preview
|
|
174
|
+
_is_large = isinstance(data, pd.DataFrame) and (
|
|
175
|
+
uploaded_file.size > _LARGE_FILE_BYTES or len(data) > _PREVIEW_MAX_ROWS
|
|
176
|
+
)
|
|
177
|
+
if _is_large:
|
|
178
|
+
st.warning(
|
|
179
|
+
f"Large dataset: {len(data):,} rows / "
|
|
180
|
+
f"{uploaded_file.size / 1_048_576:.1f} MB. "
|
|
181
|
+
"Full data stored; previews use a downsampled view."
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
# Add to session state
|
|
185
|
+
add_dataset(
|
|
186
|
+
uploaded_file.name,
|
|
187
|
+
data,
|
|
188
|
+
metadata={
|
|
189
|
+
"file_size": uploaded_file.size,
|
|
190
|
+
"file_type": uploaded_file.type,
|
|
191
|
+
"downsampled_preview": _is_large,
|
|
192
|
+
},
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
st.toast(f"Loaded '{uploaded_file.name}' successfully", icon="✅")
|
|
196
|
+
st.success(f"Successfully loaded: **{uploaded_file.name}**")
|
|
197
|
+
|
|
198
|
+
# Show preview (downsampled for large files)
|
|
199
|
+
st.markdown("### Preview")
|
|
200
|
+
_preview_data = (
|
|
201
|
+
downsample_for_preview(data, _PREVIEW_MAX_ROWS) if _is_large else data
|
|
202
|
+
)
|
|
203
|
+
display_data_preview(_preview_data, uploaded_file.name)
|
|
204
|
+
|
|
205
|
+
except Exception as e:
|
|
206
|
+
st.error(f"Error loading file: {str(e)}")
|
|
207
|
+
with st.expander("Show Error Details"):
|
|
208
|
+
st.code(traceback.format_exc())
|
|
209
|
+
|
|
210
|
+
with tab2:
|
|
211
|
+
st.markdown(
|
|
212
|
+
"Load a built-in artificial dataset to explore each plot type "
|
|
213
|
+
"immediately — no file upload required."
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
if not _EXAMPLE_DIR.exists():
|
|
217
|
+
st.warning(
|
|
218
|
+
f"Example data directory not found: `{_EXAMPLE_DIR}`. "
|
|
219
|
+
"Run `python generate_examples.py` from the repository root first."
|
|
220
|
+
)
|
|
221
|
+
else:
|
|
222
|
+
st.markdown(
|
|
223
|
+
"""
|
|
224
|
+
<style>
|
|
225
|
+
/* Equal-height example dataset cards */
|
|
226
|
+
[data-testid="stHorizontalBlock"] {
|
|
227
|
+
align-items: stretch !important;
|
|
228
|
+
}
|
|
229
|
+
[data-testid="column"] {
|
|
230
|
+
display: flex !important;
|
|
231
|
+
flex-direction: column !important;
|
|
232
|
+
}
|
|
233
|
+
[data-testid="column"] > div {
|
|
234
|
+
flex: 1 !important;
|
|
235
|
+
display: flex !important;
|
|
236
|
+
flex-direction: column !important;
|
|
237
|
+
}
|
|
238
|
+
[data-testid="stVerticalBlockBorderWrapper"] {
|
|
239
|
+
flex: 1 !important;
|
|
240
|
+
display: flex !important;
|
|
241
|
+
flex-direction: column !important;
|
|
242
|
+
}
|
|
243
|
+
[data-testid="stVerticalBlockBorderWrapper"] > div {
|
|
244
|
+
flex: 1 !important;
|
|
245
|
+
}
|
|
246
|
+
</style>
|
|
247
|
+
""",
|
|
248
|
+
unsafe_allow_html=True,
|
|
249
|
+
)
|
|
250
|
+
_available = [
|
|
251
|
+
(fname, meta) for fname, meta in _EXAMPLES.items() if (_EXAMPLE_DIR / fname).exists()
|
|
252
|
+
]
|
|
253
|
+
for i in range(0, len(_available), 2):
|
|
254
|
+
_row_cols = st.columns(2)
|
|
255
|
+
for j, (fname, meta) in enumerate(_available[i : i + 2]):
|
|
256
|
+
fpath = _EXAMPLE_DIR / fname
|
|
257
|
+
with _row_cols[j]:
|
|
258
|
+
with st.container(border=True):
|
|
259
|
+
st.markdown(f"**{meta['label']}**")
|
|
260
|
+
st.caption(meta["desc"])
|
|
261
|
+
st.caption(f"*Best for:* {meta['use']}")
|
|
262
|
+
btn_key = f"ex_load_{fname}"
|
|
263
|
+
if st.button("Load", key=btn_key, width="stretch"):
|
|
264
|
+
try:
|
|
265
|
+
ex_data = _load_example_cached(str(fpath))
|
|
266
|
+
add_dataset(
|
|
267
|
+
fname,
|
|
268
|
+
ex_data,
|
|
269
|
+
metadata={
|
|
270
|
+
"source": "example",
|
|
271
|
+
"file_type": fpath.suffix,
|
|
272
|
+
},
|
|
273
|
+
)
|
|
274
|
+
st.success(f"Loaded **{meta['label']}**")
|
|
275
|
+
st.rerun()
|
|
276
|
+
except Exception as _ex_err:
|
|
277
|
+
st.error(f"Error: {_ex_err}")
|
|
278
|
+
|
|
279
|
+
with tab3:
|
|
280
|
+
# Reading a typed folder path is the point when EaglePlot runs on your own
|
|
281
|
+
# machine, and a way to read the server's disk on a shared deployment.
|
|
282
|
+
# See audit A-15 and eagleplot/utils/access.py.
|
|
283
|
+
if not folder_loading_allowed_here():
|
|
284
|
+
st.info(
|
|
285
|
+
"Batch import from a folder is turned off on this server, because "
|
|
286
|
+
"the path would be read on the server, not your computer. Upload files "
|
|
287
|
+
"individually on the **Upload File** tab instead. To enable it, start "
|
|
288
|
+
"EaglePlot with the environment variable `EAGLEPLOT_ALLOW_FOLDER_LOAD=1`."
|
|
289
|
+
)
|
|
290
|
+
else:
|
|
291
|
+
st.markdown(
|
|
292
|
+
"Load all matching files from a local directory at once. "
|
|
293
|
+
"Each file becomes a separate dataset."
|
|
294
|
+
)
|
|
295
|
+
_batch_folder = st.text_input(
|
|
296
|
+
"Folder path",
|
|
297
|
+
placeholder="C:/Users/you/data",
|
|
298
|
+
key="batch_folder_path",
|
|
299
|
+
help="Absolute path to the directory containing your data files.",
|
|
300
|
+
)
|
|
301
|
+
# `pkl` is deliberately absent here too — see the note on the uploader above.
|
|
302
|
+
_all_exts = [
|
|
303
|
+
"csv",
|
|
304
|
+
"xlsx",
|
|
305
|
+
"xls",
|
|
306
|
+
"tsv",
|
|
307
|
+
"json",
|
|
308
|
+
"parquet",
|
|
309
|
+
"npy",
|
|
310
|
+
"npz",
|
|
311
|
+
"jdx",
|
|
312
|
+
"dx",
|
|
313
|
+
"h5",
|
|
314
|
+
"hdf5",
|
|
315
|
+
"nc",
|
|
316
|
+
"cdf",
|
|
317
|
+
"spc",
|
|
318
|
+
"asc",
|
|
319
|
+
]
|
|
320
|
+
_batch_exts = st.multiselect(
|
|
321
|
+
"File extensions to include",
|
|
322
|
+
options=_all_exts,
|
|
323
|
+
default=["csv", "xlsx", "tsv"],
|
|
324
|
+
key="batch_extensions",
|
|
325
|
+
help="Leave blank to include all supported formats.",
|
|
326
|
+
)
|
|
327
|
+
_batch_pattern = st.text_input(
|
|
328
|
+
"Filename pattern (optional)",
|
|
329
|
+
placeholder="sample_*.csv",
|
|
330
|
+
key="batch_filename_pattern",
|
|
331
|
+
help="fnmatch-style pattern, e.g. 'sample_*.csv'. Leave blank to match all.",
|
|
332
|
+
)
|
|
333
|
+
|
|
334
|
+
_scan_col, _load_col = st.columns(2)
|
|
335
|
+
with _scan_col:
|
|
336
|
+
_do_scan = st.button("Scan Directory", key="batch_scan_btn", width="stretch")
|
|
337
|
+
with _load_col:
|
|
338
|
+
_do_load = st.button("Load All Files", key="batch_load_btn", width="stretch")
|
|
339
|
+
|
|
340
|
+
if _do_scan or _do_load:
|
|
341
|
+
if not _batch_folder:
|
|
342
|
+
st.warning("Please enter a folder path.")
|
|
343
|
+
else:
|
|
344
|
+
try:
|
|
345
|
+
_exts = _batch_exts if _batch_exts else None
|
|
346
|
+
_pat = _batch_pattern.strip() if _batch_pattern.strip() else None
|
|
347
|
+
_found = scan_directory(_batch_folder, extensions=_exts, pattern=_pat)
|
|
348
|
+
|
|
349
|
+
if not _found:
|
|
350
|
+
st.info("No matching files found in that directory.")
|
|
351
|
+
else:
|
|
352
|
+
# Always show metadata table
|
|
353
|
+
_meta_rows = []
|
|
354
|
+
for _fp in _found:
|
|
355
|
+
_size = _fp.stat().st_size
|
|
356
|
+
_meta_rows.append(
|
|
357
|
+
{
|
|
358
|
+
"Filename": _fp.name,
|
|
359
|
+
"Extension": _fp.suffix,
|
|
360
|
+
"Size (KB)": f"{_size / 1024:.1f}",
|
|
361
|
+
}
|
|
362
|
+
)
|
|
363
|
+
st.markdown(f"**{len(_found)} file(s) found:**")
|
|
364
|
+
st.dataframe(pd.DataFrame(_meta_rows), hide_index=True)
|
|
365
|
+
|
|
366
|
+
if _do_load:
|
|
367
|
+
with st.spinner(f"Loading {len(_found)} file(s)..."):
|
|
368
|
+
_result = batch_load_files(_found, on_error="skip")
|
|
369
|
+
|
|
370
|
+
_loaded = _result["datasets"]
|
|
371
|
+
_errors = _result["errors"]
|
|
372
|
+
_bmeta = _result["metadata"]
|
|
373
|
+
|
|
374
|
+
for _name, _data in _loaded.items():
|
|
375
|
+
add_dataset(
|
|
376
|
+
_name,
|
|
377
|
+
_data,
|
|
378
|
+
metadata={
|
|
379
|
+
"source": "batch_import",
|
|
380
|
+
"file_type": Path(_bmeta[_name]["source"]).suffix,
|
|
381
|
+
"size_bytes": _bmeta[_name]["size_bytes"],
|
|
382
|
+
},
|
|
383
|
+
)
|
|
384
|
+
|
|
385
|
+
if _loaded:
|
|
386
|
+
st.success(
|
|
387
|
+
f"Loaded {len(_loaded)} dataset(s): "
|
|
388
|
+
+ ", ".join(f"**{n}**" for n in _loaded)
|
|
389
|
+
)
|
|
390
|
+
st.rerun()
|
|
391
|
+
if _errors:
|
|
392
|
+
with st.expander(f"{len(_errors)} file(s) failed to load"):
|
|
393
|
+
for _ename, _emsg in _errors.items():
|
|
394
|
+
st.error(f"`{_ename}`: {_emsg}")
|
|
395
|
+
|
|
396
|
+
except NotADirectoryError as _e:
|
|
397
|
+
st.error(f"Not a valid directory: {_e}")
|
|
398
|
+
except Exception as _e:
|
|
399
|
+
st.error(f"Batch import error: {_e}")
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
# Dataset management section
|
|
403
|
+
st.markdown("---")
|
|
404
|
+
st.markdown("## Loaded Datasets")
|
|
405
|
+
|
|
406
|
+
summary = get_session_summary()
|
|
407
|
+
|
|
408
|
+
if summary["num_datasets"] == 0:
|
|
409
|
+
st.info("No datasets loaded yet. Upload a file above to get started.")
|
|
410
|
+
else:
|
|
411
|
+
st.write(f"**{summary['num_datasets']} dataset(s) loaded**")
|
|
412
|
+
|
|
413
|
+
# Dataset selector with delete option
|
|
414
|
+
col1, col2 = st.columns([3, 1])
|
|
415
|
+
|
|
416
|
+
with col1:
|
|
417
|
+
selected_dataset = st.selectbox(
|
|
418
|
+
"Select dataset to view",
|
|
419
|
+
options=list(st.session_state.datasets.keys()),
|
|
420
|
+
index=(
|
|
421
|
+
list(st.session_state.datasets.keys()).index(summary["current_dataset"])
|
|
422
|
+
if summary["current_dataset"]
|
|
423
|
+
else 0
|
|
424
|
+
),
|
|
425
|
+
key="dataset_selector",
|
|
426
|
+
)
|
|
427
|
+
|
|
428
|
+
with col2:
|
|
429
|
+
st.write("") # Spacing
|
|
430
|
+
st.write("") # Spacing
|
|
431
|
+
if st.button("Delete", key="delete_btn", help="Delete selected dataset"):
|
|
432
|
+
if delete_dataset(selected_dataset):
|
|
433
|
+
st.success(f"Deleted {selected_dataset}")
|
|
434
|
+
st.rerun()
|
|
435
|
+
else:
|
|
436
|
+
st.error("Failed to delete dataset")
|
|
437
|
+
|
|
438
|
+
# Update current dataset
|
|
439
|
+
if selected_dataset:
|
|
440
|
+
st.session_state.current_dataset = selected_dataset
|
|
441
|
+
data = get_dataset(selected_dataset)
|
|
442
|
+
metadata = st.session_state.dataset_metadata.get(selected_dataset, {})
|
|
443
|
+
|
|
444
|
+
# Display dataset card (main area)
|
|
445
|
+
st.markdown("---")
|
|
446
|
+
display_dataset_card(selected_dataset, data, metadata)
|
|
447
|
+
|
|
448
|
+
# ── Sidebar: Data Preview + Quick Statistics ──────────────────────
|
|
449
|
+
with st.sidebar:
|
|
450
|
+
st.markdown("## Data Preview")
|
|
451
|
+
display_data_preview(data, selected_dataset)
|
|
452
|
+
|
|
453
|
+
st.markdown("---")
|
|
454
|
+
st.markdown("## Quick Statistics")
|
|
455
|
+
|
|
456
|
+
if isinstance(data, pd.DataFrame):
|
|
457
|
+
numeric_cols = data.select_dtypes(include=[np.number]).columns.tolist()
|
|
458
|
+
|
|
459
|
+
if len(numeric_cols) > 0:
|
|
460
|
+
selected_col = st.selectbox(
|
|
461
|
+
"Select column for statistics",
|
|
462
|
+
options=numeric_cols,
|
|
463
|
+
key="stats_column",
|
|
464
|
+
)
|
|
465
|
+
|
|
466
|
+
if st.button("Calculate Statistics", key="calc_stats"):
|
|
467
|
+
stats = calculate_statistics(data[selected_col].values)
|
|
468
|
+
st.write("**Descriptive Statistics:**")
|
|
469
|
+
mc1, mc2, mc3 = st.columns(3)
|
|
470
|
+
mc1.metric("Mean", f"{stats['mean']:.4g}")
|
|
471
|
+
mc2.metric("Median", f"{stats['median']:.4g}")
|
|
472
|
+
mc3.metric("Std Dev", f"{stats['std']:.4g}")
|
|
473
|
+
mc1, mc2, mc3 = st.columns(3)
|
|
474
|
+
mc1.metric("Min", f"{stats['min']:.4g}")
|
|
475
|
+
mc2.metric("Max", f"{stats['max']:.4g}")
|
|
476
|
+
mc3.metric("Range", f"{stats['range']:.4g}")
|
|
477
|
+
st.write("**Quartiles:**")
|
|
478
|
+
mc1, mc2, mc3 = st.columns(3)
|
|
479
|
+
mc1.metric("Q1 (25%)", f"{stats['q1']:.4g}")
|
|
480
|
+
mc2.metric("Q3 (75%)", f"{stats['q3']:.4g}")
|
|
481
|
+
mc3.metric("IQR", f"{stats['iqr']:.4g}")
|
|
482
|
+
|
|
483
|
+
if st.button("Test Normality", key="test_normality"):
|
|
484
|
+
result = check_normality(data[selected_col].values)
|
|
485
|
+
st.write("**Shapiro-Wilk Normality Test:**")
|
|
486
|
+
st.write(f"Statistic: {result['statistic']:.6f}")
|
|
487
|
+
st.write(f"P-value: {result['p_value']:.6f}")
|
|
488
|
+
if result["is_normal"]:
|
|
489
|
+
st.success("Normally distributed (p > 0.05)")
|
|
490
|
+
else:
|
|
491
|
+
st.warning("Not normally distributed (p ≤ 0.05)")
|
|
492
|
+
else:
|
|
493
|
+
st.info("No numeric columns available for statistics")
|
|
494
|
+
|
|
495
|
+
elif isinstance(data, np.ndarray):
|
|
496
|
+
if np.issubdtype(data.dtype, np.number):
|
|
497
|
+
if st.button("Calculate Statistics", key="calc_stats"):
|
|
498
|
+
flat_data = data.flatten() if data.ndim > 1 else data
|
|
499
|
+
stats = calculate_statistics(flat_data)
|
|
500
|
+
st.write("**Descriptive Statistics:**")
|
|
501
|
+
mc1, mc2, mc3 = st.columns(3)
|
|
502
|
+
mc1.metric("Mean", f"{stats['mean']:.4g}")
|
|
503
|
+
mc2.metric("Median", f"{stats['median']:.4g}")
|
|
504
|
+
mc3.metric("Std Dev", f"{stats['std']:.4g}")
|
|
505
|
+
mc1, mc2, mc3 = st.columns(3)
|
|
506
|
+
mc1.metric("Min", f"{stats['min']:.4g}")
|
|
507
|
+
mc2.metric("Max", f"{stats['max']:.4g}")
|
|
508
|
+
mc3.metric("Range", f"{stats['range']:.4g}")
|
|
509
|
+
|
|
510
|
+
if st.button("Test Normality", key="test_normality"):
|
|
511
|
+
flat_data = data.flatten() if data.ndim > 1 else data
|
|
512
|
+
result = check_normality(flat_data)
|
|
513
|
+
st.write("**Shapiro-Wilk Normality Test:**")
|
|
514
|
+
st.write(f"Statistic: {result['statistic']:.6f}")
|
|
515
|
+
st.write(f"P-value: {result['p_value']:.6f}")
|
|
516
|
+
if result["is_normal"]:
|
|
517
|
+
st.success("Normally distributed (p > 0.05)")
|
|
518
|
+
else:
|
|
519
|
+
st.warning("Not normally distributed (p ≤ 0.05)")
|
|
520
|
+
else:
|
|
521
|
+
st.info("Array is not numeric - statistics not applicable")
|