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.
- eagleplot/Home.py +362 -0
- eagleplot/__init__.py +19 -0
- eagleplot/annotations.py +215 -0
- eagleplot/assets/logo.ico +0 -0
- eagleplot/assets/logo.png +0 -0
- eagleplot/assets/nccu-horiz-logo.png +0 -0
- eagleplot/assets/nccu-wings.png +0 -0
- eagleplot/batch.py +446 -0
- eagleplot/cli.py +719 -0
- eagleplot/data_tools.py +849 -0
- eagleplot/example-data/Artificial/bimodal_distribution.csv +501 -0
- eagleplot/example-data/Artificial/correlation_matrix.npy +0 -0
- eagleplot/example-data/Artificial/gaussian_surface.npy +0 -0
- eagleplot/example-data/Artificial/grouped_categorical.csv +241 -0
- eagleplot/example-data/Artificial/ir_spectrum.csv +1802 -0
- eagleplot/example-data/Artificial/molecular_properties.csv +121 -0
- eagleplot/example-data/Artificial/normal_distribution.csv +401 -0
- eagleplot/example-data/Artificial/reaction_kinetics.csv +121 -0
- eagleplot/example-data/Artificial/scatter_correlation.csv +201 -0
- eagleplot/example-data/Artificial/sine_cosine_waves.csv +301 -0
- eagleplot/gallery/box_plot.png +0 -0
- eagleplot/gallery/contour_plot.png +0 -0
- eagleplot/gallery/distribution_plot.png +0 -0
- eagleplot/gallery/heatmap.png +0 -0
- eagleplot/gallery/histogram.png +0 -0
- eagleplot/gallery/interactive_3d_surface.png +0 -0
- eagleplot/gallery/interactive_heatmap.png +0 -0
- eagleplot/gallery/interactive_histogram.png +0 -0
- eagleplot/gallery/interactive_line.png +0 -0
- eagleplot/gallery/interactive_scatter.png +0 -0
- eagleplot/gallery/line_plot.png +0 -0
- eagleplot/gallery/manifest.json +196 -0
- eagleplot/gallery/regression_plot.png +0 -0
- eagleplot/gallery/scatter_plot.png +0 -0
- eagleplot/io.py +1008 -0
- eagleplot/math.py +1210 -0
- eagleplot/molecular/__init__.py +21 -0
- eagleplot/molecular/atom_data.py +287 -0
- eagleplot/molecular/parsers.py +747 -0
- eagleplot/nist.py +241 -0
- eagleplot/pages/10_Molecular_Viz.py +225 -0
- eagleplot/pages/11_Batch_Analysis.py +325 -0
- eagleplot/pages/12_Help.py +199 -0
- eagleplot/pages/13_Settings.py +388 -0
- eagleplot/pages/1_Data_Upload.py +521 -0
- eagleplot/pages/2_Quick_Plot.py +1693 -0
- eagleplot/pages/3_Analysis_Tools.py +2510 -0
- eagleplot/pages/4_Multi_Plot_Dashboard.py +791 -0
- eagleplot/pages/5_Advanced_Plotting.py +691 -0
- eagleplot/pages/6_Export_Results.py +442 -0
- eagleplot/pages/7_Gallery.py +201 -0
- eagleplot/pages/8_Data_Tools.py +434 -0
- eagleplot/pages/9_Spectroscopy.py +821 -0
- eagleplot/pages/__init__.py +8 -0
- eagleplot/peaks.py +513 -0
- eagleplot/plotting.py +2630 -0
- eagleplot/plugin_loader.py +208 -0
- eagleplot/report.py +448 -0
- eagleplot/signal.py +494 -0
- eagleplot/spectroscopy.py +830 -0
- eagleplot/utils/__init__.py +76 -0
- eagleplot/utils/access.py +77 -0
- eagleplot/utils/data_preview.py +352 -0
- eagleplot/utils/plot_config.py +2721 -0
- eagleplot/utils/session_state.py +535 -0
- eagleplot/utils/theming.py +312 -0
- eagleplot/utils/user_settings.py +294 -0
- eagleplot-3.0.0.dist-info/METADATA +304 -0
- eagleplot-3.0.0.dist-info/RECORD +73 -0
- eagleplot-3.0.0.dist-info/WHEEL +5 -0
- eagleplot-3.0.0.dist-info/entry_points.txt +2 -0
- eagleplot-3.0.0.dist-info/licenses/LICENSE +21 -0
- eagleplot-3.0.0.dist-info/top_level.txt +1 -0
eagleplot/plotting.py
ADDED
|
@@ -0,0 +1,2630 @@
|
|
|
1
|
+
"""Plotting module for creating scientific visualizations.
|
|
2
|
+
|
|
3
|
+
This module provides a high-level interface for creating publication-quality
|
|
4
|
+
plots using matplotlib. It includes functions for common plot types, figure
|
|
5
|
+
management, and styling utilities.
|
|
6
|
+
|
|
7
|
+
Examples
|
|
8
|
+
--------
|
|
9
|
+
>>> from eagleplot.plotting import line_plot, histogram, save_figure
|
|
10
|
+
>>> fig, ax = line_plot(x, y, xlabel='Time (s)', ylabel='Temperature (K)')
|
|
11
|
+
>>> save_figure(fig, 'output.png', dpi=300)
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import numpy as np
|
|
15
|
+
import matplotlib.pyplot as plt
|
|
16
|
+
import matplotlib.colors as mcolors
|
|
17
|
+
from matplotlib.figure import Figure
|
|
18
|
+
from matplotlib.axes import Axes
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple, Union, cast
|
|
21
|
+
|
|
22
|
+
if TYPE_CHECKING:
|
|
23
|
+
import pandas as pd
|
|
24
|
+
from matplotlib.projections.polar import PolarAxes
|
|
25
|
+
|
|
26
|
+
# Okabe-Ito colorblind-safe palette (8 colors)
|
|
27
|
+
COLORBLIND_PALETTE: List[str] = [
|
|
28
|
+
"#E69F00", # orange
|
|
29
|
+
"#56B4E9", # sky blue
|
|
30
|
+
"#009E73", # bluish green
|
|
31
|
+
"#F0E442", # yellow
|
|
32
|
+
"#0072B2", # blue
|
|
33
|
+
"#D55E00", # vermillion
|
|
34
|
+
"#CC79A7", # reddish purple
|
|
35
|
+
"#000000", # black
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
# Optional imports for advanced features
|
|
39
|
+
try:
|
|
40
|
+
import seaborn as sns
|
|
41
|
+
|
|
42
|
+
HAS_SEABORN = True
|
|
43
|
+
except ImportError:
|
|
44
|
+
HAS_SEABORN = False
|
|
45
|
+
|
|
46
|
+
try:
|
|
47
|
+
import plotly.graph_objects as go
|
|
48
|
+
|
|
49
|
+
HAS_PLOTLY = True
|
|
50
|
+
except ImportError:
|
|
51
|
+
HAS_PLOTLY = False
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
# ============================================================================
|
|
55
|
+
# Figure Management
|
|
56
|
+
# ============================================================================
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def create_figure(
|
|
60
|
+
figsize: Tuple[float, float] = (8, 6), dpi: int = 100, **kwargs
|
|
61
|
+
) -> Tuple[Figure, Axes]:
|
|
62
|
+
"""Create a new figure with axes.
|
|
63
|
+
|
|
64
|
+
Parameters
|
|
65
|
+
----------
|
|
66
|
+
figsize : tuple of (width, height), default (8, 6)
|
|
67
|
+
Figure size in inches
|
|
68
|
+
dpi : int, default 100
|
|
69
|
+
Dots per inch (resolution)
|
|
70
|
+
kwargs : dict
|
|
71
|
+
Additional arguments passed to plt.subplots()
|
|
72
|
+
|
|
73
|
+
Returns
|
|
74
|
+
-------
|
|
75
|
+
fig : Figure
|
|
76
|
+
Matplotlib figure object
|
|
77
|
+
ax : Axes
|
|
78
|
+
Matplotlib axes object
|
|
79
|
+
|
|
80
|
+
Examples
|
|
81
|
+
--------
|
|
82
|
+
>>> fig, ax = create_figure(figsize=(10, 6), dpi=150)
|
|
83
|
+
>>> ax.plot([1, 2, 3], [1, 4, 9])
|
|
84
|
+
"""
|
|
85
|
+
fig, ax = plt.subplots(figsize=figsize, dpi=dpi, **kwargs)
|
|
86
|
+
return fig, ax
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def configure_axes(
|
|
90
|
+
ax: Axes,
|
|
91
|
+
xlabel: Optional[str] = None,
|
|
92
|
+
ylabel: Optional[str] = None,
|
|
93
|
+
title: Optional[str] = None,
|
|
94
|
+
grid: bool = True,
|
|
95
|
+
legend: bool = False,
|
|
96
|
+
xlim: Optional[Tuple[float, float]] = None,
|
|
97
|
+
ylim: Optional[Tuple[float, float]] = None,
|
|
98
|
+
**kwargs,
|
|
99
|
+
) -> Axes:
|
|
100
|
+
"""Configure axes properties.
|
|
101
|
+
|
|
102
|
+
Parameters
|
|
103
|
+
----------
|
|
104
|
+
ax : Axes
|
|
105
|
+
Matplotlib axes to configure
|
|
106
|
+
xlabel : str, optional
|
|
107
|
+
X-axis label
|
|
108
|
+
ylabel : str, optional
|
|
109
|
+
Y-axis label
|
|
110
|
+
title : str, optional
|
|
111
|
+
Plot title
|
|
112
|
+
grid : bool, default True
|
|
113
|
+
Whether to show grid
|
|
114
|
+
legend : bool, default False
|
|
115
|
+
Whether to show legend
|
|
116
|
+
xlim : tuple of (min, max), optional
|
|
117
|
+
X-axis limits
|
|
118
|
+
ylim : tuple of (min, max), optional
|
|
119
|
+
Y-axis limits
|
|
120
|
+
kwargs : dict
|
|
121
|
+
Additional axes properties
|
|
122
|
+
|
|
123
|
+
Returns
|
|
124
|
+
-------
|
|
125
|
+
ax : Axes
|
|
126
|
+
Configured axes object
|
|
127
|
+
|
|
128
|
+
Examples
|
|
129
|
+
--------
|
|
130
|
+
>>> configure_axes(ax, xlabel='Time', ylabel='Value', title='Results')
|
|
131
|
+
"""
|
|
132
|
+
if xlabel is not None:
|
|
133
|
+
ax.set_xlabel(xlabel)
|
|
134
|
+
if ylabel is not None:
|
|
135
|
+
ax.set_ylabel(ylabel)
|
|
136
|
+
if title is not None:
|
|
137
|
+
ax.set_title(title)
|
|
138
|
+
if grid:
|
|
139
|
+
ax.grid(True, alpha=0.3)
|
|
140
|
+
if legend:
|
|
141
|
+
ax.legend()
|
|
142
|
+
if xlim is not None:
|
|
143
|
+
ax.set_xlim(xlim)
|
|
144
|
+
if ylim is not None:
|
|
145
|
+
ax.set_ylim(ylim)
|
|
146
|
+
|
|
147
|
+
# Apply any additional properties
|
|
148
|
+
for key, value in kwargs.items():
|
|
149
|
+
if hasattr(ax, f"set_{key}"):
|
|
150
|
+
getattr(ax, f"set_{key}")(value)
|
|
151
|
+
|
|
152
|
+
return ax
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def save_figure(
|
|
156
|
+
fig: Figure,
|
|
157
|
+
filepath: Union[str, Path],
|
|
158
|
+
format: Optional[str] = None,
|
|
159
|
+
dpi: int = 300,
|
|
160
|
+
bbox_inches: str = "tight",
|
|
161
|
+
**kwargs,
|
|
162
|
+
) -> None:
|
|
163
|
+
"""Save figure to file.
|
|
164
|
+
|
|
165
|
+
Parameters
|
|
166
|
+
----------
|
|
167
|
+
fig : Figure
|
|
168
|
+
Matplotlib figure to save
|
|
169
|
+
filepath : str or Path
|
|
170
|
+
Output file path
|
|
171
|
+
format : str, optional
|
|
172
|
+
File format (png, pdf, svg, etc.). If None, inferred from filepath
|
|
173
|
+
dpi : int, default 300
|
|
174
|
+
Resolution for raster formats
|
|
175
|
+
bbox_inches : str, default 'tight'
|
|
176
|
+
Bounding box setting
|
|
177
|
+
kwargs : dict
|
|
178
|
+
Additional arguments passed to fig.savefig()
|
|
179
|
+
|
|
180
|
+
Examples
|
|
181
|
+
--------
|
|
182
|
+
>>> save_figure(fig, 'plot.png', dpi=300)
|
|
183
|
+
>>> save_figure(fig, 'plot.pdf', format='pdf')
|
|
184
|
+
"""
|
|
185
|
+
filepath = Path(filepath)
|
|
186
|
+
filepath.parent.mkdir(parents=True, exist_ok=True)
|
|
187
|
+
|
|
188
|
+
fig.savefig(filepath, format=format, dpi=dpi, bbox_inches=bbox_inches, **kwargs)
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
# ============================================================================
|
|
192
|
+
# Core Plotting Functions
|
|
193
|
+
# ============================================================================
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def histogram(
|
|
197
|
+
data: np.ndarray,
|
|
198
|
+
bins: Union[int, str, np.ndarray] = "auto",
|
|
199
|
+
xlabel: Optional[str] = None,
|
|
200
|
+
ylabel: str = "Frequency",
|
|
201
|
+
title: Optional[str] = None,
|
|
202
|
+
figsize: Tuple[float, float] = (8, 6),
|
|
203
|
+
color: str = "steelblue",
|
|
204
|
+
alpha: float = 0.7,
|
|
205
|
+
density: bool = False,
|
|
206
|
+
**kwargs,
|
|
207
|
+
) -> Tuple[Figure, Axes, Dict[str, Any]]:
|
|
208
|
+
"""Create a histogram.
|
|
209
|
+
|
|
210
|
+
Parameters
|
|
211
|
+
----------
|
|
212
|
+
data : np.ndarray
|
|
213
|
+
Data to plot
|
|
214
|
+
bins : int, str, or array, default 'auto'
|
|
215
|
+
Number of bins or bin edges
|
|
216
|
+
xlabel : str, optional
|
|
217
|
+
X-axis label
|
|
218
|
+
ylabel : str, default 'Frequency'
|
|
219
|
+
Y-axis label
|
|
220
|
+
title : str, optional
|
|
221
|
+
Plot title
|
|
222
|
+
figsize : tuple, default (8, 6)
|
|
223
|
+
Figure size in inches
|
|
224
|
+
color : str, default 'steelblue'
|
|
225
|
+
Bar color
|
|
226
|
+
alpha : float, default 0.7
|
|
227
|
+
Transparency (0-1)
|
|
228
|
+
density : bool, default False
|
|
229
|
+
If True, plot probability density instead of counts
|
|
230
|
+
kwargs : dict
|
|
231
|
+
Additional arguments passed to ax.hist()
|
|
232
|
+
|
|
233
|
+
Returns
|
|
234
|
+
-------
|
|
235
|
+
fig : Figure
|
|
236
|
+
Matplotlib figure
|
|
237
|
+
ax : Axes
|
|
238
|
+
Matplotlib axes
|
|
239
|
+
info : dict
|
|
240
|
+
Dictionary containing:
|
|
241
|
+
- 'counts': Histogram counts
|
|
242
|
+
- 'bins': Bin edges
|
|
243
|
+
- 'mean': Data mean
|
|
244
|
+
- 'std': Data standard deviation
|
|
245
|
+
|
|
246
|
+
Examples
|
|
247
|
+
--------
|
|
248
|
+
>>> data = np.random.normal(0, 1, 1000)
|
|
249
|
+
>>> fig, ax, info = histogram(data, bins=30, xlabel='Value')
|
|
250
|
+
>>> print(f"Mean: {info['mean']:.2f}")
|
|
251
|
+
"""
|
|
252
|
+
data = np.asarray(data).flatten()
|
|
253
|
+
|
|
254
|
+
fig, ax = create_figure(figsize=figsize)
|
|
255
|
+
|
|
256
|
+
# matplotlib types `bins` as int | Sequence[float] | str | None; an ndarray of
|
|
257
|
+
# explicit edges is valid at runtime but not per the stub, so convert it.
|
|
258
|
+
_bins: Union[int, str, List[float], None] = (
|
|
259
|
+
[float(edge) for edge in bins] if isinstance(bins, np.ndarray) else bins
|
|
260
|
+
)
|
|
261
|
+
counts, bin_edges, patches = ax.hist(
|
|
262
|
+
data,
|
|
263
|
+
bins=_bins,
|
|
264
|
+
color=color,
|
|
265
|
+
alpha=alpha,
|
|
266
|
+
density=density,
|
|
267
|
+
**kwargs, # type: ignore[arg-type]
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
# Configure axes
|
|
271
|
+
if ylabel == "Frequency" and density:
|
|
272
|
+
ylabel = "Density"
|
|
273
|
+
configure_axes(ax, xlabel=xlabel, ylabel=ylabel, title=title)
|
|
274
|
+
|
|
275
|
+
# Calculate statistics
|
|
276
|
+
info = {
|
|
277
|
+
"counts": counts,
|
|
278
|
+
"bins": bin_edges,
|
|
279
|
+
"mean": float(np.mean(data)),
|
|
280
|
+
"std": float(np.std(data)),
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
return fig, ax, info
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def line_plot(
|
|
287
|
+
x: np.ndarray,
|
|
288
|
+
y: Union[np.ndarray, List[np.ndarray]],
|
|
289
|
+
xlabel: Optional[str] = None,
|
|
290
|
+
ylabel: Optional[str] = None,
|
|
291
|
+
title: Optional[str] = None,
|
|
292
|
+
figsize: Tuple[float, float] = (8, 6),
|
|
293
|
+
labels: Optional[List[str]] = None,
|
|
294
|
+
colors: Optional[List[str]] = None,
|
|
295
|
+
linestyles: Optional[List[str]] = None,
|
|
296
|
+
markers: Optional[List[str]] = None,
|
|
297
|
+
legend: bool = True,
|
|
298
|
+
yerr: Optional[Union[np.ndarray, List[np.ndarray]]] = None,
|
|
299
|
+
**kwargs,
|
|
300
|
+
) -> Tuple[Figure, Axes]:
|
|
301
|
+
"""Create a line plot.
|
|
302
|
+
|
|
303
|
+
Parameters
|
|
304
|
+
----------
|
|
305
|
+
x : np.ndarray
|
|
306
|
+
X-axis data
|
|
307
|
+
y : np.ndarray or list of np.ndarray
|
|
308
|
+
Y-axis data. Can be single array or list of arrays for multiple lines
|
|
309
|
+
xlabel : str, optional
|
|
310
|
+
X-axis label
|
|
311
|
+
ylabel : str, optional
|
|
312
|
+
Y-axis label
|
|
313
|
+
title : str, optional
|
|
314
|
+
Plot title
|
|
315
|
+
figsize : tuple, default (8, 6)
|
|
316
|
+
Figure size in inches
|
|
317
|
+
labels : list of str, optional
|
|
318
|
+
Line labels for legend
|
|
319
|
+
colors : list of str, optional
|
|
320
|
+
Line colors
|
|
321
|
+
linestyles : list of str, optional
|
|
322
|
+
Line styles ('-', '--', '-.', ':')
|
|
323
|
+
markers : list of str, optional
|
|
324
|
+
Marker styles ('o', 's', '^', etc.)
|
|
325
|
+
legend : bool, default True
|
|
326
|
+
Whether to show legend (only if labels provided)
|
|
327
|
+
yerr : np.ndarray or list of np.ndarray, optional
|
|
328
|
+
Y error values. A single array is applied to the first series;
|
|
329
|
+
a list of arrays matches each series in order.
|
|
330
|
+
kwargs : dict
|
|
331
|
+
Additional arguments passed to ax.plot() / ax.errorbar()
|
|
332
|
+
|
|
333
|
+
Returns
|
|
334
|
+
-------
|
|
335
|
+
fig : Figure
|
|
336
|
+
Matplotlib figure
|
|
337
|
+
ax : Axes
|
|
338
|
+
Matplotlib axes
|
|
339
|
+
|
|
340
|
+
Examples
|
|
341
|
+
--------
|
|
342
|
+
>>> x = np.linspace(0, 10, 100)
|
|
343
|
+
>>> y = np.sin(x)
|
|
344
|
+
>>> fig, ax = line_plot(x, y, xlabel='x', ylabel='sin(x)')
|
|
345
|
+
|
|
346
|
+
>>> # With error bars
|
|
347
|
+
>>> err = 0.05 * np.ones_like(y)
|
|
348
|
+
>>> fig, ax = line_plot(x, y, yerr=err)
|
|
349
|
+
"""
|
|
350
|
+
x = np.asarray(x)
|
|
351
|
+
|
|
352
|
+
# Handle single vs multiple y arrays
|
|
353
|
+
if isinstance(y, np.ndarray) and y.ndim == 1:
|
|
354
|
+
y = [y]
|
|
355
|
+
elif not isinstance(y, list):
|
|
356
|
+
y = [np.asarray(y)]
|
|
357
|
+
else:
|
|
358
|
+
y = [np.asarray(yi) for yi in y]
|
|
359
|
+
|
|
360
|
+
n_lines = len(y)
|
|
361
|
+
|
|
362
|
+
# Normalise yerr to a list aligned with y series
|
|
363
|
+
if yerr is not None:
|
|
364
|
+
if isinstance(yerr, np.ndarray) and yerr.ndim == 1:
|
|
365
|
+
yerr_list: List[Optional[np.ndarray]] = [yerr] + [None] * (n_lines - 1)
|
|
366
|
+
elif isinstance(yerr, list):
|
|
367
|
+
yerr_list = list(yerr) + [None] * max(0, n_lines - len(yerr))
|
|
368
|
+
else:
|
|
369
|
+
yerr_list = [np.asarray(yerr)] + [None] * (n_lines - 1)
|
|
370
|
+
else:
|
|
371
|
+
yerr_list = [None] * n_lines
|
|
372
|
+
|
|
373
|
+
fig, ax = create_figure(figsize=figsize)
|
|
374
|
+
|
|
375
|
+
# Set default colors, linestyles, markers
|
|
376
|
+
if colors is None:
|
|
377
|
+
colors = [f"C{i}" for i in range(n_lines)]
|
|
378
|
+
if linestyles is None:
|
|
379
|
+
linestyles = ["-"] * n_lines
|
|
380
|
+
if markers is None:
|
|
381
|
+
markers = [None] * n_lines
|
|
382
|
+
if labels is None:
|
|
383
|
+
labels = [None] * n_lines
|
|
384
|
+
|
|
385
|
+
# Plot each line (with or without error bars)
|
|
386
|
+
for i, yi in enumerate(y):
|
|
387
|
+
c = colors[i % len(colors)]
|
|
388
|
+
ls = linestyles[i % len(linestyles)]
|
|
389
|
+
mk = markers[i % len(markers)]
|
|
390
|
+
lbl = labels[i]
|
|
391
|
+
yi_err = yerr_list[i]
|
|
392
|
+
if yi_err is not None:
|
|
393
|
+
ax.errorbar(
|
|
394
|
+
x,
|
|
395
|
+
yi,
|
|
396
|
+
yerr=yi_err,
|
|
397
|
+
color=c,
|
|
398
|
+
linestyle=ls,
|
|
399
|
+
marker=mk,
|
|
400
|
+
label=lbl,
|
|
401
|
+
capsize=3,
|
|
402
|
+
**kwargs,
|
|
403
|
+
)
|
|
404
|
+
else:
|
|
405
|
+
ax.plot(
|
|
406
|
+
x,
|
|
407
|
+
yi,
|
|
408
|
+
color=c,
|
|
409
|
+
linestyle=ls,
|
|
410
|
+
marker=mk,
|
|
411
|
+
label=lbl,
|
|
412
|
+
**kwargs,
|
|
413
|
+
)
|
|
414
|
+
|
|
415
|
+
# Configure axes
|
|
416
|
+
show_legend = legend and any(label is not None for label in labels)
|
|
417
|
+
configure_axes(ax, xlabel=xlabel, ylabel=ylabel, title=title, legend=show_legend)
|
|
418
|
+
|
|
419
|
+
return fig, ax
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
def scatter_plot(
|
|
423
|
+
x: np.ndarray,
|
|
424
|
+
y: np.ndarray,
|
|
425
|
+
xlabel: Optional[str] = None,
|
|
426
|
+
ylabel: Optional[str] = None,
|
|
427
|
+
title: Optional[str] = None,
|
|
428
|
+
figsize: Tuple[float, float] = (8, 6),
|
|
429
|
+
color: Union[str, np.ndarray] = "steelblue",
|
|
430
|
+
size: Union[float, np.ndarray] = 50,
|
|
431
|
+
alpha: float = 0.7,
|
|
432
|
+
colorbar: bool = False,
|
|
433
|
+
yerr: Optional[np.ndarray] = None,
|
|
434
|
+
xerr: Optional[np.ndarray] = None,
|
|
435
|
+
**kwargs,
|
|
436
|
+
) -> Tuple[Figure, Axes]:
|
|
437
|
+
"""Create a scatter plot.
|
|
438
|
+
|
|
439
|
+
Parameters
|
|
440
|
+
----------
|
|
441
|
+
x : np.ndarray
|
|
442
|
+
X-axis data
|
|
443
|
+
y : np.ndarray
|
|
444
|
+
Y-axis data
|
|
445
|
+
xlabel : str, optional
|
|
446
|
+
X-axis label
|
|
447
|
+
ylabel : str, optional
|
|
448
|
+
Y-axis label
|
|
449
|
+
title : str, optional
|
|
450
|
+
Plot title
|
|
451
|
+
figsize : tuple, default (8, 6)
|
|
452
|
+
Figure size in inches
|
|
453
|
+
color : str or np.ndarray, default 'steelblue'
|
|
454
|
+
Point color(s). Can be single color or array for color mapping
|
|
455
|
+
size : float or np.ndarray, default 50
|
|
456
|
+
Point size(s). Can be single value or array
|
|
457
|
+
alpha : float, default 0.7
|
|
458
|
+
Transparency (0-1)
|
|
459
|
+
colorbar : bool, default False
|
|
460
|
+
Whether to add colorbar (only if color is array)
|
|
461
|
+
yerr : np.ndarray, optional
|
|
462
|
+
Symmetric Y error bar values
|
|
463
|
+
xerr : np.ndarray, optional
|
|
464
|
+
Symmetric X error bar values
|
|
465
|
+
kwargs : dict
|
|
466
|
+
Additional arguments passed to ax.scatter()
|
|
467
|
+
|
|
468
|
+
Returns
|
|
469
|
+
-------
|
|
470
|
+
fig : Figure
|
|
471
|
+
Matplotlib figure
|
|
472
|
+
ax : Axes
|
|
473
|
+
Matplotlib axes
|
|
474
|
+
|
|
475
|
+
Examples
|
|
476
|
+
--------
|
|
477
|
+
>>> x = np.random.rand(100)
|
|
478
|
+
>>> y = np.random.rand(100)
|
|
479
|
+
>>> fig, ax = scatter_plot(x, y, xlabel='X', ylabel='Y')
|
|
480
|
+
|
|
481
|
+
>>> # With error bars
|
|
482
|
+
>>> fig, ax = scatter_plot(x, y, yerr=0.1*np.ones_like(y))
|
|
483
|
+
"""
|
|
484
|
+
x = np.asarray(x)
|
|
485
|
+
y = np.asarray(y)
|
|
486
|
+
|
|
487
|
+
fig, ax = create_figure(figsize=figsize)
|
|
488
|
+
|
|
489
|
+
scatter = ax.scatter(x, y, c=color, s=size, alpha=alpha, **kwargs)
|
|
490
|
+
|
|
491
|
+
# Add colorbar if color is array
|
|
492
|
+
if colorbar and isinstance(color, np.ndarray):
|
|
493
|
+
plt.colorbar(scatter, ax=ax)
|
|
494
|
+
|
|
495
|
+
# Overlay error bars if provided
|
|
496
|
+
if yerr is not None or xerr is not None:
|
|
497
|
+
ecolor = color if isinstance(color, str) else "gray"
|
|
498
|
+
ax.errorbar(
|
|
499
|
+
x,
|
|
500
|
+
y,
|
|
501
|
+
yerr=yerr,
|
|
502
|
+
xerr=xerr,
|
|
503
|
+
fmt="none",
|
|
504
|
+
color=ecolor,
|
|
505
|
+
alpha=alpha,
|
|
506
|
+
capsize=3,
|
|
507
|
+
)
|
|
508
|
+
|
|
509
|
+
configure_axes(ax, xlabel=xlabel, ylabel=ylabel, title=title)
|
|
510
|
+
|
|
511
|
+
return fig, ax
|
|
512
|
+
|
|
513
|
+
|
|
514
|
+
def heatmap(
|
|
515
|
+
data: np.ndarray,
|
|
516
|
+
xlabel: Optional[str] = None,
|
|
517
|
+
ylabel: Optional[str] = None,
|
|
518
|
+
title: Optional[str] = None,
|
|
519
|
+
figsize: Tuple[float, float] = (8, 6),
|
|
520
|
+
cmap: str = "viridis",
|
|
521
|
+
colorbar: bool = True,
|
|
522
|
+
vmin: Optional[float] = None,
|
|
523
|
+
vmax: Optional[float] = None,
|
|
524
|
+
cmap_n_colors: Optional[int] = None,
|
|
525
|
+
xticklabels: Optional[List[str]] = None,
|
|
526
|
+
yticklabels: Optional[List[str]] = None,
|
|
527
|
+
**kwargs,
|
|
528
|
+
) -> Tuple[Figure, Axes]:
|
|
529
|
+
"""Create a heatmap.
|
|
530
|
+
|
|
531
|
+
Parameters
|
|
532
|
+
----------
|
|
533
|
+
data : np.ndarray
|
|
534
|
+
2D array to visualize
|
|
535
|
+
xlabel : str, optional
|
|
536
|
+
X-axis label
|
|
537
|
+
ylabel : str, optional
|
|
538
|
+
Y-axis label
|
|
539
|
+
title : str, optional
|
|
540
|
+
Plot title
|
|
541
|
+
figsize : tuple, default (8, 6)
|
|
542
|
+
Figure size in inches
|
|
543
|
+
cmap : str, default 'viridis'
|
|
544
|
+
Colormap name
|
|
545
|
+
colorbar : bool, default True
|
|
546
|
+
Whether to show colorbar
|
|
547
|
+
vmin : float, optional
|
|
548
|
+
Minimum value for color scale
|
|
549
|
+
vmax : float, optional
|
|
550
|
+
Maximum value for color scale
|
|
551
|
+
cmap_n_colors : int, optional
|
|
552
|
+
If set, discretize the colormap into this many distinct colors (2–256)
|
|
553
|
+
xticklabels : list of str, optional
|
|
554
|
+
X-axis tick labels
|
|
555
|
+
yticklabels : list of str, optional
|
|
556
|
+
Y-axis tick labels
|
|
557
|
+
kwargs : dict
|
|
558
|
+
Additional arguments passed to ax.imshow()
|
|
559
|
+
|
|
560
|
+
Returns
|
|
561
|
+
-------
|
|
562
|
+
fig : Figure
|
|
563
|
+
Matplotlib figure
|
|
564
|
+
ax : Axes
|
|
565
|
+
Matplotlib axes
|
|
566
|
+
|
|
567
|
+
Examples
|
|
568
|
+
--------
|
|
569
|
+
>>> data = np.random.rand(10, 10)
|
|
570
|
+
>>> fig, ax = heatmap(data, title='Random Data', cmap='hot')
|
|
571
|
+
>>> fig, ax = heatmap(data, cmap='viridis', cmap_n_colors=8, vmin=0.2, vmax=0.8)
|
|
572
|
+
"""
|
|
573
|
+
data = np.asarray(data)
|
|
574
|
+
|
|
575
|
+
if data.ndim != 2:
|
|
576
|
+
raise ValueError(f"Data must be 2D, got shape {data.shape}")
|
|
577
|
+
|
|
578
|
+
fig, ax = create_figure(figsize=figsize)
|
|
579
|
+
|
|
580
|
+
actual_cmap = plt.get_cmap(cmap, cmap_n_colors) if cmap_n_colors is not None else cmap
|
|
581
|
+
im = ax.imshow(data, cmap=actual_cmap, vmin=vmin, vmax=vmax, aspect="auto", **kwargs)
|
|
582
|
+
|
|
583
|
+
if colorbar:
|
|
584
|
+
plt.colorbar(im, ax=ax)
|
|
585
|
+
|
|
586
|
+
# Set tick labels if provided
|
|
587
|
+
if xticklabels is not None:
|
|
588
|
+
ax.set_xticks(np.arange(len(xticklabels)))
|
|
589
|
+
ax.set_xticklabels(xticklabels)
|
|
590
|
+
if yticklabels is not None:
|
|
591
|
+
ax.set_yticks(np.arange(len(yticklabels)))
|
|
592
|
+
ax.set_yticklabels(yticklabels)
|
|
593
|
+
|
|
594
|
+
configure_axes(ax, xlabel=xlabel, ylabel=ylabel, title=title, grid=False)
|
|
595
|
+
|
|
596
|
+
return fig, ax
|
|
597
|
+
|
|
598
|
+
|
|
599
|
+
def contour_plot(
|
|
600
|
+
X: np.ndarray,
|
|
601
|
+
Y: np.ndarray,
|
|
602
|
+
Z: np.ndarray,
|
|
603
|
+
xlabel: Optional[str] = None,
|
|
604
|
+
ylabel: Optional[str] = None,
|
|
605
|
+
title: Optional[str] = None,
|
|
606
|
+
figsize: Tuple[float, float] = (8, 6),
|
|
607
|
+
levels: Union[int, np.ndarray] = 10,
|
|
608
|
+
cmap: str = "viridis",
|
|
609
|
+
filled: bool = True,
|
|
610
|
+
colorbar: bool = True,
|
|
611
|
+
vmin: Optional[float] = None,
|
|
612
|
+
vmax: Optional[float] = None,
|
|
613
|
+
cmap_n_colors: Optional[int] = None,
|
|
614
|
+
linewidths: Optional[float] = None,
|
|
615
|
+
**kwargs,
|
|
616
|
+
) -> Tuple[Figure, Axes]:
|
|
617
|
+
"""Create a contour plot.
|
|
618
|
+
|
|
619
|
+
Parameters
|
|
620
|
+
----------
|
|
621
|
+
X : np.ndarray
|
|
622
|
+
2D array of X coordinates (from meshgrid)
|
|
623
|
+
Y : np.ndarray
|
|
624
|
+
2D array of Y coordinates (from meshgrid)
|
|
625
|
+
Z : np.ndarray
|
|
626
|
+
2D array of Z values
|
|
627
|
+
xlabel : str, optional
|
|
628
|
+
X-axis label
|
|
629
|
+
ylabel : str, optional
|
|
630
|
+
Y-axis label
|
|
631
|
+
title : str, optional
|
|
632
|
+
Plot title
|
|
633
|
+
figsize : tuple, default (8, 6)
|
|
634
|
+
Figure size in inches
|
|
635
|
+
levels : int or array, default 10
|
|
636
|
+
Number of contour levels or specific level values
|
|
637
|
+
cmap : str, default 'viridis'
|
|
638
|
+
Colormap name
|
|
639
|
+
filled : bool, default True
|
|
640
|
+
Whether to create filled contours (contourf) or line contours (contour)
|
|
641
|
+
colorbar : bool, default True
|
|
642
|
+
Whether to show colorbar
|
|
643
|
+
vmin : float, optional
|
|
644
|
+
Minimum value for color/level normalization
|
|
645
|
+
vmax : float, optional
|
|
646
|
+
Maximum value for color/level normalization
|
|
647
|
+
cmap_n_colors : int, optional
|
|
648
|
+
If set, discretize the colormap into this many distinct colors (2–256)
|
|
649
|
+
linewidths : float, optional
|
|
650
|
+
Line width for unfilled contour lines (ignored when filled=True)
|
|
651
|
+
kwargs : dict
|
|
652
|
+
Additional arguments passed to ax.contour() or ax.contourf()
|
|
653
|
+
|
|
654
|
+
Returns
|
|
655
|
+
-------
|
|
656
|
+
fig : Figure
|
|
657
|
+
Matplotlib figure
|
|
658
|
+
ax : Axes
|
|
659
|
+
Matplotlib axes
|
|
660
|
+
|
|
661
|
+
Examples
|
|
662
|
+
--------
|
|
663
|
+
>>> x = np.linspace(-3, 3, 100)
|
|
664
|
+
>>> y = np.linspace(-3, 3, 100)
|
|
665
|
+
>>> X, Y = np.meshgrid(x, y)
|
|
666
|
+
>>> Z = np.sqrt(X**2 + Y**2)
|
|
667
|
+
>>> fig, ax = contour_plot(X, Y, Z, xlabel='X', ylabel='Y')
|
|
668
|
+
>>> fig, ax = contour_plot(X, Y, Z, filled=False, cmap_n_colors=10, vmin=0, vmax=3)
|
|
669
|
+
"""
|
|
670
|
+
X = np.asarray(X)
|
|
671
|
+
Y = np.asarray(Y)
|
|
672
|
+
Z = np.asarray(Z)
|
|
673
|
+
|
|
674
|
+
if X.shape != Y.shape or X.shape != Z.shape:
|
|
675
|
+
raise ValueError(
|
|
676
|
+
f"X, Y, Z must have same shape. Got X: {X.shape}, Y: {Y.shape}, Z: {Z.shape}"
|
|
677
|
+
)
|
|
678
|
+
|
|
679
|
+
fig, ax = create_figure(figsize=figsize)
|
|
680
|
+
|
|
681
|
+
actual_cmap = plt.get_cmap(cmap, cmap_n_colors) if cmap_n_colors is not None else cmap
|
|
682
|
+
|
|
683
|
+
plot_kwargs = dict(kwargs)
|
|
684
|
+
if (vmin is not None or vmax is not None) and "norm" not in plot_kwargs:
|
|
685
|
+
plot_kwargs["norm"] = mcolors.Normalize(vmin=vmin, vmax=vmax)
|
|
686
|
+
|
|
687
|
+
if filled:
|
|
688
|
+
contour = ax.contourf(X, Y, Z, levels=levels, cmap=actual_cmap, **plot_kwargs)
|
|
689
|
+
else:
|
|
690
|
+
if linewidths is not None and "linewidths" not in plot_kwargs:
|
|
691
|
+
plot_kwargs["linewidths"] = linewidths
|
|
692
|
+
contour = ax.contour(X, Y, Z, levels=levels, cmap=actual_cmap, **plot_kwargs)
|
|
693
|
+
|
|
694
|
+
if colorbar:
|
|
695
|
+
plt.colorbar(contour, ax=ax)
|
|
696
|
+
|
|
697
|
+
configure_axes(ax, xlabel=xlabel, ylabel=ylabel, title=title, grid=False)
|
|
698
|
+
|
|
699
|
+
return fig, ax
|
|
700
|
+
|
|
701
|
+
|
|
702
|
+
# ============================================================================
|
|
703
|
+
# Extended Plot Types (M14)
|
|
704
|
+
# ============================================================================
|
|
705
|
+
|
|
706
|
+
|
|
707
|
+
def bar_chart(
|
|
708
|
+
categories: Union[List, np.ndarray],
|
|
709
|
+
values: Union[np.ndarray, List],
|
|
710
|
+
yerr: Optional[Union[np.ndarray, List]] = None,
|
|
711
|
+
xerr: Optional[Union[np.ndarray, List]] = None,
|
|
712
|
+
hue: Optional[List[str]] = None,
|
|
713
|
+
kind: str = "simple",
|
|
714
|
+
xlabel: Optional[str] = None,
|
|
715
|
+
ylabel: Optional[str] = None,
|
|
716
|
+
title: Optional[str] = None,
|
|
717
|
+
figsize: Tuple[float, float] = (8, 6),
|
|
718
|
+
color: Union[str, List[str]] = "steelblue",
|
|
719
|
+
alpha: float = 0.8,
|
|
720
|
+
**kwargs,
|
|
721
|
+
) -> Tuple[Figure, Axes, Dict[str, Any]]:
|
|
722
|
+
"""Create a bar chart (simple, grouped, or stacked).
|
|
723
|
+
|
|
724
|
+
Parameters
|
|
725
|
+
----------
|
|
726
|
+
categories : list or np.ndarray
|
|
727
|
+
Category labels for the X axis
|
|
728
|
+
values : np.ndarray
|
|
729
|
+
Bar heights. Shape ``(n_categories,)`` for simple/stacked;
|
|
730
|
+
shape ``(n_groups, n_categories)`` for grouped bars.
|
|
731
|
+
yerr : array-like, optional
|
|
732
|
+
Y error bar values. Shape matches *values*.
|
|
733
|
+
xerr : array-like, optional
|
|
734
|
+
X error bar values (simple bars only).
|
|
735
|
+
hue : list of str, optional
|
|
736
|
+
Group labels for grouped/stacked charts.
|
|
737
|
+
kind : str, default 'simple'
|
|
738
|
+
``'simple'``, ``'grouped'``, or ``'stacked'``.
|
|
739
|
+
xlabel : str, optional
|
|
740
|
+
X-axis label
|
|
741
|
+
ylabel : str, optional
|
|
742
|
+
Y-axis label
|
|
743
|
+
title : str, optional
|
|
744
|
+
Plot title
|
|
745
|
+
figsize : tuple, default (8, 6)
|
|
746
|
+
Figure size in inches
|
|
747
|
+
color : str or list of str, default 'steelblue'
|
|
748
|
+
Bar color(s).
|
|
749
|
+
alpha : float, default 0.8
|
|
750
|
+
Transparency (0–1)
|
|
751
|
+
kwargs : dict
|
|
752
|
+
Extra keyword arguments forwarded to ``ax.bar()``.
|
|
753
|
+
|
|
754
|
+
Returns
|
|
755
|
+
-------
|
|
756
|
+
fig : Figure
|
|
757
|
+
ax : Axes
|
|
758
|
+
info : dict
|
|
759
|
+
``kind``, ``n_categories``, ``n_groups``
|
|
760
|
+
|
|
761
|
+
Examples
|
|
762
|
+
--------
|
|
763
|
+
>>> fig, ax, info = bar_chart(['A', 'B', 'C'], [3, 5, 2])
|
|
764
|
+
>>> # Grouped
|
|
765
|
+
>>> fig, ax, info = bar_chart(
|
|
766
|
+
... ['A', 'B'], [[2, 4], [3, 1]],
|
|
767
|
+
... hue=['G1', 'G2'], kind='grouped',
|
|
768
|
+
... )
|
|
769
|
+
"""
|
|
770
|
+
cats = list(categories)
|
|
771
|
+
vals = np.asarray(values)
|
|
772
|
+
fig, ax = create_figure(figsize=figsize)
|
|
773
|
+
|
|
774
|
+
if kind == "simple":
|
|
775
|
+
bar_color = color if isinstance(color, str) else color[0]
|
|
776
|
+
ax.bar(
|
|
777
|
+
cats,
|
|
778
|
+
vals,
|
|
779
|
+
yerr=yerr,
|
|
780
|
+
xerr=xerr,
|
|
781
|
+
color=bar_color,
|
|
782
|
+
alpha=alpha,
|
|
783
|
+
capsize=4,
|
|
784
|
+
**kwargs,
|
|
785
|
+
)
|
|
786
|
+
n_groups = 1
|
|
787
|
+
|
|
788
|
+
elif kind == "grouped":
|
|
789
|
+
vals = vals if vals.ndim == 2 else vals.reshape(1, -1)
|
|
790
|
+
n_groups = vals.shape[0]
|
|
791
|
+
n_cats = len(cats)
|
|
792
|
+
colors_list = color if isinstance(color, list) else [f"C{i}" for i in range(n_groups)]
|
|
793
|
+
x = np.arange(n_cats)
|
|
794
|
+
width = 0.8 / n_groups
|
|
795
|
+
offsets = np.linspace(
|
|
796
|
+
-(n_groups - 1) * width / 2,
|
|
797
|
+
(n_groups - 1) * width / 2,
|
|
798
|
+
n_groups,
|
|
799
|
+
)
|
|
800
|
+
for i in range(n_groups):
|
|
801
|
+
g_err = (
|
|
802
|
+
yerr[i]
|
|
803
|
+
if (
|
|
804
|
+
yerr is not None
|
|
805
|
+
and hasattr(yerr, "__len__")
|
|
806
|
+
and len(yerr) == n_groups
|
|
807
|
+
and hasattr(yerr[0], "__len__")
|
|
808
|
+
)
|
|
809
|
+
else yerr
|
|
810
|
+
)
|
|
811
|
+
lbl = hue[i] if hue and i < len(hue) else f"Group {i + 1}"
|
|
812
|
+
ax.bar(
|
|
813
|
+
x + offsets[i],
|
|
814
|
+
vals[i],
|
|
815
|
+
width=width,
|
|
816
|
+
yerr=g_err,
|
|
817
|
+
label=lbl,
|
|
818
|
+
color=colors_list[i % len(colors_list)],
|
|
819
|
+
alpha=alpha,
|
|
820
|
+
capsize=3,
|
|
821
|
+
**kwargs,
|
|
822
|
+
)
|
|
823
|
+
ax.set_xticks(x)
|
|
824
|
+
ax.set_xticklabels(cats)
|
|
825
|
+
ax.legend()
|
|
826
|
+
|
|
827
|
+
elif kind == "stacked":
|
|
828
|
+
vals = vals if vals.ndim == 2 else vals.reshape(1, -1)
|
|
829
|
+
n_groups = vals.shape[0]
|
|
830
|
+
colors_list = color if isinstance(color, list) else [f"C{i}" for i in range(n_groups)]
|
|
831
|
+
bottom = np.zeros(len(cats))
|
|
832
|
+
for i in range(n_groups):
|
|
833
|
+
lbl = hue[i] if hue and i < len(hue) else f"Group {i + 1}"
|
|
834
|
+
ax.bar(
|
|
835
|
+
cats,
|
|
836
|
+
vals[i],
|
|
837
|
+
bottom=bottom,
|
|
838
|
+
label=lbl,
|
|
839
|
+
color=colors_list[i % len(colors_list)],
|
|
840
|
+
alpha=alpha,
|
|
841
|
+
**kwargs,
|
|
842
|
+
)
|
|
843
|
+
bottom = bottom + vals[i]
|
|
844
|
+
ax.legend()
|
|
845
|
+
|
|
846
|
+
else:
|
|
847
|
+
raise ValueError(
|
|
848
|
+
f"Unknown bar chart kind: '{kind}'. Use 'simple', 'grouped', or 'stacked'."
|
|
849
|
+
)
|
|
850
|
+
|
|
851
|
+
configure_axes(ax, xlabel=xlabel, ylabel=ylabel, title=title)
|
|
852
|
+
|
|
853
|
+
info: Dict[str, Any] = {
|
|
854
|
+
"kind": kind,
|
|
855
|
+
"n_categories": len(cats),
|
|
856
|
+
"n_groups": n_groups if kind != "simple" else 1,
|
|
857
|
+
}
|
|
858
|
+
return fig, ax, info
|
|
859
|
+
|
|
860
|
+
|
|
861
|
+
def waterfall_plot(
|
|
862
|
+
x: np.ndarray,
|
|
863
|
+
y_matrix: np.ndarray,
|
|
864
|
+
labels: Optional[List[str]] = None,
|
|
865
|
+
offset: Union[float, str] = "auto",
|
|
866
|
+
xlabel: Optional[str] = None,
|
|
867
|
+
ylabel: Optional[str] = None,
|
|
868
|
+
title: Optional[str] = None,
|
|
869
|
+
figsize: Tuple[float, float] = (10, 7),
|
|
870
|
+
cmap: str = "viridis",
|
|
871
|
+
alpha: float = 0.9,
|
|
872
|
+
linewidth: float = 1.5,
|
|
873
|
+
**kwargs,
|
|
874
|
+
) -> Tuple[Figure, Axes, Dict[str, Any]]:
|
|
875
|
+
"""Create a waterfall (stacked-offset line) plot.
|
|
876
|
+
|
|
877
|
+
Each row of *y_matrix* is plotted as a separate line shifted upward
|
|
878
|
+
by a constant *offset*, producing the characteristic waterfall
|
|
879
|
+
appearance used in time-resolved spectroscopy.
|
|
880
|
+
|
|
881
|
+
Parameters
|
|
882
|
+
----------
|
|
883
|
+
x : np.ndarray
|
|
884
|
+
Shared X axis (wavenumber, wavelength, time, …)
|
|
885
|
+
y_matrix : np.ndarray
|
|
886
|
+
2-D array of shape ``(n_traces, n_points)``. Each row is one
|
|
887
|
+
spectrum / signal trace.
|
|
888
|
+
labels : list of str, optional
|
|
889
|
+
Legend labels for each trace.
|
|
890
|
+
offset : float or 'auto', default 'auto'
|
|
891
|
+
Vertical spacing between traces. ``'auto'`` uses 60 % of the
|
|
892
|
+
largest peak-to-peak amplitude across all traces.
|
|
893
|
+
xlabel : str, optional
|
|
894
|
+
ylabel : str, optional
|
|
895
|
+
title : str, optional
|
|
896
|
+
figsize : tuple, default (10, 7)
|
|
897
|
+
cmap : str, default 'viridis'
|
|
898
|
+
Colormap used to color successive traces.
|
|
899
|
+
alpha : float, default 0.9
|
|
900
|
+
linewidth : float, default 1.5
|
|
901
|
+
kwargs : dict
|
|
902
|
+
Extra keyword arguments forwarded to ``ax.plot()``.
|
|
903
|
+
|
|
904
|
+
Returns
|
|
905
|
+
-------
|
|
906
|
+
fig : Figure
|
|
907
|
+
ax : Axes
|
|
908
|
+
info : dict
|
|
909
|
+
``n_traces``, ``offset``
|
|
910
|
+
|
|
911
|
+
Examples
|
|
912
|
+
--------
|
|
913
|
+
>>> x = np.linspace(400, 4000, 1000)
|
|
914
|
+
>>> spectra = np.random.rand(5, 1000)
|
|
915
|
+
>>> fig, ax, info = waterfall_plot(x, spectra)
|
|
916
|
+
"""
|
|
917
|
+
x = np.asarray(x)
|
|
918
|
+
y_mat = np.asarray(y_matrix)
|
|
919
|
+
if y_mat.ndim == 1:
|
|
920
|
+
y_mat = y_mat.reshape(1, -1)
|
|
921
|
+
|
|
922
|
+
n_traces = y_mat.shape[0]
|
|
923
|
+
|
|
924
|
+
if offset == "auto":
|
|
925
|
+
ptp = np.ptp(y_mat, axis=1)
|
|
926
|
+
offset_val = float(np.max(ptp) * 0.6) if np.max(ptp) > 0 else 1.0
|
|
927
|
+
else:
|
|
928
|
+
offset_val = float(offset)
|
|
929
|
+
|
|
930
|
+
fig, ax = create_figure(figsize=figsize)
|
|
931
|
+
cmap_obj = plt.get_cmap(cmap, n_traces)
|
|
932
|
+
|
|
933
|
+
for i in range(n_traces):
|
|
934
|
+
y_shifted = y_mat[i] + i * offset_val
|
|
935
|
+
clr = cmap_obj(i / max(n_traces - 1, 1))
|
|
936
|
+
lbl = labels[i] if (labels and i < len(labels)) else None
|
|
937
|
+
ax.plot(
|
|
938
|
+
x,
|
|
939
|
+
y_shifted,
|
|
940
|
+
color=clr,
|
|
941
|
+
alpha=alpha,
|
|
942
|
+
linewidth=linewidth,
|
|
943
|
+
label=lbl,
|
|
944
|
+
**kwargs,
|
|
945
|
+
)
|
|
946
|
+
|
|
947
|
+
configure_axes(
|
|
948
|
+
ax,
|
|
949
|
+
xlabel=xlabel,
|
|
950
|
+
ylabel=ylabel,
|
|
951
|
+
title=title,
|
|
952
|
+
legend=labels is not None,
|
|
953
|
+
)
|
|
954
|
+
|
|
955
|
+
info = {"n_traces": n_traces, "offset": offset_val}
|
|
956
|
+
return fig, ax, info
|
|
957
|
+
|
|
958
|
+
|
|
959
|
+
def dual_axis_plot(
|
|
960
|
+
x: np.ndarray,
|
|
961
|
+
y1: np.ndarray,
|
|
962
|
+
y2: np.ndarray,
|
|
963
|
+
xlabel: Optional[str] = None,
|
|
964
|
+
ylabel1: Optional[str] = None,
|
|
965
|
+
ylabel2: Optional[str] = None,
|
|
966
|
+
title: Optional[str] = None,
|
|
967
|
+
figsize: Tuple[float, float] = (8, 6),
|
|
968
|
+
color1: str = "steelblue",
|
|
969
|
+
color2: str = "tomato",
|
|
970
|
+
linestyle1: str = "-",
|
|
971
|
+
linestyle2: str = "--",
|
|
972
|
+
label1: Optional[str] = None,
|
|
973
|
+
label2: Optional[str] = None,
|
|
974
|
+
**kwargs,
|
|
975
|
+
) -> Tuple[Figure, Axes, Dict[str, Any]]:
|
|
976
|
+
"""Create a dual Y-axis line plot.
|
|
977
|
+
|
|
978
|
+
Plots *y1* on the left axis and *y2* on an independent right axis,
|
|
979
|
+
which is useful when comparing signals that have different units or
|
|
980
|
+
very different magnitudes.
|
|
981
|
+
|
|
982
|
+
Parameters
|
|
983
|
+
----------
|
|
984
|
+
x : np.ndarray
|
|
985
|
+
Shared X axis
|
|
986
|
+
y1 : np.ndarray
|
|
987
|
+
Data for the left Y axis
|
|
988
|
+
y2 : np.ndarray
|
|
989
|
+
Data for the right Y axis
|
|
990
|
+
xlabel : str, optional
|
|
991
|
+
ylabel1 : str, optional
|
|
992
|
+
Left Y-axis label
|
|
993
|
+
ylabel2 : str, optional
|
|
994
|
+
Right Y-axis label
|
|
995
|
+
title : str, optional
|
|
996
|
+
figsize : tuple, default (8, 6)
|
|
997
|
+
color1 : str, default 'steelblue'
|
|
998
|
+
Color for left-axis series
|
|
999
|
+
color2 : str, default 'tomato'
|
|
1000
|
+
Color for right-axis series
|
|
1001
|
+
linestyle1 : str, default '-'
|
|
1002
|
+
linestyle2 : str, default '--'
|
|
1003
|
+
label1 : str, optional
|
|
1004
|
+
Legend label for left series
|
|
1005
|
+
label2 : str, optional
|
|
1006
|
+
Legend label for right series
|
|
1007
|
+
kwargs : dict
|
|
1008
|
+
Extra keyword arguments forwarded to both ``ax.plot()`` calls.
|
|
1009
|
+
|
|
1010
|
+
Returns
|
|
1011
|
+
-------
|
|
1012
|
+
fig : Figure
|
|
1013
|
+
ax : Axes
|
|
1014
|
+
Primary (left) axes
|
|
1015
|
+
info : dict
|
|
1016
|
+
``ax2`` — the secondary (right) Axes object
|
|
1017
|
+
|
|
1018
|
+
Examples
|
|
1019
|
+
--------
|
|
1020
|
+
>>> x = np.linspace(0, 10, 100)
|
|
1021
|
+
>>> fig, ax, info = dual_axis_plot(
|
|
1022
|
+
... x, np.sin(x), np.exp(0.3 * x),
|
|
1023
|
+
... ylabel1='sin(x)', ylabel2='exp(0.3x)',
|
|
1024
|
+
... )
|
|
1025
|
+
"""
|
|
1026
|
+
x = np.asarray(x)
|
|
1027
|
+
y1 = np.asarray(y1)
|
|
1028
|
+
y2 = np.asarray(y2)
|
|
1029
|
+
|
|
1030
|
+
fig, ax1 = create_figure(figsize=figsize)
|
|
1031
|
+
ax2 = ax1.twinx()
|
|
1032
|
+
|
|
1033
|
+
lbl1 = label1 or ylabel1 or "Y1"
|
|
1034
|
+
lbl2 = label2 or ylabel2 or "Y2"
|
|
1035
|
+
|
|
1036
|
+
ax1.plot(x, y1, color=color1, linestyle=linestyle1, label=lbl1, **kwargs)
|
|
1037
|
+
ax2.plot(x, y2, color=color2, linestyle=linestyle2, label=lbl2, **kwargs)
|
|
1038
|
+
|
|
1039
|
+
if title:
|
|
1040
|
+
ax1.set_title(title)
|
|
1041
|
+
if xlabel:
|
|
1042
|
+
ax1.set_xlabel(xlabel)
|
|
1043
|
+
if ylabel1:
|
|
1044
|
+
ax1.set_ylabel(ylabel1, color=color1)
|
|
1045
|
+
ax1.tick_params(axis="y", labelcolor=color1)
|
|
1046
|
+
if ylabel2:
|
|
1047
|
+
ax2.set_ylabel(ylabel2, color=color2)
|
|
1048
|
+
ax2.tick_params(axis="y", labelcolor=color2)
|
|
1049
|
+
|
|
1050
|
+
# Merged legend from both axes
|
|
1051
|
+
h1, l1 = ax1.get_legend_handles_labels()
|
|
1052
|
+
h2, l2 = ax2.get_legend_handles_labels()
|
|
1053
|
+
ax1.legend(h1 + h2, l1 + l2, loc="best")
|
|
1054
|
+
|
|
1055
|
+
info: Dict[str, Any] = {"ax2": ax2}
|
|
1056
|
+
return fig, ax1, info
|
|
1057
|
+
|
|
1058
|
+
|
|
1059
|
+
# ============================================================================
|
|
1060
|
+
# Extended Plot Types (M21)
|
|
1061
|
+
# ============================================================================
|
|
1062
|
+
|
|
1063
|
+
|
|
1064
|
+
def z_colored_scatter(
|
|
1065
|
+
x,
|
|
1066
|
+
y,
|
|
1067
|
+
z,
|
|
1068
|
+
xlabel=None,
|
|
1069
|
+
ylabel=None,
|
|
1070
|
+
title=None,
|
|
1071
|
+
figsize=(8, 6),
|
|
1072
|
+
cmap="viridis",
|
|
1073
|
+
colorbar=True,
|
|
1074
|
+
colorbar_label=None,
|
|
1075
|
+
s=20,
|
|
1076
|
+
alpha=0.8,
|
|
1077
|
+
vmin=None,
|
|
1078
|
+
vmax=None,
|
|
1079
|
+
**kwargs,
|
|
1080
|
+
) -> Tuple[Figure, Axes, Dict[str, Any]]:
|
|
1081
|
+
"""Scatter plot with a third variable encoded as point color.
|
|
1082
|
+
|
|
1083
|
+
Parameters
|
|
1084
|
+
----------
|
|
1085
|
+
x, y : array-like
|
|
1086
|
+
Point coordinates.
|
|
1087
|
+
z : array-like
|
|
1088
|
+
Values to map to color.
|
|
1089
|
+
cmap : str, default 'viridis'
|
|
1090
|
+
Matplotlib colormap name.
|
|
1091
|
+
colorbar : bool, default True
|
|
1092
|
+
Whether to add a colorbar.
|
|
1093
|
+
colorbar_label : str, optional
|
|
1094
|
+
Label for the colorbar.
|
|
1095
|
+
s : float, default 20
|
|
1096
|
+
Marker size.
|
|
1097
|
+
alpha : float, default 0.8
|
|
1098
|
+
Marker opacity.
|
|
1099
|
+
vmin, vmax : float, optional
|
|
1100
|
+
Color scale limits.
|
|
1101
|
+
|
|
1102
|
+
Returns
|
|
1103
|
+
-------
|
|
1104
|
+
fig, ax, info : tuple
|
|
1105
|
+
info keys: 'n_points', 'z_min', 'z_max'.
|
|
1106
|
+
"""
|
|
1107
|
+
x, y, z = np.asarray(x), np.asarray(y), np.asarray(z)
|
|
1108
|
+
fig, ax = create_figure(figsize=figsize)
|
|
1109
|
+
sc = ax.scatter(x, y, c=z, cmap=cmap, s=s, alpha=alpha, vmin=vmin, vmax=vmax, **kwargs)
|
|
1110
|
+
if colorbar:
|
|
1111
|
+
cb = plt.colorbar(sc, ax=ax)
|
|
1112
|
+
if colorbar_label:
|
|
1113
|
+
cb.set_label(colorbar_label)
|
|
1114
|
+
configure_axes(ax, xlabel=xlabel, ylabel=ylabel, title=title)
|
|
1115
|
+
info: Dict[str, Any] = {
|
|
1116
|
+
"n_points": len(x),
|
|
1117
|
+
"z_min": float(np.nanmin(z)),
|
|
1118
|
+
"z_max": float(np.nanmax(z)),
|
|
1119
|
+
}
|
|
1120
|
+
return fig, ax, info
|
|
1121
|
+
|
|
1122
|
+
|
|
1123
|
+
def bubble_chart(
|
|
1124
|
+
x,
|
|
1125
|
+
y,
|
|
1126
|
+
sizes,
|
|
1127
|
+
z=None,
|
|
1128
|
+
xlabel=None,
|
|
1129
|
+
ylabel=None,
|
|
1130
|
+
title=None,
|
|
1131
|
+
figsize=(8, 6),
|
|
1132
|
+
cmap="viridis",
|
|
1133
|
+
colorbar=True,
|
|
1134
|
+
colorbar_label=None,
|
|
1135
|
+
color="steelblue",
|
|
1136
|
+
alpha=0.6,
|
|
1137
|
+
size_scale=1.0,
|
|
1138
|
+
**kwargs,
|
|
1139
|
+
) -> Tuple[Figure, Axes, Dict[str, Any]]:
|
|
1140
|
+
"""Scatter plot with size-encoded bubbles and optional color encoding.
|
|
1141
|
+
|
|
1142
|
+
Parameters
|
|
1143
|
+
----------
|
|
1144
|
+
x, y : array-like
|
|
1145
|
+
Point coordinates.
|
|
1146
|
+
sizes : array-like
|
|
1147
|
+
Values that control bubble area.
|
|
1148
|
+
z : array-like, optional
|
|
1149
|
+
Values mapped to color.
|
|
1150
|
+
size_scale : float, default 1.0
|
|
1151
|
+
Multiplicative scale applied to *sizes*.
|
|
1152
|
+
|
|
1153
|
+
Returns
|
|
1154
|
+
-------
|
|
1155
|
+
fig, ax, info : tuple
|
|
1156
|
+
info keys: 'n_points', 'size_min', 'size_max'.
|
|
1157
|
+
"""
|
|
1158
|
+
x, y, s = np.asarray(x), np.asarray(y), np.asarray(sizes, dtype=float)
|
|
1159
|
+
s_scaled = s * size_scale
|
|
1160
|
+
fig, ax = create_figure(figsize=figsize)
|
|
1161
|
+
if z is not None:
|
|
1162
|
+
z_arr = np.asarray(z)
|
|
1163
|
+
sc = ax.scatter(x, y, s=s_scaled, c=z_arr, cmap=cmap, alpha=alpha, **kwargs)
|
|
1164
|
+
if colorbar:
|
|
1165
|
+
cb = plt.colorbar(sc, ax=ax)
|
|
1166
|
+
if colorbar_label:
|
|
1167
|
+
cb.set_label(colorbar_label)
|
|
1168
|
+
else:
|
|
1169
|
+
ax.scatter(x, y, s=s_scaled, color=color, alpha=alpha, **kwargs)
|
|
1170
|
+
configure_axes(ax, xlabel=xlabel, ylabel=ylabel, title=title)
|
|
1171
|
+
info: Dict[str, Any] = {
|
|
1172
|
+
"n_points": len(x),
|
|
1173
|
+
"size_min": float(np.nanmin(s)),
|
|
1174
|
+
"size_max": float(np.nanmax(s)),
|
|
1175
|
+
}
|
|
1176
|
+
return fig, ax, info
|
|
1177
|
+
|
|
1178
|
+
|
|
1179
|
+
def polar_plot(
|
|
1180
|
+
theta,
|
|
1181
|
+
r,
|
|
1182
|
+
title=None,
|
|
1183
|
+
figsize=(7, 7),
|
|
1184
|
+
color="steelblue",
|
|
1185
|
+
linestyle="-",
|
|
1186
|
+
linewidth=1.5,
|
|
1187
|
+
fill=False,
|
|
1188
|
+
fill_alpha=0.15,
|
|
1189
|
+
theta_direction=-1,
|
|
1190
|
+
theta_zero_location="N",
|
|
1191
|
+
**kwargs,
|
|
1192
|
+
) -> Tuple[Figure, Axes, Dict[str, Any]]:
|
|
1193
|
+
"""Create a polar line/area plot.
|
|
1194
|
+
|
|
1195
|
+
Parameters
|
|
1196
|
+
----------
|
|
1197
|
+
theta : array-like
|
|
1198
|
+
Angles in radians.
|
|
1199
|
+
r : array-like
|
|
1200
|
+
Radial values.
|
|
1201
|
+
fill : bool, default False
|
|
1202
|
+
Whether to fill the area under the curve.
|
|
1203
|
+
theta_direction : int, default -1
|
|
1204
|
+
-1 for clockwise, 1 for counter-clockwise.
|
|
1205
|
+
theta_zero_location : str, default 'N'
|
|
1206
|
+
Position of 0°: 'N', 'E', 'S', or 'W'.
|
|
1207
|
+
|
|
1208
|
+
Returns
|
|
1209
|
+
-------
|
|
1210
|
+
fig, ax, info : tuple
|
|
1211
|
+
info keys: 'n_points', 'r_max'.
|
|
1212
|
+
"""
|
|
1213
|
+
theta_arr = np.asarray(theta, dtype=float)
|
|
1214
|
+
r_arr = np.asarray(r, dtype=float)
|
|
1215
|
+
fig, ax = plt.subplots(figsize=figsize, subplot_kw={"projection": "polar"})
|
|
1216
|
+
# subplot_kw guarantees a PolarAxes at runtime; plt.subplots is typed as returning
|
|
1217
|
+
# the base Axes, which has no set_theta_* methods.
|
|
1218
|
+
polar_ax = cast("PolarAxes", ax)
|
|
1219
|
+
polar_ax.set_theta_zero_location(theta_zero_location)
|
|
1220
|
+
polar_ax.set_theta_direction(theta_direction)
|
|
1221
|
+
ax.plot(theta_arr, r_arr, color=color, linestyle=linestyle, linewidth=linewidth, **kwargs)
|
|
1222
|
+
if fill:
|
|
1223
|
+
ax.fill(theta_arr, r_arr, alpha=fill_alpha, color=color)
|
|
1224
|
+
if title:
|
|
1225
|
+
ax.set_title(title, pad=15)
|
|
1226
|
+
info: Dict[str, Any] = {"n_points": len(theta_arr), "r_max": float(np.nanmax(r_arr))}
|
|
1227
|
+
return fig, ax, info
|
|
1228
|
+
|
|
1229
|
+
|
|
1230
|
+
def pair_plot(
|
|
1231
|
+
data,
|
|
1232
|
+
hue=None,
|
|
1233
|
+
vars=None,
|
|
1234
|
+
kind="scatter",
|
|
1235
|
+
diag_kind="hist",
|
|
1236
|
+
title=None,
|
|
1237
|
+
figsize=None,
|
|
1238
|
+
palette=None,
|
|
1239
|
+
**kwargs,
|
|
1240
|
+
) -> Tuple[Figure, Axes, Dict[str, Any]]:
|
|
1241
|
+
"""Create a pair plot (scatter matrix) using seaborn.
|
|
1242
|
+
|
|
1243
|
+
Parameters
|
|
1244
|
+
----------
|
|
1245
|
+
data : pd.DataFrame
|
|
1246
|
+
Input data.
|
|
1247
|
+
hue : str, optional
|
|
1248
|
+
Column name to use for color encoding.
|
|
1249
|
+
vars : list of str, optional
|
|
1250
|
+
Columns to include; defaults to all numeric columns.
|
|
1251
|
+
kind : str, default 'scatter'
|
|
1252
|
+
Off-diagonal plot type: 'scatter', 'kde', 'hist', 'reg'.
|
|
1253
|
+
diag_kind : str, default 'hist'
|
|
1254
|
+
Diagonal plot type: 'hist', 'kde', 'auto'.
|
|
1255
|
+
|
|
1256
|
+
Returns
|
|
1257
|
+
-------
|
|
1258
|
+
fig, ax, info : tuple
|
|
1259
|
+
ax is grid.axes[0][0]; info keys: 'n_vars', 'hue'.
|
|
1260
|
+
|
|
1261
|
+
Raises
|
|
1262
|
+
------
|
|
1263
|
+
ImportError
|
|
1264
|
+
If seaborn is not installed.
|
|
1265
|
+
"""
|
|
1266
|
+
if not HAS_SEABORN:
|
|
1267
|
+
raise ImportError("seaborn is required for pair_plot. Install it with: pip install seaborn")
|
|
1268
|
+
import seaborn as sns
|
|
1269
|
+
|
|
1270
|
+
kw: Dict[str, Any] = dict(kind=kind, diag_kind=diag_kind)
|
|
1271
|
+
if hue is not None:
|
|
1272
|
+
kw["hue"] = hue
|
|
1273
|
+
if vars is not None:
|
|
1274
|
+
kw["vars"] = vars
|
|
1275
|
+
if palette is not None:
|
|
1276
|
+
kw["palette"] = palette
|
|
1277
|
+
if figsize is not None:
|
|
1278
|
+
n_vars = len(vars) if vars else len(data.select_dtypes("number").columns)
|
|
1279
|
+
kw["height"] = figsize[1] / max(n_vars, 1)
|
|
1280
|
+
grid = sns.pairplot(data, **kw, **kwargs)
|
|
1281
|
+
if title:
|
|
1282
|
+
grid.figure.suptitle(title, y=1.02)
|
|
1283
|
+
n_vars = len(vars) if vars else len(data.select_dtypes(include="number").columns)
|
|
1284
|
+
info: Dict[str, Any] = {"n_vars": n_vars, "hue": hue}
|
|
1285
|
+
ax = grid.axes[0][0]
|
|
1286
|
+
return grid.figure, ax, info
|
|
1287
|
+
|
|
1288
|
+
|
|
1289
|
+
def histogram_2d(
|
|
1290
|
+
x,
|
|
1291
|
+
y,
|
|
1292
|
+
bins=30,
|
|
1293
|
+
xlabel=None,
|
|
1294
|
+
ylabel=None,
|
|
1295
|
+
title=None,
|
|
1296
|
+
figsize=(8, 6),
|
|
1297
|
+
cmap="viridis",
|
|
1298
|
+
colorbar=True,
|
|
1299
|
+
mode="hist2d",
|
|
1300
|
+
gridsize=30,
|
|
1301
|
+
vmin=None,
|
|
1302
|
+
vmax=None,
|
|
1303
|
+
**kwargs,
|
|
1304
|
+
) -> Tuple[Figure, Axes, Dict[str, Any]]:
|
|
1305
|
+
"""Create a 2D histogram or hexbin density plot.
|
|
1306
|
+
|
|
1307
|
+
Parameters
|
|
1308
|
+
----------
|
|
1309
|
+
x, y : array-like
|
|
1310
|
+
Input data arrays.
|
|
1311
|
+
bins : int, default 30
|
|
1312
|
+
Number of bins (used when mode='hist2d').
|
|
1313
|
+
mode : {'hist2d', 'hexbin'}, default 'hist2d'
|
|
1314
|
+
Plot mode.
|
|
1315
|
+
gridsize : int, default 30
|
|
1316
|
+
Hexbin grid size (used when mode='hexbin').
|
|
1317
|
+
|
|
1318
|
+
Returns
|
|
1319
|
+
-------
|
|
1320
|
+
fig, ax, info : tuple
|
|
1321
|
+
info keys: 'n_points', 'mode'.
|
|
1322
|
+
"""
|
|
1323
|
+
x, y = np.asarray(x, dtype=float), np.asarray(y, dtype=float)
|
|
1324
|
+
fig, ax = create_figure(figsize=figsize)
|
|
1325
|
+
if mode == "hexbin":
|
|
1326
|
+
hb = ax.hexbin(x, y, gridsize=gridsize, cmap=cmap, vmin=vmin, vmax=vmax, **kwargs)
|
|
1327
|
+
if colorbar:
|
|
1328
|
+
plt.colorbar(hb, ax=ax, label="count")
|
|
1329
|
+
else:
|
|
1330
|
+
h, xedges, yedges, img = ax.hist2d(
|
|
1331
|
+
x, y, bins=bins, cmap=cmap, vmin=vmin, vmax=vmax, **kwargs
|
|
1332
|
+
)
|
|
1333
|
+
if colorbar:
|
|
1334
|
+
plt.colorbar(img, ax=ax, label="count")
|
|
1335
|
+
configure_axes(ax, xlabel=xlabel, ylabel=ylabel, title=title, grid=False)
|
|
1336
|
+
info: Dict[str, Any] = {"n_points": len(x), "mode": mode}
|
|
1337
|
+
return fig, ax, info
|
|
1338
|
+
|
|
1339
|
+
|
|
1340
|
+
def interactive_3d_scatter(
|
|
1341
|
+
x,
|
|
1342
|
+
y,
|
|
1343
|
+
z,
|
|
1344
|
+
color=None,
|
|
1345
|
+
size=None,
|
|
1346
|
+
labels=None,
|
|
1347
|
+
xlabel=None,
|
|
1348
|
+
ylabel=None,
|
|
1349
|
+
zlabel=None,
|
|
1350
|
+
title=None,
|
|
1351
|
+
colorscale="Viridis",
|
|
1352
|
+
opacity=0.8,
|
|
1353
|
+
marker_size=5,
|
|
1354
|
+
**kwargs,
|
|
1355
|
+
) -> Tuple[Any, Dict[str, Any]]:
|
|
1356
|
+
"""Create an interactive 3D scatter plot using Plotly.
|
|
1357
|
+
|
|
1358
|
+
Parameters
|
|
1359
|
+
----------
|
|
1360
|
+
x, y, z : array-like
|
|
1361
|
+
Point coordinates.
|
|
1362
|
+
color : array-like, optional
|
|
1363
|
+
Values mapped to marker color.
|
|
1364
|
+
size : array-like, optional
|
|
1365
|
+
Values mapped to marker size.
|
|
1366
|
+
labels : array-like, optional
|
|
1367
|
+
Hover text labels.
|
|
1368
|
+
colorscale : str, default 'Viridis'
|
|
1369
|
+
Plotly colorscale name.
|
|
1370
|
+
opacity : float, default 0.8
|
|
1371
|
+
Marker opacity.
|
|
1372
|
+
marker_size : int, default 5
|
|
1373
|
+
Default marker size (overridden by *size* if given).
|
|
1374
|
+
|
|
1375
|
+
Returns
|
|
1376
|
+
-------
|
|
1377
|
+
fig, info : tuple
|
|
1378
|
+
info keys: 'n_points'.
|
|
1379
|
+
|
|
1380
|
+
Raises
|
|
1381
|
+
------
|
|
1382
|
+
ImportError
|
|
1383
|
+
If plotly is not installed.
|
|
1384
|
+
"""
|
|
1385
|
+
if not HAS_PLOTLY:
|
|
1386
|
+
raise ImportError(
|
|
1387
|
+
"plotly is required for interactive_3d_scatter. Install it with: pip install plotly"
|
|
1388
|
+
)
|
|
1389
|
+
x_arr, y_arr, z_arr = np.asarray(x), np.asarray(y), np.asarray(z)
|
|
1390
|
+
|
|
1391
|
+
marker_kw: Dict[str, Any] = {"size": marker_size, "opacity": opacity}
|
|
1392
|
+
if color is not None:
|
|
1393
|
+
c_arr = np.asarray(color)
|
|
1394
|
+
marker_kw["color"] = c_arr
|
|
1395
|
+
marker_kw["colorscale"] = colorscale
|
|
1396
|
+
marker_kw["showscale"] = True
|
|
1397
|
+
if size is not None:
|
|
1398
|
+
marker_kw["size"] = np.asarray(size)
|
|
1399
|
+
|
|
1400
|
+
scatter3d = go.Scatter3d(
|
|
1401
|
+
x=x_arr,
|
|
1402
|
+
y=y_arr,
|
|
1403
|
+
z=z_arr,
|
|
1404
|
+
mode="markers",
|
|
1405
|
+
marker=marker_kw,
|
|
1406
|
+
text=labels,
|
|
1407
|
+
**kwargs,
|
|
1408
|
+
)
|
|
1409
|
+
fig = go.Figure(data=[scatter3d])
|
|
1410
|
+
fig.update_layout(
|
|
1411
|
+
title=title or "",
|
|
1412
|
+
scene=dict(
|
|
1413
|
+
xaxis_title=xlabel or "X",
|
|
1414
|
+
yaxis_title=ylabel or "Y",
|
|
1415
|
+
zaxis_title=zlabel or "Z",
|
|
1416
|
+
),
|
|
1417
|
+
)
|
|
1418
|
+
info: Dict[str, Any] = {"n_points": len(x_arr)}
|
|
1419
|
+
return fig, info
|
|
1420
|
+
|
|
1421
|
+
|
|
1422
|
+
def scatter_with_regression(
|
|
1423
|
+
x: np.ndarray,
|
|
1424
|
+
y: np.ndarray,
|
|
1425
|
+
xlabel: Optional[str] = None,
|
|
1426
|
+
ylabel: Optional[str] = None,
|
|
1427
|
+
title: Optional[str] = None,
|
|
1428
|
+
figsize: Tuple[float, float] = (8, 6),
|
|
1429
|
+
color: str = "steelblue",
|
|
1430
|
+
line_color: str = "tomato",
|
|
1431
|
+
alpha: float = 0.7,
|
|
1432
|
+
show_ci: bool = True,
|
|
1433
|
+
ci_alpha: float = 0.15,
|
|
1434
|
+
show_equation: bool = True,
|
|
1435
|
+
**kwargs,
|
|
1436
|
+
) -> Tuple[Figure, Axes, Dict[str, Any]]:
|
|
1437
|
+
"""Scatter plot with a linear regression line and optional confidence band.
|
|
1438
|
+
|
|
1439
|
+
Parameters
|
|
1440
|
+
----------
|
|
1441
|
+
x, y : array-like
|
|
1442
|
+
Data arrays.
|
|
1443
|
+
xlabel, ylabel, title : str, optional
|
|
1444
|
+
figsize : tuple, default (8, 6)
|
|
1445
|
+
color : str, default 'steelblue'
|
|
1446
|
+
Scatter point color.
|
|
1447
|
+
line_color : str, default 'tomato'
|
|
1448
|
+
Regression line color.
|
|
1449
|
+
alpha : float, default 0.7
|
|
1450
|
+
Scatter point opacity.
|
|
1451
|
+
show_ci : bool, default True
|
|
1452
|
+
Shade a 95% confidence band around the regression line.
|
|
1453
|
+
ci_alpha : float, default 0.15
|
|
1454
|
+
Opacity of the confidence band.
|
|
1455
|
+
show_equation : bool, default True
|
|
1456
|
+
Annotate the plot with the regression equation and R².
|
|
1457
|
+
kwargs
|
|
1458
|
+
Extra keyword arguments forwarded to ``ax.scatter()``.
|
|
1459
|
+
|
|
1460
|
+
Returns
|
|
1461
|
+
-------
|
|
1462
|
+
fig : Figure
|
|
1463
|
+
ax : Axes
|
|
1464
|
+
info : dict
|
|
1465
|
+
``slope``, ``intercept``, ``r_squared``, ``p_value``, ``stderr``
|
|
1466
|
+
"""
|
|
1467
|
+
from scipy import stats as _stats
|
|
1468
|
+
|
|
1469
|
+
x_arr = np.asarray(x, dtype=float)
|
|
1470
|
+
y_arr = np.asarray(y, dtype=float)
|
|
1471
|
+
|
|
1472
|
+
result = _stats.linregress(x_arr, y_arr)
|
|
1473
|
+
slope, intercept, r_value, p_value, stderr = (
|
|
1474
|
+
result.slope,
|
|
1475
|
+
result.intercept,
|
|
1476
|
+
result.rvalue,
|
|
1477
|
+
result.pvalue,
|
|
1478
|
+
result.stderr,
|
|
1479
|
+
)
|
|
1480
|
+
|
|
1481
|
+
fig, ax = create_figure(figsize=figsize)
|
|
1482
|
+
ax.scatter(x_arr, y_arr, color=color, alpha=alpha, zorder=3, **kwargs)
|
|
1483
|
+
|
|
1484
|
+
x_fit = np.linspace(x_arr.min(), x_arr.max(), 300)
|
|
1485
|
+
y_fit = slope * x_fit + intercept
|
|
1486
|
+
ax.plot(x_fit, y_fit, color=line_color, linewidth=1.8, label="Regression line")
|
|
1487
|
+
|
|
1488
|
+
if show_ci:
|
|
1489
|
+
n = len(x_arr)
|
|
1490
|
+
t_crit = _stats.t.ppf(0.975, df=n - 2)
|
|
1491
|
+
se_fit = stderr * np.sqrt(
|
|
1492
|
+
1.0 / n + (x_fit - x_arr.mean()) ** 2 / np.sum((x_arr - x_arr.mean()) ** 2)
|
|
1493
|
+
)
|
|
1494
|
+
ax.fill_between(
|
|
1495
|
+
x_fit,
|
|
1496
|
+
y_fit - t_crit * se_fit,
|
|
1497
|
+
y_fit + t_crit * se_fit,
|
|
1498
|
+
color=line_color,
|
|
1499
|
+
alpha=ci_alpha,
|
|
1500
|
+
label="95% CI",
|
|
1501
|
+
)
|
|
1502
|
+
|
|
1503
|
+
if show_equation:
|
|
1504
|
+
eq = (
|
|
1505
|
+
f"y = {slope:.4g}x {'+' if intercept >= 0 else '−'} {abs(intercept):.4g}\n"
|
|
1506
|
+
f"R² = {r_value**2:.4f} p = {p_value:.3g}"
|
|
1507
|
+
)
|
|
1508
|
+
ax.annotate(
|
|
1509
|
+
eq,
|
|
1510
|
+
xy=(0.05, 0.95),
|
|
1511
|
+
xycoords="axes fraction",
|
|
1512
|
+
va="top",
|
|
1513
|
+
fontsize=9,
|
|
1514
|
+
bbox=dict(boxstyle="round,pad=0.3", fc="white", alpha=0.7),
|
|
1515
|
+
)
|
|
1516
|
+
|
|
1517
|
+
configure_axes(ax, xlabel=xlabel, ylabel=ylabel, title=title, legend=show_ci)
|
|
1518
|
+
|
|
1519
|
+
info: Dict[str, Any] = {
|
|
1520
|
+
"slope": float(slope),
|
|
1521
|
+
"intercept": float(intercept),
|
|
1522
|
+
"r_squared": float(r_value**2),
|
|
1523
|
+
"p_value": float(p_value),
|
|
1524
|
+
"stderr": float(stderr),
|
|
1525
|
+
}
|
|
1526
|
+
return fig, ax, info
|
|
1527
|
+
|
|
1528
|
+
|
|
1529
|
+
def residual_plot(
|
|
1530
|
+
x: np.ndarray,
|
|
1531
|
+
y_actual: np.ndarray,
|
|
1532
|
+
y_fitted: np.ndarray,
|
|
1533
|
+
xlabel: Optional[str] = None,
|
|
1534
|
+
ylabel: Optional[str] = "Residual",
|
|
1535
|
+
title: Optional[str] = "Residual Plot",
|
|
1536
|
+
figsize: Tuple[float, float] = (8, 5),
|
|
1537
|
+
color_pos: str = "steelblue",
|
|
1538
|
+
color_neg: str = "tomato",
|
|
1539
|
+
alpha: float = 0.75,
|
|
1540
|
+
show_zero_line: bool = True,
|
|
1541
|
+
vs_fitted: bool = False,
|
|
1542
|
+
**kwargs,
|
|
1543
|
+
) -> Tuple[Figure, Axes, Dict[str, Any]]:
|
|
1544
|
+
"""Plot residuals from a curve fit.
|
|
1545
|
+
|
|
1546
|
+
Parameters
|
|
1547
|
+
----------
|
|
1548
|
+
x : array-like
|
|
1549
|
+
Original x-axis values.
|
|
1550
|
+
y_actual : array-like
|
|
1551
|
+
Observed y values.
|
|
1552
|
+
y_fitted : array-like
|
|
1553
|
+
Model-predicted y values (from any fit).
|
|
1554
|
+
xlabel : str, optional
|
|
1555
|
+
X-axis label; defaults to 'x' or 'Fitted value' when ``vs_fitted=True``.
|
|
1556
|
+
ylabel : str, default 'Residual'
|
|
1557
|
+
title : str, default 'Residual Plot'
|
|
1558
|
+
figsize : tuple, default (8, 5)
|
|
1559
|
+
color_pos : str, default 'steelblue'
|
|
1560
|
+
Color for positive residuals.
|
|
1561
|
+
color_neg : str, default 'tomato'
|
|
1562
|
+
Color for negative residuals.
|
|
1563
|
+
alpha : float, default 0.75
|
|
1564
|
+
show_zero_line : bool, default True
|
|
1565
|
+
Draw a horizontal reference line at y = 0.
|
|
1566
|
+
vs_fitted : bool, default False
|
|
1567
|
+
If True, plot residuals vs. fitted values instead of vs. x.
|
|
1568
|
+
kwargs
|
|
1569
|
+
Extra keyword arguments forwarded to ``ax.scatter()``.
|
|
1570
|
+
|
|
1571
|
+
Returns
|
|
1572
|
+
-------
|
|
1573
|
+
fig : Figure
|
|
1574
|
+
ax : Axes
|
|
1575
|
+
info : dict
|
|
1576
|
+
``n_points``, ``rmse``, ``max_residual``, ``mean_residual``
|
|
1577
|
+
"""
|
|
1578
|
+
x_arr = np.asarray(x, dtype=float)
|
|
1579
|
+
ya = np.asarray(y_actual, dtype=float)
|
|
1580
|
+
yf = np.asarray(y_fitted, dtype=float)
|
|
1581
|
+
residuals = ya - yf
|
|
1582
|
+
|
|
1583
|
+
x_plot = yf if vs_fitted else x_arr
|
|
1584
|
+
default_xlabel = "Fitted value" if vs_fitted else (xlabel or "x")
|
|
1585
|
+
|
|
1586
|
+
colors = np.where(residuals >= 0, color_pos, color_neg)
|
|
1587
|
+
|
|
1588
|
+
fig, ax = create_figure(figsize=figsize)
|
|
1589
|
+
ax.scatter(x_plot, residuals, c=colors, alpha=alpha, zorder=3, **kwargs)
|
|
1590
|
+
|
|
1591
|
+
if show_zero_line:
|
|
1592
|
+
ax.axhline(0, color="black", linewidth=1.0, linestyle="--", zorder=2)
|
|
1593
|
+
|
|
1594
|
+
configure_axes(ax, xlabel=default_xlabel, ylabel=ylabel, title=title)
|
|
1595
|
+
|
|
1596
|
+
rmse = float(np.sqrt(np.mean(residuals**2)))
|
|
1597
|
+
info: Dict[str, Any] = {
|
|
1598
|
+
"n_points": len(residuals),
|
|
1599
|
+
"rmse": rmse,
|
|
1600
|
+
"max_residual": float(np.max(np.abs(residuals))),
|
|
1601
|
+
"mean_residual": float(np.mean(residuals)),
|
|
1602
|
+
}
|
|
1603
|
+
return fig, ax, info
|
|
1604
|
+
|
|
1605
|
+
|
|
1606
|
+
def interactive_ternary(
|
|
1607
|
+
a: np.ndarray,
|
|
1608
|
+
b: np.ndarray,
|
|
1609
|
+
c: np.ndarray,
|
|
1610
|
+
color: Optional[np.ndarray] = None,
|
|
1611
|
+
labels: Optional[List[str]] = None,
|
|
1612
|
+
a_label: str = "A",
|
|
1613
|
+
b_label: str = "B",
|
|
1614
|
+
c_label: str = "C",
|
|
1615
|
+
title: Optional[str] = None,
|
|
1616
|
+
colorscale: str = "Viridis",
|
|
1617
|
+
marker_size: int = 8,
|
|
1618
|
+
opacity: float = 0.85,
|
|
1619
|
+
**kwargs,
|
|
1620
|
+
) -> Tuple[Any, Dict[str, Any]]:
|
|
1621
|
+
"""Create an interactive ternary scatter plot using Plotly.
|
|
1622
|
+
|
|
1623
|
+
Each point represents a 3-component composition (a + b + c should ≈ 1 or
|
|
1624
|
+
a constant sum). Useful for phase diagrams, solvent composition studies,
|
|
1625
|
+
and materials science.
|
|
1626
|
+
|
|
1627
|
+
Parameters
|
|
1628
|
+
----------
|
|
1629
|
+
a, b, c : array-like
|
|
1630
|
+
Component fractions for each axis.
|
|
1631
|
+
color : array-like, optional
|
|
1632
|
+
Numeric values mapped to a colorscale.
|
|
1633
|
+
labels : list of str, optional
|
|
1634
|
+
Hover text labels for each point.
|
|
1635
|
+
a_label, b_label, c_label : str
|
|
1636
|
+
Axis labels for the three components.
|
|
1637
|
+
title : str, optional
|
|
1638
|
+
colorscale : str, default 'Viridis'
|
|
1639
|
+
marker_size : int, default 8
|
|
1640
|
+
opacity : float, default 0.85
|
|
1641
|
+
kwargs
|
|
1642
|
+
Extra keyword arguments forwarded to ``go.Scatterternary``.
|
|
1643
|
+
|
|
1644
|
+
Returns
|
|
1645
|
+
-------
|
|
1646
|
+
fig : go.Figure
|
|
1647
|
+
info : dict
|
|
1648
|
+
``n_points``
|
|
1649
|
+
|
|
1650
|
+
Raises
|
|
1651
|
+
------
|
|
1652
|
+
ImportError
|
|
1653
|
+
If plotly is not installed.
|
|
1654
|
+
"""
|
|
1655
|
+
if not HAS_PLOTLY:
|
|
1656
|
+
raise ImportError(
|
|
1657
|
+
"plotly is required for interactive_ternary. Install it with: pip install plotly"
|
|
1658
|
+
)
|
|
1659
|
+
a_arr = np.asarray(a, dtype=float)
|
|
1660
|
+
b_arr = np.asarray(b, dtype=float)
|
|
1661
|
+
c_arr = np.asarray(c, dtype=float)
|
|
1662
|
+
|
|
1663
|
+
marker_kw: Dict[str, Any] = {"size": marker_size, "opacity": opacity}
|
|
1664
|
+
if color is not None:
|
|
1665
|
+
marker_kw["color"] = np.asarray(color, dtype=float)
|
|
1666
|
+
marker_kw["colorscale"] = colorscale
|
|
1667
|
+
marker_kw["showscale"] = True
|
|
1668
|
+
|
|
1669
|
+
trace = go.Scatterternary(
|
|
1670
|
+
a=a_arr,
|
|
1671
|
+
b=b_arr,
|
|
1672
|
+
c=c_arr,
|
|
1673
|
+
mode="markers",
|
|
1674
|
+
marker=marker_kw,
|
|
1675
|
+
text=labels,
|
|
1676
|
+
hovertemplate=(
|
|
1677
|
+
f"{a_label}: %{{a:.3f}}<br>"
|
|
1678
|
+
f"{b_label}: %{{b:.3f}}<br>"
|
|
1679
|
+
f"{c_label}: %{{c:.3f}}<extra></extra>"
|
|
1680
|
+
),
|
|
1681
|
+
**kwargs,
|
|
1682
|
+
)
|
|
1683
|
+
fig = go.Figure(data=[trace])
|
|
1684
|
+
fig.update_layout(
|
|
1685
|
+
title=title or "",
|
|
1686
|
+
ternary={
|
|
1687
|
+
"aaxis": {"title": a_label},
|
|
1688
|
+
"baxis": {"title": b_label},
|
|
1689
|
+
"caxis": {"title": c_label},
|
|
1690
|
+
},
|
|
1691
|
+
)
|
|
1692
|
+
return fig, {"n_points": len(a_arr)}
|
|
1693
|
+
|
|
1694
|
+
|
|
1695
|
+
def broken_axis_plot(
|
|
1696
|
+
x: np.ndarray,
|
|
1697
|
+
y: np.ndarray,
|
|
1698
|
+
breaks: List[Tuple[float, float]],
|
|
1699
|
+
axis: str = "x",
|
|
1700
|
+
xlabel: Optional[str] = None,
|
|
1701
|
+
ylabel: Optional[str] = None,
|
|
1702
|
+
title: Optional[str] = None,
|
|
1703
|
+
figsize: Tuple[float, float] = (9, 5),
|
|
1704
|
+
color: str = "steelblue",
|
|
1705
|
+
linewidth: float = 1.5,
|
|
1706
|
+
alpha: float = 1.0,
|
|
1707
|
+
d: float = 0.012,
|
|
1708
|
+
**kwargs,
|
|
1709
|
+
) -> Tuple[Figure, Axes, Dict[str, Any]]:
|
|
1710
|
+
"""Line plot with one or more broken axis segments.
|
|
1711
|
+
|
|
1712
|
+
Creates adjacent subplots for each unbroken segment and hides the inner
|
|
1713
|
+
spines to give the visual appearance of a continuous broken axis.
|
|
1714
|
+
|
|
1715
|
+
Parameters
|
|
1716
|
+
----------
|
|
1717
|
+
x, y : array-like
|
|
1718
|
+
Data arrays. x must be sorted ascending.
|
|
1719
|
+
breaks : list of (lo, hi) tuples
|
|
1720
|
+
Each tuple defines a region to *exclude* from the plot.
|
|
1721
|
+
E.g. ``breaks=[(800, 1200)]`` skips x values between 800 and 1200.
|
|
1722
|
+
axis : {'x', 'y'}, default 'x'
|
|
1723
|
+
Which axis to break.
|
|
1724
|
+
xlabel, ylabel, title : str, optional
|
|
1725
|
+
figsize : tuple, default (9, 5)
|
|
1726
|
+
color : str, default 'steelblue'
|
|
1727
|
+
linewidth : float, default 1.5
|
|
1728
|
+
alpha : float, default 1.0
|
|
1729
|
+
d : float, default 0.012
|
|
1730
|
+
Size of the diagonal break markers (in axes-fraction units).
|
|
1731
|
+
kwargs
|
|
1732
|
+
Extra keyword arguments forwarded to ``ax.plot()``.
|
|
1733
|
+
|
|
1734
|
+
Returns
|
|
1735
|
+
-------
|
|
1736
|
+
fig : Figure
|
|
1737
|
+
ax : Axes
|
|
1738
|
+
The first (leftmost / bottom) axes object.
|
|
1739
|
+
info : dict
|
|
1740
|
+
``n_segments`` — number of unbroken segments shown.
|
|
1741
|
+
"""
|
|
1742
|
+
x_arr = np.asarray(x, dtype=float)
|
|
1743
|
+
y_arr = np.asarray(y, dtype=float)
|
|
1744
|
+
|
|
1745
|
+
# Build segment limits by excluding break regions
|
|
1746
|
+
all_breaks = sorted(breaks, key=lambda b: b[0])
|
|
1747
|
+
|
|
1748
|
+
limits = []
|
|
1749
|
+
prev = x_arr.min()
|
|
1750
|
+
for lo, hi in all_breaks:
|
|
1751
|
+
limits.append((prev, lo))
|
|
1752
|
+
prev = hi
|
|
1753
|
+
limits.append((prev, x_arr.max()))
|
|
1754
|
+
# Remove empty segments
|
|
1755
|
+
limits = [(a, b) for a, b in limits if b > a]
|
|
1756
|
+
n_seg = len(limits)
|
|
1757
|
+
|
|
1758
|
+
if axis == "x":
|
|
1759
|
+
widths = [b - a for a, b in limits]
|
|
1760
|
+
total = sum(widths)
|
|
1761
|
+
width_ratios = [w / total for w in widths]
|
|
1762
|
+
fig, axes = plt.subplots(
|
|
1763
|
+
1,
|
|
1764
|
+
n_seg,
|
|
1765
|
+
sharey=True,
|
|
1766
|
+
figsize=figsize,
|
|
1767
|
+
gridspec_kw={"width_ratios": width_ratios, "wspace": 0.08},
|
|
1768
|
+
)
|
|
1769
|
+
if n_seg == 1:
|
|
1770
|
+
axes = [axes]
|
|
1771
|
+
for i, (ax_i, (lo, hi)) in enumerate(zip(axes, limits)):
|
|
1772
|
+
mask = (x_arr >= lo) & (x_arr <= hi)
|
|
1773
|
+
ax_i.plot(
|
|
1774
|
+
x_arr[mask], y_arr[mask], color=color, linewidth=linewidth, alpha=alpha, **kwargs
|
|
1775
|
+
)
|
|
1776
|
+
ax_i.set_xlim(lo, hi)
|
|
1777
|
+
# Hide inner spines
|
|
1778
|
+
if i > 0:
|
|
1779
|
+
ax_i.spines["left"].set_visible(False)
|
|
1780
|
+
ax_i.tick_params(left=False)
|
|
1781
|
+
if i < n_seg - 1:
|
|
1782
|
+
ax_i.spines["right"].set_visible(False)
|
|
1783
|
+
# Draw diagonal break markers
|
|
1784
|
+
kw_mark = dict(transform=ax_i.transAxes, color="k", clip_on=False, linewidth=1.2)
|
|
1785
|
+
if i > 0:
|
|
1786
|
+
ax_i.plot((-d, +d), (-d, +d), **kw_mark)
|
|
1787
|
+
ax_i.plot((-d, +d), (1 - d, 1 + d), **kw_mark)
|
|
1788
|
+
if i < n_seg - 1:
|
|
1789
|
+
ax_i.plot((1 - d, 1 + d), (-d, +d), **kw_mark)
|
|
1790
|
+
ax_i.plot((1 - d, 1 + d), (1 - d, 1 + d), **kw_mark)
|
|
1791
|
+
# Shared labels
|
|
1792
|
+
if xlabel:
|
|
1793
|
+
fig.text(0.5, 0.01, xlabel, ha="center")
|
|
1794
|
+
if ylabel:
|
|
1795
|
+
axes[0].set_ylabel(ylabel)
|
|
1796
|
+
if title:
|
|
1797
|
+
fig.suptitle(title)
|
|
1798
|
+
primary_ax = axes[0]
|
|
1799
|
+
|
|
1800
|
+
else: # axis == "y"
|
|
1801
|
+
heights = [b - a for a, b in limits]
|
|
1802
|
+
total = sum(heights)
|
|
1803
|
+
height_ratios = [h / total for h in reversed(heights)]
|
|
1804
|
+
fig, axes = plt.subplots(
|
|
1805
|
+
n_seg,
|
|
1806
|
+
1,
|
|
1807
|
+
sharex=True,
|
|
1808
|
+
figsize=figsize,
|
|
1809
|
+
gridspec_kw={"height_ratios": height_ratios, "hspace": 0.08},
|
|
1810
|
+
)
|
|
1811
|
+
if n_seg == 1:
|
|
1812
|
+
axes = [axes]
|
|
1813
|
+
axes_ordered = list(reversed(axes)) # bottom to top
|
|
1814
|
+
for i, (ax_i, (lo, hi)) in enumerate(zip(axes_ordered, limits)):
|
|
1815
|
+
mask = (y_arr >= lo) & (y_arr <= hi)
|
|
1816
|
+
ax_i.plot(
|
|
1817
|
+
x_arr[mask], y_arr[mask], color=color, linewidth=linewidth, alpha=alpha, **kwargs
|
|
1818
|
+
)
|
|
1819
|
+
ax_i.set_ylim(lo, hi)
|
|
1820
|
+
if i > 0:
|
|
1821
|
+
ax_i.spines["bottom"].set_visible(False)
|
|
1822
|
+
ax_i.tick_params(bottom=False)
|
|
1823
|
+
if i < n_seg - 1:
|
|
1824
|
+
ax_i.spines["top"].set_visible(False)
|
|
1825
|
+
kw_mark = dict(transform=ax_i.transAxes, color="k", clip_on=False, linewidth=1.2)
|
|
1826
|
+
if i > 0:
|
|
1827
|
+
ax_i.plot((-d, +d), (-d, +d), **kw_mark)
|
|
1828
|
+
ax_i.plot((1 - d, 1 + d), (-d, +d), **kw_mark)
|
|
1829
|
+
if i < n_seg - 1:
|
|
1830
|
+
ax_i.plot((-d, +d), (1 - d, 1 + d), **kw_mark)
|
|
1831
|
+
ax_i.plot((1 - d, 1 + d), (1 - d, 1 + d), **kw_mark)
|
|
1832
|
+
if ylabel:
|
|
1833
|
+
fig.text(0.01, 0.5, ylabel, va="center", rotation="vertical")
|
|
1834
|
+
if xlabel:
|
|
1835
|
+
axes_ordered[0].set_xlabel(xlabel)
|
|
1836
|
+
if title:
|
|
1837
|
+
fig.suptitle(title)
|
|
1838
|
+
primary_ax = axes_ordered[0]
|
|
1839
|
+
|
|
1840
|
+
info: Dict[str, Any] = {"n_segments": n_seg}
|
|
1841
|
+
return fig, primary_ax, info
|
|
1842
|
+
|
|
1843
|
+
|
|
1844
|
+
def inset_plot(
|
|
1845
|
+
x,
|
|
1846
|
+
y,
|
|
1847
|
+
x_inset,
|
|
1848
|
+
y_inset,
|
|
1849
|
+
inset_bounds=None,
|
|
1850
|
+
indicate_region=None,
|
|
1851
|
+
title=None,
|
|
1852
|
+
xlabel=None,
|
|
1853
|
+
ylabel=None,
|
|
1854
|
+
inset_xlabel=None,
|
|
1855
|
+
inset_ylabel=None,
|
|
1856
|
+
color="steelblue",
|
|
1857
|
+
inset_color=None,
|
|
1858
|
+
linewidth=1.5,
|
|
1859
|
+
figsize=(8, 6),
|
|
1860
|
+
**kwargs,
|
|
1861
|
+
) -> Tuple[Figure, Axes, Dict[str, Any]]:
|
|
1862
|
+
"""Plot data with an inset subplot showing a zoomed or alternate view.
|
|
1863
|
+
|
|
1864
|
+
Parameters
|
|
1865
|
+
----------
|
|
1866
|
+
x : array-like
|
|
1867
|
+
X data for main plot.
|
|
1868
|
+
y : array-like
|
|
1869
|
+
Y data for main plot.
|
|
1870
|
+
x_inset : array-like
|
|
1871
|
+
X data for inset (can be same as x for zoom, or different data).
|
|
1872
|
+
y_inset : array-like
|
|
1873
|
+
Y data for inset.
|
|
1874
|
+
inset_bounds : list of float, optional
|
|
1875
|
+
[x0, y0, width, height] in axes fraction coordinates (0–1).
|
|
1876
|
+
Defaults to [0.55, 0.55, 0.4, 0.35].
|
|
1877
|
+
indicate_region : tuple of float, optional
|
|
1878
|
+
(x_start, x_end) to draw a grey shaded region on the main plot
|
|
1879
|
+
matching the inset x range.
|
|
1880
|
+
title : str, optional
|
|
1881
|
+
xlabel : str, optional
|
|
1882
|
+
ylabel : str, optional
|
|
1883
|
+
inset_xlabel : str, optional
|
|
1884
|
+
Axis label for the inset X axis.
|
|
1885
|
+
inset_ylabel : str, optional
|
|
1886
|
+
Axis label for the inset Y axis.
|
|
1887
|
+
color : str, optional
|
|
1888
|
+
Color for main plot line. Default ``'steelblue'``.
|
|
1889
|
+
inset_color : str, optional
|
|
1890
|
+
Color for inset line. Defaults to *color*.
|
|
1891
|
+
linewidth : float, optional
|
|
1892
|
+
Line width for both plots. Default 1.5.
|
|
1893
|
+
figsize : tuple, default (8, 6)
|
|
1894
|
+
Figure size in inches.
|
|
1895
|
+
kwargs : dict
|
|
1896
|
+
Extra keyword arguments forwarded to ``ax.plot()`` for the main line.
|
|
1897
|
+
|
|
1898
|
+
Returns
|
|
1899
|
+
-------
|
|
1900
|
+
fig : Figure
|
|
1901
|
+
ax : Axes
|
|
1902
|
+
Main axes.
|
|
1903
|
+
info : dict
|
|
1904
|
+
``n_points``, ``n_inset_points``, ``inset_bounds``.
|
|
1905
|
+
|
|
1906
|
+
Examples
|
|
1907
|
+
--------
|
|
1908
|
+
>>> x = np.linspace(0, 10, 200)
|
|
1909
|
+
>>> y = np.sin(x)
|
|
1910
|
+
>>> fig, ax, info = inset_plot(x, y, x[40:80], y[40:80],
|
|
1911
|
+
... indicate_region=(2.0, 4.0))
|
|
1912
|
+
"""
|
|
1913
|
+
x = np.asarray(x)
|
|
1914
|
+
y = np.asarray(y)
|
|
1915
|
+
x_inset = np.asarray(x_inset)
|
|
1916
|
+
y_inset = np.asarray(y_inset)
|
|
1917
|
+
|
|
1918
|
+
if inset_bounds is None:
|
|
1919
|
+
inset_bounds = [0.55, 0.55, 0.4, 0.35]
|
|
1920
|
+
if inset_color is None:
|
|
1921
|
+
inset_color = color
|
|
1922
|
+
|
|
1923
|
+
fig, ax = create_figure(figsize=figsize)
|
|
1924
|
+
ax.plot(x, y, color=color, linewidth=linewidth, **kwargs)
|
|
1925
|
+
if title:
|
|
1926
|
+
ax.set_title(title)
|
|
1927
|
+
if xlabel:
|
|
1928
|
+
ax.set_xlabel(xlabel)
|
|
1929
|
+
if ylabel:
|
|
1930
|
+
ax.set_ylabel(ylabel)
|
|
1931
|
+
|
|
1932
|
+
# Shade indicated region on main plot
|
|
1933
|
+
if indicate_region is not None:
|
|
1934
|
+
ax.axvspan(indicate_region[0], indicate_region[1], alpha=0.15, color="grey")
|
|
1935
|
+
|
|
1936
|
+
# Create inset axes
|
|
1937
|
+
ax_inset = ax.inset_axes(inset_bounds)
|
|
1938
|
+
ax_inset.plot(x_inset, y_inset, color=inset_color, linewidth=linewidth)
|
|
1939
|
+
if inset_xlabel:
|
|
1940
|
+
ax_inset.set_xlabel(inset_xlabel, fontsize=8)
|
|
1941
|
+
if inset_ylabel:
|
|
1942
|
+
ax_inset.set_ylabel(inset_ylabel, fontsize=8)
|
|
1943
|
+
ax_inset.tick_params(labelsize=7)
|
|
1944
|
+
|
|
1945
|
+
plt.tight_layout()
|
|
1946
|
+
|
|
1947
|
+
info: Dict[str, Any] = {
|
|
1948
|
+
"n_points": len(x),
|
|
1949
|
+
"n_inset_points": len(x_inset),
|
|
1950
|
+
"inset_bounds": inset_bounds,
|
|
1951
|
+
}
|
|
1952
|
+
return fig, ax, info
|
|
1953
|
+
|
|
1954
|
+
|
|
1955
|
+
# ============================================================================
|
|
1956
|
+
# Styling Utilities
|
|
1957
|
+
# ============================================================================
|
|
1958
|
+
|
|
1959
|
+
|
|
1960
|
+
def set_style(style: str = "default") -> None:
|
|
1961
|
+
"""Set matplotlib style.
|
|
1962
|
+
|
|
1963
|
+
Parameters
|
|
1964
|
+
----------
|
|
1965
|
+
style : str, default 'default'
|
|
1966
|
+
Style name. Options: 'default', 'seaborn', 'ggplot', 'bmh', 'dark_background'
|
|
1967
|
+
|
|
1968
|
+
Examples
|
|
1969
|
+
--------
|
|
1970
|
+
>>> set_style('seaborn')
|
|
1971
|
+
>>> fig, ax = line_plot(x, y)
|
|
1972
|
+
"""
|
|
1973
|
+
if style == "default":
|
|
1974
|
+
plt.style.use("default")
|
|
1975
|
+
elif style in plt.style.available:
|
|
1976
|
+
plt.style.use(style)
|
|
1977
|
+
else:
|
|
1978
|
+
available = ", ".join(plt.style.available[:10])
|
|
1979
|
+
raise ValueError(f"Unknown style: {style}. Available styles include: {available}, ...")
|
|
1980
|
+
|
|
1981
|
+
|
|
1982
|
+
def get_color_palette(name: str = "tab10", n_colors: int = 10) -> List[str]:
|
|
1983
|
+
"""Get a color palette.
|
|
1984
|
+
|
|
1985
|
+
Parameters
|
|
1986
|
+
----------
|
|
1987
|
+
name : str, default 'tab10'
|
|
1988
|
+
Palette name. Options: 'tab10', 'tab20', 'Set1', 'Set2', 'Set3', etc.
|
|
1989
|
+
n_colors : int, default 10
|
|
1990
|
+
Number of colors to return
|
|
1991
|
+
|
|
1992
|
+
Returns
|
|
1993
|
+
-------
|
|
1994
|
+
colors : list of str
|
|
1995
|
+
List of color hex codes
|
|
1996
|
+
|
|
1997
|
+
Examples
|
|
1998
|
+
--------
|
|
1999
|
+
>>> colors = get_color_palette('Set1', n_colors=5)
|
|
2000
|
+
>>> fig, ax = line_plot(x, [y1, y2], colors=colors[:2])
|
|
2001
|
+
"""
|
|
2002
|
+
if name == "colorblind":
|
|
2003
|
+
palette = COLORBLIND_PALETTE[:n_colors]
|
|
2004
|
+
return [str(c) for c in palette]
|
|
2005
|
+
|
|
2006
|
+
cmap = plt.get_cmap(name)
|
|
2007
|
+
|
|
2008
|
+
if hasattr(cmap, "colors"):
|
|
2009
|
+
# For qualitative colormaps
|
|
2010
|
+
colors = [mcolors.rgb2hex(c) for c in cmap.colors[:n_colors]]
|
|
2011
|
+
else:
|
|
2012
|
+
# For continuous colormaps
|
|
2013
|
+
colors = [mcolors.rgb2hex(cmap(i / n_colors)) for i in range(n_colors)]
|
|
2014
|
+
|
|
2015
|
+
return colors
|
|
2016
|
+
|
|
2017
|
+
|
|
2018
|
+
def apply_publication_style(
|
|
2019
|
+
fig: Figure,
|
|
2020
|
+
ax: Union[Axes, List[Axes]],
|
|
2021
|
+
fontsize: int = 12,
|
|
2022
|
+
labelsize: int = 10,
|
|
2023
|
+
linewidth: float = 1.5,
|
|
2024
|
+
) -> None:
|
|
2025
|
+
"""Apply publication-quality styling to figure.
|
|
2026
|
+
|
|
2027
|
+
Parameters
|
|
2028
|
+
----------
|
|
2029
|
+
fig : Figure
|
|
2030
|
+
Matplotlib figure
|
|
2031
|
+
ax : Axes or list of Axes
|
|
2032
|
+
Axes to style
|
|
2033
|
+
fontsize : int, default 12
|
|
2034
|
+
Font size for labels
|
|
2035
|
+
labelsize : int, default 10
|
|
2036
|
+
Font size for tick labels
|
|
2037
|
+
linewidth : float, default 1.5
|
|
2038
|
+
Line width for plot elements
|
|
2039
|
+
|
|
2040
|
+
Examples
|
|
2041
|
+
--------
|
|
2042
|
+
>>> fig, ax = line_plot(x, y)
|
|
2043
|
+
>>> apply_publication_style(fig, ax)
|
|
2044
|
+
"""
|
|
2045
|
+
# Handle single or multiple axes
|
|
2046
|
+
if not isinstance(ax, list):
|
|
2047
|
+
ax = [ax]
|
|
2048
|
+
|
|
2049
|
+
for a in ax:
|
|
2050
|
+
# Set font sizes
|
|
2051
|
+
a.title.set_fontsize(fontsize + 2)
|
|
2052
|
+
a.xaxis.label.set_fontsize(fontsize)
|
|
2053
|
+
a.yaxis.label.set_fontsize(fontsize)
|
|
2054
|
+
a.tick_params(labelsize=labelsize)
|
|
2055
|
+
|
|
2056
|
+
# Set line widths
|
|
2057
|
+
for line in a.lines:
|
|
2058
|
+
line.set_linewidth(linewidth)
|
|
2059
|
+
|
|
2060
|
+
# Spine styling
|
|
2061
|
+
for spine in a.spines.values():
|
|
2062
|
+
spine.set_linewidth(1.0)
|
|
2063
|
+
|
|
2064
|
+
# Tight layout
|
|
2065
|
+
fig.tight_layout()
|
|
2066
|
+
|
|
2067
|
+
|
|
2068
|
+
# ============================================================================
|
|
2069
|
+
# Seaborn Statistical Plots
|
|
2070
|
+
# ============================================================================
|
|
2071
|
+
|
|
2072
|
+
|
|
2073
|
+
def distribution_plot(
|
|
2074
|
+
data: Union[np.ndarray, List], kind: str = "hist", kde: bool = True, **kwargs
|
|
2075
|
+
) -> Tuple[Figure, Axes]:
|
|
2076
|
+
"""Create distribution plots using Seaborn.
|
|
2077
|
+
|
|
2078
|
+
Parameters
|
|
2079
|
+
----------
|
|
2080
|
+
data : array-like
|
|
2081
|
+
Data to plot
|
|
2082
|
+
kind : str, default 'hist'
|
|
2083
|
+
Plot type: 'hist', 'kde', 'ecdf'
|
|
2084
|
+
kde : bool, default True
|
|
2085
|
+
Whether to overlay kernel density estimate
|
|
2086
|
+
kwargs : dict
|
|
2087
|
+
Additional arguments passed to seaborn plotting function
|
|
2088
|
+
|
|
2089
|
+
Returns
|
|
2090
|
+
-------
|
|
2091
|
+
fig : Figure
|
|
2092
|
+
Matplotlib figure object
|
|
2093
|
+
ax : Axes
|
|
2094
|
+
Matplotlib axes object
|
|
2095
|
+
|
|
2096
|
+
Raises
|
|
2097
|
+
------
|
|
2098
|
+
ImportError
|
|
2099
|
+
If seaborn is not installed
|
|
2100
|
+
|
|
2101
|
+
Examples
|
|
2102
|
+
--------
|
|
2103
|
+
>>> data = np.random.normal(0, 1, 1000)
|
|
2104
|
+
>>> fig, ax = distribution_plot(data, kind='hist', kde=True)
|
|
2105
|
+
"""
|
|
2106
|
+
if not HAS_SEABORN:
|
|
2107
|
+
raise ImportError(
|
|
2108
|
+
"Seaborn is required for distribution_plot. Install it with: pip install seaborn"
|
|
2109
|
+
)
|
|
2110
|
+
|
|
2111
|
+
figsize = kwargs.pop("figsize", (8, 6))
|
|
2112
|
+
title = kwargs.pop("title", None)
|
|
2113
|
+
xlabel = kwargs.pop("xlabel", None)
|
|
2114
|
+
ylabel = kwargs.pop("ylabel", None)
|
|
2115
|
+
fig, ax = create_figure(figsize=figsize)
|
|
2116
|
+
|
|
2117
|
+
if kind == "hist":
|
|
2118
|
+
sns.histplot(data=data, kde=kde, ax=ax, **kwargs)
|
|
2119
|
+
elif kind == "kde":
|
|
2120
|
+
sns.kdeplot(data=data, ax=ax, **kwargs)
|
|
2121
|
+
elif kind == "ecdf":
|
|
2122
|
+
sns.ecdfplot(data=data, ax=ax, **kwargs)
|
|
2123
|
+
else:
|
|
2124
|
+
raise ValueError(f"Unknown kind: {kind}. Supported: 'hist', 'kde', 'ecdf'")
|
|
2125
|
+
|
|
2126
|
+
configure_axes(ax, xlabel=xlabel, ylabel=ylabel, title=title)
|
|
2127
|
+
return fig, ax
|
|
2128
|
+
|
|
2129
|
+
|
|
2130
|
+
def box_plot(
|
|
2131
|
+
data: Union[np.ndarray, "pd.DataFrame"],
|
|
2132
|
+
x: Optional[str] = None,
|
|
2133
|
+
y: Optional[str] = None,
|
|
2134
|
+
hue: Optional[str] = None,
|
|
2135
|
+
kind: str = "box",
|
|
2136
|
+
**kwargs,
|
|
2137
|
+
) -> Tuple[Figure, Axes]:
|
|
2138
|
+
"""Create box plots or violin plots using Seaborn.
|
|
2139
|
+
|
|
2140
|
+
Parameters
|
|
2141
|
+
----------
|
|
2142
|
+
data : array-like or DataFrame
|
|
2143
|
+
Data to plot
|
|
2144
|
+
x : str, optional
|
|
2145
|
+
Column name for x-axis (for DataFrame input)
|
|
2146
|
+
y : str, optional
|
|
2147
|
+
Column name for y-axis (for DataFrame input)
|
|
2148
|
+
hue : str, optional
|
|
2149
|
+
Column name for color grouping
|
|
2150
|
+
kind : str, default 'box'
|
|
2151
|
+
Plot type: 'box', 'violin', 'boxen'
|
|
2152
|
+
kwargs : dict
|
|
2153
|
+
Additional arguments passed to seaborn function
|
|
2154
|
+
|
|
2155
|
+
Returns
|
|
2156
|
+
-------
|
|
2157
|
+
fig : Figure
|
|
2158
|
+
Matplotlib figure object
|
|
2159
|
+
ax : Axes
|
|
2160
|
+
Matplotlib axes object
|
|
2161
|
+
|
|
2162
|
+
Raises
|
|
2163
|
+
------
|
|
2164
|
+
ImportError
|
|
2165
|
+
If seaborn is not installed
|
|
2166
|
+
|
|
2167
|
+
Examples
|
|
2168
|
+
--------
|
|
2169
|
+
>>> import pandas as pd
|
|
2170
|
+
>>> df = pd.DataFrame({'group': ['A']*50 + ['B']*50,
|
|
2171
|
+
... 'value': np.random.randn(100)})
|
|
2172
|
+
>>> fig, ax = box_plot(df, x='group', y='value')
|
|
2173
|
+
"""
|
|
2174
|
+
if not HAS_SEABORN:
|
|
2175
|
+
raise ImportError("Seaborn is required for box_plot. Install it with: pip install seaborn")
|
|
2176
|
+
|
|
2177
|
+
figsize = kwargs.pop("figsize", (8, 6))
|
|
2178
|
+
title = kwargs.pop("title", None)
|
|
2179
|
+
xlabel = kwargs.pop("xlabel", None)
|
|
2180
|
+
ylabel = kwargs.pop("ylabel", None)
|
|
2181
|
+
fig, ax = create_figure(figsize=figsize)
|
|
2182
|
+
|
|
2183
|
+
if kind == "box":
|
|
2184
|
+
sns.boxplot(data=data, x=x, y=y, hue=hue, ax=ax, **kwargs)
|
|
2185
|
+
elif kind == "violin":
|
|
2186
|
+
sns.violinplot(data=data, x=x, y=y, hue=hue, ax=ax, **kwargs)
|
|
2187
|
+
elif kind == "boxen":
|
|
2188
|
+
sns.boxenplot(data=data, x=x, y=y, hue=hue, ax=ax, **kwargs)
|
|
2189
|
+
else:
|
|
2190
|
+
raise ValueError(f"Unknown kind: {kind}. Supported: 'box', 'violin', 'boxen'")
|
|
2191
|
+
|
|
2192
|
+
configure_axes(ax, xlabel=xlabel, ylabel=ylabel, title=title)
|
|
2193
|
+
return fig, ax
|
|
2194
|
+
|
|
2195
|
+
|
|
2196
|
+
def regression_plot(
|
|
2197
|
+
x: Union[np.ndarray, str],
|
|
2198
|
+
y: Union[np.ndarray, str],
|
|
2199
|
+
data: Optional["pd.DataFrame"] = None,
|
|
2200
|
+
order: int = 1,
|
|
2201
|
+
ci: int = 95,
|
|
2202
|
+
**kwargs,
|
|
2203
|
+
) -> Tuple[Figure, Axes]:
|
|
2204
|
+
"""Create regression plot with confidence interval.
|
|
2205
|
+
|
|
2206
|
+
Parameters
|
|
2207
|
+
----------
|
|
2208
|
+
x : array-like or str
|
|
2209
|
+
X data or column name
|
|
2210
|
+
y : array-like or str
|
|
2211
|
+
Y data or column name
|
|
2212
|
+
data : DataFrame, optional
|
|
2213
|
+
DataFrame containing x and y columns
|
|
2214
|
+
order : int, default 1
|
|
2215
|
+
Polynomial order (1 = linear, 2 = quadratic, etc.)
|
|
2216
|
+
ci : int, default 95
|
|
2217
|
+
Confidence interval percentage
|
|
2218
|
+
kwargs : dict
|
|
2219
|
+
Additional arguments passed to seaborn.regplot
|
|
2220
|
+
|
|
2221
|
+
Returns
|
|
2222
|
+
-------
|
|
2223
|
+
fig : Figure
|
|
2224
|
+
Matplotlib figure object
|
|
2225
|
+
ax : Axes
|
|
2226
|
+
Matplotlib axes object
|
|
2227
|
+
|
|
2228
|
+
Raises
|
|
2229
|
+
------
|
|
2230
|
+
ImportError
|
|
2231
|
+
If seaborn is not installed
|
|
2232
|
+
|
|
2233
|
+
Examples
|
|
2234
|
+
--------
|
|
2235
|
+
>>> x = np.linspace(0, 10, 50)
|
|
2236
|
+
>>> y = 2*x + 1 + np.random.randn(50)
|
|
2237
|
+
>>> fig, ax = regression_plot(x, y, order=1)
|
|
2238
|
+
"""
|
|
2239
|
+
if not HAS_SEABORN:
|
|
2240
|
+
raise ImportError(
|
|
2241
|
+
"Seaborn is required for regression_plot. Install it with: pip install seaborn"
|
|
2242
|
+
)
|
|
2243
|
+
|
|
2244
|
+
figsize = kwargs.pop("figsize", (8, 6))
|
|
2245
|
+
title = kwargs.pop("title", None)
|
|
2246
|
+
xlabel = kwargs.pop("xlabel", None)
|
|
2247
|
+
ylabel = kwargs.pop("ylabel", None)
|
|
2248
|
+
fig, ax = create_figure(figsize=figsize)
|
|
2249
|
+
|
|
2250
|
+
sns.regplot(x=x, y=y, data=data, order=order, ci=ci, ax=ax, **kwargs)
|
|
2251
|
+
|
|
2252
|
+
configure_axes(ax, xlabel=xlabel, ylabel=ylabel, title=title)
|
|
2253
|
+
return fig, ax
|
|
2254
|
+
|
|
2255
|
+
|
|
2256
|
+
# ============================================================================
|
|
2257
|
+
# Plotly Interactive Plots
|
|
2258
|
+
# ============================================================================
|
|
2259
|
+
|
|
2260
|
+
|
|
2261
|
+
def interactive_histogram(
|
|
2262
|
+
data: np.ndarray,
|
|
2263
|
+
bins: int = 30,
|
|
2264
|
+
title: Optional[str] = None,
|
|
2265
|
+
xlabel: Optional[str] = None,
|
|
2266
|
+
ylabel: Optional[str] = None,
|
|
2267
|
+
**kwargs,
|
|
2268
|
+
) -> "go.Figure":
|
|
2269
|
+
"""Create interactive histogram using Plotly.
|
|
2270
|
+
|
|
2271
|
+
Parameters
|
|
2272
|
+
----------
|
|
2273
|
+
data : array-like
|
|
2274
|
+
Data to plot
|
|
2275
|
+
bins : int, default 30
|
|
2276
|
+
Number of bins
|
|
2277
|
+
title : str, optional
|
|
2278
|
+
Plot title
|
|
2279
|
+
xlabel : str, optional
|
|
2280
|
+
X-axis label
|
|
2281
|
+
ylabel : str, optional
|
|
2282
|
+
Y-axis label
|
|
2283
|
+
kwargs : dict
|
|
2284
|
+
Additional arguments passed to go.Histogram
|
|
2285
|
+
|
|
2286
|
+
Returns
|
|
2287
|
+
-------
|
|
2288
|
+
fig : plotly.graph_objects.Figure
|
|
2289
|
+
Plotly figure object (use fig.show() to display)
|
|
2290
|
+
|
|
2291
|
+
Raises
|
|
2292
|
+
------
|
|
2293
|
+
ImportError
|
|
2294
|
+
If plotly is not installed
|
|
2295
|
+
|
|
2296
|
+
Examples
|
|
2297
|
+
--------
|
|
2298
|
+
>>> data = np.random.normal(0, 1, 1000)
|
|
2299
|
+
>>> fig = interactive_histogram(data, bins=50, title='Distribution')
|
|
2300
|
+
>>> fig.write_html('histogram.html')
|
|
2301
|
+
"""
|
|
2302
|
+
if not HAS_PLOTLY:
|
|
2303
|
+
raise ImportError(
|
|
2304
|
+
"Plotly is required for interactive_histogram. Install it with: pip install plotly"
|
|
2305
|
+
)
|
|
2306
|
+
|
|
2307
|
+
fig = go.Figure(data=[go.Histogram(x=data, nbinsx=bins, **kwargs)])
|
|
2308
|
+
|
|
2309
|
+
fig.update_layout(
|
|
2310
|
+
title=title,
|
|
2311
|
+
xaxis_title=xlabel or "Value",
|
|
2312
|
+
yaxis_title=ylabel or "Count",
|
|
2313
|
+
hovermode="x",
|
|
2314
|
+
)
|
|
2315
|
+
|
|
2316
|
+
return fig
|
|
2317
|
+
|
|
2318
|
+
|
|
2319
|
+
def interactive_scatter(
|
|
2320
|
+
x: np.ndarray,
|
|
2321
|
+
y: np.ndarray,
|
|
2322
|
+
color: Optional[np.ndarray] = None,
|
|
2323
|
+
size: Optional[np.ndarray] = None,
|
|
2324
|
+
hover_data: Optional[Dict] = None,
|
|
2325
|
+
title: Optional[str] = None,
|
|
2326
|
+
xlabel: Optional[str] = None,
|
|
2327
|
+
ylabel: Optional[str] = None,
|
|
2328
|
+
**kwargs,
|
|
2329
|
+
) -> "go.Figure":
|
|
2330
|
+
"""Create interactive scatter plot using Plotly.
|
|
2331
|
+
|
|
2332
|
+
Parameters
|
|
2333
|
+
----------
|
|
2334
|
+
x : array-like
|
|
2335
|
+
X coordinates
|
|
2336
|
+
y : array-like
|
|
2337
|
+
Y coordinates
|
|
2338
|
+
color : array-like, optional
|
|
2339
|
+
Values for color mapping
|
|
2340
|
+
size : array-like, optional
|
|
2341
|
+
Values for marker size
|
|
2342
|
+
hover_data : dict, optional
|
|
2343
|
+
Additional data to show on hover
|
|
2344
|
+
title : str, optional
|
|
2345
|
+
Plot title
|
|
2346
|
+
xlabel : str, optional
|
|
2347
|
+
X-axis label
|
|
2348
|
+
ylabel : str, optional
|
|
2349
|
+
Y-axis label
|
|
2350
|
+
kwargs : dict
|
|
2351
|
+
Additional arguments passed to go.Scatter
|
|
2352
|
+
|
|
2353
|
+
Returns
|
|
2354
|
+
-------
|
|
2355
|
+
fig : plotly.graph_objects.Figure
|
|
2356
|
+
Plotly figure object
|
|
2357
|
+
|
|
2358
|
+
Raises
|
|
2359
|
+
------
|
|
2360
|
+
ImportError
|
|
2361
|
+
If plotly is not installed
|
|
2362
|
+
|
|
2363
|
+
Examples
|
|
2364
|
+
--------
|
|
2365
|
+
>>> x = np.random.rand(100)
|
|
2366
|
+
>>> y = np.random.rand(100)
|
|
2367
|
+
>>> fig = interactive_scatter(x, y, title='Interactive Scatter')
|
|
2368
|
+
>>> fig.show()
|
|
2369
|
+
"""
|
|
2370
|
+
if not HAS_PLOTLY:
|
|
2371
|
+
raise ImportError(
|
|
2372
|
+
"Plotly is required for interactive_scatter. Install it with: pip install plotly"
|
|
2373
|
+
)
|
|
2374
|
+
|
|
2375
|
+
scatter_kwargs = {"x": x, "y": y, "mode": "markers", **kwargs}
|
|
2376
|
+
|
|
2377
|
+
if color is not None:
|
|
2378
|
+
scatter_kwargs["marker"] = {
|
|
2379
|
+
"color": color,
|
|
2380
|
+
"colorscale": "Viridis",
|
|
2381
|
+
"showscale": True,
|
|
2382
|
+
}
|
|
2383
|
+
|
|
2384
|
+
if size is not None:
|
|
2385
|
+
if "marker" not in scatter_kwargs:
|
|
2386
|
+
scatter_kwargs["marker"] = {}
|
|
2387
|
+
scatter_kwargs["marker"]["size"] = size
|
|
2388
|
+
|
|
2389
|
+
if hover_data is not None:
|
|
2390
|
+
scatter_kwargs["customdata"] = hover_data
|
|
2391
|
+
|
|
2392
|
+
fig = go.Figure(data=[go.Scatter(**scatter_kwargs)])
|
|
2393
|
+
|
|
2394
|
+
fig.update_layout(
|
|
2395
|
+
title=title,
|
|
2396
|
+
xaxis_title=xlabel or "X",
|
|
2397
|
+
yaxis_title=ylabel or "Y",
|
|
2398
|
+
hovermode="closest",
|
|
2399
|
+
)
|
|
2400
|
+
|
|
2401
|
+
return fig
|
|
2402
|
+
|
|
2403
|
+
|
|
2404
|
+
def interactive_line(
|
|
2405
|
+
x: Union[np.ndarray, List[np.ndarray]],
|
|
2406
|
+
y: Union[np.ndarray, List[np.ndarray]],
|
|
2407
|
+
labels: Optional[List[str]] = None,
|
|
2408
|
+
title: Optional[str] = None,
|
|
2409
|
+
xlabel: Optional[str] = None,
|
|
2410
|
+
ylabel: Optional[str] = None,
|
|
2411
|
+
**kwargs,
|
|
2412
|
+
) -> "go.Figure":
|
|
2413
|
+
"""Create interactive line plot using Plotly.
|
|
2414
|
+
|
|
2415
|
+
Parameters
|
|
2416
|
+
----------
|
|
2417
|
+
x : array-like or list of arrays
|
|
2418
|
+
X coordinates (single or multiple series)
|
|
2419
|
+
y : array-like or list of arrays
|
|
2420
|
+
Y coordinates (single or multiple series)
|
|
2421
|
+
labels : list of str, optional
|
|
2422
|
+
Line labels for legend
|
|
2423
|
+
title : str, optional
|
|
2424
|
+
Plot title
|
|
2425
|
+
xlabel : str, optional
|
|
2426
|
+
X-axis label
|
|
2427
|
+
ylabel : str, optional
|
|
2428
|
+
Y-axis label
|
|
2429
|
+
kwargs : dict
|
|
2430
|
+
Additional arguments passed to go.Scatter
|
|
2431
|
+
|
|
2432
|
+
Returns
|
|
2433
|
+
-------
|
|
2434
|
+
fig : plotly.graph_objects.Figure
|
|
2435
|
+
Plotly figure object
|
|
2436
|
+
|
|
2437
|
+
Raises
|
|
2438
|
+
------
|
|
2439
|
+
ImportError
|
|
2440
|
+
If plotly is not installed
|
|
2441
|
+
|
|
2442
|
+
Examples
|
|
2443
|
+
--------
|
|
2444
|
+
>>> x = np.linspace(0, 10, 100)
|
|
2445
|
+
>>> y1 = np.sin(x)
|
|
2446
|
+
>>> y2 = np.cos(x)
|
|
2447
|
+
>>> fig = interactive_line(x, [y1, y2], labels=['sin', 'cos'])
|
|
2448
|
+
>>> fig.show()
|
|
2449
|
+
"""
|
|
2450
|
+
if not HAS_PLOTLY:
|
|
2451
|
+
raise ImportError(
|
|
2452
|
+
"Plotly is required for interactive_line. Install it with: pip install plotly"
|
|
2453
|
+
)
|
|
2454
|
+
|
|
2455
|
+
fig = go.Figure()
|
|
2456
|
+
|
|
2457
|
+
# Handle single or multiple lines
|
|
2458
|
+
if not isinstance(y, list):
|
|
2459
|
+
y = [y]
|
|
2460
|
+
x_data = [x]
|
|
2461
|
+
else:
|
|
2462
|
+
x_data = [x] * len(y) if not isinstance(x, list) else x # type: ignore[assignment]
|
|
2463
|
+
|
|
2464
|
+
if labels is None:
|
|
2465
|
+
labels = [f"Line {i + 1}" for i in range(len(y))]
|
|
2466
|
+
|
|
2467
|
+
for i, (x_i, y_i) in enumerate(zip(x_data, y)):
|
|
2468
|
+
fig.add_trace(go.Scatter(x=x_i, y=y_i, mode="lines", name=labels[i], **kwargs))
|
|
2469
|
+
|
|
2470
|
+
fig.update_layout(
|
|
2471
|
+
title=title,
|
|
2472
|
+
xaxis_title=xlabel or "X",
|
|
2473
|
+
yaxis_title=ylabel or "Y",
|
|
2474
|
+
hovermode="x unified",
|
|
2475
|
+
height=450,
|
|
2476
|
+
autosize=True,
|
|
2477
|
+
)
|
|
2478
|
+
|
|
2479
|
+
return fig
|
|
2480
|
+
|
|
2481
|
+
|
|
2482
|
+
def interactive_heatmap(
|
|
2483
|
+
data: np.ndarray,
|
|
2484
|
+
x_labels: Optional[List[str]] = None,
|
|
2485
|
+
y_labels: Optional[List[str]] = None,
|
|
2486
|
+
title: Optional[str] = None,
|
|
2487
|
+
colorscale: str = "Viridis",
|
|
2488
|
+
**kwargs,
|
|
2489
|
+
) -> "go.Figure":
|
|
2490
|
+
"""Create interactive heatmap using Plotly.
|
|
2491
|
+
|
|
2492
|
+
Parameters
|
|
2493
|
+
----------
|
|
2494
|
+
data : 2D array
|
|
2495
|
+
Data to visualize as heatmap
|
|
2496
|
+
x_labels : list of str, optional
|
|
2497
|
+
Labels for x-axis
|
|
2498
|
+
y_labels : list of str, optional
|
|
2499
|
+
Labels for y-axis
|
|
2500
|
+
title : str, optional
|
|
2501
|
+
Plot title
|
|
2502
|
+
colorscale : str, default 'Viridis'
|
|
2503
|
+
Colorscale name
|
|
2504
|
+
kwargs : dict
|
|
2505
|
+
Additional arguments passed to go.Heatmap
|
|
2506
|
+
|
|
2507
|
+
Returns
|
|
2508
|
+
-------
|
|
2509
|
+
fig : plotly.graph_objects.Figure
|
|
2510
|
+
Plotly figure object
|
|
2511
|
+
|
|
2512
|
+
Raises
|
|
2513
|
+
------
|
|
2514
|
+
ImportError
|
|
2515
|
+
If plotly is not installed
|
|
2516
|
+
|
|
2517
|
+
Examples
|
|
2518
|
+
--------
|
|
2519
|
+
>>> data = np.random.rand(10, 10)
|
|
2520
|
+
>>> fig = interactive_heatmap(data, title='Interactive Heatmap')
|
|
2521
|
+
>>> fig.show()
|
|
2522
|
+
"""
|
|
2523
|
+
if not HAS_PLOTLY:
|
|
2524
|
+
raise ImportError(
|
|
2525
|
+
"Plotly is required for interactive_heatmap. Install it with: pip install plotly"
|
|
2526
|
+
)
|
|
2527
|
+
|
|
2528
|
+
fig = go.Figure(
|
|
2529
|
+
data=go.Heatmap(z=data, x=x_labels, y=y_labels, colorscale=colorscale, **kwargs)
|
|
2530
|
+
)
|
|
2531
|
+
|
|
2532
|
+
fig.update_layout(title=title, xaxis_title="X", yaxis_title="Y")
|
|
2533
|
+
|
|
2534
|
+
return fig
|
|
2535
|
+
|
|
2536
|
+
|
|
2537
|
+
def interactive_3d_surface(
|
|
2538
|
+
x: np.ndarray,
|
|
2539
|
+
y: np.ndarray,
|
|
2540
|
+
z: np.ndarray,
|
|
2541
|
+
title: Optional[str] = None,
|
|
2542
|
+
colorscale: str = "Viridis",
|
|
2543
|
+
**kwargs,
|
|
2544
|
+
) -> "go.Figure":
|
|
2545
|
+
"""Create interactive 3D surface plot using Plotly.
|
|
2546
|
+
|
|
2547
|
+
Parameters
|
|
2548
|
+
----------
|
|
2549
|
+
x : 2D array
|
|
2550
|
+
X coordinates (meshgrid format)
|
|
2551
|
+
y : 2D array
|
|
2552
|
+
Y coordinates (meshgrid format)
|
|
2553
|
+
z : 2D array
|
|
2554
|
+
Z values
|
|
2555
|
+
title : str, optional
|
|
2556
|
+
Plot title
|
|
2557
|
+
colorscale : str, default 'Viridis'
|
|
2558
|
+
Colorscale name
|
|
2559
|
+
kwargs : dict
|
|
2560
|
+
Additional arguments passed to go.Surface
|
|
2561
|
+
|
|
2562
|
+
Returns
|
|
2563
|
+
-------
|
|
2564
|
+
fig : plotly.graph_objects.Figure
|
|
2565
|
+
Plotly figure object
|
|
2566
|
+
|
|
2567
|
+
Raises
|
|
2568
|
+
------
|
|
2569
|
+
ImportError
|
|
2570
|
+
If plotly is not installed
|
|
2571
|
+
|
|
2572
|
+
Examples
|
|
2573
|
+
--------
|
|
2574
|
+
>>> x = np.linspace(-5, 5, 50)
|
|
2575
|
+
>>> y = np.linspace(-5, 5, 50)
|
|
2576
|
+
>>> X, Y = np.meshgrid(x, y)
|
|
2577
|
+
>>> Z = np.sin(np.sqrt(X**2 + Y**2))
|
|
2578
|
+
>>> fig = interactive_3d_surface(X, Y, Z, title='3D Surface')
|
|
2579
|
+
>>> fig.show()
|
|
2580
|
+
"""
|
|
2581
|
+
if not HAS_PLOTLY:
|
|
2582
|
+
raise ImportError(
|
|
2583
|
+
"Plotly is required for interactive_3d_surface. Install it with: pip install plotly"
|
|
2584
|
+
)
|
|
2585
|
+
|
|
2586
|
+
fig = go.Figure(data=[go.Surface(x=x, y=y, z=z, colorscale=colorscale, **kwargs)])
|
|
2587
|
+
|
|
2588
|
+
fig.update_layout(title=title, scene=dict(xaxis_title="X", yaxis_title="Y", zaxis_title="Z"))
|
|
2589
|
+
|
|
2590
|
+
return fig
|
|
2591
|
+
|
|
2592
|
+
|
|
2593
|
+
# ============================================================================
|
|
2594
|
+
# Export Utilities
|
|
2595
|
+
# ============================================================================
|
|
2596
|
+
|
|
2597
|
+
|
|
2598
|
+
def export_interactive(
|
|
2599
|
+
fig: "go.Figure",
|
|
2600
|
+
filepath: Union[str, Path],
|
|
2601
|
+
include_plotlyjs: str = "cdn",
|
|
2602
|
+
**kwargs,
|
|
2603
|
+
) -> None:
|
|
2604
|
+
"""Export Plotly figure to HTML file.
|
|
2605
|
+
|
|
2606
|
+
Parameters
|
|
2607
|
+
----------
|
|
2608
|
+
fig : plotly.graph_objects.Figure
|
|
2609
|
+
Plotly figure to export
|
|
2610
|
+
filepath : str or Path
|
|
2611
|
+
Output file path (.html)
|
|
2612
|
+
include_plotlyjs : str, default 'cdn'
|
|
2613
|
+
How to include plotly.js: 'cdn', 'directory', or True (embed)
|
|
2614
|
+
kwargs : dict
|
|
2615
|
+
Additional arguments passed to fig.write_html()
|
|
2616
|
+
|
|
2617
|
+
Examples
|
|
2618
|
+
--------
|
|
2619
|
+
>>> fig = interactive_scatter(x, y)
|
|
2620
|
+
>>> export_interactive(fig, 'scatter.html')
|
|
2621
|
+
"""
|
|
2622
|
+
if not HAS_PLOTLY:
|
|
2623
|
+
raise ImportError(
|
|
2624
|
+
"Plotly is required for export_interactive. Install it with: pip install plotly"
|
|
2625
|
+
)
|
|
2626
|
+
|
|
2627
|
+
filepath = Path(filepath)
|
|
2628
|
+
filepath.parent.mkdir(parents=True, exist_ok=True)
|
|
2629
|
+
|
|
2630
|
+
fig.write_html(str(filepath), include_plotlyjs=include_plotlyjs, **kwargs)
|