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,434 @@
1
+ """Data Tools Page.
2
+
3
+ Non-destructive DataFrame transformations: column math, normalization,
4
+ transpose, pivot/melt, filter, sort, merge, fill/drop NaN, resample,
5
+ and rolling/cumulative operations.
6
+
7
+ All operations save the result as a new named dataset in session state
8
+ so the original data is always preserved.
9
+ """
10
+
11
+ from pathlib import Path
12
+ import sys
13
+
14
+ import streamlit as st
15
+ import pandas as pd
16
+
17
+ sys.path.insert(0, str(Path(__file__).parent.parent.parent))
18
+
19
+ from eagleplot.utils.session_state import (
20
+ initialize_session_state,
21
+ add_dataset,
22
+ )
23
+ from eagleplot.data_tools import (
24
+ add_formula_column,
25
+ normalize_column,
26
+ transpose_dataframe,
27
+ pivot_dataframe,
28
+ melt_dataframe,
29
+ filter_rows,
30
+ sort_dataframe,
31
+ merge_dataframes,
32
+ fill_nan,
33
+ drop_nan,
34
+ resample_dataframe,
35
+ rolling_transform,
36
+ )
37
+
38
+ initialize_session_state()
39
+
40
+ st.title("Data Tools")
41
+ st.caption("Non-destructive transformations — each operation creates a new named dataset.")
42
+
43
+ # ── dataset selector ──────────────────────────────────────────────────────────
44
+ dataset_names = list(st.session_state.datasets.keys())
45
+ if not dataset_names:
46
+ st.warning("No dataset loaded. Please upload data on the **Data Upload** page first.")
47
+ st.stop()
48
+
49
+ selected_name = st.selectbox("Source dataset", dataset_names, key="dt_source_ds")
50
+ data = st.session_state.datasets[selected_name]
51
+
52
+ if not isinstance(data, pd.DataFrame):
53
+ st.error(
54
+ "Data Tools requires a **tabular (DataFrame)** dataset. "
55
+ "Please upload a CSV, Excel, TSV, JSON, or Parquet file."
56
+ )
57
+ st.stop()
58
+
59
+ df = data
60
+ st.caption(f"Shape: {df.shape[0]} rows × {df.shape[1]} columns")
61
+
62
+ numeric_cols = df.select_dtypes(include="number").columns.tolist()
63
+ all_cols = df.columns.tolist()
64
+
65
+
66
+ def _save_result(result_df: pd.DataFrame, suffix: str) -> None:
67
+ """Register *result_df* in session state and show a preview."""
68
+ new_name = f"{selected_name.rsplit('.', 1)[0]}_{suffix}"
69
+ add_dataset(new_name, result_df)
70
+ st.session_state["current_dataset"] = new_name
71
+ st.success(f"Saved as **{new_name}** ({result_df.shape[0]} rows × {result_df.shape[1]} cols)")
72
+ st.dataframe(result_df.head(20), width="stretch")
73
+
74
+
75
+ # ── tabs ──────────────────────────────────────────────────────────────────────
76
+ tabs = st.tabs(
77
+ [
78
+ "Column Math",
79
+ "Normalize",
80
+ "Transpose",
81
+ "Pivot / Melt",
82
+ "Filter Rows",
83
+ "Sort",
84
+ "Merge",
85
+ "Fill / Drop NaN",
86
+ "Resample",
87
+ "Rolling / Cumulative",
88
+ ]
89
+ )
90
+
91
+ # ── 1. Column Math ────────────────────────────────────────────────────────────
92
+ with tabs[0]:
93
+ st.subheader("Column Math")
94
+ st.markdown(
95
+ "Compute a new column from a Python/NumPy expression. "
96
+ "Reference columns by name; available functions: "
97
+ "`log`, `log10`, `log2`, `exp`, `sqrt`, `abs`, `sin`, `cos`, `tan`, "
98
+ "`mean`, `std`, `min`, `max`, `sum`, `cumsum`, `diff`."
99
+ )
100
+ st.dataframe(df.head(5), width="stretch")
101
+
102
+ new_col_name = st.text_input("New column name", value="result", key="cm_new_col")
103
+ example_expr = f"{all_cols[0]} * 2" if all_cols else "A * 2"
104
+ expression = st.text_input(
105
+ "Expression",
106
+ value=example_expr,
107
+ help="Example: log(A) / B or (A - mean(A)) / std(A)",
108
+ key="cm_expr",
109
+ )
110
+
111
+ if st.button("Apply", key="cm_apply"):
112
+ try:
113
+ result = add_formula_column(df, new_col_name, expression)
114
+ _save_result(result, f"col_{new_col_name}")
115
+ except Exception as exc:
116
+ st.error(f"Error: {exc}")
117
+
118
+ # ── 2. Normalize ──────────────────────────────────────────────────────────────
119
+ with tabs[1]:
120
+ st.subheader("Normalize Column")
121
+ st.markdown("Scale a numeric column and append the result as a new column.")
122
+
123
+ if not numeric_cols:
124
+ st.warning("No numeric columns found in this dataset.")
125
+ else:
126
+ norm_col = st.selectbox("Column to normalize", numeric_cols, key="norm_col")
127
+ norm_method = st.selectbox(
128
+ "Method",
129
+ ["min-max", "z-score", "pct-max", "area"],
130
+ format_func=lambda m: {
131
+ "min-max": "Min-Max [0, 1]",
132
+ "z-score": "Z-Score (μ=0, σ=1)",
133
+ "pct-max": "% of Max [0, 100%]",
134
+ "area": "Unit Area (∫|y|dx = 1)",
135
+ }[m],
136
+ key="norm_method",
137
+ )
138
+ norm_new_col = st.text_input(
139
+ "Output column name", value=f"{norm_col}_norm", key="norm_new_col"
140
+ )
141
+
142
+ if st.button("Normalize", key="norm_apply"):
143
+ try:
144
+ result = normalize_column(df, norm_col, norm_method, norm_new_col)
145
+ _save_result(result, f"norm_{norm_col}")
146
+ except Exception as exc:
147
+ st.error(f"Error: {exc}")
148
+
149
+ # ── 3. Transpose ──────────────────────────────────────────────────────────────
150
+ with tabs[2]:
151
+ st.subheader("Transpose")
152
+ st.markdown("Swap rows and columns.")
153
+ st.dataframe(df.head(5), width="stretch")
154
+
155
+ if st.button("Transpose", key="tp_apply"):
156
+ try:
157
+ result = transpose_dataframe(df)
158
+ _save_result(result, "transposed")
159
+ except Exception as exc:
160
+ st.error(f"Error: {exc}")
161
+
162
+ # ── 4. Pivot / Melt ───────────────────────────────────────────────────────────
163
+ with tabs[3]:
164
+ st.subheader("Pivot / Melt")
165
+ sub_pivot, sub_melt = st.tabs(["Pivot (long → wide)", "Melt (wide → long)"])
166
+
167
+ with sub_pivot:
168
+ st.markdown(
169
+ "**Pivot** reshapes long-format data into wide format. "
170
+ "Choose the column whose unique values become new column headers."
171
+ )
172
+ piv_index = st.selectbox("Index column (row ID)", all_cols, key="piv_index")
173
+ piv_columns = st.selectbox("Columns column (header values)", all_cols, key="piv_cols")
174
+ piv_values = st.selectbox("Values column", all_cols, key="piv_vals")
175
+
176
+ if st.button("Pivot", key="piv_apply"):
177
+ try:
178
+ result = pivot_dataframe(df, piv_index, piv_columns, piv_values)
179
+ _save_result(result, "pivoted")
180
+ except Exception as exc:
181
+ st.error(f"Error: {exc}")
182
+
183
+ with sub_melt:
184
+ st.markdown(
185
+ "**Melt** reshapes wide-format data into long format. "
186
+ "ID columns are kept; value columns are stacked."
187
+ )
188
+ id_vars = st.multiselect("ID columns (keep as-is)", all_cols, key="melt_id")
189
+ value_vars = st.multiselect(
190
+ "Value columns to melt (leave empty = all remaining)", all_cols, key="melt_vals"
191
+ )
192
+ melt_var_name = st.text_input("Variable column name", value="variable", key="melt_var")
193
+ melt_val_name = st.text_input("Value column name", value="value", key="melt_val")
194
+
195
+ if st.button("Melt", key="melt_apply"):
196
+ try:
197
+ result = melt_dataframe(
198
+ df,
199
+ id_vars=id_vars,
200
+ value_vars=value_vars if value_vars else None,
201
+ var_name=melt_var_name,
202
+ value_name=melt_val_name,
203
+ )
204
+ _save_result(result, "melted")
205
+ except Exception as exc:
206
+ st.error(f"Error: {exc}")
207
+
208
+ # ── 5. Filter Rows ────────────────────────────────────────────────────────────
209
+ with tabs[4]:
210
+ st.subheader("Filter Rows")
211
+ st.markdown(
212
+ "Keep or drop rows matching a condition using pandas `query` syntax. "
213
+ "Examples: `A > 0`, `B == 'control'`, `A > 0 and C < 10`."
214
+ )
215
+ st.caption(f"Available columns: {', '.join(all_cols)}")
216
+
217
+ filt_condition = st.text_input(
218
+ "Condition",
219
+ value=f"{all_cols[0]} > 0" if all_cols else "A > 0",
220
+ key="filt_cond",
221
+ )
222
+ filt_keep = st.radio("Action", ["Keep matching rows", "Drop matching rows"], key="filt_keep")
223
+
224
+ if st.button("Apply Filter", key="filt_apply"):
225
+ try:
226
+ keep = filt_keep == "Keep matching rows"
227
+ result = filter_rows(df, filt_condition, keep=keep)
228
+ st.info(
229
+ f"Rows before: {len(df)} → after: {len(result)} "
230
+ f"({'kept' if keep else 'dropped'} {abs(len(df) - len(result))} rows)"
231
+ )
232
+ _save_result(result, "filtered")
233
+ except Exception as exc:
234
+ st.error(f"Error: {exc}")
235
+
236
+ # ── 6. Sort ───────────────────────────────────────────────────────────────────
237
+ with tabs[5]:
238
+ st.subheader("Sort")
239
+ sort_by = st.multiselect("Sort by (in order)", all_cols, default=all_cols[:1], key="sort_by")
240
+ sort_asc = st.radio("Direction", ["Ascending ↑", "Descending ↓"], key="sort_dir")
241
+
242
+ if st.button("Sort", key="sort_apply"):
243
+ if not sort_by:
244
+ st.warning("Select at least one column to sort by.")
245
+ else:
246
+ try:
247
+ result = sort_dataframe(df, sort_by, ascending=(sort_asc == "Ascending ↑"))
248
+ _save_result(result, "sorted")
249
+ except Exception as exc:
250
+ st.error(f"Error: {exc}")
251
+
252
+ # ── 7. Merge ──────────────────────────────────────────────────────────────────
253
+ with tabs[6]:
254
+ st.subheader("Merge / Join")
255
+ st.markdown("Join the current dataset with a second loaded dataset on a shared key column.")
256
+
257
+ other_options = [n for n in dataset_names if n != selected_name]
258
+ if not other_options:
259
+ st.info("Load at least **two** datasets to use the Merge tool.")
260
+ else:
261
+ right_name = st.selectbox("Right dataset", other_options, key="merge_right")
262
+ right_df = st.session_state.datasets[right_name]
263
+
264
+ if not isinstance(right_df, pd.DataFrame):
265
+ st.error("Right dataset must be a DataFrame.")
266
+ else:
267
+ common_cols = list(set(all_cols) & set(right_df.columns.tolist()))
268
+ if not common_cols:
269
+ st.warning(
270
+ "No columns in common between the two datasets. "
271
+ "Ensure both have at least one column with the same name to join on."
272
+ )
273
+ else:
274
+ merge_key = st.selectbox("Join key column", common_cols, key="merge_key")
275
+ merge_how = st.selectbox(
276
+ "Join type", ["inner", "left", "right", "outer"], key="merge_how"
277
+ )
278
+
279
+ if st.button("Merge", key="merge_apply"):
280
+ try:
281
+ result = merge_dataframes(df, right_df, on=merge_key, how=merge_how)
282
+ _save_result(result, "merged")
283
+ except Exception as exc:
284
+ st.error(f"Error: {exc}")
285
+
286
+ # ── 8. Fill / Drop NaN ────────────────────────────────────────────────────────
287
+ with tabs[7]:
288
+ st.subheader("Fill / Drop NaN")
289
+
290
+ nan_counts = df.isna().sum()
291
+ total_nan = int(nan_counts.sum())
292
+ st.metric("Total NaN cells", total_nan)
293
+ if total_nan > 0:
294
+ nan_table = nan_counts[nan_counts > 0].rename("NaN count").to_frame()
295
+ st.dataframe(nan_table, width="content")
296
+
297
+ nan_op = st.radio("Operation", ["Fill NaN", "Drop rows with NaN"], key="nan_op")
298
+
299
+ if nan_op == "Fill NaN":
300
+ fill_cols = st.multiselect(
301
+ "Columns to fill (empty = all numeric)", numeric_cols, key="fill_cols"
302
+ )
303
+ fill_method = st.selectbox(
304
+ "Fill strategy",
305
+ ["mean", "median", "zero", "interpolate", "forward", "backward"],
306
+ format_func=lambda m: {
307
+ "mean": "Column mean",
308
+ "median": "Column median",
309
+ "zero": "Zero (0)",
310
+ "interpolate": "Linear interpolation",
311
+ "forward": "Forward-fill",
312
+ "backward": "Backward-fill",
313
+ }[m],
314
+ key="fill_method",
315
+ )
316
+
317
+ if st.button("Fill NaN", key="fill_apply"):
318
+ try:
319
+ cols = fill_cols if fill_cols else None
320
+ result = fill_nan(df, columns=cols, method=fill_method)
321
+ remaining = int(result.isna().sum().sum())
322
+ st.info(f"NaN cells before: {total_nan} → after: {remaining}")
323
+ _save_result(result, "filled")
324
+ except Exception as exc:
325
+ st.error(f"Error: {exc}")
326
+
327
+ else:
328
+ drop_cols = st.multiselect(
329
+ "Only check these columns (empty = any column)", all_cols, key="drop_cols"
330
+ )
331
+
332
+ if st.button("Drop rows with NaN", key="drop_apply"):
333
+ try:
334
+ cols = drop_cols if drop_cols else None
335
+ result = drop_nan(df, columns=cols)
336
+ st.info(f"Rows before: {len(df)} → after: {len(result)}")
337
+ _save_result(result, "dropna")
338
+ except Exception as exc:
339
+ st.error(f"Error: {exc}")
340
+
341
+ # ── 9. Resample ───────────────────────────────────────────────────────────────
342
+ with tabs[8]:
343
+ st.subheader("Resample")
344
+ st.markdown(
345
+ "Interpolate selected y-columns onto a new, uniformly-spaced x-grid. "
346
+ "Useful for aligning spectra collected at different wavelength spacings."
347
+ )
348
+
349
+ if len(numeric_cols) < 2:
350
+ st.warning("Need at least 2 numeric columns (one x, one y) to resample.")
351
+ else:
352
+ rs_x = st.selectbox("X column (independent variable)", numeric_cols, key="rs_x")
353
+ rs_y = st.multiselect(
354
+ "Y columns to resample",
355
+ [c for c in numeric_cols if c != rs_x],
356
+ default=[c for c in numeric_cols if c != rs_x][:1],
357
+ key="rs_y",
358
+ )
359
+ rs_n = st.number_input(
360
+ "Number of output points",
361
+ min_value=10,
362
+ max_value=10000,
363
+ value=min(500, len(df)),
364
+ step=10,
365
+ key="rs_n",
366
+ )
367
+ rs_method = st.selectbox(
368
+ "Interpolation method", ["cubic", "linear", "nearest"], key="rs_method"
369
+ )
370
+
371
+ x_vals = df[rs_x].dropna()
372
+ rs_xmin = st.number_input("X min", value=float(x_vals.min()), key="rs_xmin")
373
+ rs_xmax = st.number_input("X max", value=float(x_vals.max()), key="rs_xmax")
374
+
375
+ if st.button("Resample", key="rs_apply"):
376
+ if not rs_y:
377
+ st.warning("Select at least one Y column.")
378
+ else:
379
+ try:
380
+ result = resample_dataframe(
381
+ df,
382
+ rs_x,
383
+ rs_y,
384
+ n_points=int(rs_n),
385
+ method=rs_method,
386
+ x_min=rs_xmin,
387
+ x_max=rs_xmax,
388
+ )
389
+ _save_result(result, "resampled")
390
+ except Exception as exc:
391
+ st.error(f"Error: {exc}")
392
+
393
+ # ── 10. Rolling / Cumulative ──────────────────────────────────────────────────
394
+ with tabs[9]:
395
+ st.subheader("Rolling / Cumulative")
396
+ st.markdown(
397
+ "Apply a sliding-window or cumulative transform to selected columns. "
398
+ "The result is appended as new column(s) in a copy of the dataset."
399
+ )
400
+
401
+ if not numeric_cols:
402
+ st.warning("No numeric columns found.")
403
+ else:
404
+ roll_cols = st.multiselect(
405
+ "Columns to transform", numeric_cols, default=numeric_cols[:1], key="roll_cols"
406
+ )
407
+ roll_op = st.selectbox(
408
+ "Operation",
409
+ ["rolling_mean", "rolling_sum", "cumsum", "cumprod"],
410
+ format_func=lambda m: {
411
+ "rolling_mean": "Rolling mean",
412
+ "rolling_sum": "Rolling sum",
413
+ "cumsum": "Cumulative sum",
414
+ "cumprod": "Cumulative product",
415
+ }[m],
416
+ key="roll_op",
417
+ )
418
+ roll_window = 5
419
+ if roll_op in ("rolling_mean", "rolling_sum"):
420
+ roll_window = int(
421
+ st.number_input(
422
+ "Window size", min_value=1, max_value=len(df), value=5, step=1, key="roll_win"
423
+ )
424
+ )
425
+
426
+ if st.button("Apply", key="roll_apply"):
427
+ if not roll_cols:
428
+ st.warning("Select at least one column.")
429
+ else:
430
+ try:
431
+ result = rolling_transform(df, roll_cols, operation=roll_op, window=roll_window)
432
+ _save_result(result, f"roll_{roll_op}")
433
+ except Exception as exc:
434
+ st.error(f"Error: {exc}")