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,388 @@
|
|
|
1
|
+
"""Settings Page.
|
|
2
|
+
|
|
3
|
+
Manage persistent user defaults and named configuration presets that are
|
|
4
|
+
saved to a local ``config.json`` file next to the app. The file is
|
|
5
|
+
untracked by version control — add ``config.json`` to .gitignore.
|
|
6
|
+
|
|
7
|
+
Sections
|
|
8
|
+
--------
|
|
9
|
+
1. Default Settings — global style defaults applied on every new plot
|
|
10
|
+
2. Presets — save / load / delete named configurations
|
|
11
|
+
3. Config file info — path and raw JSON viewer
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import json
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
import sys
|
|
17
|
+
|
|
18
|
+
import streamlit as st
|
|
19
|
+
|
|
20
|
+
sys.path.insert(0, str(Path(__file__).parent.parent.parent))
|
|
21
|
+
|
|
22
|
+
from eagleplot.utils import initialize_session_state
|
|
23
|
+
from eagleplot.utils.user_settings import (
|
|
24
|
+
get_config_path,
|
|
25
|
+
get_defaults,
|
|
26
|
+
save_defaults,
|
|
27
|
+
list_presets,
|
|
28
|
+
save_preset,
|
|
29
|
+
load_preset,
|
|
30
|
+
delete_preset,
|
|
31
|
+
)
|
|
32
|
+
from eagleplot.utils.plot_config import COLOR_PALETTE_NAMES, _FONT_OPTIONS
|
|
33
|
+
from eagleplot.utils.theming import (
|
|
34
|
+
STREAMLIT_DEFAULTS,
|
|
35
|
+
read_theme,
|
|
36
|
+
write_theme,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
# Aliased: this page shows two different config files, and an unaliased import
|
|
40
|
+
# would shadow user_settings.get_config_path() (config.json) with the theming
|
|
41
|
+
# one (config.toml), silently repointing the "Config File" section below.
|
|
42
|
+
from eagleplot.utils.theming import get_config_path as get_theme_config_path
|
|
43
|
+
|
|
44
|
+
initialize_session_state()
|
|
45
|
+
|
|
46
|
+
# ── Page header ───────────────────────────────────────────────────────────────
|
|
47
|
+
st.title("Settings")
|
|
48
|
+
st.caption(
|
|
49
|
+
"Persistent defaults and named presets are saved to **`config.json`**. "
|
|
50
|
+
"Changes take effect the next time you open Quick Plot."
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
# ══════════════════════════════════════════════════════════════════════════════
|
|
54
|
+
# Section 0 — Theme
|
|
55
|
+
# ══════════════════════════════════════════════════════════════════════════════
|
|
56
|
+
|
|
57
|
+
st.markdown("## Theme")
|
|
58
|
+
st.markdown(
|
|
59
|
+
"Choose a base theme and optionally customize colours. "
|
|
60
|
+
"Click **Save theme** then **restart the app** for changes to take effect."
|
|
61
|
+
)
|
|
62
|
+
st.caption(
|
|
63
|
+
f"Theme is saved to `{get_theme_config_path()}` — the location Streamlit reads when "
|
|
64
|
+
"launched from this directory."
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
_cur = read_theme()
|
|
68
|
+
|
|
69
|
+
with st.form("theme_form"):
|
|
70
|
+
_tc1, _tc2 = st.columns(2)
|
|
71
|
+
with _tc1:
|
|
72
|
+
_base = st.selectbox(
|
|
73
|
+
"Base theme",
|
|
74
|
+
["light", "dark"],
|
|
75
|
+
index=0 if _cur.get("base", "light") == "light" else 1,
|
|
76
|
+
)
|
|
77
|
+
with _tc2:
|
|
78
|
+
_primary = st.color_picker(
|
|
79
|
+
"Primary colour",
|
|
80
|
+
value=_cur.get("primaryColor", "#1f77b4"),
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
_tc3, _tc4, _tc5 = st.columns(3)
|
|
84
|
+
with _tc3:
|
|
85
|
+
_bg = st.color_picker(
|
|
86
|
+
"Background",
|
|
87
|
+
value=_cur.get("backgroundColor", "#ffffff"),
|
|
88
|
+
)
|
|
89
|
+
with _tc4:
|
|
90
|
+
_sbg = st.color_picker(
|
|
91
|
+
"Sidebar / widget background",
|
|
92
|
+
value=_cur.get("secondaryBackgroundColor", "#f0f2f6"),
|
|
93
|
+
)
|
|
94
|
+
with _tc5:
|
|
95
|
+
_text = st.color_picker(
|
|
96
|
+
"Text colour",
|
|
97
|
+
value=_cur.get("textColor", "#262730"),
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
_theme_submitted = st.form_submit_button("Save theme", type="primary")
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _save_theme(theme: dict, success_message: str) -> None:
|
|
104
|
+
"""Write a theme and report what actually happened.
|
|
105
|
+
|
|
106
|
+
The previous implementation returned silently when config.toml was missing --
|
|
107
|
+
the default for every pip-installed user -- while still printing a success
|
|
108
|
+
message. See audit A-18.
|
|
109
|
+
"""
|
|
110
|
+
try:
|
|
111
|
+
written = write_theme(theme)
|
|
112
|
+
except OSError as exc:
|
|
113
|
+
st.error(
|
|
114
|
+
f"Could not write the theme: {exc}\n\n"
|
|
115
|
+
f"EaglePlot tried to write `{get_theme_config_path()}`. If that directory is "
|
|
116
|
+
"read-only, launch EaglePlot from a directory you can write to, or create "
|
|
117
|
+
"`.streamlit/config.toml` there yourself."
|
|
118
|
+
)
|
|
119
|
+
return
|
|
120
|
+
st.success(f"{success_message} Saved to `{written}` — restart the app to apply.")
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
if _theme_submitted:
|
|
124
|
+
_save_theme(
|
|
125
|
+
{
|
|
126
|
+
"base": _base,
|
|
127
|
+
"primaryColor": _primary,
|
|
128
|
+
"backgroundColor": _bg,
|
|
129
|
+
"secondaryBackgroundColor": _sbg,
|
|
130
|
+
"textColor": _text,
|
|
131
|
+
},
|
|
132
|
+
"Theme saved.",
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
if st.button("Reset theme to Streamlit default"):
|
|
136
|
+
_save_theme(dict(STREAMLIT_DEFAULTS), "Theme reset to Streamlit defaults.")
|
|
137
|
+
|
|
138
|
+
st.markdown("---")
|
|
139
|
+
|
|
140
|
+
# ══════════════════════════════════════════════════════════════════════════════
|
|
141
|
+
# Section 1 — Default settings
|
|
142
|
+
# ══════════════════════════════════════════════════════════════════════════════
|
|
143
|
+
st.markdown("## Default Settings")
|
|
144
|
+
st.markdown(
|
|
145
|
+
"These values are pre-filled in the Quick Plot appearance controls "
|
|
146
|
+
"whenever a new session starts."
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
current = get_defaults()
|
|
150
|
+
|
|
151
|
+
with st.form("defaults_form"):
|
|
152
|
+
c1, c2, c3 = st.columns(3)
|
|
153
|
+
|
|
154
|
+
with c1:
|
|
155
|
+
grid = st.checkbox(
|
|
156
|
+
"Show grid by default",
|
|
157
|
+
value=current.get("grid", True),
|
|
158
|
+
)
|
|
159
|
+
grid_linestyle = st.selectbox(
|
|
160
|
+
"Default grid style",
|
|
161
|
+
["--", "-", ":", "-."],
|
|
162
|
+
index=["--", "-", ":", "-."].index(current.get("grid_linestyle", "--")),
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
with c2:
|
|
166
|
+
fontsize = st.slider(
|
|
167
|
+
"Default font size",
|
|
168
|
+
8,
|
|
169
|
+
24,
|
|
170
|
+
value=int(current.get("fontsize", 11)),
|
|
171
|
+
)
|
|
172
|
+
fontfamily = st.selectbox(
|
|
173
|
+
"Default font family",
|
|
174
|
+
_FONT_OPTIONS,
|
|
175
|
+
index=(
|
|
176
|
+
_FONT_OPTIONS.index(current.get("fontfamily", "sans-serif"))
|
|
177
|
+
if current.get("fontfamily", "sans-serif") in _FONT_OPTIONS
|
|
178
|
+
else 0
|
|
179
|
+
),
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
with c3:
|
|
183
|
+
color_palette = st.selectbox(
|
|
184
|
+
"Default color palette",
|
|
185
|
+
COLOR_PALETTE_NAMES,
|
|
186
|
+
index=(
|
|
187
|
+
COLOR_PALETTE_NAMES.index(current.get("color_palette", "Default"))
|
|
188
|
+
if current.get("color_palette", "Default") in COLOR_PALETTE_NAMES
|
|
189
|
+
else 0
|
|
190
|
+
),
|
|
191
|
+
)
|
|
192
|
+
linewidth = st.slider(
|
|
193
|
+
"Default line width",
|
|
194
|
+
0.5,
|
|
195
|
+
6.0,
|
|
196
|
+
value=float(current.get("linewidth", 1.5)),
|
|
197
|
+
step=0.5,
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
c4, c5 = st.columns(2)
|
|
201
|
+
with c4:
|
|
202
|
+
legend_position = st.selectbox(
|
|
203
|
+
"Default legend position",
|
|
204
|
+
[
|
|
205
|
+
"best",
|
|
206
|
+
"upper right",
|
|
207
|
+
"upper left",
|
|
208
|
+
"lower right",
|
|
209
|
+
"lower left",
|
|
210
|
+
"center right",
|
|
211
|
+
"outside right",
|
|
212
|
+
],
|
|
213
|
+
index=[
|
|
214
|
+
"best",
|
|
215
|
+
"upper right",
|
|
216
|
+
"upper left",
|
|
217
|
+
"lower right",
|
|
218
|
+
"lower left",
|
|
219
|
+
"center right",
|
|
220
|
+
"outside right",
|
|
221
|
+
].index(current.get("legend_position", "best")),
|
|
222
|
+
)
|
|
223
|
+
with c5:
|
|
224
|
+
y_notation = st.selectbox(
|
|
225
|
+
"Default Y notation",
|
|
226
|
+
["default", "scientific", "engineering"],
|
|
227
|
+
index=["default", "scientific", "engineering"].index(
|
|
228
|
+
current.get("y_notation", "default")
|
|
229
|
+
),
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
submitted = st.form_submit_button("Save defaults", type="primary")
|
|
233
|
+
|
|
234
|
+
if submitted:
|
|
235
|
+
save_defaults(
|
|
236
|
+
{
|
|
237
|
+
"grid": grid,
|
|
238
|
+
"grid_linestyle": grid_linestyle,
|
|
239
|
+
"fontsize": fontsize,
|
|
240
|
+
"fontfamily": fontfamily,
|
|
241
|
+
"color_palette": color_palette,
|
|
242
|
+
"linewidth": linewidth,
|
|
243
|
+
"legend_position": legend_position,
|
|
244
|
+
"y_notation": y_notation,
|
|
245
|
+
}
|
|
246
|
+
)
|
|
247
|
+
st.session_state.pop("user_defaults_loaded", None)
|
|
248
|
+
st.success("Defaults saved to config.json.")
|
|
249
|
+
|
|
250
|
+
# ── Reset button (outside the form) ──────────────────────────────────────────
|
|
251
|
+
# Each destructive action on this page is disabled until its own confirmation
|
|
252
|
+
# box is ticked. They used to fire on a single click. See audit A-33.
|
|
253
|
+
_confirm_reset = st.checkbox(
|
|
254
|
+
"I understand resetting clears my saved defaults",
|
|
255
|
+
key="confirm_reset_defaults",
|
|
256
|
+
)
|
|
257
|
+
if st.button("Reset defaults to factory settings", disabled=not _confirm_reset):
|
|
258
|
+
save_defaults({})
|
|
259
|
+
st.session_state.pop("user_defaults_loaded", None)
|
|
260
|
+
st.session_state.pop("confirm_reset_defaults", None) # re-arm for next time
|
|
261
|
+
st.success("Defaults cleared.")
|
|
262
|
+
st.rerun()
|
|
263
|
+
|
|
264
|
+
st.markdown("---")
|
|
265
|
+
|
|
266
|
+
# ══════════════════════════════════════════════════════════════════════════════
|
|
267
|
+
# Section 2 — Presets
|
|
268
|
+
# ══════════════════════════════════════════════════════════════════════════════
|
|
269
|
+
st.markdown("## Presets")
|
|
270
|
+
st.markdown(
|
|
271
|
+
"A preset captures the current default settings under a name so you can "
|
|
272
|
+
"switch between configurations quickly (e.g., *Publication*, *Poster*, "
|
|
273
|
+
"*Dark Background*)."
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
tab_save, tab_load, tab_delete = st.tabs(["Save preset", "Load preset", "Delete preset"])
|
|
277
|
+
|
|
278
|
+
# ── Save ──────────────────────────────────────────────────────────────────────
|
|
279
|
+
with tab_save:
|
|
280
|
+
st.markdown(
|
|
281
|
+
"Enter a name and click **Save** to snapshot the current defaults "
|
|
282
|
+
"as a named preset. An existing preset with the same name will be "
|
|
283
|
+
"overwritten."
|
|
284
|
+
)
|
|
285
|
+
preset_name_save = st.text_input(
|
|
286
|
+
"Preset name",
|
|
287
|
+
placeholder="e.g. Publication",
|
|
288
|
+
key="preset_save_name",
|
|
289
|
+
)
|
|
290
|
+
if st.button("Save as preset", type="primary", key="preset_save_btn"):
|
|
291
|
+
if not preset_name_save.strip():
|
|
292
|
+
st.warning("Please enter a preset name.")
|
|
293
|
+
else:
|
|
294
|
+
save_preset(preset_name_save.strip(), get_defaults())
|
|
295
|
+
st.success(f"Preset **{preset_name_save.strip()}** saved.")
|
|
296
|
+
|
|
297
|
+
# ── Load ──────────────────────────────────────────────────────────────────────
|
|
298
|
+
with tab_load:
|
|
299
|
+
presets = list_presets()
|
|
300
|
+
if not presets:
|
|
301
|
+
st.info("No presets saved yet.")
|
|
302
|
+
else:
|
|
303
|
+
preset_name_load = st.selectbox(
|
|
304
|
+
"Choose preset",
|
|
305
|
+
presets,
|
|
306
|
+
key="preset_load_sel",
|
|
307
|
+
)
|
|
308
|
+
if st.button("Load preset", type="primary", key="preset_load_btn"):
|
|
309
|
+
settings = load_preset(preset_name_load)
|
|
310
|
+
save_defaults(settings)
|
|
311
|
+
st.session_state.pop("user_defaults_loaded", None)
|
|
312
|
+
st.success(f"Preset **{preset_name_load}** loaded as defaults.")
|
|
313
|
+
st.rerun()
|
|
314
|
+
|
|
315
|
+
# Preview
|
|
316
|
+
with st.expander("Preview preset values"):
|
|
317
|
+
st.json(load_preset(preset_name_load))
|
|
318
|
+
|
|
319
|
+
# ── Delete ────────────────────────────────────────────────────────────────────
|
|
320
|
+
with tab_delete:
|
|
321
|
+
presets = list_presets()
|
|
322
|
+
if not presets:
|
|
323
|
+
st.info("No presets saved yet.")
|
|
324
|
+
else:
|
|
325
|
+
preset_name_del = st.selectbox(
|
|
326
|
+
"Choose preset to delete",
|
|
327
|
+
presets,
|
|
328
|
+
key="preset_del_sel",
|
|
329
|
+
)
|
|
330
|
+
_confirm_del = st.checkbox(
|
|
331
|
+
f"I understand **{preset_name_del}** will be deleted permanently",
|
|
332
|
+
key="confirm_preset_del",
|
|
333
|
+
)
|
|
334
|
+
if st.button(
|
|
335
|
+
"Delete preset",
|
|
336
|
+
type="primary",
|
|
337
|
+
key="preset_del_btn",
|
|
338
|
+
disabled=not _confirm_del,
|
|
339
|
+
):
|
|
340
|
+
removed = delete_preset(preset_name_del)
|
|
341
|
+
if removed:
|
|
342
|
+
st.session_state.pop("confirm_preset_del", None)
|
|
343
|
+
st.success(f"Preset **{preset_name_del}** deleted.")
|
|
344
|
+
st.rerun()
|
|
345
|
+
else:
|
|
346
|
+
st.error("Preset not found.")
|
|
347
|
+
|
|
348
|
+
st.markdown("---")
|
|
349
|
+
|
|
350
|
+
# ══════════════════════════════════════════════════════════════════════════════
|
|
351
|
+
# Section 3 — Config file info
|
|
352
|
+
# ══════════════════════════════════════════════════════════════════════════════
|
|
353
|
+
st.markdown("## Config File")
|
|
354
|
+
|
|
355
|
+
cfg_path = get_config_path()
|
|
356
|
+
st.markdown(f"**Location:** `{cfg_path}`")
|
|
357
|
+
|
|
358
|
+
if cfg_path.exists():
|
|
359
|
+
st.markdown(
|
|
360
|
+
f"**Size:** {cfg_path.stat().st_size:,} bytes | **Presets saved:** {len(list_presets())}"
|
|
361
|
+
)
|
|
362
|
+
with st.expander("View raw config.json"):
|
|
363
|
+
try:
|
|
364
|
+
raw = json.loads(cfg_path.read_text(encoding="utf-8"))
|
|
365
|
+
st.json(raw)
|
|
366
|
+
except Exception as exc:
|
|
367
|
+
st.error(f"Could not read config.json: {exc}")
|
|
368
|
+
|
|
369
|
+
st.download_button(
|
|
370
|
+
"Download a backup of config.json",
|
|
371
|
+
data=cfg_path.read_bytes(),
|
|
372
|
+
file_name="eagleplot_config_backup.json",
|
|
373
|
+
mime="application/json",
|
|
374
|
+
key="dl_config_backup",
|
|
375
|
+
)
|
|
376
|
+
_confirm_wipe = st.checkbox(
|
|
377
|
+
"I understand this deletes all my defaults **and** presets",
|
|
378
|
+
key="confirm_config_delete",
|
|
379
|
+
)
|
|
380
|
+
if st.button("Delete config.json (reset everything)", disabled=not _confirm_wipe):
|
|
381
|
+
# missing_ok: the file can vanish between the exists() check above and
|
|
382
|
+
# here, which used to surface a raw FileNotFoundError.
|
|
383
|
+
cfg_path.unlink(missing_ok=True)
|
|
384
|
+
st.session_state.pop("confirm_config_delete", None)
|
|
385
|
+
st.success("config.json deleted. All defaults and presets cleared.")
|
|
386
|
+
st.rerun()
|
|
387
|
+
else:
|
|
388
|
+
st.info("No config.json file found — it will be created when you save defaults or a preset.")
|