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,691 @@
|
|
|
1
|
+
"""Advanced Plotting Page.
|
|
2
|
+
|
|
3
|
+
Provides visualizations that go beyond the 13 core eagleplot.plotting functions
|
|
4
|
+
by using seaborn, plotly.graph_objects, and matplotlib directly.
|
|
5
|
+
|
|
6
|
+
Tabs
|
|
7
|
+
----
|
|
8
|
+
- Correlation Heatmap: annotated correlation matrix (DataFrame only)
|
|
9
|
+
- Overlaid Distributions: histogram or KDE overlay for multiple columns
|
|
10
|
+
- Grouped Categorical: box / violin / swarm with optional hue grouping
|
|
11
|
+
- 3D Scatter: Plotly interactive 3-D scatter with color encoding
|
|
12
|
+
- HTML Export: generate any interactive Plotly chart + download HTML
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import traceback
|
|
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
|
+
import matplotlib.pyplot as plt
|
|
23
|
+
import seaborn as sns
|
|
24
|
+
import plotly.graph_objects as go
|
|
25
|
+
|
|
26
|
+
sys.path.insert(0, str(Path(__file__).parent.parent.parent))
|
|
27
|
+
|
|
28
|
+
from eagleplot.plotting import (
|
|
29
|
+
interactive_histogram,
|
|
30
|
+
interactive_scatter,
|
|
31
|
+
interactive_line,
|
|
32
|
+
interactive_heatmap,
|
|
33
|
+
interactive_3d_surface,
|
|
34
|
+
)
|
|
35
|
+
from eagleplot.utils import (
|
|
36
|
+
initialize_session_state,
|
|
37
|
+
get_dataset,
|
|
38
|
+
get_session_summary,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
initialize_session_state()
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
# ── Constants ─────────────────────────────────────────────────────────────────
|
|
45
|
+
|
|
46
|
+
_CMAPS_DIV = ["RdBu_r", "coolwarm", "PiYG", "BrBG", "seismic"]
|
|
47
|
+
_CMAPS_SEQ = ["viridis", "plasma", "magma", "cividis", "Blues", "Greens"]
|
|
48
|
+
_CORR_METHODS = ["pearson", "spearman", "kendall"]
|
|
49
|
+
|
|
50
|
+
# Interactive plot types available for HTML export
|
|
51
|
+
_INTERACTIVE_TYPES = {
|
|
52
|
+
"interactive_histogram": "Interactive Histogram",
|
|
53
|
+
"interactive_scatter": "Interactive Scatter",
|
|
54
|
+
"interactive_line": "Interactive Line",
|
|
55
|
+
"interactive_heatmap": "Interactive Heatmap",
|
|
56
|
+
"interactive_3d_surface": "3D Surface",
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
# ── Helpers ───────────────────────────────────────────────────────────────────
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _numeric_cols(data) -> list:
|
|
64
|
+
if isinstance(data, pd.DataFrame):
|
|
65
|
+
return data.select_dtypes(include=[np.number]).columns.tolist()
|
|
66
|
+
return []
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _all_cols(data) -> list:
|
|
70
|
+
"""All column names (DataFrame) or column indices (2-D array)."""
|
|
71
|
+
if isinstance(data, pd.DataFrame):
|
|
72
|
+
return list(data.columns)
|
|
73
|
+
arr = np.asarray(data)
|
|
74
|
+
if arr.ndim == 2:
|
|
75
|
+
return list(range(arr.shape[1]))
|
|
76
|
+
return []
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _to_1d(data, col_ref) -> np.ndarray:
|
|
80
|
+
if isinstance(data, pd.DataFrame):
|
|
81
|
+
return data[col_ref].dropna().values.astype(float)
|
|
82
|
+
arr = np.asarray(data, dtype=float)
|
|
83
|
+
if col_ref is None:
|
|
84
|
+
return arr.flatten()
|
|
85
|
+
return arr[:, int(col_ref)]
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _df_required(data) -> bool:
|
|
89
|
+
"""Return True and show error if data is not a DataFrame."""
|
|
90
|
+
if not isinstance(data, pd.DataFrame):
|
|
91
|
+
st.warning(
|
|
92
|
+
"This feature requires a **DataFrame** (CSV, Excel, JSON, etc.). "
|
|
93
|
+
"The current dataset is a NumPy array."
|
|
94
|
+
)
|
|
95
|
+
return False
|
|
96
|
+
return True
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _html_download(fig, filename: str = "interactive_plot.html") -> None:
|
|
100
|
+
"""Render an HTML download button for a Plotly figure."""
|
|
101
|
+
html_str = fig.to_html(full_html=True, include_plotlyjs=True)
|
|
102
|
+
st.download_button(
|
|
103
|
+
label="Download as HTML",
|
|
104
|
+
data=html_str,
|
|
105
|
+
file_name=filename,
|
|
106
|
+
mime="text/html",
|
|
107
|
+
help="Standalone HTML file — opens in any browser without Python.",
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
# ── Page layout ───────────────────────────────────────────────────────────────
|
|
112
|
+
|
|
113
|
+
st.title("Advanced Plotting")
|
|
114
|
+
st.caption(
|
|
115
|
+
"Annotated correlation matrices, overlaid distributions, grouped categoricals, "
|
|
116
|
+
"3D scatter, and HTML export."
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
# Guard: need at least one dataset
|
|
120
|
+
summary = get_session_summary()
|
|
121
|
+
if summary["num_datasets"] == 0:
|
|
122
|
+
st.warning("No datasets loaded. Go to **Data Upload** first.")
|
|
123
|
+
st.stop()
|
|
124
|
+
|
|
125
|
+
# ── Dataset selector ──────────────────────────────────────────────────────────
|
|
126
|
+
st.markdown("---")
|
|
127
|
+
st.markdown("## Dataset")
|
|
128
|
+
|
|
129
|
+
dataset_names = list(st.session_state.datasets.keys())
|
|
130
|
+
default_idx = (
|
|
131
|
+
dataset_names.index(summary["current_dataset"])
|
|
132
|
+
if summary["current_dataset"] in dataset_names
|
|
133
|
+
else 0
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
col1, col2 = st.columns([3, 1])
|
|
137
|
+
with col1:
|
|
138
|
+
dataset_name = st.selectbox(
|
|
139
|
+
"Select dataset",
|
|
140
|
+
dataset_names,
|
|
141
|
+
index=default_idx,
|
|
142
|
+
key="adv_dataset",
|
|
143
|
+
)
|
|
144
|
+
with col2:
|
|
145
|
+
st.write("")
|
|
146
|
+
st.write("")
|
|
147
|
+
st.metric("Loaded", summary["num_datasets"])
|
|
148
|
+
|
|
149
|
+
if dataset_name:
|
|
150
|
+
st.session_state.current_dataset = dataset_name
|
|
151
|
+
|
|
152
|
+
data = get_dataset(dataset_name)
|
|
153
|
+
|
|
154
|
+
if isinstance(data, pd.DataFrame):
|
|
155
|
+
st.caption(f"DataFrame — {data.shape[0]} rows × {data.shape[1]} cols")
|
|
156
|
+
elif isinstance(data, np.ndarray):
|
|
157
|
+
st.caption(f"NumPy array — shape {data.shape}, dtype {data.dtype}")
|
|
158
|
+
|
|
159
|
+
# ── Tabs ──────────────────────────────────────────────────────────────────────
|
|
160
|
+
st.markdown("---")
|
|
161
|
+
|
|
162
|
+
tab_corr, tab_dist, tab_cat, tab_3d, tab_export = st.tabs(
|
|
163
|
+
[
|
|
164
|
+
"Correlation Heatmap",
|
|
165
|
+
"Overlaid Distributions",
|
|
166
|
+
"Grouped Categorical",
|
|
167
|
+
"3D Scatter",
|
|
168
|
+
"HTML Export",
|
|
169
|
+
]
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
# ══════════════════════════════════════════════════════════════════════════════
|
|
173
|
+
# Tab 1 — Correlation Heatmap
|
|
174
|
+
# ══════════════════════════════════════════════════════════════════════════════
|
|
175
|
+
with tab_corr:
|
|
176
|
+
st.markdown("### Annotated Correlation Heatmap")
|
|
177
|
+
|
|
178
|
+
if _df_required(data):
|
|
179
|
+
num_cols = _numeric_cols(data)
|
|
180
|
+
if len(num_cols) < 2:
|
|
181
|
+
st.warning("Need at least 2 numeric columns for a correlation matrix.")
|
|
182
|
+
else:
|
|
183
|
+
# Column subset
|
|
184
|
+
selected_corr_cols = st.multiselect(
|
|
185
|
+
"Columns to include",
|
|
186
|
+
num_cols,
|
|
187
|
+
default=num_cols,
|
|
188
|
+
key="corr_cols",
|
|
189
|
+
)
|
|
190
|
+
if len(selected_corr_cols) < 2:
|
|
191
|
+
st.info("Select at least 2 columns.")
|
|
192
|
+
else:
|
|
193
|
+
c1, c2, c3 = st.columns(3)
|
|
194
|
+
with c1:
|
|
195
|
+
corr_method = st.selectbox(
|
|
196
|
+
"Method",
|
|
197
|
+
_CORR_METHODS,
|
|
198
|
+
key="corr_method",
|
|
199
|
+
)
|
|
200
|
+
with c2:
|
|
201
|
+
corr_cmap = st.selectbox(
|
|
202
|
+
"Color map",
|
|
203
|
+
_CMAPS_DIV,
|
|
204
|
+
key="corr_cmap",
|
|
205
|
+
)
|
|
206
|
+
with c3:
|
|
207
|
+
corr_annot = st.checkbox("Show values", value=True, key="corr_annot")
|
|
208
|
+
|
|
209
|
+
figw = st.slider("Figure width", 4, 16, 8, key="corr_figw")
|
|
210
|
+
|
|
211
|
+
if st.button("Generate Correlation Heatmap", type="primary", key="corr_run"):
|
|
212
|
+
try:
|
|
213
|
+
corr = data[selected_corr_cols].corr(method=corr_method)
|
|
214
|
+
n = len(selected_corr_cols)
|
|
215
|
+
fig, ax = plt.subplots(figsize=(figw, max(4, figw * n // 8)))
|
|
216
|
+
sns.heatmap(
|
|
217
|
+
corr,
|
|
218
|
+
annot=corr_annot,
|
|
219
|
+
fmt=".2f",
|
|
220
|
+
cmap=corr_cmap,
|
|
221
|
+
center=0,
|
|
222
|
+
square=True,
|
|
223
|
+
linewidths=0.5,
|
|
224
|
+
ax=ax,
|
|
225
|
+
vmin=-1,
|
|
226
|
+
vmax=1,
|
|
227
|
+
)
|
|
228
|
+
ax.set_title(
|
|
229
|
+
f"{corr_method.capitalize()} Correlation Matrix — {dataset_name}",
|
|
230
|
+
pad=12,
|
|
231
|
+
)
|
|
232
|
+
fig.tight_layout()
|
|
233
|
+
st.pyplot(fig)
|
|
234
|
+
plt.close(fig)
|
|
235
|
+
except Exception as exc:
|
|
236
|
+
st.error(f"Error: {exc}")
|
|
237
|
+
with st.expander("Details"):
|
|
238
|
+
st.code(traceback.format_exc())
|
|
239
|
+
|
|
240
|
+
# ══════════════════════════════════════════════════════════════════════════════
|
|
241
|
+
# Tab 2 — Overlaid Distributions
|
|
242
|
+
# ══════════════════════════════════════════════════════════════════════════════
|
|
243
|
+
with tab_dist:
|
|
244
|
+
st.markdown("### Overlaid Distributions")
|
|
245
|
+
st.markdown("Compare multiple variables on the same axes.")
|
|
246
|
+
|
|
247
|
+
# Build column options for both DataFrame and 2-D array
|
|
248
|
+
if isinstance(data, pd.DataFrame):
|
|
249
|
+
dist_col_options = _numeric_cols(data)
|
|
250
|
+
dist_col_fmt = str
|
|
251
|
+
elif isinstance(data, np.ndarray) and data.ndim == 2:
|
|
252
|
+
dist_col_options = list(range(data.shape[1]))
|
|
253
|
+
dist_col_fmt = lambda i: f"Column {i}" # noqa: E731
|
|
254
|
+
else:
|
|
255
|
+
dist_col_options = []
|
|
256
|
+
dist_col_fmt = str
|
|
257
|
+
|
|
258
|
+
if len(dist_col_options) < 2:
|
|
259
|
+
st.warning("Need at least 2 columns. Load a DataFrame or a 2-D NumPy array.")
|
|
260
|
+
else:
|
|
261
|
+
dist_cols_sel = st.multiselect(
|
|
262
|
+
"Select columns to overlay",
|
|
263
|
+
dist_col_options,
|
|
264
|
+
default=dist_col_options[: min(4, len(dist_col_options))],
|
|
265
|
+
format_func=dist_col_fmt,
|
|
266
|
+
key="dist_cols",
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
if dist_cols_sel:
|
|
270
|
+
c1, c2, c3 = st.columns(3)
|
|
271
|
+
with c1:
|
|
272
|
+
dist_kind = st.radio(
|
|
273
|
+
"Type",
|
|
274
|
+
["Histogram", "KDE"],
|
|
275
|
+
horizontal=True,
|
|
276
|
+
key="dist_kind",
|
|
277
|
+
)
|
|
278
|
+
with c2:
|
|
279
|
+
dist_density = st.checkbox("Normalize (density)", value=True, key="dist_density")
|
|
280
|
+
with c3:
|
|
281
|
+
dist_bins = (
|
|
282
|
+
st.slider("Bins", 5, 100, 20, key="dist_bins")
|
|
283
|
+
if dist_kind == "Histogram"
|
|
284
|
+
else None
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
if st.button("Generate Overlaid Distributions", type="primary", key="dist_run"):
|
|
288
|
+
try:
|
|
289
|
+
fig, ax = plt.subplots(figsize=(8, 5))
|
|
290
|
+
palette = sns.color_palette("tab10", n_colors=len(dist_cols_sel))
|
|
291
|
+
|
|
292
|
+
for col, color in zip(dist_cols_sel, palette):
|
|
293
|
+
arr = _to_1d(data, col)
|
|
294
|
+
label = str(col) if not isinstance(col, int) else f"Column {col}"
|
|
295
|
+
if dist_kind == "Histogram":
|
|
296
|
+
ax.hist(
|
|
297
|
+
arr,
|
|
298
|
+
bins=dist_bins,
|
|
299
|
+
density=dist_density,
|
|
300
|
+
alpha=0.55,
|
|
301
|
+
color=color,
|
|
302
|
+
label=label,
|
|
303
|
+
edgecolor="white",
|
|
304
|
+
linewidth=0.5,
|
|
305
|
+
)
|
|
306
|
+
else:
|
|
307
|
+
sns.kdeplot(
|
|
308
|
+
arr,
|
|
309
|
+
ax=ax,
|
|
310
|
+
fill=True,
|
|
311
|
+
alpha=0.35,
|
|
312
|
+
color=color,
|
|
313
|
+
label=label,
|
|
314
|
+
linewidth=2,
|
|
315
|
+
)
|
|
316
|
+
|
|
317
|
+
ax.set_ylabel("Density" if dist_density else "Count")
|
|
318
|
+
ax.set_title(f"Overlaid {dist_kind}s — {dataset_name}", pad=10)
|
|
319
|
+
ax.legend(framealpha=0.8)
|
|
320
|
+
fig.tight_layout()
|
|
321
|
+
st.pyplot(fig)
|
|
322
|
+
plt.close(fig)
|
|
323
|
+
except Exception as exc:
|
|
324
|
+
st.error(f"Error: {exc}")
|
|
325
|
+
with st.expander("Details"):
|
|
326
|
+
st.code(traceback.format_exc())
|
|
327
|
+
|
|
328
|
+
# ══════════════════════════════════════════════════════════════════════════════
|
|
329
|
+
# Tab 3 — Grouped Categorical Plot
|
|
330
|
+
# ══════════════════════════════════════════════════════════════════════════════
|
|
331
|
+
with tab_cat:
|
|
332
|
+
st.markdown("### Grouped Categorical Plot")
|
|
333
|
+
st.markdown(
|
|
334
|
+
"Box, violin, or swarm plot with optional colour grouping (hue). Requires a **DataFrame**."
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
if _df_required(data):
|
|
338
|
+
all_df_cols = list(data.columns)
|
|
339
|
+
num_df_cols = _numeric_cols(data)
|
|
340
|
+
|
|
341
|
+
if not num_df_cols:
|
|
342
|
+
st.warning("No numeric columns available.")
|
|
343
|
+
else:
|
|
344
|
+
c1, c2 = st.columns(2)
|
|
345
|
+
with c1:
|
|
346
|
+
cat_kind = st.selectbox(
|
|
347
|
+
"Plot kind",
|
|
348
|
+
["Box", "Violin", "Swarm"],
|
|
349
|
+
key="cat_kind",
|
|
350
|
+
)
|
|
351
|
+
with c2:
|
|
352
|
+
cat_palette = st.selectbox(
|
|
353
|
+
"Palette",
|
|
354
|
+
["tab10", "Set2", "Set3", "pastel", "muted", "deep"],
|
|
355
|
+
key="cat_palette",
|
|
356
|
+
)
|
|
357
|
+
|
|
358
|
+
c1, c2, c3 = st.columns(3)
|
|
359
|
+
with c1:
|
|
360
|
+
cat_x = st.selectbox(
|
|
361
|
+
"X axis (grouping)",
|
|
362
|
+
[None] + all_df_cols,
|
|
363
|
+
format_func=lambda v: "— (none)" if v is None else str(v),
|
|
364
|
+
key="cat_x",
|
|
365
|
+
)
|
|
366
|
+
with c2:
|
|
367
|
+
cat_y = st.selectbox("Y axis (numeric)", num_df_cols, key="cat_y")
|
|
368
|
+
with c3:
|
|
369
|
+
cat_hue = st.selectbox(
|
|
370
|
+
"Hue (optional)",
|
|
371
|
+
[None] + all_df_cols,
|
|
372
|
+
format_func=lambda v: "— (none)" if v is None else str(v),
|
|
373
|
+
key="cat_hue",
|
|
374
|
+
)
|
|
375
|
+
|
|
376
|
+
if cat_kind == "Swarm" and len(data) > 500:
|
|
377
|
+
st.warning(
|
|
378
|
+
f"Dataset has {len(data):,} rows — swarm plot may be slow. "
|
|
379
|
+
"Consider filtering to ≤ 500 rows first."
|
|
380
|
+
)
|
|
381
|
+
|
|
382
|
+
if st.button("Generate Categorical Plot", type="primary", key="cat_run"):
|
|
383
|
+
try:
|
|
384
|
+
fig, ax = plt.subplots(figsize=(9, 5))
|
|
385
|
+
shared_kw = dict(
|
|
386
|
+
data=data,
|
|
387
|
+
x=cat_x,
|
|
388
|
+
y=cat_y,
|
|
389
|
+
hue=cat_hue,
|
|
390
|
+
palette=cat_palette,
|
|
391
|
+
ax=ax,
|
|
392
|
+
)
|
|
393
|
+
if cat_kind == "Box":
|
|
394
|
+
sns.boxplot(**shared_kw)
|
|
395
|
+
elif cat_kind == "Violin":
|
|
396
|
+
sns.violinplot(**shared_kw)
|
|
397
|
+
else:
|
|
398
|
+
sns.swarmplot(**shared_kw, size=4)
|
|
399
|
+
|
|
400
|
+
title_parts = [cat_kind, "plot of", cat_y]
|
|
401
|
+
if cat_x:
|
|
402
|
+
title_parts += ["by", cat_x]
|
|
403
|
+
if cat_hue:
|
|
404
|
+
title_parts += ["(hue:", cat_hue + ")"]
|
|
405
|
+
ax.set_title(" ".join(title_parts), pad=10)
|
|
406
|
+
if cat_hue:
|
|
407
|
+
ax.legend(title=cat_hue, bbox_to_anchor=(1.01, 1), loc="upper left")
|
|
408
|
+
fig.tight_layout()
|
|
409
|
+
st.pyplot(fig)
|
|
410
|
+
plt.close(fig)
|
|
411
|
+
except Exception as exc:
|
|
412
|
+
st.error(f"Error: {exc}")
|
|
413
|
+
with st.expander("Details"):
|
|
414
|
+
st.code(traceback.format_exc())
|
|
415
|
+
|
|
416
|
+
# ══════════════════════════════════════════════════════════════════════════════
|
|
417
|
+
# Tab 4 — 3D Scatter (Plotly)
|
|
418
|
+
# ══════════════════════════════════════════════════════════════════════════════
|
|
419
|
+
with tab_3d:
|
|
420
|
+
st.markdown("### Interactive 3D Scatter Plot")
|
|
421
|
+
|
|
422
|
+
# Build column options
|
|
423
|
+
if isinstance(data, pd.DataFrame):
|
|
424
|
+
scatter3d_options = _numeric_cols(data)
|
|
425
|
+
scatter3d_fmt = str
|
|
426
|
+
all_scatter_opts = list(data.columns)
|
|
427
|
+
elif isinstance(data, np.ndarray) and data.ndim == 2 and data.shape[1] >= 3:
|
|
428
|
+
scatter3d_options = list(range(data.shape[1]))
|
|
429
|
+
scatter3d_fmt = lambda i: f"Column {i}" # noqa: E731
|
|
430
|
+
all_scatter_opts = scatter3d_options
|
|
431
|
+
else:
|
|
432
|
+
scatter3d_options = []
|
|
433
|
+
scatter3d_fmt = str
|
|
434
|
+
all_scatter_opts = []
|
|
435
|
+
|
|
436
|
+
if len(scatter3d_options) < 3:
|
|
437
|
+
st.warning(
|
|
438
|
+
"3D scatter requires at least 3 numeric columns. "
|
|
439
|
+
"Load a DataFrame or a 2-D NumPy array with ≥ 3 columns."
|
|
440
|
+
)
|
|
441
|
+
else:
|
|
442
|
+
c1, c2, c3 = st.columns(3)
|
|
443
|
+
with c1:
|
|
444
|
+
s3d_x = st.selectbox(
|
|
445
|
+
"X axis",
|
|
446
|
+
scatter3d_options,
|
|
447
|
+
index=0,
|
|
448
|
+
format_func=scatter3d_fmt,
|
|
449
|
+
key="s3d_x",
|
|
450
|
+
)
|
|
451
|
+
with c2:
|
|
452
|
+
s3d_y = st.selectbox(
|
|
453
|
+
"Y axis",
|
|
454
|
+
scatter3d_options,
|
|
455
|
+
index=min(1, len(scatter3d_options) - 1),
|
|
456
|
+
format_func=scatter3d_fmt,
|
|
457
|
+
key="s3d_y",
|
|
458
|
+
)
|
|
459
|
+
with c3:
|
|
460
|
+
s3d_z = st.selectbox(
|
|
461
|
+
"Z axis",
|
|
462
|
+
scatter3d_options,
|
|
463
|
+
index=min(2, len(scatter3d_options) - 1),
|
|
464
|
+
format_func=scatter3d_fmt,
|
|
465
|
+
key="s3d_z",
|
|
466
|
+
)
|
|
467
|
+
|
|
468
|
+
c1, c2 = st.columns(2)
|
|
469
|
+
with c1:
|
|
470
|
+
s3d_color = st.selectbox(
|
|
471
|
+
"Color encoding (optional)",
|
|
472
|
+
[None] + list(all_scatter_opts),
|
|
473
|
+
format_func=lambda v: (
|
|
474
|
+
"— (none)"
|
|
475
|
+
if v is None
|
|
476
|
+
else (str(v) if not isinstance(v, int) else f"Column {v}")
|
|
477
|
+
),
|
|
478
|
+
key="s3d_color",
|
|
479
|
+
)
|
|
480
|
+
with c2:
|
|
481
|
+
s3d_colorscale = st.selectbox(
|
|
482
|
+
"Color scale",
|
|
483
|
+
["Viridis", "Plasma", "Cividis", "RdBu", "Turbo"],
|
|
484
|
+
key="s3d_colorscale",
|
|
485
|
+
)
|
|
486
|
+
|
|
487
|
+
c1, c2 = st.columns(2)
|
|
488
|
+
with c1:
|
|
489
|
+
s3d_size = st.slider("Marker size", 2, 20, 5, key="s3d_size")
|
|
490
|
+
with c2:
|
|
491
|
+
s3d_opacity = st.slider("Opacity", 0.1, 1.0, 0.8, 0.05, key="s3d_opacity")
|
|
492
|
+
|
|
493
|
+
s3d_title = st.text_input("Title (optional)", key="s3d_title", placeholder="3D Scatter")
|
|
494
|
+
|
|
495
|
+
if st.button("Generate 3D Scatter", type="primary", key="s3d_run"):
|
|
496
|
+
try:
|
|
497
|
+
x_arr = _to_1d(data, s3d_x)
|
|
498
|
+
y_arr = _to_1d(data, s3d_y)
|
|
499
|
+
z_arr = _to_1d(data, s3d_z)
|
|
500
|
+
|
|
501
|
+
color_arr = _to_1d(data, s3d_color) if s3d_color is not None else None
|
|
502
|
+
|
|
503
|
+
x_lbl = str(s3d_x) if not isinstance(s3d_x, int) else f"Column {s3d_x}"
|
|
504
|
+
y_lbl = str(s3d_y) if not isinstance(s3d_y, int) else f"Column {s3d_y}"
|
|
505
|
+
z_lbl = str(s3d_z) if not isinstance(s3d_z, int) else f"Column {s3d_z}"
|
|
506
|
+
|
|
507
|
+
marker = dict(
|
|
508
|
+
size=s3d_size,
|
|
509
|
+
opacity=s3d_opacity,
|
|
510
|
+
colorscale=s3d_colorscale,
|
|
511
|
+
)
|
|
512
|
+
if color_arr is not None:
|
|
513
|
+
marker["color"] = color_arr
|
|
514
|
+
marker["showscale"] = True
|
|
515
|
+
c_lbl = (
|
|
516
|
+
str(s3d_color) if not isinstance(s3d_color, int) else f"Column {s3d_color}"
|
|
517
|
+
)
|
|
518
|
+
marker["colorbar"] = dict(title=c_lbl)
|
|
519
|
+
else:
|
|
520
|
+
marker["color"] = "#636EFA"
|
|
521
|
+
|
|
522
|
+
fig = go.Figure(
|
|
523
|
+
data=[
|
|
524
|
+
go.Scatter3d(
|
|
525
|
+
x=x_arr,
|
|
526
|
+
y=y_arr,
|
|
527
|
+
z=z_arr,
|
|
528
|
+
mode="markers",
|
|
529
|
+
marker=marker,
|
|
530
|
+
)
|
|
531
|
+
]
|
|
532
|
+
)
|
|
533
|
+
fig.update_layout(
|
|
534
|
+
title=s3d_title or f"3D Scatter — {dataset_name}",
|
|
535
|
+
scene=dict(
|
|
536
|
+
xaxis_title=x_lbl,
|
|
537
|
+
yaxis_title=y_lbl,
|
|
538
|
+
zaxis_title=z_lbl,
|
|
539
|
+
),
|
|
540
|
+
margin=dict(l=0, r=0, t=40, b=0),
|
|
541
|
+
)
|
|
542
|
+
st.plotly_chart(fig, width="stretch")
|
|
543
|
+
_html_download(fig, filename="3d_scatter.html")
|
|
544
|
+
|
|
545
|
+
except Exception as exc:
|
|
546
|
+
st.error(f"Error: {exc}")
|
|
547
|
+
with st.expander("Details"):
|
|
548
|
+
st.code(traceback.format_exc())
|
|
549
|
+
|
|
550
|
+
# ══════════════════════════════════════════════════════════════════════════════
|
|
551
|
+
# Tab 5 — HTML Export
|
|
552
|
+
# ══════════════════════════════════════════════════════════════════════════════
|
|
553
|
+
with tab_export:
|
|
554
|
+
st.markdown("### Export Interactive Plot as HTML")
|
|
555
|
+
st.markdown(
|
|
556
|
+
"Generate any Plotly interactive chart and download it as a "
|
|
557
|
+
"**standalone HTML file** that works in any browser — no Python required."
|
|
558
|
+
)
|
|
559
|
+
|
|
560
|
+
# Build column options (reused from the current dataset)
|
|
561
|
+
if isinstance(data, pd.DataFrame):
|
|
562
|
+
exp_num_cols = _numeric_cols(data)
|
|
563
|
+
exp_col_fmt = str
|
|
564
|
+
elif isinstance(data, np.ndarray) and data.ndim >= 2:
|
|
565
|
+
exp_num_cols = list(range(data.shape[1]))
|
|
566
|
+
exp_col_fmt = lambda i: f"Column {i}" # noqa: E731
|
|
567
|
+
else:
|
|
568
|
+
exp_num_cols = []
|
|
569
|
+
exp_col_fmt = str
|
|
570
|
+
|
|
571
|
+
exp_type = st.selectbox(
|
|
572
|
+
"Interactive plot type",
|
|
573
|
+
list(_INTERACTIVE_TYPES.keys()),
|
|
574
|
+
format_func=lambda k: _INTERACTIVE_TYPES[k],
|
|
575
|
+
key="exp_type",
|
|
576
|
+
)
|
|
577
|
+
|
|
578
|
+
exp_title = st.text_input(
|
|
579
|
+
"Plot title", key="exp_title", placeholder=_INTERACTIVE_TYPES[exp_type]
|
|
580
|
+
)
|
|
581
|
+
|
|
582
|
+
# Column selectors — shown conditionally
|
|
583
|
+
exp_x = exp_y = exp_data_col = None
|
|
584
|
+
_1d_types = {"interactive_histogram"}
|
|
585
|
+
_xy_types = {"interactive_scatter", "interactive_line"}
|
|
586
|
+
_mat_types = {"interactive_heatmap", "interactive_3d_surface"}
|
|
587
|
+
|
|
588
|
+
if exp_type in _1d_types:
|
|
589
|
+
if exp_num_cols:
|
|
590
|
+
exp_data_col = st.selectbox(
|
|
591
|
+
"Data column",
|
|
592
|
+
exp_num_cols,
|
|
593
|
+
format_func=exp_col_fmt,
|
|
594
|
+
key="exp_dcol",
|
|
595
|
+
)
|
|
596
|
+
elif exp_type in _xy_types:
|
|
597
|
+
if len(exp_num_cols) >= 2:
|
|
598
|
+
c1, c2 = st.columns(2)
|
|
599
|
+
with c1:
|
|
600
|
+
exp_x = st.selectbox(
|
|
601
|
+
"X column",
|
|
602
|
+
exp_num_cols,
|
|
603
|
+
format_func=exp_col_fmt,
|
|
604
|
+
key="exp_xcol",
|
|
605
|
+
)
|
|
606
|
+
with c2:
|
|
607
|
+
exp_y = st.selectbox(
|
|
608
|
+
"Y column",
|
|
609
|
+
exp_num_cols,
|
|
610
|
+
index=min(1, len(exp_num_cols) - 1),
|
|
611
|
+
format_func=exp_col_fmt,
|
|
612
|
+
key="exp_ycol",
|
|
613
|
+
)
|
|
614
|
+
else:
|
|
615
|
+
st.caption("Uses the full dataset matrix (no column selection needed).")
|
|
616
|
+
|
|
617
|
+
if st.button("Generate & Export", type="primary", key="exp_run"):
|
|
618
|
+
if not exp_num_cols and exp_type not in _mat_types:
|
|
619
|
+
st.error("No numeric columns available for this dataset.")
|
|
620
|
+
else:
|
|
621
|
+
try:
|
|
622
|
+
title = exp_title or _INTERACTIVE_TYPES[exp_type]
|
|
623
|
+
|
|
624
|
+
if exp_type == "interactive_histogram":
|
|
625
|
+
arr = (
|
|
626
|
+
_to_1d(data, exp_data_col)
|
|
627
|
+
if exp_data_col is not None
|
|
628
|
+
else (np.asarray(data, dtype=float).flatten())
|
|
629
|
+
)
|
|
630
|
+
fig = interactive_histogram(arr, title=title)
|
|
631
|
+
|
|
632
|
+
elif exp_type == "interactive_scatter":
|
|
633
|
+
x = _to_1d(data, exp_x) if exp_x is not None else np.array([])
|
|
634
|
+
y = _to_1d(data, exp_y) if exp_y is not None else np.array([])
|
|
635
|
+
fig = interactive_scatter(
|
|
636
|
+
x,
|
|
637
|
+
y,
|
|
638
|
+
xlabel=str(exp_x or "x"),
|
|
639
|
+
ylabel=str(exp_y or "y"),
|
|
640
|
+
title=title,
|
|
641
|
+
)
|
|
642
|
+
|
|
643
|
+
elif exp_type == "interactive_line":
|
|
644
|
+
if exp_x is not None and exp_y is not None:
|
|
645
|
+
x = _to_1d(data, exp_x)
|
|
646
|
+
y = [_to_1d(data, exp_y)]
|
|
647
|
+
lbl = [str(exp_y)]
|
|
648
|
+
else:
|
|
649
|
+
arr = np.asarray(data, dtype=float).flatten()
|
|
650
|
+
x = np.arange(len(arr))
|
|
651
|
+
y = [arr]
|
|
652
|
+
lbl = ["Data"]
|
|
653
|
+
fig = interactive_line(x, y, labels=lbl, title=title)
|
|
654
|
+
|
|
655
|
+
elif exp_type == "interactive_heatmap":
|
|
656
|
+
if isinstance(data, pd.DataFrame):
|
|
657
|
+
cols = _numeric_cols(data)
|
|
658
|
+
mat = data[cols].values.astype(float)
|
|
659
|
+
else:
|
|
660
|
+
mat = np.asarray(data, dtype=float)
|
|
661
|
+
if mat.ndim != 2:
|
|
662
|
+
st.error("Heatmap requires a 2-D array.")
|
|
663
|
+
st.stop()
|
|
664
|
+
fig = interactive_heatmap(mat, title=title)
|
|
665
|
+
|
|
666
|
+
else: # interactive_3d_surface
|
|
667
|
+
Z = np.asarray(data, dtype=float)
|
|
668
|
+
if Z.ndim != 2:
|
|
669
|
+
st.error("3D surface requires a 2-D array.")
|
|
670
|
+
st.stop()
|
|
671
|
+
nr, nc = Z.shape
|
|
672
|
+
X, Y = np.meshgrid(np.arange(nc), np.arange(nr))
|
|
673
|
+
fig = interactive_3d_surface(X, Y, Z, title=title)
|
|
674
|
+
|
|
675
|
+
st.plotly_chart(fig, width="stretch")
|
|
676
|
+
_html_download(fig, filename=f"{exp_type}.html")
|
|
677
|
+
st.success("Plot ready — click the button above to download.")
|
|
678
|
+
|
|
679
|
+
except Exception as exc:
|
|
680
|
+
st.error(f"Error: {exc}")
|
|
681
|
+
with st.expander("Details"):
|
|
682
|
+
st.code(traceback.format_exc())
|
|
683
|
+
|
|
684
|
+
with st.expander("About HTML export"):
|
|
685
|
+
st.markdown("""
|
|
686
|
+
The downloaded HTML file:
|
|
687
|
+
- Contains the full Plotly chart with zoom, pan, hover, and download toolbar
|
|
688
|
+
- Includes the Plotly.js library (~3 MB) — no internet connection needed to view
|
|
689
|
+
- Can be embedded in HTML reports or shared with collaborators who don't have Python
|
|
690
|
+
- Opens in Chrome, Firefox, Safari, and Edge
|
|
691
|
+
""")
|