rsplotlib 0.2.6__tar.gz → 0.2.8__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. {rsplotlib-0.2.6 → rsplotlib-0.2.8}/.gitignore +2 -1
  2. {rsplotlib-0.2.6 → rsplotlib-0.2.8}/Cargo.lock +5 -3
  3. {rsplotlib-0.2.6 → rsplotlib-0.2.8}/Cargo.toml +5 -1
  4. {rsplotlib-0.2.6 → rsplotlib-0.2.8}/PKG-INFO +1 -1
  5. {rsplotlib-0.2.6 → rsplotlib-0.2.8}/pyproject.toml +1 -1
  6. {rsplotlib-0.2.6 → rsplotlib-0.2.8}/python/rsplotlib/__init__.py +1 -1
  7. {rsplotlib-0.2.6 → rsplotlib-0.2.8}/python/rsplotlib/pyplot.py +166 -44
  8. {rsplotlib-0.2.6 → rsplotlib-0.2.8}/src/core/elements.rs +10 -0
  9. {rsplotlib-0.2.6 → rsplotlib-0.2.8}/src/figure/axes.rs +389 -53
  10. {rsplotlib-0.2.6 → rsplotlib-0.2.8}/src/figure/axes_bounds.rs +54 -25
  11. {rsplotlib-0.2.6 → rsplotlib-0.2.8}/src/figure/axes_render_elements.rs +532 -82
  12. {rsplotlib-0.2.6 → rsplotlib-0.2.8}/src/figure/axis.rs +76 -6
  13. {rsplotlib-0.2.6 → rsplotlib-0.2.8}/src/figure/figure.rs +50 -30
  14. rsplotlib-0.2.8/src/fonts/DejaVuSans.ttf +0 -0
  15. {rsplotlib-0.2.6 → rsplotlib-0.2.8}/src/lib.rs +112 -0
  16. {rsplotlib-0.2.6 → rsplotlib-0.2.8}/src/utils/font_stack.rs +51 -5
  17. rsplotlib-0.2.8/src/utils/glyph_cache.rs +229 -0
  18. {rsplotlib-0.2.6 → rsplotlib-0.2.8}/src/utils/mod.rs +2 -0
  19. {rsplotlib-0.2.6 → rsplotlib-0.2.8}/src/utils/pyfuncs.rs +45 -3
  20. rsplotlib-0.2.8/src/utils/rgb_backend.rs +232 -0
  21. {rsplotlib-0.2.6 → rsplotlib-0.2.8}/.github/workflows/ci.yml +0 -0
  22. {rsplotlib-0.2.6 → rsplotlib-0.2.8}/.github/workflows/release.yml +0 -0
  23. {rsplotlib-0.2.6 → rsplotlib-0.2.8}/.github/workflows/rust.yml +0 -0
  24. {rsplotlib-0.2.6 → rsplotlib-0.2.8}/LICENSE +0 -0
  25. {rsplotlib-0.2.6 → rsplotlib-0.2.8}/README.md +0 -0
  26. {rsplotlib-0.2.6 → rsplotlib-0.2.8}/README_zh.md +0 -0
  27. {rsplotlib-0.2.6 → rsplotlib-0.2.8}/build_wheel.sh +0 -0
  28. {rsplotlib-0.2.6 → rsplotlib-0.2.8}/ci/smoke_test.py +0 -0
  29. {rsplotlib-0.2.6 → rsplotlib-0.2.8}/python/rsplotlib/_font_resolver.py +0 -0
  30. {rsplotlib-0.2.6 → rsplotlib-0.2.8}/python/rsplotlib/colors.py +0 -0
  31. {rsplotlib-0.2.6 → rsplotlib-0.2.8}/python/rsplotlib/core/__init__.py +0 -0
  32. {rsplotlib-0.2.6 → rsplotlib-0.2.8}/python/rsplotlib/core/api.py +0 -0
  33. {rsplotlib-0.2.6 → rsplotlib-0.2.8}/python/rsplotlib/dates.py +0 -0
  34. {rsplotlib-0.2.6 → rsplotlib-0.2.8}/python/rsplotlib/figure/__init__.py +0 -0
  35. {rsplotlib-0.2.6 → rsplotlib-0.2.8}/python/rsplotlib/figure/_defaults.py +0 -0
  36. {rsplotlib-0.2.6 → rsplotlib-0.2.8}/python/rsplotlib/gridspec.py +0 -0
  37. {rsplotlib-0.2.6 → rsplotlib-0.2.8}/python/rsplotlib/layout/__init__.py +0 -0
  38. {rsplotlib-0.2.6 → rsplotlib-0.2.8}/python/rsplotlib/layout/gridspec.py +0 -0
  39. {rsplotlib-0.2.6 → rsplotlib-0.2.8}/python/rsplotlib/pylab.py +0 -0
  40. {rsplotlib-0.2.6 → rsplotlib-0.2.8}/python/rsplotlib/ticker.py +0 -0
  41. {rsplotlib-0.2.6 → rsplotlib-0.2.8}/python/rsplotlib/ticks/__init__.py +0 -0
  42. {rsplotlib-0.2.6 → rsplotlib-0.2.8}/python/rsplotlib/ticks/ticker.py +0 -0
  43. {rsplotlib-0.2.6 → rsplotlib-0.2.8}/python/rsplotlib/utils/__init__.py +0 -0
  44. {rsplotlib-0.2.6 → rsplotlib-0.2.8}/python/rsplotlib/utils/_font_resolver.py +0 -0
  45. {rsplotlib-0.2.6 → rsplotlib-0.2.8}/python/rsplotlib/utils/_rcparams.py +0 -0
  46. {rsplotlib-0.2.6 → rsplotlib-0.2.8}/python/rsplotlib/utils/style.py +0 -0
  47. {rsplotlib-0.2.6 → rsplotlib-0.2.8}/rust-toolchain.toml +0 -0
  48. {rsplotlib-0.2.6 → rsplotlib-0.2.8}/src/core/colormap.rs +0 -0
  49. {rsplotlib-0.2.6 → rsplotlib-0.2.8}/src/core/colors.rs +0 -0
  50. {rsplotlib-0.2.6 → rsplotlib-0.2.8}/src/core/marker.rs +0 -0
  51. {rsplotlib-0.2.6 → rsplotlib-0.2.8}/src/core/mod.rs +0 -0
  52. {rsplotlib-0.2.6 → rsplotlib-0.2.8}/src/figure/axes_colorbar.rs +0 -0
  53. {rsplotlib-0.2.6 → rsplotlib-0.2.8}/src/figure/axes_grid.rs +0 -0
  54. {rsplotlib-0.2.6 → rsplotlib-0.2.8}/src/figure/axes_legend.rs +0 -0
  55. {rsplotlib-0.2.6 → rsplotlib-0.2.8}/src/figure/axes_mesh.rs +0 -0
  56. {rsplotlib-0.2.6 → rsplotlib-0.2.8}/src/figure/axes_secondary.rs +0 -0
  57. {rsplotlib-0.2.6 → rsplotlib-0.2.8}/src/figure/axes_title.rs +0 -0
  58. {rsplotlib-0.2.6 → rsplotlib-0.2.8}/src/figure/mod.rs +0 -0
  59. {rsplotlib-0.2.6 → rsplotlib-0.2.8}/src/layout/gridspec.rs +0 -0
  60. {rsplotlib-0.2.6 → rsplotlib-0.2.8}/src/layout/mod.rs +0 -0
  61. {rsplotlib-0.2.6 → rsplotlib-0.2.8}/src/ticks/mod.rs +0 -0
  62. {rsplotlib-0.2.6 → rsplotlib-0.2.8}/src/ticks/ticker.rs +0 -0
  63. {rsplotlib-0.2.6 → rsplotlib-0.2.8}/src/utils/font_resolver.rs +0 -0
  64. {rsplotlib-0.2.6 → rsplotlib-0.2.8}/src/utils/mathtext.rs +0 -0
  65. {rsplotlib-0.2.6 → rsplotlib-0.2.8}/src/utils/style.rs +0 -0
  66. {rsplotlib-0.2.6 → rsplotlib-0.2.8}/tutorial.css +0 -0
  67. {rsplotlib-0.2.6 → rsplotlib-0.2.8}/tutorial.html +0 -0
  68. {rsplotlib-0.2.6 → rsplotlib-0.2.8}/tutorial.js +0 -0
@@ -32,7 +32,8 @@ test/
32
32
  test*.py
33
33
  *.png
34
34
  main*.py
35
- debug.txt
35
+ .trae/
36
+ debug.md
36
37
  out/
37
38
  wheelhouse/
38
39
  N237S NB.csv
@@ -50,9 +50,9 @@ checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649"
50
50
 
51
51
  [[package]]
52
52
  name = "bytemuck"
53
- version = "1.25.0"
53
+ version = "1.25.1"
54
54
  source = "registry+https://github.com/rust-lang/crates.io-index"
55
- checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec"
55
+ checksum = "d6aedf8ae72766347502cf3cb4f41cf5e9cc37d28bee90f1fdaaae15f9cf9424"
56
56
 
57
57
  [[package]]
58
58
  name = "byteorder"
@@ -417,12 +417,14 @@ dependencies = [
417
417
 
418
418
  [[package]]
419
419
  name = "rsplotlib"
420
- version = "0.2.6"
420
+ version = "0.2.8"
421
421
  dependencies = [
422
+ "ab_glyph",
422
423
  "jpeg-decoder",
423
424
  "jpeg-encoder",
424
425
  "owned_ttf_parser",
425
426
  "plotters",
427
+ "plotters-backend",
426
428
  "png 0.18.1",
427
429
  "pyo3",
428
430
  ]
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "rsplotlib"
3
- version = "0.2.6"
3
+ version = "0.2.8"
4
4
  edition = "2024"
5
5
  readme = "README.md"
6
6
 
@@ -15,6 +15,10 @@ png = "0.18.1"
15
15
  owned_ttf_parser = "0.25.1"
16
16
  jpeg-encoder = "0.7.0"
17
17
  jpeg-decoder = "0.3.2"
18
+ # 与 plotters 0.3.7 相同版本的 ab_glyph / plotters-backend(均已在 Cargo.lock 锁定,
19
+ # 不引入新下载):glyph 缓存需直接引用 ab_glyph 光栅化器与后端 trait 类型。
20
+ ab_glyph = "0.2.32"
21
+ plotters-backend = "0.3.7"
18
22
 
19
23
  [profile.release]
20
24
  lto = "fat"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rsplotlib
3
- Version: 0.2.6
3
+ Version: 0.2.8
4
4
  Classifier: Programming Language :: Rust
5
5
  Classifier: Programming Language :: Python :: 3
6
6
  Classifier: Programming Language :: Python :: 3 :: Only
@@ -4,7 +4,7 @@ build-backend = "maturin"
4
4
 
5
5
  [project]
6
6
  name = "rsplotlib"
7
- version = "0.2.6"
7
+ version = "0.2.8"
8
8
  description = "A high-performance matplotlib-compatible plotting library powered by Rust"
9
9
  readme = "README_zh.md"
10
10
  license = { text = "MIT" }
@@ -13,7 +13,7 @@ MaxNLocator = ticker.MaxNLocator
13
13
  MultipleLocator = ticker.MultipleLocator
14
14
  AutoMinorLocator = ticker.AutoMinorLocator
15
15
 
16
- __version__ = "0.2.6"
16
+ __version__ = "0.2.8"
17
17
  # 从内部 Rust 模块导出字体注册函数
18
18
 
19
19
  __all__ = list(_api_all) + [
@@ -49,20 +49,44 @@ def _to_list_recursive(obj):
49
49
  return obj
50
50
 
51
51
 
52
+ def _buffer_kind(obj):
53
+ """返回 obj 的 dtype kind 字符(numpy 约定 'f'/'i'/'u'/'b'/'M'/'S'/'U'/'c'…),
54
+ 无法判定时返回 None。
55
+
56
+ 优先读廉价的 obj.dtype.kind:rsnumpy 的 __array_interface__ 属性每次访问都会即时把
57
+ 整个缓冲区序列化成 bytes(百万点约 2.6ms/次,随后被丢弃),而 dtype 访问是 O(1)。
58
+ 仅当对象没有 dtype(如 Python list、标量、或 numpy 之外仅实现数组接口的第三方缓冲)
59
+ 时,才回退读取 __array_interface__ 的 typestr。真实 numpy 的 __array_interface__ 为
60
+ 指针形式、开销极小,不受此影响。
61
+ """
62
+ dt = None
63
+ try:
64
+ dt = getattr(obj, 'dtype', None)
65
+ except Exception:
66
+ # rsnumpy 对 datetime64[h]/timedelta64 等 dtype 的 .dtype 属性会抛 TypeError
67
+ # (非 AttributeError,getattr 默认值挡不住)。此处按"无法判定"处理,落到
68
+ # __array_interface__ 回退——这些非数值 dtype 本就应返回非 fiub kind。
69
+ dt = None
70
+ if dt is not None:
71
+ kind = getattr(dt, 'kind', None)
72
+ if isinstance(kind, str) and len(kind) == 1:
73
+ return kind
74
+ ai = getattr(obj, '__array_interface__', None)
75
+ if isinstance(ai, dict):
76
+ typestr = ai.get('typestr', '')
77
+ return typestr[1:2] if len(typestr) >= 2 else None
78
+ return None
79
+
80
+
52
81
  def _is_numeric_buffer(obj):
53
- """obj 是否为暴露 __array_interface__ 的纯数值缓冲数组(float/int/uint/bool)。
82
+ """obj 是否为纯数值缓冲数组(float/int/uint/bool)。
54
83
 
55
84
  这类数组可零拷贝下沉给 Rust,且不可能是字符串类别或日期,故可跳过 .tolist()
56
- 全量物化(plot/scatter 热路径最大收益)。typestr 形如 '<f8'/'|b1'/'<M8[h]'/'<U3',
57
- 第 2 个字符为 kind:datetime64('M')、字符串('U'/'S')、复数('c')、timedelta('m')、
58
- object('O') 均需 Python 侧特殊处理,返回 False 交回原有 .tolist() 路径。
85
+ 全量物化(plot/scatter 热路径最大收益)。dtype kind 为 datetime64('M')、字符串
86
+ ('U'/'S')、复数('c')、timedelta('m')、object('O') 均需 Python 侧特殊处理,返回
87
+ False 交回原有 .tolist() 路径。
59
88
  """
60
- ai = getattr(obj, '__array_interface__', None)
61
- if not isinstance(ai, dict):
62
- return False
63
- typestr = ai.get('typestr', '')
64
- kind = typestr[1:2] if len(typestr) >= 2 else ''
65
- return kind in ('f', 'i', 'u', 'b')
89
+ return _buffer_kind(obj) in ('f', 'i', 'u', 'b')
66
90
 
67
91
 
68
92
  def _numeric_buffer_1d_len(obj):
@@ -71,14 +95,13 @@ def _numeric_buffer_1d_len(obj):
71
95
  用于 scatter 数值 c 的快路径:一维数值缓冲不可能是颜色字符串或 RGB(A) 行数组,
72
96
  可零拷贝直传 Rust 经 colormap 上色,跳过 .tolist() / 逐元素类别检查 / [float(v)] 物化。
73
97
  """
74
- ai = getattr(obj, '__array_interface__', None)
75
- if not isinstance(ai, dict):
76
- return None
77
- typestr = ai.get('typestr', '')
78
- kind = typestr[1:2] if len(typestr) >= 2 else ''
79
- if kind not in ('f', 'i', 'u', 'b'):
98
+ if _buffer_kind(obj) not in ('f', 'i', 'u', 'b'):
80
99
  return None
81
- shape = ai.get('shape')
100
+ shape = getattr(obj, 'shape', None)
101
+ if shape is None:
102
+ ai = getattr(obj, '__array_interface__', None)
103
+ if isinstance(ai, dict):
104
+ shape = ai.get('shape')
82
105
  if not (isinstance(shape, tuple) and len(shape) == 1):
83
106
  return None
84
107
  return shape[0]
@@ -421,6 +444,7 @@ def _map_aliases(kwargs):
421
444
  kwargs.pop(alias)
422
445
  # 规范化 linestyle 词形 ('solid'/'dotted'/'dashed'/'dashdot') 与空值到简写,
423
446
  # 与 matplotlib 一致:既可写 linestyle='dotted' 也可写 linestyle=':'。
447
+ # 元组形式 (offset, onoffseq)(参数化 dash 图案)编码为 "dashes=..." 串下沉到 Rust。
424
448
  ls = kwargs.get('linestyle')
425
449
  if isinstance(ls, str):
426
450
  key = ls.strip().lower()
@@ -429,6 +453,35 @@ def _map_aliases(kwargs):
429
453
  elif key in _LINESTYLE_ALIASES:
430
454
  kwargs['linestyle'] = _LINESTYLE_ALIASES[key]
431
455
  # 已是简写 ('-' / '--' / ':' / '-.') 时保持不变
456
+ elif isinstance(ls, (tuple, list)):
457
+ enc = _encode_dash_linestyle(ls)
458
+ if enc is not None:
459
+ kwargs['linestyle'] = enc
460
+
461
+
462
+ def _encode_dash_linestyle(ls):
463
+ """matplotlib 元组 linestyle ``(offset, onoffseq)`` -> ``"dashes=<offset>;<v0>,<v1>,..."`` 编码串。
464
+
465
+ - 空 / None 的 onoffseq 视为实线 ('-')。
466
+ - 结构非法时返回 None(交由下游忽略, 保持原值)。
467
+ """
468
+ if len(ls) != 2:
469
+ return None
470
+ offset, seq = ls
471
+ if seq is None:
472
+ return '-'
473
+ try:
474
+ seq = list(seq)
475
+ except TypeError:
476
+ return None
477
+ if len(seq) == 0:
478
+ return '-'
479
+ if offset is None:
480
+ offset = 0
481
+ try:
482
+ return "dashes=%g;%s" % (float(offset), ",".join("%g" % float(v) for v in seq))
483
+ except (TypeError, ValueError):
484
+ return None
432
485
 
433
486
 
434
487
  # linestyle 词形 -> 简写。空串 / 'None' 在 _map_aliases 中单独处理为 ' '(不画线)。
@@ -942,6 +995,28 @@ def _parse_fmt(fmt):
942
995
  return result
943
996
 
944
997
 
998
+ def _implicit_x(y):
999
+ """plot(y) 的隐式横坐标 = [0, 1, ..., len(y)-1]。
1000
+
1001
+ 优先用 rsnumpy.arange(n, dtype=float) 生成 float64 数组:可经 PEP 3118 缓冲协议
1002
+ 零拷贝下沉 Rust,并让 _categorical/_maybe_dates_to_num/_to_seq 走数值缓冲快路径,
1003
+ 跳过对 list(range(n)) 的逐元素扫描与百万级 Python int 提取(大数据热路径)。
1004
+ 值 0.0..n-1 与 range 完全一致(n ≤ 2^53 精确),渲染输出逐字节不变。
1005
+
1006
+ rsnumpy 不可用(ImportError)或 arange 异常时回退到 list(range(n));y 无 len 时
1007
+ 沿用原逻辑(可迭代取 list(y),否则空)。
1008
+ """
1009
+ try:
1010
+ n = len(y)
1011
+ except Exception:
1012
+ return list(y) if hasattr(y, '__iter__') else []
1013
+ try:
1014
+ import rsnumpy as _rsnp
1015
+ return _rsnp.arange(n, dtype=float)
1016
+ except Exception:
1017
+ return list(range(n))
1018
+
1019
+
945
1020
  def _parse_plot_args(args, kwargs):
946
1021
  """解析 plot() 的位置参数为 [(x, y, fmt_or_none), ...] 对列表 + kwargs
947
1022
 
@@ -965,10 +1040,7 @@ def _parse_plot_args(args, kwargs):
965
1040
 
966
1041
  if len(group) == 1:
967
1042
  y = group[0]
968
- try:
969
- x = list(range(len(y)))
970
- except Exception:
971
- x = list(y) if hasattr(y, '__iter__') else []
1043
+ x = _implicit_x(y)
972
1044
  pairs.append((x, y, fmt))
973
1045
  else:
974
1046
  pairs.append((group[0], group[1], fmt))
@@ -1193,21 +1265,29 @@ def hist(x, bins=None, range=None, density=False, weights=None,
1193
1265
  if bins is None:
1194
1266
  bins = 10
1195
1267
 
1196
- # 数据规整为“组的列表”
1197
- x = _to_list_recursive(x)
1198
- if x and isinstance(x[0], (list, tuple)):
1199
- x_list = [list(v) for v in x]
1268
+ # 数据规整为“组的列表”。一维纯数值缓冲直接下沉给 Rust 零拷贝读取,避免
1269
+ # _to_list_recursive + [list(x)] 把百万级数据点物化成 Python 对象(hist 大数据热路径)。
1270
+ if _numeric_buffer_1d_len(x) is not None:
1271
+ x_list = x
1272
+ n_datasets = 1
1200
1273
  else:
1201
- x_list = [list(x)]
1202
- n_datasets = len(x_list)
1274
+ x = _to_list_recursive(x)
1275
+ if x and isinstance(x[0], (list, tuple)):
1276
+ x_list = [list(v) for v in x]
1277
+ else:
1278
+ x_list = [list(x)]
1279
+ n_datasets = len(x_list)
1203
1280
 
1204
- # weights 规整为与 x 平行的结构
1281
+ # weights 规整为与 x 平行的结构(一维数值缓冲同样直传,Rust 侧零拷贝读取)
1205
1282
  if weights is not None:
1206
- w = _to_list_recursive(weights)
1207
- if w and isinstance(w[0], (list, tuple)):
1208
- weights_arg = [list(v) for v in w]
1283
+ if _numeric_buffer_1d_len(weights) is not None:
1284
+ weights_arg = weights
1209
1285
  else:
1210
- weights_arg = [list(w)]
1286
+ w = _to_list_recursive(weights)
1287
+ if w and isinstance(w[0], (list, tuple)):
1288
+ weights_arg = [list(v) for v in w]
1289
+ else:
1290
+ weights_arg = [list(w)]
1211
1291
  else:
1212
1292
  weights_arg = None
1213
1293
 
@@ -1293,7 +1373,11 @@ def boxplot(x, labels=None, vert=True, **kwargs):
1293
1373
  labels: 每个箱的标签列表
1294
1374
  vert: 是否垂直绘制 (默认 True)
1295
1375
  """
1296
- x = _to_list_recursive(x)
1376
+ # 纯数值缓冲数组直接下沉给 Rust 零拷贝读取(py_to_vec_vec_f64:一维→单箱,
1377
+ # 二维→按行拆多箱,与旧 _to_list_recursive 语义一致),避免物化百万级数据点。
1378
+ # Python list(含 list of arrays)、含字符串等非缓冲对象保持原路径。
1379
+ if not _is_numeric_buffer(x):
1380
+ x = _to_list_recursive(x)
1297
1381
  return _route_to_ax('boxplot', _rsplotlib.boxplot, x, labels, vert)
1298
1382
 
1299
1383
 
@@ -1385,10 +1469,10 @@ def stackplot(x, *args, labels=None, colors=None, alpha=1.0, **kwargs):
1385
1469
  colors: 每个数据集的颜色列表
1386
1470
  alpha: 透明度 (默认 1.0)
1387
1471
  """
1388
- x = _to_list(x)
1389
- y_data = [list(a) for a in args if a is not None]
1472
+ x = _to_seq(x)
1473
+ y_data = [_to_seq(a) for a in args if a is not None]
1390
1474
  if not y_data and 'y' in kwargs:
1391
- y_data = [_to_list(kwargs['y'])]
1475
+ y_data = [_to_seq(kwargs['y'])]
1392
1476
  return _route_to_ax('stackplot', _rsplotlib.stackplot, x, *y_data,
1393
1477
  labels=labels, colors=colors, alpha=alpha)
1394
1478
 
@@ -1632,15 +1716,15 @@ def annotate(text, xy, xytext=None, fontsize=None, color='black', arrowprops=Non
1632
1716
  xytext 绘制箭头指向 xy。支持简单箭头 (width/headwidth/headlength/
1633
1717
  shrink) 与花式箭头 (arrowstyle/connectionstyle/mutation_scale/
1634
1718
  shrinkA/shrinkB 等)。
1635
- **kwargs: 其他关键字参数 (如 xycoords/textcoords, 当前忽略)
1719
+ **kwargs: 其他关键字参数 (如 xycoords/textcoords/ha/family),转发给 Axes.annotate
1636
1720
  """
1637
1721
  text = _render_mathtext(text)
1638
1722
  ax = _get_axes()
1639
1723
  if ax is not None and hasattr(ax, 'annotate'):
1640
- ax.annotate(text, xy, xytext, fontsize, color, arrowprops)
1724
+ ax.annotate(text, xy, xytext, fontsize, color, arrowprops, **kwargs)
1641
1725
  return _get_figure()
1642
1726
  fig, ax = _rsplotlib.subplots()
1643
- ax.annotate(text, xy, xytext, fontsize, color, arrowprops)
1727
+ ax.annotate(text, xy, xytext, fontsize, color, arrowprops, **kwargs)
1644
1728
  return fig, ax
1645
1729
 
1646
1730
 
@@ -1989,12 +2073,23 @@ def subplots(nrows=1, ncols=1, figsize=None, dpi=None, squeeze=True, **kwargs):
1989
2073
  - 1xN 或 Nx1: (Figure, 一维 ndarray[Axes])
1990
2074
  - MxN: (Figure, 二维 ndarray[Axes]), 支持 axs[i, j] 索引
1991
2075
  """
1992
- result = _rsplotlib.subplots(nrows, ncols, figsize, dpi)
2076
+ # width_ratios/height_ratios 可作为 subplots 的直接关键字,或经 gridspec_kw 传入
2077
+ # (与 matplotlib 一致,直接关键字优先)。传给 Rust 侧 subplots 以按比例分配行/列尺寸。
2078
+ gridspec_kw = kwargs.get('gridspec_kw') or {}
2079
+ width_ratios = kwargs.get('width_ratios')
2080
+ if width_ratios is None:
2081
+ width_ratios = gridspec_kw.get('width_ratios')
2082
+ height_ratios = kwargs.get('height_ratios')
2083
+ if height_ratios is None:
2084
+ height_ratios = gridspec_kw.get('height_ratios')
2085
+ width_ratios = _to_list(width_ratios) if width_ratios is not None else None
2086
+ height_ratios = _to_list(height_ratios) if height_ratios is not None else None
2087
+
2088
+ result = _rsplotlib.subplots(nrows, ncols, figsize, dpi, width_ratios, height_ratios)
1993
2089
  fig = result[0]
1994
2090
 
1995
2091
  # gridspec_kw={'wspace':.., 'hspace':..} 与 matplotlib 一致地控制子图间距,
1996
2092
  # 复用 subplots_adjust 的存储路径(在渲染阶段覆盖默认/启发式间距)。
1997
- gridspec_kw = kwargs.get('gridspec_kw')
1998
2093
  if gridspec_kw:
1999
2094
  ws = gridspec_kw.get('wspace')
2000
2095
  hs = gridspec_kw.get('hspace')
@@ -2924,6 +3019,20 @@ def _patch_axes():
2924
3019
  _rs.Axes.set_xticks = _set_xticks
2925
3020
  _rs.Axes.set_yticks = _set_yticks
2926
3021
 
3022
+ # set_xticklabels / set_yticklabels: 归一为字符串 list,吸收 rotation/ha/fontsize 等
3023
+ # 未支持的样式 kwargs。须在 set_xticks/set_yticks 固定刻度位置后调用(matplotlib 语义)。
3024
+ _orig_set_xticklabels = _rs.Axes.set_xticklabels
3025
+ _orig_set_yticklabels = _rs.Axes.set_yticklabels
3026
+
3027
+ def _set_xticklabels(self, labels, **kwargs):
3028
+ return _orig_set_xticklabels(self, [str(x) for x in _to_list(labels)])
3029
+
3030
+ def _set_yticklabels(self, labels, **kwargs):
3031
+ return _orig_set_yticklabels(self, [str(x) for x in _to_list(labels)])
3032
+
3033
+ _rs.Axes.set_xticklabels = _set_xticklabels
3034
+ _rs.Axes.set_yticklabels = _set_yticklabels
3035
+
2927
3036
  # axis: 支持序列 [xmin,xmax,ymin,ymax] 设定轴限,及 'off'/'on'/'equal' 等字符串。
2928
3037
  _orig_axis = _rs.Axes.axis
2929
3038
 
@@ -2982,15 +3091,28 @@ def _patch_axes():
2982
3091
  _orig_annotate = _rs.Axes.annotate
2983
3092
 
2984
3093
  def _annotate(self, text, xy, xytext=None, fontsize=None,
2985
- color="black", arrowprops=None, **kwargs):
2986
- # va/ha/xycoords 等后端未支持的参数被 **kwargs 吸收后丢弃。
3094
+ color="black", arrowprops=None, xycoords='data',
3095
+ textcoords=None, ha=None, family=None, **kwargs):
3096
+ # 支持坐标系 (xycoords/textcoords)、水平对齐 (ha/horizontalalignment)、
3097
+ # 字体族 (family/fontfamily);其余未支持参数 (如 va) 被 **kwargs 吸收后丢弃。
2987
3098
  if isinstance(text, str):
2988
3099
  text = _render_mathtext(text)
2989
3100
  # 未显式指定 fontsize 时,默认字号随其余默认字号一并放大 DEFAULT_FONT_SCALE 倍;
2990
3101
  # 用户显式传入 fontsize 则保持原值不放大。
2991
3102
  if fontsize is None:
2992
3103
  fontsize = _DEFAULT_ANNOTATE_FONTSIZE
2993
- return _orig_annotate(self, text, xy, xytext, fontsize, color, arrowprops)
3104
+ if ha is None:
3105
+ ha = kwargs.get('horizontalalignment', 'center')
3106
+ if family is None:
3107
+ family = kwargs.get('fontfamily', None)
3108
+ # xycoords 可能是 get_xaxis_transform / get_yaxis_transform 返回的标记字符串;
3109
+ # 若传入的是其它非字符串的 transform 对象则回退到 'data'。
3110
+ if not isinstance(xycoords, str):
3111
+ xycoords = 'data'
3112
+ if textcoords is not None and not isinstance(textcoords, str):
3113
+ textcoords = None
3114
+ return _orig_annotate(self, text, xy, xytext, fontsize, color,
3115
+ arrowprops, xycoords, textcoords, ha, family)
2994
3116
 
2995
3117
  _rs.Axes.annotate = _annotate
2996
3118
 
@@ -202,6 +202,16 @@ pub enum PlotElement {
202
202
  color: String,
203
203
  /// 箭头参数;None 表示不画箭头(仅放置文本)。
204
204
  arrow: Option<ArrowSpec>,
205
+ /// `xy` 的坐标系:"data" / "axes fraction" / "yaxis_transform"
206
+ /// (x 轴取 axes 分数, y 轴取 data) / "xaxis_transform" (x 取 data, y 取 axes 分数)。
207
+ xycoords: String,
208
+ /// `xytext` 的坐标系:"data" / "offset points" / "axes fraction" 等;
209
+ /// 未显式指定时与 `xycoords` 一致 (matplotlib 语义)。
210
+ textcoords: String,
211
+ /// 文本水平对齐:"left" / "center" / "right"。
212
+ ha: String,
213
+ /// 字体族 (family / fontfamily),None 表示用默认族。
214
+ family: Option<String>,
205
215
  },
206
216
  Stack {
207
217
  x: Vec<f64>,