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
eagleplot/Home.py ADDED
@@ -0,0 +1,362 @@
1
+ """Graphical User Interface Module - Main Entry Point.
2
+
3
+ This module provides the main entry point for the Streamlit-based GUI for
4
+ the EaglePlot toolkit. It provides an interactive interface for data
5
+ exploration, visualization, and analysis.
6
+
7
+ Navigation Structure
8
+ --------------------
9
+ Home
10
+ Data Upload
11
+ Plot
12
+ Basic — Quick Plot (27 plot types, Matplotlib/Seaborn/Plotly)
13
+ Multiplot — Multi-Plot Dashboard (grid layouts, axis sharing)
14
+ Advanced
15
+ ↳ Advanced Plotting — Seaborn statistical + Plotly interactive
16
+ ↳ Spectroscopy — IR/Raman, NMR, UV-Vis, Mass Spec + NIST
17
+ ↳ Molecular Viz — 3D structure and vibrational modes
18
+ Analyze
19
+ Single — Analysis Tools (stats, curve fit, signal, peaks, …)
20
+ Batch — Batch Analysis + workflow presets
21
+ Export
22
+ Gallery
23
+ Help
24
+ Settings
25
+
26
+ Usage
27
+ -----
28
+ streamlit run eagleplot/Home.py
29
+ """
30
+
31
+ import base64
32
+ import sys
33
+ import streamlit as st
34
+ from pathlib import Path
35
+
36
+ _REPO_ROOT = Path(__file__).resolve().parent.parent
37
+ sys.path.insert(0, str(_REPO_ROOT))
38
+
39
+ _PAGES_DIR = Path(__file__).resolve().parent / "pages"
40
+
41
+ # Assets are inside the package (not at the repo root) so that they survive
42
+ # `pip install` -- see audit A-09. Resolving them relative to the repo root meant
43
+ # a pip-installed GUI came up with no logo and no NCCU branding.
44
+ _ASSETS_DIR = Path(__file__).resolve().parent / "assets"
45
+ _LOGO_PNG = _ASSETS_DIR / "logo.png"
46
+ _NCCU_HORIZ = _ASSETS_DIR / "nccu-horiz-logo.png"
47
+ _NCCU_WINGS = _ASSETS_DIR / "nccu-wings.png"
48
+
49
+ from eagleplot import __version__ # noqa: E402
50
+ from eagleplot.utils import initialize_session_state, get_session_summary # noqa: E402
51
+ from eagleplot.utils.theming import app_css # noqa: E402
52
+
53
+ try:
54
+ from PIL import Image as _PILImage
55
+
56
+ _page_icon = _PILImage.open(_LOGO_PNG) if _LOGO_PNG.exists() else "📊"
57
+ except ImportError:
58
+ _page_icon = "📊"
59
+
60
+ # ── Page configuration (must be the first Streamlit call) ─────────────────────
61
+ st.set_page_config(
62
+ page_title="EaglePlot",
63
+ page_icon=_page_icon,
64
+ layout="wide",
65
+ initial_sidebar_state="expanded",
66
+ menu_items={
67
+ "About": (
68
+ f"**EaglePlot** v{__version__}\n\n"
69
+ "Scientific data visualization and analysis toolkit for research "
70
+ "and teaching in computational science — developed at North Carolina "
71
+ "Central University.\n\n"
72
+ "**Institution:** NCCU Department of Chemistry and Biochemistry"
73
+ )
74
+ },
75
+ )
76
+
77
+ # ── App-wide styling, derived from the active theme ───────────────────────────
78
+ # Colours come from eagleplot.utils.theming.app_css() rather than being hardcoded, so
79
+ # the stylesheet follows whatever the user selects in Settings. Previously the NCCU
80
+ # dark palette was baked in as literals, which left the hero subtitle
81
+ # white-on-white on a light theme (A-17).
82
+ st.markdown(app_css(), unsafe_allow_html=True)
83
+
84
+ initialize_session_state()
85
+
86
+
87
+ # ── Home page content ─────────────────────────────────────────────────────────
88
+
89
+
90
+ def _home_page() -> None:
91
+ """Render the Dashboard / Help landing page."""
92
+ tab_dash, tab_help = st.tabs(["Dashboard", "Help"])
93
+
94
+ with tab_dash:
95
+ # ── Hero ──────────────────────────────────────────────────────────────
96
+ _logo_col, _title_col = st.columns([1, 5], vertical_alignment="center")
97
+ with _logo_col:
98
+ if _LOGO_PNG.exists():
99
+ _b64 = base64.b64encode(_LOGO_PNG.read_bytes()).decode()
100
+ st.markdown(
101
+ f'<img src="data:image/png;base64,{_b64}" alt="EaglePlot logo"'
102
+ ' style="width:110px;height:auto;display:block;">',
103
+ unsafe_allow_html=True,
104
+ )
105
+ with _title_col:
106
+ st.markdown(
107
+ "<h1 style='margin-bottom: 0.1rem;'>EaglePlot</h1>",
108
+ unsafe_allow_html=True,
109
+ )
110
+ st.markdown(
111
+ "<p class='eagleplot-muted'>"
112
+ "Scientific data visualization and analysis "
113
+ f"&nbsp;&middot;&nbsp; v{__version__}"
114
+ "</p>",
115
+ unsafe_allow_html=True,
116
+ )
117
+
118
+ st.divider()
119
+
120
+ # ── Session metrics ───────────────────────────────────────────────────
121
+ summary = get_session_summary()
122
+ col1, col2, col3 = st.columns(3)
123
+ col1.metric("Datasets", summary["num_datasets"])
124
+ col2.metric("Plots", summary["num_plots"])
125
+ col3.metric("Analyses", summary["num_analyses"])
126
+
127
+ if summary["current_dataset"]:
128
+ st.success(f"Active dataset: **{summary['current_dataset']}**")
129
+ st.page_link(basic_pg, label="Go to Quick Plot →")
130
+ else:
131
+ st.info("No dataset loaded.")
132
+ st.page_link(data_pg, label="Upload Data →")
133
+
134
+ with tab_help:
135
+ st.markdown("## Quick Start")
136
+ st.markdown("""
137
+ 1. **Upload Data** — Go to *Data Upload* to load a CSV, Excel, NumPy, or other file.
138
+ Or load a built-in example dataset.
139
+ 2. **Plot** — *Plot → Basic* lets you choose from 27 plot types and configure them
140
+ interactively.
141
+ 3. **Analyse** — *Analyze → Single* has curve fitting, statistics, signal processing,
142
+ peak analysis, and more.
143
+ 4. **Export** — *Export* saves plots as PNG/PDF/SVG and data in multiple formats.
144
+ """)
145
+
146
+ st.markdown("## Supported File Formats")
147
+ st.markdown("""
148
+ | Format | Extensions |
149
+ |---|---|
150
+ | Tabular | CSV, TSV, Excel (.xlsx/.xls), JSON, Parquet |
151
+ | NumPy | .npy, .npz |
152
+ | Python | Pickle (.pkl) |
153
+ | Spectroscopy | JCAMP-DX (.jdx/.dx), SPC (.spc), ASC (.asc) |
154
+ | Scientific | HDF5 (.h5/.hdf5), NetCDF (.nc/.cdf), mzML (.mzml/.mzxml) |
155
+ """)
156
+
157
+ st.markdown("## Tips")
158
+ col1, col2 = st.columns(2)
159
+ with col1:
160
+ with st.expander("Session management"):
161
+ st.markdown("""
162
+ - Datasets persist across all pages during a session
163
+ - Use *Export → Save Session* to preserve your work between restarts
164
+ - Saved sessions can be reloaded from the same page
165
+ """)
166
+ with st.expander("Performance"):
167
+ st.markdown("""
168
+ - Files >50 MB or >10k rows get a downsampled preview automatically
169
+ - Interactive Plotly plots are slower than Matplotlib for large data
170
+ - Clear plot history periodically to free memory
171
+ """)
172
+ with col2:
173
+ with st.expander("Choosing a plot type"):
174
+ st.markdown("""
175
+ - **Histogram** — single-variable distributions
176
+ - **Scatter** — relationship between two variables
177
+ - **Line** — time series or ordered data
178
+ - **Box / Violin** — compare distributions across groups
179
+ - **Heatmap** — 2D matrix or correlation data
180
+ - **Ternary** — 3-component compositions
181
+ """)
182
+ with st.expander("Publication figures"):
183
+ st.markdown("""
184
+ - Apply *Publication Style* in the style sidebar for paper-ready figures
185
+ - Export at 300 DPI from *Export*
186
+ - Use SVG export for vector graphics in manuscripts
187
+ """)
188
+
189
+ st.markdown("## About")
190
+ st.markdown(f"""
191
+ **EaglePlot** is developed at North Carolina Central University for research and teaching
192
+ in computational science.
193
+ Built with Streamlit · NumPy · Pandas · Matplotlib · Seaborn · Plotly · SciPy.
194
+ Version {__version__}
195
+ """)
196
+
197
+
198
+ # ── Page objects ──────────────────────────────────────────────────────────────
199
+
200
+ home_pg = st.Page(_home_page, title="Home", default=True)
201
+ data_pg = st.Page(
202
+ str(_PAGES_DIR / "1_Data_Upload.py"),
203
+ title="Data Upload",
204
+ url_path="data-upload",
205
+ )
206
+
207
+ # Plot group
208
+ basic_pg = st.Page(
209
+ str(_PAGES_DIR / "2_Quick_Plot.py"),
210
+ title="Basic",
211
+ url_path="plot-basic",
212
+ )
213
+ multi_pg = st.Page(
214
+ str(_PAGES_DIR / "4_Multi_Plot_Dashboard.py"),
215
+ title="Multiplot",
216
+ url_path="plot-multiplot",
217
+ )
218
+ adv_plt_pg = st.Page(
219
+ str(_PAGES_DIR / "5_Advanced_Plotting.py"),
220
+ title="Advanced Plotting",
221
+ url_path="plot-advanced",
222
+ )
223
+ spec_pg = st.Page(
224
+ str(_PAGES_DIR / "9_Spectroscopy.py"),
225
+ title="Spectroscopy",
226
+ url_path="plot-spectroscopy",
227
+ )
228
+ molvis_pg = st.Page(
229
+ str(_PAGES_DIR / "10_Molecular_Viz.py"),
230
+ title="Molecular Viz",
231
+ url_path="plot-molecular-viz",
232
+ )
233
+
234
+ # Analyze group
235
+ single_pg = st.Page(
236
+ str(_PAGES_DIR / "3_Analysis_Tools.py"),
237
+ title="Single",
238
+ url_path="analyze-single",
239
+ )
240
+ batch_pg = st.Page(
241
+ str(_PAGES_DIR / "11_Batch_Analysis.py"),
242
+ title="Batch",
243
+ url_path="analyze-batch",
244
+ )
245
+
246
+ # Top-level pages
247
+ export_pg = st.Page(
248
+ str(_PAGES_DIR / "6_Export_Results.py"),
249
+ title="Export",
250
+ url_path="export",
251
+ )
252
+ gallery_pg = st.Page(
253
+ str(_PAGES_DIR / "7_Gallery.py"),
254
+ title="Gallery",
255
+ url_path="gallery",
256
+ )
257
+ help_pg = st.Page(
258
+ str(_PAGES_DIR / "12_Help.py"),
259
+ title="Help",
260
+ url_path="help",
261
+ )
262
+ settings_pg = st.Page(
263
+ str(_PAGES_DIR / "13_Settings.py"),
264
+ title="Settings",
265
+ url_path="settings",
266
+ )
267
+
268
+ datatools_pg = st.Page(
269
+ str(_PAGES_DIR / "8_Data_Tools.py"),
270
+ title="Data Tools",
271
+ url_path="analyze-data-tools",
272
+ )
273
+
274
+ # ── Navigation (position="hidden" — custom sidebar built below) ───────────────
275
+
276
+ _PLOT_PAGES = {basic_pg, multi_pg, adv_plt_pg, spec_pg, molvis_pg}
277
+ _ANALYZE_PAGES = {single_pg, batch_pg, datatools_pg}
278
+
279
+ pg = st.navigation(
280
+ [
281
+ home_pg,
282
+ data_pg,
283
+ basic_pg,
284
+ multi_pg,
285
+ adv_plt_pg,
286
+ spec_pg,
287
+ molvis_pg,
288
+ single_pg,
289
+ batch_pg,
290
+ datatools_pg,
291
+ export_pg,
292
+ gallery_pg,
293
+ help_pg,
294
+ settings_pg,
295
+ ],
296
+ position="hidden",
297
+ )
298
+
299
+
300
+ # ── Custom sidebar navigation ─────────────────────────────────────────────────
301
+
302
+ _ADV_HEADER = "<p class='eagleplot-section-label'>Advanced</p>"
303
+
304
+ with st.sidebar:
305
+ if _LOGO_PNG.exists():
306
+ _sb_logo_b64 = base64.b64encode(_LOGO_PNG.read_bytes()).decode()
307
+ st.markdown(
308
+ f'<img src="data:image/png;base64,{_sb_logo_b64}" alt="EaglePlot logo"'
309
+ ' style="width:72px;height:auto;display:block;margin:0.5rem auto 1rem;">',
310
+ unsafe_allow_html=True,
311
+ )
312
+ st.page_link(home_pg, label="Home")
313
+ st.page_link(data_pg, label="Data Upload")
314
+
315
+ with st.expander("**Plot**", expanded=pg in _PLOT_PAGES):
316
+ st.page_link(basic_pg, label="Basic")
317
+ st.page_link(multi_pg, label="Multiplot")
318
+ st.markdown(_ADV_HEADER, unsafe_allow_html=True)
319
+ st.page_link(adv_plt_pg, label="↳ Advanced Plotting")
320
+ st.page_link(spec_pg, label="↳ Spectroscopy")
321
+ st.page_link(molvis_pg, label="↳ Molecular Viz")
322
+
323
+ with st.expander("**Analyze**", expanded=pg in _ANALYZE_PAGES):
324
+ st.page_link(single_pg, label="Single")
325
+ st.page_link(batch_pg, label="Batch")
326
+ st.page_link(datatools_pg, label="Data Tools")
327
+
328
+ st.divider()
329
+ st.page_link(export_pg, label="Export")
330
+ st.page_link(gallery_pg, label="Gallery")
331
+ st.page_link(help_pg, label="Help")
332
+ st.page_link(settings_pg, label="Settings")
333
+
334
+ st.divider()
335
+ with st.expander("About", expanded=True):
336
+ if _NCCU_HORIZ.exists():
337
+ _horiz_b64 = base64.b64encode(_NCCU_HORIZ.read_bytes()).decode()
338
+ st.markdown(
339
+ f'<img src="data:image/png;base64,{_horiz_b64}"'
340
+ ' alt="North Carolina Central University"'
341
+ ' style="width:100%;height:auto;margin-bottom:0.4rem;">',
342
+ unsafe_allow_html=True,
343
+ )
344
+ if _NCCU_WINGS.exists():
345
+ _wings_b64 = base64.b64encode(_NCCU_WINGS.read_bytes()).decode()
346
+ st.markdown(
347
+ f'<img src="data:image/png;base64,{_wings_b64}"'
348
+ ' alt="NCCU Eagle Wings emblem"'
349
+ ' style="width:60%;height:auto;display:block;margin:0 auto 0.4rem;">',
350
+ unsafe_allow_html=True,
351
+ )
352
+ st.markdown(
353
+ "**EaglePlot** is a scientific data visualization and analysis "
354
+ "toolkit developed for research and teaching in computational "
355
+ "science at North Carolina Central University."
356
+ )
357
+ st.markdown("[NCCU Schultz Lab on GitHub](https://github.com/The-Schultz-Lab)")
358
+
359
+
360
+ # ── Execute the active page ───────────────────────────────────────────────────
361
+
362
+ pg.run()
eagleplot/__init__.py ADDED
@@ -0,0 +1,19 @@
1
+ """Modules Package.
2
+
3
+ This package contains the core functionality for the EaglePlot toolkit.
4
+
5
+ Available Modules
6
+ -----------------
7
+ io : Data input/output operations
8
+ math : Mathematical and statistical functions
9
+ plotting : Visualization and plotting tools
10
+ gui : Interactive GUI components
11
+ """
12
+
13
+ # Single source of truth for the version. `pyproject.toml` reads this via
14
+ # `[tool.setuptools.dynamic]`, and cli.py / Home.py / docs/conf.py import it, so
15
+ # the number cannot drift between the package, the CLI, and the GUI.
16
+ # See audit A-26.
17
+ __version__ = "3.0.0"
18
+
19
+ __all__ = ["io", "math", "plotting", "gui", "__version__"]
@@ -0,0 +1,215 @@
1
+ """Figure annotation helpers.
2
+
3
+ Provides ``apply_annotations`` for drawing overlay annotations
4
+ (reference lines, spans, text labels, and shapes) onto a Matplotlib Axes.
5
+
6
+ Used by Quick Plot (per-figure annotations) and the Multi-Panel Dashboard
7
+ (per-cell annotations before combined export).
8
+
9
+ Supported overlay types
10
+ -----------------------
11
+ ``hline``
12
+ Horizontal reference line at *y*.
13
+ Keys: ``y``, ``color``, ``linestyle``, ``lw``, ``label``
14
+
15
+ ``vline``
16
+ Vertical reference line at *x*.
17
+ Keys: ``x``, ``color``, ``linestyle``, ``lw``, ``label``
18
+
19
+ ``hspan``
20
+ Horizontal band shading between *y1* and *y2*.
21
+ Keys: ``y1``, ``y2``, ``color``, ``alpha``, ``label``
22
+
23
+ ``vspan``
24
+ Vertical band shading between *x1* and *x2*.
25
+ Keys: ``x1``, ``x2``, ``color``, ``alpha``, ``label``
26
+
27
+ ``text``
28
+ Text label placed at (*x*, *y*), with optional annotating arrow.
29
+ Keys: ``x``, ``y``, ``text``, ``color``, ``fontsize``,
30
+ ``arrow`` (bool), ``tx``, ``ty`` (arrow-tail coordinates)
31
+
32
+ ``rectangle``
33
+ Filled/outlined rectangle spanning (x1, y1) → (x2, y2).
34
+ Keys: ``x1``, ``y1``, ``x2``, ``y2``,
35
+ ``color``, ``facecolor``, ``lw``, ``alpha``, ``label``
36
+
37
+ ``ellipse``
38
+ Filled/outlined ellipse centred at (*cx*, *cy*).
39
+ Keys: ``cx``, ``cy``, ``width``, ``height``,
40
+ ``color``, ``facecolor``, ``lw``, ``alpha``, ``label``
41
+ """
42
+
43
+ from __future__ import annotations
44
+
45
+ from typing import TYPE_CHECKING
46
+
47
+ if TYPE_CHECKING: # pragma: no cover
48
+ from matplotlib.axes import Axes
49
+
50
+ # Colour choices available in the GUI colour picker
51
+ ANNOTATION_COLORS: list[str] = [
52
+ "red",
53
+ "blue",
54
+ "green",
55
+ "orange",
56
+ "purple",
57
+ "black",
58
+ "gray",
59
+ "cyan",
60
+ "magenta",
61
+ "yellow",
62
+ ]
63
+
64
+ # Line-style choices available in the GUI selector
65
+ ANNOTATION_LINESTYLES: list[str] = ["--", "-", ":", "-."]
66
+
67
+
68
+ def apply_annotations(ax: "Axes", overlays: list) -> None: # noqa: C901
69
+ """Apply a list of annotation overlays to a Matplotlib Axes.
70
+
71
+ Parameters
72
+ ----------
73
+ ax:
74
+ Target Matplotlib ``Axes`` object.
75
+ overlays:
76
+ Sequence of annotation dicts. Each dict **must** contain a
77
+ ``'type'`` key; see module docstring for required/optional keys
78
+ per type. Unknown types are silently skipped.
79
+ """
80
+ if not overlays:
81
+ return
82
+
83
+ for ann in overlays:
84
+ t = ann.get("type")
85
+ color: str = str(ann.get("color", "red"))
86
+ label = ann.get("label") or None
87
+ lw: float = float(ann.get("lw", 1.5))
88
+ ls: str = str(ann.get("linestyle", "--"))
89
+ alpha: float = float(ann.get("alpha", 0.3))
90
+
91
+ if t == "hline":
92
+ ax.axhline(
93
+ float(ann["y"]),
94
+ color=color,
95
+ linestyle=ls,
96
+ linewidth=lw,
97
+ label=label,
98
+ )
99
+
100
+ elif t == "vline":
101
+ ax.axvline(
102
+ float(ann["x"]),
103
+ color=color,
104
+ linestyle=ls,
105
+ linewidth=lw,
106
+ label=label,
107
+ )
108
+
109
+ elif t == "hspan":
110
+ ax.axhspan(
111
+ float(ann["y1"]),
112
+ float(ann["y2"]),
113
+ alpha=alpha,
114
+ color=color,
115
+ label=label,
116
+ )
117
+
118
+ elif t == "vspan":
119
+ ax.axvspan(
120
+ float(ann["x1"]),
121
+ float(ann["x2"]),
122
+ alpha=alpha,
123
+ color=color,
124
+ label=label,
125
+ )
126
+
127
+ elif t == "text":
128
+ kw: dict = {
129
+ "fontsize": int(ann.get("fontsize", 10)),
130
+ "color": color,
131
+ }
132
+ if ann.get("arrow"):
133
+ ax.annotate(
134
+ str(ann["text"]),
135
+ xy=(float(ann["x"]), float(ann["y"])),
136
+ xytext=(
137
+ float(ann.get("tx", ann["x"])),
138
+ float(ann.get("ty", ann["y"])),
139
+ ),
140
+ arrowprops=dict(arrowstyle="->", color=color),
141
+ **kw,
142
+ )
143
+ else:
144
+ ax.text(float(ann["x"]), float(ann["y"]), str(ann["text"]), **kw)
145
+
146
+ elif t == "rectangle":
147
+ from matplotlib.patches import Rectangle # local import
148
+
149
+ w = float(ann["x2"]) - float(ann["x1"])
150
+ h = float(ann["y2"]) - float(ann["y1"])
151
+ rect = Rectangle(
152
+ (float(ann["x1"]), float(ann["y1"])),
153
+ w,
154
+ h,
155
+ linewidth=lw,
156
+ edgecolor=color,
157
+ facecolor=ann.get("facecolor", "none"),
158
+ alpha=alpha,
159
+ label=label,
160
+ )
161
+ ax.add_patch(rect)
162
+
163
+ elif t == "ellipse":
164
+ from matplotlib.patches import Ellipse # local import
165
+
166
+ el = Ellipse(
167
+ (float(ann["cx"]), float(ann["cy"])),
168
+ float(ann["width"]),
169
+ float(ann["height"]),
170
+ linewidth=lw,
171
+ edgecolor=color,
172
+ facecolor=ann.get("facecolor", "none"),
173
+ alpha=alpha,
174
+ label=label,
175
+ )
176
+ ax.add_patch(el)
177
+
178
+ # Refresh legend if any overlay carries a label
179
+ if any(a.get("label") for a in overlays):
180
+ handles, labels = ax.get_legend_handles_labels()
181
+ if handles:
182
+ ax.legend(handles, labels, loc="best")
183
+
184
+
185
+ def describe_overlay(ann: dict) -> str:
186
+ """Return a human-readable one-line description of an annotation dict.
187
+
188
+ Used in the Quick Plot annotations list UI.
189
+ """
190
+ t = ann.get("type", "unknown")
191
+ color = ann.get("color", "red")
192
+ label = ann.get("label", "")
193
+ suffix = f" [{label}]" if label else ""
194
+
195
+ if t == "hline":
196
+ return f"H-line y = {ann.get('y')} ({color}){suffix}"
197
+ if t == "vline":
198
+ return f"V-line x = {ann.get('x')} ({color}){suffix}"
199
+ if t == "hspan":
200
+ return f"H-span y = {ann.get('y1')} – {ann.get('y2')} ({color}){suffix}"
201
+ if t == "vspan":
202
+ return f"V-span x = {ann.get('x1')} – {ann.get('x2')} ({color}){suffix}"
203
+ if t == "text":
204
+ return f"Text '{ann.get('text')}' @ ({ann.get('x')}, {ann.get('y')}){suffix}"
205
+ if t == "rectangle":
206
+ return (
207
+ f"Rect ({ann.get('x1')}, {ann.get('y1')}) → "
208
+ f"({ann.get('x2')}, {ann.get('y2')}) ({color}){suffix}"
209
+ )
210
+ if t == "ellipse":
211
+ return (
212
+ f"Ellipse c=({ann.get('cx')}, {ann.get('cy')}) "
213
+ f"w={ann.get('width')} h={ann.get('height')} ({color}){suffix}"
214
+ )
215
+ return f"{t}{suffix}"
Binary file
Binary file
Binary file
Binary file