rsplotlib 0.3.5__cp39-cp39-win_amd64.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.
rsplotlib/pyplot.py ADDED
@@ -0,0 +1,3772 @@
1
+ """rsplotlib.pyplot - Matplotlib pyplot 兼容接口
2
+
3
+ 此模块提供与 matplotlib.pyplot 兼容的 API,所有函数代理到 rsplotlib 核心模块。
4
+ 使用方法: import rsplotlib.pyplot as plt
5
+ """
6
+
7
+ import math
8
+ from . import rsplotlib as _rsplotlib
9
+ from .figure._defaults import DEFAULT_FIGSIZE
10
+ # ============ 样式接口 ============
11
+ from .utils import style as _style_module
12
+ from .pylab import mpl
13
+
14
+ # 延迟获取 mpl.rcParams,避免 pyplot <-> pylab 循环导入
15
+
16
+
17
+ def _get_rcparams():
18
+ """从 pylab.mpl 获取 rcParams,统一配置入口"""
19
+ return mpl.rcParams
20
+
21
+
22
+ # 模块级 rcParams,与 mpl.rcParams 为同一单例,供 plt.rcParams 使用
23
+ rcParams = mpl.rcParams
24
+
25
+
26
+ def rc(group, **kwargs):
27
+ """设置全局 rcParams(matplotlib.rc 兼容)。
28
+
29
+ - ``rc('lines', linewidth=2, color='r')`` 等价于设置
30
+ ``rcParams['lines.linewidth'] = 2`` 与 ``rcParams['lines.color'] = 'r'``。
31
+ - ``group`` 也可为分组名的列表/元组,如 ``('xtick', 'ytick')``。
32
+ - 传入 dict 时按 ``{完整键: 值}`` 直接更新 rcParams(空 dict 为空操作)。
33
+ """
34
+ if isinstance(group, dict):
35
+ for key, value in group.items():
36
+ rcParams[key] = value
37
+ return
38
+ if isinstance(group, str):
39
+ group = (group,)
40
+ for g in group:
41
+ for name, value in kwargs.items():
42
+ rcParams[f'{g}.{name}'] = value
43
+
44
+
45
+ # ==================== 内部辅助函数 ====================
46
+
47
+ def _round_float(value):
48
+ """对浮点数进行合理的四舍五入,避免精度问题"""
49
+ if isinstance(value, float):
50
+ rounded = round(value, 15)
51
+ rounded_int = round(rounded)
52
+ if abs(rounded - rounded_int) < 1e-10:
53
+ return rounded_int
54
+ return rounded
55
+ return value
56
+
57
+
58
+ def _to_list(obj):
59
+ """将数组对象或其他可迭代对象转换为 Python list
60
+
61
+ 支持带 tolist() 方法的数组对象、Python list、tuple 及其他可迭代对象。
62
+ 标量值直接返回。浮点数会进行合理的四舍五入处理。
63
+ """
64
+ if obj is None:
65
+ return None
66
+ if hasattr(obj, 'tolist'):
67
+ result = obj.tolist()
68
+ if isinstance(result, list):
69
+ return [_round_float(item) for item in result]
70
+ return _round_float(result)
71
+ if isinstance(obj, (list, tuple)):
72
+ return [_round_float(item) for item in obj]
73
+ return _round_float(obj)
74
+
75
+
76
+ def _to_list_recursive(obj):
77
+ """递归转换嵌套的数组对象为 Python list"""
78
+ if obj is None:
79
+ return None
80
+ if hasattr(obj, 'tolist'):
81
+ result = obj.tolist()
82
+ if isinstance(result, list):
83
+ return _to_list_recursive(result)
84
+ return _round_float(result)
85
+ if isinstance(obj, (list, tuple)):
86
+ return [_to_list_recursive(item) for item in obj]
87
+ return _round_float(obj)
88
+
89
+
90
+ def _buffer_kind(obj):
91
+ """返回 obj 的 dtype kind 字符(numpy 约定 'f'/'i'/'u'/'b'/'M'/'S'/'U'/'c'…),
92
+ 无法判定时返回 None。
93
+
94
+ 优先读廉价的 obj.dtype.kind:第三方数组库的 __array_interface__ 属性每次访问都会即时把
95
+ 整个缓冲区序列化成 bytes(百万点约 2.6ms/次,随后被丢弃),而 dtype 访问是 O(1)。
96
+ 仅当对象没有 dtype(如 Python list、标量、或仅实现数组接口的第三方缓冲)
97
+ 时,才回退读取 __array_interface__ 的 typestr。
98
+ """
99
+ dt = None
100
+ try:
101
+ dt = getattr(obj, 'dtype', None)
102
+ except Exception:
103
+ # 第三方数组库对 datetime64[h]/timedelta64 等 dtype 的 .dtype 属性会抛 TypeError
104
+ # (非 AttributeError,getattr 默认值挡不住)。此处按"无法判定"处理,落到
105
+ # __array_interface__ 回退——这些非数值 dtype 本就应返回非 fiub kind。
106
+ dt = None
107
+ if dt is not None:
108
+ kind = getattr(dt, 'kind', None)
109
+ if isinstance(kind, str) and len(kind) == 1:
110
+ return kind
111
+ ai = getattr(obj, '__array_interface__', None)
112
+ if isinstance(ai, dict):
113
+ typestr = ai.get('typestr', '')
114
+ return typestr[1:2] if len(typestr) >= 2 else None
115
+ return None
116
+
117
+
118
+ def _is_numeric_buffer(obj):
119
+ """obj 是否为纯数值缓冲数组(float/int/uint/bool)。
120
+
121
+ 这类数组可零拷贝下沉给 Rust,且不可能是字符串类别或日期,故可跳过 .tolist()
122
+ 全量物化(plot/scatter 热路径最大收益)。dtype kind 为 datetime64('M')、字符串
123
+ ('U'/'S')、复数('c')、timedelta('m')、object('O') 均需 Python 侧特殊处理,返回
124
+ False 交回原有 .tolist() 路径。
125
+ """
126
+ return _buffer_kind(obj) in ('f', 'i', 'u', 'b')
127
+
128
+
129
+ def _numeric_buffer_1d_len(obj):
130
+ """若 obj 为一维纯数值缓冲数组 (float/int/uint/bool),返回其长度;否则 None。
131
+
132
+ 用于 scatter 数值 c 的快路径:一维数值缓冲不可能是颜色字符串或 RGB(A) 行数组,
133
+ 可零拷贝直传 Rust 经 colormap 上色,跳过 .tolist() / 逐元素类别检查 / [float(v)] 物化。
134
+ """
135
+ if _buffer_kind(obj) not in ('f', 'i', 'u', 'b'):
136
+ return None
137
+ shape = getattr(obj, 'shape', None)
138
+ if shape is None:
139
+ ai = getattr(obj, '__array_interface__', None)
140
+ if isinstance(ai, dict):
141
+ shape = ai.get('shape')
142
+ if not (isinstance(shape, tuple) and len(shape) == 1):
143
+ return None
144
+ return shape[0]
145
+
146
+
147
+ def _reduce_to_float(v):
148
+ """把数组规约结果 (.min()/.max()) 归一为 Python float。
149
+
150
+ 数组库的规约返回类型可能不稳定:有时返回 Python float,
151
+ 有时返回 0 维 ndarray(float() 报错,但 .item() 可取标量)。
152
+ """
153
+ if isinstance(v, (int, float)):
154
+ return float(v)
155
+ item = getattr(v, 'item', None)
156
+ if item is not None:
157
+ return float(item())
158
+ return float(v)
159
+
160
+
161
+ def _to_seq(obj):
162
+ """一维数值参数的透传辅助:纯数值缓冲数组原样返回,交由 Rust 层零拷贝读取
163
+ 原始缓冲区(避免 .tolist() 生成大量 Python 对象);其余对象(含 datetime64/
164
+ 字符串数组、Python list/tuple、标量)走 _to_list 保持原行为。
165
+
166
+ 仅用于纯数值、直接下沉给 Rust 的坐标/长度参数,不用于需在 Python 侧
167
+ 做类别检测或逐元素判断的参数。
168
+ """
169
+ if _is_numeric_buffer(obj):
170
+ return obj
171
+ return _to_list(obj)
172
+
173
+
174
+ def _replace_from_data(value, data):
175
+ """matplotlib `data` 关键字支持:当 data 提供且 value 为字符串键时,
176
+ 以 data[value] 替换。查找失败(非键 / data 不支持索引)时保持原值,
177
+ 这样普通颜色字符串(如 c='red')不会被误当作数据键。
178
+ """
179
+ if data is not None and isinstance(value, str):
180
+ try:
181
+ return data[value]
182
+ except (KeyError, TypeError, IndexError):
183
+ return value
184
+ return value
185
+
186
+
187
+ def _categorical(vals):
188
+ """分类坐标支持:若 vals 是含字符串的序列,返回 (等距整数位置, 标签列表);
189
+ 否则原样返回 (vals, None)。与 matplotlib 一致——字符串映射到 0,1,2,... 位置,
190
+ 字符串本身作为该轴刻度标签。
191
+
192
+ 性能:数值缓冲区数组 (纯 float/int/bool) 不可能含字符串,直接原样返回,
193
+ 避免 .tolist() 把上百万元素物化成 Python 对象 (plot/scatter 的热路径)。字符串/
194
+ datetime64 数组的 typestr kind 不是数值,_is_numeric_buffer 返回 False,才走 tolist
195
+ 做类别检测。
196
+ """
197
+ if vals is None:
198
+ return vals, None
199
+ if _is_numeric_buffer(vals):
200
+ return vals, None
201
+ if isinstance(vals, (list, tuple)):
202
+ seq = vals
203
+ elif hasattr(vals, 'tolist'):
204
+ seq = vals.tolist()
205
+ else:
206
+ return vals, None
207
+ if any(isinstance(v, str) for v in seq):
208
+ labels = [str(v) for v in seq]
209
+ return list(range(len(labels))), labels
210
+ return vals, None
211
+
212
+
213
+ def _maybe_dates_to_num(seq):
214
+ """日期坐标支持:若 seq 为 datetime/date 序列 (含 numpy datetime64 数组,
215
+ 经 .tolist() 得 datetime 对象),转为自 1970-01-01 起天数的 float 列表;
216
+ 否则返回 None。与 matplotlib 日期约定一致,供 ConciseDateFormatter 反解。
217
+
218
+ 性能:纯数值缓冲区数组直接返回 None,避免 .tolist() 物化 (仅为窥视首元素)。
219
+ datetime64 数组 kind 为 'M',非数值缓冲,仍走 .tolist() 做日期转换。
220
+ """
221
+ import datetime as _dt
222
+ if seq is None or isinstance(seq, (str, _dt.date, _dt.datetime)):
223
+ return None
224
+ if _is_numeric_buffer(seq):
225
+ return None
226
+ if isinstance(seq, (list, tuple)):
227
+ lst = seq
228
+ elif hasattr(seq, 'tolist'):
229
+ lst = seq.tolist()
230
+ else:
231
+ return None
232
+ if len(lst) == 0 or not isinstance(lst[0], (_dt.date, _dt.datetime)):
233
+ return None
234
+ epoch = _dt.datetime(1970, 1, 1)
235
+ out = []
236
+ for v in lst:
237
+ if isinstance(v, _dt.datetime):
238
+ out.append((v - epoch).total_seconds() / 86400.0)
239
+ elif isinstance(v, _dt.date):
240
+ out.append((_dt.datetime(v.year, v.month, v.day) - epoch).total_seconds() / 86400.0)
241
+ else:
242
+ return None
243
+ return out
244
+
245
+
246
+ def _is_scatter_sequence(obj):
247
+ """判断是否为序列(含数组对象),但排除字符串标量。"""
248
+ if obj is None or isinstance(obj, str):
249
+ return False
250
+ return hasattr(obj, 'tolist') or isinstance(obj, (list, tuple))
251
+
252
+
253
+ def _rgba_to_hex(row):
254
+ """将 (r, g, b[, a]) (0-1 浮点) 转为 '#rrggbb' 十六进制颜色。"""
255
+ row = list(row)
256
+
257
+ def _ch(v):
258
+ return max(0, min(255, int(round(float(v) * 255))))
259
+
260
+ return '#{:02x}{:02x}{:02x}'.format(_ch(row[0]), _ch(row[1]), _ch(row[2]))
261
+
262
+
263
+ def _resolve_scatter_colors(c_vals, cmap, vmin, vmax):
264
+ """把 c 序列解析为传给 scatter_multi 的逐点颜色参数 + colorbar 所需的 mappable 元数据。
265
+
266
+ 返回 (c_arg, cmap_name, mappable):
267
+ - 颜色字符串序列 / RGB(A) 二维行数组: c_arg 为颜色字符串列表, cmap_name 与 mappable 为 None
268
+ - 数值序列: c_arg 为原始数值列表, cmap_name 为 colormap 名, mappable = (名, lo, hi);
269
+ 逐点 RGB 由 Rust 层直接经 colormap_color 求得, 不再经百万级 hex 字符串往返。
270
+ """
271
+ if len(c_vals) == 0:
272
+ return None, None, None
273
+ first = c_vals[0]
274
+ if _is_scatter_sequence(first): # RGB(A) 二维行数组
275
+ return [_rgba_to_hex(row) for row in c_vals], None, None
276
+ if isinstance(first, str): # 颜色字符串序列
277
+ return [str(v) for v in c_vals], None, None
278
+ vals = [float(v) for v in c_vals] # 数值 -> 交由 Rust 直接 colormap 上色
279
+ name = cmap if isinstance(cmap, str) else 'viridis'
280
+ lo = min(vals) if vmin is None else float(vmin)
281
+ hi = max(vals) if vmax is None else float(vmax)
282
+ return vals, name, (name, lo, hi)
283
+
284
+
285
+ def _normalize_scatter(x, y, s, c, marker, label, alpha, edgecolor, linewidth, kwargs):
286
+ """将 matplotlib 风格的 scatter 参数规整为对 Rust 层的调用参数。
287
+
288
+ 返回 (use_multi, args, mappable):
289
+ - use_multi=False: args = (x, y, s:float, c:str|None, marker, label, alpha, edgecolor, linewidth)
290
+ - use_multi=True: args = (x, y, s:list|None, c:list|None, marker, label, alpha, edgecolor,
291
+ linewidth, cmap:str|None, vmin:float|None, vmax:float|None)。c 为数值数组时保持原始数值,
292
+ 配合 cmap/vmin/vmax 由 Rust 层直接经 colormap_color 上色(避免 hex 字符串往返)。
293
+ - mappable: None 或 (cmap名, vmin, vmax),当 c 为数值数组经 colormap 映射时给出,
294
+ 供随后的 plt.colorbar() 绘制颜色条。
295
+ """
296
+ alpha = alpha if alpha is not None else 1.0
297
+ x = _to_seq(x)
298
+ y = _to_seq(y)
299
+ if hasattr(x, 'ndim') and x.ndim == 0:
300
+ x = [float(x.item()) if hasattr(x, 'item') else float(x)]
301
+ elif not hasattr(x, '__len__'):
302
+ x = [x]
303
+ if hasattr(y, 'ndim') and y.ndim == 0:
304
+ y = [float(y.item()) if hasattr(y, 'item') else float(y)]
305
+ elif not hasattr(y, '__len__'):
306
+ y = [y]
307
+ n = len(x) if hasattr(x, '__len__') else 0
308
+ marker = marker or 'o'
309
+ if c is None:
310
+ c = kwargs.pop('color', None)
311
+ cmap = kwargs.pop('cmap', None)
312
+ vmin = kwargs.pop('vmin', None)
313
+ vmax = kwargs.pop('vmax', None)
314
+ # norm / colorizer / plotnonfinite / data 等参数当前接受但不生效
315
+
316
+ s_is_seq = _is_scatter_sequence(s)
317
+ c_is_seq = _is_scatter_sequence(c)
318
+
319
+ c_list = None
320
+ c_single = None
321
+ mappable = None
322
+ cmap_name = None
323
+ if c_is_seq:
324
+ # 快路径:一维数值缓冲 c(且非「长度 3/4 单 RGB」歧义)明确是 colormap 数值,
325
+ # 零拷贝直传 Rust,跳过 .tolist() / 逐元素类别检查 / [float(v)] 物化(scatter 热路径)。
326
+ clen = _numeric_buffer_1d_len(c)
327
+ can_fast = clen is not None and not (clen in (3, 4) and clen != n)
328
+ fast_done = False
329
+ if can_fast:
330
+ # 数组库的 .min()/.max() 返回类型可能不稳定(float 或 0 维 ndarray),
331
+ # 用 _reduce_to_float 归一;缺少 min/max 等异常时回退慢路径确保不崩。
332
+ try:
333
+ lo = _reduce_to_float(c.min()) if vmin is None else float(vmin)
334
+ hi = _reduce_to_float(c.max()) if vmax is None else float(vmax)
335
+ cmap_name = cmap if isinstance(cmap, str) else 'viridis'
336
+ c_list = c
337
+ mappable = (cmap_name, lo, hi)
338
+ fast_done = True
339
+ except (TypeError, ValueError, AttributeError):
340
+ cmap_name = None
341
+ c_list = None
342
+ mappable = None
343
+ if not fast_done:
344
+ c_vals = _to_list(c)
345
+ # 单个 RGB(A)(长度 3/4 的纯数值序列,且与点数不同)视为统一单色
346
+ is_flat_numeric = all(
347
+ not isinstance(v, str) and not _is_scatter_sequence(v) for v in c_vals
348
+ )
349
+ if is_flat_numeric and len(c_vals) in (3, 4) and len(c_vals) != n:
350
+ c_single = _rgba_to_hex(c_vals)
351
+ else:
352
+ c_list, cmap_name, mappable = _resolve_scatter_colors(c_vals, cmap, vmin, vmax)
353
+ elif isinstance(c, str):
354
+ c_single = c
355
+
356
+ use_multi = s_is_seq or (c_list is not None)
357
+ if not use_multi:
358
+ s_val = 100.0 if s is None else float(s)
359
+ return (False,
360
+ (x, y, s_val, c_single, marker, label, alpha, edgecolor, linewidth),
361
+ mappable)
362
+
363
+ if s_is_seq:
364
+ s_arg = [float(v) for v in _to_list(s)]
365
+ elif s is None:
366
+ s_arg = None
367
+ else:
368
+ s_arg = [float(s)] * n
369
+
370
+ if c_list is not None:
371
+ c_arg = c_list
372
+ elif c_single is not None:
373
+ c_arg = [c_single] * n
374
+ else:
375
+ c_arg = None
376
+
377
+ # 数值 colormap 路径:把 cmap 名与已解析的 lo/hi 传给 Rust,由其直接算 RGB(跳过 hex 往返)。
378
+ # 字符串 / RGB(A) 颜色路径 cmap_name 为 None,Rust 按颜色字符串解析。
379
+ if cmap_name is not None and mappable is not None:
380
+ cmap_arg, vmin_arg, vmax_arg = mappable
381
+ else:
382
+ cmap_arg, vmin_arg, vmax_arg = None, None, None
383
+ return (True,
384
+ (x, y, s_arg, c_arg, marker, label, alpha, edgecolor, linewidth,
385
+ cmap_arg, vmin_arg, vmax_arg),
386
+ mappable)
387
+
388
+
389
+ def _coerce_edgecolor(edgecolors):
390
+ """把 matplotlib 的 edgecolors 归一化为单个颜色字符串 (后端仅支持统一描边色)。
391
+
392
+ - 标量颜色字符串 (如 'black' / 'none' / 'face') 原样返回;
393
+ - 单个 RGB(A) 数值序列转为 '#rrggbb';
394
+ - 逐点颜色序列取首个元素作为整体描边色;
395
+ - 其他情况返回 None (不描边)。
396
+ """
397
+ if edgecolors is None:
398
+ return None
399
+ if isinstance(edgecolors, str):
400
+ return edgecolors
401
+ seq = _to_list(edgecolors)
402
+ if isinstance(seq, (list, tuple)) and len(seq) > 0:
403
+ if len(seq) in (3, 4) and all(
404
+ not isinstance(v, str) and not _is_scatter_sequence(v) for v in seq):
405
+ return _rgba_to_hex(seq)
406
+ first = seq[0]
407
+ if isinstance(first, str):
408
+ return first
409
+ if _is_scatter_sequence(first):
410
+ return _rgba_to_hex(first)
411
+ return None
412
+
413
+
414
+ def _coerce_linewidth(linewidths):
415
+ """把 matplotlib 的 linewidths 归一化为单个浮点数 (后端仅支持统一线宽)。
416
+
417
+ 标量数值原样转 float;序列取首个元素;无法解析时返回 None (用默认 1.5)。
418
+ """
419
+ if linewidths is None:
420
+ return None
421
+ if isinstance(linewidths, bool):
422
+ return None
423
+ if isinstance(linewidths, (int, float)):
424
+ return float(linewidths)
425
+ seq = _to_list(linewidths)
426
+ if isinstance(seq, (list, tuple)) and len(seq) > 0:
427
+ try:
428
+ return float(seq[0])
429
+ except (TypeError, ValueError):
430
+ return None
431
+ return None
432
+
433
+
434
+ def _get_axes():
435
+ """获取当前 axes,如果没有则返回 None"""
436
+ try:
437
+ return _rsplotlib.gca()
438
+ except Exception:
439
+ return None
440
+
441
+
442
+ def _get_figure():
443
+ """获取当前 figure,如果没有则返回 None"""
444
+ try:
445
+ return _rsplotlib.gcf()
446
+ except Exception:
447
+ return None
448
+
449
+
450
+ def _apply_axes_label(ax, label):
451
+ """将 label 应用到子图(若后端支持 set_label);否则静默忽略。"""
452
+ if label is not None:
453
+ setter = getattr(ax, 'set_label', None)
454
+ if setter is not None:
455
+ setter(label)
456
+
457
+
458
+ def _route_to_ax(ax_method_name, module_method, *args, **kwargs):
459
+ """将调用路由到当前 axes(如果存在)或模块级函数
460
+
461
+ Args:
462
+ ax_method_name: axes 的方法名(字符串)
463
+ module_method: 模块级函数(可调用对象)
464
+ *args: 传递给方法的参数
465
+ **kwargs: 关键字参数(必须同时转发到 axes 端,否则会丢参)
466
+ """
467
+ ax = _get_axes()
468
+ if ax is not None and hasattr(ax, ax_method_name):
469
+ method = getattr(ax, ax_method_name)
470
+ method(*args, **kwargs)
471
+ return _get_figure()
472
+ return module_method(*args, **kwargs)
473
+
474
+
475
+ def _map_aliases(kwargs):
476
+ """规范化 matplotlib 别名到标准名"""
477
+ alias_map = {
478
+ 'lw': 'linewidth',
479
+ 'c': 'color',
480
+ 'ls': 'linestyle',
481
+ 'ms': 'markersize',
482
+ 'mfc': 'markerfacecolor',
483
+ 'mec': 'markeredgecolor',
484
+ 'mew': 'markeredgewidth',
485
+ }
486
+ for alias, target in alias_map.items():
487
+ if alias in kwargs and target not in kwargs:
488
+ kwargs[target] = kwargs.pop(alias)
489
+ elif alias in kwargs:
490
+ kwargs.pop(alias)
491
+ # 规范化 linestyle 词形 ('solid'/'dotted'/'dashed'/'dashdot') 与空值到简写,
492
+ # 与 matplotlib 一致:既可写 linestyle='dotted' 也可写 linestyle=':'。
493
+ # 元组形式 (offset, onoffseq)(参数化 dash 图案)编码为 "dashes=..." 串下沉到 Rust。
494
+ ls = kwargs.get('linestyle')
495
+ if isinstance(ls, str):
496
+ key = ls.strip().lower()
497
+ if key == '' or key == 'none':
498
+ kwargs['linestyle'] = ' ' # 空串 / ' ' / 'None' 均表示不画线
499
+ elif key in _LINESTYLE_ALIASES:
500
+ kwargs['linestyle'] = _LINESTYLE_ALIASES[key]
501
+ # 已是简写 ('-' / '--' / ':' / '-.') 时保持不变
502
+ elif isinstance(ls, (tuple, list)):
503
+ enc = _encode_dash_linestyle(ls)
504
+ if enc is not None:
505
+ kwargs['linestyle'] = enc
506
+
507
+
508
+ def _encode_dash_linestyle(ls):
509
+ """matplotlib 元组 linestyle ``(offset, onoffseq)`` -> ``"dashes=<offset>;<v0>,<v1>,..."`` 编码串。
510
+
511
+ - 空 / None 的 onoffseq 视为实线 ('-')。
512
+ - 结构非法时返回 None(交由下游忽略, 保持原值)。
513
+ """
514
+ if len(ls) != 2:
515
+ return None
516
+ offset, seq = ls
517
+ if seq is None:
518
+ return '-'
519
+ try:
520
+ seq = list(seq)
521
+ except TypeError:
522
+ return None
523
+ if len(seq) == 0:
524
+ return '-'
525
+ if offset is None:
526
+ offset = 0
527
+ try:
528
+ return "dashes=%g;%s" % (float(offset), ",".join("%g" % float(v) for v in seq))
529
+ except (TypeError, ValueError):
530
+ return None
531
+
532
+
533
+ # linestyle 词形 -> 简写。空串 / 'None' 在 _map_aliases 中单独处理为 ' '(不画线)。
534
+ _LINESTYLE_ALIASES = {
535
+ 'solid': '-',
536
+ 'dotted': ':',
537
+ 'dashed': '--',
538
+ 'dashdot': '-.',
539
+ }
540
+
541
+
542
+ # ==================== 轻量 mathtext (LaTeX -> Unicode) ====================
543
+
544
+ # LaTeX 命令 -> Unicode。覆盖希腊字母(大小写)与常见数学符号,满足 matplotlib
545
+ # mathtext 最常见的用法(如 r'$\mu=100,\ \sigma=15$' 渲染为 'μ=100, σ=15')。
546
+ # 完整的 LaTeX 数学排版(真正的上下标定位、分式、根号盒子)不在支持范围内。
547
+ _MATHTEXT_SYMBOLS = {
548
+ 'alpha': 'α', 'beta': 'β', 'gamma': 'γ', 'delta': 'δ', 'epsilon': 'ε',
549
+ 'varepsilon': 'ε', 'zeta': 'ζ', 'eta': 'η', 'theta': 'θ', 'vartheta': 'ϑ',
550
+ 'iota': 'ι', 'kappa': 'κ', 'lambda': 'λ', 'mu': 'μ', 'nu': 'ν', 'xi': 'ξ',
551
+ 'omicron': 'ο', 'pi': 'π', 'varpi': 'ϖ', 'rho': 'ρ', 'varrho': 'ϱ',
552
+ 'sigma': 'σ', 'varsigma': 'ς', 'tau': 'τ', 'upsilon': 'υ', 'phi': 'φ',
553
+ 'varphi': 'φ', 'chi': 'χ', 'psi': 'ψ', 'omega': 'ω',
554
+ 'Gamma': 'Γ', 'Delta': 'Δ', 'Theta': 'Θ', 'Lambda': 'Λ', 'Xi': 'Ξ',
555
+ 'Pi': 'Π', 'Sigma': 'Σ', 'Upsilon': 'Υ', 'Phi': 'Φ', 'Psi': 'Ψ',
556
+ 'Omega': 'Ω',
557
+ 'times': '×', 'div': '÷', 'pm': '±', 'mp': '∓', 'cdot': '·',
558
+ 'ast': '∗', 'star': '⋆', 'circ': '∘', 'bullet': '•',
559
+ 'infty': '∞', 'partial': '∂', 'nabla': '∇', 'forall': '∀', 'exists': '∃',
560
+ 'leq': '≤', 'le': '≤', 'geq': '≥', 'ge': '≥', 'neq': '≠', 'ne': '≠',
561
+ 'approx': '≈', 'equiv': '≡', 'sim': '∼', 'propto': '∝', 'll': '≪',
562
+ 'gg': '≫', 'in': '∈', 'notin': '∉', 'subset': '⊂', 'supset': '⊃',
563
+ 'cup': '∪', 'cap': '∩', 'sum': '∑', 'prod': '∏', 'int': '∫',
564
+ 'sqrt': '√', 'angle': '∠', 'degree': '°', 'prime': '′', 'ell': 'ℓ',
565
+ 'hbar': 'ℏ', 'Re': 'ℜ', 'Im': 'ℑ', 'aleph': 'ℵ', 'emptyset': '∅',
566
+ 'rightarrow': '→', 'to': '→', 'leftarrow': '←', 'gets': '←',
567
+ 'Rightarrow': '⇒', 'Leftarrow': '⇐', 'leftrightarrow': '↔',
568
+ 'Leftrightarrow': '⇔', 'uparrow': '↑', 'downarrow': '↓',
569
+ 'langle': '⟨', 'rangle': '⟩', 'cdots': '⋯', 'ldots': '…', 'dots': '…',
570
+ 'imath': 'ı', 'jmath': 'ȷ', 'wp': '℘', 'surd': '√', 'neg': '¬',
571
+ 'perp': '⊥', 'parallel': '∥', 'therefore': '∴', 'because': '∵',
572
+ 'oplus': '⊕', 'otimes': '⊗', 'wedge': '∧', 'vee': '∨', 'setminus': '∖',
573
+ }
574
+
575
+ # 变音符号命令 -> 组合用 Unicode 变音符(跟在基字符之后即叠加其上)。
576
+ _ACCENTS = {
577
+ 'hat': '\u0302', 'widehat': '\u0302', 'check': '\u030c',
578
+ 'tilde': '\u0303', 'widetilde': '\u0303', 'acute': '\u0301',
579
+ 'grave': '\u0300', 'bar': '\u0304', 'overline': '\u0305',
580
+ 'breve': '\u0306', 'dot': '\u0307', 'ddot': '\u0308',
581
+ 'dddot': '\u20db', 'ddddot': '\u20dc', 'vec': '\u20d7',
582
+ 'overrightarrow': '\u20d7', 'mathring': '\u030a',
583
+ }
584
+ # 覆盖每个字符(而非仅首字符)的变音符命令。
585
+ _ACCENTS_SPREAD = {'overline', 'widehat', 'widetilde', 'overrightarrow'}
586
+
587
+ # 字体命令:仅剥离命令本身,递归转换其花括号内的内容。
588
+ _FONT_COMMANDS = {
589
+ 'mathrm', 'mathit', 'mathtt', 'mathcal', 'mathbb', 'mathfrak',
590
+ 'mathsf', 'mathbf', 'mathbfit', 'mathdefault', 'mathregular',
591
+ 'mathnormal', 'boldsymbol', 'text', 'textrm', 'textit', 'textbf',
592
+ 'operatorname',
593
+ }
594
+ # \text 系列在数学模式里保留字面空格;其余字体命令与普通数学模式一致(忽略空格)。
595
+ _TEXT_COMMANDS = {'text', 'textrm', 'textit', 'textbf'}
596
+
597
+ # 数学字体命令 -> 样式键。映射到 Unicode 数学字母符号(Mathematical Alphanumeric
598
+ # Symbols)。未列出的字体命令(mathrm/mathnormal/text/operatorname 等)视为默认
599
+ # 罗马体,仅剥离命令、内容不改字形。
600
+ _MATH_FONT_STYLES = {
601
+ 'mathbf': 'bf', 'boldsymbol': 'bf', 'textbf': 'bf',
602
+ 'mathit': 'it', 'textit': 'it',
603
+ 'mathbfit': 'bfit',
604
+ 'mathcal': 'cal',
605
+ 'mathfrak': 'frak',
606
+ 'mathbb': 'bb',
607
+ 'mathsf': 'sf',
608
+ 'mathtt': 'tt',
609
+ }
610
+ # 每种样式:(大写字母基址, 小写字母基址, 数字基址或 None, 例外洞表)。
611
+ # 例外洞:部分字形在 SMP 数学字母块中留空,Unicode 把它们放到 BMP 的
612
+ # Letterlike Symbols 区(如 \mathcal{R}=ℛ U+211B、\mathbb{R}=ℝ U+211D)。
613
+ _MATH_ALPHA = {
614
+ 'bf': (0x1D400, 0x1D41A, 0x1D7CE, {}),
615
+ 'it': (0x1D434, 0x1D44E, None, {'h': 0x210E}),
616
+ 'bfit': (0x1D468, 0x1D482, None, {}),
617
+ 'cal': (0x1D49C, 0x1D4B6, None, {
618
+ 'B': 0x212C, 'E': 0x2130, 'F': 0x2131, 'H': 0x210B, 'I': 0x2110,
619
+ 'L': 0x2112, 'M': 0x2133, 'R': 0x211B, 'e': 0x212F, 'g': 0x210A,
620
+ 'o': 0x2134}),
621
+ 'frak': (0x1D504, 0x1D51E, None, {
622
+ 'C': 0x212D, 'H': 0x210C, 'I': 0x2111, 'R': 0x211C, 'Z': 0x2128}),
623
+ 'bb': (0x1D538, 0x1D552, 0x1D7D8, {
624
+ 'C': 0x2102, 'H': 0x210D, 'N': 0x2115, 'P': 0x2119, 'Q': 0x211A,
625
+ 'R': 0x211D, 'Z': 0x2124}),
626
+ 'sf': (0x1D5A0, 0x1D5BA, 0x1D7E2, {}),
627
+ 'tt': (0x1D670, 0x1D68A, 0x1D7F6, {}),
628
+ }
629
+
630
+
631
+ def _style_char(ch, style):
632
+ """把单个 ASCII 字母/数字映射为对应数学字体的 Unicode 字符。
633
+
634
+ 非字母/数字、或该样式无对应字形(如斜体数字)时原样返回。若目标字形不被
635
+ 实际渲染字体支持(如 macOS Arial Unicode 缺 SMP 数学字母块),回退为原字符,
636
+ 避免渲染出缺字形方框。
637
+ """
638
+ if not style:
639
+ return ch
640
+ spec = _MATH_ALPHA.get(style)
641
+ if spec is None:
642
+ return ch
643
+ up, low, dig, holes = spec
644
+ if ch in holes:
645
+ cp = holes[ch]
646
+ elif 'A' <= ch <= 'Z':
647
+ cp = up + (ord(ch) - ord('A'))
648
+ elif 'a' <= ch <= 'z':
649
+ cp = low + (ord(ch) - ord('a'))
650
+ elif dig is not None and '0' <= ch <= '9':
651
+ cp = dig + (ord(ch) - ord('0'))
652
+ else:
653
+ return ch
654
+ mapped = chr(cp)
655
+ try:
656
+ if not _rsplotlib.glyph_supported(mapped):
657
+ return ch
658
+ except Exception:
659
+ pass
660
+ return mapped
661
+
662
+
663
+ # 罗马体函数名:原样输出名称本身(如 \sin -> "sin")。
664
+ _FUNCTION_NAMES = {
665
+ 'sin', 'cos', 'tan', 'cot', 'sec', 'csc', 'sinh', 'cosh', 'tanh',
666
+ 'coth', 'arcsin', 'arccos', 'arctan', 'exp', 'log', 'ln', 'lg',
667
+ 'det', 'dim', 'ker', 'deg', 'gcd', 'hom', 'lim', 'liminf', 'limsup',
668
+ 'max', 'min', 'sup', 'inf', 'arg', 'Pr', 'sgn',
669
+ }
670
+ # 需要吞掉一个 {..} 尺寸参数、输出等宽空白的间距 / 占位命令。
671
+ _SPACE_GROUP_COMMANDS = {
672
+ 'hspace', 'mspace', 'kern', 'mkern', 'phantom', 'hphantom', 'vphantom',
673
+ }
674
+ # 无参数间距命令 -> 空格。
675
+ _SPACE_COMMANDS = {
676
+ 'quad', 'qquad', 'thinspace', 'medspace', 'thickspace', 'space',
677
+ 'enspace', 'negthinspace',
678
+ }
679
+ # 反斜杠 + 单个非字母字符 -> 空格(TeX 显式间距)。
680
+ _BACKSLASH_SPACE = set(' ,;:><./')
681
+
682
+ # 结构化数学构造的中间表示(IR)控制字符,交给 Rust 二维排版引擎解析。
683
+ # 必须与 src/utils/mathtext.rs 中的常量完全一致:
684
+ # script: START 's' base SEP sup SEP sub END
685
+ # frac: START 'f' num SEP den END (带分数线)
686
+ # binom: START 'b' num SEP den END (括号,无线)
687
+ # genfrac: START 'g' ld SEP rd SEP bar SEP num SEP den END
688
+ # sqrt: START 'r' index SEP body END (index 为空表示平方根)
689
+ _IR_START = '\u0002'
690
+ _IR_SEP = '\u001f'
691
+ _IR_END = '\u0003'
692
+
693
+
694
+ def _read_group(expr, i):
695
+ """expr[i] 为 '{',返回 (组内内容, 右括号之后的下标)。允许花括号嵌套。"""
696
+ depth, i, buf = 1, i + 1, []
697
+ n = len(expr)
698
+ while i < n and depth > 0:
699
+ c = expr[i]
700
+ if c == '{':
701
+ depth += 1
702
+ buf.append(c)
703
+ elif c == '}':
704
+ depth -= 1
705
+ if depth > 0:
706
+ buf.append(c)
707
+ else:
708
+ buf.append(c)
709
+ i += 1
710
+ return ''.join(buf), i
711
+
712
+
713
+ def _read_command(expr, i):
714
+ """expr[i] 为 '\\',返回 (命令名或单字符, 之后的下标)。
715
+
716
+ 命令名是紧随的连续字母;若反斜杠后为非字母,则命令为该单个字符。
717
+ 与 TeX 一致:字母命令名后的空白被忽略(这样 '\\hat i' 的重音作用于 i)。
718
+ """
719
+ n = len(expr)
720
+ j = i + 1
721
+ if j < n and expr[j].isalpha():
722
+ k = j
723
+ while k < n and expr[k].isalpha():
724
+ k += 1
725
+ cmd = expr[j:k]
726
+ while k < n and expr[k] == ' ':
727
+ k += 1
728
+ return cmd, k
729
+ if j < n:
730
+ return expr[j], j + 1
731
+ return '', j
732
+
733
+
734
+ def _read_atom(expr, i, keep_spaces, font_style=None):
735
+ """读取上/下标作用的“原子”,返回 (已转换的 Unicode 串, 新下标)。
736
+
737
+ 原子可为 {..} 组、\\命令、或单个字符。
738
+ """
739
+ n = len(expr)
740
+ if i >= n:
741
+ return '', i
742
+ ch = expr[i]
743
+ if ch == '{':
744
+ content, i = _read_group(expr, i)
745
+ return _convert_math(content, keep_spaces, font_style), i
746
+ if ch == '\\':
747
+ _, after = _read_command(expr, i)
748
+ return _convert_math(expr[i:after], keep_spaces, font_style), after
749
+ return _style_char(ch, font_style), i + 1
750
+
751
+
752
+ def _thickness_is_zero(spec):
753
+ """genfrac 的分数线粗细参数是否表示“无线”(堆叠数字)。
754
+
755
+ 空串表示默认线宽(有线);数值 0(含 '0pt'/'0mm' 等单位)表示无线。
756
+ """
757
+ spec = spec.strip()
758
+ if not spec:
759
+ return False
760
+ for unit in ('pt', 'mm', 'cm', 'in', 'em', 'ex', 'px'):
761
+ if spec.endswith(unit):
762
+ spec = spec[:-len(unit)].strip()
763
+ break
764
+ try:
765
+ return float(spec) == 0.0
766
+ except ValueError:
767
+ return False
768
+
769
+
770
+ def _convert_math(expr, keep_spaces=False, font_style=None):
771
+ """把一段数学模式文本($...$ 内部)转换为可供渲染的字符串。
772
+
773
+ 希腊字母/符号/字体命令/函数名/变音符号/间距等转换为 Unicode 文本;
774
+ 结构化构造(^ / _ 上下标、\\frac/\\binom/\\genfrac、\\sqrt[n]{})编码为
775
+ 控制字符 IR(见 _IR_START 等),交给 Rust 二维排版引擎堆叠、画分数线/
776
+ 根号盖线;无法承载二维排版的渲染站点会把 IR 降级为单行 Unicode 近似。
777
+ 支持范围:希腊字母与常见符号命令;字体命令
778
+ (\\mathrm/\\mathcal/\\text ... 剥离保留内容);函数名 (\\sin -> "sin");
779
+ ^ / _ 上下标;\\frac/\\binom/\\genfrac;\\sqrt[n]{};变音符号
780
+ (\\hat/\\bar/\\vec ... 组合字符);间距命令 (\\hspace{}/\\,/\\;/\\quad ...);
781
+ \\left/\\right(丢弃)。数学模式忽略字面空格(\\text{} 内保留)。
782
+ """
783
+ out = []
784
+ i, n = 0, len(expr)
785
+ while i < n:
786
+ ch = expr[i]
787
+ if ch == '\\':
788
+ cmd, after = _read_command(expr, i)
789
+ if cmd == '':
790
+ i = after
791
+ continue
792
+ if len(cmd) == 1 and not cmd.isalpha():
793
+ if cmd in _BACKSLASH_SPACE:
794
+ out.append(' ')
795
+ elif cmd == '!':
796
+ pass # \! 负间距 -> 忽略
797
+ elif cmd == '\\':
798
+ out.append('\n') # \\ -> 换行
799
+ else:
800
+ out.append(cmd) # \$ \% \# \& \_ \{ \} \| 等 -> 字面
801
+ i = after
802
+ continue
803
+ if cmd in _ACCENTS:
804
+ atom, i = _read_atom(expr, after, keep_spaces, font_style)
805
+ comb = _ACCENTS[cmd]
806
+ if cmd in _ACCENTS_SPREAD and atom:
807
+ out.append(''.join(c + comb for c in atom))
808
+ elif atom:
809
+ out.append(atom[0] + comb + atom[1:])
810
+ else:
811
+ out.append(comb)
812
+ continue
813
+ if cmd in _FONT_COMMANDS:
814
+ # 字体命令:进入其花括号内容并切换字体样式。未列入 _MATH_FONT_STYLES
815
+ # 的命令(mathrm/text/… 罗马体)把样式重置为默认(None)。
816
+ new_style = _MATH_FONT_STYLES.get(cmd)
817
+ if after < n and expr[after] == '{':
818
+ content, i = _read_group(expr, after)
819
+ out.append(_convert_math(
820
+ content, keep_spaces or cmd in _TEXT_COMMANDS,
821
+ new_style))
822
+ else:
823
+ i = after
824
+ continue
825
+ if cmd == 'sqrt':
826
+ i = after
827
+ root = ''
828
+ if i < n and expr[i] == '[':
829
+ end = expr.find(']', i)
830
+ if end != -1:
831
+ root, i = expr[i + 1:end], end + 1
832
+ if i < n and expr[i] == '{':
833
+ content, i = _read_group(expr, i)
834
+ body = _convert_math(content, keep_spaces, font_style)
835
+ else:
836
+ body = ''
837
+ index = (_convert_math(root, keep_spaces, font_style)
838
+ if root else '')
839
+ out.append(_IR_START + 'r' + index + _IR_SEP + body + _IR_END)
840
+ continue
841
+ if cmd in ('frac', 'dfrac', 'tfrac', 'binom', 'dbinom', 'tbinom'):
842
+ i = after
843
+ parts = []
844
+ for _ in range(2):
845
+ if i < n and expr[i] == '{':
846
+ grp, i = _read_group(expr, i)
847
+ parts.append(_convert_math(grp, keep_spaces, font_style))
848
+ else:
849
+ parts.append('')
850
+ kind = 'b' if cmd.endswith('binom') else 'f'
851
+ out.append(''.join(
852
+ (_IR_START, kind, parts[0], _IR_SEP, parts[1], _IR_END)))
853
+ continue
854
+ if cmd == 'genfrac':
855
+ i = after
856
+ groups = []
857
+ while len(groups) < 6 and i < n and expr[i] == '{':
858
+ grp, i = _read_group(expr, i)
859
+ groups.append(grp)
860
+ groups += [''] * (6 - len(groups))
861
+ ld = _convert_math(groups[0], keep_spaces, font_style)
862
+ rd = _convert_math(groups[1], keep_spaces, font_style)
863
+ bar_flag = '0' if _thickness_is_zero(groups[2]) else '1'
864
+ num = _convert_math(groups[4], keep_spaces, font_style)
865
+ den = _convert_math(groups[5], keep_spaces, font_style)
866
+ out.append(''.join(
867
+ (_IR_START, 'g', ld, _IR_SEP, rd, _IR_SEP,
868
+ bar_flag, _IR_SEP, num, _IR_SEP, den, _IR_END)))
869
+ continue
870
+ if cmd in _SPACE_GROUP_COMMANDS:
871
+ i = after
872
+ if i < n and expr[i] == '{':
873
+ _, i = _read_group(expr, i) # 丢弃尺寸 / 占位内容
874
+ out.append(' ')
875
+ continue
876
+ if cmd in ('left', 'right'):
877
+ i = after
878
+ if i < n and expr[i] == '.':
879
+ i += 1 # \left. / \right. 隐形定界符
880
+ continue
881
+ if cmd in _SPACE_COMMANDS:
882
+ out.append(' ')
883
+ i = after
884
+ continue
885
+ if cmd in _FUNCTION_NAMES:
886
+ out.append(cmd)
887
+ i = after
888
+ continue
889
+ out.append(_MATHTEXT_SYMBOLS.get(cmd, '')) # 普通符号,未知命令丢弃
890
+ i = after
891
+ continue
892
+ if ch in '^_':
893
+ # 上/下标作用于紧邻的前一个原子(out 的最后一项);连续的
894
+ # ^ 与 _ 合并到同一 base,交给 Rust 二维排版为真正的上下标。
895
+ base = out.pop() if out else ''
896
+ sup = sub = ''
897
+ atom, i = _read_atom(expr, i + 1, keep_spaces, font_style)
898
+ if ch == '^':
899
+ sup = atom
900
+ else:
901
+ sub = atom
902
+ if i < n and expr[i] in '^_':
903
+ ch2 = expr[i]
904
+ atom2, i = _read_atom(expr, i + 1, keep_spaces, font_style)
905
+ if ch2 == '^':
906
+ sup = atom2
907
+ else:
908
+ sub = atom2
909
+ out.append(''.join(
910
+ (_IR_START, 's', base, _IR_SEP, sup, _IR_SEP, sub, _IR_END)))
911
+ continue
912
+ if ch == '{':
913
+ content, i = _read_group(expr, i)
914
+ out.append(_convert_math(content, keep_spaces, font_style))
915
+ continue
916
+ if ch == '}':
917
+ i += 1
918
+ continue
919
+ if ch == '~':
920
+ out.append(' ') # ~ 不折行空格
921
+ i += 1
922
+ continue
923
+ if ch == ' ' and not keep_spaces:
924
+ i += 1 # 数学模式忽略字面空格
925
+ continue
926
+ out.append(_style_char(ch, font_style))
927
+ i += 1
928
+ return ''.join(out)
929
+
930
+
931
+ def _split_dollar(s):
932
+ """按未转义的 '$' 把字符串切成 (is_math, text) 段序列。
933
+
934
+ '\\$' 视为字面 '$' 并入所在段。返回 (segments, balanced);balanced 为
935
+ False 表示未转义 '$' 为奇数个(matplotlib 语义:整串按普通文本处理)。
936
+ """
937
+ segments, buf = [], []
938
+ is_math, count = False, 0
939
+ i, n = 0, len(s)
940
+ while i < n:
941
+ c = s[i]
942
+ if c == '\\' and i + 1 < n and s[i + 1] == '$':
943
+ buf.append('$') # 转义的字面美元符
944
+ i += 2
945
+ continue
946
+ if c == '$':
947
+ segments.append((is_math, ''.join(buf)))
948
+ buf, is_math, count = [], not is_math, count + 1
949
+ i += 1
950
+ continue
951
+ buf.append(c)
952
+ i += 1
953
+ segments.append((is_math, ''.join(buf)))
954
+ return segments, count % 2 == 0
955
+
956
+
957
+ def _render_mathtext(s):
958
+ """把 matplotlib 风格的 mathtext ($...$) 转换为 Unicode 文本。
959
+
960
+ 成对 $...$ 之间按数学模式转换,之外保持字面;'\\$' 为字面美元符。
961
+ 未转义 '$' 为奇数个时整串按普通文本处理(仅把 '\\$' 归一为 '$')。
962
+ 非字符串或不含 '$' 时快速返回原值。
963
+ """
964
+ if not isinstance(s, str) or '$' not in s:
965
+ return s
966
+ segments, balanced = _split_dollar(s)
967
+ if not balanced:
968
+ return s.replace('\\$', '$')
969
+ return ''.join(
970
+ _convert_math(seg) if is_math else seg for is_math, seg in segments)
971
+
972
+
973
+ def _parse_fmt(fmt):
974
+ """解析 matplotlib 风格的格式字符串 '[marker][line][color]'。
975
+
976
+ fmt 由三部分任意组合而成 (均可省略):
977
+ marker: 数据点标记, 如 'o' 圆, '^' 三角, 's' 方块, '*' 星 ...
978
+ line: 线型, '-' 实线, '--' 虚线, '-.' 点划线, ':' 点线
979
+ color: 颜色单字母代码 b/g/r/c/m/y/k/w
980
+
981
+ 例如 'o:r' = 圆形标记 + 点线 + 红色。
982
+
983
+ 注意 black 与 blue 首字母冲突: 按 matplotlib 约定,
984
+ 单字母代码里 'b' 表示 blue, 'k' 才表示 black, 因此不会产生歧义。
985
+
986
+ 返回 dict, 可能包含 'marker' / 'linestyle' / 'color' 键。
987
+ """
988
+ if not fmt:
989
+ return {}
990
+ line_styles_multi = ('--', '-.')
991
+ line_styles_single = ('-', ':')
992
+ markers = set(".,ov^<>12348spP*hH+xXDd|_")
993
+ color_map = {
994
+ 'b': 'blue', 'g': 'green', 'r': 'red', 'c': 'cyan',
995
+ 'm': 'magenta', 'y': 'yellow', 'k': 'black', 'w': 'white',
996
+ }
997
+
998
+ marker = linestyle = color = None
999
+ i, n = 0, len(fmt)
1000
+ while i < n:
1001
+ two = fmt[i:i + 2]
1002
+ ch = fmt[i]
1003
+ if two in line_styles_multi:
1004
+ if linestyle is not None:
1005
+ raise ValueError(f"格式字符串 {fmt!r} 中出现了重复的线型")
1006
+ linestyle, i = two, i + 2
1007
+ elif ch in line_styles_single:
1008
+ if linestyle is not None:
1009
+ raise ValueError(f"格式字符串 {fmt!r} 中出现了重复的线型")
1010
+ linestyle, i = ch, i + 1
1011
+ elif ch in markers:
1012
+ if marker is not None:
1013
+ raise ValueError(f"格式字符串 {fmt!r} 中出现了重复的标记")
1014
+ marker, i = ch, i + 1
1015
+ elif ch == 'C' and i + 1 < n and fmt[i + 1].isdigit():
1016
+ # matplotlib 'CN' 颜色循环记号 ('C0'..'C9'):C 后跟数字,交由后端解析。
1017
+ if color is not None:
1018
+ raise ValueError(f"格式字符串 {fmt!r} 中出现了重复的颜色")
1019
+ j = i + 1
1020
+ while j < n and fmt[j].isdigit():
1021
+ j += 1
1022
+ color, i = fmt[i:j], j
1023
+ elif ch in color_map:
1024
+ if color is not None:
1025
+ raise ValueError(f"格式字符串 {fmt!r} 中出现了重复的颜色")
1026
+ color, i = color_map[ch], i + 1
1027
+ else:
1028
+ raise ValueError(f"无法识别的格式字符串 {fmt!r} (非法字符 {ch!r})")
1029
+
1030
+ result = {}
1031
+ if marker is not None:
1032
+ result['marker'] = marker
1033
+ if linestyle is not None:
1034
+ result['linestyle'] = linestyle
1035
+ elif marker is not None:
1036
+ # fmt 指定了 marker 但未指定线型:与 matplotlib 一致,只画标记不画线
1037
+ # (' ' 为本库的"无线"表示,见 _map_aliases)。
1038
+ result['linestyle'] = ' '
1039
+ if color is not None:
1040
+ result['color'] = color
1041
+ return result
1042
+
1043
+
1044
+ def _implicit_x(y):
1045
+ """plot(y) 的隐式横坐标 = [0, 1, ..., len(y)-1]。"""
1046
+ try:
1047
+ n = len(y)
1048
+ except Exception:
1049
+ return list(y) if hasattr(y, '__iter__') else []
1050
+ return list(range(n))
1051
+
1052
+
1053
+ def _parse_plot_args(args, kwargs):
1054
+ """解析 plot() 的位置参数为 [(x, y, fmt_or_none), ...] 对列表 + kwargs
1055
+
1056
+ 支持的调用形式 (与 matplotlib 一致):
1057
+ plot(y) plot(x, y)
1058
+ plot(y, fmt) plot(x, y, fmt)
1059
+ plot(x1, y1, fmt1, x2, y2, fmt2, ...) # 多条线, 每组 fmt 可选
1060
+ """
1061
+ args = list(args)
1062
+ pairs = []
1063
+ while args:
1064
+ # 取出本组数据参数 (1~2 个); 若第 2 个其实是 fmt 字符串, 则本组只有 1 个
1065
+ group = args[:2]
1066
+ if len(group) == 2 and isinstance(group[1], str):
1067
+ group = group[:1]
1068
+ args = args[len(group):]
1069
+ # 消费紧跟其后的 fmt 字符串 (若有)
1070
+ fmt = None
1071
+ if args and isinstance(args[0], str):
1072
+ fmt, args = args[0], args[1:]
1073
+
1074
+ if len(group) == 1:
1075
+ y = group[0]
1076
+ x = _implicit_x(y)
1077
+ pairs.append((x, y, fmt))
1078
+ else:
1079
+ pairs.append((group[0], group[1], fmt))
1080
+ return pairs, kwargs
1081
+
1082
+
1083
+ # ==================== 绘图函数 ====================
1084
+
1085
+ def plot(*args, **kwargs):
1086
+ """绘制折线图。
1087
+
1088
+ 用法:
1089
+ plt.plot(x, y) # 以 x 为横坐标, y 为纵坐标
1090
+ plt.plot(y) # 仅提供 y, 自动 x = [0, 1, ...]
1091
+ plt.plot(x, y, lw=2.0) # 自定义线宽
1092
+ plt.plot(x, y, x, z) # 绘制多条线
1093
+ plt.plot(y, 'o:r') # 格式字符串: 圆标记 + 点线 + 红色
1094
+
1095
+ 格式字符串 fmt = '[marker][line][color]', 各部分均可省略, 例如:
1096
+ 'o' 仅圆形标记 '--' 仅虚线
1097
+ 'o:r' 圆标记+点线+红色 '^-g' 三角标记+实线+绿色
1098
+ 颜色单字母: b=blue g=green r=red c=cyan m=magenta y=yellow k=black w=white
1099
+ (注意 black 用 'k' 而非 'b', 'b' 表示 blue, 避免与 blue 首字母冲突)
1100
+ 显式关键字参数优先于 fmt 中的同名设置。
1101
+
1102
+ 关键字参数 (matplotlib 兼容别名):
1103
+ lw / linewidth: 线宽 (float)
1104
+ c / color: 颜色 (如 'red', '#FF0000')
1105
+ ls / linestyle: 线型 ('-', '--', ':', '-.')
1106
+ marker: 数据点标记 ('o', 's', '^', 'D', '*', 'x', '+')
1107
+ ms / markersize: 标记大小 (float)
1108
+ mfc / markerfacecolor: 标记内部填充色
1109
+ mec / markeredgecolor: 标记边框色
1110
+ solid_capstyle: 端点 ('butt', 'round', 'projecting')
1111
+ label: 图例标签
1112
+
1113
+ Returns:
1114
+ (Figure, Axes) 元组
1115
+ """
1116
+ _map_aliases(kwargs)
1117
+ pairs, _ = _parse_plot_args(args, kwargs)
1118
+
1119
+ def _call(*a, **k):
1120
+ return _rsplotlib.plot(*a, **k)
1121
+
1122
+ result = None
1123
+ for x, y, fmt in pairs:
1124
+ call_kwargs = dict(kwargs)
1125
+ if fmt:
1126
+ # fmt 解析出的样式作为默认值, 不覆盖用户显式传入的关键字参数
1127
+ for key, value in _parse_fmt(fmt).items():
1128
+ call_kwargs.setdefault(key, value)
1129
+ # 分类坐标:字符串 x/y 映射到 0,1,2,... 位置,字符串作为刻度标签。
1130
+ x, x_tick_labels = _categorical(x)
1131
+ y, y_tick_labels = _categorical(y)
1132
+ # 将数组对象转换为 Python list,避免类型不一致问题
1133
+ x = _to_list(x)
1134
+ y = _to_list(y)
1135
+ result = _route_to_ax('plot', _call, x, y, **call_kwargs)
1136
+ if x_tick_labels is not None:
1137
+ xticks(list(range(len(x_tick_labels))), x_tick_labels)
1138
+ if y_tick_labels is not None:
1139
+ yticks(list(range(len(y_tick_labels))), y_tick_labels)
1140
+ return result
1141
+
1142
+
1143
+ def scatter(x, y, s=None, c=None, marker=None, cmap=None, norm=None,
1144
+ vmin=None, vmax=None, alpha=None, linewidths=None,
1145
+ edgecolors=None, colorizer=None, plotnonfinite=False,
1146
+ data=None, **kwargs):
1147
+ """绘制散点图,兼容 matplotlib.pyplot.scatter 的参数签名。
1148
+
1149
+ 用法:
1150
+ plt.scatter(x, y) # 默认大小 100、默认蓝色
1151
+ plt.scatter(x, y, s=50, c='red') # 统一大小和颜色
1152
+ plt.scatter(x, y, s=[10, 20, 30], c=['r','g','b']) # 逐点大小/颜色
1153
+ plt.scatter(x, y, c=values, cmap='viridis') # 数值经 colormap 映射
1154
+ plt.scatter(x, y, c=[[1,0,0],[0,1,0]]) # RGB(A) 二维行数组
1155
+ plt.scatter(x, y, edgecolors='black', linewidths=1.5) # 黑色描边
1156
+
1157
+ Args:
1158
+ x, y: 长度相同的数据点坐标 (list / tuple / array)
1159
+ s: 点大小, 默认 100; 可为标量或与点数等长的数组
1160
+ c: 颜色; 默认蓝色; 可为颜色字符串、颜色字符串数组、数值数组
1161
+ (配合 cmap) 或 RGB(A) 二维行数组
1162
+ marker: 标记形状, 默认 'o'
1163
+ cmap: 当 c 为数值数组时使用的 colormap 名称 (如 'viridis')
1164
+ vmin, vmax: colormap 归一化范围
1165
+ alpha: 透明度 (0.0 - 1.0)
1166
+ linewidths: 标记边缘线宽 (points); 后端取统一线宽, 序列取首个元素
1167
+ edgecolors: 标记边缘颜色; 'face'/'none' 表示不额外描边, 其他颜色启用描边
1168
+ (后端仅支持统一描边色, 逐点颜色取首个元素)。也接受单数别名 edgecolor。
1169
+ norm / colorizer / plotnonfinite: 接受但当前不生效
1170
+ data: 若提供 (如 dict / DataFrame),x/y/s/c 等字符串参数将按键在 data 中查找取值
1171
+ **kwargs: 额外关键字参数 (color 将作为 c 的别名)
1172
+ """
1173
+ if data is not None:
1174
+ # matplotlib 兼容:以字符串键索引 data 得到实际数据。
1175
+ x = _replace_from_data(x, data)
1176
+ y = _replace_from_data(y, data)
1177
+ s = _replace_from_data(s, data)
1178
+ c = _replace_from_data(c, data)
1179
+ if 'color' in kwargs:
1180
+ kwargs['color'] = _replace_from_data(kwargs['color'], data)
1181
+ # 单数形式 edgecolor / linewidth 作为别名 (matplotlib 主用复数名)。
1182
+ if edgecolors is None:
1183
+ edgecolors = kwargs.pop('edgecolor', None)
1184
+ else:
1185
+ kwargs.pop('edgecolor', None)
1186
+ if linewidths is None:
1187
+ linewidths = kwargs.pop('linewidth', None)
1188
+ else:
1189
+ kwargs.pop('linewidth', None)
1190
+ edgecolor = _coerce_edgecolor(edgecolors)
1191
+ linewidth = _coerce_linewidth(linewidths)
1192
+ kwargs['cmap'] = cmap
1193
+ kwargs['vmin'] = vmin
1194
+ kwargs['vmax'] = vmax
1195
+ a = 1.0 if alpha is None else alpha
1196
+ label = kwargs.pop('label', None)
1197
+ # 分类坐标:字符串 x/y 映射到 0,1,2,... 位置,字符串作为刻度标签。
1198
+ x, x_tick_labels = _categorical(x)
1199
+ y, y_tick_labels = _categorical(y)
1200
+ use_multi, args, mappable = _normalize_scatter(
1201
+ x, y, s, c, marker, label=label, alpha=a,
1202
+ edgecolor=edgecolor, linewidth=linewidth, kwargs=kwargs)
1203
+ if use_multi:
1204
+ result = _route_to_ax('scatter_multi', _rsplotlib.scatter_multi, *args)
1205
+ else:
1206
+ result = _route_to_ax('scatter', _rsplotlib.scatter, *args)
1207
+ if x_tick_labels is not None:
1208
+ xticks(list(range(len(x_tick_labels))), x_tick_labels)
1209
+ if y_tick_labels is not None:
1210
+ yticks(list(range(len(y_tick_labels))), y_tick_labels)
1211
+ if mappable is not None:
1212
+ ax = _get_axes()
1213
+ if ax is not None and hasattr(ax, 'set_mappable'):
1214
+ ax.set_mappable(*mappable)
1215
+ return result
1216
+
1217
+
1218
+ def bar(x, height, width=0.8, color=None, label=None):
1219
+ """绘制柱状图。
1220
+
1221
+ Args:
1222
+ x: 每个柱子的 x 坐标 (list / tuple / array)
1223
+ height: 每个柱子的高度 (y 值)
1224
+ width: 柱子的宽度 (默认 0.8)
1225
+ color: 柱子的颜色字符串
1226
+ label: 图例标签
1227
+
1228
+ 用法:
1229
+ plt.bar([0, 1, 2], [1, 2, 3])
1230
+ plt.bar(["A", "B", "C"], [1, 2, 3]) # 字符串 x 作为类别标签
1231
+ """
1232
+ x = _to_list(x)
1233
+ height = _to_seq(height)
1234
+ # 类别型 x:x 为字符串序列时,柱子落在 0,1,2,... 位置,字符串作为 x 轴刻度标签。
1235
+ tick_labels = None
1236
+ if isinstance(x, (list, tuple)) and any(isinstance(v, str) for v in x):
1237
+ tick_labels = [str(v) for v in x]
1238
+ x = list(range(len(x)))
1239
+ result = _route_to_ax('bar', _rsplotlib.bar, x, height, width, color, label)
1240
+ if tick_labels is not None:
1241
+ xticks(x, tick_labels)
1242
+ return result
1243
+
1244
+
1245
+ def barh(y, width, height=0.8, color=None, label=None):
1246
+ """绘制水平柱状图。
1247
+
1248
+ Args:
1249
+ y: 每个柱子的 y 坐标
1250
+ width: 每个柱子的宽度 (x 方向长度)
1251
+ height: 柱子的高度 (y 方向, 默认 0.8)
1252
+ color: 颜色
1253
+ label: 图例标签
1254
+ """
1255
+ y = _to_list(y)
1256
+ width = _to_seq(width)
1257
+ # 类别型 y:y 为字符串序列时,柱子落在 0,1,2,... 位置,字符串作为 y 轴刻度标签。
1258
+ tick_labels = None
1259
+ if isinstance(y, (list, tuple)) and any(isinstance(v, str) for v in y):
1260
+ tick_labels = [str(v) for v in y]
1261
+ y = list(range(len(y)))
1262
+ result = _route_to_ax('barh', _rsplotlib.barh, y, width, height, color, label)
1263
+ if tick_labels is not None:
1264
+ yticks(y, tick_labels)
1265
+ return result
1266
+
1267
+
1268
+ def hist(x, bins=None, range=None, density=False, weights=None,
1269
+ cumulative=False, bottom=None, histtype='bar', align='mid',
1270
+ orientation='vertical', rwidth=None, log=False, color=None,
1271
+ label=None, stacked=False, **kwargs):
1272
+ """绘制直方图。
1273
+
1274
+ 用法:
1275
+ plt.hist(data, bins=20)
1276
+ plt.hist([data1, data2], bins=10, color=['red', 'blue'], stacked=True)
1277
+ plt.hist(data, histtype='step', cumulative=True, density=True)
1278
+ plt.hist(data, orientation='horizontal', log=True)
1279
+
1280
+ Args:
1281
+ x: 数据 (一维数组, 或多组数据组成的列表)
1282
+ bins: 分箱数量 (默认 10) 或箱边界列表
1283
+ range: 值域范围 (lo, hi),None 表示使用数据的最小/最大值
1284
+ density: 是否归一化为概率密度 (默认 False)
1285
+ weights: 每个数据点的权重
1286
+ cumulative: 是否绘制累积分布 (True/False/-1)
1287
+ bottom: 每个柱子的起始基线 (默认 0)
1288
+ histtype: 'bar' | 'barstacked' | 'step' | 'stepfilled'
1289
+ align: 'left' | 'mid' | 'right'
1290
+ orientation: 'vertical' | 'horizontal'
1291
+ rwidth: 每个柱子相对于分箱宽度的比例 (0~1)
1292
+ log: 计数轴是否使用对数刻度
1293
+ color: 颜色或颜色列表
1294
+ label: 图例标签
1295
+ stacked: 是否堆叠多组直方图
1296
+ **kwargs: 额外关键字参数 (facecolor, alpha)
1297
+ """
1298
+ facecolor = kwargs.pop('facecolor', None)
1299
+ alpha = kwargs.pop('alpha', 1.0)
1300
+
1301
+ if bins is None:
1302
+ bins = 10
1303
+
1304
+ # 数据规整为“组的列表”。一维纯数值缓冲直接下沉给 Rust 零拷贝读取,避免
1305
+ # _to_list_recursive + [list(x)] 把百万级数据点物化成 Python 对象(hist 大数据热路径)。
1306
+ if _numeric_buffer_1d_len(x) is not None:
1307
+ x_list = x
1308
+ n_datasets = 1
1309
+ else:
1310
+ x = _to_list_recursive(x)
1311
+ if x and isinstance(x[0], (list, tuple)):
1312
+ x_list = [list(v) for v in x]
1313
+ else:
1314
+ x_list = [list(x)]
1315
+ n_datasets = len(x_list)
1316
+
1317
+ # weights 规整为与 x 平行的结构(一维数值缓冲同样直传,Rust 侧零拷贝读取)
1318
+ if weights is not None:
1319
+ if _numeric_buffer_1d_len(weights) is not None:
1320
+ weights_arg = weights
1321
+ else:
1322
+ w = _to_list_recursive(weights)
1323
+ if w and isinstance(w[0], (list, tuple)):
1324
+ weights_arg = [list(v) for v in w]
1325
+ else:
1326
+ weights_arg = [list(w)]
1327
+ else:
1328
+ weights_arg = None
1329
+
1330
+ # bins: 整数箱数 或 箱边界列表
1331
+ if isinstance(bins, (list, tuple)):
1332
+ bins_arg = [float(b) for b in bins]
1333
+ elif hasattr(bins, 'tolist'):
1334
+ bins_arg = [float(b) for b in bins.tolist()]
1335
+ else:
1336
+ bins_arg = int(bins)
1337
+
1338
+ def _norm_color(c):
1339
+ if c is None:
1340
+ return None
1341
+ if isinstance(c, str):
1342
+ return [c] * n_datasets
1343
+ if isinstance(c, (list, tuple)):
1344
+ return list(c)
1345
+ return None
1346
+
1347
+ color_arg = _norm_color(color)
1348
+ facecolor_arg = _norm_color(facecolor)
1349
+
1350
+ # cumulative -> int (True=1, False=0, -1=反向累积)
1351
+ if cumulative is True:
1352
+ cum = 1
1353
+ elif cumulative is False or cumulative is None:
1354
+ cum = 0
1355
+ else:
1356
+ cum = int(cumulative)
1357
+
1358
+ range_arg = tuple(range) if range is not None else None
1359
+
1360
+ hist_kwargs = dict(
1361
+ bins=bins_arg, range=range_arg, density=density, weights=weights_arg,
1362
+ cumulative=cum, bottom=bottom, histtype=histtype, align=align,
1363
+ orientation=orientation, rwidth=rwidth, log=log, color=color_arg,
1364
+ facecolor=facecolor_arg, label=label, stacked=stacked, alpha=alpha,
1365
+ )
1366
+
1367
+ ax = _get_axes()
1368
+ if ax is not None and hasattr(ax, 'hist'):
1369
+ return ax.hist(x_list, **hist_kwargs)
1370
+ return _rsplotlib.hist(x_list, **hist_kwargs)
1371
+
1372
+
1373
+ def pie(x, labels=None, colors=None, autopct=False, startangle=0.0, explode=None, **kwargs):
1374
+ """绘制饼图。
1375
+
1376
+ 用法:
1377
+ plt.pie([30, 40, 30], labels=['A', 'B', 'C'])
1378
+
1379
+ Args:
1380
+ x: 数据列表 (各部分数值)
1381
+ labels: 每部分的标签列表
1382
+ colors: 每部分的颜色列表
1383
+ autopct: 百分比格式字符串 (如 '%1.1f%%'), 或布尔值 True
1384
+ startangle: 起始角度 (度), 默认从 x 轴正方向逆时针画起
1385
+ explode: 各扇形沿半径方向向外偏移的比例列表 (如 (0, 0.1, 0, 0))
1386
+ **kwargs: 其他关键字参数
1387
+ """
1388
+ x = _to_list(x)
1389
+ if autopct and isinstance(autopct, str):
1390
+ autopct_str = autopct
1391
+ elif autopct:
1392
+ autopct_str = "%1.1f%%"
1393
+ else:
1394
+ autopct_str = None
1395
+ explode_list = _to_list(explode) if explode is not None else None
1396
+ return _route_to_ax('pie', _rsplotlib.pie, x, labels, colors, autopct_str, startangle, explode_list)
1397
+
1398
+
1399
+ def boxplot(x, labels=None, vert=True, **kwargs):
1400
+ """绘制箱线图 (box-and-whisker plot)。
1401
+
1402
+ 展示数据的中位数、四分位、离群值等统计信息。
1403
+
1404
+ 用法:
1405
+ plt.boxplot([data1, data2, data3])
1406
+
1407
+ Args:
1408
+ x: 数据集 (可以是一个或多个一维数组)
1409
+ labels: 每个箱的标签列表
1410
+ vert: 是否垂直绘制 (默认 True)
1411
+ """
1412
+ # 纯数值缓冲数组直接下沉给 Rust 零拷贝读取(py_to_vec_vec_f64:一维→单箱,
1413
+ # 二维→按行拆多箱,与旧 _to_list_recursive 语义一致),避免物化百万级数据点。
1414
+ # Python list(含 list of arrays)、含字符串等非缓冲对象保持原路径。
1415
+ if not _is_numeric_buffer(x):
1416
+ x = _to_list_recursive(x)
1417
+ return _route_to_ax('boxplot', _rsplotlib.boxplot, x, labels, vert)
1418
+
1419
+
1420
+ def fill_between(x, y1, y2=0.0, color=None, alpha=0.3, label=None, **kwargs):
1421
+ """填充两条曲线之间的区域。
1422
+
1423
+ 用法:
1424
+ plt.fill_between(x, y1, y2, color='red', alpha=0.3)
1425
+
1426
+ Args:
1427
+ x: x 坐标数据
1428
+ y1: 第一条曲线的 y 坐标
1429
+ y2: 第二条曲线的 y 坐标 (默认 0.0)
1430
+ color: 填充颜色
1431
+ alpha: 透明度 (0.0-1.0, 默认 0.3)
1432
+ label: 图例标签
1433
+ """
1434
+ x = _to_seq(x)
1435
+ y1 = _to_seq(y1)
1436
+ y2 = _to_seq(y2)
1437
+ return _route_to_ax('fill_between', _rsplotlib.fill_between, x, y1, y2, color, alpha, label)
1438
+
1439
+
1440
+ def fill_betweenx(y, x1, x2=0.0, color=None, alpha=0.3, label=None, **kwargs):
1441
+ """填充两条曲线之间的区域(以 y 为基准)。
1442
+
1443
+ 用法:
1444
+ plt.fill_betweenx(y, x1, x2, color='red', alpha=0.3)
1445
+
1446
+ Args:
1447
+ y: y 坐标数据
1448
+ x1: 第一条曲线的 x 坐标
1449
+ x2: 第二条曲线的 x 坐标 (默认 0.0)
1450
+ color: 填充颜色
1451
+ alpha: 透明度 (0.0-1.0, 默认 0.3)
1452
+ label: 图例标签
1453
+ """
1454
+ y = _to_seq(y)
1455
+ x1 = _to_seq(x1)
1456
+ x2 = _to_seq(x2)
1457
+ return _route_to_ax('fill_betweenx', _rsplotlib.fill_betweenx, y, x1, x2, color, alpha, label)
1458
+
1459
+
1460
+ def errorbar(x, y, yerr=None, xerr=None, fmt='o', color=None, label=None, capsize=3.0, **kwargs):
1461
+ """绘制带误差棒的图。
1462
+
1463
+ 用法:
1464
+ plt.errorbar(x, y, yerr=0.1)
1465
+
1466
+ Args:
1467
+ x: x 坐标数据
1468
+ y: y 坐标数据
1469
+ yerr: y 方向误差 (标量或数组)
1470
+ xerr: x 方向误差 (标量或数组)
1471
+ fmt: 数据点/线格式字符串 (默认 'o')
1472
+ color: 颜色
1473
+ label: 图例标签
1474
+ capsize: 误差棒末端横线长度 (默认 3.0)
1475
+ """
1476
+ x = _to_seq(x)
1477
+ y = _to_seq(y)
1478
+ yerr = _to_seq(yerr)
1479
+ xerr = _to_seq(xerr)
1480
+ return _route_to_ax('errorbar', _rsplotlib.errorbar, x, y, yerr, xerr, fmt, color, label, capsize)
1481
+
1482
+
1483
+ def stem(x, y, linefmt=None, markerfmt=None, label=None, **kwargs):
1484
+ """绘制茎叶图 (火柴杆图)。
1485
+
1486
+ Args:
1487
+ x: x 坐标数据
1488
+ y: y 坐标数据
1489
+ linefmt: 线样式
1490
+ markerfmt: 标记样式
1491
+ label: 图例标签
1492
+ """
1493
+ x = _to_seq(x)
1494
+ y = _to_seq(y)
1495
+ return _route_to_ax('stem', _rsplotlib.stem, x, y, linefmt or '-', markerfmt or 'o', label)
1496
+
1497
+
1498
+ def step(x, y, where='pre', label=None, color=None, linestyle='-', linewidth=1.5, **kwargs):
1499
+ """绘制阶梯图。
1500
+
1501
+ Args:
1502
+ x: x 坐标数据
1503
+ y: y 坐标数据
1504
+ where: 阶梯位置 ('pre', 'mid', 'post', 默认 'pre')
1505
+ label: 图例标签
1506
+ color: 颜色
1507
+ linestyle: 线型
1508
+ linewidth: 线宽
1509
+ """
1510
+ x = _to_seq(x)
1511
+ y = _to_seq(y)
1512
+ return _route_to_ax('step', _rsplotlib.step, x, y, where, label, color, linestyle, linewidth)
1513
+
1514
+
1515
+ def stackplot(x, *args, labels=None, colors=None, alpha=1.0, **kwargs):
1516
+ """绘制堆叠面积图。
1517
+
1518
+ 用法:
1519
+ plt.stackplot(x, y1, y2, y3, labels=['A', 'B', 'C'])
1520
+
1521
+ Args:
1522
+ x: x 坐标数据
1523
+ *args: 多个 y 数据集
1524
+ labels: 每个数据集的标签列表
1525
+ colors: 每个数据集的颜色列表
1526
+ alpha: 透明度 (默认 1.0)
1527
+ """
1528
+ x = _to_seq(x)
1529
+ y_data = [_to_seq(a) for a in args if a is not None]
1530
+ if not y_data and 'y' in kwargs:
1531
+ y_data = [_to_seq(kwargs['y'])]
1532
+ return _route_to_ax('stackplot', _rsplotlib.stackplot, x, *y_data,
1533
+ labels=labels, colors=colors, alpha=alpha)
1534
+
1535
+
1536
+ def imshow(x, cmap=None, norm=None, aspect=None, interpolation=None,
1537
+ alpha=None, vmin=None, vmax=None, origin=None, extent=None, **kwargs):
1538
+ """显示图像 (矩阵热力图 / 灰度图 / RGB 彩色图)。
1539
+
1540
+ Args:
1541
+ x: 图像数据。2D 数组 (行->y 轴, 列->x 轴) 经 cmap 上色;
1542
+ 3D 数组 (H, W, 3/4) 视为 RGB(A) 彩色图,直接按像素颜色绘制
1543
+ (浮点取值 [0,1],整数取值 [0,255])。
1544
+ cmap: 颜色映射名称 (默认 'viridis'),仅对 2D 数据生效
1545
+ aspect: 宽高比。'equal'(默认) 使 X/Y 轴单位长度相同 (图像单元为正方形);
1546
+ 'auto' 让图像填满子图框;也可传数值比例
1547
+ alpha: 图像整体透明度 (0.0-1.0)
1548
+ vmin, vmax: 2D 数据的颜色映射值域 (缺省取数据 min/max)
1549
+ origin: 'upper' (默认, 首行在顶部) 或 'lower' (首行在底部)
1550
+ interpolation: 插值方法,控制平滑程度。'nearest'/'none'/'antialiased'(默认)
1551
+ 为块状显示、有明显分界线;'bilinear'/'bicubic' 等对像素做平滑上采样,
1552
+ 颜色渐变、无硬分界线
1553
+ norm: matplotlib Normalize/LogNorm 或 'linear'/'log'。LogNorm 时按对数刻度
1554
+ 归一化上色,颜色条刻度呈 10 的幂;extent 等: 接受但当前不生效
1555
+ """
1556
+ # numpy 风格数组(暴露 __array_interface__)直接下沉给 Rust,由底层零拷贝式
1557
+ # 读取原始缓冲区;仅对普通 list/tuple 等才在 Python 层递归转换。避免对大图像
1558
+ # 调用 .tolist() 生成数百万 Python 浮点对象的开销。
1559
+ if not hasattr(x, '__array_interface__'):
1560
+ x = _to_list_recursive(x)
1561
+ cmap = 'viridis' if cmap is None else cmap
1562
+ aspect = 'equal' if aspect is None else aspect
1563
+ vmin, vmax = _norm_vminmax(norm, vmin, vmax)
1564
+ ax = _get_axes()
1565
+ if ax is not None and hasattr(ax, 'imshow'):
1566
+ ax.imshow(x, cmap=cmap, aspect=aspect, vmin=vmin, vmax=vmax,
1567
+ alpha=alpha, origin=origin, interpolation=interpolation, norm=norm)
1568
+ return _get_figure()
1569
+ return _rsplotlib.imshow(x, cmap, aspect, vmin, vmax, alpha, origin,
1570
+ interpolation, _norm_kind(norm))
1571
+
1572
+
1573
+ def imsave(fname, arr, **kwargs):
1574
+ """将图像数据直接保存为图片文件 (无坐标轴 / 边距),兼容 matplotlib.pyplot.imsave。
1575
+
1576
+ 输出图片的像素尺寸等于数组尺寸 (N 列 -> 宽, M 行 -> 高)。
1577
+
1578
+ Args:
1579
+ fname: 保存的文件名, 相对或绝对路径。格式由 `format` 或文件扩展名决定,
1580
+ 支持 PNG / JPEG。
1581
+ arr: 图像的数组数据。2D 数组经 cmap 上色 (缺省 'viridis'); 3D 数组
1582
+ (H, W, 3/4) 视为 RGB(A), 直接按像素颜色写出 (浮点取 [0,1], 整数取 [0,255])。
1583
+ cmap: 2D 数据的颜色映射名称 (默认 'viridis')。
1584
+ vmin, vmax: 2D 数据的颜色映射值域 (缺省取数据 min/max)。
1585
+ origin: 'upper' (默认, 首行在顶部) 或 'lower' (首行在底部)。
1586
+ format: 显式指定图片格式 ('png' / 'jpeg'),缺省按扩展名推断。
1587
+ dpi: 写入 PNG 的分辨率元数据 (默认 100)。
1588
+ """
1589
+ # 同 imshow:numpy 风格数组直接下沉给 Rust,避免 .tolist() 开销。
1590
+ if not hasattr(arr, '__array_interface__'):
1591
+ arr = _to_list_recursive(arr)
1592
+ cmap = kwargs.pop('cmap', None) or 'viridis'
1593
+ vmin = kwargs.pop('vmin', None)
1594
+ vmax = kwargs.pop('vmax', None)
1595
+ origin = kwargs.pop('origin', None)
1596
+ fmt = kwargs.pop('format', None)
1597
+ dpi = kwargs.pop('dpi', None)
1598
+ dpi = 100.0 if dpi is None else float(dpi)
1599
+ return _rsplotlib.imsave(fname, arr, cmap, vmin, vmax, origin, fmt, dpi)
1600
+
1601
+
1602
+ def imread(fname, format=None):
1603
+ """从图像文件读取图像数据,兼容 matplotlib.pyplot.imread。
1604
+
1605
+ 返回 ndarray,形状为 (nrows, ncols) 或 (nrows, ncols, nchannels):
1606
+ 灰度图为 2D (无通道维);彩色图 nchannels 为 3 (RGB) 或 4 (RGBA)。
1607
+
1608
+ Args:
1609
+ fname: 图像文件名或路径 (相对或绝对)。
1610
+ format: 图像格式 (如 'png' / 'jpeg'),缺省先按文件内容嗅探,再按扩展名识别。
1611
+
1612
+ 按 matplotlib 约定: PNG 返回取值 [0,1] 的浮点数组,其余格式返回取值
1613
+ [0,255] 的整数数组。
1614
+ """
1615
+ from PIL import Image
1616
+ import rsnumpy as np
1617
+
1618
+ img = Image.open(fname)
1619
+ arr = np.array(img)
1620
+
1621
+ if img.mode == 'L' or img.mode == 'P':
1622
+ arr = arr.squeeze()
1623
+
1624
+ lower = fname.lower()
1625
+ if lower.endswith('.png'):
1626
+ arr = arr.astype('float64') / 255.0
1627
+
1628
+ return arr
1629
+
1630
+
1631
+ def semilogx(x, y, label=None, color=None, linestyle=None, marker=None, linewidth=None, **kwargs):
1632
+ """绘制 x 轴对数刻度图。"""
1633
+ x = _to_seq(x)
1634
+ y = _to_seq(y)
1635
+ return _rsplotlib.semilogx(x, y, label, color, linestyle, marker, linewidth)
1636
+
1637
+
1638
+ def semilogy(x, y, label=None, color=None, linestyle=None, marker=None, linewidth=None, **kwargs):
1639
+ """绘制 y 轴对数刻度图。"""
1640
+ x = _to_seq(x)
1641
+ y = _to_seq(y)
1642
+ return _rsplotlib.semilogy(x, y, label, color, linestyle, marker, linewidth)
1643
+
1644
+
1645
+ def loglog(x, y, label=None, color=None, linestyle=None, marker=None, linewidth=None, **kwargs):
1646
+ """绘制双对数刻度图。"""
1647
+ x = _to_seq(x)
1648
+ y = _to_seq(y)
1649
+ return _rsplotlib.loglog(x, y, label, color, linestyle, marker, linewidth)
1650
+
1651
+
1652
+ # ==================== 辅助元素 ====================
1653
+
1654
+ def text(x, y, s, fontdict=None, **kwargs):
1655
+ """添加文本标注。
1656
+
1657
+ Args:
1658
+ x, y: 文本位置 (数据坐标)
1659
+ s: 文本内容
1660
+ fontdict: 字体属性字典 (可选)
1661
+ **kwargs: 支持 fontsize, color/c, family, ha, va, rotation, dx, dy 等参数
1662
+ """
1663
+ fontsize = kwargs.get('fontsize', fontdict.get('fontsize', 12) if fontdict else 12)
1664
+ color = kwargs.get('color', fontdict.get('color', 'black') if fontdict else 'black')
1665
+ c = kwargs.get('c', None)
1666
+ family = kwargs.get('family', None)
1667
+ ha = kwargs.get('ha', kwargs.get('horizontalalignment', None))
1668
+ va = kwargs.get('va', kwargs.get('verticalalignment', None))
1669
+ rotation = kwargs.get('rotation', None)
1670
+ dx = kwargs.get('dx', None)
1671
+ dy = kwargs.get('dy', None)
1672
+ bbox = kwargs.get('bbox', None)
1673
+ if not isinstance(s, str):
1674
+ s = str(s)
1675
+ s = _render_mathtext(s)
1676
+
1677
+ # family 处理:若用户显式指定了字体族名,解析为本地字体文件并注册到
1678
+ # plotters 的字体数据库,这样真正驱动文本渲染(而不是被忽略)。
1679
+ if family:
1680
+ try:
1681
+ import os
1682
+ from .utils._font_resolver import resolve_font_path
1683
+ path = resolve_font_path(family)
1684
+ if path is None and os.path.isfile(family):
1685
+ path = family # 也允许直接传文件路径
1686
+ if path is not None:
1687
+ _rsplotlib.register_sans_serif_font(path)
1688
+ except Exception:
1689
+ # 字体注册失败不影响绘制(会回退到默认 sans-serif)
1690
+ pass
1691
+
1692
+ return _rsplotlib.text(x, y, s, fontsize, color, c, family, ha, va, rotation, dx, dy, bbox)
1693
+
1694
+
1695
+ def axhline(y=0, **kwargs):
1696
+ """绘制水平参考线。
1697
+
1698
+ Args:
1699
+ y: y 坐标位置
1700
+ color: 线颜色
1701
+ linestyle: 线型 ('-', '--', ':', '-.')
1702
+ linewidth: 线宽
1703
+ """
1704
+ return _rsplotlib.axhline(y, kwargs.get('color'), kwargs.get('linestyle'), kwargs.get('linewidth'))
1705
+
1706
+
1707
+ def axvline(x=0, **kwargs):
1708
+ """绘制垂直参考线。
1709
+
1710
+ Args:
1711
+ x: x 坐标位置
1712
+ color: 线颜色
1713
+ linestyle: 线型
1714
+ linewidth: 线宽
1715
+ """
1716
+ return _rsplotlib.axvline(x, kwargs.get('color'), kwargs.get('linestyle'), kwargs.get('linewidth'))
1717
+
1718
+
1719
+ def axhspan(ymin=None, ymax=None, **kwargs):
1720
+ """绘制水平区间填充 (在 y 方向高亮一个区间)。
1721
+
1722
+ 用法:
1723
+ plt.axhspan(0.0, 1.0, color='yellow', alpha=0.3)
1724
+ plt.axhspan(ymin=0.0, ymax=1.0, label='Range')
1725
+
1726
+ Args:
1727
+ ymin: y 轴下限
1728
+ ymax: y 轴上限
1729
+ color: 填充颜色 (默认蓝灰色)
1730
+ alpha: 透明度 (0.0-1.0, 默认 0.3)
1731
+ label: 图例标签
1732
+ **kwargs: 其他关键字参数
1733
+ """
1734
+ ymin = ymin if ymin is not None else kwargs.get('ymin')
1735
+ ymax = ymax if ymax is not None else kwargs.get('ymax')
1736
+ return _rsplotlib.axhspan(ymin, ymax, kwargs.get('color'), kwargs.get('alpha', 0.3), kwargs.get('label'))
1737
+
1738
+
1739
+ def axvspan(xmin=None, xmax=None, **kwargs):
1740
+ """绘制垂直区间填充 (在 x 方向高亮一个区间)。
1741
+
1742
+ 用法:
1743
+ plt.axvspan(0.0, 1.0, color='yellow', alpha=0.3)
1744
+ plt.axvspan(xmin=0.0, xmax=1.0, label='Range')
1745
+
1746
+ Args:
1747
+ xmin: x 轴下限
1748
+ xmax: x 轴上限
1749
+ color: 填充颜色 (默认蓝灰色)
1750
+ alpha: 透明度 (0.0-1.0, 默认 0.3)
1751
+ label: 图例标签
1752
+ **kwargs: 其他关键字参数
1753
+ """
1754
+ xmin = xmin if xmin is not None else kwargs.get('xmin')
1755
+ xmax = xmax if xmax is not None else kwargs.get('xmax')
1756
+ return _rsplotlib.axvspan(xmin, xmax, kwargs.get('color'), kwargs.get('alpha', 0.3), kwargs.get('label'))
1757
+
1758
+
1759
+ def axline(xy1, xy2, **kwargs):
1760
+ """通过两点绘制任意斜率的直线 (延长到整个绘图区域)。
1761
+
1762
+ 用法:
1763
+ plt.axline((0, 0), (1, 1), color='red')
1764
+
1765
+ Args:
1766
+ xy1: 起点坐标 (x1, y1)
1767
+ xy2: 终点坐标 (x2, y2)
1768
+ color: 线颜色
1769
+ linestyle: 线型
1770
+ linewidth: 线宽
1771
+ **kwargs: 其他关键字参数
1772
+ """
1773
+ return _rsplotlib.axline(
1774
+ tuple(xy1), tuple(xy2),
1775
+ kwargs.get('color'), kwargs.get('linestyle'),
1776
+ kwargs.get('linewidth'),
1777
+ )
1778
+
1779
+
1780
+ # annotate 默认字号:基准 12.0 随其余默认字号一并放大 DEFAULT_FONT_SCALE(=2.0) 倍。
1781
+ # 与 axes.rs 中默认字号在调用点乘以 DEFAULT_FONT_SCALE 的约定保持一致。
1782
+ _DEFAULT_ANNOTATE_FONTSIZE = 12.0 * 2.0
1783
+
1784
+
1785
+ def annotate(text, xy, xytext=None, fontsize=None, color='black', arrowprops=None, **kwargs):
1786
+ """在指定坐标添加文本标注, 可选带箭头。
1787
+
1788
+ 用法:
1789
+ plt.annotate('重要点', xy=(1, 2), xytext=(3, 4),
1790
+ arrowprops=dict(arrowstyle='->'))
1791
+
1792
+ Args:
1793
+ text: 标注文本内容
1794
+ xy: 被标注点的坐标 (数据坐标)
1795
+ xytext: 文本放置位置 (数据坐标)。默认与 xy 相同
1796
+ fontsize: 字体大小 (默认 None, 采用放大后的默认字号 24.0)
1797
+ color: 文本颜色
1798
+ arrowprops: 箭头属性字典。None 表示不画箭头; 提供 (哪怕空 dict) 则从
1799
+ xytext 绘制箭头指向 xy。支持简单箭头 (width/headwidth/headlength/
1800
+ shrink) 与花式箭头 (arrowstyle/connectionstyle/mutation_scale/
1801
+ shrinkA/shrinkB 等)。
1802
+ **kwargs: 其他关键字参数 (如 xycoords/textcoords/ha/family),转发给 Axes.annotate
1803
+ """
1804
+ text = _render_mathtext(text)
1805
+ ax = _get_axes()
1806
+ if ax is not None and hasattr(ax, 'annotate'):
1807
+ ax.annotate(text, xy, xytext, fontsize, color, arrowprops, **kwargs)
1808
+ return _get_figure()
1809
+ fig, ax = _rsplotlib.subplots()
1810
+ ax.annotate(text, xy, xytext, fontsize, color, arrowprops, **kwargs)
1811
+ return fig, ax
1812
+
1813
+
1814
+ def hlines(y, xmin=None, xmax=None, **kwargs):
1815
+ """在指定 y 位置绘制多条水平线段。
1816
+
1817
+ 由 Rust 层批量实现, 避免 Python 级 for 循环。
1818
+
1819
+ Args:
1820
+ y: 单个 y 值 或 多个 y 值的列表
1821
+ color: 线颜色
1822
+ linestyle: 线型
1823
+ linewidth: 线宽
1824
+ **kwargs: 其他关键字参数
1825
+ """
1826
+ y_arr = _to_list(y) if isinstance(y, (list, tuple)) or hasattr(y, 'tolist') else [y]
1827
+ ax = _get_axes()
1828
+ if ax is not None and hasattr(ax, 'hlines'):
1829
+ ax.hlines(y_arr, kwargs.get('color'), kwargs.get('linestyle'), kwargs.get('linewidth'))
1830
+ return _get_figure()
1831
+ return _rsplotlib.hlines(y_arr, kwargs.get('color'), kwargs.get('linestyle'), kwargs.get('linewidth'))
1832
+
1833
+
1834
+ def vlines(x, ymin=None, ymax=None, **kwargs):
1835
+ """在指定 x 位置绘制多条垂直线段 (Rust 层批量实现)。
1836
+
1837
+ Args:
1838
+ x: 单个 x 值 或 多个 x 值的列表
1839
+ color: 线颜色
1840
+ linestyle: 线型
1841
+ linewidth: 线宽
1842
+ **kwargs: 其他关键字参数
1843
+ """
1844
+ x_arr = _to_list(x) if isinstance(x, (list, tuple)) or hasattr(x, 'tolist') else [x]
1845
+ ax = _get_axes()
1846
+ if ax is not None and hasattr(ax, 'vlines'):
1847
+ ax.vlines(x_arr, kwargs.get('color'), kwargs.get('linestyle'), kwargs.get('linewidth'))
1848
+ return _get_figure()
1849
+ return _rsplotlib.vlines(x_arr, kwargs.get('color'), kwargs.get('linestyle'), kwargs.get('linewidth'))
1850
+
1851
+
1852
+ # ==================== 配置函数 ====================
1853
+
1854
+ def _font_props(fontdict, kwargs):
1855
+ """从 fontdict 与关键字参数中提取字体属性 (family, size, color)。
1856
+
1857
+ matplotlib 语义:关键字参数优先于 fontdict;family 支持 family/fontfamily/fontname
1858
+ 别名,size 支持 size/fontsize,color 支持 color/c。返回 (family, size, color),
1859
+ 其中 size 已转为 float 或 None。
1860
+ """
1861
+ fd = fontdict or {}
1862
+
1863
+ def _pick(*keys):
1864
+ for k in keys:
1865
+ if kwargs.get(k) is not None:
1866
+ return kwargs[k]
1867
+ for k in keys:
1868
+ if fd.get(k) is not None:
1869
+ return fd[k]
1870
+ return None
1871
+
1872
+ family = _pick('family', 'fontfamily', 'fontname')
1873
+ size = _pick('size', 'fontsize')
1874
+ color = _pick('color', 'c')
1875
+ size = float(size) if size is not None else None
1876
+ return family, size, color
1877
+
1878
+
1879
+ def xlabel(text, fontdict=None, loc=None, **kwargs):
1880
+ """设置 x 轴标签文本,并可通过 fontdict / 关键字参数自定义字体属性。
1881
+
1882
+ 支持的字体属性 (fontdict 的键或直接关键字参数, 关键字参数优先):
1883
+ family / fontfamily / fontname: 字体族名 (如 'Courier'、'STHeiti Light' 等)
1884
+ size / fontsize: 字号 (points)
1885
+ color: 文本颜色 (如 'r'、'#ff0000'、'SeaGreen')
1886
+
1887
+ loc: 标签水平位置,可选 'left'、'center'、'right',默认 'center'。
1888
+
1889
+ 用法:
1890
+ plt.xlabel("x - label")
1891
+ plt.xlabel("x 轴", fontdict={"family": "STHeiti Light", "size": 16, "color": "b"})
1892
+ plt.xlabel("x 轴", loc="left")
1893
+ """
1894
+ family, size, color = _font_props(fontdict, kwargs)
1895
+ return _rsplotlib.xlabel(_render_mathtext(text), color, size, family, loc)
1896
+
1897
+
1898
+ def ylabel(text, fontdict=None, loc=None, **kwargs):
1899
+ """设置 y 轴标签文本,并可通过 fontdict / 关键字参数自定义字体属性。
1900
+
1901
+ 支持的字体属性同 xlabel(family / size / color,关键字参数优先于 fontdict)。
1902
+
1903
+ loc: 标签垂直位置,可选 'bottom'、'center'、'top',默认 'center'。
1904
+
1905
+ 用法:
1906
+ plt.ylabel("y - label")
1907
+ plt.ylabel("y 轴", fontdict={"family": "STHeiti Light", "size": 16, "color": "g"})
1908
+ plt.ylabel("y 轴", loc="top")
1909
+ """
1910
+ family, size, color = _font_props(fontdict, kwargs)
1911
+ return _rsplotlib.ylabel(_render_mathtext(text), color, size, family, loc)
1912
+
1913
+
1914
+ def title(label, fontdict=None, loc=None, **kwargs):
1915
+ """设置图表标题文本,并可通过 fontdict / 关键字参数自定义字体属性。
1916
+
1917
+ 支持的字体属性 (fontdict 的键或直接关键字参数, 关键字参数优先):
1918
+ family / fontfamily / fontname: 字体族名 (如 'Courier'、'Times New Roman'、
1919
+ 'SimHei' 等)
1920
+ size / fontsize: 字号 (points)
1921
+ color: 文本颜色 (如 'r'、'#ff0000'、'SeaGreen')
1922
+
1923
+ loc: 标题水平位置,可选 'left'、'center'、'right',默认 'center'。
1924
+ pad: 标题与数据区顶部的间距(points,默认 5.0)。
1925
+
1926
+ 用法:
1927
+ plt.title("标题")
1928
+ plt.title("标题", fontdict={"family": "Courier", "size": 18, "color": "red"})
1929
+ plt.title("标题", fontsize=18, color='b')
1930
+ plt.title("标题", loc="left")
1931
+ plt.title("第一行\n第二行", pad=0.02)
1932
+ """
1933
+ family, size, color = _font_props(fontdict, kwargs)
1934
+ fd = fontdict or {}
1935
+ pad = kwargs.get('pad') or fd.get('pad')
1936
+ try:
1937
+ pad = None if pad is None else float(pad)
1938
+ except (TypeError, ValueError):
1939
+ pad = None
1940
+ return _rsplotlib.title(_render_mathtext(label), color, size, family, loc, pad)
1941
+
1942
+
1943
+ def suptitle(t, pad=5.0, **kwargs):
1944
+ """设置整个图形的总标题(居中显示在所有子图上方)。
1945
+
1946
+ Args:
1947
+ t: 标题文本
1948
+ pad: 标题与最上面子图之间的间距(默认5.0)
1949
+
1950
+ 用法:
1951
+ plt.suptitle("总标题")
1952
+ plt.suptitle("总标题", pad=10.0)
1953
+ """
1954
+ return _rsplotlib.gcf().suptitle(_render_mathtext(str(t)), pad)
1955
+
1956
+
1957
+ def grid(visible=True, **kwargs):
1958
+ """显示或隐藏网格线。
1959
+
1960
+ Args:
1961
+ visible: 是否显示 (默认 True)
1962
+ color/c: 线颜色
1963
+ linestyle/ls: 线型
1964
+ linewidth/lw: 线宽
1965
+ axis: 坐标轴 ('x', 'y', 或 'both')
1966
+ """
1967
+ c = kwargs.get('color') or kwargs.get('c')
1968
+ ls = kwargs.get('linestyle') or kwargs.get('ls')
1969
+ lw = kwargs.get('linewidth') or kwargs.get('lw')
1970
+ axis = kwargs.get('axis')
1971
+ return _rsplotlib.grid(visible, c, ls, lw, axis)
1972
+
1973
+
1974
+ _LOC_MAP = {
1975
+ 0: 'best',
1976
+ 1: 'upper right',
1977
+ 2: 'upper left',
1978
+ 3: 'lower left',
1979
+ 4: 'lower right',
1980
+ 5: 'right',
1981
+ 6: 'center left',
1982
+ 7: 'center right',
1983
+ 8: 'lower center',
1984
+ 9: 'upper center',
1985
+ 10: 'center',
1986
+ }
1987
+
1988
+
1989
+ def legend(loc='best', **kwargs):
1990
+ """显示图例 (需要 plot 时设置 label 参数)。
1991
+
1992
+ Args:
1993
+ loc: 图例位置 ('best', 'upper right', 'upper left', 'lower left',
1994
+ 'lower right', 'upper center', 'lower center',
1995
+ 'center left', 'center right', 'center'),也支持整数 0-10
1996
+ facecolor: 图例框背景色 (颜色名或 '#RRGGBB'),默认沿用半透明白底
1997
+ framealpha: 图例框背景不透明度 (0-1),默认 0.85
1998
+ edgecolor: 图例框边框色,默认浅灰
1999
+ fontsize: 图例文字字号 (point),默认 11.0
2000
+ ncol: 图例列数,默认根据位置和空间自动判定(上部/下部居中位置自动启用多列)
2001
+ """
2002
+ if isinstance(loc, int):
2003
+ loc = _LOC_MAP.get(loc, 'best')
2004
+ facecolor, framealpha, edgecolor, fontsize = _legend_frame_kwargs(kwargs)
2005
+ ncol = kwargs.get('ncol')
2006
+ return _rsplotlib.legend(loc, facecolor, framealpha, edgecolor, fontsize, ncol)
2007
+
2008
+
2009
+ def _legend_frame_kwargs(kwargs):
2010
+ """从 kwargs 提取并规范化图例框样式 (facecolor, framealpha, edgecolor, fontsize)。
2011
+
2012
+ 非字符串颜色一律忽略 (置 None),交由后端使用默认值。
2013
+ """
2014
+ facecolor = kwargs.get('facecolor')
2015
+ edgecolor = kwargs.get('edgecolor')
2016
+ framealpha = kwargs.get('framealpha')
2017
+ fontsize = kwargs.get('fontsize')
2018
+ if not isinstance(facecolor, str):
2019
+ facecolor = None
2020
+ if not isinstance(edgecolor, str):
2021
+ edgecolor = None
2022
+ try:
2023
+ framealpha = None if framealpha is None else float(framealpha)
2024
+ except (TypeError, ValueError):
2025
+ framealpha = None
2026
+ try:
2027
+ fontsize = None if fontsize is None else float(fontsize)
2028
+ except (TypeError, ValueError):
2029
+ fontsize = None
2030
+ return facecolor, framealpha, edgecolor, fontsize
2031
+
2032
+
2033
+ def xlim(left=None, right=None, **kwargs):
2034
+ """设置 x 轴显示范围。
2035
+
2036
+ Args:
2037
+ left: x 轴最小值
2038
+ right: x 轴最大值
2039
+ """
2040
+ return _rsplotlib.xlim(left, right)
2041
+
2042
+
2043
+ def ylim(bottom=None, top=None, **kwargs):
2044
+ """设置 y 轴显示范围。
2045
+
2046
+ Args:
2047
+ bottom: y 轴最小值
2048
+ top: y 轴最大值
2049
+ """
2050
+ return _rsplotlib.ylim(bottom, top)
2051
+
2052
+
2053
+ def xticks(ticks=None, labels=None, **kwargs):
2054
+ """设置 x 轴刻度。
2055
+
2056
+ Args:
2057
+ ticks: 刻度位置列表
2058
+ labels: 刻度标签列表
2059
+ """
2060
+ ticks = _to_list(ticks)
2061
+ return _rsplotlib.xticks(ticks, labels)
2062
+
2063
+
2064
+ def yticks(ticks=None, labels=None, **kwargs):
2065
+ """设置 y 轴刻度。
2066
+
2067
+ Args:
2068
+ ticks: 刻度位置列表
2069
+ labels: 刻度标签列表
2070
+ """
2071
+ ticks = _to_list(ticks)
2072
+ return _rsplotlib.yticks(ticks, labels)
2073
+
2074
+
2075
+ def xscale(scale, **kwargs):
2076
+ """设置 x 轴刻度类型 ('linear', 'log', 'logit', 'symlog' 等)。"""
2077
+ return _rsplotlib.xscale(scale)
2078
+
2079
+
2080
+ def yscale(scale, **kwargs):
2081
+ """设置 y 轴刻度类型。"""
2082
+ return _rsplotlib.yscale(scale)
2083
+
2084
+
2085
+ def margins(x_margin=None, y_margin=None, **kwargs):
2086
+ """设置自动缩放的边距。"""
2087
+ return _rsplotlib.margins(x_margin, y_margin)
2088
+
2089
+
2090
+ def box(on=None):
2091
+ """设置坐标轴边框。"""
2092
+ return _rsplotlib.box_(on)
2093
+
2094
+
2095
+ def minorticks_on():
2096
+ """显示次要刻度。"""
2097
+ return _rsplotlib.minorticks_on()
2098
+
2099
+
2100
+ def minorticks_off():
2101
+ """隐藏次要刻度。"""
2102
+ return _rsplotlib.minorticks_off()
2103
+
2104
+
2105
+ # ==================== 子图与布局 ====================
2106
+
2107
+ class _AxesArray:
2108
+ """轻量 Axes 网格容器,模拟 numpy 数组的常用索引方式。
2109
+
2110
+ 使 plt.subplots 的返回值支持 axs[i, j] 元组索引、axs[i] 行/元素索引、
2111
+ 迭代、.flat / .flatten() / .ravel(),且不依赖任何第三方数组库。
2112
+
2113
+ 内部以行主序扁平 list 保存 Axes;shape 为 (n,) 表示一维、(nrows, ncols)
2114
+ 表示二维。
2115
+ """
2116
+ __slots__ = ('_data', 'shape')
2117
+
2118
+ def __init__(self, data, shape):
2119
+ self._data = list(data)
2120
+ self.shape = shape
2121
+
2122
+ @property
2123
+ def ndim(self):
2124
+ return len(self.shape)
2125
+
2126
+ @property
2127
+ def size(self):
2128
+ return len(self._data)
2129
+
2130
+ @property
2131
+ def flat(self):
2132
+ """按行主序遍历所有 Axes 的迭代器。"""
2133
+ return iter(self._data)
2134
+
2135
+ def flatten(self):
2136
+ """返回按行主序排列的一维 list。"""
2137
+ return list(self._data)
2138
+
2139
+ ravel = flatten
2140
+
2141
+ def __len__(self):
2142
+ return self.shape[0]
2143
+
2144
+ def __iter__(self):
2145
+ # 一维:逐个产出 Axes;二维:逐行产出子 _AxesArray(与 numpy 一致)。
2146
+ if self.ndim == 1:
2147
+ return iter(self._data)
2148
+ ncols = self.shape[1]
2149
+ return (
2150
+ _AxesArray(self._data[r * ncols:(r + 1) * ncols], (ncols,))
2151
+ for r in range(self.shape[0])
2152
+ )
2153
+
2154
+ def __getitem__(self, key):
2155
+ if isinstance(key, tuple):
2156
+ if self.ndim != 2 or len(key) != 2:
2157
+ raise IndexError('二维索引仅适用于二维 Axes 数组')
2158
+ nrows, ncols = self.shape
2159
+ r, c = key
2160
+
2161
+ # 处理行索引
2162
+ if isinstance(r, slice):
2163
+ row_indices = range(nrows)[r]
2164
+ else:
2165
+ r = r % nrows
2166
+ row_indices = [r]
2167
+
2168
+ # 处理列索引
2169
+ if isinstance(c, slice):
2170
+ col_indices = range(ncols)[c]
2171
+ else:
2172
+ c = c % ncols
2173
+ col_indices = [c]
2174
+
2175
+ # 收集所有匹配的 Axes
2176
+ result = []
2177
+ for r_idx in row_indices:
2178
+ for c_idx in col_indices:
2179
+ result.append(self._data[r_idx * ncols + c_idx])
2180
+
2181
+ # 根据结果形状返回
2182
+ if len(row_indices) == 1 and len(col_indices) == 1:
2183
+ return result[0]
2184
+ elif len(row_indices) == 1:
2185
+ return _AxesArray(result, (len(col_indices),))
2186
+ elif len(col_indices) == 1:
2187
+ return _AxesArray(result, (len(row_indices),))
2188
+ else:
2189
+ return _AxesArray(result, (len(row_indices), len(col_indices)))
2190
+
2191
+ if self.ndim == 1:
2192
+ if isinstance(key, slice):
2193
+ indices = range(self.shape[0])[key]
2194
+ result = [self._data[i] for i in indices]
2195
+ return _AxesArray(result, (len(result),))
2196
+ return self._data[key]
2197
+
2198
+ # 二维单整数索引返回对应行(子 _AxesArray)。
2199
+ nrows, ncols = self.shape
2200
+ if isinstance(key, slice):
2201
+ row_indices = range(nrows)[key]
2202
+ result = []
2203
+ for r_idx in row_indices:
2204
+ result.extend(self._data[r_idx * ncols:(r_idx + 1) * ncols])
2205
+ return _AxesArray(result, (len(row_indices), ncols))
2206
+
2207
+ if key < 0:
2208
+ key += nrows
2209
+ return _AxesArray(self._data[key * ncols:(key + 1) * ncols], (ncols,))
2210
+
2211
+ def __repr__(self):
2212
+ return f'AxesArray(shape={self.shape})'
2213
+
2214
+
2215
+ def _apply_layout(fig, kwargs):
2216
+ """把 matplotlib 的 layout 关键字转成后端智能均匀边距开关。
2217
+
2218
+ layout='constrained'/'tight'/'compressed'(或已弃用的 constrained_layout=True /
2219
+ tight_layout=True)时,启用 constrained 智能布局:渲染时按各边装饰范围反解四周
2220
+ 边距,使图四周留白均匀、适中(保持 figsize 不变)。其余取值不启用。
2221
+ """
2222
+ setter = getattr(fig, 'set_constrained_layout', None)
2223
+ if setter is None:
2224
+ return
2225
+ layout = kwargs.get('layout')
2226
+ if isinstance(layout, str) and layout.lower() in ('constrained', 'tight', 'compressed'):
2227
+ setter(True)
2228
+ elif kwargs.get('constrained_layout') or kwargs.get('tight_layout'):
2229
+ setter(True)
2230
+
2231
+
2232
+ def subplots(nrows=1, ncols=1, figsize=None, dpi=None, squeeze=True, **kwargs):
2233
+ """创建子图网格 (Figure + Axes)。
2234
+
2235
+ 用法:
2236
+ fig, ax = plt.subplots() # 单图 (1x1)
2237
+ fig, axes = plt.subplots(2, 2) # 2x2 网格
2238
+ fig, axes = plt.subplots(1, 2, figsize=(10, 5)) # 自定义尺寸
2239
+ fig, ax = plt.subplots(subplot_kw={'projection': 'polar'}) # 极坐标
2240
+
2241
+ Args:
2242
+ nrows: 子图行数 (默认 1)
2243
+ ncols: 子图列数 (默认 1)
2244
+ figsize: 图的尺寸 (width, height), 单位英寸
2245
+ dpi: 分辨率 (每英寸点数)
2246
+ squeeze: 是否压缩返回的 Axes 数组维度 (默认 True), 与 matplotlib 一致
2247
+ **kwargs: 其他关键字参数,包括 subplot_kw 用于传递子图参数如 projection
2248
+
2249
+ Returns:
2250
+ 与 matplotlib 一致的返回值 (squeeze=True 时):
2251
+ - 1x1: (Figure, Axes)
2252
+ - 1xN 或 Nx1: (Figure, 一维 ndarray[Axes])
2253
+ - MxN: (Figure, 二维 ndarray[Axes]), 支持 axs[i, j] 索引
2254
+ """
2255
+ gridspec_kw = kwargs.get('gridspec_kw') or {}
2256
+ subplot_kw = kwargs.get('subplot_kw') or {}
2257
+ width_ratios = kwargs.get('width_ratios')
2258
+ if width_ratios is None:
2259
+ width_ratios = gridspec_kw.get('width_ratios')
2260
+ height_ratios = kwargs.get('height_ratios')
2261
+ if height_ratios is None:
2262
+ height_ratios = gridspec_kw.get('height_ratios')
2263
+ width_ratios = _to_list(width_ratios) if width_ratios is not None else None
2264
+ height_ratios = _to_list(height_ratios) if height_ratios is not None else None
2265
+
2266
+ layout = kwargs.get('layout')
2267
+ projection = subplot_kw.get('projection')
2268
+ result = _rsplotlib.subplots(nrows, ncols, figsize, dpi, width_ratios, height_ratios, layout, projection)
2269
+ fig = result[0]
2270
+
2271
+ # gridspec_kw={'wspace':.., 'hspace':..} 与 matplotlib 一致地控制子图间距,
2272
+ # 复用 subplots_adjust 的存储路径(在渲染阶段覆盖默认/启发式间距)。
2273
+ if gridspec_kw:
2274
+ ws = gridspec_kw.get('wspace')
2275
+ hs = gridspec_kw.get('hspace')
2276
+ if ws is not None or hs is not None:
2277
+ fig.subplots_adjust(wspace=ws, hspace=hs)
2278
+
2279
+ if nrows == 1 and ncols == 1:
2280
+ single = result[1]
2281
+ if squeeze:
2282
+ return fig, single
2283
+ flat = [single]
2284
+ else:
2285
+ flat = list(result[1])
2286
+
2287
+ # 依 matplotlib 的 squeeze 规则确定返回形状:单行 / 单列压成一维,其余保持二维。
2288
+ # 用模块自带的 _AxesArray 提供 axs[i, j] 索引,不依赖任何第三方数组库。
2289
+ if squeeze and nrows == 1:
2290
+ return fig, _AxesArray(flat, (ncols,))
2291
+ if squeeze and ncols == 1:
2292
+ return fig, _AxesArray(flat, (nrows,))
2293
+ return fig, _AxesArray(flat, (nrows, ncols))
2294
+
2295
+
2296
+ def _mosaic_track_edges(n, ratios, space):
2297
+ """把 [0,1] 分成 n 条轨道,相邻轨道间留 `space` 比例间隙。
2298
+
2299
+ 返回长度 n 的 [(start, size), ...](沿正方向)。`ratios` 为各轨道相对尺寸
2300
+ (None 视为等分);`space` 为间隙占「平均轨道尺寸」的比例,与 matplotlib 的
2301
+ wspace/hspace 语义一致 (等分时退化为后端 grid_position 的公式)。
2302
+ """
2303
+ if n <= 0:
2304
+ return []
2305
+ if ratios is None:
2306
+ ratios = [1.0] * n
2307
+ ratios = [float(r) for r in ratios]
2308
+ if len(ratios) != n:
2309
+ raise ValueError(f"ratios 长度 {len(ratios)} 与轨道数 {n} 不符")
2310
+ total_r = sum(ratios)
2311
+ if total_r <= 0:
2312
+ raise ValueError("ratios 之和必须为正")
2313
+ gap_r = space * (total_r / n) # 间隙的 ratio 单位
2314
+ unit = 1.0 / (total_r + (n - 1) * gap_r) # ratio 单位 -> [0,1] 分数
2315
+ edges = []
2316
+ cursor = 0.0
2317
+ for r in ratios:
2318
+ size = r * unit
2319
+ edges.append((cursor, size))
2320
+ cursor += size + gap_r * unit
2321
+ return edges
2322
+
2323
+
2324
+ # subplot_mosaic 默认间距(未经 gridspec_kw 指定时):与后端规则网格默认一致,
2325
+ # 足以容纳每个子图的标题 + 刻度值而不相互重叠。
2326
+ _MOSAIC_WSPACE = 0.3
2327
+ _MOSAIC_HSPACE = 0.55
2328
+
2329
+
2330
+ def subplot_mosaic(mosaic, *, sharex=False, sharey=False,
2331
+ width_ratios=None, height_ratios=None,
2332
+ empty_sentinel='.', subplot_kw=None, gridspec_kw=None,
2333
+ per_subplot_kw=None, figsize=None, dpi=None, **fig_kw):
2334
+ """根据 ASCII 布局或嵌套标签列表创建命名子图 (matplotlib subplot_mosaic 兼容)。
2335
+
2336
+ 参数:
2337
+ mosaic: 可视化布局。可为多行字符串 (每字符一列、每行一行) 或标签的二维列表。
2338
+ 相同标签在网格中的矩形包围盒合并为一个跨行/跨列的子图,`empty_sentinel`
2339
+ (默认 '.') 表示留空。
2340
+ sharex, sharey: 接受以兼容 matplotlib 签名 (当前后端各子图坐标轴相互独立)。
2341
+ width_ratios: 长度为 ncols 的列宽相对比例;等价于 gridspec_kw={'width_ratios': ...}。
2342
+ height_ratios: 长度为 nrows 的行高相对比例;等价于 gridspec_kw={'height_ratios': ...}。
2343
+ empty_sentinel: 表示「留空」的条目,默认 '.'。
2344
+ subplot_kw: 传给每个子图的关键字参数 (经 Axes.set 应用)。
2345
+ gridspec_kw: 网格参数,识别 wspace / hspace / width_ratios / height_ratios。
2346
+ per_subplot_kw: {标签 或 标签元组: kwargs} 的按图覆盖 (优先级高于 subplot_kw);
2347
+ 字符串 mosaic 下键可用多字符串 (如 "AB" 等价于 ('A','B'))。
2348
+ figsize, dpi, **fig_kw: 传给 figure() (如 layout='constrained')。
2349
+
2350
+ 返回:
2351
+ (Figure, {label: Axes})。各子图之间按 wspace/hspace 自动留出间距。
2352
+ """
2353
+ # —— 解析 mosaic 为二维标签网格 ——
2354
+ if isinstance(mosaic, str):
2355
+ lines = [ln.strip() for ln in mosaic.strip().splitlines()]
2356
+ rows = [list(ln) for ln in lines if ln]
2357
+ else:
2358
+ rows = []
2359
+ for r in mosaic:
2360
+ if any(isinstance(c, (list, tuple)) for c in r):
2361
+ raise NotImplementedError("subplot_mosaic 暂不支持嵌套布局")
2362
+ rows.append(list(r))
2363
+ nrows = len(rows)
2364
+ ncols = max((len(r) for r in rows), default=0)
2365
+ if nrows == 0 or ncols == 0:
2366
+ raise ValueError("mosaic 不能为空")
2367
+ # 列表输入允许行长不齐:短行用 empty_sentinel 补齐。
2368
+ for r in rows:
2369
+ if len(r) < ncols:
2370
+ r.extend([empty_sentinel] * (ncols - len(r)))
2371
+
2372
+ # —— 收集每个标签的矩形包围盒 (row_start, row_end, col_start, col_end) ——
2373
+ def _is_empty(label):
2374
+ return label is None or label == empty_sentinel
2375
+
2376
+ boxes = {}
2377
+ order = []
2378
+ for ri, row in enumerate(rows):
2379
+ for ci, label in enumerate(row):
2380
+ if _is_empty(label):
2381
+ continue
2382
+ if label not in boxes:
2383
+ boxes[label] = [ri, ri + 1, ci, ci + 1]
2384
+ order.append(label)
2385
+ else:
2386
+ b = boxes[label]
2387
+ b[0] = min(b[0], ri)
2388
+ b[1] = max(b[1], ri + 1)
2389
+ b[2] = min(b[2], ci)
2390
+ b[3] = max(b[3], ci + 1)
2391
+
2392
+ # —— 网格间距 / 比例:显式参数优先于 gridspec_kw ——
2393
+ gridspec_kw = dict(gridspec_kw or {})
2394
+ if width_ratios is None:
2395
+ width_ratios = gridspec_kw.get('width_ratios')
2396
+ if height_ratios is None:
2397
+ height_ratios = gridspec_kw.get('height_ratios')
2398
+ wspace = gridspec_kw.get('wspace')
2399
+ hspace = gridspec_kw.get('hspace')
2400
+ if wspace is None:
2401
+ wspace = _MOSAIC_WSPACE
2402
+ if hspace is None:
2403
+ hspace = _MOSAIC_HSPACE
2404
+
2405
+ col_edges = _mosaic_track_edges(ncols, width_ratios, wspace) # 从左向右 (x)
2406
+ row_edges = _mosaic_track_edges(nrows, height_ratios, hspace) # 从上向下 (自顶部量)
2407
+
2408
+ # —— per_subplot_kw 归一化为 {label: kwargs} ——
2409
+ psk = {}
2410
+ if per_subplot_kw:
2411
+ for k, v in per_subplot_kw.items():
2412
+ if isinstance(k, tuple):
2413
+ keys = k
2414
+ elif isinstance(mosaic, str) and isinstance(k, str) and len(k) > 1:
2415
+ keys = tuple(k)
2416
+ else:
2417
+ keys = (k,)
2418
+ for kk in keys:
2419
+ psk.setdefault(kk, {}).update(v)
2420
+
2421
+ fig = figure(figsize=figsize, dpi=dpi, **fig_kw)
2422
+ axd = {}
2423
+ for label in order:
2424
+ rs, re, cs, ce = boxes[label]
2425
+ left = col_edges[cs][0]
2426
+ right = col_edges[ce - 1][0] + col_edges[ce - 1][1]
2427
+ top = 1.0 - row_edges[rs][0]
2428
+ bottom = 1.0 - (row_edges[re - 1][0] + row_edges[re - 1][1])
2429
+ ax = fig.add_axes(left, bottom, right - left, top - bottom)
2430
+ merged = {}
2431
+ if subplot_kw:
2432
+ merged.update(subplot_kw)
2433
+ if label in psk:
2434
+ merged.update(psk[label])
2435
+ if merged:
2436
+ ax.set(**merged)
2437
+ axd[label] = ax
2438
+ return fig, axd
2439
+
2440
+
2441
+ def subplot(*args, **kwargs):
2442
+ """在当前 figure 中添加一个子图 (Axes),兼容 matplotlib 的调用签名。
2443
+
2444
+ 调用签名:
2445
+ subplot(nrows, ncols, index, **kwargs)
2446
+ subplot(pos, **kwargs) # pos 为三位整数, 如 subplot(131) 等价于 subplot(1, 3, 1)
2447
+ subplot(**kwargs) # 无位置参数, 默认为 (1, 1, 1)
2448
+
2449
+ *args 描述子图位置, 可为下列之一:
2450
+ - 三个整数 (nrows, ncols, index): 子图占据 nrows 行 ncols 列网格中的 index
2451
+ 位置, index 从左上角的 1 开始向右递增。index 也可为二元组 (first, last)
2452
+ (基于 1, 含 last), 表示子图跨越 first 到 last 的格子, 例如
2453
+ subplot(3, 1, (1, 2)) 创建一个跨越上部 2/3 的子图。
2454
+ - 一个三位整数 (如 131 等价于 1, 3, 1), 仅在子图数不超过 9 时可用。
2455
+ - 一个 SubplotSpec。
2456
+
2457
+ 关键字参数:
2458
+ projection: 投影类型。当前后端仅支持默认的直角坐标 (rectilinear),
2459
+ 其他值被接受但不生效。
2460
+ polar (bool): 为 True 时等价于 projection='polar' (当前后端不支持, 接受但不生效)。
2461
+ sharex, sharey: 与之共享 x / y 轴的 Axes (当前接受但不生效)。
2462
+ label (str): 返回 Axes 的标签。
2463
+
2464
+ Returns:
2465
+ Axes: 新建或已存在的子图。
2466
+ """
2467
+ polar = kwargs.pop('polar', False)
2468
+ projection = kwargs.pop('projection', None)
2469
+ kwargs.pop('sharex', None)
2470
+ kwargs.pop('sharey', None)
2471
+ label = kwargs.pop('label', None)
2472
+ if polar and projection is None:
2473
+ projection = 'polar'
2474
+
2475
+ if len(args) == 0:
2476
+ nrows, ncols, index = 1, 1, 1
2477
+ elif len(args) == 1:
2478
+ pos = args[0]
2479
+ if hasattr(pos, 'rowStart'): # SubplotSpec
2480
+ nrows, ncols, index = _spec_to_grid(pos)
2481
+ elif isinstance(pos, int) and not isinstance(pos, bool):
2482
+ if not 100 <= pos <= 999:
2483
+ raise ValueError(f"整数子图参数必须是三位数 (如 131),收到 {pos}")
2484
+ nrows, ncols, index = pos // 100, (pos // 10) % 10, pos % 10
2485
+ else:
2486
+ raise TypeError("subplot() 单参数形式仅支持三位整数 (如 131) 或 SubplotSpec")
2487
+ elif len(args) == 3:
2488
+ nrows, ncols, index = args
2489
+ else:
2490
+ raise TypeError(
2491
+ f"subplot() 需要 0 个、1 个 (三位整数 / SubplotSpec) 或 3 个位置参数,收到 {len(args)}"
2492
+ )
2493
+
2494
+ # index 既可为整数(单格子)也可为 (first, last) 二元组(跨格子),原生 subplot 均支持。
2495
+ ax = _rsplotlib.subplot(nrows, ncols, index)[1]
2496
+ _apply_axes_label(ax, label)
2497
+ return ax
2498
+
2499
+
2500
+ def _spec_to_grid(spec):
2501
+ """将 SubplotSpec 转换为 (nrows, ncols, (first, last)),供原生 subplot 使用。
2502
+
2503
+ SubplotSpec 用 0 基、rowStop/colStop 为开区间上界的方式描述网格跨度;
2504
+ 这里换算为基于 1、含端点的线性 (first, last) 索引。
2505
+ """
2506
+ nrows = int(spec.numRows)
2507
+ ncols = int(spec.numCols)
2508
+ first = int(spec.rowStart) * ncols + int(spec.colStart) + 1
2509
+ last = (int(spec.rowStop) - 1) * ncols + (int(spec.colStop) - 1) + 1
2510
+ return nrows, ncols, (first, last)
2511
+
2512
+
2513
+ def tight_layout(**kwargs):
2514
+ """自动调整子图布局, 避免标签重叠。"""
2515
+ return _rsplotlib.tight_layout()
2516
+
2517
+
2518
+ def subplots_adjust(left=None, right=None, bottom=None, top=None, wspace=None, hspace=None):
2519
+ """调整子图布局参数"""
2520
+ fig = _get_figure()
2521
+ if fig is not None:
2522
+ fig.subplots_adjust(left, right, bottom, top, wspace, hspace)
2523
+
2524
+
2525
+ def set_size(width, height):
2526
+ """设置图形像素尺寸。"""
2527
+ return _rsplotlib.set_size(width, height)
2528
+
2529
+
2530
+ def twinx():
2531
+ """创建共享 x 轴的双 y 轴。"""
2532
+ return _rsplotlib.twinx()
2533
+
2534
+
2535
+ def twiny():
2536
+ """创建共享 y 轴的双 x 轴。"""
2537
+ return _rsplotlib.twiny()
2538
+
2539
+
2540
+ # ==================== 图形控制 ====================
2541
+
2542
+ _FIGURES = {}
2543
+ _FIGURE_COUNTER = 1
2544
+
2545
+
2546
+ def get_fignums():
2547
+ """返回当前所有图形的编号列表。"""
2548
+ return [num for num, fig in _FIGURES.items() if len(fig.get_axes()) > 0]
2549
+
2550
+
2551
+ def figure(num=None, figsize=None, dpi=None, **kwargs):
2552
+ """创建新的 Figure 对象。
2553
+
2554
+ Args:
2555
+ num: 图形编号,如果已存在则返回该图形
2556
+ figsize: (width, height) 英寸数
2557
+ dpi: 分辨率
2558
+ **kwargs: 其他关键字参数
2559
+
2560
+ Returns:
2561
+ Figure 对象
2562
+ """
2563
+ global _FIGURE_COUNTER
2564
+
2565
+ if num is not None and num in _FIGURES:
2566
+ return _FIGURES[num]
2567
+
2568
+ if figsize is None:
2569
+ figsize = tuple(_get_rcparams().get('figure.figsize', DEFAULT_FIGSIZE))
2570
+ layout = kwargs.get('layout')
2571
+ fig = _rsplotlib.figure(figsize=figsize, dpi=dpi, layout=layout)
2572
+
2573
+ if num is None:
2574
+ num = _FIGURE_COUNTER
2575
+ _FIGURE_COUNTER += 1
2576
+ _FIGURES[num] = fig
2577
+
2578
+ return fig
2579
+
2580
+
2581
+ def axes(arg=None, **kwargs):
2582
+ """向当前图形添加一个坐标区并返回它 (matplotlib plt.axes 兼容)。
2583
+
2584
+ arg 为 None 时在当前图形上创建一个全幅坐标区 (等价于 add_subplot());
2585
+ 没有当前图形则新建一个。传入 rect [left, bottom, width, height] 的定位形式
2586
+ 当前不支持,按全幅处理。其余关键字作为坐标区属性 (如 xlabel/ylabel/title) 应用。
2587
+ """
2588
+ fig = _get_figure()
2589
+ if fig is None:
2590
+ fig = figure()
2591
+ ax = fig.add_subplot()
2592
+ if kwargs:
2593
+ ax.set(**kwargs)
2594
+ return ax
2595
+
2596
+
2597
+ def savefig(fname, **kwargs):
2598
+ """保存图形到文件。
2599
+
2600
+ 支持的文件格式:
2601
+ - .png: 便携式网络图形 (位图)
2602
+ - .jpg: JPEG 图像
2603
+ - .svg: 可缩放矢量图形
2604
+
2605
+ 用法:
2606
+ plt.savefig('figure.png')
2607
+ plt.savefig('figure.png', dpi=300) # 高分辨率
2608
+ plt.savefig('figure.svg')
2609
+
2610
+ Args:
2611
+ fname: 输出文件名 (含扩展名)
2612
+ dpi: 分辨率 (每英寸点数, 默认与创建时一致)。
2613
+ 对于高清晰度出版物, 使用 300 或更高。
2614
+ **kwargs: 其他关键字参数
2615
+ """
2616
+ dpi = kwargs.get('dpi')
2617
+ fig = _get_figure()
2618
+ if fig is not None:
2619
+ if dpi is not None:
2620
+ fig.savefig(fname, dpi)
2621
+ else:
2622
+ fig.savefig(fname)
2623
+ return
2624
+ # 无 Figure 时回退到模块级
2625
+ if dpi is not None:
2626
+ _rsplotlib.savefig(fname, dpi)
2627
+ else:
2628
+ _rsplotlib.savefig(fname)
2629
+
2630
+
2631
+ def show(**kwargs):
2632
+ """在默认应用中显示图形。无当前 figure 时静默返回(与 matplotlib 一致)。"""
2633
+ if _get_figure() is None:
2634
+ return None
2635
+ return _rsplotlib.show()
2636
+
2637
+
2638
+ def isinteractive():
2639
+ """交互模式状态。rsplotlib 使用非交互 (Agg) 后端,恒为 False(与 matplotlib 非交互模式一致)。"""
2640
+ return False
2641
+
2642
+
2643
+ def draw(*args, **kwargs):
2644
+ """重绘当前 figure。非交互后端下为空操作(实际渲染在 savefig/show 时进行)。"""
2645
+ return None
2646
+
2647
+
2648
+ def gca(*args, **kwargs):
2649
+ """获取当前 Axes;无当前 figure/axes 时自动新建(与 matplotlib 一致)。"""
2650
+ try:
2651
+ return _rsplotlib.gca()
2652
+ except Exception:
2653
+ pass
2654
+ fig = _get_figure()
2655
+ if fig is None:
2656
+ fig = figure()
2657
+ return fig.add_subplot()
2658
+
2659
+
2660
+ def gcf(**kwargs):
2661
+ """获取当前 Figure。"""
2662
+ return _rsplotlib.gcf()
2663
+
2664
+
2665
+ def cla():
2666
+ """清空当前 Axes 内容。"""
2667
+ return _rsplotlib.cla()
2668
+
2669
+
2670
+ def clf():
2671
+ """清空当前 Figure 内容 (清除所有子图)。"""
2672
+ return _rsplotlib.clf()
2673
+
2674
+
2675
+ def close(fig=None):
2676
+ """关闭当前 Figure。
2677
+
2678
+ Args:
2679
+ fig: 图形或 'all' (兼容 matplotlib)
2680
+ """
2681
+ return _rsplotlib.close()
2682
+
2683
+
2684
+ def axis(arg=None, **kwargs):
2685
+ """获取或设置坐标轴属性,兼容 matplotlib.pyplot.axis。
2686
+
2687
+ 调用形式:
2688
+ axis() -> 返回当前 (xmin, xmax, ymin, ymax)
2689
+ axis((xmin, xmax, ymin, ymax)) -> 一次性设置四个轴限 (也接受 list)
2690
+ axis('off') / axis(False) -> 隐藏坐标轴装饰
2691
+ axis('on') / axis(True) -> 显示坐标轴装饰
2692
+ axis('equal') / axis('scaled') -> 等比例缩放
2693
+ axis(xmin=.., xmax=.., ymin=.., ymax=..) -> 关键字设置轴限
2694
+
2695
+ 返回当前的 (xmin, xmax, ymin, ymax)(无法获取时返回 None)。
2696
+ """
2697
+ ax = _get_axes()
2698
+ if isinstance(arg, (list, tuple)):
2699
+ # 序列形式: [xmin, xmax, ymin, ymax]
2700
+ if len(arg) != 4:
2701
+ raise ValueError(
2702
+ "axis(): 序列参数必须为 (xmin, xmax, ymin, ymax) 四个值"
2703
+ )
2704
+ xmin, xmax, ymin, ymax = arg
2705
+ xlim(xmin, xmax)
2706
+ ylim(ymin, ymax)
2707
+ elif arg is False or arg == 'off':
2708
+ if ax is not None:
2709
+ try:
2710
+ ax._axis_off()
2711
+ except Exception:
2712
+ pass
2713
+ elif arg is True or arg == 'on':
2714
+ if ax is not None and hasattr(ax, '_axis_on'):
2715
+ ax._axis_on()
2716
+ elif arg in ('equal', 'scaled', 'image', 'square'):
2717
+ if ax is not None:
2718
+ ax.set_aspect('equal')
2719
+ # 'tight' / 'auto' 等自动缩放选项:数据本已填充绘图框,保持当前行为。
2720
+
2721
+ # 关键字形式: axis(xmin=.., xmax=.., ymin=.., ymax=..)
2722
+ if kwargs:
2723
+ xmn, xmx = kwargs.get('xmin'), kwargs.get('xmax')
2724
+ ymn, ymx = kwargs.get('ymin'), kwargs.get('ymax')
2725
+ if xmn is not None or xmx is not None:
2726
+ xlim(xmn, xmx)
2727
+ if ymn is not None or ymx is not None:
2728
+ ylim(ymn, ymx)
2729
+
2730
+ # 返回当前范围 (xmin, xmax, ymin, ymax)
2731
+ if ax is not None and hasattr(ax, 'get_xlim') and hasattr(ax, 'get_ylim'):
2732
+ try:
2733
+ x0, x1 = ax.get_xlim()
2734
+ y0, y1 = ax.get_ylim()
2735
+ return (x0, x1, y0, y1)
2736
+ except Exception:
2737
+ return None
2738
+ return None
2739
+
2740
+
2741
+ def _apply_colorbar(target, kwargs):
2742
+ """把 matplotlib colorbar kwargs 解析后应用到目标 Axes。
2743
+
2744
+ 支持 location / orientation / shrink / aspect / pad / fraction / label / extend /
2745
+ ticks / format;其余参数(cax / use_gridspec / anchor / panchor / extendfrac /
2746
+ extendrect / drawedges / boundaries / values / spacing 等)接受但当前不生效。
2747
+ """
2748
+ if target is None:
2749
+ return None
2750
+ ex = getattr(target, 'enable_colorbar_ex', None)
2751
+ if ex is None:
2752
+ if hasattr(target, 'enable_colorbar'):
2753
+ target.enable_colorbar()
2754
+ return None
2755
+
2756
+ def _f(v):
2757
+ try:
2758
+ return float(v)
2759
+ except (TypeError, ValueError):
2760
+ return None
2761
+
2762
+ ticks = kwargs.get('ticks')
2763
+ ticks_list = None
2764
+ if ticks is not None:
2765
+ seq = _to_list(ticks)
2766
+ if isinstance(seq, (list, tuple)):
2767
+ ticks_list = [float(t) for t in seq if _f(t) is not None]
2768
+
2769
+ fmt = kwargs.get('format')
2770
+ location = kwargs.get('location')
2771
+ orientation = kwargs.get('orientation')
2772
+ extend = kwargs.get('extend')
2773
+ label = kwargs.get('label')
2774
+
2775
+ ex(
2776
+ location=str(location) if location is not None else None,
2777
+ orientation=str(orientation) if orientation is not None else None,
2778
+ shrink=_f(kwargs.get('shrink')),
2779
+ aspect=_f(kwargs.get('aspect')),
2780
+ pad=_f(kwargs.get('pad')),
2781
+ fraction=_f(kwargs.get('fraction')),
2782
+ label=str(label) if label is not None else None,
2783
+ extend=str(extend) if extend is not None else None,
2784
+ ticks=ticks_list,
2785
+ format=fmt if isinstance(fmt, str) else None,
2786
+ )
2787
+ return None
2788
+
2789
+
2790
+ def colorbar(mappable=None, cax=None, ax=None, **kwargs):
2791
+ """在目标坐标区上添加颜色条。
2792
+
2793
+ 颜色条基于最近一次可映射绘制(scatter 数值 c + cmap,或 imshow / pcolormesh /
2794
+ contourf)记录的 (cmap, vmin, vmax) 信息渲染。若此前没有可映射绘制,则按
2795
+ viridis / [0,1] 兜底。支持 location/orientation/shrink/aspect/pad/fraction/
2796
+ label/extend/ticks/format 等参数(见 `_apply_colorbar`)。
2797
+ """
2798
+ target = ax
2799
+ if target is None and mappable is not None:
2800
+ target = getattr(mappable, 'axes', None)
2801
+ if target is None:
2802
+ target = _get_axes()
2803
+ _apply_colorbar(target, kwargs)
2804
+ return None
2805
+
2806
+
2807
+ def get_cmap(name=None, lut=None):
2808
+ """获取颜色映射 (占位实现)。"""
2809
+ return name
2810
+
2811
+
2812
+ # ==================== Axes 类补丁 ====================
2813
+
2814
+ def _patch_axes_get_gridspec():
2815
+ """为 Rust Axes 类添加 get_gridspec() 支持。
2816
+
2817
+ 这是 matplotlib 兼容接口,用于获取子图所在的网格布局对象。
2818
+ 返回的 GridSpec 支持 gs[row, col] 和 gs[row_start:row_end, col_start:col_end] 语法。
2819
+ """
2820
+ from . import rsplotlib as _rs
2821
+ from .layout.gridspec import GridSpec
2822
+
2823
+ def _get_gridspec(self):
2824
+ # 获取 Axes 所属的 Figure
2825
+ try:
2826
+ # 获取当前 figure
2827
+ fig = _get_figure()
2828
+ if fig is None:
2829
+ return None
2830
+
2831
+ # 检查 figure 是否有 nrows 和 ncols 方法
2832
+ if hasattr(fig, 'nrows') and callable(fig.nrows):
2833
+ nrows = fig.nrows()
2834
+ else:
2835
+ nrows = 1
2836
+
2837
+ if hasattr(fig, 'ncols') and callable(fig.ncols):
2838
+ ncols = fig.ncols()
2839
+ else:
2840
+ ncols = 1
2841
+
2842
+ # 获取 figure 的边界信息
2843
+ if hasattr(fig, 'subplot_left') and callable(fig.subplot_left):
2844
+ left = fig.subplot_left()
2845
+ else:
2846
+ left = None
2847
+
2848
+ if hasattr(fig, 'subplot_right') and callable(fig.subplot_right):
2849
+ right = fig.subplot_right()
2850
+ else:
2851
+ right = None
2852
+
2853
+ if hasattr(fig, 'subplot_bottom') and callable(fig.subplot_bottom):
2854
+ bottom = fig.subplot_bottom()
2855
+ else:
2856
+ bottom = None
2857
+
2858
+ if hasattr(fig, 'subplot_top') and callable(fig.subplot_top):
2859
+ top = fig.subplot_top()
2860
+ else:
2861
+ top = None
2862
+
2863
+ # 获取间距信息
2864
+ if hasattr(fig, 'subplot_wspace') and callable(fig.subplot_wspace):
2865
+ wspace = fig.subplot_wspace()
2866
+ else:
2867
+ wspace = None
2868
+
2869
+ if hasattr(fig, 'subplot_hspace') and callable(fig.subplot_hspace):
2870
+ hspace = fig.subplot_hspace()
2871
+ else:
2872
+ hspace = None
2873
+
2874
+ # 获取宽度和高度比例
2875
+ if hasattr(fig, 'width_ratios') and callable(fig.width_ratios):
2876
+ width_ratios = fig.width_ratios()
2877
+ else:
2878
+ width_ratios = None
2879
+
2880
+ if hasattr(fig, 'height_ratios') and callable(fig.height_ratios):
2881
+ height_ratios = fig.height_ratios()
2882
+ else:
2883
+ height_ratios = None
2884
+
2885
+ # 创建一个 GridSpec 对象,使用 figure 的边界信息
2886
+ gs = GridSpec(nrows, ncols, left=left, right=right, bottom=bottom, top=top, wspace=wspace, hspace=hspace, width_ratios=width_ratios, height_ratios=height_ratios)
2887
+ return gs
2888
+ except Exception:
2889
+ # 如果获取失败,返回一个默认的 GridSpec
2890
+ return GridSpec(1, 1)
2891
+
2892
+ _rs.Axes.get_gridspec = _get_gridspec
2893
+
2894
+
2895
+ def _patch_axes_remove():
2896
+ """为 Rust Axes 类添加 remove() 支持。
2897
+
2898
+ 这是 matplotlib 兼容接口,用于从 Figure 中移除当前 Axes。
2899
+ """
2900
+ from . import rsplotlib as _rs
2901
+
2902
+ def _remove(self):
2903
+ # 从 Figure 中移除当前 Axes
2904
+ try:
2905
+ # 尝试获取当前 figure
2906
+ fig = _get_figure()
2907
+ if fig is None:
2908
+ return
2909
+
2910
+ # 检查当前 axes 是否在 figure 中
2911
+ if self in fig.get_axes():
2912
+ # 使用 figure 的 remove_axes 方法
2913
+ if hasattr(fig, 'remove_axes'):
2914
+ fig.remove_axes(self)
2915
+ except Exception:
2916
+ pass
2917
+
2918
+ _rs.Axes.remove = _remove
2919
+
2920
+
2921
+ # ==================== Figure 类补丁 ====================
2922
+
2923
+ def _patch_figure_add_subplot():
2924
+ """为 Rust Figure 类添加 add_subplot(nrows, ncols, index) 支持。"""
2925
+ from . import rsplotlib as _rs
2926
+
2927
+ _orig_add_subplot = _rs.Figure.add_subplot
2928
+
2929
+ def _add_subplot(self, *args, **kwargs):
2930
+ if len(args) == 0:
2931
+ # matplotlib: add_subplot() 等价于 add_subplot(1, 1, 1)
2932
+ args = (1, 1, 1)
2933
+ if len(args) == 1:
2934
+ ax = _orig_add_subplot(self, args[0])
2935
+ elif len(args) == 3:
2936
+ nrows, ncols, index = args
2937
+ if isinstance(index, tuple):
2938
+ indices = [i - 1 for i in index]
2939
+ row_start = indices[0] // ncols
2940
+ row_end = indices[-1] // ncols + 1
2941
+ col_start = indices[0] % ncols
2942
+ col_end = indices[-1] % ncols + 1
2943
+ else:
2944
+ index_0 = index - 1
2945
+ row_start = index_0 // ncols
2946
+ row_end = row_start + 1
2947
+ col_start = index_0 % ncols
2948
+ col_end = col_start + 1
2949
+ from .layout.gridspec import SubplotSpec
2950
+ spec = SubplotSpec(None, row_start, row_end, col_start, col_end)
2951
+ ax = _orig_add_subplot(self, spec)
2952
+ else:
2953
+ raise TypeError(
2954
+ f"add_subplot() takes 1 or 3 positional arguments but {len(args)} were given"
2955
+ )
2956
+ # matplotlib: 余下关键字作为 Axes 属性,经 ax.set(**kwargs) 应用
2957
+ if kwargs:
2958
+ ax.set(**kwargs)
2959
+ return ax
2960
+
2961
+ _rs.Figure.add_subplot = _add_subplot
2962
+
2963
+ def _fig_colorbar(self, mappable=None, cax=None, ax=None, use_gridspec=True, **kwargs):
2964
+ """在目标子图上启用颜色条。优先用显式 ax,其次用 mappable 记录的 Axes,最后
2965
+ 回退到当前 Axes。支持 location/orientation/shrink/aspect/pad/fraction/label/
2966
+ extend/ticks/format 等参数(见 `_apply_colorbar`)。"""
2967
+ target = ax
2968
+ if target is None and mappable is not None:
2969
+ target = getattr(mappable, 'axes', None)
2970
+ if target is None:
2971
+ target = _get_axes()
2972
+ _apply_colorbar(target, kwargs)
2973
+ return None
2974
+
2975
+ _rs.Figure.colorbar = _fig_colorbar
2976
+
2977
+
2978
+ class _SecondaryAxis:
2979
+ """secondary_xaxis / secondary_yaxis 返回的副轴句柄。
2980
+
2981
+ 副轴本身不新建坐标系,仅由 Rust 后端在主轴对侧按变换后的刻度绘制刻度线/刻度值。
2982
+ 此对象持有父 Axes 引用与轴向 ('x'/'y'),让 set_xlabel/set_ylabel 把轴标签回写到
2983
+ Rust;其余未实现的链式调用(set_xticks / tick_params 等)由 __getattr__ 吸收为空操作。
2984
+ """
2985
+
2986
+ def __init__(self, parent=None, which='x'):
2987
+ self._parent = parent
2988
+ self._which = which
2989
+
2990
+ def _set_label(self, label):
2991
+ if self._parent is not None:
2992
+ self._parent.set_secondary_label(self._which, _render_mathtext(str(label)))
2993
+ return None
2994
+
2995
+ def set_xlabel(self, label, *args, **kwargs):
2996
+ return self._set_label(label)
2997
+
2998
+ def set_ylabel(self, label, *args, **kwargs):
2999
+ return self._set_label(label)
3000
+
3001
+ def __getattr__(self, name):
3002
+ def _noop(*args, **kwargs):
3003
+ return None
3004
+ return _noop
3005
+
3006
+
3007
+ class _ScalarMappable:
3008
+ """colorbar 所需的可映射句柄(近似)。持有绘制所在的 Axes 引用,
3009
+ 供 Figure.colorbar 在未显式传 ax 时定位目标子图。"""
3010
+
3011
+ def __init__(self, ax):
3012
+ self.axes = ax
3013
+
3014
+
3015
+ def _norm_vminmax(norm, vmin, vmax):
3016
+ """从 matplotlib Normalize/LogNorm 对象取 vmin/vmax(显式 vmin/vmax 优先)。"""
3017
+ if norm is not None:
3018
+ if vmin is None:
3019
+ vmin = getattr(norm, 'vmin', None)
3020
+ if vmax is None:
3021
+ vmax = getattr(norm, 'vmax', None)
3022
+ return vmin, vmax
3023
+
3024
+
3025
+ def _norm_kind(norm):
3026
+ """返回归一化类型标记 'linear' / 'log',供 Rust 侧选择上色与颜色条刻度方式。
3027
+
3028
+ 识别 rsplotlib.colors.Normalize/LogNorm(带 _norm_kind 属性)以及字符串
3029
+ 'log' / 'linear';其余(含 None)默认线性。
3030
+ """
3031
+ if norm is None:
3032
+ return 'linear'
3033
+ if isinstance(norm, str):
3034
+ return 'log' if norm.strip().lower() == 'log' else 'linear'
3035
+ kind = getattr(norm, '_norm_kind', None)
3036
+ return kind if kind in ('linear', 'log') else 'linear'
3037
+
3038
+
3039
+ def _seq_minmax(a):
3040
+ """序列/数组的 (min, max),无法解析时返回 (None, None)。"""
3041
+ if a is None:
3042
+ return None, None
3043
+ try:
3044
+ if hasattr(a, 'min') and hasattr(a, 'max'):
3045
+ return float(a.min()), float(a.max())
3046
+ except (TypeError, ValueError):
3047
+ pass
3048
+ lst = _to_list(a)
3049
+ if isinstance(lst, (list, tuple)) and lst:
3050
+ try:
3051
+ nums = [float(v) for v in lst]
3052
+ return min(nums), max(nums)
3053
+ except (TypeError, ValueError):
3054
+ return None, None
3055
+ return None, None
3056
+
3057
+
3058
+ def _patch_axes():
3059
+ """为 Rust Axes 类添加 Python 级别的 API 兼容补丁。"""
3060
+ from . import rsplotlib as _rs
3061
+
3062
+ # secondary_xaxis / secondary_yaxis: 在主轴对侧绘制变换后的刻度(见 _SecondaryAxis)。
3063
+ # functions 为 (forward, inverse) 元组或单个可调用;forward 把主轴数据映射到副轴刻度值。
3064
+ def _parse_secondary_functions(functions):
3065
+ forward = None
3066
+ inverse = None
3067
+ if isinstance(functions, (tuple, list)):
3068
+ if len(functions) >= 1:
3069
+ forward = functions[0]
3070
+ if len(functions) >= 2:
3071
+ inverse = functions[1]
3072
+ elif callable(functions):
3073
+ forward = functions
3074
+ if not callable(forward):
3075
+ def forward(v):
3076
+ return v
3077
+ if not callable(inverse):
3078
+ inverse = None
3079
+ return forward, inverse
3080
+
3081
+ def _secondary_xaxis(self, location=None, functions=None, *args, **kwargs):
3082
+ forward, inverse = _parse_secondary_functions(functions)
3083
+ loc = location if isinstance(location, str) else 'top'
3084
+ self.register_secondary_axis('x', loc, forward, inverse)
3085
+ return _SecondaryAxis(self, 'x')
3086
+
3087
+ def _secondary_yaxis(self, location=None, functions=None, *args, **kwargs):
3088
+ forward, inverse = _parse_secondary_functions(functions)
3089
+ loc = location if isinstance(location, str) else 'right'
3090
+ self.register_secondary_axis('y', loc, forward, inverse)
3091
+ return _SecondaryAxis(self, 'y')
3092
+
3093
+ _rs.Axes.secondary_xaxis = _secondary_xaxis
3094
+ _rs.Axes.secondary_yaxis = _secondary_yaxis
3095
+
3096
+ def _get_projection(self):
3097
+ return self.get_projection()
3098
+
3099
+ _rs.Axes.projection = property(_get_projection)
3100
+
3101
+ # plot: 支持单参数 ax.plot(y)、格式字符串 ax.plot(y, 'o')/ax.plot(x, y, 'o:r')
3102
+ # 以及一次多条线 ax.plot(x1, y1, fmt1, x2, y2, ...)。
3103
+ _orig_plot = _rs.Axes.plot
3104
+
3105
+ def _plot(self, *args, **kwargs):
3106
+ _map_aliases(kwargs)
3107
+ if isinstance(kwargs.get('label'), str):
3108
+ kwargs['label'] = _render_mathtext(kwargs['label'])
3109
+ pairs, _ = _parse_plot_args(args, kwargs)
3110
+ lines = []
3111
+ markevery = kwargs.get('markevery')
3112
+ for x, y, fmt in pairs:
3113
+ call_kwargs = dict(kwargs)
3114
+ if fmt:
3115
+ # fmt 解析出的样式作为默认值,不覆盖用户显式传入的关键字参数。
3116
+ for key, value in _parse_fmt(fmt).items():
3117
+ call_kwargs.setdefault(key, value)
3118
+ # 日期坐标:datetime/date 序列的 x 转为自 1970-01-01 起天数的 float。
3119
+ xnum = _maybe_dates_to_num(x)
3120
+ if xnum is not None:
3121
+ x = xnum
3122
+ # 分类坐标:字符串 x/y 映射到 0,1,2,... 位置,字符串作为刻度标签。
3123
+ x, x_tick_labels = _categorical(x)
3124
+ y, y_tick_labels = _categorical(y)
3125
+ # 过滤掉 Rust 层不支持的关键字参数
3126
+ unsupported_args = {'markevery', 'alpha'}
3127
+ call_kwargs = {k: v for k, v in call_kwargs.items() if k not in unsupported_args}
3128
+ # 如果有 markevery,先去掉 marker 只画折线,然后用 scatter 单独绘制标记点
3129
+ if markevery is not None and call_kwargs.get('marker'):
3130
+ # 保存 marker 相关参数
3131
+ marker = call_kwargs.pop('marker')
3132
+ markersize = call_kwargs.pop('markersize', 6)
3133
+ markerfacecolor = call_kwargs.pop('markerfacecolor', None)
3134
+ markeredgecolor = call_kwargs.pop('markeredgecolor', None)
3135
+ linecolor = call_kwargs.get('color', None)
3136
+ # 画折线(不带 marker)
3137
+ lines.append(_orig_plot(self, x, y, **call_kwargs))
3138
+ # 用 scatter 绘制每隔 markevery 的标记点
3139
+ x_list = x if isinstance(x, list) else list(x)
3140
+ y_list = y if isinstance(y, list) else list(y)
3141
+ indices = list(range(0, len(x_list), markevery))
3142
+ x_sub = [x_list[i] for i in indices]
3143
+ y_sub = [y_list[i] for i in indices]
3144
+ scatter_kwargs = {'marker': marker, 's': markersize ** 2}
3145
+ if markerfacecolor is not None:
3146
+ scatter_kwargs['c'] = markerfacecolor
3147
+ elif linecolor is not None:
3148
+ scatter_kwargs['c'] = linecolor
3149
+ if markeredgecolor is not None:
3150
+ scatter_kwargs['edgecolor'] = markeredgecolor
3151
+ self.scatter(x_sub, y_sub, **scatter_kwargs)
3152
+ else:
3153
+ # 正常绘制(所有点都标记)
3154
+ lines.append(_orig_plot(self, x, y, **call_kwargs))
3155
+ if x_tick_labels is not None:
3156
+ self.set_xticks(list(range(len(x_tick_labels))), x_tick_labels)
3157
+ if y_tick_labels is not None:
3158
+ self.set_yticks(list(range(len(y_tick_labels))), y_tick_labels)
3159
+ # matplotlib: plot() 返回 Line2D 列表, 支持 `l, = ax.plot(...)` 解包。
3160
+ return lines
3161
+
3162
+ _rs.Axes.plot = _plot
3163
+
3164
+ # hlines / vlines: Rust 层已支持批量, 直接转发
3165
+ _orig_hlines = _rs.Axes.hlines
3166
+ _orig_vlines = _rs.Axes.vlines
3167
+
3168
+ def _hlines(self, y, xmin=None, xmax=None, color=None, linestyle=None, linewidth=None, **kwargs):
3169
+ y_arr = _to_list(y) if isinstance(y, (list, tuple)) or hasattr(y, 'tolist') else [y]
3170
+ return _orig_hlines(self, y_arr, color, linestyle, linewidth)
3171
+
3172
+ def _vlines(self, x, ymin=None, ymax=None, color=None, linestyle=None, linewidth=None, **kwargs):
3173
+ x_arr = _to_list(x) if isinstance(x, (list, tuple)) or hasattr(x, 'tolist') else [x]
3174
+ return _orig_vlines(self, x_arr, color, linestyle, linewidth)
3175
+
3176
+ _rs.Axes.hlines = _hlines
3177
+ _rs.Axes.vlines = _vlines
3178
+
3179
+ # scatter: 支持 c/s 为数组、数值 c + cmap、RGB(A) 二维行数组
3180
+ _orig_scatter = _rs.Axes.scatter
3181
+
3182
+ def _scatter(self, x, y, s=None, c=None, marker=None, label=None, alpha=1.0,
3183
+ edgecolor=None, linewidth=None, **kwargs):
3184
+ # matplotlib data= 关键字:x/y 位置参数与 c/s 若为字符串键,从 data 中取值。
3185
+ data = kwargs.pop('data', None)
3186
+ if data is not None:
3187
+ x = _replace_from_data(x, data)
3188
+ y = _replace_from_data(y, data)
3189
+ c = _replace_from_data(c, data)
3190
+ s = _replace_from_data(s, data)
3191
+ # 复数名 edgecolors/linewidths 为 matplotlib 主用形式(OO API 以关键字传入),
3192
+ # 单数名 edgecolor/linewidth 既是别名、也是模块级 scatter() 路由过来的位置参数。
3193
+ edgecolors = kwargs.pop('edgecolors', None)
3194
+ linewidths = kwargs.pop('linewidths', None)
3195
+ if edgecolor is None:
3196
+ edgecolor = kwargs.pop('edgecolor', None)
3197
+ else:
3198
+ kwargs.pop('edgecolor', None)
3199
+ if linewidth is None:
3200
+ linewidth = kwargs.pop('linewidth', None)
3201
+ else:
3202
+ kwargs.pop('linewidth', None)
3203
+ edgecolor = _coerce_edgecolor(edgecolors if edgecolors is not None else edgecolor)
3204
+ linewidth = _coerce_linewidth(linewidths if linewidths is not None else linewidth)
3205
+ use_multi, args, mappable = _normalize_scatter(
3206
+ x, y, s, c, marker, label=label, alpha=alpha,
3207
+ edgecolor=edgecolor, linewidth=linewidth, kwargs=kwargs)
3208
+ if mappable is not None:
3209
+ self.set_mappable(*mappable)
3210
+ if use_multi:
3211
+ self.scatter_multi(*args)
3212
+ else:
3213
+ _orig_scatter(self, *args)
3214
+ from .collections import PathCollection
3215
+ return PathCollection(None)
3216
+
3217
+ _rs.Axes.scatter = _scatter
3218
+
3219
+ # imshow: 吸收 matplotlib 的 norm 参数(从 Normalize/LogNorm 取 vmin/vmax 与
3220
+ # 归一化类型),并返回可映射句柄供 fig.colorbar 使用。
3221
+ _orig_imshow = _rs.Axes.imshow
3222
+
3223
+ def _imshow(self, x, cmap='viridis', aspect='equal', vmin=None, vmax=None,
3224
+ alpha=None, origin=None, interpolation=None, norm=None, **kwargs):
3225
+ vmin, vmax = _norm_vminmax(norm, vmin, vmax)
3226
+ cmap = cmap or 'viridis'
3227
+ aspect = aspect or 'equal'
3228
+ _orig_imshow(self, x, cmap=cmap, aspect=aspect, vmin=vmin, vmax=vmax,
3229
+ alpha=alpha, origin=origin, interpolation=interpolation,
3230
+ norm=_norm_kind(norm))
3231
+ return _ScalarMappable(self)
3232
+
3233
+ _rs.Axes.imshow = _imshow
3234
+
3235
+ _orig_tick_params = _rs.Axes.tick_params
3236
+
3237
+ def _tick_params(self, **kwargs):
3238
+ kwargs.pop('which', None)
3239
+ return _orig_tick_params(self, **kwargs)
3240
+ _rs.Axes.tick_params = _tick_params
3241
+
3242
+ # pcolormesh / contourf: 后端未原生支持,近似为 imshow 上色
3243
+ # (origin='lower', aspect='auto' 使色块填满子图框)。返回可映射句柄供 colorbar 使用。
3244
+ def _pcolormesh(self, *args, **kwargs):
3245
+ cmap = kwargs.pop('cmap', None) or 'viridis'
3246
+ vmin = kwargs.pop('vmin', None)
3247
+ vmax = kwargs.pop('vmax', None)
3248
+ norm = kwargs.pop('norm', None)
3249
+ vmin, vmax = _norm_vminmax(norm, vmin, vmax)
3250
+ z = args[2] if len(args) >= 3 else args[0]
3251
+ self.imshow(z, cmap=cmap, aspect='auto', vmin=vmin, vmax=vmax,
3252
+ origin='lower', norm=norm)
3253
+ return _ScalarMappable(self)
3254
+
3255
+ def _contourf(self, *args, **kwargs):
3256
+ cmap = kwargs.pop('cmap', None) or 'viridis'
3257
+ vmin = kwargs.pop('vmin', None)
3258
+ vmax = kwargs.pop('vmax', None)
3259
+ norm = kwargs.pop('norm', None)
3260
+ vmin, vmax = _norm_vminmax(norm, vmin, vmax)
3261
+ levels = kwargs.pop('levels', None)
3262
+ if levels is not None:
3263
+ lo, hi = _seq_minmax(levels)
3264
+ vmin = lo if vmin is None else vmin
3265
+ vmax = hi if vmax is None else vmax
3266
+ z = args[2] if len(args) >= 3 else args[0]
3267
+ self.imshow(z, cmap=cmap, aspect='auto', vmin=vmin, vmax=vmax,
3268
+ origin='lower', norm=norm)
3269
+ return _ScalarMappable(self)
3270
+
3271
+ _rs.Axes.pcolormesh = _pcolormesh
3272
+ _rs.Axes.contourf = _contourf
3273
+
3274
+ # bar / barh: 支持类别坐标(字符串 x/y 映射到 0,1,2,... 位置,字符串作为刻度标签)。
3275
+ _orig_bar = _rs.Axes.bar
3276
+ _orig_barh = _rs.Axes.barh
3277
+
3278
+ def _bar(self, x, height, width=0.8, color=None, label=None, **kwargs):
3279
+ x = _to_list(x)
3280
+ tick_labels = None
3281
+ if isinstance(x, (list, tuple)) and any(isinstance(v, str) for v in x):
3282
+ tick_labels = [str(v) for v in x]
3283
+ x = list(range(len(x)))
3284
+ if isinstance(label, str):
3285
+ label = _render_mathtext(label)
3286
+ result = _orig_bar(self, x, height, width, color, label)
3287
+ if tick_labels is not None:
3288
+ self.set_xticks(list(range(len(tick_labels))), tick_labels)
3289
+ return result
3290
+
3291
+ def _barh(self, y, width, height=0.8, color=None, label=None, **kwargs):
3292
+ y = _to_list(y)
3293
+ tick_labels = None
3294
+ if isinstance(y, (list, tuple)) and any(isinstance(v, str) for v in y):
3295
+ tick_labels = [str(v) for v in y]
3296
+ y = list(range(len(y)))
3297
+ if isinstance(label, str):
3298
+ label = _render_mathtext(label)
3299
+ result = _orig_barh(self, y, width, height, color, label)
3300
+ if tick_labels is not None:
3301
+ self.set_yticks(list(range(len(tick_labels))), tick_labels)
3302
+ return result
3303
+
3304
+ _rs.Axes.bar = _bar
3305
+ _rs.Axes.barh = _barh
3306
+
3307
+ # legend: 支持 legend()、legend(loc=...)、legend(handles, labels)。
3308
+ # handles 为 Line2D 句柄时,从其 get_color/get_linestyle/... 取样式,labels 取文本,
3309
+ # 组装为显式图例条目(替换自动收集的条目)。
3310
+ _orig_legend = _rs.Axes.legend
3311
+
3312
+ def _legend(self, *args, **kwargs):
3313
+ loc = kwargs.pop('loc', 'best')
3314
+ handles = kwargs.pop('handles', None)
3315
+ labels = kwargs.pop('labels', None)
3316
+ facecolor, framealpha, edgecolor, fontsize = _legend_frame_kwargs(kwargs)
3317
+ if len(args) == 2:
3318
+ handles, labels = args[0], args[1]
3319
+ elif len(args) == 1:
3320
+ if isinstance(args[0], str):
3321
+ loc = args[0]
3322
+ else:
3323
+ labels = args[0]
3324
+ if not isinstance(loc, str):
3325
+ loc = 'best'
3326
+ if handles is not None and labels is not None:
3327
+ entries = []
3328
+ for h, lbl in zip(handles, _to_list(labels)):
3329
+ getc = getattr(h, 'get_color', None)
3330
+ color = getc() if getc is not None else None
3331
+ getls = getattr(h, 'get_linestyle', None)
3332
+ ls = getls() if getls is not None else None
3333
+ getlw = getattr(h, 'get_linewidth', None)
3334
+ lw = getlw() if getlw is not None else None
3335
+ getm = getattr(h, 'get_marker', None)
3336
+ marker = getm() if getm is not None else None
3337
+ color = color if isinstance(color, str) else 'C0'
3338
+ ls = ls if (isinstance(ls, str) and ls.strip()) else '-'
3339
+ lw = 1.5 if lw is None else float(lw)
3340
+ if not (isinstance(marker, str) and marker.strip() not in ('', 'none')):
3341
+ marker = None
3342
+ entries.append((_render_mathtext(str(lbl)), color, ls, marker, lw, 1.0))
3343
+ return self.set_legend_entries(
3344
+ entries, loc, facecolor, framealpha, edgecolor, fontsize)
3345
+ return _orig_legend(self, loc, facecolor, framealpha, edgecolor, fontsize)
3346
+
3347
+ _rs.Axes.legend = _legend
3348
+
3349
+ # set_xlim / set_ylim: 支持元组参数 (left, right)
3350
+ _orig_set_xlim = _rs.Axes.set_xlim
3351
+ _orig_set_ylim = _rs.Axes.set_ylim
3352
+
3353
+ def _set_xlim(self, *args, **kwargs):
3354
+ if len(args) == 1 and isinstance(args[0], (tuple, list)):
3355
+ lo, hi = args[0]
3356
+ return _orig_set_xlim(self, left=lo, right=hi)
3357
+ return _orig_set_xlim(self, *args, **kwargs)
3358
+
3359
+ def _set_ylim(self, *args, **kwargs):
3360
+ if len(args) == 1 and isinstance(args[0], (tuple, list)):
3361
+ lo, hi = args[0]
3362
+ return _orig_set_ylim(self, bottom=lo, top=hi)
3363
+ return _orig_set_ylim(self, *args, **kwargs)
3364
+
3365
+ _rs.Axes.set_xlim = _set_xlim
3366
+ _rs.Axes.set_ylim = _set_ylim
3367
+
3368
+ # set_xticks / set_yticks: 支持第二个位置参数 labels,且把数组对象归一为 list。
3369
+ _orig_set_xticks = _rs.Axes.set_xticks
3370
+ _orig_set_yticks = _rs.Axes.set_yticks
3371
+
3372
+ def _set_xticks(self, ticks=None, labels=None, **kwargs):
3373
+ ticks = _to_list(ticks) if ticks is not None else None
3374
+ labels = [str(x) for x in _to_list(labels)] if labels is not None else None
3375
+ return _orig_set_xticks(self, ticks, labels)
3376
+
3377
+ def _set_yticks(self, ticks=None, labels=None, **kwargs):
3378
+ ticks = _to_list(ticks) if ticks is not None else None
3379
+ labels = [str(x) for x in _to_list(labels)] if labels is not None else None
3380
+ return _orig_set_yticks(self, ticks, labels)
3381
+
3382
+ _rs.Axes.set_xticks = _set_xticks
3383
+ _rs.Axes.set_yticks = _set_yticks
3384
+
3385
+ # set_xticklabels / set_yticklabels: 归一为字符串 list,吸收 rotation/ha/fontsize 等
3386
+ # 未支持的样式 kwargs。须在 set_xticks/set_yticks 固定刻度位置后调用(matplotlib 语义)。
3387
+ _orig_set_xticklabels = _rs.Axes.set_xticklabels
3388
+ _orig_set_yticklabels = _rs.Axes.set_yticklabels
3389
+
3390
+ def _set_xticklabels(self, labels, **kwargs):
3391
+ return _orig_set_xticklabels(self, [str(x) for x in _to_list(labels)])
3392
+
3393
+ def _set_yticklabels(self, labels, **kwargs):
3394
+ return _orig_set_yticklabels(self, [str(x) for x in _to_list(labels)])
3395
+
3396
+ _rs.Axes.set_xticklabels = _set_xticklabels
3397
+ _rs.Axes.set_yticklabels = _set_yticklabels
3398
+
3399
+ # axis: 支持序列 [xmin,xmax,ymin,ymax] 设定轴限,及 'off'/'on'/'equal' 等字符串。
3400
+ _orig_axis = _rs.Axes.axis
3401
+
3402
+ def _axis(self, arg=None, **kwargs):
3403
+ if isinstance(arg, (list, tuple)):
3404
+ if len(arg) == 4:
3405
+ xmin, xmax, ymin, ymax = arg
3406
+ self.set_xlim(xmin, xmax)
3407
+ self.set_ylim(ymin, ymax)
3408
+ return None
3409
+ if arg is False:
3410
+ arg = 'off'
3411
+ elif arg is True:
3412
+ arg = 'on'
3413
+ if isinstance(arg, str):
3414
+ if arg in ('equal', 'scaled', 'image', 'square'):
3415
+ setter = getattr(self, 'set_aspect', None)
3416
+ if setter is not None:
3417
+ setter('equal')
3418
+ return None
3419
+ return _orig_axis(self, arg)
3420
+ return _orig_axis(self, None)
3421
+
3422
+ _rs.Axes.axis = _axis
3423
+
3424
+ # autoscale(enable=True, axis='both', tight=None): rsplotlib 默认按数据自动计算
3425
+ # 坐标范围,启用自动缩放即默认行为;关闭 (enable=False) 当前不支持冻结范围,按空操作处理。
3426
+ def _autoscale(self, enable=True, axis='both', tight=None):
3427
+ return None
3428
+
3429
+ _rs.Axes.autoscale = _autoscale
3430
+
3431
+ # 文本类方法:把 matplotlib mathtext ($...$) 转成 Unicode 后再下沉到 Rust。
3432
+ # OO API (ax.set_xlabel / ax.text ...) 不经过模块级 plt.* 函数,需在此单独接入。
3433
+ _orig_set_xlabel = _rs.Axes.set_xlabel
3434
+ _orig_set_ylabel = _rs.Axes.set_ylabel
3435
+ _orig_set_title = _rs.Axes.set_title
3436
+
3437
+ def _set_xlabel(self, text, *args, **kwargs):
3438
+ return _orig_set_xlabel(self, _render_mathtext(text), *args, **kwargs)
3439
+
3440
+ def _set_ylabel(self, text, *args, **kwargs):
3441
+ return _orig_set_ylabel(self, _render_mathtext(text), *args, **kwargs)
3442
+
3443
+ def _set_title(self, text, *args, **kwargs):
3444
+ return _orig_set_title(self, _render_mathtext(text), *args, **kwargs)
3445
+
3446
+ _rs.Axes.set_xlabel = _set_xlabel
3447
+ _rs.Axes.set_ylabel = _set_ylabel
3448
+ _rs.Axes.set_title = _set_title
3449
+
3450
+ _orig_text = _rs.Axes.text
3451
+
3452
+ def _text(self, x, y, s, fontsize=None, color=None, c=None, family=None, rotation=None, horizontalalignment=None, verticalalignment=None, transform=None, bbox=None, clip_on=None, alpha=None, weight=None, dx=None, dy=None, **kwargs):
3453
+ if not isinstance(s, str):
3454
+ s = str(s)
3455
+ if fontsize is None:
3456
+ fontsize = kwargs.get('size', None)
3457
+ if hasattr(x, 'item'):
3458
+ x = x.item()
3459
+ elif not isinstance(x, (int, float)):
3460
+ x = float(x)
3461
+ if hasattr(y, 'item'):
3462
+ y = y.item()
3463
+ elif not isinstance(y, (int, float)):
3464
+ y = float(y)
3465
+ if rotation is None:
3466
+ rotation = 0.0
3467
+ s_rendered = _render_mathtext(s)
3468
+ return _orig_text(self, x, y, s_rendered, fontsize, color, c, family, horizontalalignment, verticalalignment, rotation, dx, dy, bbox)
3469
+
3470
+ _rs.Axes.text = _text
3471
+
3472
+ _orig_annotate = _rs.Axes.annotate
3473
+
3474
+ def _annotate(self, text, xy, xytext=None, fontsize=None,
3475
+ color="black", arrowprops=None, xycoords='data',
3476
+ textcoords=None, ha=None, family=None, **kwargs):
3477
+ # 支持坐标系 (xycoords/textcoords)、水平对齐 (ha/horizontalalignment)、
3478
+ # 字体族 (family/fontfamily);其余未支持参数 (如 va) 被 **kwargs 吸收后丢弃。
3479
+ if isinstance(text, str):
3480
+ text = _render_mathtext(text)
3481
+ # 未显式指定 fontsize 时,默认字号随其余默认字号一并放大 DEFAULT_FONT_SCALE 倍;
3482
+ # 用户显式传入 fontsize 则保持原值不放大。
3483
+ if fontsize is None:
3484
+ fontsize = _DEFAULT_ANNOTATE_FONTSIZE
3485
+ if ha is None:
3486
+ ha = kwargs.get('horizontalalignment', 'center')
3487
+ if family is None:
3488
+ family = kwargs.get('fontfamily', None)
3489
+ # xycoords 可能是 get_xaxis_transform / get_yaxis_transform 返回的标记字符串;
3490
+ # 若传入的是其它非字符串的 transform 对象则回退到 'data'。
3491
+ if not isinstance(xycoords, str):
3492
+ xycoords = 'data'
3493
+ if textcoords is not None and not isinstance(textcoords, str):
3494
+ textcoords = None
3495
+ # 处理 xy 中的 0-d 数组对象
3496
+ if isinstance(xy, (list, tuple)) and len(xy) >= 2:
3497
+ def _to_float(v):
3498
+ if hasattr(v, 'ndim') and v.ndim == 0:
3499
+ return float(v.item()) if hasattr(v, 'item') else float(v)
3500
+ return float(v)
3501
+ xy = (_to_float(xy[0]), _to_float(xy[1]))
3502
+ return _orig_annotate(self, text, xy, xytext, fontsize, color,
3503
+ arrowprops, xycoords, textcoords, ha, family)
3504
+
3505
+ _rs.Axes.annotate = _annotate
3506
+
3507
+ class _Table:
3508
+ def __init__(self, ax, cell_text, col_widths, row_labels, col_labels, row_colors, loc):
3509
+ self.ax = ax
3510
+ self._cell_text = cell_text
3511
+ self._col_widths = col_widths
3512
+ self._row_labels = row_labels
3513
+ self._col_labels = col_labels
3514
+ self._row_colors = row_colors
3515
+ self._loc = loc
3516
+ self._fontsize = 10.0
3517
+
3518
+ def auto_set_font_size(self, auto):
3519
+ pass
3520
+
3521
+ def set_fontsize(self, size):
3522
+ self._fontsize = size
3523
+
3524
+ def scale(self, xscale, yscale):
3525
+ pass
3526
+
3527
+ def _table(self, cellText=None, colWidths=None, rowLabels=None, colLabels=None,
3528
+ rowColours=None, loc='bottom', **kwargs):
3529
+ cell_text = cellText or []
3530
+ col_widths = colWidths or []
3531
+ row_labels = rowLabels or []
3532
+ col_labels = colLabels or []
3533
+ row_colors = rowColours or []
3534
+
3535
+ table_obj = _Table(self, cell_text, col_widths, row_labels, col_labels, row_colors, loc)
3536
+ fontsize = kwargs.get('fontsize', 10.0)
3537
+ table_obj.set_fontsize(fontsize)
3538
+
3539
+ _orig_table(self, cell_text, col_widths, row_labels, col_labels, row_colors, loc)
3540
+
3541
+ return table_obj
3542
+
3543
+ _orig_table = _rs.Axes.table
3544
+ _rs.Axes.table = _table
3545
+
3546
+ # set(**kwargs): matplotlib 语义, 每个 key 映射到 set_<key>(value)
3547
+ def _ax_set(self, **kwargs):
3548
+ for key, value in kwargs.items():
3549
+ setter = getattr(self, 'set_' + key, None)
3550
+ if setter is None:
3551
+ continue
3552
+ # 数组对象转 list, 供 Rust 侧 Vec<f64> 提取; tuple 保留给 set_xlim 处理
3553
+ if hasattr(value, 'tolist'):
3554
+ value = value.tolist()
3555
+ setter(value)
3556
+ return None
3557
+
3558
+ _rs.Axes.set = _ax_set
3559
+
3560
+ class _Transform:
3561
+ def transform(self, coords):
3562
+ return coords
3563
+
3564
+ def inverted(self):
3565
+ return self
3566
+
3567
+ _rs.Axes.transData = _Transform()
3568
+
3569
+ def _to_color_string(color):
3570
+ """Convert a color value (string, list, tuple) to a CSS color string."""
3571
+ if isinstance(color, str):
3572
+ return color
3573
+ if isinstance(color, (list, tuple)):
3574
+ n = len(color)
3575
+ if n == 3:
3576
+ r = int(max(0, min(1, color[0])) * 255)
3577
+ g = int(max(0, min(1, color[1])) * 255)
3578
+ b = int(max(0, min(1, color[2])) * 255)
3579
+ return f'rgb({r},{g},{b})'
3580
+ if n == 4:
3581
+ r = int(max(0, min(1, color[0])) * 255)
3582
+ g = int(max(0, min(1, color[1])) * 255)
3583
+ b = int(max(0, min(1, color[2])) * 255)
3584
+ a = float(color[3])
3585
+ return f'rgba({r},{g},{b},{a})'
3586
+ return 'black'
3587
+
3588
+ def _add_collection(self, collection, autolim=True):
3589
+ from rsplotlib.collections import LineCollection
3590
+ if isinstance(collection, LineCollection):
3591
+ segments = collection.segments
3592
+ if segments is None:
3593
+ return
3594
+ color = _to_color_string(collection.colors) if collection.colors else 'black'
3595
+ linewidth = collection.linewidths if collection.linewidths else 1.0
3596
+ linestyle = collection.linestyle if collection.linestyle else '-'
3597
+ alpha = collection.alpha if collection.alpha else None
3598
+
3599
+ if hasattr(segments, 'tolist'):
3600
+ segments = segments.tolist()
3601
+
3602
+ for i, seg in enumerate(segments):
3603
+ if len(seg) >= 2:
3604
+ x = [float(p[0]) for p in seg]
3605
+ y = [float(p[1]) for p in seg]
3606
+ try:
3607
+ self.plot(x, y, color=color, linewidth=linewidth, linestyle=linestyle, alpha=alpha)
3608
+ except Exception:
3609
+ pass
3610
+
3611
+ _rs.Axes.add_collection = _add_collection
3612
+
3613
+ def _update_datalim(self, xydata, update_datalim=True):
3614
+ pass
3615
+
3616
+ _rs.Axes.update_datalim = _update_datalim
3617
+
3618
+ def _autoscale_view(self, tight=None, scalex=True, scaley=True):
3619
+ pass
3620
+ _rs.Axes.autoscale_view = _autoscale_view
3621
+
3622
+ def _add_artist(self, artist):
3623
+ # Edge label text rendering from CurvedArrowTextBase
3624
+ is_edge_label = hasattr(artist, '_update_text_pos_angle') and hasattr(artist, 'arrow')
3625
+ if is_edge_label:
3626
+ text_str = artist.text if hasattr(artist, 'text') else ''
3627
+ family = getattr(artist, 'family', None)
3628
+ fontsize = artist.fontsize
3629
+ color = artist.color
3630
+ ha = artist.horizontalalignment
3631
+ va = artist.verticalalignment
3632
+ x, y = artist.x, artist.y
3633
+
3634
+ lines = text_str.split('\n')
3635
+ if len(lines) <= 1:
3636
+ self.text(x, y, text_str, fontsize=fontsize, color=color, rotation=0,
3637
+ family=family, horizontalalignment=ha, verticalalignment=va,
3638
+ bbox=dict(facecolor='white', edgecolor='none', pad=1, alpha=1.0))
3639
+ return
3640
+
3641
+ # 多行文本:全部水平显示(rotation=0),上下排列
3642
+ # 编号在上,阻抗在下
3643
+ line_height = fontsize * 0.002
3644
+
3645
+ n = len(lines)
3646
+
3647
+ # 绘制文字(使用 bbox 参数添加白色背景)
3648
+ for i, line in enumerate(lines):
3649
+ # idx 越小的行越靠上(y_offset为正 = 向上偏移,因为数据坐标y越大越靠上)
3650
+ y_offset = - (i - (n - 1) / 2.0) * line_height
3651
+ self.text(x, y + y_offset, line, fontsize=fontsize, color=color, rotation=0,
3652
+ family=family, horizontalalignment='center', verticalalignment='center',
3653
+ bbox=dict(facecolor='white', edgecolor='none', pad=0.5, alpha=1.0))
3654
+ _rs.Axes.add_artist = _add_artist
3655
+
3656
+ def _add_patch(self, patch):
3657
+ from rsplotlib.patches import FancyArrowPatch, _Path
3658
+ if isinstance(patch, FancyArrowPatch):
3659
+ posA = patch.posA
3660
+ posB = patch.posB
3661
+ color = _to_color_string(patch.color) if patch.color else 'black'
3662
+ linewidth = patch.linewidth if patch.linewidth else 1.0
3663
+
3664
+ # Apply shrinkA/shrinkB to adjust endpoints
3665
+ shrinkA = patch.shrinkA if patch.shrinkA else 0.0
3666
+ shrinkB = patch.shrinkB if patch.shrinkB else 0.0
3667
+
3668
+ x1, y1 = posA
3669
+ x2, y2 = posB
3670
+
3671
+ # Calculate direction vector
3672
+ dx = x2 - x1
3673
+ dy = y2 - y1
3674
+ length = math.sqrt(dx*dx + dy*dy)
3675
+
3676
+ # 标记是否需要在标签位置断开线
3677
+ need_break = False
3678
+ break_x1, break_y1 = x1, y1
3679
+ break_x2, break_y2 = x2, y2
3680
+
3681
+ if length > 0:
3682
+ # Apply shrink
3683
+ if shrinkA > 0 or shrinkB > 0:
3684
+ ux, uy = dx / length, dy / length
3685
+ x1 += ux * shrinkA
3686
+ y1 += uy * shrinkA
3687
+ x2 -= ux * shrinkB
3688
+ y2 -= uy * shrinkB
3689
+
3690
+ # 如果边有标签,在标签位置断开线
3691
+ # 标签位于边的中间位置,留出约 15% 的边长度作为标签区域
3692
+ if hasattr(patch, 'arrow') or hasattr(patch, '_update_text_pos_angle'):
3693
+ need_break = True
3694
+ label_region = length * 0.15
3695
+ ux, uy = dx / length, dy / length
3696
+ # 第一段终点 = 起点 + (总长度 - 标签区域) / 2
3697
+ break_x1 = x1 + ux * (length - label_region) / 2
3698
+ break_y1 = y1 + uy * (length - label_region) / 2
3699
+ # 第二段起点 = 终点 - (总长度 - 标签区域) / 2
3700
+ break_x2 = x2 - ux * (length - label_region) / 2
3701
+ break_y2 = y2 - uy * (length - label_region) / 2
3702
+
3703
+ # Use connectionstyle to get the curve path
3704
+ conn = patch.get_connectionstyle()
3705
+ if conn is not None:
3706
+ path = conn((x1, y1), (x2, y2))
3707
+ if isinstance(path, _Path) and len(path.vertices) >= 3:
3708
+ if need_break:
3709
+ # 断开绘制:第一段从起点到标签区域起点
3710
+ path1 = conn((x1, y1), (break_x1, break_y1))
3711
+ if isinstance(path1, _Path) and len(path1.vertices) >= 3:
3712
+ vertices = path1.vertices
3713
+ xs, ys = [], []
3714
+ for i in range(21):
3715
+ t = i / 20
3716
+ tt = 1 - t
3717
+ px = tt*tt * vertices[0][0] + 2*tt*t * vertices[1][0] + t*t * vertices[2][0]
3718
+ py = tt*tt * vertices[0][1] + 2*tt*t * vertices[1][1] + t*t * vertices[2][1]
3719
+ xs.append(px)
3720
+ ys.append(py)
3721
+ self.plot(xs, ys, color=color, linewidth=linewidth)
3722
+ else:
3723
+ self.plot([x1, break_x1], [y1, break_y1], color=color, linewidth=linewidth)
3724
+ # 第二段从标签区域终点到终点
3725
+ path2 = conn((break_x2, break_y2), (x2, y2))
3726
+ if isinstance(path2, _Path) and len(path2.vertices) >= 3:
3727
+ vertices = path2.vertices
3728
+ xs, ys = [], []
3729
+ for i in range(21):
3730
+ t = i / 20
3731
+ tt = 1 - t
3732
+ px = tt*tt * vertices[0][0] + 2*tt*t * vertices[1][0] + t*t * vertices[2][0]
3733
+ py = tt*tt * vertices[0][1] + 2*tt*t * vertices[1][1] + t*t * vertices[2][1]
3734
+ xs.append(px)
3735
+ ys.append(py)
3736
+ self.plot(xs, ys, color=color, linewidth=linewidth)
3737
+ else:
3738
+ self.plot([break_x2, x2], [break_y2, y2], color=color, linewidth=linewidth)
3739
+ else:
3740
+ vertices = path.vertices
3741
+ xs, ys = [], []
3742
+ for i in range(21):
3743
+ t = i / 20
3744
+ tt = 1 - t
3745
+ px = tt*tt * vertices[0][0] + 2*tt*t * vertices[1][0] + t*t * vertices[2][0]
3746
+ py = tt*tt * vertices[0][1] + 2*tt*t * vertices[1][1] + t*t * vertices[2][1]
3747
+ xs.append(px)
3748
+ ys.append(py)
3749
+ self.plot(xs, ys, color=color, linewidth=linewidth)
3750
+ else:
3751
+ if need_break:
3752
+ self.plot([x1, break_x1], [y1, break_y1], color=color, linewidth=linewidth)
3753
+ self.plot([break_x2, x2], [break_y2, y2], color=color, linewidth=linewidth)
3754
+ else:
3755
+ self.plot([x1, x2], [y1, y2], color=color, linewidth=linewidth)
3756
+ else:
3757
+ self.plot([x1, x2], [y1, y2], color=color, linewidth=linewidth)
3758
+
3759
+ arrowstyle = patch.arrowstyle
3760
+ if arrowstyle and arrowstyle != '-':
3761
+ self.annotate('', xy=(x2, y2), xytext=(x1, y1), arrowprops=dict(color=color, arrowstyle=arrowstyle, linewidth=linewidth))
3762
+
3763
+ _rs.Axes.add_patch = _add_patch
3764
+
3765
+
3766
+ _patch_figure_add_subplot()
3767
+ _patch_axes()
3768
+ _patch_axes_get_gridspec()
3769
+ _patch_axes_remove()
3770
+
3771
+
3772
+ style = _style_module.style