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.
- data_visuals/__init__.py +18 -0
- data_visuals/api.py +159 -0
- data_visuals/charts/__init__.py +340 -0
- data_visuals/charts/bar.py +367 -0
- data_visuals/charts/box.py +180 -0
- data_visuals/charts/confusion.py +125 -0
- data_visuals/charts/heatmap.py +181 -0
- data_visuals/charts/histogram.py +120 -0
- data_visuals/charts/line.py +160 -0
- data_visuals/charts/pie.py +129 -0
- data_visuals/charts/radar.py +95 -0
- data_visuals/charts/scatter.py +153 -0
- data_visuals/compute/__init__.py +59 -0
- data_visuals/compute/aggregation.py +174 -0
- data_visuals/compute/preprocess.py +113 -0
- data_visuals/compute/statistics.py +396 -0
- data_visuals/core/__init__.py +37 -0
- data_visuals/core/canvas.py +67 -0
- data_visuals/core/config.py +227 -0
- data_visuals/core/facets.py +37 -0
- data_visuals/core/figure.py +1295 -0
- data_visuals/core/identity.py +197 -0
- data_visuals/core/legend.py +130 -0
- data_visuals/core/metadata.py +159 -0
- data_visuals/core/panel.py +197 -0
- data_visuals/core/quality.py +191 -0
- data_visuals/core/regression.py +370 -0
- data_visuals/core/spec.py +278 -0
- data_visuals/data/__init__.py +80 -0
- data_visuals/data/labels.py +126 -0
- data_visuals/data/loader.py +139 -0
- data_visuals/data/schema.py +305 -0
- data_visuals/data/transform.py +286 -0
- data_visuals/layout/__init__.py +106 -0
- data_visuals/layout/auto.py +295 -0
- data_visuals/layout/grid.py +562 -0
- data_visuals/layout/manager.py +694 -0
- data_visuals/layout/panels.py +36 -0
- data_visuals/layout/physical.py +344 -0
- data_visuals/layout/placement.py +107 -0
- data_visuals/layout/shared.py +156 -0
- data_visuals/layout/sizing.py +402 -0
- data_visuals/renderer/__init__.py +5 -0
- data_visuals/renderer/export.py +84 -0
- data_visuals/renderer/mpl/__init__.py +23 -0
- data_visuals/renderer/mpl/annotations.py +104 -0
- data_visuals/renderer/mpl/axes.py +176 -0
- data_visuals/renderer/mpl/canvas.py +1085 -0
- data_visuals/renderer/mpl/charts/__init__.py +7 -0
- data_visuals/renderer/mpl/charts/bar.py +105 -0
- data_visuals/renderer/mpl/charts/box.py +101 -0
- data_visuals/renderer/mpl/charts/confusion.py +30 -0
- data_visuals/renderer/mpl/charts/heatmap.py +26 -0
- data_visuals/renderer/mpl/charts/helpers.py +228 -0
- data_visuals/renderer/mpl/charts/histogram.py +58 -0
- data_visuals/renderer/mpl/charts/line.py +93 -0
- data_visuals/renderer/mpl/charts/pie.py +101 -0
- data_visuals/renderer/mpl/charts/radar.py +75 -0
- data_visuals/renderer/mpl/charts/registry.py +41 -0
- data_visuals/renderer/mpl/charts/scatter.py +291 -0
- data_visuals/renderer/mpl/config.py +281 -0
- data_visuals/renderer/mpl/custom.py +256 -0
- data_visuals/renderer/mpl/export.py +73 -0
- data_visuals/renderer/mpl/facets.py +141 -0
- data_visuals/renderer/mpl/legends.py +311 -0
- data_visuals/renderer/mpl/quality.py +552 -0
- data_visuals/renderer/mpl/styling.py +56 -0
- data_visuals/themes/__init__.py +131 -0
- data_visuals/themes/general.json +121 -0
- data_visuals/themes/modern.json +122 -0
- data_visuals/utils/__init__.py +34 -0
- data_visuals/utils/colors.py +402 -0
- figcompose-0.1.1.dist-info/METADATA +364 -0
- figcompose-0.1.1.dist-info/RECORD +77 -0
- figcompose-0.1.1.dist-info/WHEEL +5 -0
- figcompose-0.1.1.dist-info/licenses/LICENSE +202 -0
- figcompose-0.1.1.dist-info/top_level.txt +1 -0
data_visuals/__init__.py
ADDED
|
@@ -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
|
+
|