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,325 @@
1
+ """Batch Analysis Page.
2
+
3
+ Run statistical summaries, curve fits, and peak analysis across multiple
4
+ loaded datasets simultaneously. Workflow presets let you save and recall
5
+ named analysis configurations that persist across app restarts via config.json.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from pathlib import Path
11
+ import sys
12
+ import json
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 initialize_session_state
20
+ from eagleplot.batch import (
21
+ batch_statistics,
22
+ batch_curve_fit,
23
+ batch_peak_analysis,
24
+ )
25
+ from eagleplot.utils.user_settings import (
26
+ list_workflows,
27
+ save_workflow,
28
+ load_workflow,
29
+ delete_workflow,
30
+ )
31
+
32
+ initialize_session_state()
33
+
34
+ st.title("Batch Analysis")
35
+ st.caption(
36
+ "Run the same analysis across multiple loaded datasets at once. "
37
+ "Workflow presets let you save and reload named analysis configurations."
38
+ )
39
+
40
+ # ── helpers ───────────────────────────────────────────────────────────────────
41
+
42
+ _RESULT_OUTPUT_COLS = [
43
+ "dataset",
44
+ "column",
45
+ "n",
46
+ "mean",
47
+ "median",
48
+ "std",
49
+ "min",
50
+ "max",
51
+ "q1",
52
+ "q3",
53
+ "iqr",
54
+ "range",
55
+ ]
56
+
57
+
58
+ def _csv_download(df: pd.DataFrame, filename: str, label: str = "Download CSV") -> None:
59
+ """Render a CSV download button for *df*."""
60
+ csv_bytes = df.to_csv(index=False).encode("utf-8")
61
+ st.download_button(label, data=csv_bytes, file_name=filename, mime="text/csv")
62
+
63
+
64
+ # ── top-level tabs ─────────────────────────────────────────────────────────────
65
+ tab_analysis, tab_workflows = st.tabs(["Batch Analysis", "Workflow Presets"])
66
+
67
+ # ══════════════════════════════════════════════════════════════════════════════
68
+ # TAB 1 — Batch Analysis
69
+ # ══════════════════════════════════════════════════════════════════════════════
70
+ with tab_analysis:
71
+ # Dataset selection — only DataFrames
72
+ all_datasets = st.session_state.get("datasets", {})
73
+ df_datasets = {k: v for k, v in all_datasets.items() if isinstance(v, pd.DataFrame)}
74
+
75
+ if not df_datasets:
76
+ st.warning(
77
+ "No tabular datasets loaded. "
78
+ "Please upload CSV / Excel / TSV / JSON / Parquet files on the "
79
+ "**Data Upload** page first."
80
+ )
81
+ st.stop()
82
+
83
+ selected_names = st.multiselect(
84
+ "Select datasets to analyse",
85
+ list(df_datasets.keys()),
86
+ default=list(df_datasets.keys()),
87
+ key="ba_selected_datasets",
88
+ )
89
+
90
+ if not selected_names:
91
+ st.info("Select at least one dataset above to continue.")
92
+ st.stop()
93
+
94
+ selected_datasets: dict[str, pd.DataFrame] = {k: df_datasets[k] for k in selected_names}
95
+
96
+ # Common columns across all selected datasets
97
+ common_cols = set.intersection(*(set(df.columns.tolist()) for df in selected_datasets.values()))
98
+ common_cols_list = sorted(common_cols)
99
+
100
+ # ── sub-tabs ──────────────────────────────────────────────────────────────
101
+ sub_stats, sub_fit, sub_peaks = st.tabs(["Statistics", "Curve Fit", "Peak Analysis"])
102
+
103
+ # ── Statistics ────────────────────────────────────────────────────────────
104
+ with sub_stats:
105
+ st.subheader("Batch Statistics")
106
+ st.markdown(
107
+ "Compute descriptive statistics (mean, median, std, quartiles, …) "
108
+ "for every selected dataset."
109
+ )
110
+
111
+ col_filter = st.multiselect(
112
+ "Column filter (leave blank for all columns)",
113
+ common_cols_list,
114
+ key="ba_stats_cols",
115
+ help="Only columns common to all selected datasets are shown here. "
116
+ "Leave empty to include all columns in each dataset.",
117
+ )
118
+
119
+ if st.button("Run Statistics", key="ba_stats_run"):
120
+ cols_arg = col_filter if col_filter else None
121
+ try:
122
+ result_df = batch_statistics(selected_datasets, columns=cols_arg)
123
+ st.success(f"Statistics computed for {len(selected_names)} dataset(s).")
124
+ st.dataframe(result_df, width="stretch")
125
+ _csv_download(result_df, "batch_statistics.csv")
126
+ except Exception as exc:
127
+ st.error(f"Error: {exc}")
128
+
129
+ # ── Curve Fit ─────────────────────────────────────────────────────────────
130
+ with sub_fit:
131
+ st.subheader("Batch Curve Fit")
132
+ st.markdown("Fit the same model to the same x/y columns across all selected datasets.")
133
+
134
+ fit_x_col = st.text_input("X column name", value="x", key="ba_fit_x")
135
+ fit_y_col = st.text_input("Y column name", value="y", key="ba_fit_y")
136
+ fit_type = st.selectbox(
137
+ "Fit type",
138
+ ["linear", "polynomial", "exponential"],
139
+ key="ba_fit_type",
140
+ )
141
+
142
+ fit_degree = 2
143
+ if fit_type == "polynomial":
144
+ fit_degree = int(
145
+ st.number_input(
146
+ "Polynomial degree",
147
+ min_value=2,
148
+ max_value=5,
149
+ value=2,
150
+ step=1,
151
+ key="ba_fit_degree",
152
+ )
153
+ )
154
+
155
+ if st.button("Run Curve Fit", key="ba_fit_run"):
156
+ if not fit_x_col.strip() or not fit_y_col.strip():
157
+ st.warning("Enter both an X column name and a Y column name.")
158
+ else:
159
+ try:
160
+ result_df = batch_curve_fit(
161
+ selected_datasets,
162
+ x_col=fit_x_col.strip(),
163
+ y_col=fit_y_col.strip(),
164
+ fit_type=fit_type,
165
+ degree=fit_degree,
166
+ )
167
+ st.success(f"Curve fit complete for {len(selected_names)} dataset(s).")
168
+ st.dataframe(result_df, width="stretch")
169
+ _csv_download(result_df, "batch_curve_fit.csv")
170
+ except Exception as exc:
171
+ st.error(f"Error: {exc}")
172
+
173
+ # ── Peak Analysis ─────────────────────────────────────────────────────────
174
+ with sub_peaks:
175
+ st.subheader("Batch Peak Analysis")
176
+ st.markdown(
177
+ "Detect and characterise peaks in the same column across all selected datasets."
178
+ )
179
+
180
+ pa_y_col = st.text_input("Y column (signal)", value="y", key="ba_pa_y")
181
+ pa_x_col = st.text_input(
182
+ "X column (optional — leave blank to use row indices)",
183
+ value="",
184
+ key="ba_pa_x",
185
+ )
186
+
187
+ pa_use_height = st.checkbox("Set minimum height", value=False, key="ba_pa_use_height")
188
+ pa_height: float | None = None
189
+ if pa_use_height:
190
+ pa_height = st.number_input(
191
+ "Minimum height (absolute)",
192
+ value=0.0,
193
+ step=0.01,
194
+ format="%.4f",
195
+ key="ba_pa_height",
196
+ )
197
+
198
+ pa_use_prominence = st.checkbox("Set minimum prominence", value=False, key="ba_pa_use_prom")
199
+ pa_prominence: float | None = None
200
+ if pa_use_prominence:
201
+ pa_prominence = st.number_input(
202
+ "Minimum prominence (absolute)",
203
+ value=0.0,
204
+ step=0.01,
205
+ format="%.4f",
206
+ key="ba_pa_prominence",
207
+ )
208
+
209
+ pa_distance = int(
210
+ st.number_input(
211
+ "Minimum distance between peaks (samples)",
212
+ min_value=1,
213
+ value=1,
214
+ step=1,
215
+ key="ba_pa_distance",
216
+ )
217
+ )
218
+
219
+ if st.button("Run Peak Analysis", key="ba_pa_run"):
220
+ if not pa_y_col.strip():
221
+ st.warning("Enter a Y column name.")
222
+ else:
223
+ try:
224
+ result_df = batch_peak_analysis(
225
+ selected_datasets,
226
+ y_col=pa_y_col.strip(),
227
+ x_col=pa_x_col.strip() if pa_x_col.strip() else None,
228
+ height=pa_height,
229
+ prominence=pa_prominence,
230
+ distance=pa_distance,
231
+ )
232
+ st.success(f"Peak analysis complete for {len(selected_names)} dataset(s).")
233
+ st.dataframe(result_df, width="stretch")
234
+ _csv_download(result_df, "batch_peak_analysis.csv")
235
+ except Exception as exc:
236
+ st.error(f"Error: {exc}")
237
+
238
+
239
+ # ══════════════════════════════════════════════════════════════════════════════
240
+ # TAB 2 — Workflow Presets
241
+ # ══════════════════════════════════════════════════════════════════════════════
242
+ with tab_workflows:
243
+ st.subheader("Workflow Presets")
244
+ st.info(
245
+ "Workflows are saved in **config.json** and persist across app restarts. "
246
+ "A workflow is a named list of analysis steps stored as JSON."
247
+ )
248
+
249
+ _TEMPLATE_STEPS = [
250
+ {"operation": "batch_statistics", "params": {"columns": []}},
251
+ {
252
+ "operation": "batch_curve_fit",
253
+ "params": {"x_col": "x", "y_col": "y", "fit_type": "linear"},
254
+ },
255
+ ]
256
+
257
+ # ── Load section ──────────────────────────────────────────────────────────
258
+ st.markdown("### Load a Workflow")
259
+ existing_workflows = list_workflows()
260
+
261
+ if not existing_workflows:
262
+ st.caption("No workflows saved yet. Use the **Save** section below to create one.")
263
+ else:
264
+ load_name = st.selectbox("Select workflow", existing_workflows, key="wf_load_name")
265
+ if st.button("Load workflow description", key="wf_load_btn"):
266
+ try:
267
+ steps = load_workflow(load_name)
268
+ st.json(steps)
269
+ except Exception as exc:
270
+ st.error(f"Could not load workflow: {exc}")
271
+
272
+ st.divider()
273
+
274
+ # ── Save section ──────────────────────────────────────────────────────────
275
+ st.markdown("### Save a Workflow")
276
+
277
+ wf_save_name = st.text_input(
278
+ "Workflow name",
279
+ value="my_workflow",
280
+ key="wf_save_name",
281
+ help="Use a descriptive name; spaces and special characters are allowed.",
282
+ )
283
+ wf_json_text = st.text_area(
284
+ "Workflow steps (JSON list)",
285
+ value=json.dumps(_TEMPLATE_STEPS, indent=2),
286
+ height=200,
287
+ key="wf_json_text",
288
+ help="Each step is an object with an 'operation' key and a 'params' dict.",
289
+ )
290
+
291
+ if st.button("Save Workflow", key="wf_save_btn"):
292
+ name = wf_save_name.strip()
293
+ if not name:
294
+ st.warning("Enter a workflow name.")
295
+ else:
296
+ try:
297
+ steps = json.loads(wf_json_text)
298
+ if not isinstance(steps, list):
299
+ st.error("Workflow steps must be a JSON **list** (array).")
300
+ else:
301
+ save_workflow(name, steps)
302
+ st.success(f"Workflow **{name}** saved.")
303
+ st.rerun()
304
+ except json.JSONDecodeError as exc:
305
+ st.error(f"Invalid JSON: {exc}")
306
+ except Exception as exc:
307
+ st.error(f"Error saving workflow: {exc}")
308
+
309
+ st.divider()
310
+
311
+ # ── Delete section ────────────────────────────────────────────────────────
312
+ st.markdown("### Delete a Workflow")
313
+
314
+ existing_for_delete = list_workflows()
315
+ if not existing_for_delete:
316
+ st.caption("No workflows to delete.")
317
+ else:
318
+ del_name = st.selectbox("Select workflow to delete", existing_for_delete, key="wf_del_name")
319
+ if st.button("Delete", key="wf_del_btn", type="primary"):
320
+ removed = delete_workflow(del_name)
321
+ if removed:
322
+ st.success(f"Workflow **{del_name}** deleted.")
323
+ st.rerun()
324
+ else:
325
+ st.error(f"Workflow **{del_name}** not found.")
@@ -0,0 +1,199 @@
1
+ """Help & Documentation page."""
2
+
3
+ import sys
4
+ from pathlib import Path
5
+ import streamlit as st
6
+
7
+ sys.path.insert(0, str(Path(__file__).parent.parent.parent))
8
+
9
+ from eagleplot.utils.session_state import initialize_session_state
10
+
11
+ initialize_session_state()
12
+
13
+ st.title("Help")
14
+ st.caption("Documentation, quick reference, and tips for using EaglePlot.")
15
+
16
+ tab_start, tab_plots, tab_analysis, tab_formats, tab_tips = st.tabs(
17
+ ["Getting Started", "Plot Types", "Analysis Tools", "File Formats", "Tips & Tricks"]
18
+ )
19
+
20
+ with tab_start:
21
+ st.markdown("""
22
+ ## Workflow
23
+
24
+ 1. **Upload Data** — Go to *Data Upload* and upload a file, or load a built-in example dataset.
25
+ 2. **Quick Plot** — Choose from 27 plot types, configure parameters interactively
26
+ in the sidebar.
27
+ 3. **Analysis Tools** — Statistical tests, curve fitting, signal processing,
28
+ peak analysis, and more.
29
+ 4. **Export Results** — Download plots as PNG/PDF/SVG, data as CSV/JSON,
30
+ or save your full session.
31
+
32
+ ## Navigation
33
+
34
+ All pages are listed in the left sidebar. Your session data (loaded datasets, plot history)
35
+ persists as you navigate between pages.
36
+
37
+ ## Example Datasets
38
+
39
+ Go to *Data Upload → Example Datasets* to load built-in artificial datasets
40
+ — no file needed. Each one is designed to demonstrate specific plot types.
41
+ """)
42
+
43
+ with tab_plots:
44
+ st.markdown("## 27 Plot Types")
45
+
46
+ col1, col2, col3 = st.columns(3)
47
+ with col1:
48
+ st.markdown("""
49
+ **Matplotlib (static)**
50
+ - Histogram
51
+ - Line Plot
52
+ - Scatter Plot
53
+ - Bar Chart
54
+ - Heatmap
55
+ - Contour Plot
56
+ - Waterfall Plot
57
+ - Dual Axis Plot
58
+ - Broken Axis Plot
59
+ - Inset Plot
60
+ - Z-Colored Scatter
61
+ - Bubble Chart
62
+ - Polar Plot
63
+ - 2D Histogram / Hexbin
64
+ - Residual Plot
65
+ - Scatter with Regression
66
+ """)
67
+ with col2:
68
+ st.markdown("""
69
+ **Seaborn (statistical)**
70
+ - Distribution Plot
71
+ - Box / Violin / Swarm Plot
72
+ - Regression Plot
73
+ - Pair Plot
74
+
75
+ **Plotly (interactive)**
76
+ - Interactive Histogram
77
+ - Interactive Scatter
78
+ - Interactive Line
79
+ - Interactive Heatmap
80
+ - Interactive 3D Surface
81
+ - Interactive 3D Scatter
82
+ - Interactive Ternary
83
+ """)
84
+ with col3:
85
+ st.markdown("""
86
+ **Choosing the right plot**
87
+
88
+ | Goal | Plot type |
89
+ |---|---|
90
+ | Single distribution | Histogram |
91
+ | Two-variable relationship | Scatter |
92
+ | Time series | Line Plot |
93
+ | Compare groups | Box / Violin |
94
+ | 2D density | 2D Histogram |
95
+ | 3-component data | Ternary |
96
+ | 3D exploration | 3D Scatter |
97
+ | Matrix data | Heatmap |
98
+ | Compositions | Polar |
99
+ """)
100
+
101
+ st.markdown("""
102
+ **Zoom and pan on static plots:** Click *Convert to Plotly* below any matplotlib
103
+ figure in Quick Plot to get an interactive version.
104
+ """)
105
+
106
+ with tab_analysis:
107
+ st.markdown("""
108
+ ## Analysis Tools (page 3)
109
+
110
+ | Tab | Capabilities |
111
+ |---|---|
112
+ | Statistics | Descriptive stats, normality tests, distribution fitting |
113
+ | Curve Fitting | Linear, polynomial, exponential, custom functions |
114
+ | Signal Processing | Smoothing, filtering, FFT, baseline correction, derivatives |
115
+ | Peak Analysis | Auto peak finding, FWHM, integration, Gaussian/Lorentzian/Voigt fitting |
116
+ | Statistical Tests | t-tests, ANOVA (one-way, two-way), Mann-Whitney, Tukey HSD, chi-square |
117
+ | Optimization | Function minimization, root finding |
118
+ | Linear Algebra | Eigenvalues, linear systems, matrix decomposition |
119
+
120
+ ## Spectroscopy (page 10)
121
+
122
+ | Tab | Capabilities |
123
+ |---|---|
124
+ | IR / Raman | ATR correction, spectral subtraction, band assignment, cosmic ray removal |
125
+ | NMR | Chemical shift calibration, line broadening, FFT, peak picking, integration |
126
+ | UV-Vis | Beer-Lambert law, molar absorptivity, spectral overlap integral |
127
+ | Mass Spec | m/z peak finding, centroid spectrum |
128
+
129
+ Also includes NIST WebBook lookup (fetch IR spectra by CAS number).
130
+
131
+ ## Data Tools (page 9)
132
+
133
+ Formula columns, normalization, transpose, pivot/melt, filter, sort, merge,
134
+ fill/drop NaN, resample, rolling transform.
135
+ """)
136
+
137
+ with tab_formats:
138
+ st.markdown("""
139
+ ## Supported Input Formats
140
+
141
+ | Format | Extensions | Notes |
142
+ |---|---|---|
143
+ | CSV | .csv | Auto-detected delimiter |
144
+ | TSV | .tsv | Tab-separated |
145
+ | Excel | .xlsx, .xls | First sheet by default |
146
+ | JSON | .json | Records or columns orientation |
147
+ | Parquet | .parquet | Apache Parquet columnar |
148
+ | NumPy | .npy, .npz | Single array or dict of arrays |
149
+ | Pickle | .pkl | Any Python object |
150
+ | JCAMP-DX | .jdx, .dx | Spectroscopy (requires `jcamp`) |
151
+ | HDF5 | .h5, .hdf5 | Hierarchical data (requires `h5py`) |
152
+ | NetCDF | .nc, .cdf | Climate/scientific arrays (requires `xarray`) |
153
+ | SPC | .spc | Thermo spectroscopy (requires `spc`) |
154
+ | ASC | .asc | Generic whitespace-delimited text |
155
+ | mzML/mzXML | .mzml, .mzxml | Mass spectrometry (requires `pymzml`) |
156
+
157
+ ## Export Formats
158
+
159
+ **Figures:** PNG (150/300 DPI), SVG, PDF, HTML (Plotly only)
160
+ **Data:** CSV, JSON, NPY, PKL
161
+ **Reports:** Multi-page PDF via Export Results page
162
+ **Session:** Full session save/load (JSON)
163
+ """)
164
+
165
+ with tab_tips:
166
+ col1, col2 = st.columns(2)
167
+ with col1:
168
+ st.markdown("### Session Management")
169
+ st.markdown("""
170
+ - Datasets persist as you navigate between pages
171
+ - Use *Export Results → Save Session* to preserve work between restarts
172
+ - Reload a session from the same Export Results page
173
+ - Plot history and analysis results are included in saved sessions
174
+ """)
175
+
176
+ st.markdown("### Publication Figures")
177
+ st.markdown("""
178
+ - Apply *Publication Style* in the Quick Plot sidebar
179
+ - Export at 300 DPI from Export Results for print quality
180
+ - Use SVG export for vector graphics in manuscripts
181
+ - The colorblind-safe palette (Okabe-Ito) is available in Color Palette settings
182
+ """)
183
+
184
+ with col2:
185
+ st.markdown("### Performance")
186
+ st.markdown("""
187
+ - Files >50 MB or >10,000 rows show a downsampled preview automatically
188
+ - Full data is always stored; only the preview is reduced
189
+ - Interactive Plotly plots are slower than Matplotlib for large datasets
190
+ - Clear plot history in Export Results to free memory
191
+ """)
192
+
193
+ st.markdown("### Plugins")
194
+ st.markdown("""
195
+ - Drop `plugin_*.py` files into the `plugins/` directory
196
+ - Plugins are auto-discovered at startup
197
+ - See `plugins/plugin_example.py` for the plugin template
198
+ - Plugin status is shown in Settings (page 6)
199
+ """)