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,2721 @@
|
|
|
1
|
+
"""Dynamic plot configuration widgets for Streamlit GUI.
|
|
2
|
+
|
|
3
|
+
Provides widget factories that render Streamlit controls for each plot type
|
|
4
|
+
and return configuration dictionaries ready to pass to plotting functions.
|
|
5
|
+
|
|
6
|
+
Main API
|
|
7
|
+
--------
|
|
8
|
+
get_plot_config_widgets(plot_type, data) -> dict
|
|
9
|
+
Render Streamlit widgets for the given plot type and dataset.
|
|
10
|
+
Returns a config dict with plot parameters and ``_``-prefixed
|
|
11
|
+
data-selection hints (column names or array indices).
|
|
12
|
+
|
|
13
|
+
get_plot_kwargs(config) -> dict
|
|
14
|
+
Strip ``_``-prefixed keys and None values from a config dict so the
|
|
15
|
+
result can be unpacked directly into a plotting function.
|
|
16
|
+
|
|
17
|
+
PLOT_TYPES : dict
|
|
18
|
+
Ordered catalogue of all supported plot types with display labels
|
|
19
|
+
and library categories.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
import streamlit as st
|
|
25
|
+
import numpy as np
|
|
26
|
+
import pandas as pd
|
|
27
|
+
from typing import Any, Dict, List, Optional, Tuple
|
|
28
|
+
|
|
29
|
+
# ── Plot type catalogue ──────────────────────────────────────────────────────
|
|
30
|
+
|
|
31
|
+
PLOT_TYPES: Dict[str, Dict[str, str]] = {
|
|
32
|
+
# Matplotlib (static)
|
|
33
|
+
"histogram": {"label": "Histogram", "category": "Matplotlib"},
|
|
34
|
+
"line_plot": {"label": "Line Plot", "category": "Matplotlib"},
|
|
35
|
+
"scatter_plot": {"label": "Scatter Plot", "category": "Matplotlib"},
|
|
36
|
+
"bar_chart": {"label": "Bar Chart", "category": "Matplotlib"},
|
|
37
|
+
"heatmap": {"label": "Heatmap", "category": "Matplotlib"},
|
|
38
|
+
"contour_plot": {"label": "Contour Plot", "category": "Matplotlib"},
|
|
39
|
+
"waterfall_plot": {"label": "Waterfall Plot", "category": "Matplotlib"},
|
|
40
|
+
"dual_axis_plot": {"label": "Dual Y-Axis Plot", "category": "Matplotlib"},
|
|
41
|
+
"broken_axis_plot": {"label": "Broken Axis Plot", "category": "Matplotlib"},
|
|
42
|
+
"z_colored_scatter": {
|
|
43
|
+
"label": "Z-Colored Scatter",
|
|
44
|
+
"category": "Matplotlib",
|
|
45
|
+
},
|
|
46
|
+
"bubble_chart": {"label": "Bubble Chart", "category": "Matplotlib"},
|
|
47
|
+
"polar_plot": {"label": "Polar Plot", "category": "Matplotlib"},
|
|
48
|
+
"histogram_2d": {"label": "2D Histogram", "category": "Matplotlib"},
|
|
49
|
+
# Seaborn (statistical)
|
|
50
|
+
"distribution_plot": {"label": "Distribution", "category": "Seaborn"},
|
|
51
|
+
"box_plot": {"label": "Box / Violin Plot", "category": "Seaborn"},
|
|
52
|
+
"regression_plot": {"label": "Regression Plot", "category": "Seaborn"},
|
|
53
|
+
"pair_plot": {"label": "Pair Plot", "category": "Seaborn"},
|
|
54
|
+
# Plotly (interactive)
|
|
55
|
+
"interactive_histogram": {
|
|
56
|
+
"label": "Interactive Histogram",
|
|
57
|
+
"category": "Plotly",
|
|
58
|
+
},
|
|
59
|
+
"interactive_scatter": {"label": "Interactive Scatter", "category": "Plotly"},
|
|
60
|
+
"interactive_line": {"label": "Interactive Line", "category": "Plotly"},
|
|
61
|
+
"interactive_heatmap": {"label": "Interactive Heatmap", "category": "Plotly"},
|
|
62
|
+
"interactive_3d_surface": {"label": "3D Surface Plot", "category": "Plotly"},
|
|
63
|
+
"interactive_3d_scatter": {"label": "3D Scatter Plot", "category": "Plotly"},
|
|
64
|
+
# M backlog — new plot types
|
|
65
|
+
"scatter_with_regression": {"label": "Scatter + Regression", "category": "Matplotlib"},
|
|
66
|
+
"residual_plot": {"label": "Residual Plot", "category": "Matplotlib"},
|
|
67
|
+
"interactive_ternary": {"label": "Ternary Plot", "category": "Plotly"},
|
|
68
|
+
# Specialty
|
|
69
|
+
"inset_plot": {"label": "Inset Plot", "category": "Specialty"},
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
# ── Internal helpers ─────────────────────────────────────────────────────────
|
|
74
|
+
|
|
75
|
+
# Named discrete color palettes available in the GUI.
|
|
76
|
+
# Each entry: (display_label, list_of_hex_or_named_colors)
|
|
77
|
+
COLOR_PALETTES: Dict[str, List[str]] = {
|
|
78
|
+
"Default": [
|
|
79
|
+
"steelblue",
|
|
80
|
+
"tomato",
|
|
81
|
+
"seagreen",
|
|
82
|
+
"darkorange",
|
|
83
|
+
"mediumpurple",
|
|
84
|
+
"crimson",
|
|
85
|
+
"teal",
|
|
86
|
+
"slategray",
|
|
87
|
+
],
|
|
88
|
+
# Wong (2011) — 8-color, safe for ~8% deuteranopia/protanopia population
|
|
89
|
+
"Color-Blind Safe (Wong)": [
|
|
90
|
+
"#000000",
|
|
91
|
+
"#E69F00",
|
|
92
|
+
"#56B4E9",
|
|
93
|
+
"#009E73",
|
|
94
|
+
"#F0E442",
|
|
95
|
+
"#0072B2",
|
|
96
|
+
"#D55E00",
|
|
97
|
+
"#CC79A7",
|
|
98
|
+
],
|
|
99
|
+
# Okabe & Ito — widely recommended by Nature/Science style guides
|
|
100
|
+
"Color-Blind Safe (Okabe-Ito)": [
|
|
101
|
+
"#E69F00",
|
|
102
|
+
"#56B4E9",
|
|
103
|
+
"#009E73",
|
|
104
|
+
"#F0E442",
|
|
105
|
+
"#0072B2",
|
|
106
|
+
"#D55E00",
|
|
107
|
+
"#CC79A7",
|
|
108
|
+
"#000000",
|
|
109
|
+
],
|
|
110
|
+
# Paul Tol muted — perceptually distinct, print-friendly
|
|
111
|
+
"Muted (Tol)": [
|
|
112
|
+
"#332288",
|
|
113
|
+
"#88CCEE",
|
|
114
|
+
"#44AA99",
|
|
115
|
+
"#117733",
|
|
116
|
+
"#999933",
|
|
117
|
+
"#DDCC77",
|
|
118
|
+
"#CC6677",
|
|
119
|
+
"#882255",
|
|
120
|
+
],
|
|
121
|
+
"Pastel": [
|
|
122
|
+
"#AEC6CF",
|
|
123
|
+
"#FFD1DC",
|
|
124
|
+
"#B5EAD7",
|
|
125
|
+
"#FFDAC1",
|
|
126
|
+
"#C7CEEA",
|
|
127
|
+
"#FF9AA2",
|
|
128
|
+
"#E2F0CB",
|
|
129
|
+
"#F2C4CE",
|
|
130
|
+
],
|
|
131
|
+
"Vibrant": [
|
|
132
|
+
"#E6194B",
|
|
133
|
+
"#3CB44B",
|
|
134
|
+
"#FFE119",
|
|
135
|
+
"#4363D8",
|
|
136
|
+
"#F58231",
|
|
137
|
+
"#911EB4",
|
|
138
|
+
"#42D4F4",
|
|
139
|
+
"#F032E6",
|
|
140
|
+
],
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
# Convenience list for selectbox / dropdowns
|
|
144
|
+
COLOR_PALETTE_NAMES: List[str] = list(COLOR_PALETTES.keys())
|
|
145
|
+
|
|
146
|
+
# Default single-series color list (backward-compat alias)
|
|
147
|
+
_COLORS = COLOR_PALETTES["Default"]
|
|
148
|
+
|
|
149
|
+
_CMAPS = [
|
|
150
|
+
"viridis",
|
|
151
|
+
"plasma",
|
|
152
|
+
"inferno",
|
|
153
|
+
"magma",
|
|
154
|
+
"cividis",
|
|
155
|
+
"hot",
|
|
156
|
+
"cool",
|
|
157
|
+
"RdYlBu",
|
|
158
|
+
"seismic",
|
|
159
|
+
"bwr",
|
|
160
|
+
"coolwarm",
|
|
161
|
+
]
|
|
162
|
+
_COLORSCALES = [
|
|
163
|
+
"Viridis",
|
|
164
|
+
"Plasma",
|
|
165
|
+
"Inferno",
|
|
166
|
+
"Magma",
|
|
167
|
+
"Cividis",
|
|
168
|
+
"Hot",
|
|
169
|
+
"RdBu",
|
|
170
|
+
"Seismic",
|
|
171
|
+
"Earth",
|
|
172
|
+
"Electric",
|
|
173
|
+
]
|
|
174
|
+
|
|
175
|
+
_FONT_OPTIONS = [
|
|
176
|
+
"sans-serif",
|
|
177
|
+
"serif",
|
|
178
|
+
"monospace",
|
|
179
|
+
"Arial",
|
|
180
|
+
"Helvetica",
|
|
181
|
+
"Times New Roman",
|
|
182
|
+
"Courier New",
|
|
183
|
+
"DejaVu Sans",
|
|
184
|
+
"DejaVu Serif",
|
|
185
|
+
]
|
|
186
|
+
|
|
187
|
+
# Keys that are GUI-only style controls — must be stripped from plotting kwargs
|
|
188
|
+
_STYLE_KEYS = frozenset(
|
|
189
|
+
{
|
|
190
|
+
"grid",
|
|
191
|
+
"grid_linestyle",
|
|
192
|
+
"grid_which",
|
|
193
|
+
"fontsize", # legacy fallback — kept for old presets
|
|
194
|
+
"fontsize_label",
|
|
195
|
+
"fontsize_tick",
|
|
196
|
+
"fontsize_title",
|
|
197
|
+
"fontsize_legend",
|
|
198
|
+
"fontfamily",
|
|
199
|
+
"fontcolor",
|
|
200
|
+
"linewidth",
|
|
201
|
+
"legend_frameon",
|
|
202
|
+
"legend_framealpha",
|
|
203
|
+
"legend_position",
|
|
204
|
+
"xlim_min",
|
|
205
|
+
"xlim_max",
|
|
206
|
+
"ylim_min",
|
|
207
|
+
"ylim_max",
|
|
208
|
+
"x_scale",
|
|
209
|
+
"x_notation",
|
|
210
|
+
"x_transform",
|
|
211
|
+
"x_transform_value",
|
|
212
|
+
"y_scale",
|
|
213
|
+
"y_notation",
|
|
214
|
+
"y_transform",
|
|
215
|
+
"y_transform_value",
|
|
216
|
+
"z_scale",
|
|
217
|
+
"z_notation",
|
|
218
|
+
"z_transform",
|
|
219
|
+
"z_transform_value",
|
|
220
|
+
"figure_caption",
|
|
221
|
+
"color_palette",
|
|
222
|
+
"cmap_reversed",
|
|
223
|
+
# Plotly-specific style
|
|
224
|
+
"plotly_height",
|
|
225
|
+
"plotly_line_width",
|
|
226
|
+
}
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def _numeric_cols(data: Any) -> List[str]:
|
|
231
|
+
if isinstance(data, pd.DataFrame):
|
|
232
|
+
return data.select_dtypes(include=[np.number]).columns.tolist()
|
|
233
|
+
return []
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def _all_cols(data: Any) -> List[str]:
|
|
237
|
+
if isinstance(data, pd.DataFrame):
|
|
238
|
+
return list(data.columns)
|
|
239
|
+
return []
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def _is_2d_array(data: Any) -> bool:
|
|
243
|
+
return isinstance(data, np.ndarray) and data.ndim == 2
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
_LABEL_HELP = (
|
|
247
|
+
"Supports matplotlib math notation — wrap in $…$. "
|
|
248
|
+
"Examples: $\\alpha$, $x^{2}$, $x_{0}$, $\\mu$m, $\\AA$"
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def _symbol_reference_expander() -> None:
|
|
253
|
+
"""Collapsible symbol reference for axis/title label inputs."""
|
|
254
|
+
with st.expander("Symbol reference"):
|
|
255
|
+
st.caption(
|
|
256
|
+
"Copy any code below and paste it into a label field. "
|
|
257
|
+
"Wrap expressions in **$…$** — e.g. `$\\alpha$` renders as α."
|
|
258
|
+
)
|
|
259
|
+
col_a, col_b, col_c = st.columns(3)
|
|
260
|
+
with col_a:
|
|
261
|
+
st.markdown(
|
|
262
|
+
"**Greek — lowercase** \n"
|
|
263
|
+
"α `$\\alpha$` · β `$\\beta$` · γ `$\\gamma$` \n"
|
|
264
|
+
"δ `$\\delta$` · ε `$\\epsilon$` · ζ `$\\zeta$` \n"
|
|
265
|
+
"η `$\\eta$` · θ `$\\theta$` · κ `$\\kappa$` \n"
|
|
266
|
+
"λ `$\\lambda$` · μ `$\\mu$` · ν `$\\nu$` \n"
|
|
267
|
+
"ξ `$\\xi$` · π `$\\pi$` · ρ `$\\rho$` \n"
|
|
268
|
+
"σ `$\\sigma$` · τ `$\\tau$` · φ `$\\phi$` \n"
|
|
269
|
+
"χ `$\\chi$` · ψ `$\\psi$` · ω `$\\omega$`"
|
|
270
|
+
)
|
|
271
|
+
with col_b:
|
|
272
|
+
st.markdown(
|
|
273
|
+
"**Greek — uppercase** \n"
|
|
274
|
+
"Γ `$\\Gamma$` · Δ `$\\Delta$` · Θ `$\\Theta$` \n"
|
|
275
|
+
"Λ `$\\Lambda$` · Ξ `$\\Xi$` · Π `$\\Pi$` \n"
|
|
276
|
+
"Σ `$\\Sigma$` · Φ `$\\Phi$` · Ψ `$\\Psi$` \n"
|
|
277
|
+
"Ω `$\\Omega$` \n\n"
|
|
278
|
+
"**Accents** \n"
|
|
279
|
+
"x̄ `$\\bar{x}$` · ẋ `$\\dot{x}$` \n"
|
|
280
|
+
"x̃ `$\\tilde{x}$` · x̂ `$\\hat{x}$` \n"
|
|
281
|
+
"x⃗ `$\\vec{x}$`"
|
|
282
|
+
)
|
|
283
|
+
with col_c:
|
|
284
|
+
st.markdown(
|
|
285
|
+
"**Super / subscript** \n"
|
|
286
|
+
"x² `$x^{2}$` · xⁿ `$x^{n}$` \n"
|
|
287
|
+
"x₀ `$x_{0}$` · xₙ `$x_{n}$` \n\n"
|
|
288
|
+
"**Units & special** \n"
|
|
289
|
+
"Å `$\\AA$` · ° `$^{\\circ}$` \n"
|
|
290
|
+
"± `$\\pm$` · × `$\\times$` \n"
|
|
291
|
+
"≤ `$\\leq$` · ≥ `$\\geq$` · ≠ `$\\neq$` \n"
|
|
292
|
+
"∞ `$\\infty$` · ∂ `$\\partial$` · ∇ `$\\nabla$` \n"
|
|
293
|
+
"√ `$\\sqrt{x}$` · ∫ `$\\int$` \n"
|
|
294
|
+
"½ `$\\frac{1}{2}$`"
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def _label_widgets(
|
|
299
|
+
prefix: str,
|
|
300
|
+
x_default: str = "",
|
|
301
|
+
y_default: str = "",
|
|
302
|
+
) -> Dict[str, Optional[str]]:
|
|
303
|
+
"""Render title + axis-label text inputs; return partial config dict.
|
|
304
|
+
|
|
305
|
+
x_default / y_default are shown as placeholder hints and used as
|
|
306
|
+
fallback values when the user leaves the field empty (so the
|
|
307
|
+
selected column name becomes the default axis label).
|
|
308
|
+
"""
|
|
309
|
+
col1, col2, col3 = st.columns(3)
|
|
310
|
+
with col1:
|
|
311
|
+
title = st.text_input(
|
|
312
|
+
"Title",
|
|
313
|
+
key=f"{prefix}_title",
|
|
314
|
+
help=_LABEL_HELP,
|
|
315
|
+
)
|
|
316
|
+
with col2:
|
|
317
|
+
xlabel = st.text_input(
|
|
318
|
+
"X Label",
|
|
319
|
+
placeholder=f"Default: {x_default}" if x_default else "",
|
|
320
|
+
key=f"{prefix}_xlabel",
|
|
321
|
+
help=_LABEL_HELP,
|
|
322
|
+
)
|
|
323
|
+
with col3:
|
|
324
|
+
ylabel = st.text_input(
|
|
325
|
+
"Y Label",
|
|
326
|
+
placeholder=f"Default: {y_default}" if y_default else "",
|
|
327
|
+
key=f"{prefix}_ylabel",
|
|
328
|
+
help=_LABEL_HELP,
|
|
329
|
+
)
|
|
330
|
+
_symbol_reference_expander()
|
|
331
|
+
return {
|
|
332
|
+
"title": title or None,
|
|
333
|
+
"xlabel": xlabel or x_default or None,
|
|
334
|
+
"ylabel": ylabel or y_default or None,
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
def _figsize_widget(
|
|
339
|
+
prefix: str,
|
|
340
|
+
default: Tuple[float, float] = (8.0, 6.0),
|
|
341
|
+
) -> Tuple[float, float]:
|
|
342
|
+
col1, col2 = st.columns(2)
|
|
343
|
+
with col1:
|
|
344
|
+
w = st.number_input(
|
|
345
|
+
"Width (in)",
|
|
346
|
+
min_value=4.0,
|
|
347
|
+
max_value=20.0,
|
|
348
|
+
value=float(default[0]),
|
|
349
|
+
step=0.5,
|
|
350
|
+
key=f"{prefix}_fw",
|
|
351
|
+
)
|
|
352
|
+
with col2:
|
|
353
|
+
h = st.number_input(
|
|
354
|
+
"Height (in)",
|
|
355
|
+
min_value=3.0,
|
|
356
|
+
max_value=16.0,
|
|
357
|
+
value=float(default[1]),
|
|
358
|
+
step=0.5,
|
|
359
|
+
key=f"{prefix}_fh",
|
|
360
|
+
)
|
|
361
|
+
return (w, h)
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
def _color_widget(prefix: str, default: str = "steelblue") -> str:
|
|
365
|
+
options = _COLORS + ["custom"]
|
|
366
|
+
idx = options.index(default) if default in options else 0
|
|
367
|
+
choice = st.selectbox(
|
|
368
|
+
"Color",
|
|
369
|
+
options,
|
|
370
|
+
index=idx,
|
|
371
|
+
key=f"{prefix}_color",
|
|
372
|
+
)
|
|
373
|
+
if choice == "custom":
|
|
374
|
+
choice = st.color_picker(
|
|
375
|
+
"Pick color",
|
|
376
|
+
"#1f77b4",
|
|
377
|
+
key=f"{prefix}_color_pick",
|
|
378
|
+
)
|
|
379
|
+
return choice
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
def _axis_limits_widgets(prefix: str) -> Dict:
|
|
383
|
+
"""Optional manual X/Y axis limit overrides."""
|
|
384
|
+
config: Dict = {}
|
|
385
|
+
c1, c2 = st.columns(2)
|
|
386
|
+
with c1:
|
|
387
|
+
if st.checkbox("Override X limits", key=f"{prefix}_use_xlim"):
|
|
388
|
+
cc1, cc2 = st.columns(2)
|
|
389
|
+
with cc1:
|
|
390
|
+
config["xlim_min"] = st.number_input(
|
|
391
|
+
"X min",
|
|
392
|
+
value=0.0,
|
|
393
|
+
key=f"{prefix}_xmin",
|
|
394
|
+
)
|
|
395
|
+
with cc2:
|
|
396
|
+
config["xlim_max"] = st.number_input(
|
|
397
|
+
"X max",
|
|
398
|
+
value=1.0,
|
|
399
|
+
key=f"{prefix}_xmax",
|
|
400
|
+
)
|
|
401
|
+
with c2:
|
|
402
|
+
if st.checkbox("Override Y limits", key=f"{prefix}_use_ylim"):
|
|
403
|
+
cc1, cc2 = st.columns(2)
|
|
404
|
+
with cc1:
|
|
405
|
+
config["ylim_min"] = st.number_input(
|
|
406
|
+
"Y min",
|
|
407
|
+
value=0.0,
|
|
408
|
+
key=f"{prefix}_ymin",
|
|
409
|
+
)
|
|
410
|
+
with cc2:
|
|
411
|
+
config["ylim_max"] = st.number_input(
|
|
412
|
+
"Y max",
|
|
413
|
+
value=1.0,
|
|
414
|
+
key=f"{prefix}_ymax",
|
|
415
|
+
)
|
|
416
|
+
return config
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
def _grid_widgets(prefix: str) -> Dict:
|
|
420
|
+
"""Grid line controls (Matplotlib).
|
|
421
|
+
|
|
422
|
+
Uses ``_shared_`` keys so the preference is consistent across all
|
|
423
|
+
plot types in the same session.
|
|
424
|
+
"""
|
|
425
|
+
c1, c2, c3 = st.columns(3)
|
|
426
|
+
with c1:
|
|
427
|
+
show_grid = st.checkbox(
|
|
428
|
+
"Show grid",
|
|
429
|
+
value=True,
|
|
430
|
+
key="_shared_grid",
|
|
431
|
+
)
|
|
432
|
+
config: Dict = {"grid": show_grid}
|
|
433
|
+
if show_grid:
|
|
434
|
+
with c2:
|
|
435
|
+
config["grid_linestyle"] = st.selectbox(
|
|
436
|
+
"Grid style",
|
|
437
|
+
["--", "-", ":", "-."],
|
|
438
|
+
key="_shared_grid_ls",
|
|
439
|
+
)
|
|
440
|
+
with c3:
|
|
441
|
+
config["grid_which"] = st.selectbox(
|
|
442
|
+
"Grid density",
|
|
443
|
+
["major", "minor", "both"],
|
|
444
|
+
key="_shared_grid_which",
|
|
445
|
+
)
|
|
446
|
+
return config
|
|
447
|
+
|
|
448
|
+
|
|
449
|
+
def _appearance_widgets(
|
|
450
|
+
prefix: str,
|
|
451
|
+
include_linewidth: bool = False,
|
|
452
|
+
) -> Dict:
|
|
453
|
+
"""Font family, color, per-element font sizes, and optionally line width.
|
|
454
|
+
|
|
455
|
+
All widget keys use the ``_shared_`` prefix so that settings persist
|
|
456
|
+
when switching between plot types within the same session.
|
|
457
|
+
"""
|
|
458
|
+
config: Dict = {}
|
|
459
|
+
|
|
460
|
+
# Row 1 — font style, color, and (optional) line width
|
|
461
|
+
n_cols = 3 if not include_linewidth else 4
|
|
462
|
+
cols = st.columns(n_cols)
|
|
463
|
+
with cols[0]:
|
|
464
|
+
config["fontfamily"] = st.selectbox(
|
|
465
|
+
"Font",
|
|
466
|
+
_FONT_OPTIONS,
|
|
467
|
+
key="_shared_fontfamily",
|
|
468
|
+
)
|
|
469
|
+
with cols[1]:
|
|
470
|
+
config["fontcolor"] = st.color_picker(
|
|
471
|
+
"Font color",
|
|
472
|
+
"#262730",
|
|
473
|
+
key="_shared_fontcolor",
|
|
474
|
+
)
|
|
475
|
+
if include_linewidth:
|
|
476
|
+
with cols[2]:
|
|
477
|
+
config["linewidth"] = st.slider(
|
|
478
|
+
"Line width",
|
|
479
|
+
0.5,
|
|
480
|
+
6.0,
|
|
481
|
+
1.5,
|
|
482
|
+
0.5,
|
|
483
|
+
key="_shared_lw",
|
|
484
|
+
)
|
|
485
|
+
|
|
486
|
+
# Row 2 — individual element font sizes
|
|
487
|
+
s1, s2, s3, s4 = st.columns(4)
|
|
488
|
+
with s1:
|
|
489
|
+
config["fontsize_label"] = st.number_input(
|
|
490
|
+
"Label size",
|
|
491
|
+
min_value=6,
|
|
492
|
+
max_value=32,
|
|
493
|
+
value=11,
|
|
494
|
+
key="_shared_fs_label",
|
|
495
|
+
)
|
|
496
|
+
with s2:
|
|
497
|
+
config["fontsize_tick"] = st.number_input(
|
|
498
|
+
"Tick size",
|
|
499
|
+
min_value=6,
|
|
500
|
+
max_value=32,
|
|
501
|
+
value=10,
|
|
502
|
+
key="_shared_fs_tick",
|
|
503
|
+
)
|
|
504
|
+
with s3:
|
|
505
|
+
config["fontsize_title"] = st.number_input(
|
|
506
|
+
"Title size",
|
|
507
|
+
min_value=6,
|
|
508
|
+
max_value=36,
|
|
509
|
+
value=13,
|
|
510
|
+
key="_shared_fs_title",
|
|
511
|
+
)
|
|
512
|
+
with s4:
|
|
513
|
+
config["fontsize_legend"] = st.number_input(
|
|
514
|
+
"Legend size",
|
|
515
|
+
min_value=6,
|
|
516
|
+
max_value=32,
|
|
517
|
+
value=10,
|
|
518
|
+
key="_shared_fs_legend",
|
|
519
|
+
)
|
|
520
|
+
return config
|
|
521
|
+
|
|
522
|
+
|
|
523
|
+
def _legend_style_widgets(prefix: str) -> Dict:
|
|
524
|
+
"""Legend border, background, and position controls.
|
|
525
|
+
|
|
526
|
+
Uses ``_shared_`` keys so the preference is consistent across all
|
|
527
|
+
plot types in the same session.
|
|
528
|
+
"""
|
|
529
|
+
c1, c2, c3 = st.columns(3)
|
|
530
|
+
with c1:
|
|
531
|
+
frameon = st.checkbox(
|
|
532
|
+
"Legend border",
|
|
533
|
+
value=True,
|
|
534
|
+
key="_shared_leg_frame",
|
|
535
|
+
)
|
|
536
|
+
with c2:
|
|
537
|
+
opaque_bg = st.checkbox(
|
|
538
|
+
"Opaque background",
|
|
539
|
+
value=True,
|
|
540
|
+
key="_shared_leg_bg",
|
|
541
|
+
)
|
|
542
|
+
with c3:
|
|
543
|
+
position = st.selectbox(
|
|
544
|
+
"Legend position",
|
|
545
|
+
[
|
|
546
|
+
"best",
|
|
547
|
+
"upper right",
|
|
548
|
+
"upper left",
|
|
549
|
+
"lower right",
|
|
550
|
+
"lower left",
|
|
551
|
+
"center right",
|
|
552
|
+
"outside right",
|
|
553
|
+
],
|
|
554
|
+
key="_shared_leg_pos",
|
|
555
|
+
)
|
|
556
|
+
return {
|
|
557
|
+
"legend_frameon": frameon,
|
|
558
|
+
"legend_framealpha": 0.8 if opaque_bg else 0.0,
|
|
559
|
+
"legend_position": position,
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
|
|
563
|
+
def _custom_labels_widget(prefix: str, y_cols: List[str]) -> Dict:
|
|
564
|
+
"""Optional per-series legend label overrides.
|
|
565
|
+
|
|
566
|
+
Renders one text-input per Y series. Empty inputs keep the column
|
|
567
|
+
name as the label. Returns ``{'_custom_labels': [str, ...]}``.
|
|
568
|
+
"""
|
|
569
|
+
if not y_cols:
|
|
570
|
+
return {}
|
|
571
|
+
with st.expander("Custom legend labels (optional)"):
|
|
572
|
+
labels = []
|
|
573
|
+
cols_ui = st.columns(min(len(y_cols), 4))
|
|
574
|
+
for i, col in enumerate(y_cols):
|
|
575
|
+
with cols_ui[i % len(cols_ui)]:
|
|
576
|
+
lbl = st.text_input(
|
|
577
|
+
f"'{col}'",
|
|
578
|
+
placeholder=str(col),
|
|
579
|
+
key=f"{prefix}_clbl_{i}",
|
|
580
|
+
)
|
|
581
|
+
labels.append(lbl.strip() or str(col))
|
|
582
|
+
return {"_custom_labels": labels}
|
|
583
|
+
|
|
584
|
+
|
|
585
|
+
def _custom_palette_widget(prefix: str) -> Dict:
|
|
586
|
+
"""Color pickers for a user-defined palette (2–8 colors).
|
|
587
|
+
|
|
588
|
+
Uses ``_shared_`` keys so the custom colors survive plot-type switches.
|
|
589
|
+
"""
|
|
590
|
+
defaults = [
|
|
591
|
+
"#1f77b4",
|
|
592
|
+
"#ff7f0e",
|
|
593
|
+
"#2ca02c",
|
|
594
|
+
"#d62728",
|
|
595
|
+
"#9467bd",
|
|
596
|
+
"#8c564b",
|
|
597
|
+
"#e377c2",
|
|
598
|
+
"#7f7f7f",
|
|
599
|
+
]
|
|
600
|
+
n_colors = int(
|
|
601
|
+
st.number_input(
|
|
602
|
+
"Number of colors",
|
|
603
|
+
min_value=2,
|
|
604
|
+
max_value=8,
|
|
605
|
+
value=4,
|
|
606
|
+
key="_shared_custom_n",
|
|
607
|
+
)
|
|
608
|
+
)
|
|
609
|
+
cols = st.columns(n_colors)
|
|
610
|
+
colors = []
|
|
611
|
+
for i in range(n_colors):
|
|
612
|
+
with cols[i]:
|
|
613
|
+
c = st.color_picker(
|
|
614
|
+
f"C{i + 1}",
|
|
615
|
+
defaults[i],
|
|
616
|
+
key=f"_shared_custom_c{i}",
|
|
617
|
+
)
|
|
618
|
+
colors.append(c)
|
|
619
|
+
return {"_custom_colors": colors}
|
|
620
|
+
|
|
621
|
+
|
|
622
|
+
def _palette_widget(prefix: str) -> Dict:
|
|
623
|
+
"""Color palette selector for multi-series plots.
|
|
624
|
+
|
|
625
|
+
Uses a ``_shared_`` key so the selection persists across plot types.
|
|
626
|
+
"""
|
|
627
|
+
options = COLOR_PALETTE_NAMES + ["Custom"]
|
|
628
|
+
palette = st.selectbox(
|
|
629
|
+
"Color palette",
|
|
630
|
+
options,
|
|
631
|
+
key="_shared_palette",
|
|
632
|
+
help="Choose a discrete color cycle for plot series.",
|
|
633
|
+
)
|
|
634
|
+
config: Dict = {"color_palette": palette}
|
|
635
|
+
if palette == "Custom":
|
|
636
|
+
config.update(_custom_palette_widget(prefix))
|
|
637
|
+
return config
|
|
638
|
+
|
|
639
|
+
|
|
640
|
+
_TRANSFORM_LABELS = {
|
|
641
|
+
"none": "None",
|
|
642
|
+
"normalize_max": "Normalize to max",
|
|
643
|
+
"normalize_01": "Normalize to [0, 1]",
|
|
644
|
+
"scale_by": "Scale by constant",
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
|
|
648
|
+
def _axis_scale_widgets(
|
|
649
|
+
prefix: str,
|
|
650
|
+
axis: str,
|
|
651
|
+
label: str,
|
|
652
|
+
) -> Dict:
|
|
653
|
+
"""Generic scale / notation / transform block for one axis.
|
|
654
|
+
|
|
655
|
+
Parameters
|
|
656
|
+
----------
|
|
657
|
+
prefix : str
|
|
658
|
+
Widget key namespace.
|
|
659
|
+
axis : str
|
|
660
|
+
Short axis tag used in config keys and widget keys, e.g. ``'x'``,
|
|
661
|
+
``'y'``, or ``'z'``.
|
|
662
|
+
label : str
|
|
663
|
+
Human-readable axis name shown in widget labels, e.g. ``'X'``,
|
|
664
|
+
``'Y'``, or ``'Color'``.
|
|
665
|
+
"""
|
|
666
|
+
config: Dict = {}
|
|
667
|
+
c1, c2, c3 = st.columns(3)
|
|
668
|
+
with c1:
|
|
669
|
+
config[f"{axis}_scale"] = st.selectbox(
|
|
670
|
+
f"{label} scale",
|
|
671
|
+
["linear", "log"],
|
|
672
|
+
key=f"{prefix}_{axis}scale",
|
|
673
|
+
)
|
|
674
|
+
with c2:
|
|
675
|
+
config[f"{axis}_notation"] = st.selectbox(
|
|
676
|
+
f"{label} notation",
|
|
677
|
+
["default", "scientific", "engineering"],
|
|
678
|
+
key=f"{prefix}_{axis}notation",
|
|
679
|
+
)
|
|
680
|
+
with c3:
|
|
681
|
+
transform = st.selectbox(
|
|
682
|
+
f"{label} transform",
|
|
683
|
+
["none", "normalize_max", "normalize_01", "scale_by"],
|
|
684
|
+
format_func=lambda x: _TRANSFORM_LABELS[x],
|
|
685
|
+
key=f"{prefix}_{axis}transform",
|
|
686
|
+
)
|
|
687
|
+
config[f"{axis}_transform"] = transform
|
|
688
|
+
if transform == "scale_by":
|
|
689
|
+
config[f"{axis}_transform_value"] = st.number_input(
|
|
690
|
+
f"{label} scale factor",
|
|
691
|
+
value=1.0,
|
|
692
|
+
key=f"{prefix}_{axis}scaleval",
|
|
693
|
+
)
|
|
694
|
+
return config
|
|
695
|
+
|
|
696
|
+
|
|
697
|
+
def _x_axis_widgets(prefix: str) -> Dict:
|
|
698
|
+
"""X-axis scale, notation, and data-transform controls."""
|
|
699
|
+
return _axis_scale_widgets(prefix, "x", "X")
|
|
700
|
+
|
|
701
|
+
|
|
702
|
+
def _y_axis_widgets(prefix: str) -> Dict:
|
|
703
|
+
"""Y-axis scale, notation, and data-transform controls."""
|
|
704
|
+
return _axis_scale_widgets(prefix, "y", "Y")
|
|
705
|
+
|
|
706
|
+
|
|
707
|
+
def _z_axis_widgets(prefix: str) -> Dict:
|
|
708
|
+
"""Color-axis (Z) scale, notation, and data-transform controls."""
|
|
709
|
+
return _axis_scale_widgets(prefix, "z", "Color")
|
|
710
|
+
|
|
711
|
+
|
|
712
|
+
def _caption_widget(prefix: str) -> Dict:
|
|
713
|
+
"""Optional figure caption text field."""
|
|
714
|
+
caption = st.text_input(
|
|
715
|
+
"Figure caption (optional)",
|
|
716
|
+
key=f"{prefix}_caption",
|
|
717
|
+
)
|
|
718
|
+
return {"figure_caption": caption or None}
|
|
719
|
+
|
|
720
|
+
|
|
721
|
+
def _plotly_appearance_widgets(prefix: str) -> Dict:
|
|
722
|
+
"""Appearance controls for Plotly interactive plots.
|
|
723
|
+
|
|
724
|
+
Uses ``_shared_ply_`` keys so settings persist across Plotly plot types.
|
|
725
|
+
"""
|
|
726
|
+
config: Dict = {}
|
|
727
|
+
c1, c2, c3, c4 = st.columns(4)
|
|
728
|
+
with c1:
|
|
729
|
+
config["fontsize"] = st.slider(
|
|
730
|
+
"Font size",
|
|
731
|
+
8,
|
|
732
|
+
24,
|
|
733
|
+
13,
|
|
734
|
+
key="_shared_ply_fontsize",
|
|
735
|
+
)
|
|
736
|
+
with c2:
|
|
737
|
+
config["fontfamily"] = st.selectbox(
|
|
738
|
+
"Font",
|
|
739
|
+
_FONT_OPTIONS,
|
|
740
|
+
key="_shared_ply_fontfamily",
|
|
741
|
+
)
|
|
742
|
+
with c3:
|
|
743
|
+
config["fontcolor"] = st.color_picker(
|
|
744
|
+
"Font color",
|
|
745
|
+
"#262730",
|
|
746
|
+
key="_shared_ply_fontcolor",
|
|
747
|
+
)
|
|
748
|
+
with c4:
|
|
749
|
+
config["plotly_height"] = st.slider(
|
|
750
|
+
"Chart height",
|
|
751
|
+
300,
|
|
752
|
+
900,
|
|
753
|
+
450,
|
|
754
|
+
50,
|
|
755
|
+
key="_shared_ply_height",
|
|
756
|
+
)
|
|
757
|
+
c1, c2, c3 = st.columns(3)
|
|
758
|
+
with c1:
|
|
759
|
+
show_grid = st.checkbox(
|
|
760
|
+
"Show grid",
|
|
761
|
+
value=True,
|
|
762
|
+
key="_shared_ply_grid",
|
|
763
|
+
)
|
|
764
|
+
config["grid"] = show_grid
|
|
765
|
+
with c2:
|
|
766
|
+
config["legend_frameon"] = st.checkbox(
|
|
767
|
+
"Legend border",
|
|
768
|
+
value=False,
|
|
769
|
+
key="_shared_ply_leg_frame",
|
|
770
|
+
)
|
|
771
|
+
with c3:
|
|
772
|
+
config["legend_framealpha"] = (
|
|
773
|
+
0.8
|
|
774
|
+
if st.checkbox(
|
|
775
|
+
"Opaque legend bg",
|
|
776
|
+
value=True,
|
|
777
|
+
key="_shared_ply_leg_bg",
|
|
778
|
+
)
|
|
779
|
+
else 0.0
|
|
780
|
+
)
|
|
781
|
+
return config
|
|
782
|
+
|
|
783
|
+
|
|
784
|
+
# ── Per-plot-type widget functions ───────────────────────────────────────────
|
|
785
|
+
|
|
786
|
+
|
|
787
|
+
def _widgets_histogram(data: Any, prefix: str) -> Dict:
|
|
788
|
+
config: Dict = {}
|
|
789
|
+
numeric = _numeric_cols(data)
|
|
790
|
+
|
|
791
|
+
if isinstance(data, pd.DataFrame):
|
|
792
|
+
if not numeric:
|
|
793
|
+
st.warning("No numeric columns found in dataset.")
|
|
794
|
+
return {}
|
|
795
|
+
config["_data_col"] = st.selectbox(
|
|
796
|
+
"Data column",
|
|
797
|
+
numeric,
|
|
798
|
+
key=f"{prefix}_dcol",
|
|
799
|
+
)
|
|
800
|
+
elif isinstance(data, np.ndarray) and np.issubdtype(data.dtype, np.number):
|
|
801
|
+
config["_data_col"] = None
|
|
802
|
+
else:
|
|
803
|
+
st.warning("Histogram requires a numeric array or a DataFrame with numeric columns.")
|
|
804
|
+
return {}
|
|
805
|
+
|
|
806
|
+
st.markdown("**Histogram parameters**")
|
|
807
|
+
col1, col2, col3 = st.columns(3)
|
|
808
|
+
with col1:
|
|
809
|
+
bins_choice = st.selectbox(
|
|
810
|
+
"Bin strategy",
|
|
811
|
+
["auto", "sqrt", "sturges", "custom"],
|
|
812
|
+
key=f"{prefix}_bins_choice",
|
|
813
|
+
)
|
|
814
|
+
if bins_choice == "custom":
|
|
815
|
+
config["bins"] = st.number_input(
|
|
816
|
+
"Number of bins",
|
|
817
|
+
min_value=2,
|
|
818
|
+
max_value=500,
|
|
819
|
+
value=30,
|
|
820
|
+
key=f"{prefix}_bins_n",
|
|
821
|
+
)
|
|
822
|
+
else:
|
|
823
|
+
config["bins"] = bins_choice
|
|
824
|
+
with col2:
|
|
825
|
+
config["color"] = _color_widget(prefix)
|
|
826
|
+
with col3:
|
|
827
|
+
config["alpha"] = st.slider(
|
|
828
|
+
"Opacity",
|
|
829
|
+
0.1,
|
|
830
|
+
1.0,
|
|
831
|
+
0.7,
|
|
832
|
+
0.05,
|
|
833
|
+
key=f"{prefix}_alpha",
|
|
834
|
+
)
|
|
835
|
+
config["density"] = st.checkbox("Normalize (density)", key=f"{prefix}_density")
|
|
836
|
+
|
|
837
|
+
st.markdown("**Figure & labels**")
|
|
838
|
+
config["figsize"] = _figsize_widget(prefix)
|
|
839
|
+
x_lbl = str(config.get("_data_col", "")) or ""
|
|
840
|
+
y_lbl = "Density" if config.get("density") else "Frequency"
|
|
841
|
+
config.update(_label_widgets(prefix, x_default=x_lbl, y_default=y_lbl))
|
|
842
|
+
with st.expander("Appearance"):
|
|
843
|
+
config.update(_grid_widgets(prefix))
|
|
844
|
+
config.update(_appearance_widgets(prefix))
|
|
845
|
+
config.update(_legend_style_widgets(prefix))
|
|
846
|
+
config.update(_axis_limits_widgets(prefix))
|
|
847
|
+
config.update(_y_axis_widgets(prefix))
|
|
848
|
+
config.update(_caption_widget(prefix))
|
|
849
|
+
return config
|
|
850
|
+
|
|
851
|
+
|
|
852
|
+
def _widgets_line_plot(data: Any, prefix: str) -> Dict:
|
|
853
|
+
config: Dict = {}
|
|
854
|
+
numeric = _numeric_cols(data)
|
|
855
|
+
|
|
856
|
+
if isinstance(data, pd.DataFrame):
|
|
857
|
+
if len(numeric) < 2:
|
|
858
|
+
st.warning("Line plot requires at least 2 numeric columns.")
|
|
859
|
+
return {}
|
|
860
|
+
config["_x_col"] = st.selectbox(
|
|
861
|
+
"X column",
|
|
862
|
+
numeric,
|
|
863
|
+
index=0,
|
|
864
|
+
key=f"{prefix}_xcol",
|
|
865
|
+
)
|
|
866
|
+
y_opts = [c for c in numeric if c != config["_x_col"]]
|
|
867
|
+
config["_y_cols"] = st.multiselect(
|
|
868
|
+
"Y column(s)",
|
|
869
|
+
y_opts,
|
|
870
|
+
default=[y_opts[0]] if y_opts else [],
|
|
871
|
+
key=f"{prefix}_ycols",
|
|
872
|
+
)
|
|
873
|
+
if not config["_y_cols"]:
|
|
874
|
+
st.info("Select at least one Y column.")
|
|
875
|
+
return {}
|
|
876
|
+
|
|
877
|
+
elif isinstance(data, np.ndarray):
|
|
878
|
+
if data.ndim == 1:
|
|
879
|
+
st.info("1D array: array index will be used as the X axis.")
|
|
880
|
+
config["_x_col"] = None
|
|
881
|
+
config["_y_cols"] = None
|
|
882
|
+
elif data.ndim == 2:
|
|
883
|
+
opts = [str(i) for i in range(data.shape[1])]
|
|
884
|
+
config["_x_col"] = st.selectbox(
|
|
885
|
+
"X column index",
|
|
886
|
+
opts,
|
|
887
|
+
index=0,
|
|
888
|
+
key=f"{prefix}_xcol",
|
|
889
|
+
)
|
|
890
|
+
y_opts = [c for c in opts if c != config["_x_col"]]
|
|
891
|
+
config["_y_cols"] = st.multiselect(
|
|
892
|
+
"Y column index(es)",
|
|
893
|
+
y_opts,
|
|
894
|
+
default=[y_opts[0]] if y_opts else [],
|
|
895
|
+
key=f"{prefix}_ycols",
|
|
896
|
+
)
|
|
897
|
+
if not config["_y_cols"]:
|
|
898
|
+
st.info("Select at least one Y column.")
|
|
899
|
+
return {}
|
|
900
|
+
else:
|
|
901
|
+
st.warning("Line plot requires a 1D or 2D array.")
|
|
902
|
+
return {}
|
|
903
|
+
else:
|
|
904
|
+
st.warning("Unsupported data type for line plot.")
|
|
905
|
+
return {}
|
|
906
|
+
|
|
907
|
+
st.markdown("**Style**")
|
|
908
|
+
col1, col2 = st.columns(2)
|
|
909
|
+
with col1:
|
|
910
|
+
linestyle = st.selectbox(
|
|
911
|
+
"Line style",
|
|
912
|
+
["-", "--", "-.", ":"],
|
|
913
|
+
key=f"{prefix}_ls",
|
|
914
|
+
)
|
|
915
|
+
config["linestyles"] = [linestyle]
|
|
916
|
+
with col2:
|
|
917
|
+
marker = st.selectbox(
|
|
918
|
+
"Marker",
|
|
919
|
+
["None", "o", "s", "^", "D", "v", "*"],
|
|
920
|
+
key=f"{prefix}_marker",
|
|
921
|
+
)
|
|
922
|
+
config["markers"] = [None if marker == "None" else marker]
|
|
923
|
+
|
|
924
|
+
# ── Custom legend labels ──────────────────────────────────────────────────
|
|
925
|
+
y_cols_lp = config.get("_y_cols") or []
|
|
926
|
+
config.update(_custom_labels_widget(prefix, [str(c) for c in y_cols_lp]))
|
|
927
|
+
|
|
928
|
+
# ── Secondary Y axis (DataFrame only) ────────────────────────────────────
|
|
929
|
+
if isinstance(data, pd.DataFrame) and y_cols_lp:
|
|
930
|
+
with st.expander("Secondary Y axis (optional)"):
|
|
931
|
+
use_sec = st.checkbox(
|
|
932
|
+
"Enable secondary Y axis",
|
|
933
|
+
key=f"{prefix}_use_sec_y",
|
|
934
|
+
)
|
|
935
|
+
if use_sec:
|
|
936
|
+
x_col_lp = config.get("_x_col")
|
|
937
|
+
sec_opts = [c for c in numeric if c != x_col_lp and c not in y_cols_lp]
|
|
938
|
+
if sec_opts:
|
|
939
|
+
config["_secondary_y_cols"] = st.multiselect(
|
|
940
|
+
"Secondary Y column(s)",
|
|
941
|
+
sec_opts,
|
|
942
|
+
key=f"{prefix}_sec_ycols",
|
|
943
|
+
)
|
|
944
|
+
c1, c2 = st.columns(2)
|
|
945
|
+
with c1:
|
|
946
|
+
config["_secondary_y_label"] = st.text_input(
|
|
947
|
+
"Secondary Y label",
|
|
948
|
+
key=f"{prefix}_sec_ylabel",
|
|
949
|
+
)
|
|
950
|
+
with c2:
|
|
951
|
+
config["_secondary_y_scale"] = st.selectbox(
|
|
952
|
+
"Secondary Y scale",
|
|
953
|
+
["linear", "log"],
|
|
954
|
+
key=f"{prefix}_sec_yscale",
|
|
955
|
+
)
|
|
956
|
+
else:
|
|
957
|
+
st.info(
|
|
958
|
+
"All available numeric columns are already assigned to the primary Y axis."
|
|
959
|
+
)
|
|
960
|
+
|
|
961
|
+
st.markdown("**Figure & labels**")
|
|
962
|
+
config["figsize"] = _figsize_widget(prefix)
|
|
963
|
+
x_lbl = str(config.get("_x_col", "")) or ""
|
|
964
|
+
y_cols = config.get("_y_cols") or []
|
|
965
|
+
y_lbl = str(y_cols[0]) if y_cols else ""
|
|
966
|
+
config.update(_label_widgets(prefix, x_default=x_lbl, y_default=y_lbl))
|
|
967
|
+
with st.expander("Appearance"):
|
|
968
|
+
config.update(_grid_widgets(prefix))
|
|
969
|
+
config.update(_appearance_widgets(prefix, include_linewidth=True))
|
|
970
|
+
config.update(_palette_widget(prefix))
|
|
971
|
+
config.update(_legend_style_widgets(prefix))
|
|
972
|
+
config.update(_axis_limits_widgets(prefix))
|
|
973
|
+
config.update(_x_axis_widgets(prefix))
|
|
974
|
+
config.update(_y_axis_widgets(prefix))
|
|
975
|
+
config.update(_caption_widget(prefix))
|
|
976
|
+
return config
|
|
977
|
+
|
|
978
|
+
|
|
979
|
+
def _widgets_scatter_plot(data: Any, prefix: str) -> Dict:
|
|
980
|
+
config: Dict = {}
|
|
981
|
+
numeric = _numeric_cols(data)
|
|
982
|
+
|
|
983
|
+
if isinstance(data, pd.DataFrame):
|
|
984
|
+
if len(numeric) < 2:
|
|
985
|
+
st.warning("Scatter plot requires at least 2 numeric columns.")
|
|
986
|
+
return {}
|
|
987
|
+
col1, col2 = st.columns(2)
|
|
988
|
+
with col1:
|
|
989
|
+
config["_x_col"] = st.selectbox(
|
|
990
|
+
"X column",
|
|
991
|
+
numeric,
|
|
992
|
+
index=0,
|
|
993
|
+
key=f"{prefix}_xcol",
|
|
994
|
+
)
|
|
995
|
+
with col2:
|
|
996
|
+
y_opts = [c for c in numeric if c != config["_x_col"]]
|
|
997
|
+
config["_y_col"] = st.selectbox(
|
|
998
|
+
"Y column",
|
|
999
|
+
y_opts if y_opts else numeric,
|
|
1000
|
+
index=0,
|
|
1001
|
+
key=f"{prefix}_ycol",
|
|
1002
|
+
)
|
|
1003
|
+
elif isinstance(data, np.ndarray) and data.ndim == 2 and data.shape[1] >= 2:
|
|
1004
|
+
opts = [str(i) for i in range(data.shape[1])]
|
|
1005
|
+
col1, col2 = st.columns(2)
|
|
1006
|
+
with col1:
|
|
1007
|
+
config["_x_col"] = st.selectbox(
|
|
1008
|
+
"X column index",
|
|
1009
|
+
opts,
|
|
1010
|
+
index=0,
|
|
1011
|
+
key=f"{prefix}_xcol",
|
|
1012
|
+
)
|
|
1013
|
+
with col2:
|
|
1014
|
+
y_opts = [c for c in opts if c != config["_x_col"]]
|
|
1015
|
+
config["_y_col"] = st.selectbox(
|
|
1016
|
+
"Y column index",
|
|
1017
|
+
y_opts if y_opts else opts,
|
|
1018
|
+
index=0,
|
|
1019
|
+
key=f"{prefix}_ycol",
|
|
1020
|
+
)
|
|
1021
|
+
else:
|
|
1022
|
+
st.warning("Scatter plot requires a DataFrame or 2D array with >= 2 columns.")
|
|
1023
|
+
return {}
|
|
1024
|
+
|
|
1025
|
+
st.markdown("**Style**")
|
|
1026
|
+
col1, col2, col3 = st.columns(3)
|
|
1027
|
+
with col1:
|
|
1028
|
+
config["color"] = _color_widget(prefix)
|
|
1029
|
+
with col2:
|
|
1030
|
+
config["size"] = st.number_input(
|
|
1031
|
+
"Point size",
|
|
1032
|
+
min_value=1,
|
|
1033
|
+
max_value=500,
|
|
1034
|
+
value=50,
|
|
1035
|
+
key=f"{prefix}_size",
|
|
1036
|
+
)
|
|
1037
|
+
with col3:
|
|
1038
|
+
config["alpha"] = st.slider(
|
|
1039
|
+
"Opacity",
|
|
1040
|
+
0.1,
|
|
1041
|
+
1.0,
|
|
1042
|
+
0.7,
|
|
1043
|
+
0.05,
|
|
1044
|
+
key=f"{prefix}_alpha",
|
|
1045
|
+
)
|
|
1046
|
+
|
|
1047
|
+
st.markdown("**Figure & labels**")
|
|
1048
|
+
config["figsize"] = _figsize_widget(prefix)
|
|
1049
|
+
x_lbl = str(config.get("_x_col", "")) or ""
|
|
1050
|
+
y_lbl = str(config.get("_y_col", "")) or ""
|
|
1051
|
+
config.update(_label_widgets(prefix, x_default=x_lbl, y_default=y_lbl))
|
|
1052
|
+
with st.expander("Appearance"):
|
|
1053
|
+
config.update(_grid_widgets(prefix))
|
|
1054
|
+
config.update(_appearance_widgets(prefix))
|
|
1055
|
+
config.update(_legend_style_widgets(prefix))
|
|
1056
|
+
config.update(_axis_limits_widgets(prefix))
|
|
1057
|
+
config.update(_x_axis_widgets(prefix))
|
|
1058
|
+
config.update(_y_axis_widgets(prefix))
|
|
1059
|
+
config.update(_caption_widget(prefix))
|
|
1060
|
+
return config
|
|
1061
|
+
|
|
1062
|
+
|
|
1063
|
+
def _widgets_heatmap(data: Any, prefix: str) -> Dict:
|
|
1064
|
+
config: Dict = {}
|
|
1065
|
+
|
|
1066
|
+
if _is_2d_array(data):
|
|
1067
|
+
config["_use_numeric_df"] = False
|
|
1068
|
+
elif isinstance(data, pd.DataFrame):
|
|
1069
|
+
numeric = _numeric_cols(data)
|
|
1070
|
+
if len(numeric) < 2:
|
|
1071
|
+
st.warning("Heatmap requires a 2D array or DataFrame with >= 2 numeric columns.")
|
|
1072
|
+
return {}
|
|
1073
|
+
st.info(f"Heatmap will use all {len(numeric)} numeric columns as a matrix.")
|
|
1074
|
+
config["_use_numeric_df"] = True
|
|
1075
|
+
else:
|
|
1076
|
+
st.warning("Heatmap requires a 2D NumPy array or DataFrame.")
|
|
1077
|
+
return {}
|
|
1078
|
+
|
|
1079
|
+
col1, col2, col3 = st.columns(3)
|
|
1080
|
+
with col1:
|
|
1081
|
+
config["cmap"] = st.selectbox("Colormap", _CMAPS, key=f"{prefix}_cmap")
|
|
1082
|
+
config["colorbar"] = st.checkbox(
|
|
1083
|
+
"Show colorbar",
|
|
1084
|
+
value=True,
|
|
1085
|
+
key=f"{prefix}_cbar",
|
|
1086
|
+
)
|
|
1087
|
+
config["cmap_reversed"] = st.checkbox(
|
|
1088
|
+
"Reverse colormap",
|
|
1089
|
+
key=f"{prefix}_cmap_rev",
|
|
1090
|
+
)
|
|
1091
|
+
with col2:
|
|
1092
|
+
use_clim = st.checkbox("Set color limits", key=f"{prefix}_use_clim")
|
|
1093
|
+
if use_clim:
|
|
1094
|
+
config["vmin"] = st.number_input("vmin", value=0.0, key=f"{prefix}_vmin")
|
|
1095
|
+
config["vmax"] = st.number_input("vmax", value=1.0, key=f"{prefix}_vmax")
|
|
1096
|
+
with col3:
|
|
1097
|
+
use_n = st.checkbox("Discretize colormap", key=f"{prefix}_use_ncol")
|
|
1098
|
+
if use_n:
|
|
1099
|
+
config["cmap_n_colors"] = st.number_input(
|
|
1100
|
+
"N colors",
|
|
1101
|
+
min_value=2,
|
|
1102
|
+
max_value=256,
|
|
1103
|
+
value=10,
|
|
1104
|
+
key=f"{prefix}_ncol",
|
|
1105
|
+
)
|
|
1106
|
+
|
|
1107
|
+
st.markdown("**Figure & labels**")
|
|
1108
|
+
config["figsize"] = _figsize_widget(prefix)
|
|
1109
|
+
config.update(_label_widgets(prefix))
|
|
1110
|
+
with st.expander("Appearance"):
|
|
1111
|
+
config.update(_appearance_widgets(prefix))
|
|
1112
|
+
config.update(_grid_widgets(prefix))
|
|
1113
|
+
config.update(_x_axis_widgets(prefix))
|
|
1114
|
+
config.update(_y_axis_widgets(prefix))
|
|
1115
|
+
config.update(_z_axis_widgets(prefix))
|
|
1116
|
+
config.update(_axis_limits_widgets(prefix))
|
|
1117
|
+
config.update(_caption_widget(prefix))
|
|
1118
|
+
return config
|
|
1119
|
+
|
|
1120
|
+
|
|
1121
|
+
def _widgets_contour_plot(data: Any, prefix: str) -> Dict:
|
|
1122
|
+
config: Dict = {}
|
|
1123
|
+
|
|
1124
|
+
if _is_2d_array(data):
|
|
1125
|
+
config["_mode"] = "array"
|
|
1126
|
+
st.info("X and Y meshgrid coordinates are auto-generated from the Z array shape.")
|
|
1127
|
+
elif isinstance(data, pd.DataFrame):
|
|
1128
|
+
numeric = _numeric_cols(data)
|
|
1129
|
+
if len(numeric) < 3:
|
|
1130
|
+
st.warning(
|
|
1131
|
+
"Contour plot from a DataFrame requires at least 3 numeric columns (X, Y, Z)."
|
|
1132
|
+
)
|
|
1133
|
+
return {}
|
|
1134
|
+
config["_mode"] = "dataframe"
|
|
1135
|
+
c1, c2, c3 = st.columns(3)
|
|
1136
|
+
with c1:
|
|
1137
|
+
config["_x_col"] = st.selectbox("X column", numeric, index=0, key=f"{prefix}_xcol")
|
|
1138
|
+
with c2:
|
|
1139
|
+
y_opts = [c for c in numeric if c != config["_x_col"]]
|
|
1140
|
+
config["_y_col"] = st.selectbox(
|
|
1141
|
+
"Y column",
|
|
1142
|
+
y_opts or numeric,
|
|
1143
|
+
index=0,
|
|
1144
|
+
key=f"{prefix}_ycol",
|
|
1145
|
+
)
|
|
1146
|
+
with c3:
|
|
1147
|
+
z_opts = [c for c in numeric if c not in (config["_x_col"], config.get("_y_col", ""))]
|
|
1148
|
+
config["_z_col"] = st.selectbox(
|
|
1149
|
+
"Z column",
|
|
1150
|
+
z_opts or numeric,
|
|
1151
|
+
index=0,
|
|
1152
|
+
key=f"{prefix}_zcol",
|
|
1153
|
+
)
|
|
1154
|
+
c1, c2 = st.columns(2)
|
|
1155
|
+
with c1:
|
|
1156
|
+
config["_grid_n"] = st.number_input(
|
|
1157
|
+
"Grid resolution (N×N)",
|
|
1158
|
+
min_value=20,
|
|
1159
|
+
max_value=500,
|
|
1160
|
+
value=100,
|
|
1161
|
+
key=f"{prefix}_gridn",
|
|
1162
|
+
)
|
|
1163
|
+
with c2:
|
|
1164
|
+
config["_interp_method"] = st.selectbox(
|
|
1165
|
+
"Interpolation method",
|
|
1166
|
+
["cubic", "linear", "nearest"],
|
|
1167
|
+
key=f"{prefix}_interp",
|
|
1168
|
+
help=(
|
|
1169
|
+
"'cubic' is smoothest; 'linear' is exact on grid points; "
|
|
1170
|
+
"'nearest' preserves raw values."
|
|
1171
|
+
),
|
|
1172
|
+
)
|
|
1173
|
+
st.caption(
|
|
1174
|
+
"Structured-grid CSVs are reshaped directly; "
|
|
1175
|
+
"scattered XYZ data is interpolated via scipy griddata."
|
|
1176
|
+
)
|
|
1177
|
+
else:
|
|
1178
|
+
st.warning("Contour plot requires a 2D NumPy array or a DataFrame with X, Y, Z columns.")
|
|
1179
|
+
return {}
|
|
1180
|
+
|
|
1181
|
+
col1, col2, col3, col4 = st.columns(4)
|
|
1182
|
+
with col1:
|
|
1183
|
+
config["levels"] = st.number_input(
|
|
1184
|
+
"Contour levels",
|
|
1185
|
+
min_value=2,
|
|
1186
|
+
max_value=50,
|
|
1187
|
+
value=10,
|
|
1188
|
+
key=f"{prefix}_levels",
|
|
1189
|
+
)
|
|
1190
|
+
with col2:
|
|
1191
|
+
config["cmap"] = st.selectbox("Colormap", _CMAPS, key=f"{prefix}_cmap")
|
|
1192
|
+
config["cmap_reversed"] = st.checkbox(
|
|
1193
|
+
"Reverse colormap",
|
|
1194
|
+
key=f"{prefix}_cmap_rev",
|
|
1195
|
+
)
|
|
1196
|
+
with col3:
|
|
1197
|
+
use_n = st.checkbox("Discretize colormap", key=f"{prefix}_use_ncol")
|
|
1198
|
+
if use_n:
|
|
1199
|
+
config["cmap_n_colors"] = st.number_input(
|
|
1200
|
+
"N colors",
|
|
1201
|
+
min_value=2,
|
|
1202
|
+
max_value=256,
|
|
1203
|
+
value=10,
|
|
1204
|
+
key=f"{prefix}_ncol",
|
|
1205
|
+
)
|
|
1206
|
+
with col4:
|
|
1207
|
+
config["filled"] = st.checkbox(
|
|
1208
|
+
"Filled contours",
|
|
1209
|
+
value=True,
|
|
1210
|
+
key=f"{prefix}_filled",
|
|
1211
|
+
)
|
|
1212
|
+
config["colorbar"] = st.checkbox(
|
|
1213
|
+
"Show colorbar",
|
|
1214
|
+
value=True,
|
|
1215
|
+
key=f"{prefix}_cbar",
|
|
1216
|
+
)
|
|
1217
|
+
|
|
1218
|
+
c1, c2 = st.columns(2)
|
|
1219
|
+
with c1:
|
|
1220
|
+
if not config.get("filled", True):
|
|
1221
|
+
config["linewidths"] = st.slider(
|
|
1222
|
+
"Line width",
|
|
1223
|
+
0.5,
|
|
1224
|
+
5.0,
|
|
1225
|
+
1.5,
|
|
1226
|
+
0.25,
|
|
1227
|
+
key=f"{prefix}_lw",
|
|
1228
|
+
)
|
|
1229
|
+
with c2:
|
|
1230
|
+
use_clim = st.checkbox("Set color limits", key=f"{prefix}_use_clim")
|
|
1231
|
+
if use_clim:
|
|
1232
|
+
cc1, cc2 = st.columns(2)
|
|
1233
|
+
with cc1:
|
|
1234
|
+
config["vmin"] = st.number_input("vmin", value=0.0, key=f"{prefix}_vmin")
|
|
1235
|
+
with cc2:
|
|
1236
|
+
config["vmax"] = st.number_input("vmax", value=1.0, key=f"{prefix}_vmax")
|
|
1237
|
+
|
|
1238
|
+
st.markdown("**Figure & labels**")
|
|
1239
|
+
config["figsize"] = _figsize_widget(prefix)
|
|
1240
|
+
config.update(_label_widgets(prefix))
|
|
1241
|
+
with st.expander("Appearance"):
|
|
1242
|
+
config.update(_appearance_widgets(prefix))
|
|
1243
|
+
config.update(_grid_widgets(prefix))
|
|
1244
|
+
config.update(_x_axis_widgets(prefix))
|
|
1245
|
+
config.update(_y_axis_widgets(prefix))
|
|
1246
|
+
config.update(_z_axis_widgets(prefix))
|
|
1247
|
+
config.update(_axis_limits_widgets(prefix))
|
|
1248
|
+
config.update(_caption_widget(prefix))
|
|
1249
|
+
return config
|
|
1250
|
+
|
|
1251
|
+
|
|
1252
|
+
def _widgets_distribution_plot(data: Any, prefix: str) -> Dict:
|
|
1253
|
+
config: Dict = {}
|
|
1254
|
+
numeric = _numeric_cols(data)
|
|
1255
|
+
|
|
1256
|
+
if isinstance(data, pd.DataFrame):
|
|
1257
|
+
if not numeric:
|
|
1258
|
+
st.warning("No numeric columns found.")
|
|
1259
|
+
return {}
|
|
1260
|
+
config["_data_col"] = st.selectbox(
|
|
1261
|
+
"Data column",
|
|
1262
|
+
numeric,
|
|
1263
|
+
key=f"{prefix}_dcol",
|
|
1264
|
+
)
|
|
1265
|
+
elif isinstance(data, np.ndarray) and np.issubdtype(data.dtype, np.number):
|
|
1266
|
+
config["_data_col"] = None
|
|
1267
|
+
else:
|
|
1268
|
+
st.warning("Distribution plot requires a numeric array or DataFrame.")
|
|
1269
|
+
return {}
|
|
1270
|
+
|
|
1271
|
+
col1, col2 = st.columns(2)
|
|
1272
|
+
with col1:
|
|
1273
|
+
config["kind"] = st.selectbox(
|
|
1274
|
+
"Plot kind",
|
|
1275
|
+
["hist", "kde", "ecdf"],
|
|
1276
|
+
key=f"{prefix}_kind",
|
|
1277
|
+
)
|
|
1278
|
+
with col2:
|
|
1279
|
+
config["kde"] = st.checkbox(
|
|
1280
|
+
"Overlay KDE (hist only)",
|
|
1281
|
+
value=True,
|
|
1282
|
+
key=f"{prefix}_kde",
|
|
1283
|
+
)
|
|
1284
|
+
with st.expander("Appearance"):
|
|
1285
|
+
config.update(_grid_widgets(prefix))
|
|
1286
|
+
config.update(_appearance_widgets(prefix))
|
|
1287
|
+
config.update(_axis_limits_widgets(prefix))
|
|
1288
|
+
return config
|
|
1289
|
+
|
|
1290
|
+
|
|
1291
|
+
def _widgets_box_plot(data: Any, prefix: str) -> Dict:
|
|
1292
|
+
config: Dict = {}
|
|
1293
|
+
all_cols = _all_cols(data)
|
|
1294
|
+
numeric = _numeric_cols(data)
|
|
1295
|
+
|
|
1296
|
+
if isinstance(data, pd.DataFrame):
|
|
1297
|
+
if not numeric:
|
|
1298
|
+
st.warning("No numeric columns found.")
|
|
1299
|
+
return {}
|
|
1300
|
+
categorical = [c for c in all_cols if c not in numeric]
|
|
1301
|
+
col1, col2, col3 = st.columns(3)
|
|
1302
|
+
with col1:
|
|
1303
|
+
x_choice = st.selectbox(
|
|
1304
|
+
"X (grouping) column",
|
|
1305
|
+
["— none —"] + categorical + numeric,
|
|
1306
|
+
key=f"{prefix}_xcol",
|
|
1307
|
+
)
|
|
1308
|
+
config["x"] = None if x_choice == "— none —" else x_choice
|
|
1309
|
+
with col2:
|
|
1310
|
+
config["y"] = st.selectbox(
|
|
1311
|
+
"Y (value) column",
|
|
1312
|
+
numeric,
|
|
1313
|
+
key=f"{prefix}_ycol",
|
|
1314
|
+
)
|
|
1315
|
+
with col3:
|
|
1316
|
+
hue_choice = st.selectbox(
|
|
1317
|
+
"Hue (optional)",
|
|
1318
|
+
["— none —"] + all_cols,
|
|
1319
|
+
key=f"{prefix}_hue",
|
|
1320
|
+
)
|
|
1321
|
+
config["hue"] = None if hue_choice == "— none —" else hue_choice
|
|
1322
|
+
elif isinstance(data, np.ndarray) and np.issubdtype(data.dtype, np.number):
|
|
1323
|
+
config["x"] = None
|
|
1324
|
+
config["y"] = None
|
|
1325
|
+
config["hue"] = None
|
|
1326
|
+
st.info("Box plot will use the numeric array as a single group.")
|
|
1327
|
+
else:
|
|
1328
|
+
st.warning("Box plot requires a numeric array or DataFrame.")
|
|
1329
|
+
return {}
|
|
1330
|
+
|
|
1331
|
+
config["kind"] = st.selectbox(
|
|
1332
|
+
"Plot kind",
|
|
1333
|
+
["box", "violin", "boxen"],
|
|
1334
|
+
key=f"{prefix}_kind",
|
|
1335
|
+
)
|
|
1336
|
+
with st.expander("Appearance"):
|
|
1337
|
+
config.update(_grid_widgets(prefix))
|
|
1338
|
+
config.update(_appearance_widgets(prefix))
|
|
1339
|
+
config.update(_axis_limits_widgets(prefix))
|
|
1340
|
+
return config
|
|
1341
|
+
|
|
1342
|
+
|
|
1343
|
+
def _widgets_regression_plot(data: Any, prefix: str) -> Dict:
|
|
1344
|
+
config: Dict = {}
|
|
1345
|
+
numeric = _numeric_cols(data)
|
|
1346
|
+
|
|
1347
|
+
if isinstance(data, pd.DataFrame):
|
|
1348
|
+
if len(numeric) < 2:
|
|
1349
|
+
st.warning("Regression plot requires at least 2 numeric columns.")
|
|
1350
|
+
return {}
|
|
1351
|
+
col1, col2 = st.columns(2)
|
|
1352
|
+
with col1:
|
|
1353
|
+
config["_x_col"] = st.selectbox(
|
|
1354
|
+
"X column",
|
|
1355
|
+
numeric,
|
|
1356
|
+
index=0,
|
|
1357
|
+
key=f"{prefix}_xcol",
|
|
1358
|
+
)
|
|
1359
|
+
with col2:
|
|
1360
|
+
y_opts = [c for c in numeric if c != config["_x_col"]]
|
|
1361
|
+
config["_y_col"] = st.selectbox(
|
|
1362
|
+
"Y column",
|
|
1363
|
+
y_opts if y_opts else numeric,
|
|
1364
|
+
index=0,
|
|
1365
|
+
key=f"{prefix}_ycol",
|
|
1366
|
+
)
|
|
1367
|
+
config["_use_df"] = True
|
|
1368
|
+
|
|
1369
|
+
elif isinstance(data, np.ndarray) and data.ndim == 2 and data.shape[1] >= 2:
|
|
1370
|
+
opts = [str(i) for i in range(data.shape[1])]
|
|
1371
|
+
col1, col2 = st.columns(2)
|
|
1372
|
+
with col1:
|
|
1373
|
+
config["_x_col"] = st.selectbox(
|
|
1374
|
+
"X column index",
|
|
1375
|
+
opts,
|
|
1376
|
+
index=0,
|
|
1377
|
+
key=f"{prefix}_xcol",
|
|
1378
|
+
)
|
|
1379
|
+
with col2:
|
|
1380
|
+
y_opts = [c for c in opts if c != config["_x_col"]]
|
|
1381
|
+
config["_y_col"] = st.selectbox(
|
|
1382
|
+
"Y column index",
|
|
1383
|
+
y_opts if y_opts else opts,
|
|
1384
|
+
index=0,
|
|
1385
|
+
key=f"{prefix}_ycol",
|
|
1386
|
+
)
|
|
1387
|
+
config["_use_df"] = False
|
|
1388
|
+
else:
|
|
1389
|
+
st.warning("Regression plot requires a DataFrame or 2D array with >= 2 columns.")
|
|
1390
|
+
return {}
|
|
1391
|
+
|
|
1392
|
+
col1, col2 = st.columns(2)
|
|
1393
|
+
with col1:
|
|
1394
|
+
config["order"] = st.number_input(
|
|
1395
|
+
"Polynomial order",
|
|
1396
|
+
min_value=1,
|
|
1397
|
+
max_value=5,
|
|
1398
|
+
value=1,
|
|
1399
|
+
key=f"{prefix}_order",
|
|
1400
|
+
)
|
|
1401
|
+
with col2:
|
|
1402
|
+
config["ci"] = st.slider(
|
|
1403
|
+
"Confidence interval (%)",
|
|
1404
|
+
0,
|
|
1405
|
+
99,
|
|
1406
|
+
95,
|
|
1407
|
+
key=f"{prefix}_ci",
|
|
1408
|
+
)
|
|
1409
|
+
x_lbl = str(config.get("_x_col", "")) or ""
|
|
1410
|
+
y_lbl = str(config.get("_y_col", "")) or ""
|
|
1411
|
+
with st.expander("Appearance"):
|
|
1412
|
+
config.update(_grid_widgets(prefix))
|
|
1413
|
+
config.update(_appearance_widgets(prefix))
|
|
1414
|
+
config.update(_axis_limits_widgets(prefix))
|
|
1415
|
+
# Regression has no separate label widgets — show them here
|
|
1416
|
+
col1, col2 = st.columns(2)
|
|
1417
|
+
with col1:
|
|
1418
|
+
xl = st.text_input(
|
|
1419
|
+
"X Label",
|
|
1420
|
+
placeholder=f"Default: {x_lbl}" if x_lbl else "",
|
|
1421
|
+
key=f"{prefix}_xlabel",
|
|
1422
|
+
)
|
|
1423
|
+
config["xlabel"] = xl or x_lbl or None
|
|
1424
|
+
with col2:
|
|
1425
|
+
yl = st.text_input(
|
|
1426
|
+
"Y Label",
|
|
1427
|
+
placeholder=f"Default: {y_lbl}" if y_lbl else "",
|
|
1428
|
+
key=f"{prefix}_ylabel",
|
|
1429
|
+
)
|
|
1430
|
+
config["ylabel"] = yl or y_lbl or None
|
|
1431
|
+
return config
|
|
1432
|
+
|
|
1433
|
+
|
|
1434
|
+
def _widgets_interactive_histogram(data: Any, prefix: str) -> Dict:
|
|
1435
|
+
config: Dict = {}
|
|
1436
|
+
numeric = _numeric_cols(data)
|
|
1437
|
+
|
|
1438
|
+
if isinstance(data, pd.DataFrame):
|
|
1439
|
+
if not numeric:
|
|
1440
|
+
st.warning("No numeric columns found.")
|
|
1441
|
+
return {}
|
|
1442
|
+
config["_data_col"] = st.selectbox(
|
|
1443
|
+
"Data column",
|
|
1444
|
+
numeric,
|
|
1445
|
+
key=f"{prefix}_dcol",
|
|
1446
|
+
)
|
|
1447
|
+
elif isinstance(data, np.ndarray) and np.issubdtype(data.dtype, np.number):
|
|
1448
|
+
config["_data_col"] = None
|
|
1449
|
+
else:
|
|
1450
|
+
st.warning("Interactive histogram requires a numeric array or DataFrame.")
|
|
1451
|
+
return {}
|
|
1452
|
+
|
|
1453
|
+
col1, col2, col3 = st.columns(3)
|
|
1454
|
+
with col1:
|
|
1455
|
+
config["bins"] = st.number_input(
|
|
1456
|
+
"Bins",
|
|
1457
|
+
min_value=2,
|
|
1458
|
+
max_value=500,
|
|
1459
|
+
value=30,
|
|
1460
|
+
key=f"{prefix}_bins",
|
|
1461
|
+
)
|
|
1462
|
+
with col2:
|
|
1463
|
+
config["title"] = st.text_input("Title", key=f"{prefix}_title") or None
|
|
1464
|
+
with col3:
|
|
1465
|
+
x_lbl = str(config.get("_data_col", "")) or ""
|
|
1466
|
+
xl = st.text_input(
|
|
1467
|
+
"X Label",
|
|
1468
|
+
placeholder=f"Default: {x_lbl}" if x_lbl else "",
|
|
1469
|
+
key=f"{prefix}_xlabel",
|
|
1470
|
+
)
|
|
1471
|
+
config["xlabel"] = xl or x_lbl or None
|
|
1472
|
+
with st.expander("Appearance"):
|
|
1473
|
+
config.update(_plotly_appearance_widgets(prefix))
|
|
1474
|
+
config.update(_axis_limits_widgets(prefix))
|
|
1475
|
+
return config
|
|
1476
|
+
|
|
1477
|
+
|
|
1478
|
+
def _widgets_interactive_scatter(data: Any, prefix: str) -> Dict:
|
|
1479
|
+
config: Dict = {}
|
|
1480
|
+
numeric = _numeric_cols(data)
|
|
1481
|
+
|
|
1482
|
+
if isinstance(data, pd.DataFrame):
|
|
1483
|
+
if len(numeric) < 2:
|
|
1484
|
+
st.warning("Interactive scatter requires at least 2 numeric columns.")
|
|
1485
|
+
return {}
|
|
1486
|
+
col1, col2 = st.columns(2)
|
|
1487
|
+
with col1:
|
|
1488
|
+
config["_x_col"] = st.selectbox(
|
|
1489
|
+
"X column",
|
|
1490
|
+
numeric,
|
|
1491
|
+
index=0,
|
|
1492
|
+
key=f"{prefix}_xcol",
|
|
1493
|
+
)
|
|
1494
|
+
with col2:
|
|
1495
|
+
y_opts = [c for c in numeric if c != config["_x_col"]]
|
|
1496
|
+
config["_y_col"] = st.selectbox(
|
|
1497
|
+
"Y column",
|
|
1498
|
+
y_opts if y_opts else numeric,
|
|
1499
|
+
index=0,
|
|
1500
|
+
key=f"{prefix}_ycol",
|
|
1501
|
+
)
|
|
1502
|
+
elif isinstance(data, np.ndarray) and data.ndim == 2 and data.shape[1] >= 2:
|
|
1503
|
+
opts = [str(i) for i in range(data.shape[1])]
|
|
1504
|
+
col1, col2 = st.columns(2)
|
|
1505
|
+
with col1:
|
|
1506
|
+
config["_x_col"] = st.selectbox(
|
|
1507
|
+
"X column index",
|
|
1508
|
+
opts,
|
|
1509
|
+
index=0,
|
|
1510
|
+
key=f"{prefix}_xcol",
|
|
1511
|
+
)
|
|
1512
|
+
with col2:
|
|
1513
|
+
y_opts = [c for c in opts if c != config["_x_col"]]
|
|
1514
|
+
config["_y_col"] = st.selectbox(
|
|
1515
|
+
"Y column index",
|
|
1516
|
+
y_opts if y_opts else opts,
|
|
1517
|
+
index=0,
|
|
1518
|
+
key=f"{prefix}_ycol",
|
|
1519
|
+
)
|
|
1520
|
+
else:
|
|
1521
|
+
st.warning("Interactive scatter requires a DataFrame or 2D array with >= 2 columns.")
|
|
1522
|
+
return {}
|
|
1523
|
+
|
|
1524
|
+
x_lbl = str(config.get("_x_col", "")) or ""
|
|
1525
|
+
y_lbl = str(config.get("_y_col", "")) or ""
|
|
1526
|
+
col1, col2, col3 = st.columns(3)
|
|
1527
|
+
with col1:
|
|
1528
|
+
config["title"] = st.text_input("Title", key=f"{prefix}_title") or None
|
|
1529
|
+
with col2:
|
|
1530
|
+
xl = st.text_input(
|
|
1531
|
+
"X Label",
|
|
1532
|
+
placeholder=f"Default: {x_lbl}" if x_lbl else "",
|
|
1533
|
+
key=f"{prefix}_xlabel",
|
|
1534
|
+
)
|
|
1535
|
+
config["xlabel"] = xl or x_lbl or None
|
|
1536
|
+
with col3:
|
|
1537
|
+
yl = st.text_input(
|
|
1538
|
+
"Y Label",
|
|
1539
|
+
placeholder=f"Default: {y_lbl}" if y_lbl else "",
|
|
1540
|
+
key=f"{prefix}_ylabel",
|
|
1541
|
+
)
|
|
1542
|
+
config["ylabel"] = yl or y_lbl or None
|
|
1543
|
+
with st.expander("Appearance"):
|
|
1544
|
+
config.update(_plotly_appearance_widgets(prefix))
|
|
1545
|
+
config.update(_axis_limits_widgets(prefix))
|
|
1546
|
+
config.update(_y_axis_widgets(prefix))
|
|
1547
|
+
return config
|
|
1548
|
+
|
|
1549
|
+
|
|
1550
|
+
def _widgets_interactive_line(data: Any, prefix: str) -> Dict:
|
|
1551
|
+
config: Dict = {}
|
|
1552
|
+
numeric = _numeric_cols(data)
|
|
1553
|
+
|
|
1554
|
+
if isinstance(data, pd.DataFrame):
|
|
1555
|
+
if len(numeric) < 2:
|
|
1556
|
+
st.warning("Interactive line requires at least 2 numeric columns.")
|
|
1557
|
+
return {}
|
|
1558
|
+
config["_x_col"] = st.selectbox(
|
|
1559
|
+
"X column",
|
|
1560
|
+
numeric,
|
|
1561
|
+
index=0,
|
|
1562
|
+
key=f"{prefix}_xcol",
|
|
1563
|
+
)
|
|
1564
|
+
y_opts = [c for c in numeric if c != config["_x_col"]]
|
|
1565
|
+
config["_y_cols"] = st.multiselect(
|
|
1566
|
+
"Y column(s)",
|
|
1567
|
+
y_opts,
|
|
1568
|
+
default=[y_opts[0]] if y_opts else [],
|
|
1569
|
+
key=f"{prefix}_ycols",
|
|
1570
|
+
)
|
|
1571
|
+
if not config["_y_cols"]:
|
|
1572
|
+
st.info("Select at least one Y column.")
|
|
1573
|
+
return {}
|
|
1574
|
+
|
|
1575
|
+
elif isinstance(data, np.ndarray):
|
|
1576
|
+
if data.ndim == 1:
|
|
1577
|
+
st.info("1D array: array index as X axis.")
|
|
1578
|
+
config["_x_col"] = None
|
|
1579
|
+
config["_y_cols"] = None
|
|
1580
|
+
elif data.ndim == 2:
|
|
1581
|
+
opts = [str(i) for i in range(data.shape[1])]
|
|
1582
|
+
config["_x_col"] = st.selectbox(
|
|
1583
|
+
"X column index",
|
|
1584
|
+
opts,
|
|
1585
|
+
index=0,
|
|
1586
|
+
key=f"{prefix}_xcol",
|
|
1587
|
+
)
|
|
1588
|
+
y_opts = [c for c in opts if c != config["_x_col"]]
|
|
1589
|
+
config["_y_cols"] = st.multiselect(
|
|
1590
|
+
"Y column index(es)",
|
|
1591
|
+
y_opts,
|
|
1592
|
+
default=[y_opts[0]] if y_opts else [],
|
|
1593
|
+
key=f"{prefix}_ycols",
|
|
1594
|
+
)
|
|
1595
|
+
if not config["_y_cols"]:
|
|
1596
|
+
st.info("Select at least one Y column.")
|
|
1597
|
+
return {}
|
|
1598
|
+
else:
|
|
1599
|
+
st.warning("Interactive line requires a 1D or 2D array.")
|
|
1600
|
+
return {}
|
|
1601
|
+
else:
|
|
1602
|
+
st.warning("Unsupported data type for interactive line plot.")
|
|
1603
|
+
return {}
|
|
1604
|
+
|
|
1605
|
+
x_lbl = str(config.get("_x_col", "")) or ""
|
|
1606
|
+
y_cols = config.get("_y_cols") or []
|
|
1607
|
+
y_lbl = str(y_cols[0]) if y_cols else ""
|
|
1608
|
+
col1, col2, col3 = st.columns(3)
|
|
1609
|
+
with col1:
|
|
1610
|
+
config["title"] = st.text_input("Title", key=f"{prefix}_title") or None
|
|
1611
|
+
with col2:
|
|
1612
|
+
xl = st.text_input(
|
|
1613
|
+
"X Label",
|
|
1614
|
+
placeholder=f"Default: {x_lbl}" if x_lbl else "",
|
|
1615
|
+
key=f"{prefix}_xlabel",
|
|
1616
|
+
)
|
|
1617
|
+
config["xlabel"] = xl or x_lbl or None
|
|
1618
|
+
with col3:
|
|
1619
|
+
yl = st.text_input(
|
|
1620
|
+
"Y Label",
|
|
1621
|
+
placeholder=f"Default: {y_lbl}" if y_lbl else "",
|
|
1622
|
+
key=f"{prefix}_ylabel",
|
|
1623
|
+
)
|
|
1624
|
+
config["ylabel"] = yl or y_lbl or None
|
|
1625
|
+
with st.expander("Appearance"):
|
|
1626
|
+
config.update(_plotly_appearance_widgets(prefix))
|
|
1627
|
+
config.update(_axis_limits_widgets(prefix))
|
|
1628
|
+
config.update(_y_axis_widgets(prefix))
|
|
1629
|
+
return config
|
|
1630
|
+
|
|
1631
|
+
|
|
1632
|
+
def _widgets_interactive_heatmap(data: Any, prefix: str) -> Dict:
|
|
1633
|
+
config: Dict = {}
|
|
1634
|
+
|
|
1635
|
+
if _is_2d_array(data):
|
|
1636
|
+
config["_use_numeric_df"] = False
|
|
1637
|
+
elif isinstance(data, pd.DataFrame):
|
|
1638
|
+
numeric = _numeric_cols(data)
|
|
1639
|
+
if len(numeric) < 2:
|
|
1640
|
+
st.warning(
|
|
1641
|
+
"Interactive heatmap requires a 2D array or DataFrame with >= 2 numeric columns."
|
|
1642
|
+
)
|
|
1643
|
+
return {}
|
|
1644
|
+
st.info(f"Heatmap will use all {len(numeric)} numeric columns.")
|
|
1645
|
+
config["_use_numeric_df"] = True
|
|
1646
|
+
else:
|
|
1647
|
+
st.warning("Interactive heatmap requires a 2D NumPy array or DataFrame.")
|
|
1648
|
+
return {}
|
|
1649
|
+
|
|
1650
|
+
col1, col2 = st.columns(2)
|
|
1651
|
+
with col1:
|
|
1652
|
+
config["colorscale"] = st.selectbox(
|
|
1653
|
+
"Color scale",
|
|
1654
|
+
_COLORSCALES,
|
|
1655
|
+
key=f"{prefix}_cs",
|
|
1656
|
+
)
|
|
1657
|
+
with col2:
|
|
1658
|
+
config["title"] = st.text_input("Title", key=f"{prefix}_title") or None
|
|
1659
|
+
with st.expander("Appearance"):
|
|
1660
|
+
config.update(_plotly_appearance_widgets(prefix))
|
|
1661
|
+
return config
|
|
1662
|
+
|
|
1663
|
+
|
|
1664
|
+
def _widgets_interactive_3d_surface(data: Any, prefix: str) -> Dict:
|
|
1665
|
+
config: Dict = {}
|
|
1666
|
+
|
|
1667
|
+
if not _is_2d_array(data):
|
|
1668
|
+
st.warning("3D surface plot requires a 2D NumPy array (Z values). Load a 2D .npy file.")
|
|
1669
|
+
return {}
|
|
1670
|
+
|
|
1671
|
+
st.info("X and Y meshgrid coordinates are auto-generated from the Z array shape.")
|
|
1672
|
+
col1, col2 = st.columns(2)
|
|
1673
|
+
with col1:
|
|
1674
|
+
config["colorscale"] = st.selectbox(
|
|
1675
|
+
"Color scale",
|
|
1676
|
+
_COLORSCALES,
|
|
1677
|
+
key=f"{prefix}_cs",
|
|
1678
|
+
)
|
|
1679
|
+
with col2:
|
|
1680
|
+
config["title"] = st.text_input("Title", key=f"{prefix}_title") or None
|
|
1681
|
+
with st.expander("Appearance"):
|
|
1682
|
+
config.update(_plotly_appearance_widgets(prefix))
|
|
1683
|
+
return config
|
|
1684
|
+
|
|
1685
|
+
|
|
1686
|
+
# ── M14 widget functions ─────────────────────────────────────────────────────
|
|
1687
|
+
|
|
1688
|
+
|
|
1689
|
+
def _widgets_bar_chart(data: Any, prefix: str) -> Dict:
|
|
1690
|
+
config: Dict = {}
|
|
1691
|
+
numeric = _numeric_cols(data)
|
|
1692
|
+
|
|
1693
|
+
if isinstance(data, pd.DataFrame):
|
|
1694
|
+
all_cols = _all_cols(data)
|
|
1695
|
+
col1, col2 = st.columns(2)
|
|
1696
|
+
with col1:
|
|
1697
|
+
config["_cat_col"] = st.selectbox(
|
|
1698
|
+
"Category column",
|
|
1699
|
+
all_cols,
|
|
1700
|
+
index=0,
|
|
1701
|
+
key=f"{prefix}_catcol",
|
|
1702
|
+
)
|
|
1703
|
+
with col2:
|
|
1704
|
+
config["_val_col"] = st.selectbox(
|
|
1705
|
+
"Value column",
|
|
1706
|
+
numeric if numeric else all_cols,
|
|
1707
|
+
index=min(1, len(all_cols) - 1),
|
|
1708
|
+
key=f"{prefix}_valcol",
|
|
1709
|
+
)
|
|
1710
|
+
elif isinstance(data, np.ndarray) and data.ndim == 2:
|
|
1711
|
+
opts = [str(i) for i in range(data.shape[1])]
|
|
1712
|
+
col1, col2 = st.columns(2)
|
|
1713
|
+
with col1:
|
|
1714
|
+
config["_cat_col"] = st.selectbox(
|
|
1715
|
+
"Category column index",
|
|
1716
|
+
opts,
|
|
1717
|
+
index=0,
|
|
1718
|
+
key=f"{prefix}_catcol",
|
|
1719
|
+
)
|
|
1720
|
+
with col2:
|
|
1721
|
+
config["_val_col"] = st.selectbox(
|
|
1722
|
+
"Value column index",
|
|
1723
|
+
opts,
|
|
1724
|
+
index=min(1, data.shape[1] - 1),
|
|
1725
|
+
key=f"{prefix}_valcol",
|
|
1726
|
+
)
|
|
1727
|
+
else:
|
|
1728
|
+
st.info(
|
|
1729
|
+
"Bar chart will use the loaded array as values. "
|
|
1730
|
+
"Integer index will be used as categories."
|
|
1731
|
+
)
|
|
1732
|
+
config["_cat_col"] = None
|
|
1733
|
+
config["_val_col"] = None
|
|
1734
|
+
|
|
1735
|
+
st.markdown("**Chart type & grouping**")
|
|
1736
|
+
col1, col2 = st.columns(2)
|
|
1737
|
+
with col1:
|
|
1738
|
+
config["kind"] = st.selectbox(
|
|
1739
|
+
"Chart kind",
|
|
1740
|
+
["simple", "grouped", "stacked"],
|
|
1741
|
+
key=f"{prefix}_kind",
|
|
1742
|
+
)
|
|
1743
|
+
with col2:
|
|
1744
|
+
if config["kind"] in ("grouped", "stacked"):
|
|
1745
|
+
if isinstance(data, pd.DataFrame) and numeric:
|
|
1746
|
+
hue_opts = [
|
|
1747
|
+
c
|
|
1748
|
+
for c in numeric
|
|
1749
|
+
if c != config.get("_val_col") and c != config.get("_cat_col")
|
|
1750
|
+
]
|
|
1751
|
+
config["_hue_cols"] = st.multiselect(
|
|
1752
|
+
"Group columns",
|
|
1753
|
+
hue_opts,
|
|
1754
|
+
default=[],
|
|
1755
|
+
key=f"{prefix}_huecols",
|
|
1756
|
+
help=(
|
|
1757
|
+
"Each selected column becomes a bar group. "
|
|
1758
|
+
"Leave empty to use a single grouped series."
|
|
1759
|
+
),
|
|
1760
|
+
)
|
|
1761
|
+
else:
|
|
1762
|
+
st.info("Grouped/stacked mode requires a DataFrame with multiple numeric columns.")
|
|
1763
|
+
|
|
1764
|
+
st.markdown("**Style**")
|
|
1765
|
+
col1, col2 = st.columns(2)
|
|
1766
|
+
with col1:
|
|
1767
|
+
config["color"] = _color_widget(prefix)
|
|
1768
|
+
with col2:
|
|
1769
|
+
config["alpha"] = st.slider("Opacity", 0.1, 1.0, 0.8, 0.05, key=f"{prefix}_alpha")
|
|
1770
|
+
|
|
1771
|
+
# Error bars (simple mode only)
|
|
1772
|
+
if config.get("kind", "simple") == "simple":
|
|
1773
|
+
if isinstance(data, pd.DataFrame) and numeric:
|
|
1774
|
+
with st.expander("Error bars (optional)"):
|
|
1775
|
+
err_opts = ["None"] + [c for c in numeric if c != config.get("_val_col")]
|
|
1776
|
+
err_sel = st.selectbox(
|
|
1777
|
+
"Y error column",
|
|
1778
|
+
err_opts,
|
|
1779
|
+
key=f"{prefix}_yerr_col",
|
|
1780
|
+
)
|
|
1781
|
+
config["_yerr_col"] = None if err_sel == "None" else err_sel
|
|
1782
|
+
|
|
1783
|
+
st.markdown("**Figure & labels**")
|
|
1784
|
+
config["figsize"] = _figsize_widget(prefix)
|
|
1785
|
+
config.update(_label_widgets(prefix))
|
|
1786
|
+
with st.expander("Appearance"):
|
|
1787
|
+
config.update(_grid_widgets(prefix))
|
|
1788
|
+
config.update(_appearance_widgets(prefix))
|
|
1789
|
+
config.update(_axis_limits_widgets(prefix))
|
|
1790
|
+
config.update(_y_axis_widgets(prefix))
|
|
1791
|
+
config.update(_caption_widget(prefix))
|
|
1792
|
+
return config
|
|
1793
|
+
|
|
1794
|
+
|
|
1795
|
+
def _widgets_waterfall_plot(data: Any, prefix: str) -> Dict:
|
|
1796
|
+
config: Dict = {}
|
|
1797
|
+
numeric = _numeric_cols(data)
|
|
1798
|
+
|
|
1799
|
+
if isinstance(data, pd.DataFrame):
|
|
1800
|
+
if len(numeric) < 2:
|
|
1801
|
+
st.warning(
|
|
1802
|
+
"Waterfall plot requires at least 2 numeric columns (1 X + 1 or more Y traces)."
|
|
1803
|
+
)
|
|
1804
|
+
return {}
|
|
1805
|
+
config["_x_col"] = st.selectbox(
|
|
1806
|
+
"X column",
|
|
1807
|
+
numeric,
|
|
1808
|
+
index=0,
|
|
1809
|
+
key=f"{prefix}_xcol",
|
|
1810
|
+
)
|
|
1811
|
+
y_opts = [c for c in numeric if c != config["_x_col"]]
|
|
1812
|
+
config["_y_cols"] = st.multiselect(
|
|
1813
|
+
"Y (trace) column(s)",
|
|
1814
|
+
y_opts,
|
|
1815
|
+
default=y_opts[: min(5, len(y_opts))],
|
|
1816
|
+
key=f"{prefix}_ycols",
|
|
1817
|
+
help="Each column becomes one trace in the waterfall.",
|
|
1818
|
+
)
|
|
1819
|
+
if not config["_y_cols"]:
|
|
1820
|
+
st.info("Select at least one Y column.")
|
|
1821
|
+
return {}
|
|
1822
|
+
elif isinstance(data, np.ndarray):
|
|
1823
|
+
if data.ndim == 2:
|
|
1824
|
+
opts = [str(i) for i in range(data.shape[1])]
|
|
1825
|
+
config["_x_col"] = st.selectbox(
|
|
1826
|
+
"X column index",
|
|
1827
|
+
opts,
|
|
1828
|
+
index=0,
|
|
1829
|
+
key=f"{prefix}_xcol",
|
|
1830
|
+
)
|
|
1831
|
+
y_opts2 = [c for c in opts if c != config["_x_col"]]
|
|
1832
|
+
config["_y_cols"] = st.multiselect(
|
|
1833
|
+
"Y (trace) column indices",
|
|
1834
|
+
y_opts2,
|
|
1835
|
+
default=y_opts2,
|
|
1836
|
+
key=f"{prefix}_ycols",
|
|
1837
|
+
)
|
|
1838
|
+
elif data.ndim == 3:
|
|
1839
|
+
st.info("3-D array detected: each row slice will be one trace.")
|
|
1840
|
+
config["_x_col"] = None
|
|
1841
|
+
config["_y_cols"] = None
|
|
1842
|
+
else:
|
|
1843
|
+
st.warning("Waterfall plot requires a 2-D or 3-D array.")
|
|
1844
|
+
return {}
|
|
1845
|
+
else:
|
|
1846
|
+
st.warning("Unsupported data type for waterfall plot.")
|
|
1847
|
+
return {}
|
|
1848
|
+
|
|
1849
|
+
st.markdown("**Style**")
|
|
1850
|
+
col1, col2, col3 = st.columns(3)
|
|
1851
|
+
with col1:
|
|
1852
|
+
config["cmap"] = st.selectbox("Colormap", _CMAPS, index=0, key=f"{prefix}_cmap")
|
|
1853
|
+
with col2:
|
|
1854
|
+
config["alpha"] = st.slider("Opacity", 0.1, 1.0, 0.9, 0.05, key=f"{prefix}_alpha")
|
|
1855
|
+
with col3:
|
|
1856
|
+
offset_mode = st.selectbox(
|
|
1857
|
+
"Trace offset",
|
|
1858
|
+
["Auto", "Manual"],
|
|
1859
|
+
key=f"{prefix}_offset_mode",
|
|
1860
|
+
)
|
|
1861
|
+
if offset_mode == "Manual":
|
|
1862
|
+
config["offset"] = st.number_input(
|
|
1863
|
+
"Offset value",
|
|
1864
|
+
value=1.0,
|
|
1865
|
+
key=f"{prefix}_offset",
|
|
1866
|
+
)
|
|
1867
|
+
else:
|
|
1868
|
+
config["offset"] = "auto"
|
|
1869
|
+
|
|
1870
|
+
st.markdown("**Figure & labels**")
|
|
1871
|
+
config["figsize"] = _figsize_widget(prefix, default=(10.0, 7.0))
|
|
1872
|
+
config.update(_label_widgets(prefix))
|
|
1873
|
+
with st.expander("Appearance"):
|
|
1874
|
+
config.update(_grid_widgets(prefix))
|
|
1875
|
+
config.update(_appearance_widgets(prefix, include_linewidth=True))
|
|
1876
|
+
config.update(_axis_limits_widgets(prefix))
|
|
1877
|
+
config.update(_caption_widget(prefix))
|
|
1878
|
+
return config
|
|
1879
|
+
|
|
1880
|
+
|
|
1881
|
+
def _widgets_dual_axis_plot(data: Any, prefix: str) -> Dict:
|
|
1882
|
+
config: Dict = {}
|
|
1883
|
+
numeric = _numeric_cols(data)
|
|
1884
|
+
|
|
1885
|
+
if isinstance(data, pd.DataFrame):
|
|
1886
|
+
if len(numeric) < 3:
|
|
1887
|
+
st.warning("Dual Y-axis plot requires at least 3 numeric columns (1 X + 2 Y).")
|
|
1888
|
+
return {}
|
|
1889
|
+
config["_x_col"] = st.selectbox(
|
|
1890
|
+
"X column",
|
|
1891
|
+
numeric,
|
|
1892
|
+
index=0,
|
|
1893
|
+
key=f"{prefix}_xcol",
|
|
1894
|
+
)
|
|
1895
|
+
remaining = [c for c in numeric if c != config["_x_col"]]
|
|
1896
|
+
col1, col2 = st.columns(2)
|
|
1897
|
+
with col1:
|
|
1898
|
+
config["_y1_col"] = st.selectbox(
|
|
1899
|
+
"Left Y column",
|
|
1900
|
+
remaining,
|
|
1901
|
+
index=0,
|
|
1902
|
+
key=f"{prefix}_y1col",
|
|
1903
|
+
)
|
|
1904
|
+
with col2:
|
|
1905
|
+
y2_opts = [c for c in remaining if c != config["_y1_col"]]
|
|
1906
|
+
config["_y2_col"] = st.selectbox(
|
|
1907
|
+
"Right Y column",
|
|
1908
|
+
y2_opts if y2_opts else remaining,
|
|
1909
|
+
index=0,
|
|
1910
|
+
key=f"{prefix}_y2col",
|
|
1911
|
+
)
|
|
1912
|
+
elif isinstance(data, np.ndarray) and data.ndim == 2 and data.shape[1] >= 3:
|
|
1913
|
+
opts = [str(i) for i in range(data.shape[1])]
|
|
1914
|
+
config["_x_col"] = st.selectbox("X column index", opts, index=0, key=f"{prefix}_xcol")
|
|
1915
|
+
col1, col2 = st.columns(2)
|
|
1916
|
+
with col1:
|
|
1917
|
+
config["_y1_col"] = st.selectbox(
|
|
1918
|
+
"Left Y column index",
|
|
1919
|
+
[c for c in opts if c != config["_x_col"]],
|
|
1920
|
+
index=0,
|
|
1921
|
+
key=f"{prefix}_y1col",
|
|
1922
|
+
)
|
|
1923
|
+
with col2:
|
|
1924
|
+
y2_opts2 = [c for c in opts if c not in (config["_x_col"], config["_y1_col"])]
|
|
1925
|
+
config["_y2_col"] = st.selectbox(
|
|
1926
|
+
"Right Y column index",
|
|
1927
|
+
y2_opts2 if y2_opts2 else opts,
|
|
1928
|
+
index=0,
|
|
1929
|
+
key=f"{prefix}_y2col",
|
|
1930
|
+
)
|
|
1931
|
+
else:
|
|
1932
|
+
st.warning(
|
|
1933
|
+
"Dual Y-axis plot requires a DataFrame or 2-D array with at least 3 numeric columns."
|
|
1934
|
+
)
|
|
1935
|
+
return {}
|
|
1936
|
+
|
|
1937
|
+
st.markdown("**Style**")
|
|
1938
|
+
col1, col2, col3, col4 = st.columns(4)
|
|
1939
|
+
with col1:
|
|
1940
|
+
config["color1"] = st.color_picker("Left color", "#4682B4", key=f"{prefix}_c1")
|
|
1941
|
+
with col2:
|
|
1942
|
+
config["color2"] = st.color_picker("Right color", "#FF6347", key=f"{prefix}_c2")
|
|
1943
|
+
with col3:
|
|
1944
|
+
config["linestyle1"] = st.selectbox(
|
|
1945
|
+
"Left style",
|
|
1946
|
+
["-", "--", "-.", ":"],
|
|
1947
|
+
key=f"{prefix}_ls1",
|
|
1948
|
+
)
|
|
1949
|
+
with col4:
|
|
1950
|
+
config["linestyle2"] = st.selectbox(
|
|
1951
|
+
"Right style",
|
|
1952
|
+
["--", "-", "-.", ":"],
|
|
1953
|
+
key=f"{prefix}_ls2",
|
|
1954
|
+
)
|
|
1955
|
+
|
|
1956
|
+
st.markdown("**Figure & labels**")
|
|
1957
|
+
config["figsize"] = _figsize_widget(prefix)
|
|
1958
|
+
col1, col2, col3, col4 = st.columns(4)
|
|
1959
|
+
with col1:
|
|
1960
|
+
config["title"] = st.text_input("Title", key=f"{prefix}_title", help=_LABEL_HELP) or None
|
|
1961
|
+
with col2:
|
|
1962
|
+
config["xlabel"] = (
|
|
1963
|
+
st.text_input("X Label", key=f"{prefix}_xlabel", help=_LABEL_HELP) or None
|
|
1964
|
+
)
|
|
1965
|
+
with col3:
|
|
1966
|
+
config["ylabel1"] = (
|
|
1967
|
+
st.text_input("Left Y Label", key=f"{prefix}_ylabel1", help=_LABEL_HELP) or None
|
|
1968
|
+
)
|
|
1969
|
+
with col4:
|
|
1970
|
+
config["ylabel2"] = (
|
|
1971
|
+
st.text_input("Right Y Label", key=f"{prefix}_ylabel2", help=_LABEL_HELP) or None
|
|
1972
|
+
)
|
|
1973
|
+
_symbol_reference_expander()
|
|
1974
|
+
|
|
1975
|
+
with st.expander("Appearance"):
|
|
1976
|
+
config.update(_appearance_widgets(prefix, include_linewidth=True))
|
|
1977
|
+
config.update(_caption_widget(prefix))
|
|
1978
|
+
return config
|
|
1979
|
+
|
|
1980
|
+
|
|
1981
|
+
def _widgets_z_colored_scatter(data: Any, prefix: str) -> Dict:
|
|
1982
|
+
config: Dict = {}
|
|
1983
|
+
numeric = _numeric_cols(data)
|
|
1984
|
+
if isinstance(data, pd.DataFrame) and len(numeric) >= 2:
|
|
1985
|
+
col1, col2, col3 = st.columns(3)
|
|
1986
|
+
with col1:
|
|
1987
|
+
config["_x_col"] = st.selectbox("X column", numeric, index=0, key=f"{prefix}_xcol")
|
|
1988
|
+
with col2:
|
|
1989
|
+
config["_y_col"] = st.selectbox(
|
|
1990
|
+
"Y column", numeric, index=min(1, len(numeric) - 1), key=f"{prefix}_ycol"
|
|
1991
|
+
)
|
|
1992
|
+
with col3:
|
|
1993
|
+
config["_z_col"] = st.selectbox(
|
|
1994
|
+
"Color column", numeric, index=min(2, len(numeric) - 1), key=f"{prefix}_zcol"
|
|
1995
|
+
)
|
|
1996
|
+
elif isinstance(data, np.ndarray) and data.ndim == 2 and data.shape[1] >= 3:
|
|
1997
|
+
opts = [str(i) for i in range(data.shape[1])]
|
|
1998
|
+
col1, col2, col3 = st.columns(3)
|
|
1999
|
+
with col1:
|
|
2000
|
+
config["_x_col"] = st.selectbox("X column index", opts, index=0, key=f"{prefix}_xcol")
|
|
2001
|
+
with col2:
|
|
2002
|
+
config["_y_col"] = st.selectbox("Y column index", opts, index=1, key=f"{prefix}_ycol")
|
|
2003
|
+
with col3:
|
|
2004
|
+
config["_z_col"] = st.selectbox(
|
|
2005
|
+
"Color column index", opts, index=2, key=f"{prefix}_zcol"
|
|
2006
|
+
)
|
|
2007
|
+
else:
|
|
2008
|
+
st.warning("Z-colored scatter requires at least 3 columns.")
|
|
2009
|
+
|
|
2010
|
+
st.markdown("**Appearance**")
|
|
2011
|
+
col1, col2 = st.columns(2)
|
|
2012
|
+
with col1:
|
|
2013
|
+
config["cmap"] = st.selectbox("Colormap", _CMAPS, index=0, key=f"{prefix}_cmap")
|
|
2014
|
+
with col2:
|
|
2015
|
+
config["alpha"] = st.slider("Opacity", 0.1, 1.0, 0.8, 0.05, key=f"{prefix}_alpha")
|
|
2016
|
+
config["colorbar"] = st.checkbox("Show colorbar", value=True, key=f"{prefix}_colorbar")
|
|
2017
|
+
config["colorbar_label"] = (
|
|
2018
|
+
st.text_input("Colorbar label", value="", key=f"{prefix}_cblabel") or None
|
|
2019
|
+
)
|
|
2020
|
+
config["s"] = st.slider("Marker size", 5, 200, 20, 5, key=f"{prefix}_markersize")
|
|
2021
|
+
config["figsize"] = _figsize_widget(prefix)
|
|
2022
|
+
config.update(_label_widgets(prefix))
|
|
2023
|
+
with st.expander("Appearance"):
|
|
2024
|
+
config.update(_grid_widgets(prefix))
|
|
2025
|
+
config.update(_appearance_widgets(prefix))
|
|
2026
|
+
config.update(_axis_limits_widgets(prefix))
|
|
2027
|
+
config.update(_caption_widget(prefix))
|
|
2028
|
+
return config
|
|
2029
|
+
|
|
2030
|
+
|
|
2031
|
+
def _widgets_bubble_chart(data: Any, prefix: str) -> Dict:
|
|
2032
|
+
config: Dict = {}
|
|
2033
|
+
numeric = _numeric_cols(data)
|
|
2034
|
+
if isinstance(data, pd.DataFrame) and len(numeric) >= 3:
|
|
2035
|
+
all_cols = numeric
|
|
2036
|
+
col1, col2, col3 = st.columns(3)
|
|
2037
|
+
with col1:
|
|
2038
|
+
config["_x_col"] = st.selectbox("X column", all_cols, index=0, key=f"{prefix}_xcol")
|
|
2039
|
+
with col2:
|
|
2040
|
+
config["_y_col"] = st.selectbox(
|
|
2041
|
+
"Y column", all_cols, index=min(1, len(all_cols) - 1), key=f"{prefix}_ycol"
|
|
2042
|
+
)
|
|
2043
|
+
with col3:
|
|
2044
|
+
config["_size_col"] = st.selectbox(
|
|
2045
|
+
"Size column", all_cols, index=min(2, len(all_cols) - 1), key=f"{prefix}_sizecol"
|
|
2046
|
+
)
|
|
2047
|
+
z_opts = ["None"] + list(all_cols)
|
|
2048
|
+
z_sel = st.selectbox("Color column (optional)", z_opts, index=0, key=f"{prefix}_zcol")
|
|
2049
|
+
config["_z_col"] = None if z_sel == "None" else z_sel
|
|
2050
|
+
elif isinstance(data, np.ndarray) and data.ndim == 2 and data.shape[1] >= 3:
|
|
2051
|
+
opts = [str(i) for i in range(data.shape[1])]
|
|
2052
|
+
col1, col2, col3 = st.columns(3)
|
|
2053
|
+
with col1:
|
|
2054
|
+
config["_x_col"] = st.selectbox("X col index", opts, index=0, key=f"{prefix}_xcol")
|
|
2055
|
+
with col2:
|
|
2056
|
+
config["_y_col"] = st.selectbox("Y col index", opts, index=1, key=f"{prefix}_ycol")
|
|
2057
|
+
with col3:
|
|
2058
|
+
config["_size_col"] = st.selectbox(
|
|
2059
|
+
"Size col index", opts, index=2, key=f"{prefix}_sizecol"
|
|
2060
|
+
)
|
|
2061
|
+
config["_z_col"] = None
|
|
2062
|
+
else:
|
|
2063
|
+
st.warning("Bubble chart requires at least 3 columns (x, y, size).")
|
|
2064
|
+
|
|
2065
|
+
st.markdown("**Appearance**")
|
|
2066
|
+
col1, col2 = st.columns(2)
|
|
2067
|
+
with col1:
|
|
2068
|
+
config["color"] = _color_widget(prefix)
|
|
2069
|
+
with col2:
|
|
2070
|
+
config["alpha"] = st.slider("Opacity", 0.1, 1.0, 0.6, 0.05, key=f"{prefix}_alpha")
|
|
2071
|
+
config["size_scale"] = st.slider("Size scale", 0.1, 10.0, 1.0, 0.1, key=f"{prefix}_sizescale")
|
|
2072
|
+
if config.get("_z_col"):
|
|
2073
|
+
config["cmap"] = st.selectbox("Colormap", _CMAPS, index=0, key=f"{prefix}_cmap")
|
|
2074
|
+
config["colorbar"] = st.checkbox("Show colorbar", value=True, key=f"{prefix}_colorbar")
|
|
2075
|
+
config["figsize"] = _figsize_widget(prefix)
|
|
2076
|
+
config.update(_label_widgets(prefix))
|
|
2077
|
+
with st.expander("Appearance"):
|
|
2078
|
+
config.update(_grid_widgets(prefix))
|
|
2079
|
+
config.update(_appearance_widgets(prefix))
|
|
2080
|
+
config.update(_axis_limits_widgets(prefix))
|
|
2081
|
+
config.update(_caption_widget(prefix))
|
|
2082
|
+
return config
|
|
2083
|
+
|
|
2084
|
+
|
|
2085
|
+
def _widgets_polar_plot(data: Any, prefix: str) -> Dict:
|
|
2086
|
+
config: Dict = {}
|
|
2087
|
+
numeric = _numeric_cols(data)
|
|
2088
|
+
if isinstance(data, pd.DataFrame) and len(numeric) >= 2:
|
|
2089
|
+
col1, col2 = st.columns(2)
|
|
2090
|
+
with col1:
|
|
2091
|
+
config["_theta_col"] = st.selectbox(
|
|
2092
|
+
"Theta column (radians)", numeric, index=0, key=f"{prefix}_thetacol"
|
|
2093
|
+
)
|
|
2094
|
+
with col2:
|
|
2095
|
+
config["_r_col"] = st.selectbox(
|
|
2096
|
+
"R column", numeric, index=min(1, len(numeric) - 1), key=f"{prefix}_rcol"
|
|
2097
|
+
)
|
|
2098
|
+
elif isinstance(data, np.ndarray) and data.ndim == 2 and data.shape[1] >= 2:
|
|
2099
|
+
opts = [str(i) for i in range(data.shape[1])]
|
|
2100
|
+
col1, col2 = st.columns(2)
|
|
2101
|
+
with col1:
|
|
2102
|
+
config["_theta_col"] = st.selectbox(
|
|
2103
|
+
"Theta column index", opts, index=0, key=f"{prefix}_thetacol"
|
|
2104
|
+
)
|
|
2105
|
+
with col2:
|
|
2106
|
+
config["_r_col"] = st.selectbox("R column index", opts, index=1, key=f"{prefix}_rcol")
|
|
2107
|
+
elif isinstance(data, np.ndarray) and data.ndim == 1:
|
|
2108
|
+
config["_theta_col"] = None
|
|
2109
|
+
config["_r_col"] = None
|
|
2110
|
+
st.info("1D array: will use index as theta (radians) and values as r.")
|
|
2111
|
+
|
|
2112
|
+
st.markdown("**Appearance**")
|
|
2113
|
+
col1, col2 = st.columns(2)
|
|
2114
|
+
with col1:
|
|
2115
|
+
config["color"] = _color_widget(prefix)
|
|
2116
|
+
config["fill"] = st.checkbox("Fill area", value=False, key=f"{prefix}_fill")
|
|
2117
|
+
with col2:
|
|
2118
|
+
config["theta_zero_location"] = st.selectbox(
|
|
2119
|
+
"0° at", ["N", "E", "S", "W"], index=0, key=f"{prefix}_theta0"
|
|
2120
|
+
)
|
|
2121
|
+
config["theta_direction"] = st.selectbox(
|
|
2122
|
+
"Direction",
|
|
2123
|
+
[-1, 1],
|
|
2124
|
+
format_func=lambda x: "Clockwise" if x == -1 else "Counter-clockwise",
|
|
2125
|
+
index=0,
|
|
2126
|
+
key=f"{prefix}_thetadir",
|
|
2127
|
+
)
|
|
2128
|
+
config["figsize"] = _figsize_widget(prefix)
|
|
2129
|
+
config["title"] = st.text_input("Title", value="", key=f"{prefix}_title") or None
|
|
2130
|
+
with st.expander("Appearance"):
|
|
2131
|
+
config.update(_grid_widgets(prefix))
|
|
2132
|
+
config.update(_appearance_widgets(prefix))
|
|
2133
|
+
return config
|
|
2134
|
+
|
|
2135
|
+
|
|
2136
|
+
def _widgets_histogram_2d(data: Any, prefix: str) -> Dict:
|
|
2137
|
+
config: Dict = {}
|
|
2138
|
+
numeric = _numeric_cols(data)
|
|
2139
|
+
if isinstance(data, pd.DataFrame) and len(numeric) >= 2:
|
|
2140
|
+
col1, col2 = st.columns(2)
|
|
2141
|
+
with col1:
|
|
2142
|
+
config["_x_col"] = st.selectbox("X column", numeric, index=0, key=f"{prefix}_xcol")
|
|
2143
|
+
with col2:
|
|
2144
|
+
config["_y_col"] = st.selectbox(
|
|
2145
|
+
"Y column", numeric, index=min(1, len(numeric) - 1), key=f"{prefix}_ycol"
|
|
2146
|
+
)
|
|
2147
|
+
elif isinstance(data, np.ndarray) and data.ndim == 2 and data.shape[1] >= 2:
|
|
2148
|
+
opts = [str(i) for i in range(data.shape[1])]
|
|
2149
|
+
col1, col2 = st.columns(2)
|
|
2150
|
+
with col1:
|
|
2151
|
+
config["_x_col"] = st.selectbox("X col index", opts, index=0, key=f"{prefix}_xcol")
|
|
2152
|
+
with col2:
|
|
2153
|
+
config["_y_col"] = st.selectbox("Y col index", opts, index=1, key=f"{prefix}_ycol")
|
|
2154
|
+
else:
|
|
2155
|
+
st.warning("2D histogram requires at least 2 columns.")
|
|
2156
|
+
|
|
2157
|
+
st.markdown("**Settings**")
|
|
2158
|
+
col1, col2 = st.columns(2)
|
|
2159
|
+
with col1:
|
|
2160
|
+
config["mode"] = st.selectbox("Mode", ["hist2d", "hexbin"], index=0, key=f"{prefix}_mode")
|
|
2161
|
+
with col2:
|
|
2162
|
+
config["cmap"] = st.selectbox("Colormap", _CMAPS, index=0, key=f"{prefix}_cmap")
|
|
2163
|
+
if config.get("mode") == "hexbin":
|
|
2164
|
+
config["gridsize"] = st.slider("Grid size", 5, 80, 30, 5, key=f"{prefix}_gridsize")
|
|
2165
|
+
else:
|
|
2166
|
+
config["bins"] = st.slider("Bins", 5, 100, 30, 5, key=f"{prefix}_bins")
|
|
2167
|
+
config["colorbar"] = st.checkbox("Show colorbar", value=True, key=f"{prefix}_colorbar")
|
|
2168
|
+
config["figsize"] = _figsize_widget(prefix)
|
|
2169
|
+
config.update(_label_widgets(prefix))
|
|
2170
|
+
with st.expander("Appearance"):
|
|
2171
|
+
config.update(_appearance_widgets(prefix))
|
|
2172
|
+
config.update(_axis_limits_widgets(prefix))
|
|
2173
|
+
config.update(_caption_widget(prefix))
|
|
2174
|
+
return config
|
|
2175
|
+
|
|
2176
|
+
|
|
2177
|
+
def _widgets_pair_plot(data: Any, prefix: str) -> Dict:
|
|
2178
|
+
config: Dict = {}
|
|
2179
|
+
if not isinstance(data, pd.DataFrame):
|
|
2180
|
+
st.warning("Pair plot requires a DataFrame.")
|
|
2181
|
+
return config
|
|
2182
|
+
numeric = _numeric_cols(data)
|
|
2183
|
+
all_cols = _all_cols(data)
|
|
2184
|
+
if len(numeric) < 2:
|
|
2185
|
+
st.warning("Pair plot requires at least 2 numeric columns.")
|
|
2186
|
+
return config
|
|
2187
|
+
|
|
2188
|
+
selected_vars = st.multiselect(
|
|
2189
|
+
"Variables to plot (leave empty for all numeric)",
|
|
2190
|
+
numeric,
|
|
2191
|
+
default=[],
|
|
2192
|
+
key=f"{prefix}_vars",
|
|
2193
|
+
)
|
|
2194
|
+
config["vars"] = selected_vars if selected_vars else None
|
|
2195
|
+
|
|
2196
|
+
hue_opts = ["None"] + all_cols
|
|
2197
|
+
hue_sel = st.selectbox("Hue column", hue_opts, index=0, key=f"{prefix}_hue")
|
|
2198
|
+
config["hue"] = None if hue_sel == "None" else hue_sel
|
|
2199
|
+
|
|
2200
|
+
col1, col2 = st.columns(2)
|
|
2201
|
+
with col1:
|
|
2202
|
+
config["kind"] = st.selectbox(
|
|
2203
|
+
"Off-diagonal", ["scatter", "kde", "hist", "reg"], index=0, key=f"{prefix}_kind"
|
|
2204
|
+
)
|
|
2205
|
+
with col2:
|
|
2206
|
+
config["diag_kind"] = st.selectbox(
|
|
2207
|
+
"Diagonal", ["hist", "kde", "auto"], index=0, key=f"{prefix}_diagkind"
|
|
2208
|
+
)
|
|
2209
|
+
|
|
2210
|
+
config["title"] = st.text_input("Title", value="", key=f"{prefix}_title") or None
|
|
2211
|
+
return config
|
|
2212
|
+
|
|
2213
|
+
|
|
2214
|
+
def _widgets_interactive_3d_scatter(data: Any, prefix: str) -> Dict:
|
|
2215
|
+
config: Dict = {}
|
|
2216
|
+
numeric = _numeric_cols(data)
|
|
2217
|
+
if isinstance(data, pd.DataFrame) and len(numeric) >= 3:
|
|
2218
|
+
col1, col2, col3 = st.columns(3)
|
|
2219
|
+
with col1:
|
|
2220
|
+
config["_x_col"] = st.selectbox("X column", numeric, index=0, key=f"{prefix}_xcol")
|
|
2221
|
+
with col2:
|
|
2222
|
+
config["_y_col"] = st.selectbox(
|
|
2223
|
+
"Y column", numeric, index=min(1, len(numeric) - 1), key=f"{prefix}_ycol"
|
|
2224
|
+
)
|
|
2225
|
+
with col3:
|
|
2226
|
+
config["_z_col"] = st.selectbox(
|
|
2227
|
+
"Z column", numeric, index=min(2, len(numeric) - 1), key=f"{prefix}_zcol"
|
|
2228
|
+
)
|
|
2229
|
+
extra_opts = ["None"] + list(numeric)
|
|
2230
|
+
col_a, col_b = st.columns(2)
|
|
2231
|
+
with col_a:
|
|
2232
|
+
color_sel = st.selectbox(
|
|
2233
|
+
"Color column (optional)", extra_opts, index=0, key=f"{prefix}_ccol"
|
|
2234
|
+
)
|
|
2235
|
+
config["_color_col"] = None if color_sel == "None" else color_sel
|
|
2236
|
+
with col_b:
|
|
2237
|
+
size_sel = st.selectbox(
|
|
2238
|
+
"Size column (optional)", extra_opts, index=0, key=f"{prefix}_scol"
|
|
2239
|
+
)
|
|
2240
|
+
config["_size_col"] = None if size_sel == "None" else size_sel
|
|
2241
|
+
elif isinstance(data, np.ndarray) and data.ndim == 2 and data.shape[1] >= 3:
|
|
2242
|
+
opts = [str(i) for i in range(data.shape[1])]
|
|
2243
|
+
col1, col2, col3 = st.columns(3)
|
|
2244
|
+
with col1:
|
|
2245
|
+
config["_x_col"] = st.selectbox("X col index", opts, index=0, key=f"{prefix}_xcol")
|
|
2246
|
+
with col2:
|
|
2247
|
+
config["_y_col"] = st.selectbox("Y col index", opts, index=1, key=f"{prefix}_ycol")
|
|
2248
|
+
with col3:
|
|
2249
|
+
config["_z_col"] = st.selectbox("Z col index", opts, index=2, key=f"{prefix}_zcol")
|
|
2250
|
+
config["_color_col"] = None
|
|
2251
|
+
config["_size_col"] = None
|
|
2252
|
+
else:
|
|
2253
|
+
st.warning("3D scatter requires at least 3 columns.")
|
|
2254
|
+
|
|
2255
|
+
st.markdown("**Appearance**")
|
|
2256
|
+
col1, col2 = st.columns(2)
|
|
2257
|
+
with col1:
|
|
2258
|
+
config["colorscale"] = st.selectbox("Colorscale", _COLORSCALES, index=0, key=f"{prefix}_cs")
|
|
2259
|
+
config["marker_size"] = st.slider("Marker size", 2, 20, 5, 1, key=f"{prefix}_msize")
|
|
2260
|
+
with col2:
|
|
2261
|
+
config["opacity"] = st.slider("Opacity", 0.1, 1.0, 0.8, 0.05, key=f"{prefix}_opacity")
|
|
2262
|
+
config["title"] = st.text_input("Title", value="", key=f"{prefix}_title") or None
|
|
2263
|
+
config["xlabel"] = st.text_input("X label", value="", key=f"{prefix}_xl") or None
|
|
2264
|
+
config["ylabel"] = st.text_input("Y label", value="", key=f"{prefix}_yl") or None
|
|
2265
|
+
config["zlabel"] = st.text_input("Z label", value="", key=f"{prefix}_zl") or None
|
|
2266
|
+
with st.expander("Plotly settings"):
|
|
2267
|
+
config["plotly_height"] = st.slider("Height (px)", 400, 1000, 600, 50, key=f"{prefix}_ph")
|
|
2268
|
+
return config
|
|
2269
|
+
|
|
2270
|
+
|
|
2271
|
+
def _widgets_scatter_with_regression(data: Any, prefix: str) -> Dict:
|
|
2272
|
+
config: Dict = {}
|
|
2273
|
+
numeric = _numeric_cols(data)
|
|
2274
|
+
if isinstance(data, pd.DataFrame) and len(numeric) >= 2:
|
|
2275
|
+
col1, col2 = st.columns(2)
|
|
2276
|
+
with col1:
|
|
2277
|
+
config["_x_col"] = st.selectbox("X column", numeric, index=0, key=f"{prefix}_xcol")
|
|
2278
|
+
with col2:
|
|
2279
|
+
config["_y_col"] = st.selectbox(
|
|
2280
|
+
"Y column", numeric, index=min(1, len(numeric) - 1), key=f"{prefix}_ycol"
|
|
2281
|
+
)
|
|
2282
|
+
elif isinstance(data, np.ndarray) and data.ndim == 2 and data.shape[1] >= 2:
|
|
2283
|
+
opts = [str(i) for i in range(data.shape[1])]
|
|
2284
|
+
col1, col2 = st.columns(2)
|
|
2285
|
+
with col1:
|
|
2286
|
+
config["_x_col"] = st.selectbox("X col index", opts, index=0, key=f"{prefix}_xcol")
|
|
2287
|
+
with col2:
|
|
2288
|
+
config["_y_col"] = st.selectbox("Y col index", opts, index=1, key=f"{prefix}_ycol")
|
|
2289
|
+
else:
|
|
2290
|
+
st.warning("Scatter + Regression requires at least 2 numeric columns.")
|
|
2291
|
+
col1, col2 = st.columns(2)
|
|
2292
|
+
with col1:
|
|
2293
|
+
config["color"] = _color_widget(prefix)
|
|
2294
|
+
config["show_ci"] = st.checkbox("Show 95% CI band", value=True, key=f"{prefix}_ci")
|
|
2295
|
+
with col2:
|
|
2296
|
+
config["alpha"] = st.slider("Opacity", 0.1, 1.0, 0.7, 0.05, key=f"{prefix}_alpha")
|
|
2297
|
+
config["show_equation"] = st.checkbox("Show equation", value=True, key=f"{prefix}_eq")
|
|
2298
|
+
config["figsize"] = _figsize_widget(prefix)
|
|
2299
|
+
config.update(_label_widgets(prefix))
|
|
2300
|
+
with st.expander("Appearance"):
|
|
2301
|
+
config.update(_grid_widgets(prefix))
|
|
2302
|
+
config.update(_appearance_widgets(prefix))
|
|
2303
|
+
config.update(_axis_limits_widgets(prefix))
|
|
2304
|
+
config.update(_caption_widget(prefix))
|
|
2305
|
+
return config
|
|
2306
|
+
|
|
2307
|
+
|
|
2308
|
+
def _widgets_residual_plot(data: Any, prefix: str) -> Dict:
|
|
2309
|
+
config: Dict = {}
|
|
2310
|
+
numeric = _numeric_cols(data)
|
|
2311
|
+
if isinstance(data, pd.DataFrame) and len(numeric) >= 3:
|
|
2312
|
+
col1, col2, col3 = st.columns(3)
|
|
2313
|
+
with col1:
|
|
2314
|
+
config["_x_col"] = st.selectbox("X column", numeric, index=0, key=f"{prefix}_xcol")
|
|
2315
|
+
with col2:
|
|
2316
|
+
config["_y_actual_col"] = st.selectbox(
|
|
2317
|
+
"Y actual", numeric, index=min(1, len(numeric) - 1), key=f"{prefix}_yacol"
|
|
2318
|
+
)
|
|
2319
|
+
with col3:
|
|
2320
|
+
config["_y_fitted_col"] = st.selectbox(
|
|
2321
|
+
"Y fitted", numeric, index=min(2, len(numeric) - 1), key=f"{prefix}_yfcol"
|
|
2322
|
+
)
|
|
2323
|
+
elif isinstance(data, np.ndarray) and data.ndim == 2 and data.shape[1] >= 3:
|
|
2324
|
+
opts = [str(i) for i in range(data.shape[1])]
|
|
2325
|
+
col1, col2, col3 = st.columns(3)
|
|
2326
|
+
with col1:
|
|
2327
|
+
config["_x_col"] = st.selectbox("X col index", opts, index=0, key=f"{prefix}_xcol")
|
|
2328
|
+
with col2:
|
|
2329
|
+
config["_y_actual_col"] = st.selectbox(
|
|
2330
|
+
"Y actual index", opts, index=1, key=f"{prefix}_yacol"
|
|
2331
|
+
)
|
|
2332
|
+
with col3:
|
|
2333
|
+
config["_y_fitted_col"] = st.selectbox(
|
|
2334
|
+
"Y fitted index", opts, index=2, key=f"{prefix}_yfcol"
|
|
2335
|
+
)
|
|
2336
|
+
else:
|
|
2337
|
+
st.warning("Residual plot requires at least 3 columns: x, y_actual, y_fitted.")
|
|
2338
|
+
config["vs_fitted"] = st.checkbox(
|
|
2339
|
+
"Plot vs. fitted values (not x)", value=False, key=f"{prefix}_vsfitted"
|
|
2340
|
+
)
|
|
2341
|
+
config["show_zero_line"] = st.checkbox("Show zero line", value=True, key=f"{prefix}_zeroline")
|
|
2342
|
+
config["alpha"] = st.slider("Opacity", 0.1, 1.0, 0.75, 0.05, key=f"{prefix}_alpha")
|
|
2343
|
+
config["figsize"] = _figsize_widget(prefix)
|
|
2344
|
+
config.update(_label_widgets(prefix))
|
|
2345
|
+
with st.expander("Appearance"):
|
|
2346
|
+
config.update(_grid_widgets(prefix))
|
|
2347
|
+
config.update(_appearance_widgets(prefix))
|
|
2348
|
+
config.update(_axis_limits_widgets(prefix))
|
|
2349
|
+
config.update(_caption_widget(prefix))
|
|
2350
|
+
return config
|
|
2351
|
+
|
|
2352
|
+
|
|
2353
|
+
def _widgets_interactive_ternary(data: Any, prefix: str) -> Dict:
|
|
2354
|
+
config: Dict = {}
|
|
2355
|
+
numeric = _numeric_cols(data)
|
|
2356
|
+
if isinstance(data, pd.DataFrame) and len(numeric) >= 3:
|
|
2357
|
+
col1, col2, col3 = st.columns(3)
|
|
2358
|
+
with col1:
|
|
2359
|
+
config["_a_col"] = st.selectbox("A column", numeric, index=0, key=f"{prefix}_acol")
|
|
2360
|
+
with col2:
|
|
2361
|
+
config["_b_col"] = st.selectbox(
|
|
2362
|
+
"B column", numeric, index=min(1, len(numeric) - 1), key=f"{prefix}_bcol"
|
|
2363
|
+
)
|
|
2364
|
+
with col3:
|
|
2365
|
+
config["_c_col"] = st.selectbox(
|
|
2366
|
+
"C column", numeric, index=min(2, len(numeric) - 1), key=f"{prefix}_ccol"
|
|
2367
|
+
)
|
|
2368
|
+
extra_opts = ["None"] + numeric
|
|
2369
|
+
color_sel = st.selectbox(
|
|
2370
|
+
"Color column (optional)", extra_opts, index=0, key=f"{prefix}_colcol"
|
|
2371
|
+
)
|
|
2372
|
+
config["_color_col"] = None if color_sel == "None" else color_sel
|
|
2373
|
+
elif isinstance(data, np.ndarray) and data.ndim == 2 and data.shape[1] >= 3:
|
|
2374
|
+
opts = [str(i) for i in range(data.shape[1])]
|
|
2375
|
+
col1, col2, col3 = st.columns(3)
|
|
2376
|
+
with col1:
|
|
2377
|
+
config["_a_col"] = st.selectbox("A col index", opts, index=0, key=f"{prefix}_acol")
|
|
2378
|
+
with col2:
|
|
2379
|
+
config["_b_col"] = st.selectbox("B col index", opts, index=1, key=f"{prefix}_bcol")
|
|
2380
|
+
with col3:
|
|
2381
|
+
config["_c_col"] = st.selectbox("C col index", opts, index=2, key=f"{prefix}_ccol")
|
|
2382
|
+
config["_color_col"] = None
|
|
2383
|
+
else:
|
|
2384
|
+
st.warning("Ternary plot requires at least 3 columns (A, B, C components).")
|
|
2385
|
+
col1, col2, col3 = st.columns(3)
|
|
2386
|
+
with col1:
|
|
2387
|
+
config["a_label"] = st.text_input("A label", value="A", key=f"{prefix}_al")
|
|
2388
|
+
with col2:
|
|
2389
|
+
config["b_label"] = st.text_input("B label", value="B", key=f"{prefix}_bl")
|
|
2390
|
+
with col3:
|
|
2391
|
+
config["c_label"] = st.text_input("C label", value="C", key=f"{prefix}_cl")
|
|
2392
|
+
config["colorscale"] = st.selectbox("Colorscale", _COLORSCALES, index=0, key=f"{prefix}_cs")
|
|
2393
|
+
config["marker_size"] = st.slider("Marker size", 3, 20, 8, 1, key=f"{prefix}_ms")
|
|
2394
|
+
config["opacity"] = st.slider("Opacity", 0.1, 1.0, 0.85, 0.05, key=f"{prefix}_op")
|
|
2395
|
+
config["title"] = st.text_input("Title", value="", key=f"{prefix}_title") or None
|
|
2396
|
+
with st.expander("Plotly settings"):
|
|
2397
|
+
config["plotly_height"] = st.slider("Height (px)", 400, 900, 600, 50, key=f"{prefix}_ph")
|
|
2398
|
+
return config
|
|
2399
|
+
|
|
2400
|
+
|
|
2401
|
+
def _widgets_broken_axis_plot(data: Any, prefix: str) -> Dict:
|
|
2402
|
+
config: Dict = {}
|
|
2403
|
+
numeric = _numeric_cols(data)
|
|
2404
|
+
if isinstance(data, pd.DataFrame) and len(numeric) >= 2:
|
|
2405
|
+
col1, col2 = st.columns(2)
|
|
2406
|
+
with col1:
|
|
2407
|
+
config["_x_col"] = st.selectbox("X column", numeric, index=0, key=f"{prefix}_xcol")
|
|
2408
|
+
with col2:
|
|
2409
|
+
config["_y_col"] = st.selectbox(
|
|
2410
|
+
"Y column", numeric, index=min(1, len(numeric) - 1), key=f"{prefix}_ycol"
|
|
2411
|
+
)
|
|
2412
|
+
elif isinstance(data, np.ndarray) and data.ndim == 2 and data.shape[1] >= 2:
|
|
2413
|
+
opts = [str(i) for i in range(data.shape[1])]
|
|
2414
|
+
col1, col2 = st.columns(2)
|
|
2415
|
+
with col1:
|
|
2416
|
+
config["_x_col"] = st.selectbox("X col index", opts, index=0, key=f"{prefix}_xcol")
|
|
2417
|
+
with col2:
|
|
2418
|
+
config["_y_col"] = st.selectbox("Y col index", opts, index=1, key=f"{prefix}_ycol")
|
|
2419
|
+
else:
|
|
2420
|
+
st.warning("Broken axis plot requires at least 2 columns (x and y).")
|
|
2421
|
+
|
|
2422
|
+
st.markdown("**Axis breaks** — define regions to skip")
|
|
2423
|
+
n_breaks = st.number_input(
|
|
2424
|
+
"Number of breaks", min_value=1, max_value=4, value=1, step=1, key=f"{prefix}_nbreaks"
|
|
2425
|
+
)
|
|
2426
|
+
breaks = []
|
|
2427
|
+
for i in range(int(n_breaks)):
|
|
2428
|
+
c1, c2 = st.columns(2)
|
|
2429
|
+
with c1:
|
|
2430
|
+
lo = st.number_input(
|
|
2431
|
+
f"Break {i + 1} start", value=0.0, key=f"{prefix}_blo{i}", format="%.4g"
|
|
2432
|
+
)
|
|
2433
|
+
with c2:
|
|
2434
|
+
hi = st.number_input(
|
|
2435
|
+
f"Break {i + 1} end", value=1.0, key=f"{prefix}_bhi{i}", format="%.4g"
|
|
2436
|
+
)
|
|
2437
|
+
if hi > lo:
|
|
2438
|
+
breaks.append((float(lo), float(hi)))
|
|
2439
|
+
config["_breaks"] = breaks if breaks else [(0.0, 0.0)]
|
|
2440
|
+
config["axis"] = st.selectbox("Break axis", ["x", "y"], index=0, key=f"{prefix}_axis")
|
|
2441
|
+
col1, col2 = st.columns(2)
|
|
2442
|
+
with col1:
|
|
2443
|
+
config["color"] = _color_widget(prefix)
|
|
2444
|
+
with col2:
|
|
2445
|
+
config["linewidth"] = st.slider("Line width", 0.5, 5.0, 1.5, 0.25, key=f"{prefix}_lw")
|
|
2446
|
+
config["figsize"] = _figsize_widget(prefix)
|
|
2447
|
+
config.update(_label_widgets(prefix))
|
|
2448
|
+
with st.expander("Appearance"):
|
|
2449
|
+
config.update(_grid_widgets(prefix))
|
|
2450
|
+
config.update(_appearance_widgets(prefix))
|
|
2451
|
+
config.update(_caption_widget(prefix))
|
|
2452
|
+
return config
|
|
2453
|
+
|
|
2454
|
+
|
|
2455
|
+
def _widgets_inset_plot(data: Any, prefix: str) -> Dict:
|
|
2456
|
+
"""Widget function for the inset_plot specialty plot type."""
|
|
2457
|
+
config: Dict = {}
|
|
2458
|
+
numeric = _numeric_cols(data)
|
|
2459
|
+
|
|
2460
|
+
if isinstance(data, pd.DataFrame):
|
|
2461
|
+
if len(numeric) < 2:
|
|
2462
|
+
st.warning("Inset plot requires at least 2 numeric columns.")
|
|
2463
|
+
return {}
|
|
2464
|
+
st.markdown("**Main plot columns**")
|
|
2465
|
+
c1, c2 = st.columns(2)
|
|
2466
|
+
with c1:
|
|
2467
|
+
config["_x_col"] = st.selectbox(
|
|
2468
|
+
"X column (main)", numeric, index=0, key=f"{prefix}_xcol"
|
|
2469
|
+
)
|
|
2470
|
+
with c2:
|
|
2471
|
+
config["_y_col"] = st.selectbox(
|
|
2472
|
+
"Y column (main)", numeric, index=min(1, len(numeric) - 1), key=f"{prefix}_ycol"
|
|
2473
|
+
)
|
|
2474
|
+
st.markdown("**Inset plot columns**")
|
|
2475
|
+
c3, c4 = st.columns(2)
|
|
2476
|
+
with c3:
|
|
2477
|
+
config["_xi_col"] = st.selectbox(
|
|
2478
|
+
"X column (inset)", numeric, index=0, key=f"{prefix}_xicol"
|
|
2479
|
+
)
|
|
2480
|
+
with c4:
|
|
2481
|
+
config["_yi_col"] = st.selectbox(
|
|
2482
|
+
"Y column (inset)", numeric, index=min(1, len(numeric) - 1), key=f"{prefix}_yicol"
|
|
2483
|
+
)
|
|
2484
|
+
elif isinstance(data, np.ndarray) and data.ndim == 2 and data.shape[1] >= 2:
|
|
2485
|
+
opts = [str(i) for i in range(data.shape[1])]
|
|
2486
|
+
st.markdown("**Main plot columns**")
|
|
2487
|
+
c1, c2 = st.columns(2)
|
|
2488
|
+
with c1:
|
|
2489
|
+
config["_x_col"] = st.selectbox(
|
|
2490
|
+
"X col index (main)", opts, index=0, key=f"{prefix}_xcol"
|
|
2491
|
+
)
|
|
2492
|
+
with c2:
|
|
2493
|
+
config["_y_col"] = st.selectbox(
|
|
2494
|
+
"Y col index (main)", opts, index=min(1, len(opts) - 1), key=f"{prefix}_ycol"
|
|
2495
|
+
)
|
|
2496
|
+
st.markdown("**Inset plot columns**")
|
|
2497
|
+
c3, c4 = st.columns(2)
|
|
2498
|
+
with c3:
|
|
2499
|
+
config["_xi_col"] = st.selectbox(
|
|
2500
|
+
"X col index (inset)", opts, index=0, key=f"{prefix}_xicol"
|
|
2501
|
+
)
|
|
2502
|
+
with c4:
|
|
2503
|
+
config["_yi_col"] = st.selectbox(
|
|
2504
|
+
"Y col index (inset)", opts, index=min(1, len(opts) - 1), key=f"{prefix}_yicol"
|
|
2505
|
+
)
|
|
2506
|
+
else:
|
|
2507
|
+
st.warning("Inset plot requires a DataFrame or 2D array with >= 2 columns.")
|
|
2508
|
+
return {}
|
|
2509
|
+
|
|
2510
|
+
st.markdown("**Inset bounds** (axes fraction 0–1)")
|
|
2511
|
+
ib1, ib2, ib3, ib4 = st.columns(4)
|
|
2512
|
+
with ib1:
|
|
2513
|
+
config["_ib_x0"] = st.number_input(
|
|
2514
|
+
"x0", min_value=0.0, max_value=0.95, value=0.55, step=0.05, key=f"{prefix}_ibx0"
|
|
2515
|
+
)
|
|
2516
|
+
with ib2:
|
|
2517
|
+
config["_ib_y0"] = st.number_input(
|
|
2518
|
+
"y0", min_value=0.0, max_value=0.95, value=0.55, step=0.05, key=f"{prefix}_iby0"
|
|
2519
|
+
)
|
|
2520
|
+
with ib3:
|
|
2521
|
+
config["_ib_w"] = st.number_input(
|
|
2522
|
+
"width", min_value=0.05, max_value=0.9, value=0.4, step=0.05, key=f"{prefix}_ibw"
|
|
2523
|
+
)
|
|
2524
|
+
with ib4:
|
|
2525
|
+
config["_ib_h"] = st.number_input(
|
|
2526
|
+
"height", min_value=0.05, max_value=0.9, value=0.35, step=0.05, key=f"{prefix}_ibh"
|
|
2527
|
+
)
|
|
2528
|
+
|
|
2529
|
+
st.markdown("**Indicate region on main plot** (optional)")
|
|
2530
|
+
use_region = st.checkbox("Shade region on main plot", value=False, key=f"{prefix}_useregion")
|
|
2531
|
+
if use_region:
|
|
2532
|
+
rc1, rc2 = st.columns(2)
|
|
2533
|
+
with rc1:
|
|
2534
|
+
config["_region_start"] = st.number_input(
|
|
2535
|
+
"Region start", value=0.0, key=f"{prefix}_rstart"
|
|
2536
|
+
)
|
|
2537
|
+
with rc2:
|
|
2538
|
+
config["_region_end"] = st.number_input("Region end", value=1.0, key=f"{prefix}_rend")
|
|
2539
|
+
|
|
2540
|
+
st.markdown("**Labels**")
|
|
2541
|
+
config["inset_xlabel"] = (
|
|
2542
|
+
st.text_input("Inset X label", value="", key=f"{prefix}_ixlabel") or None
|
|
2543
|
+
)
|
|
2544
|
+
config["inset_ylabel"] = (
|
|
2545
|
+
st.text_input("Inset Y label", value="", key=f"{prefix}_iylabel") or None
|
|
2546
|
+
)
|
|
2547
|
+
config["figsize"] = _figsize_widget(prefix)
|
|
2548
|
+
config.update(_label_widgets(prefix))
|
|
2549
|
+
with st.expander("Appearance"):
|
|
2550
|
+
config.update(_grid_widgets(prefix))
|
|
2551
|
+
config.update(_appearance_widgets(prefix, include_linewidth=True))
|
|
2552
|
+
config.update(_axis_limits_widgets(prefix))
|
|
2553
|
+
config.update(_caption_widget(prefix))
|
|
2554
|
+
return config
|
|
2555
|
+
|
|
2556
|
+
|
|
2557
|
+
# ── Dispatch table ───────────────────────────────────────────────────────────
|
|
2558
|
+
|
|
2559
|
+
_WIDGET_FUNCS: Dict[str, Any] = {
|
|
2560
|
+
"histogram": _widgets_histogram,
|
|
2561
|
+
"line_plot": _widgets_line_plot,
|
|
2562
|
+
"scatter_plot": _widgets_scatter_plot,
|
|
2563
|
+
"bar_chart": _widgets_bar_chart,
|
|
2564
|
+
"heatmap": _widgets_heatmap,
|
|
2565
|
+
"contour_plot": _widgets_contour_plot,
|
|
2566
|
+
"waterfall_plot": _widgets_waterfall_plot,
|
|
2567
|
+
"dual_axis_plot": _widgets_dual_axis_plot,
|
|
2568
|
+
"broken_axis_plot": _widgets_broken_axis_plot,
|
|
2569
|
+
"z_colored_scatter": _widgets_z_colored_scatter,
|
|
2570
|
+
"bubble_chart": _widgets_bubble_chart,
|
|
2571
|
+
"polar_plot": _widgets_polar_plot,
|
|
2572
|
+
"histogram_2d": _widgets_histogram_2d,
|
|
2573
|
+
"distribution_plot": _widgets_distribution_plot,
|
|
2574
|
+
"box_plot": _widgets_box_plot,
|
|
2575
|
+
"regression_plot": _widgets_regression_plot,
|
|
2576
|
+
"pair_plot": _widgets_pair_plot,
|
|
2577
|
+
"interactive_histogram": _widgets_interactive_histogram,
|
|
2578
|
+
"interactive_scatter": _widgets_interactive_scatter,
|
|
2579
|
+
"interactive_line": _widgets_interactive_line,
|
|
2580
|
+
"interactive_heatmap": _widgets_interactive_heatmap,
|
|
2581
|
+
"interactive_3d_surface": _widgets_interactive_3d_surface,
|
|
2582
|
+
"interactive_3d_scatter": _widgets_interactive_3d_scatter,
|
|
2583
|
+
"scatter_with_regression": _widgets_scatter_with_regression,
|
|
2584
|
+
"residual_plot": _widgets_residual_plot,
|
|
2585
|
+
"interactive_ternary": _widgets_interactive_ternary,
|
|
2586
|
+
"inset_plot": _widgets_inset_plot,
|
|
2587
|
+
}
|
|
2588
|
+
|
|
2589
|
+
|
|
2590
|
+
# ── Public API ───────────────────────────────────────────────────────────────
|
|
2591
|
+
|
|
2592
|
+
|
|
2593
|
+
def get_plot_config_widgets(plot_type: str, data: Any) -> Dict:
|
|
2594
|
+
"""Render Streamlit widgets for *plot_type* and return a config dict.
|
|
2595
|
+
|
|
2596
|
+
Call inside a Streamlit page after loading a dataset. Renders
|
|
2597
|
+
data-column selectors and plot-parameter controls appropriate for the
|
|
2598
|
+
chosen plot type and the actual type/shape of *data*.
|
|
2599
|
+
|
|
2600
|
+
The returned dictionary contains two kinds of keys:
|
|
2601
|
+
|
|
2602
|
+
* **Public keys** — valid keyword arguments for the matching plotting
|
|
2603
|
+
function (``title``, ``xlabel``, ``bins``, ``color``, ``figsize``, …).
|
|
2604
|
+
* **Private keys** (prefixed ``_``) — data-selection hints the caller
|
|
2605
|
+
uses to extract arrays from *data* before calling the plotting
|
|
2606
|
+
function (e.g. ``_x_col``, ``_y_col``, ``_data_col``).
|
|
2607
|
+
|
|
2608
|
+
Use :func:`get_plot_kwargs` to strip private keys before unpacking
|
|
2609
|
+
into a plotting function.
|
|
2610
|
+
|
|
2611
|
+
Parameters
|
|
2612
|
+
----------
|
|
2613
|
+
plot_type : str
|
|
2614
|
+
One of the keys in :data:`PLOT_TYPES`.
|
|
2615
|
+
data : Any
|
|
2616
|
+
Currently loaded dataset (DataFrame, ndarray, …).
|
|
2617
|
+
|
|
2618
|
+
Returns
|
|
2619
|
+
-------
|
|
2620
|
+
config : dict
|
|
2621
|
+
Configuration dict; empty dict if *plot_type* is unknown or *data*
|
|
2622
|
+
is incompatible with the selected plot type.
|
|
2623
|
+
|
|
2624
|
+
Examples
|
|
2625
|
+
--------
|
|
2626
|
+
>>> config = get_plot_config_widgets('histogram', arr)
|
|
2627
|
+
>>> col = config.get('_data_col') # None -> use full array
|
|
2628
|
+
>>> plot_data = arr if col is None else df[col]
|
|
2629
|
+
>>> fig, ax, info = histogram(plot_data, **get_plot_kwargs(config))
|
|
2630
|
+
>>> st.pyplot(fig)
|
|
2631
|
+
"""
|
|
2632
|
+
if plot_type not in _WIDGET_FUNCS:
|
|
2633
|
+
st.error(f"Unknown plot type: '{plot_type}'")
|
|
2634
|
+
return {}
|
|
2635
|
+
return _WIDGET_FUNCS[plot_type](data, plot_type)
|
|
2636
|
+
|
|
2637
|
+
|
|
2638
|
+
def get_plot_kwargs(config: Dict) -> Dict:
|
|
2639
|
+
"""Return plotting-function kwargs extracted from *config*.
|
|
2640
|
+
|
|
2641
|
+
Strips private (``_``-prefixed) keys and drops ``None`` values so the
|
|
2642
|
+
result can be unpacked directly into any plotting function.
|
|
2643
|
+
|
|
2644
|
+
Parameters
|
|
2645
|
+
----------
|
|
2646
|
+
config : dict
|
|
2647
|
+
Raw config returned by :func:`get_plot_config_widgets`.
|
|
2648
|
+
|
|
2649
|
+
Returns
|
|
2650
|
+
-------
|
|
2651
|
+
kwargs : dict
|
|
2652
|
+
Clean dict ready for ``**kwargs`` unpacking into a plotting function.
|
|
2653
|
+
|
|
2654
|
+
Examples
|
|
2655
|
+
--------
|
|
2656
|
+
>>> config = get_plot_config_widgets('scatter_plot', data)
|
|
2657
|
+
>>> fig, ax = scatter_plot(x, y, **get_plot_kwargs(config))
|
|
2658
|
+
"""
|
|
2659
|
+
return {
|
|
2660
|
+
k: v
|
|
2661
|
+
for k, v in config.items()
|
|
2662
|
+
if not k.startswith("_") and v is not None and k not in _STYLE_KEYS
|
|
2663
|
+
}
|
|
2664
|
+
|
|
2665
|
+
|
|
2666
|
+
# ── Click-to-annotate helpers (Quick Plot, Plotly mode) ──────────────────────
|
|
2667
|
+
|
|
2668
|
+
|
|
2669
|
+
def _get(obj: Any, key: str, default: Any = None) -> Any:
|
|
2670
|
+
"""Read ``key`` from a mapping or an attribute-style object."""
|
|
2671
|
+
if isinstance(obj, dict):
|
|
2672
|
+
return obj.get(key, default)
|
|
2673
|
+
try:
|
|
2674
|
+
return obj[key]
|
|
2675
|
+
except (KeyError, TypeError, IndexError):
|
|
2676
|
+
return getattr(obj, key, default)
|
|
2677
|
+
|
|
2678
|
+
|
|
2679
|
+
def selected_points(event: Any) -> List[Dict[str, Any]]:
|
|
2680
|
+
"""Extract ``{"x", "y"}`` pairs from a ``st.plotly_chart`` selection event.
|
|
2681
|
+
|
|
2682
|
+
Streamlit returns each selected point as a plain ``dict`` (not an object),
|
|
2683
|
+
so attribute access such as ``point.x`` raises ``AttributeError``. This
|
|
2684
|
+
helper reads both forms and skips points that carry no x/y coordinate
|
|
2685
|
+
(e.g. pie slices).
|
|
2686
|
+
|
|
2687
|
+
Args:
|
|
2688
|
+
event: The value returned by ``st.plotly_chart(..., on_select="rerun")``,
|
|
2689
|
+
or ``None``.
|
|
2690
|
+
|
|
2691
|
+
Returns:
|
|
2692
|
+
List of ``{"x": ..., "y": ...}`` dicts, in selection order.
|
|
2693
|
+
"""
|
|
2694
|
+
if not event:
|
|
2695
|
+
return []
|
|
2696
|
+
selection = _get(event, "selection")
|
|
2697
|
+
if not selection:
|
|
2698
|
+
return []
|
|
2699
|
+
points = _get(selection, "points") or []
|
|
2700
|
+
result: List[Dict[str, Any]] = []
|
|
2701
|
+
for pt in points:
|
|
2702
|
+
x, y = _get(pt, "x"), _get(pt, "y")
|
|
2703
|
+
if x is None or y is None:
|
|
2704
|
+
continue
|
|
2705
|
+
result.append({"x": x, "y": y})
|
|
2706
|
+
return result
|
|
2707
|
+
|
|
2708
|
+
|
|
2709
|
+
def format_marker_label(x: Any, y: Any) -> str:
|
|
2710
|
+
"""Format a marked point as ``"(x, y)"``; numbers use 4 significant digits.
|
|
2711
|
+
|
|
2712
|
+
Non-numeric coordinates (category names, date strings) are shown as-is
|
|
2713
|
+
instead of raising on the ``:.4g`` format spec.
|
|
2714
|
+
"""
|
|
2715
|
+
|
|
2716
|
+
def _fmt(v: Any) -> str:
|
|
2717
|
+
if isinstance(v, (int, float, np.number)) and not isinstance(v, bool):
|
|
2718
|
+
return f"{v:.4g}"
|
|
2719
|
+
return str(v)
|
|
2720
|
+
|
|
2721
|
+
return f"({_fmt(x)}, {_fmt(y)})"
|