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.
Files changed (73) hide show
  1. eagleplot/Home.py +362 -0
  2. eagleplot/__init__.py +19 -0
  3. eagleplot/annotations.py +215 -0
  4. eagleplot/assets/logo.ico +0 -0
  5. eagleplot/assets/logo.png +0 -0
  6. eagleplot/assets/nccu-horiz-logo.png +0 -0
  7. eagleplot/assets/nccu-wings.png +0 -0
  8. eagleplot/batch.py +446 -0
  9. eagleplot/cli.py +719 -0
  10. eagleplot/data_tools.py +849 -0
  11. eagleplot/example-data/Artificial/bimodal_distribution.csv +501 -0
  12. eagleplot/example-data/Artificial/correlation_matrix.npy +0 -0
  13. eagleplot/example-data/Artificial/gaussian_surface.npy +0 -0
  14. eagleplot/example-data/Artificial/grouped_categorical.csv +241 -0
  15. eagleplot/example-data/Artificial/ir_spectrum.csv +1802 -0
  16. eagleplot/example-data/Artificial/molecular_properties.csv +121 -0
  17. eagleplot/example-data/Artificial/normal_distribution.csv +401 -0
  18. eagleplot/example-data/Artificial/reaction_kinetics.csv +121 -0
  19. eagleplot/example-data/Artificial/scatter_correlation.csv +201 -0
  20. eagleplot/example-data/Artificial/sine_cosine_waves.csv +301 -0
  21. eagleplot/gallery/box_plot.png +0 -0
  22. eagleplot/gallery/contour_plot.png +0 -0
  23. eagleplot/gallery/distribution_plot.png +0 -0
  24. eagleplot/gallery/heatmap.png +0 -0
  25. eagleplot/gallery/histogram.png +0 -0
  26. eagleplot/gallery/interactive_3d_surface.png +0 -0
  27. eagleplot/gallery/interactive_heatmap.png +0 -0
  28. eagleplot/gallery/interactive_histogram.png +0 -0
  29. eagleplot/gallery/interactive_line.png +0 -0
  30. eagleplot/gallery/interactive_scatter.png +0 -0
  31. eagleplot/gallery/line_plot.png +0 -0
  32. eagleplot/gallery/manifest.json +196 -0
  33. eagleplot/gallery/regression_plot.png +0 -0
  34. eagleplot/gallery/scatter_plot.png +0 -0
  35. eagleplot/io.py +1008 -0
  36. eagleplot/math.py +1210 -0
  37. eagleplot/molecular/__init__.py +21 -0
  38. eagleplot/molecular/atom_data.py +287 -0
  39. eagleplot/molecular/parsers.py +747 -0
  40. eagleplot/nist.py +241 -0
  41. eagleplot/pages/10_Molecular_Viz.py +225 -0
  42. eagleplot/pages/11_Batch_Analysis.py +325 -0
  43. eagleplot/pages/12_Help.py +199 -0
  44. eagleplot/pages/13_Settings.py +388 -0
  45. eagleplot/pages/1_Data_Upload.py +521 -0
  46. eagleplot/pages/2_Quick_Plot.py +1693 -0
  47. eagleplot/pages/3_Analysis_Tools.py +2510 -0
  48. eagleplot/pages/4_Multi_Plot_Dashboard.py +791 -0
  49. eagleplot/pages/5_Advanced_Plotting.py +691 -0
  50. eagleplot/pages/6_Export_Results.py +442 -0
  51. eagleplot/pages/7_Gallery.py +201 -0
  52. eagleplot/pages/8_Data_Tools.py +434 -0
  53. eagleplot/pages/9_Spectroscopy.py +821 -0
  54. eagleplot/pages/__init__.py +8 -0
  55. eagleplot/peaks.py +513 -0
  56. eagleplot/plotting.py +2630 -0
  57. eagleplot/plugin_loader.py +208 -0
  58. eagleplot/report.py +448 -0
  59. eagleplot/signal.py +494 -0
  60. eagleplot/spectroscopy.py +830 -0
  61. eagleplot/utils/__init__.py +76 -0
  62. eagleplot/utils/access.py +77 -0
  63. eagleplot/utils/data_preview.py +352 -0
  64. eagleplot/utils/plot_config.py +2721 -0
  65. eagleplot/utils/session_state.py +535 -0
  66. eagleplot/utils/theming.py +312 -0
  67. eagleplot/utils/user_settings.py +294 -0
  68. eagleplot-3.0.0.dist-info/METADATA +304 -0
  69. eagleplot-3.0.0.dist-info/RECORD +73 -0
  70. eagleplot-3.0.0.dist-info/WHEEL +5 -0
  71. eagleplot-3.0.0.dist-info/entry_points.txt +2 -0
  72. eagleplot-3.0.0.dist-info/licenses/LICENSE +21 -0
  73. 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.")