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,76 @@
1
+ """Utility modules for Streamlit GUI."""
2
+
3
+ from .session_state import (
4
+ initialize_session_state,
5
+ add_dataset,
6
+ get_current_dataset,
7
+ get_dataset,
8
+ delete_dataset,
9
+ add_plot_to_history,
10
+ clear_plot_history,
11
+ add_analysis_result,
12
+ save_session_to_file,
13
+ load_session_from_file,
14
+ clear_session,
15
+ get_session_summary,
16
+ )
17
+
18
+ from .data_preview import (
19
+ preview_dataframe,
20
+ get_dataframe_info,
21
+ get_array_info,
22
+ format_data_size,
23
+ display_dataset_card,
24
+ get_column_suggestions,
25
+ display_data_preview,
26
+ get_plottable_arrays,
27
+ )
28
+
29
+ from .user_settings import (
30
+ get_config_path,
31
+ load_config,
32
+ save_config,
33
+ get_defaults,
34
+ save_defaults,
35
+ list_presets,
36
+ save_preset,
37
+ load_preset,
38
+ delete_preset,
39
+ SAVEABLE_KEYS,
40
+ )
41
+
42
+ __all__ = [
43
+ # Session state
44
+ "initialize_session_state",
45
+ "add_dataset",
46
+ "get_current_dataset",
47
+ "get_dataset",
48
+ "delete_dataset",
49
+ "add_plot_to_history",
50
+ "clear_plot_history",
51
+ "add_analysis_result",
52
+ "save_session_to_file",
53
+ "load_session_from_file",
54
+ "clear_session",
55
+ "get_session_summary",
56
+ # Data preview
57
+ "preview_dataframe",
58
+ "get_dataframe_info",
59
+ "get_array_info",
60
+ "format_data_size",
61
+ "display_dataset_card",
62
+ "get_column_suggestions",
63
+ "display_data_preview",
64
+ "get_plottable_arrays",
65
+ # User settings
66
+ "get_config_path",
67
+ "load_config",
68
+ "save_config",
69
+ "get_defaults",
70
+ "save_defaults",
71
+ "list_presets",
72
+ "save_preset",
73
+ "load_preset",
74
+ "delete_preset",
75
+ "SAVEABLE_KEYS",
76
+ ]
@@ -0,0 +1,77 @@
1
+ """Gates for GUI features that reach outside the browser sandbox.
2
+
3
+ EaglePlot is meant to be run locally (``launch.bat`` / ``launch.command``), where
4
+ reading a folder on the user's own disk is the point. On a shared deployment the
5
+ same feature lets any visitor list and read files on the *server*. See audit A-15.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import os
11
+ import warnings
12
+ from typing import Mapping, Optional
13
+
14
+ #: Environment variable that overrides the folder-loading default.
15
+ #: ``1``/``true``/``yes``/``on`` always allow; ``0``/``false``/``no``/``off`` always deny.
16
+ FOLDER_LOAD_ENV = "EAGLEPLOT_ALLOW_FOLDER_LOAD"
17
+ #: Name used before the Plottle -> EaglePlot rename (3.0.0). Still honoured, with a
18
+ #: deprecation warning, when the new name is not set. Remove in 4.0.
19
+ LEGACY_FOLDER_LOAD_ENV = "PLOTTLE_ALLOW_FOLDER_LOAD"
20
+
21
+ _TRUE = {"1", "true", "yes", "on"}
22
+ _FALSE = {"0", "false", "no", "off"}
23
+
24
+
25
+ def folder_loading_allowed(env_value: Optional[str], client_ip: Optional[str]) -> bool:
26
+ """Decide whether the GUI may read a folder path typed by the user.
27
+
28
+ Parameters
29
+ ----------
30
+ env_value : str or None
31
+ Value of :data:`FOLDER_LOAD_ENV`, or ``None`` if unset.
32
+ client_ip : str or None
33
+ ``st.context.ip_address``. Streamlit reports ``None`` for a connection
34
+ from ``localhost`` and the remote address otherwise.
35
+
36
+ Returns
37
+ -------
38
+ bool
39
+ An explicit setting wins. Unset or unrecognised, folder loading is allowed
40
+ only for a local connection. The IP check alone is not a security control
41
+ (a reverse proxy on the same host looks local), so shared deployments
42
+ should set ``EAGLEPLOT_ALLOW_FOLDER_LOAD=0``.
43
+ """
44
+ if env_value is not None:
45
+ value = env_value.strip().lower()
46
+ if value in _TRUE:
47
+ return True
48
+ if value in _FALSE:
49
+ return False
50
+ return client_ip is None
51
+
52
+
53
+ def _client_ip() -> Optional[str]:
54
+ """``st.context.ip_address`` for the current session (``None`` = localhost)."""
55
+ import streamlit as st
56
+
57
+ return st.context.ip_address
58
+
59
+
60
+ def folder_loading_allowed_here(environ: Optional[Mapping[str, str]] = None) -> bool:
61
+ """:func:`folder_loading_allowed` for the current Streamlit session."""
62
+ env = os.environ if environ is None else environ
63
+ return folder_loading_allowed(folder_load_setting(env), _client_ip())
64
+
65
+
66
+ def folder_load_setting(env: Mapping[str, str]) -> Optional[str]:
67
+ """The folder-loading setting from *env*, accepting the pre-3.0 variable name."""
68
+ value = env.get(FOLDER_LOAD_ENV)
69
+ if value is None and LEGACY_FOLDER_LOAD_ENV in env:
70
+ warnings.warn(
71
+ f"{LEGACY_FOLDER_LOAD_ENV} is deprecated; set {FOLDER_LOAD_ENV} instead "
72
+ "(Plottle was renamed EaglePlot in 3.0).",
73
+ DeprecationWarning,
74
+ stacklevel=2,
75
+ )
76
+ value = env[LEGACY_FOLDER_LOAD_ENV]
77
+ return value
@@ -0,0 +1,352 @@
1
+ """Data Preview Utilities for Streamlit GUI.
2
+
3
+ This module provides helper functions for displaying data previews,
4
+ summaries, and metadata in the Streamlit interface.
5
+
6
+ Functions
7
+ ---------
8
+ preview_dataframe(df, n_rows=10)
9
+ Display DataFrame preview with first and last rows
10
+ get_dataframe_info(df)
11
+ Get DataFrame information (shape, types, memory)
12
+ get_array_info(array)
13
+ Get NumPy array information (shape, dtype, stats)
14
+ format_data_size(size_bytes)
15
+ Format data size in human-readable format
16
+ display_dataset_card(name, data, metadata)
17
+ Display a dataset summary card in Streamlit
18
+ get_column_suggestions(df)
19
+ Suggest numeric/categorical columns for plotting
20
+
21
+ Examples
22
+ --------
23
+ >>> import streamlit as st
24
+ >>> import pandas as pd
25
+ >>> from eagleplot.utils.data_preview import preview_dataframe, get_dataframe_info
26
+ >>> df = pd.DataFrame({'a': [1, 2, 3], 'b': [4, 5, 6]})
27
+ >>> info = get_dataframe_info(df)
28
+ >>> st.write(info)
29
+ """
30
+
31
+ import streamlit as st
32
+ import numpy as np
33
+ import pandas as pd
34
+ from typing import Any, Dict, List, Tuple, Optional
35
+
36
+
37
+ def preview_dataframe(df: pd.DataFrame, n_rows: int = 10) -> pd.DataFrame:
38
+ """Display DataFrame preview with first and last rows.
39
+
40
+ Parameters
41
+ ----------
42
+ df : pd.DataFrame
43
+ DataFrame to preview
44
+ n_rows : int, default 10
45
+ Number of rows to show from head and tail
46
+
47
+ Returns
48
+ -------
49
+ preview : pd.DataFrame
50
+ Preview DataFrame (first n_rows + last n_rows if applicable)
51
+ """
52
+ if len(df) <= 2 * n_rows:
53
+ return df
54
+ else:
55
+ # Show first n_rows and last n_rows with separator
56
+ head = df.head(n_rows)
57
+ tail = df.tail(n_rows)
58
+ return pd.concat([head, tail])
59
+
60
+
61
+ @st.cache_data(show_spinner=False)
62
+ def get_dataframe_info(df: pd.DataFrame) -> Dict:
63
+ """Get DataFrame information (shape, types, memory).
64
+
65
+ Parameters
66
+ ----------
67
+ df : pd.DataFrame
68
+ DataFrame to analyze
69
+
70
+ Returns
71
+ -------
72
+ info : dict
73
+ Dictionary with DataFrame metadata:
74
+ - shape: (rows, cols)
75
+ - columns: list of column names
76
+ - dtypes: dict of column types
77
+ - memory_usage: total memory in bytes
78
+ - missing_values: dict of missing counts per column
79
+ - numeric_columns: list of numeric column names
80
+ - categorical_columns: list of object/category column names
81
+ """
82
+ info = {
83
+ "shape": df.shape,
84
+ "columns": list(df.columns),
85
+ "dtypes": {col: str(dtype) for col, dtype in df.dtypes.items()},
86
+ "memory_usage": df.memory_usage(deep=True).sum(),
87
+ "missing_values": df.isnull().sum().to_dict(),
88
+ "numeric_columns": list(df.select_dtypes(include=[np.number]).columns),
89
+ "categorical_columns": list(df.select_dtypes(include=["object", "category"]).columns),
90
+ }
91
+
92
+ return info
93
+
94
+
95
+ @st.cache_data(show_spinner=False)
96
+ def get_array_info(array: np.ndarray) -> Dict:
97
+ """Get NumPy array information (shape, dtype, stats).
98
+
99
+ Parameters
100
+ ----------
101
+ array : np.ndarray
102
+ Array to analyze
103
+
104
+ Returns
105
+ -------
106
+ info : dict
107
+ Dictionary with array metadata:
108
+ - shape: array shape
109
+ - dtype: data type
110
+ - size: total number of elements
111
+ - memory_usage: total memory in bytes
112
+ - ndim: number of dimensions
113
+ - min: minimum value (if numeric)
114
+ - max: maximum value (if numeric)
115
+ - mean: mean value (if numeric)
116
+ - std: standard deviation (if numeric)
117
+ """
118
+ info = {
119
+ "shape": array.shape,
120
+ "dtype": str(array.dtype),
121
+ "size": array.size,
122
+ "memory_usage": array.nbytes,
123
+ "ndim": array.ndim,
124
+ }
125
+
126
+ # Add statistics for numeric arrays
127
+ if np.issubdtype(array.dtype, np.number):
128
+ try:
129
+ info["min"] = float(np.min(array))
130
+ info["max"] = float(np.max(array))
131
+ info["mean"] = float(np.mean(array))
132
+ info["std"] = float(np.std(array))
133
+ except Exception:
134
+ pass
135
+
136
+ return info
137
+
138
+
139
+ def format_data_size(size_bytes: int) -> str:
140
+ """Format data size in human-readable format.
141
+
142
+ Parameters
143
+ ----------
144
+ size_bytes : int
145
+ Size in bytes
146
+
147
+ Returns
148
+ -------
149
+ formatted : str
150
+ Human-readable size (e.g., "1.5 MB")
151
+ """
152
+ for unit in ["B", "KB", "MB", "GB"]:
153
+ if size_bytes < 1024.0:
154
+ return f"{size_bytes:.1f} {unit}"
155
+ size_bytes /= 1024.0
156
+ return f"{size_bytes:.1f} TB"
157
+
158
+
159
+ def display_dataset_card(name: str, data: Any, metadata: Optional[Dict] = None):
160
+ """Display a dataset summary card in Streamlit.
161
+
162
+ Parameters
163
+ ----------
164
+ name : str
165
+ Dataset name
166
+ data : Any
167
+ Dataset (DataFrame, ndarray, etc.)
168
+ metadata : dict, optional
169
+ Additional metadata to display
170
+ """
171
+ with st.container():
172
+ st.subheader(f"📊 {name}")
173
+
174
+ if isinstance(data, pd.DataFrame):
175
+ info = get_dataframe_info(data)
176
+
177
+ col1, col2, col3 = st.columns(3)
178
+ col1.metric("Rows", info["shape"][0])
179
+ col2.metric("Columns", info["shape"][1])
180
+ col3.metric("Memory", format_data_size(info["memory_usage"]))
181
+
182
+ st.write("**Column Types:**")
183
+ dtypes_df = pd.DataFrame(
184
+ [{"Column": col, "Type": dtype} for col, dtype in info["dtypes"].items()]
185
+ )
186
+ st.dataframe(dtypes_df, hide_index=True, width="stretch")
187
+
188
+ # Show missing values if any
189
+ missing_total = sum(info["missing_values"].values())
190
+ if missing_total > 0:
191
+ st.warning(f"⚠️ {missing_total} missing values found")
192
+
193
+ elif isinstance(data, np.ndarray):
194
+ info = get_array_info(data)
195
+
196
+ col1, col2, col3 = st.columns(3)
197
+ col1.metric("Shape", str(info["shape"]))
198
+ col2.metric("Type", info["dtype"])
199
+ col3.metric("Memory", format_data_size(info["memory_usage"]))
200
+
201
+ if "mean" in info:
202
+ st.write("**Statistics:**")
203
+ col1, col2, col3, col4 = st.columns(4)
204
+ col1.metric("Min", f"{info['min']:.4g}")
205
+ col2.metric("Max", f"{info['max']:.4g}")
206
+ col3.metric("Mean", f"{info['mean']:.4g}")
207
+ col4.metric("Std Dev", f"{info['std']:.4g}")
208
+
209
+ else:
210
+ st.write(f"**Type:** {type(data).__name__}")
211
+ if hasattr(data, "__len__"):
212
+ st.write(f"**Length:** {len(data)}")
213
+
214
+ # Display additional metadata if provided
215
+ if metadata:
216
+ with st.expander("Additional Metadata"):
217
+ st.json(metadata)
218
+
219
+
220
+ def get_column_suggestions(df: pd.DataFrame) -> Dict[str, List[str]]:
221
+ """Suggest numeric/categorical columns for plotting.
222
+
223
+ Parameters
224
+ ----------
225
+ df : pd.DataFrame
226
+ DataFrame to analyze
227
+
228
+ Returns
229
+ -------
230
+ suggestions : dict
231
+ Dictionary with suggestions:
232
+ - x_candidates: columns suitable for X-axis
233
+ - y_candidates: columns suitable for Y-axis
234
+ - hue_candidates: columns suitable for grouping/color
235
+ """
236
+ numeric_cols = list(df.select_dtypes(include=[np.number]).columns)
237
+ categorical_cols = list(df.select_dtypes(include=["object", "category"]).columns)
238
+
239
+ # For X-axis: prefer numeric or datetime
240
+ x_candidates = numeric_cols.copy()
241
+
242
+ # For Y-axis: numeric only
243
+ y_candidates = numeric_cols.copy()
244
+
245
+ # For hue/grouping: categorical or low-cardinality numeric
246
+ hue_candidates = categorical_cols.copy()
247
+ for col in numeric_cols:
248
+ if df[col].nunique() <= 10: # Low cardinality
249
+ hue_candidates.append(col)
250
+
251
+ return {
252
+ "x_candidates": x_candidates,
253
+ "y_candidates": y_candidates,
254
+ "hue_candidates": hue_candidates,
255
+ "numeric": numeric_cols,
256
+ "categorical": categorical_cols,
257
+ }
258
+
259
+
260
+ def display_data_preview(data: Any, name: str = "Data"):
261
+ """Display an intelligent preview of any data type.
262
+
263
+ Parameters
264
+ ----------
265
+ data : Any
266
+ Data to preview
267
+ name : str, default "Data"
268
+ Name to display in preview
269
+ """
270
+ st.write(f"### {name} Preview")
271
+
272
+ if isinstance(data, pd.DataFrame):
273
+ st.write(f"**Shape:** {data.shape[0]} rows × {data.shape[1]} columns")
274
+
275
+ # Show preview
276
+ preview = preview_dataframe(data, n_rows=5)
277
+ st.dataframe(preview, width="stretch")
278
+
279
+ # Quick stats for numeric columns
280
+ numeric_cols = data.select_dtypes(include=[np.number]).columns
281
+ if len(numeric_cols) > 0:
282
+ with st.expander("Quick Statistics"):
283
+ st.dataframe(data[numeric_cols].describe(), width="stretch")
284
+
285
+ elif isinstance(data, np.ndarray):
286
+ info = get_array_info(data)
287
+ st.write(f"**Shape:** {info['shape']}, **Type:** {info['dtype']}")
288
+
289
+ # For 1D or 2D arrays, show a sample
290
+ if data.ndim == 1:
291
+ st.write("**First 10 elements:**")
292
+ st.write(data[:10])
293
+ elif data.ndim == 2:
294
+ st.write("**First 5 rows:**")
295
+ st.write(data[:5])
296
+ else:
297
+ st.write(f"**{data.ndim}D array** - too complex to preview directly")
298
+
299
+ elif isinstance(data, dict):
300
+ st.write(f"**Dictionary with {len(data)} keys:**")
301
+ st.json({k: str(v)[:100] for k, v in list(data.items())[:10]})
302
+
303
+ elif isinstance(data, (list, tuple)):
304
+ st.write(f"**{type(data).__name__} with {len(data)} elements:**")
305
+ st.write(data[:10] if len(data) > 10 else data)
306
+
307
+ else:
308
+ st.write(f"**Type:** {type(data).__name__}")
309
+ st.write(data)
310
+
311
+
312
+ def get_plottable_arrays(data: Any) -> Tuple[Optional[np.ndarray], Optional[List[str]]]:
313
+ """Extract plottable arrays from various data types.
314
+
315
+ Parameters
316
+ ----------
317
+ data : Any
318
+ Input data (DataFrame, ndarray, dict, etc.)
319
+
320
+ Returns
321
+ -------
322
+ arrays : np.ndarray or None
323
+ Extracted numeric arrays, or None if not applicable
324
+ labels : list of str or None
325
+ Column/array labels, or None
326
+ """
327
+ if isinstance(data, pd.DataFrame):
328
+ numeric_cols = data.select_dtypes(include=[np.number]).columns
329
+ if len(numeric_cols) > 0:
330
+ return data[numeric_cols].values, list(numeric_cols)
331
+ return None, None
332
+
333
+ elif isinstance(data, np.ndarray):
334
+ if np.issubdtype(data.dtype, np.number):
335
+ return data, None
336
+ return None, None
337
+
338
+ elif isinstance(data, dict):
339
+ # Try to extract numeric arrays from dict
340
+ numeric_items = {
341
+ k: v
342
+ for k, v in data.items()
343
+ if isinstance(v, (np.ndarray, list))
344
+ and len(v) > 0
345
+ and isinstance(v[0] if isinstance(v, list) else v.flat[0], (int, float, np.number))
346
+ }
347
+ if numeric_items:
348
+ arrays = np.column_stack(list(numeric_items.values()))
349
+ return arrays, list(numeric_items.keys())
350
+ return None, None
351
+
352
+ return None, None