FigCompose 0.1.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. data_visuals/__init__.py +18 -0
  2. data_visuals/api.py +159 -0
  3. data_visuals/charts/__init__.py +340 -0
  4. data_visuals/charts/bar.py +367 -0
  5. data_visuals/charts/box.py +180 -0
  6. data_visuals/charts/confusion.py +125 -0
  7. data_visuals/charts/heatmap.py +181 -0
  8. data_visuals/charts/histogram.py +120 -0
  9. data_visuals/charts/line.py +160 -0
  10. data_visuals/charts/pie.py +129 -0
  11. data_visuals/charts/radar.py +95 -0
  12. data_visuals/charts/scatter.py +153 -0
  13. data_visuals/compute/__init__.py +59 -0
  14. data_visuals/compute/aggregation.py +174 -0
  15. data_visuals/compute/preprocess.py +113 -0
  16. data_visuals/compute/statistics.py +396 -0
  17. data_visuals/core/__init__.py +37 -0
  18. data_visuals/core/canvas.py +67 -0
  19. data_visuals/core/config.py +227 -0
  20. data_visuals/core/facets.py +37 -0
  21. data_visuals/core/figure.py +1295 -0
  22. data_visuals/core/identity.py +197 -0
  23. data_visuals/core/legend.py +130 -0
  24. data_visuals/core/metadata.py +159 -0
  25. data_visuals/core/panel.py +197 -0
  26. data_visuals/core/quality.py +191 -0
  27. data_visuals/core/regression.py +370 -0
  28. data_visuals/core/spec.py +278 -0
  29. data_visuals/data/__init__.py +80 -0
  30. data_visuals/data/labels.py +126 -0
  31. data_visuals/data/loader.py +139 -0
  32. data_visuals/data/schema.py +305 -0
  33. data_visuals/data/transform.py +286 -0
  34. data_visuals/layout/__init__.py +106 -0
  35. data_visuals/layout/auto.py +295 -0
  36. data_visuals/layout/grid.py +562 -0
  37. data_visuals/layout/manager.py +694 -0
  38. data_visuals/layout/panels.py +36 -0
  39. data_visuals/layout/physical.py +344 -0
  40. data_visuals/layout/placement.py +107 -0
  41. data_visuals/layout/shared.py +156 -0
  42. data_visuals/layout/sizing.py +402 -0
  43. data_visuals/renderer/__init__.py +5 -0
  44. data_visuals/renderer/export.py +84 -0
  45. data_visuals/renderer/mpl/__init__.py +23 -0
  46. data_visuals/renderer/mpl/annotations.py +104 -0
  47. data_visuals/renderer/mpl/axes.py +176 -0
  48. data_visuals/renderer/mpl/canvas.py +1085 -0
  49. data_visuals/renderer/mpl/charts/__init__.py +7 -0
  50. data_visuals/renderer/mpl/charts/bar.py +105 -0
  51. data_visuals/renderer/mpl/charts/box.py +101 -0
  52. data_visuals/renderer/mpl/charts/confusion.py +30 -0
  53. data_visuals/renderer/mpl/charts/heatmap.py +26 -0
  54. data_visuals/renderer/mpl/charts/helpers.py +228 -0
  55. data_visuals/renderer/mpl/charts/histogram.py +58 -0
  56. data_visuals/renderer/mpl/charts/line.py +93 -0
  57. data_visuals/renderer/mpl/charts/pie.py +101 -0
  58. data_visuals/renderer/mpl/charts/radar.py +75 -0
  59. data_visuals/renderer/mpl/charts/registry.py +41 -0
  60. data_visuals/renderer/mpl/charts/scatter.py +291 -0
  61. data_visuals/renderer/mpl/config.py +281 -0
  62. data_visuals/renderer/mpl/custom.py +256 -0
  63. data_visuals/renderer/mpl/export.py +73 -0
  64. data_visuals/renderer/mpl/facets.py +141 -0
  65. data_visuals/renderer/mpl/legends.py +311 -0
  66. data_visuals/renderer/mpl/quality.py +552 -0
  67. data_visuals/renderer/mpl/styling.py +56 -0
  68. data_visuals/themes/__init__.py +131 -0
  69. data_visuals/themes/general.json +121 -0
  70. data_visuals/themes/modern.json +122 -0
  71. data_visuals/utils/__init__.py +34 -0
  72. data_visuals/utils/colors.py +402 -0
  73. figcompose-0.1.1.dist-info/METADATA +364 -0
  74. figcompose-0.1.1.dist-info/RECORD +77 -0
  75. figcompose-0.1.1.dist-info/WHEEL +5 -0
  76. figcompose-0.1.1.dist-info/licenses/LICENSE +202 -0
  77. figcompose-0.1.1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,18 @@
1
+ """
2
+ FigCompose — 规则驱动的论文数据可视化工具
3
+ =========================================
4
+
5
+ 一个完全基于规则运行的声明式数据可视化引擎。
6
+ 核心设计原则:Chart 决定"画什么",Compute 决定"怎么算",
7
+ Layout 决定"放哪里",Theme 决定"长什么样"。
8
+ """
9
+
10
+ __version__ = "0.1.1"
11
+
12
+ from data_visuals.api import plot
13
+ from data_visuals.core.config import FigureConfig
14
+ from data_visuals.core.quality import FigureReport, FigureQualityError, QualityIssue, check_figure
15
+ from data_visuals.utils.colors import list_color_presets
16
+
17
+ __all__ = ["plot", "FigureConfig", "FigureReport", "FigureQualityError", "QualityIssue",
18
+ "check_figure", "list_color_presets", "__version__"]
data_visuals/api.py ADDED
@@ -0,0 +1,159 @@
1
+ """Small, data-first entry point backed by the existing Figure pipeline."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Mapping, Sequence
6
+ from os import PathLike
7
+ from typing import Any
8
+
9
+ import pandas as pd
10
+
11
+ from data_visuals.charts import get_chart
12
+ from data_visuals.core import Figure
13
+ from data_visuals.core.canvas import normalize_canvas
14
+ from data_visuals.core.config import FigureConfig, get_default_config
15
+ from data_visuals.core.identity import normalize_fields
16
+ from data_visuals.core.metadata import field_label
17
+ from data_visuals.data.loader import load_csv
18
+ from data_visuals.layout.panels import plan_panels
19
+ from data_visuals.themes import load_theme
20
+ from data_visuals.utils.colors import get_color_preset
21
+
22
+
23
+ def _default_title(kind: str, declaration: dict[str, Any], index: int, count: int,
24
+ fields: Mapping[str, Any] | None = None) -> str:
25
+ if kind == "histogram":
26
+ metric = declaration.get("x", "")
27
+ elif kind in {"pie", "donut"}:
28
+ metric = declaration.get("value") or declaration.get("group", "")
29
+ elif kind == "heatmap":
30
+ metric = declaration.get("value", "")
31
+ elif kind == "confusion":
32
+ vertical = field_label(declaration.get("y", ""), fields or {}, include_unit=False)
33
+ horizontal = field_label(declaration.get("x", ""), fields or {}, include_unit=False)
34
+ metric = f"{vertical} × {horizontal}"
35
+ else:
36
+ metric = declaration.get("y", "")
37
+ names = metric if isinstance(metric, (list, tuple)) else [metric]
38
+ text = ", ".join(field_label(name, fields or {}, include_unit=False) for name in names)
39
+ if count == 1:
40
+ return text
41
+ label = chr(ord("a") + index) if count <= 26 else str(index + 1)
42
+ return f"({label}) {text}"
43
+
44
+
45
+ def plot(
46
+ data: str | PathLike[str] | pd.DataFrame,
47
+ *,
48
+ plots: Sequence[Mapping[str, Any]],
49
+ x: str | None = None,
50
+ group: str | None = None,
51
+ canvas: Mapping[str, Any] | None = None,
52
+ cols: int | None = None,
53
+ style: str | None = None,
54
+ palette: str | None = None,
55
+ title: str | None = None,
56
+ legend: Mapping[str, Any] | bool | None = None,
57
+ config: FigureConfig | None = None,
58
+ csv_options: Mapping[str, Any] | None = None,
59
+ ) -> Figure:
60
+ """Describe a complete Figure with a CSV, metrics, plot kinds and a style.
61
+
62
+ Each entry in ``plots`` is one panel, in reading order. ``kind`` selects
63
+ an existing chart type; ``metric`` selects its measured column (``y`` for
64
+ bar/line/box/scatter/radar, ``x`` for histogram, and ``value`` for
65
+ heatmap/confusion/pie/donut). Pie/donut require ``group`` and sum their
66
+ values per category; omit ``metric``/``value`` to count rows. Radar uses
67
+ ``x`` for metric names and requires every series to contain all metrics.
68
+ Native chart mappings and options are also accepted in the entry.
69
+ ``palette`` optionally selects an original curated combination, such as
70
+ ``curated-5-default``, independently of typography and layout style.
71
+ ``legend`` optionally overrides shared/local placement, columns, labels,
72
+ order, and spacing in points; ``False`` hides legends. Individual entries
73
+ can override these fields through ``chart_opts={'legend': {...}}``.
74
+ ``config`` reuses a :class:`FigureConfig` across Figures: physical canvas,
75
+ typography, palette, legends, field labels/units/formats and series identity.
76
+ Explicit call arguments and panel options override those defaults.
77
+
78
+ Global ``x`` and ``group`` are defaults for bar/line/box/scatter/radar.
79
+ Pie/donut and histogram inherit only ``group``; matrix plots require explicit axes. An entry can
80
+ override a default, including ``group=None`` to turn grouping off.
81
+
82
+ ``canvas`` sets final physical dimensions, e.g. ``{'width': 180,
83
+ 'height': 110}`` in mm (or ``unit='in'``). Omit height for measured automatic
84
+ height; omit canvas to inherit the global default physical width.
85
+
86
+ Panel count, row count, panel titles, fonts, colors,
87
+ legends and decoration spacing are automatic. ``cols`` optionally limits
88
+ panels per row; an incomplete last row fills the width. No columns or
89
+ statistical meanings are guessed from their names.
90
+
91
+ Returns the regular :class:`Figure`, ready for ``.render('figure.svg')``,
92
+ ``.render('figure.pdf')`` or ``.show()``. CSV reading options such as
93
+ ``encoding`` and ``delimiter`` can be passed in ``csv_options``.
94
+ """
95
+ cfg = config if config is not None else get_default_config()
96
+ if not isinstance(cfg, FigureConfig):
97
+ raise TypeError("config must be a FigureConfig; use FigureConfig.from_dict() or .load().")
98
+ fields = normalize_fields(cfg.fields)
99
+ if not isinstance(plots, Sequence) or isinstance(plots, (str, bytes)):
100
+ raise TypeError("plots 必须是子图声明列表,例如 [{'kind': 'bar', 'metric': 'accuracy'}]。")
101
+ layout, positions, _ = plan_panels(len(plots), cols)
102
+ if canvas is not None:
103
+ normalize_canvas(canvas)
104
+ if csv_options is not None and not isinstance(csv_options, Mapping):
105
+ raise TypeError("csv_options 必须是字典。")
106
+ for name, column in (("x", x), ("group", group)):
107
+ if column is not None and (not isinstance(column, str) or not column):
108
+ raise TypeError(f"{name} 必须是列名字符串或 None。")
109
+
110
+ # Normalize declarations without modifying the caller's dictionaries.
111
+ declarations = []
112
+ for index, panel in enumerate(plots):
113
+ if not isinstance(panel, Mapping):
114
+ raise TypeError(f"plots[{index}] 必须是字典。")
115
+ declaration = dict(panel)
116
+ kind = declaration.pop("kind", None)
117
+ if not isinstance(kind, str) or not kind:
118
+ raise ValueError(f"plots[{index}] 需要 kind,例如 'bar'、'line' 或 'box'。")
119
+ try:
120
+ chart = get_chart(kind)
121
+ except ValueError as error:
122
+ raise ValueError(f"plots[{index}]: {error}") from error
123
+
124
+ if "metric" in declaration:
125
+ target = "x" if kind == "histogram" else "value" if kind in {"heatmap", "confusion", "pie", "donut"} else "y"
126
+ if target in declaration:
127
+ raise ValueError(f"plots[{index}] 的 metric 与 {target} 重复,请只指定其中一个。")
128
+ declaration[target] = declaration.pop("metric")
129
+ if kind in {"bar", "line", "box", "scatter", "radar"} and x is not None:
130
+ declaration.setdefault("x", x)
131
+ if "group" in (set(chart.required_fields) | set(chart.optional_fields)) and group is not None:
132
+ declaration.setdefault("group", group)
133
+ if "title" not in declaration:
134
+ declaration["title"] = _default_title(kind, declaration, index, len(plots), fields)
135
+ declarations.append((kind, declaration))
136
+
137
+ selected_style = style if style is not None else cfg.theme
138
+ if not isinstance(selected_style, str) or not selected_style:
139
+ raise TypeError("style 必须是风格名称,例如 'general' 或 'modern'。")
140
+ load_theme(selected_style)
141
+ if palette is not None:
142
+ get_color_preset(palette)
143
+ if isinstance(data, pd.DataFrame):
144
+ if csv_options:
145
+ raise ValueError("csv_options 仅适用于 CSV 文件输入。")
146
+ frame = data
147
+ elif isinstance(data, (str, PathLike)):
148
+ frame = load_csv(data, **dict(csv_options or {}))
149
+ else:
150
+ raise TypeError("data 必须是 CSV 路径或 pandas DataFrame。")
151
+
152
+ figure = Figure(data=frame, layout=layout, canvas=canvas, theme=selected_style, config=cfg,
153
+ palette=palette, title=title, legend=legend)
154
+ figure._auto_cols = cols
155
+ for (row, col), (kind, declaration) in zip(positions, declarations):
156
+ figure[row, col].set(kind, **declaration)
157
+ # Validate data mappings now, before a notebook user starts exporting files.
158
+ figure.build()
159
+ return figure
@@ -0,0 +1,340 @@
1
+ """
2
+ charts/ — 图表类型定义层
3
+ ========================
4
+
5
+ 图表层只定义"画什么":声明每个图表类型需要的数据字段映射,
6
+ 以及触发哪些统计计算。不涉及渲染、颜色、布局。
7
+
8
+ 设计原则(来自架构文档):
9
+ Chart 决定"画什么",Compute 决定"怎么算",
10
+ Layout 决定"放哪里",Theme 决定"长什么样"。
11
+
12
+ 每条边界永不互侵 → 永不退化为 matplotlib 参数堆积。
13
+
14
+ 支持图表类型:
15
+ - bar — 柱状图(支持 error bar)
16
+ - line — 折线图
17
+ - scatter — 散点图
18
+ - box — 箱线图
19
+ - heatmap — 热力图
20
+ - histogram — 直方图
21
+ - confusion — 混淆矩阵
22
+ - pie / donut — 饼图与环形图
23
+ - radar — 雷达图
24
+ """
25
+
26
+ from __future__ import annotations
27
+
28
+ from dataclasses import dataclass, field, fields as dataclass_fields
29
+ from typing import Optional, Dict, List, Any, Type, ClassVar
30
+
31
+ import pandas as pd
32
+
33
+
34
+ # ── 图表类型枚举 ──────────────────────────────────────────────
35
+
36
+ ChartType = str # Names are resolved through the chart registry below.
37
+
38
+
39
+ # ═══════════════════════════════════════════════════════════════
40
+ # 基类:ChartSpec
41
+ # ═══════════════════════════════════════════════════════════════
42
+
43
+ @dataclass
44
+ class ChartSpec:
45
+ """
46
+ 图表类型规格基类。
47
+
48
+ 每个子类声明:
49
+ - required_fields: 必须提供的数据列映射键
50
+ - optional_fields: 可选的数据列映射键及其默认值
51
+ - compute_ops: 该图表类型需要触发的统计计算操作
52
+
53
+ 实例化后可通过 .to_spec() 导出为 FigureSpec 的 chart 片段,
54
+ 或通过 .compute_requirements() 告知 compute 层该执行什么聚合。
55
+ """
56
+
57
+ # ── 子类覆盖的类变量 ──
58
+
59
+ # 图表类型名称,子类必须覆盖
60
+ chart_type: ClassVar[str] = ""
61
+
62
+ # 必需的数据映射键(如 ["x", "y"])
63
+ required_fields: ClassVar[List[str]] = []
64
+
65
+ # 可选的数据映射键,key=字段名, value=默认值(None 表示无默认值)
66
+ optional_fields: ClassVar[Dict[str, Optional[str]]] = {}
67
+
68
+ # ── 实例字段 ──
69
+
70
+ # 数据映射:用户声明的 x/y/group/error 等 → CSV 列名
71
+ mapping: Dict[str, Any] = field(default_factory=dict)
72
+
73
+ # 子图标题(可选)
74
+ title: Optional[str] = None
75
+
76
+ def __post_init__(self) -> None:
77
+ """校验子类声明的 chart_type 非空。"""
78
+ if not self.__class__.chart_type:
79
+ raise ValueError(f"{self.__class__.__name__} 必须定义 chart_type 类变量")
80
+
81
+ # ── 校验 ──────────────────────────────────────────────
82
+
83
+ def validate(self, df: pd.DataFrame) -> List[str]:
84
+ """
85
+ 校验数据映射中引用的所有列是否在 DataFrame 中存在。
86
+
87
+ 遍历 required_fields + optional_fields 中已赋值的字段,
88
+ 自动处理 str 和 List[str] 两种值类型。
89
+
90
+ Parameters
91
+ ----------
92
+ df : pd.DataFrame
93
+ 数据源
94
+
95
+ Returns
96
+ -------
97
+ list[str] — 错误信息列表,空列表表示校验通过
98
+ """
99
+ errors: List[str] = []
100
+
101
+ fields_to_check: set = set(self.__class__.required_fields)
102
+ fields_to_check.update(self.__class__.optional_fields.keys())
103
+
104
+ for field_name in fields_to_check:
105
+ col_name = self.mapping.get(field_name)
106
+ if col_name is None:
107
+ continue # 可选字段未提供
108
+
109
+ if isinstance(col_name, str):
110
+ if col_name not in df.columns:
111
+ errors.append(
112
+ f"[{self.chart_type}] 字段 '{field_name}' 映射到列 "
113
+ f"'{col_name}',但该列不存在于数据中。"
114
+ f"有效列: {list(df.columns)}"
115
+ )
116
+ elif isinstance(col_name, list):
117
+ for cn in col_name:
118
+ if isinstance(cn, str) and cn not in df.columns:
119
+ errors.append(
120
+ f"[{self.chart_type}] 字段 '{field_name}' 中的列 "
121
+ f"'{cn}' 不存在于数据中。"
122
+ f"有效列: {list(df.columns)}"
123
+ )
124
+
125
+ return errors
126
+
127
+ def validate_fields(self, user_mapping: Dict[str, Any]) -> List[str]:
128
+ """
129
+ 校验用户提供的映射是否包含所有必需字段。
130
+
131
+ Parameters
132
+ ----------
133
+ user_mapping : dict
134
+ 用户提供的数据映射
135
+
136
+ Returns
137
+ -------
138
+ list[str] — 缺失字段名列表
139
+ """
140
+ missing = []
141
+ for f in self.__class__.required_fields:
142
+ if f not in user_mapping or user_mapping[f] is None:
143
+ missing.append(f)
144
+ return missing
145
+
146
+ # ── 计算需求 ──────────────────────────────────────────
147
+
148
+ def compute_requirements(self) -> Dict[str, Any]:
149
+ """
150
+ 返回该图表类型所需的统计计算描述。
151
+
152
+ 子类覆盖此方法以声明具体的聚合需求。
153
+ 返回格式供 compute/ 层消费:
154
+ {
155
+ "group_by": ["col1", "col2"], # 分组列名
156
+ "columns": { # 需聚合的列 → 算子列表
157
+ "col_name": ["mean", "std"]
158
+ },
159
+ "preprocess": [...], # 预处理操作
160
+ "statistics": [...], # 额外统计量
161
+ }
162
+
163
+ Returns
164
+ -------
165
+ dict
166
+ """
167
+ return {
168
+ "group_by": [],
169
+ "columns": {},
170
+ "preprocess": [],
171
+ "statistics": [],
172
+ }
173
+
174
+ # ── 导出 ──────────────────────────────────────────────
175
+
176
+ def _chart_options(self) -> Dict[str, Any]:
177
+ """
178
+ 子类覆盖此方法,返回仅非默认值的渲染选项。
179
+
180
+ 只导出值不为默认值的选项 → spec 精简、无冗余。
181
+ """
182
+ return {}
183
+
184
+ def to_spec(self) -> Dict[str, Any]:
185
+ """
186
+ 导出为 FigureSpec 中 chart 片段的 dict 格式。
187
+
188
+ Returns
189
+ -------
190
+ dict — 可直接合并到 FigureSpec["charts"][i] 中
191
+ """
192
+ spec: Dict[str, Any] = {
193
+ "chart": self.chart_type,
194
+ "data": self.mapping.copy(),
195
+ }
196
+ if self.title:
197
+ spec["title"] = self.title
198
+ opts = self._chart_options()
199
+ if opts:
200
+ spec["options"] = opts
201
+ return spec
202
+
203
+ def __repr__(self) -> str:
204
+ fields = ", ".join(f"{k}={v}" for k, v in self.mapping.items())
205
+ return f"{self.__class__.__name__}({fields})"
206
+
207
+
208
+ # ═══════════════════════════════════════════════════════════════
209
+ # 图表注册中心
210
+ # ═══════════════════════════════════════════════════════════════
211
+
212
+ # name → ChartSpec 子类
213
+ _chart_registry: Dict[str, Type[ChartSpec]] = {}
214
+
215
+
216
+ def register_chart(cls: Type[ChartSpec]) -> Type[ChartSpec]:
217
+ """装饰器:注册图表类型到全局注册表。"""
218
+ chart_type = cls.chart_type
219
+ if not chart_type:
220
+ raise ValueError(f"{cls.__name__} 必须定义 chart_type 类变量")
221
+ _chart_registry[chart_type] = cls
222
+ return cls
223
+
224
+
225
+ def get_chart(chart_type: str) -> Type[ChartSpec]:
226
+ """
227
+ 按名称获取图表类型类。
228
+
229
+ Parameters
230
+ ----------
231
+ chart_type : str
232
+ 图表类型名称,可通过 list_chart_types() 查询。
233
+
234
+ Returns
235
+ -------
236
+ type[ChartSpec]
237
+
238
+ Raises
239
+ ------
240
+ ValueError — 未知的图表类型
241
+ """
242
+ if chart_type not in _chart_registry:
243
+ raise ValueError(
244
+ f"未知图表类型 '{chart_type}'。"
245
+ f"已注册类型: {list_chart_types()}"
246
+ )
247
+ return _chart_registry[chart_type]
248
+
249
+
250
+ def list_chart_types() -> List[str]:
251
+ """列出所有已注册的图表类型名称。"""
252
+ return sorted(_chart_registry.keys())
253
+
254
+
255
+ def create_chart(
256
+ chart_type: str,
257
+ mapping: Dict[str, Any],
258
+ *,
259
+ title: Optional[str] = None,
260
+ ) -> ChartSpec:
261
+ """
262
+ 工厂函数:按名称创建图表规格实例。
263
+
264
+ Parameters
265
+ ----------
266
+ chart_type : str
267
+ 图表类型名称
268
+ mapping : dict
269
+ 数据映射,如 {"x": "method", "y": "accuracy", "error": "std"}
270
+ title : str | None
271
+ 子图标题
272
+
273
+ Returns
274
+ -------
275
+ ChartSpec 实例
276
+
277
+ Examples
278
+ --------
279
+ >>> chart = create_chart("bar", {"x": "method", "y": "accuracy", "error": "std"})
280
+ >>> chart.chart_type
281
+ 'bar'
282
+ >>> chart.compute_requirements()
283
+ {'group_by': ['method'], 'columns': {'accuracy': ['mean', 'std']}}
284
+ """
285
+ cls = get_chart(chart_type)
286
+
287
+ if chart_type in {"pie", "donut"} and ("x" in mapping or "y" in mapping):
288
+ raise ValueError(
289
+ f"[{chart_type}] 饼图使用 group='类别列' 和 value='数量列',不使用 x/y。"
290
+ "省略 value 时按类别统计行数;快捷 API 的 metric 对应 value。"
291
+ )
292
+
293
+ mapping_fields = set(cls.required_fields) | set(cls.optional_fields.keys())
294
+ chart_field_names = {
295
+ field.name
296
+ for field in dataclass_fields(cls)
297
+ if field.init and field.name not in {"mapping", "title"}
298
+ }
299
+
300
+ pure_mapping: Dict[str, Any] = {}
301
+ option_kwargs: Dict[str, Any] = {}
302
+ unknown_keys: List[str] = []
303
+
304
+ for key, value in mapping.items():
305
+ if key in mapping_fields:
306
+ pure_mapping[key] = value
307
+ elif key in chart_field_names:
308
+ option_kwargs[key] = value
309
+ else:
310
+ unknown_keys.append(key)
311
+
312
+ if unknown_keys:
313
+ raise ValueError(
314
+ f"[{chart_type}] 未知参数: {unknown_keys}。"
315
+ f"支持的数据映射: {sorted(mapping_fields)};"
316
+ f"支持的图表选项: {sorted(chart_field_names)}"
317
+ )
318
+
319
+ instance = cls(mapping=pure_mapping, title=title, **option_kwargs)
320
+ missing = instance.validate_fields(pure_mapping)
321
+ if missing:
322
+ raise ValueError(
323
+ f"[{chart_type}] 缺少必需字段: {missing}。"
324
+ f"必需: {cls.required_fields}"
325
+ )
326
+ return instance
327
+
328
+
329
+ # ── 导入所有图表子类,触发 @register_chart 注册 ─────────────
330
+ # 放在文件末尾避免循环导入
331
+ from data_visuals.charts.bar import Bar # noqa: E402, F401
332
+ from data_visuals.charts.line import Line # noqa: E402, F401
333
+ from data_visuals.charts.scatter import Scatter # noqa: E402, F401
334
+ from data_visuals.charts.box import Box # noqa: E402, F401
335
+ from data_visuals.charts.heatmap import Heatmap # noqa: E402, F401
336
+ from data_visuals.charts.histogram import Histogram # noqa: E402, F401
337
+ from data_visuals.charts.confusion import Confusion # noqa: E402, F401
338
+ from data_visuals.charts.pie import Pie, Donut # noqa: E402, F401
339
+ from data_visuals.charts.radar import Radar # noqa: E402, F401
340
+