transone-chart 0.1.2

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 (46) hide show
  1. package/README.md +181 -0
  2. package/dist/adapters/index.d.ts +7 -0
  3. package/dist/adapters/miniprogram.d.ts +44 -0
  4. package/dist/adapters/native.d.ts +28 -0
  5. package/dist/adapters/types.d.ts +27 -0
  6. package/dist/adapters/web.d.ts +13 -0
  7. package/dist/charts/bar.d.ts +25 -0
  8. package/dist/charts/index.d.ts +4 -0
  9. package/dist/charts/line.d.ts +20 -0
  10. package/dist/charts/pie.d.ts +14 -0
  11. package/dist/charts/radar.d.ts +16 -0
  12. package/dist/component.d.ts +41 -0
  13. package/dist/core/axis.d.ts +47 -0
  14. package/dist/core/canvas.d.ts +61 -0
  15. package/dist/core/chart.d.ts +69 -0
  16. package/dist/core/layout.d.ts +58 -0
  17. package/dist/core/legend.d.ts +26 -0
  18. package/dist/core/scale.d.ts +67 -0
  19. package/dist/core/text.d.ts +20 -0
  20. package/dist/factory.d.ts +22 -0
  21. package/dist/index.d.ts +29 -0
  22. package/dist/index.js +17 -0
  23. package/dist/index.js.map +49 -0
  24. package/dist/types.d.ts +180 -0
  25. package/lib/adapters/index.ts +18 -0
  26. package/lib/adapters/miniprogram.ts +146 -0
  27. package/lib/adapters/native.ts +39 -0
  28. package/lib/adapters/types.ts +44 -0
  29. package/lib/adapters/web.ts +41 -0
  30. package/lib/charts/bar.ts +270 -0
  31. package/lib/charts/index.ts +4 -0
  32. package/lib/charts/line.ts +153 -0
  33. package/lib/charts/pie.ts +110 -0
  34. package/lib/charts/radar.ts +166 -0
  35. package/lib/component.ts +149 -0
  36. package/lib/core/axis.ts +174 -0
  37. package/lib/core/canvas.ts +130 -0
  38. package/lib/core/chart.ts +322 -0
  39. package/lib/core/layout.ts +221 -0
  40. package/lib/core/legend.ts +94 -0
  41. package/lib/core/scale.ts +183 -0
  42. package/lib/core/text.ts +70 -0
  43. package/lib/factory.ts +62 -0
  44. package/lib/index.ts +69 -0
  45. package/lib/types.ts +218 -0
  46. package/package.json +55 -0
@@ -0,0 +1,149 @@
1
+ /**
2
+ * 可选集成:基于 transone Component 的声明式图表组件 TcChart。
3
+ *
4
+ * 与 transone 组件系统集成,一份源码同时编译 Web 与小程序:
5
+ * - Web:canvas 元素由组件渲染,onMounted 后自动解析并渲染
6
+ * - 小程序:canvas 编译为 <canvas type="2d">,通过 SelectorQuery 获取节点
7
+ * - 未来原生:通过 resolve 注入自定义解析器
8
+ *
9
+ * 组件本身是薄壳:状态由父级以 option 传入(完全受控),
10
+ * 数据变化时自动 setOption + render。
11
+ */
12
+
13
+ import {
14
+ Component,
15
+ h,
16
+ type VNode,
17
+ } from 'transone';
18
+ import type { ChartRenderContext, ChartOption } from './types';
19
+ import type { ChartBase } from './core/chart';
20
+ import { createChart } from './factory';
21
+ import {
22
+ detectMiniProgramGlobal,
23
+ getMiniProgramCanvasNode,
24
+ resolveMiniProgramCanvas,
25
+ } from './adapters/miniprogram';
26
+ import { resolveWebCanvas } from './adapters/web';
27
+
28
+ export interface TcChartProps {
29
+ /** 图表配置(line / bar / pie / radar)。 */
30
+ option: ChartOption;
31
+ /** 自定义解析器:把 canvas 元素/节点解析为渲染上下文。 */
32
+ resolve?: (
33
+ element: unknown
34
+ ) => Promise<ChartRenderContext> | ChartRenderContext;
35
+ className?: string;
36
+ }
37
+
38
+ interface TcChartState {
39
+ canvasId: string;
40
+ }
41
+
42
+ /** canvas 元素 id:Web 端仅作 DOM 属性;小程序端配合 SelectorQuery .in(实例) 隔离查询,多实例共存不冲突。 */
43
+ const CANVAS_ID = 'tc-chart-canvas';
44
+
45
+ export class TcChart extends Component<TcChartProps, TcChartState> {
46
+ private chart: ChartBase<ChartOption> | null = null;
47
+ private destroyed = false;
48
+
49
+ protected initState(): TcChartState {
50
+ return { canvasId: CANVAS_ID };
51
+ }
52
+
53
+ protected initStyles(): void {
54
+ this.styleManager.addStyle('tc-chart-canvas', {
55
+ selector: '.tc-chart__canvas',
56
+ properties: {
57
+ display: 'block',
58
+ height: '100%',
59
+ width: '100%',
60
+ },
61
+ });
62
+ }
63
+
64
+ protected render(): VNode {
65
+ return h('canvas', {
66
+ id: this.state.canvasId,
67
+ className: `tc-chart__canvas${this.props.className ? ` ${this.props.className}` : ''}`,
68
+ type: '2d',
69
+ });
70
+ }
71
+
72
+ protected onMounted(): void {
73
+ this.attachChart();
74
+ }
75
+
76
+ protected onUpdated(): void {
77
+ if (this.chart && !this.destroyed) {
78
+ this.chart.setOption(this.props.option).render();
79
+ } else if (!this.chart && !this.destroyed) {
80
+ this.attachChart();
81
+ }
82
+ }
83
+
84
+ protected onUnmounted(): void {
85
+ this.destroyed = true;
86
+ this.chart?.destroy();
87
+ this.chart = null;
88
+ }
89
+
90
+ /** 对外暴露图表实例(高级用法:手动 resize / 订阅事件)。 */
91
+ public getChart(): ChartBase<ChartOption> | null {
92
+ return this.chart;
93
+ }
94
+
95
+ private attachChart(): void {
96
+ if (this.destroyed) {
97
+ return;
98
+ }
99
+ this.resolveContext()
100
+ .then((context) => {
101
+ if (this.destroyed) {
102
+ return;
103
+ }
104
+ this.chart = createChart(context, this.props.option);
105
+ this.chart.render();
106
+ })
107
+ .catch((error: unknown) => {
108
+ console.error('[TcChart] attach failed:', error);
109
+ });
110
+ }
111
+
112
+ /** 组件根元素(canvas)。Web 端用于解析渲染上下文;小程序端经 SelectorQuery .in(实例) 查询,不依赖此方法。 */
113
+ public getElement(): HTMLCanvasElement | null {
114
+ return super.getElement() as HTMLCanvasElement | null;
115
+ }
116
+
117
+ private resolveContext(): Promise<ChartRenderContext> {
118
+ if (this.props.resolve) {
119
+ return Promise.resolve(this.props.resolve(this.getElement()));
120
+ }
121
+
122
+ const platform = detectMiniProgramGlobal();
123
+ if (platform) {
124
+ // 小程序:SelectorQuery 查询节点(组件可能尚未渲染完,有限重试)
125
+ const selector = `#${this.state.canvasId}`;
126
+ const attempt = (left: number): Promise<ChartRenderContext> =>
127
+ getMiniProgramCanvasNode({
128
+ platform,
129
+ instance: this,
130
+ selector,
131
+ })
132
+ .then((node) => resolveMiniProgramCanvas(node))
133
+ .catch((error: unknown) => {
134
+ if (left <= 0) {
135
+ throw error;
136
+ }
137
+ return new Promise((resolve) => setTimeout(resolve, 50)).then(() =>
138
+ attempt(left - 1)
139
+ );
140
+ });
141
+ return attempt(5);
142
+ }
143
+
144
+ // Web:canvas 元素即 HTMLCanvasElement
145
+ return Promise.resolve(
146
+ resolveWebCanvas(this.getElement() as unknown as HTMLCanvasElement)
147
+ );
148
+ }
149
+ }
@@ -0,0 +1,174 @@
1
+ /**
2
+ * 坐标轴绘制:数值轴(默认纵向居左、横向图居底)与类目轴(默认横向居底、
3
+ * 横向图居左)。网格线与刻度标签统一在这里绘制,数据系列只画在 plot 内。
4
+ *
5
+ * 位置计算全部基于布局结果与比例尺,与平台无关。
6
+ */
7
+
8
+ import type { ICanvas2D } from './canvas';
9
+ import type { Box } from './layout';
10
+ import { CategoryScale, LinearScale } from './scale';
11
+ import { applyFont } from './text';
12
+ import {
13
+ DEFAULT_AXIS_COLOR,
14
+ DEFAULT_GRID_COLOR,
15
+ DEFAULT_TEXT_COLOR,
16
+ } from '../types';
17
+
18
+ export interface ValueAxisDrawOptions {
19
+ /** 绘图区。 */
20
+ plot: Box;
21
+ /** 数值比例尺:纵向图 range=[plot.bottom, plot.top],横向图 range=[plot.left, plot.right]。 */
22
+ scale: LinearScale;
23
+ /** 刻度标签(长度需与 scale.ticks 一致)。 */
24
+ labels: readonly string[];
25
+ /** 横向图:数值轴位于底部。 */
26
+ horizontal?: boolean;
27
+ axisColor?: string;
28
+ labelFontSize?: number;
29
+ labelColor?: string;
30
+ gridColor?: string;
31
+ gridLineWidth?: number;
32
+ gridLineDash?: readonly number[];
33
+ showGrid?: boolean;
34
+ }
35
+
36
+ export interface CategoryAxisDrawOptions {
37
+ plot: Box;
38
+ scale: CategoryScale;
39
+ labels: readonly string[];
40
+ /** 横向图:类目轴位于左侧。 */
41
+ horizontal?: boolean;
42
+ axisColor?: string;
43
+ labelFontSize?: number;
44
+ labelColor?: string;
45
+ gridColor?: string;
46
+ gridLineWidth?: number;
47
+ showGrid?: boolean;
48
+ }
49
+
50
+ export class AxisDrawer {
51
+ public constructor(private readonly ctx: ICanvas2D) {}
52
+
53
+ /** 绘制数值轴:网格横线(或纵向图时) + 刻度标签。 */
54
+ public drawValueAxis(options: ValueAxisDrawOptions): void {
55
+ const {
56
+ plot,
57
+ scale,
58
+ labels,
59
+ horizontal = false,
60
+ labelFontSize = 11,
61
+ labelColor = DEFAULT_TEXT_COLOR,
62
+ gridColor = DEFAULT_GRID_COLOR,
63
+ gridLineWidth = 1,
64
+ gridLineDash,
65
+ showGrid = true,
66
+ } = options;
67
+
68
+ const ticks = scale.ticks;
69
+ const { ctx } = this;
70
+
71
+ for (let i = 0; i < ticks.length; i += 1) {
72
+ const pos = scale.scale(ticks[i]);
73
+ const label = labels[i] ?? String(ticks[i]);
74
+
75
+ if (showGrid) {
76
+ ctx.save();
77
+ ctx.strokeStyle = gridColor;
78
+ ctx.lineWidth = gridLineWidth;
79
+ if (gridLineDash && gridLineDash.length > 0) {
80
+ ctx.setLineDash(gridLineDash);
81
+ }
82
+ ctx.beginPath();
83
+ if (horizontal) {
84
+ ctx.moveTo(plot.x, pos);
85
+ ctx.lineTo(plot.x + plot.width, pos);
86
+ } else {
87
+ ctx.moveTo(plot.x, pos);
88
+ ctx.lineTo(plot.x + plot.width, pos);
89
+ }
90
+ ctx.stroke();
91
+ ctx.restore();
92
+ }
93
+
94
+ applyFont(ctx, labelFontSize);
95
+ ctx.fillStyle = labelColor;
96
+ ctx.textBaseline = 'middle';
97
+ if (horizontal) {
98
+ ctx.textAlign = 'center';
99
+ ctx.fillText(label, pos, plot.y + plot.height + 8);
100
+ } else {
101
+ ctx.textAlign = 'right';
102
+ ctx.fillText(label, plot.x - 8, pos);
103
+ }
104
+ }
105
+ }
106
+
107
+ /** 绘制类目轴:轴线 + 类目标签(可选竖网格)。 */
108
+ public drawCategoryAxis(options: CategoryAxisDrawOptions): void {
109
+ const {
110
+ plot,
111
+ scale,
112
+ labels,
113
+ horizontal = false,
114
+ axisColor = DEFAULT_AXIS_COLOR,
115
+ labelFontSize = 11,
116
+ labelColor = DEFAULT_TEXT_COLOR,
117
+ gridColor = DEFAULT_GRID_COLOR,
118
+ gridLineWidth = 1,
119
+ showGrid = false,
120
+ } = options;
121
+
122
+ const { ctx } = this;
123
+ const count = labels.length;
124
+
125
+ if (showGrid) {
126
+ ctx.save();
127
+ ctx.strokeStyle = gridColor;
128
+ ctx.lineWidth = gridLineWidth;
129
+ ctx.beginPath();
130
+ for (let i = 0; i < count; i += 1) {
131
+ const pos = scale.center(i);
132
+ if (horizontal) {
133
+ ctx.moveTo(plot.x, pos);
134
+ ctx.lineTo(plot.x + plot.width, pos);
135
+ } else {
136
+ ctx.moveTo(pos, plot.y);
137
+ ctx.lineTo(pos, plot.y + plot.height);
138
+ }
139
+ }
140
+ ctx.stroke();
141
+ ctx.restore();
142
+ }
143
+
144
+ applyFont(ctx, labelFontSize);
145
+ ctx.fillStyle = labelColor;
146
+ ctx.textBaseline = 'middle';
147
+
148
+ for (let i = 0; i < count; i += 1) {
149
+ const pos = scale.center(i);
150
+ if (horizontal) {
151
+ ctx.textAlign = 'right';
152
+ ctx.fillText(labels[i], plot.x - 8, pos);
153
+ } else {
154
+ ctx.textAlign = 'center';
155
+ ctx.fillText(labels[i], pos, plot.y + plot.height + 8);
156
+ }
157
+ }
158
+
159
+ // 轴线
160
+ ctx.save();
161
+ ctx.strokeStyle = axisColor;
162
+ ctx.lineWidth = 1;
163
+ ctx.beginPath();
164
+ if (horizontal) {
165
+ ctx.moveTo(plot.x, plot.y);
166
+ ctx.lineTo(plot.x, plot.y + plot.height);
167
+ } else {
168
+ ctx.moveTo(plot.x, plot.y + plot.height);
169
+ ctx.lineTo(plot.x + plot.width, plot.y + plot.height);
170
+ }
171
+ ctx.stroke();
172
+ ctx.restore();
173
+ }
174
+ }
@@ -0,0 +1,130 @@
1
+ /**
2
+ * 跨端 Canvas 2D 契约(ICanvas2D)。
3
+ *
4
+ * 这是 transone-chart 与平台解耦的唯一边界:
5
+ * - Web:HTMLCanvasElement.getContext('2d') 天然满足,适配器薄封装
6
+ * - 小程序:微信 / 阿里 / 字节的 Canvas 2D(type="2d")节点上下文与 Web 接口基本对齐
7
+ * - 未来原生 App:Skia / ArkUI 等原生 canvas 只需实现本接口即可接入同一套引擎
8
+ *
9
+ * 接口刻意收敛为图表实际需要的最小真子集,避免依赖平台各自的扩展 API;
10
+ * 绘制一律使用「beginPath → 路径命令 → fill()/stroke() 无参」形式,
11
+ * 不依赖 Path2D 等小程序端可能缺失的高级对象。
12
+ */
13
+
14
+ export interface IGradient {
15
+ addColorStop(offset: number, color: string): void;
16
+ }
17
+
18
+ export type CanvasLineCap = 'butt' | 'round' | 'square';
19
+ export type CanvasLineJoin = 'bevel' | 'round' | 'miter';
20
+ export type CanvasTextAlign =
21
+ | 'left'
22
+ | 'right'
23
+ | 'center'
24
+ | 'start'
25
+ | 'end';
26
+ export type CanvasTextBaseline =
27
+ | 'top'
28
+ | 'hanging'
29
+ | 'middle'
30
+ | 'alphabetic'
31
+ | 'ideographic'
32
+ | 'bottom';
33
+
34
+ export interface ICanvas2D {
35
+ /* —— 状态管理 —— */
36
+ save(): void;
37
+ restore(): void;
38
+ translate(x: number, y: number): void;
39
+ scale(x: number, y: number): void;
40
+ rotate(angle: number): void;
41
+
42
+ /* —— 样式属性 —— */
43
+ fillStyle: string | IGradient;
44
+ strokeStyle: string | IGradient;
45
+ lineWidth: number;
46
+ lineCap: CanvasLineCap;
47
+ lineJoin: CanvasLineJoin;
48
+ globalAlpha: number;
49
+ font: string;
50
+ textAlign: CanvasTextAlign;
51
+ textBaseline: CanvasTextBaseline;
52
+
53
+ /* —— 路径 —— */
54
+ beginPath(): void;
55
+ closePath(): void;
56
+ moveTo(x: number, y: number): void;
57
+ lineTo(x: number, y: number): void;
58
+ bezierCurveTo(
59
+ cp1x: number,
60
+ cp1y: number,
61
+ cp2x: number,
62
+ cp2y: number,
63
+ x: number,
64
+ y: number
65
+ ): void;
66
+ arc(
67
+ x: number,
68
+ y: number,
69
+ radius: number,
70
+ startAngle: number,
71
+ endAngle: number,
72
+ counterclockwise?: boolean
73
+ ): void;
74
+ arcTo(x1: number, y1: number, x2: number, y2: number, radius: number): void;
75
+ rect(x: number, y: number, width: number, height: number): void;
76
+ ellipse(
77
+ x: number,
78
+ y: number,
79
+ radiusX: number,
80
+ radiusY: number,
81
+ rotation: number,
82
+ startAngle: number,
83
+ endAngle: number,
84
+ counterclockwise?: boolean
85
+ ): void;
86
+
87
+ /* —— 绘制 —— */
88
+ fill(): void;
89
+ stroke(): void;
90
+ fillRect(x: number, y: number, width: number, height: number): void;
91
+ clearRect(x: number, y: number, width: number, height: number): void;
92
+ clip(): void;
93
+ setLineDash(segments: readonly number[]): void;
94
+
95
+ /* —— 渐变 —— */
96
+ createLinearGradient(
97
+ x0: number,
98
+ y0: number,
99
+ x1: number,
100
+ y1: number
101
+ ): IGradient;
102
+ createRadialGradient(
103
+ x0: number,
104
+ y0: number,
105
+ r0: number,
106
+ x1: number,
107
+ y1: number,
108
+ r1: number
109
+ ): IGradient;
110
+
111
+ /* —— 文字 —— */
112
+ fillText(text: string, x: number, y: number, maxWidth?: number): void;
113
+ measureText(text: string): { width: number };
114
+ }
115
+
116
+ /**
117
+ * ICanvas2D 的便捷判断:任意对象只要实现了最小方法集即视为满足契约。
118
+ * 用于适配器防御与测试 mock 的类型断言,不承担运行时校验。
119
+ */
120
+ export function isCanvas2DLike(value: unknown): value is ICanvas2D {
121
+ return (
122
+ typeof value === 'object' &&
123
+ value !== null &&
124
+ typeof (value as ICanvas2D).save === 'function' &&
125
+ typeof (value as ICanvas2D).beginPath === 'function' &&
126
+ typeof (value as ICanvas2D).fill === 'function' &&
127
+ typeof (value as ICanvas2D).stroke === 'function' &&
128
+ typeof (value as ICanvas2D).fillText === 'function'
129
+ );
130
+ }