transone-chart 0.1.2 → 0.2.0
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.
- package/README.md +7 -4
- package/dist/charts/bar.d.ts +5 -1
- package/dist/charts/line.d.ts +5 -1
- package/dist/charts/pie.d.ts +3 -1
- package/dist/charts/radar.d.ts +3 -1
- package/dist/component.d.ts +21 -2
- package/dist/core/chart.d.ts +23 -2
- package/dist/core/debounce.d.ts +17 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +4 -4
- package/dist/index.js.map +12 -11
- package/dist/types.d.ts +42 -0
- package/lib/charts/bar.ts +95 -8
- package/lib/charts/line.ts +62 -3
- package/lib/charts/pie.ts +102 -10
- package/lib/charts/radar.ts +52 -1
- package/lib/component.ts +176 -7
- package/lib/core/chart.ts +166 -1
- package/lib/core/debounce.ts +52 -0
- package/lib/index.ts +4 -0
- package/lib/types.ts +46 -0
- package/package.json +2 -2
package/lib/component.ts
CHANGED
|
@@ -7,7 +7,10 @@
|
|
|
7
7
|
* - 未来原生:通过 resolve 注入自定义解析器
|
|
8
8
|
*
|
|
9
9
|
* 组件本身是薄壳:状态由父级以 option 传入(完全受控),
|
|
10
|
-
* 数据变化时自动 setOption + render
|
|
10
|
+
* 数据变化时自动 setOption + render;
|
|
11
|
+
* 容器 / 窗口尺寸变化时防抖(150ms)自动重绘:
|
|
12
|
+
* - Web:ResizeObserver 观察 canvas,重测 clientWidth/Height 后增量 resize + render
|
|
13
|
+
* - 小程序:平台窗口尺寸回调触发后重建图表(SelectorQuery 取最新节点尺寸)
|
|
11
14
|
*/
|
|
12
15
|
|
|
13
16
|
import {
|
|
@@ -17,12 +20,15 @@ import {
|
|
|
17
20
|
} from 'transone';
|
|
18
21
|
import type { ChartRenderContext, ChartOption } from './types';
|
|
19
22
|
import type { ChartBase } from './core/chart';
|
|
23
|
+
import { debounce, type Debounced } from './core/debounce';
|
|
20
24
|
import { createChart } from './factory';
|
|
21
25
|
import {
|
|
22
26
|
detectMiniProgramGlobal,
|
|
23
27
|
getMiniProgramCanvasNode,
|
|
28
|
+
getMiniProgramGlobal,
|
|
24
29
|
resolveMiniProgramCanvas,
|
|
25
30
|
} from './adapters/miniprogram';
|
|
31
|
+
import { detectPixelRatio } from './adapters/types';
|
|
26
32
|
import { resolveWebCanvas } from './adapters/web';
|
|
27
33
|
|
|
28
34
|
export interface TcChartProps {
|
|
@@ -42,9 +48,22 @@ interface TcChartState {
|
|
|
42
48
|
/** canvas 元素 id:Web 端仅作 DOM 属性;小程序端配合 SelectorQuery .in(实例) 隔离查询,多实例共存不冲突。 */
|
|
43
49
|
const CANVAS_ID = 'tc-chart-canvas';
|
|
44
50
|
|
|
51
|
+
/** 尺寸变化自动重绘的防抖窗口(毫秒)。连续 resize 事件只在此窗口后重绘一次。 */
|
|
52
|
+
const RESIZE_DEBOUNCE_MS = 150;
|
|
53
|
+
|
|
54
|
+
interface MpWindowResizeApi {
|
|
55
|
+
onWindowResize?: (callback: () => void) => unknown;
|
|
56
|
+
offWindowResize?: (callback: () => void) => unknown;
|
|
57
|
+
}
|
|
58
|
+
|
|
45
59
|
export class TcChart extends Component<TcChartProps, TcChartState> {
|
|
46
60
|
private chart: ChartBase<ChartOption> | null = null;
|
|
47
61
|
private destroyed = false;
|
|
62
|
+
private resizeDebounced: Debounced<() => void> | null = null;
|
|
63
|
+
private resizeObserver: ResizeObserver | null = null;
|
|
64
|
+
private mpResizeHandler: (() => void) | null = null;
|
|
65
|
+
/** Web 端 hover 事件句柄(mousemove / mouseleave),卸载时移除。 */
|
|
66
|
+
private hoverHandlers: Array<[string, (e: Event) => void]> = [];
|
|
48
67
|
|
|
49
68
|
protected initState(): TcChartState {
|
|
50
69
|
return { canvasId: CANVAS_ID };
|
|
@@ -71,6 +90,8 @@ export class TcChart extends Component<TcChartProps, TcChartState> {
|
|
|
71
90
|
|
|
72
91
|
protected onMounted(): void {
|
|
73
92
|
this.attachChart();
|
|
93
|
+
this.setupAutoResize();
|
|
94
|
+
this.setupHoverEvents();
|
|
74
95
|
}
|
|
75
96
|
|
|
76
97
|
protected onUpdated(): void {
|
|
@@ -82,6 +103,8 @@ export class TcChart extends Component<TcChartProps, TcChartState> {
|
|
|
82
103
|
}
|
|
83
104
|
|
|
84
105
|
protected onUnmounted(): void {
|
|
106
|
+
this.teardownAutoResize();
|
|
107
|
+
this.teardownHoverEvents();
|
|
85
108
|
this.destroyed = true;
|
|
86
109
|
this.chart?.destroy();
|
|
87
110
|
this.chart = null;
|
|
@@ -98,7 +121,8 @@ export class TcChart extends Component<TcChartProps, TcChartState> {
|
|
|
98
121
|
}
|
|
99
122
|
this.resolveContext()
|
|
100
123
|
.then((context) => {
|
|
101
|
-
if (this.destroyed) {
|
|
124
|
+
if (this.destroyed || !context) {
|
|
125
|
+
// 非浏览器环境(SSR / 构建期静态渲染)或显式跳过:渲染交给客户端水合阶段。
|
|
102
126
|
return;
|
|
103
127
|
}
|
|
104
128
|
this.chart = createChart(context, this.props.option);
|
|
@@ -109,12 +133,151 @@ export class TcChart extends Component<TcChartProps, TcChartState> {
|
|
|
109
133
|
});
|
|
110
134
|
}
|
|
111
135
|
|
|
112
|
-
/**
|
|
136
|
+
/**
|
|
137
|
+
* Web 端返回根 canvas;小程序端此方法不会被调用(走 SelectorQuery)。
|
|
138
|
+
* 注意:方法体不能写 super.X() —— transone-cli 编译小程序时会把组件方法拍平为
|
|
139
|
+
* Component options 的普通函数属性,super 在普通函数里非法(JSCore 直接报错)。
|
|
140
|
+
* 基类 el 为 protected,此处直接取用;返回类型收窄在使用点做断言。
|
|
141
|
+
*/
|
|
113
142
|
public getElement(): HTMLCanvasElement | null {
|
|
114
|
-
return
|
|
143
|
+
return this.el as HTMLCanvasElement | null;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/* —— 悬浮提示(tooltip):Web mousemove / mouseleave —— */
|
|
147
|
+
|
|
148
|
+
/** Web 端绑定 hover 事件:鼠标移动命中数据点即显示 tooltip,离开清除。 */
|
|
149
|
+
private setupHoverEvents(): void {
|
|
150
|
+
if (this.destroyed || this.hoverHandlers.length > 0) {
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
// 小程序端暂无鼠标事件,一期仅 Web;后续可在 touchstart/touchmove 接入同一 setHover API
|
|
154
|
+
if (detectMiniProgramGlobal()) {
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
const element = this.getElement() as HTMLCanvasElement | null;
|
|
158
|
+
if (!element || typeof element.addEventListener !== 'function') {
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const onMove = (event: Event): void => {
|
|
163
|
+
const chart = this.chart;
|
|
164
|
+
if (!chart) {
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
const mouse = event as MouseEvent;
|
|
168
|
+
const rect = element.getBoundingClientRect();
|
|
169
|
+
const relX = mouse.clientX - rect.left;
|
|
170
|
+
const relY = mouse.clientY - rect.top;
|
|
171
|
+
// 同步图表逻辑尺寸,避免 resize 防抖窗口内坐标系偏移导致命中错位
|
|
172
|
+
const w = rect.width;
|
|
173
|
+
const h = rect.height;
|
|
174
|
+
if (w > 0 && h > 0) {
|
|
175
|
+
chart.resize(w, h);
|
|
176
|
+
}
|
|
177
|
+
chart.setHover(relX, relY);
|
|
178
|
+
};
|
|
179
|
+
const onLeave = (): void => {
|
|
180
|
+
this.chart?.clearHover();
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
element.addEventListener('mousemove', onMove);
|
|
184
|
+
element.addEventListener('mouseleave', onLeave);
|
|
185
|
+
this.hoverHandlers = [
|
|
186
|
+
['mousemove', onMove],
|
|
187
|
+
['mouseleave', onLeave],
|
|
188
|
+
];
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
private teardownHoverEvents(): void {
|
|
192
|
+
const element = this.getElement() as HTMLCanvasElement | null;
|
|
193
|
+
for (const [type, handler] of this.hoverHandlers) {
|
|
194
|
+
element?.removeEventListener(type, handler);
|
|
195
|
+
}
|
|
196
|
+
this.hoverHandlers = [];
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/* —— 自动重绘:容器 / 窗口尺寸变化时防抖重绘 —— */
|
|
200
|
+
|
|
201
|
+
private setupAutoResize(): void {
|
|
202
|
+
if (this.destroyed || this.resizeDebounced) {
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
this.resizeDebounced = debounce(() => this.handleResize(), RESIZE_DEBOUNCE_MS);
|
|
206
|
+
|
|
207
|
+
const platform = detectMiniProgramGlobal();
|
|
208
|
+
if (platform) {
|
|
209
|
+
// 小程序:各平台均有窗口尺寸回调(横竖屏切换 / 分屏等场景)
|
|
210
|
+
const mp = getMiniProgramGlobal(platform) as MpWindowResizeApi | null;
|
|
211
|
+
if (mp && typeof mp.onWindowResize === 'function') {
|
|
212
|
+
this.mpResizeHandler = () => this.resizeDebounced!.run();
|
|
213
|
+
mp.onWindowResize(this.mpResizeHandler);
|
|
214
|
+
}
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Web:ResizeObserver 观察 canvas 自身(CSS 宽高 100%,容器变化即触发)。
|
|
219
|
+
// SSR / 构建期无真实元素与 ResizeObserver,跳过。
|
|
220
|
+
const element = this.getElement() as HTMLCanvasElement | null;
|
|
221
|
+
if (
|
|
222
|
+
element &&
|
|
223
|
+
typeof element.getContext === 'function' &&
|
|
224
|
+
typeof ResizeObserver === 'function'
|
|
225
|
+
) {
|
|
226
|
+
this.resizeObserver = new ResizeObserver(() => this.resizeDebounced!.run());
|
|
227
|
+
this.resizeObserver.observe(element);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
private teardownAutoResize(): void {
|
|
232
|
+
this.resizeObserver?.disconnect();
|
|
233
|
+
this.resizeObserver = null;
|
|
234
|
+
this.resizeDebounced?.cancel();
|
|
235
|
+
this.resizeDebounced = null;
|
|
236
|
+
if (this.mpResizeHandler) {
|
|
237
|
+
const platform = detectMiniProgramGlobal();
|
|
238
|
+
const mp = platform
|
|
239
|
+
? (getMiniProgramGlobal(platform) as MpWindowResizeApi | null)
|
|
240
|
+
: null;
|
|
241
|
+
if (mp && typeof mp.offWindowResize === 'function') {
|
|
242
|
+
mp.offWindowResize(this.mpResizeHandler);
|
|
243
|
+
}
|
|
244
|
+
this.mpResizeHandler = null;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
private handleResize(): void {
|
|
249
|
+
if (this.destroyed) {
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
if (detectMiniProgramGlobal()) {
|
|
253
|
+
// 小程序:窗口尺寸变化后 SelectorQuery 取到的节点尺寸已更新,
|
|
254
|
+
// 重建图表即可拿到最新尺寸(上下文持有节点实测宽高与 pixelRatio)。
|
|
255
|
+
this.chart?.destroy();
|
|
256
|
+
this.chart = null;
|
|
257
|
+
this.attachChart();
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
const element = this.getElement() as HTMLCanvasElement | null;
|
|
261
|
+
if (!element || typeof element.getContext !== 'function') {
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
const width = element.clientWidth || 0;
|
|
265
|
+
const height = element.clientHeight || 0;
|
|
266
|
+
if (width <= 0 || height <= 0) {
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
if (!this.chart) {
|
|
270
|
+
this.attachChart();
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
// 更新物理像素缓冲(逻辑尺寸 × DPR),并增量重绘(保留图表状态)
|
|
274
|
+
const dpr = detectPixelRatio();
|
|
275
|
+
element.width = Math.round(width * dpr);
|
|
276
|
+
element.height = Math.round(height * dpr);
|
|
277
|
+
this.chart.resize(width, height, dpr).render();
|
|
115
278
|
}
|
|
116
279
|
|
|
117
|
-
private resolveContext(): Promise<ChartRenderContext> {
|
|
280
|
+
private resolveContext(): Promise<ChartRenderContext | null> {
|
|
118
281
|
if (this.props.resolve) {
|
|
119
282
|
return Promise.resolve(this.props.resolve(this.getElement()));
|
|
120
283
|
}
|
|
@@ -141,9 +304,15 @@ export class TcChart extends Component<TcChartProps, TcChartState> {
|
|
|
141
304
|
return attempt(5);
|
|
142
305
|
}
|
|
143
306
|
|
|
144
|
-
// Web:canvas 元素即 HTMLCanvasElement
|
|
307
|
+
// Web:canvas 元素即 HTMLCanvasElement。SSR / 构建期静态渲染环境下
|
|
308
|
+
// 元素没有真实 2d context(如 projectDom),返回 null 跳过渲染,
|
|
309
|
+
// 由客户端水合阶段(真实浏览器)完成绘制。
|
|
310
|
+
const element = this.getElement() as HTMLCanvasElement | null;
|
|
311
|
+
if (!element || typeof element.getContext !== 'function') {
|
|
312
|
+
return Promise.resolve(null);
|
|
313
|
+
}
|
|
145
314
|
return Promise.resolve(
|
|
146
|
-
resolveWebCanvas(
|
|
315
|
+
resolveWebCanvas(element)
|
|
147
316
|
);
|
|
148
317
|
}
|
|
149
318
|
}
|
package/lib/core/chart.ts
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
|
|
13
13
|
import type { ICanvas2D } from './canvas';
|
|
14
14
|
import { AxisDrawer } from './axis';
|
|
15
|
+
import type { Box } from './layout';
|
|
15
16
|
import {
|
|
16
17
|
computeLayout,
|
|
17
18
|
type LayoutInput,
|
|
@@ -27,6 +28,8 @@ import {
|
|
|
27
28
|
type ChartRenderContext,
|
|
28
29
|
type LegendOption,
|
|
29
30
|
type TitleOption,
|
|
31
|
+
type TooltipOption,
|
|
32
|
+
type TooltipParams,
|
|
30
33
|
type ValueAxisOption,
|
|
31
34
|
} from '../types';
|
|
32
35
|
|
|
@@ -54,6 +57,12 @@ export abstract class ChartBase<T extends ChartOption> {
|
|
|
54
57
|
/** 每次 render 构建的笛卡尔比例尺,供 drawSeries 读取。 */
|
|
55
58
|
protected cartesian: CartesianScales | null = null;
|
|
56
59
|
|
|
60
|
+
/** 最近一次 render 的绘图区,供 hitTestSeries 使用。 */
|
|
61
|
+
protected plot: Box | null = null;
|
|
62
|
+
|
|
63
|
+
/** 当前悬浮命中的 tooltip 参数(由 setHover 触发 render 后绘制)。 */
|
|
64
|
+
protected hover: TooltipParams | null = null;
|
|
65
|
+
|
|
57
66
|
private destroyed = false;
|
|
58
67
|
|
|
59
68
|
public constructor(context: ChartRenderContext, option: T) {
|
|
@@ -72,9 +81,13 @@ export abstract class ChartBase<T extends ChartOption> {
|
|
|
72
81
|
return this;
|
|
73
82
|
}
|
|
74
83
|
|
|
75
|
-
public resize(width: number, height: number): this {
|
|
84
|
+
public resize(width: number, height: number, dpr?: number): this {
|
|
76
85
|
this.width = Math.max(0, width);
|
|
77
86
|
this.height = Math.max(0, height);
|
|
87
|
+
// dpr 变化(如窗口跨屏移动)时同步更新,绘制管线按新 dpr 缩放
|
|
88
|
+
if (dpr !== undefined && Number.isFinite(dpr) && dpr > 0) {
|
|
89
|
+
this.dpr = dpr;
|
|
90
|
+
}
|
|
78
91
|
return this;
|
|
79
92
|
}
|
|
80
93
|
|
|
@@ -95,6 +108,7 @@ export abstract class ChartBase<T extends ChartOption> {
|
|
|
95
108
|
}
|
|
96
109
|
|
|
97
110
|
const layout = this.computeLayout();
|
|
111
|
+
this.plot = layout.plot;
|
|
98
112
|
this.drawTitle(layout);
|
|
99
113
|
this.drawLegend(layout);
|
|
100
114
|
|
|
@@ -107,10 +121,39 @@ export abstract class ChartBase<T extends ChartOption> {
|
|
|
107
121
|
|
|
108
122
|
this.drawSeries(ctx, layout, this.option);
|
|
109
123
|
|
|
124
|
+
// tooltip 浮层最后绘制,保证在最上层
|
|
125
|
+
if (this.hover) {
|
|
126
|
+
this.drawTooltip(this.hover);
|
|
127
|
+
}
|
|
128
|
+
|
|
110
129
|
ctx.restore();
|
|
111
130
|
return this;
|
|
112
131
|
}
|
|
113
132
|
|
|
133
|
+
/* —— 悬浮交互(tooltip)—— */
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* 命中测试并显示 tooltip。坐标为逻辑像素(CSS px),由事件层换算后传入。
|
|
137
|
+
* 未命中任何数据 / tooltip 关闭时不显示。调用后自动重绘。
|
|
138
|
+
*/
|
|
139
|
+
public setHover(x: number, y: number): this {
|
|
140
|
+
if (this.option.tooltip?.show === false) {
|
|
141
|
+
this.hover = null;
|
|
142
|
+
} else {
|
|
143
|
+
this.hover = this.hitTestSeries(x, y);
|
|
144
|
+
}
|
|
145
|
+
return this.render();
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** 清除悬浮状态并重绘。 */
|
|
149
|
+
public clearHover(): this {
|
|
150
|
+
this.hover = null;
|
|
151
|
+
return this.render();
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** 子类实现:命中检测——返回 null 表示该位置无数据。 */
|
|
155
|
+
protected abstract hitTestSeries(x: number, y: number): TooltipParams | null;
|
|
156
|
+
|
|
114
157
|
public destroy(): void {
|
|
115
158
|
this.destroyed = true;
|
|
116
159
|
}
|
|
@@ -319,4 +362,126 @@ export abstract class ChartBase<T extends ChartOption> {
|
|
|
319
362
|
protected seriesColor(index: number, color?: string): string {
|
|
320
363
|
return color ?? this.palette[index % this.palette.length];
|
|
321
364
|
}
|
|
365
|
+
|
|
366
|
+
/* —— tooltip 浮层绘制 —— */
|
|
367
|
+
|
|
368
|
+
protected getTooltipOption(): TooltipOption | undefined {
|
|
369
|
+
return (this.option as { tooltip?: TooltipOption }).tooltip;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/** 默认 tooltip 文本:首行标题 + 每行一个数据项。 */
|
|
373
|
+
protected defaultTooltipLines(params: TooltipParams): string[] {
|
|
374
|
+
const lines: string[] = [params.name];
|
|
375
|
+
for (const item of params.items) {
|
|
376
|
+
lines.push(`${item.name}: ${item.value}`);
|
|
377
|
+
}
|
|
378
|
+
return lines;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/** 在 canvas 上绘制 tooltip 浮层:跟随触发点,边缘自动翻转。 */
|
|
382
|
+
protected drawTooltip(params: TooltipParams): void {
|
|
383
|
+
const option = this.getTooltipOption();
|
|
384
|
+
if (option?.show === false) {
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
const formatter = option?.formatter;
|
|
388
|
+
const raw = formatter ? formatter(params) : this.defaultTooltipLines(params);
|
|
389
|
+
const lines = Array.isArray(raw) ? raw : [raw];
|
|
390
|
+
if (lines.length === 0) {
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
const { ctx } = this;
|
|
395
|
+
const fontSize = 12;
|
|
396
|
+
const padX = 10;
|
|
397
|
+
const padY = 8;
|
|
398
|
+
const lineH = 18;
|
|
399
|
+
const dotR = 4;
|
|
400
|
+
const dotGap = 6;
|
|
401
|
+
|
|
402
|
+
applyFont(ctx, fontSize);
|
|
403
|
+
let maxW = 0;
|
|
404
|
+
for (const line of lines) {
|
|
405
|
+
maxW = Math.max(maxW, measureWidth(ctx, line, fontSize));
|
|
406
|
+
}
|
|
407
|
+
const boxW = maxW + padX * 2 + dotR * 2 + dotGap;
|
|
408
|
+
const boxH = lines.length * lineH + padY * 2;
|
|
409
|
+
|
|
410
|
+
// 优先将 tooltip 定位在绘图区外,避免遮挡数据元素
|
|
411
|
+
const plot = this.plot;
|
|
412
|
+
let bx: number;
|
|
413
|
+
let by: number;
|
|
414
|
+
|
|
415
|
+
if (plot && this.isHorizontal()) {
|
|
416
|
+
// 横向图:tooltip 定位在绘图区右侧
|
|
417
|
+
bx = plot.x + plot.width + 8;
|
|
418
|
+
by = params.y - boxH / 2;
|
|
419
|
+
// 右侧空间不足时,定位到左侧
|
|
420
|
+
if (bx + boxW > this.width - 4) {
|
|
421
|
+
bx = plot.x - boxW - 8;
|
|
422
|
+
}
|
|
423
|
+
} else if (plot) {
|
|
424
|
+
// 纵向图:tooltip 定位在触发点右侧,避免与绘图区重叠
|
|
425
|
+
bx = params.x + 14;
|
|
426
|
+
by = params.y - boxH / 2;
|
|
427
|
+
// 右侧空间不足时,定位到左侧
|
|
428
|
+
if (bx + boxW > this.width - 4) {
|
|
429
|
+
bx = params.x - boxW - 14;
|
|
430
|
+
}
|
|
431
|
+
} else {
|
|
432
|
+
// 无绘图区信息(饼图/雷达图):跟随触发点
|
|
433
|
+
bx = params.x + 14;
|
|
434
|
+
by = params.y - boxH / 2;
|
|
435
|
+
if (bx + boxW > this.width - 4) {
|
|
436
|
+
bx = params.x - boxW - 14;
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
bx = Math.max(4, bx);
|
|
441
|
+
by = Math.max(4, Math.min(by, this.height - boxH - 4));
|
|
442
|
+
|
|
443
|
+
ctx.save();
|
|
444
|
+
// 背景
|
|
445
|
+
ctx.fillStyle = 'rgba(50, 50, 51, 0.92)';
|
|
446
|
+
ctx.beginPath();
|
|
447
|
+
this.roundRectPath(bx, by, boxW, boxH, 6);
|
|
448
|
+
ctx.fill();
|
|
449
|
+
|
|
450
|
+
// 文本行:数据行前画系列色小圆点
|
|
451
|
+
ctx.textBaseline = 'middle';
|
|
452
|
+
ctx.textAlign = 'left';
|
|
453
|
+
lines.forEach((line, i) => {
|
|
454
|
+
const ty = by + padY + lineH * i + lineH / 2;
|
|
455
|
+
const dotColor = i > 0 ? params.items[i - 1]?.color : undefined;
|
|
456
|
+
if (dotColor) {
|
|
457
|
+
ctx.fillStyle = dotColor;
|
|
458
|
+
ctx.beginPath();
|
|
459
|
+
ctx.arc(bx + padX + dotR, ty, dotR, 0, Math.PI * 2);
|
|
460
|
+
ctx.fill();
|
|
461
|
+
}
|
|
462
|
+
ctx.fillStyle = '#ffffff';
|
|
463
|
+
const tx = bx + padX + (dotColor ? dotR * 2 + dotGap : 0);
|
|
464
|
+
ctx.fillText(line, tx, ty);
|
|
465
|
+
});
|
|
466
|
+
ctx.restore();
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
/** 圆角矩形路径(当前路径),与平台无关。 */
|
|
470
|
+
private roundRectPath(
|
|
471
|
+
x: number,
|
|
472
|
+
y: number,
|
|
473
|
+
w: number,
|
|
474
|
+
h: number,
|
|
475
|
+
r: number
|
|
476
|
+
): void {
|
|
477
|
+
const ctx = this.ctx;
|
|
478
|
+
const radius = Math.min(r, w / 2, h / 2);
|
|
479
|
+
ctx.moveTo(x + radius, y);
|
|
480
|
+
ctx.lineTo(x + w - radius, y);
|
|
481
|
+
ctx.arcTo(x + w, y, x + w, y + h, radius);
|
|
482
|
+
ctx.arcTo(x + w, y + h, x, y + h, radius);
|
|
483
|
+
ctx.arcTo(x, y + h, x, y, radius);
|
|
484
|
+
ctx.arcTo(x, y, x + w, y, radius);
|
|
485
|
+
ctx.closePath();
|
|
486
|
+
}
|
|
322
487
|
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 零依赖防抖工具。
|
|
3
|
+
*
|
|
4
|
+
* 用于高频触发场景(如窗口 / 容器尺寸变化)的合并调度:
|
|
5
|
+
* 连续调用只会在停止触发 delay 毫秒后执行一次。
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export interface Debounced<T extends (...args: never[]) => unknown> {
|
|
9
|
+
/** 触发一次;若已有待执行调用则重置计时器(合并连续触发)。 */
|
|
10
|
+
run: (...args: Parameters<T>) => void;
|
|
11
|
+
/** 取消尚未执行的调用。 */
|
|
12
|
+
cancel: () => void;
|
|
13
|
+
/** 是否已有待执行的调用。 */
|
|
14
|
+
pending: () => boolean;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
type AnyFunction = (...args: any[]) => unknown;
|
|
18
|
+
|
|
19
|
+
export function debounce<T extends AnyFunction>(
|
|
20
|
+
fn: T,
|
|
21
|
+
delay: number
|
|
22
|
+
): Debounced<T> {
|
|
23
|
+
let timer: ReturnType<typeof setTimeout> | null = null;
|
|
24
|
+
let lastArgs: Parameters<T> | null = null;
|
|
25
|
+
|
|
26
|
+
return {
|
|
27
|
+
run(...args: Parameters<T>): void {
|
|
28
|
+
lastArgs = args;
|
|
29
|
+
if (timer !== null) {
|
|
30
|
+
clearTimeout(timer);
|
|
31
|
+
}
|
|
32
|
+
timer = setTimeout(() => {
|
|
33
|
+
timer = null;
|
|
34
|
+
if (lastArgs !== null) {
|
|
35
|
+
const runArgs = lastArgs;
|
|
36
|
+
lastArgs = null;
|
|
37
|
+
fn(...runArgs);
|
|
38
|
+
}
|
|
39
|
+
}, delay);
|
|
40
|
+
},
|
|
41
|
+
cancel(): void {
|
|
42
|
+
if (timer !== null) {
|
|
43
|
+
clearTimeout(timer);
|
|
44
|
+
timer = null;
|
|
45
|
+
}
|
|
46
|
+
lastArgs = null;
|
|
47
|
+
},
|
|
48
|
+
pending(): boolean {
|
|
49
|
+
return timer !== null;
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
}
|
package/lib/index.ts
CHANGED
|
@@ -30,6 +30,10 @@ export type { LinearScaleOptions, ScaleResult } from './core/scale';
|
|
|
30
30
|
export { computeLayout } from './core/layout';
|
|
31
31
|
export type { Box, LayoutInput, LayoutResult, Padding } from './core/layout';
|
|
32
32
|
|
|
33
|
+
// 工具:防抖(自动重绘等高频触发场景)
|
|
34
|
+
export { debounce } from './core/debounce';
|
|
35
|
+
export type { Debounced } from './core/debounce';
|
|
36
|
+
|
|
33
37
|
// 图表基类
|
|
34
38
|
export { ChartBase } from './core/chart';
|
|
35
39
|
export type { CartesianScales } from './core/chart';
|
package/lib/types.ts
CHANGED
|
@@ -81,6 +81,36 @@ export interface CategoryAxisOption {
|
|
|
81
81
|
showGrid?: boolean;
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
+
/** —— Tooltip(悬浮提示)—— */
|
|
85
|
+
|
|
86
|
+
/** tooltip 中一条数据(一个系列 / 一个扇区)。 */
|
|
87
|
+
export interface TooltipDatum {
|
|
88
|
+
/** 系列名 / 扇区名。 */
|
|
89
|
+
name: string;
|
|
90
|
+
/** 显示值(已格式化)。 */
|
|
91
|
+
value: number | string;
|
|
92
|
+
/** 色块颜色,用于 tooltip 前的小圆点。 */
|
|
93
|
+
color?: string;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** tooltip 触发时命中的一组数据。 */
|
|
97
|
+
export interface TooltipParams {
|
|
98
|
+
/** 触发位置(逻辑像素)。 */
|
|
99
|
+
x: number;
|
|
100
|
+
y: number;
|
|
101
|
+
/** 类目名 / 扇区名。 */
|
|
102
|
+
name: string;
|
|
103
|
+
/** 该类目 / 扇区下的所有数据项。 */
|
|
104
|
+
items: readonly TooltipDatum[];
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export interface TooltipOption {
|
|
108
|
+
/** 是否显示 tooltip,默认 true。 */
|
|
109
|
+
show?: boolean;
|
|
110
|
+
/** 自定义展示内容;返回文本行数组。缺省自动生成(首行标题 + 每行一个色块数据项)。 */
|
|
111
|
+
formatter?: (params: TooltipParams) => string | readonly string[];
|
|
112
|
+
}
|
|
113
|
+
|
|
84
114
|
/** —— 折线图 —— */
|
|
85
115
|
|
|
86
116
|
export interface LineSeries {
|
|
@@ -105,6 +135,8 @@ export interface LineChartOption {
|
|
|
105
135
|
series: readonly LineSeries[];
|
|
106
136
|
/** 数值轴是否从 0 开始(数据全为正时强制含 0),默认 false(按数据范围紧凑显示)。 */
|
|
107
137
|
startFromZero?: boolean;
|
|
138
|
+
/** 悬浮提示配置;默认开启,可 tooltip: { show: false } 关闭。 */
|
|
139
|
+
tooltip?: TooltipOption;
|
|
108
140
|
/** 背景色(如 'rgba(22,119,255,0.06)'),默认无。 */
|
|
109
141
|
backgroundColor?: string;
|
|
110
142
|
}
|
|
@@ -132,6 +164,8 @@ export interface BarChartOption {
|
|
|
132
164
|
series: readonly BarSeries[];
|
|
133
165
|
/** 横向柱状图(类目轴转纵向、数值轴转横向),默认 false。 */
|
|
134
166
|
horizontal?: boolean;
|
|
167
|
+
/** 悬浮提示配置;默认开启,可 tooltip: { show: false } 关闭。 */
|
|
168
|
+
tooltip?: TooltipOption;
|
|
135
169
|
backgroundColor?: string;
|
|
136
170
|
}
|
|
137
171
|
|
|
@@ -154,10 +188,20 @@ export interface PieChartOption {
|
|
|
154
188
|
innerRadius?: number | string;
|
|
155
189
|
/** 是否显示扇区标签(名称 + 百分比),默认 true。 */
|
|
156
190
|
showLabel?: boolean;
|
|
191
|
+
/** 标签位置:'inside' 画在扇区内(默认,白字);'outside' 画在扇区外并带引线(适合小扇区 / 长名称)。 */
|
|
192
|
+
labelPosition?: 'inside' | 'outside';
|
|
193
|
+
/** 外部标签引线沿平分线的长度(px),仅 labelPosition: 'outside' 生效,默认 14。 */
|
|
194
|
+
labelLineLength?: number;
|
|
195
|
+
/** 外部标签与引线端点的水平间距(px),仅 labelPosition: 'outside' 生效,默认 6。 */
|
|
196
|
+
labelGap?: number;
|
|
197
|
+
/** 外部标签引线颜色,默认 '#c0c4cc'。 */
|
|
198
|
+
labelLineColor?: string;
|
|
157
199
|
labelFontSize?: number;
|
|
158
200
|
labelColor?: string;
|
|
159
201
|
/** 起始角(弧度),默认 -Math.PI / 2(12 点方向),顺时针。 */
|
|
160
202
|
startAngle?: number;
|
|
203
|
+
/** 悬浮提示配置;默认开启,可 tooltip: { show: false } 关闭。 */
|
|
204
|
+
tooltip?: TooltipOption;
|
|
161
205
|
backgroundColor?: string;
|
|
162
206
|
}
|
|
163
207
|
|
|
@@ -194,6 +238,8 @@ export interface RadarChartOption {
|
|
|
194
238
|
radius?: number;
|
|
195
239
|
/** 起始角(弧度),默认 -Math.PI / 2(12 点方向),顺时针。 */
|
|
196
240
|
startAngle?: number;
|
|
241
|
+
/** 悬浮提示配置;默认开启,可 tooltip: { show: false } 关闭。 */
|
|
242
|
+
tooltip?: TooltipOption;
|
|
197
243
|
backgroundColor?: string;
|
|
198
244
|
}
|
|
199
245
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "transone-chart",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "TransOne 跨端图表库:一份 TypeScript 源码,基于 Canvas 2D 渲染,兼容 Web 与多端小程序,预留原生 App(iOS/Android/鸿蒙)扩展",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
"transone": ">=0.3.0"
|
|
27
27
|
},
|
|
28
28
|
"devDependencies": {
|
|
29
|
-
"transone": "0.
|
|
29
|
+
"transone": "0.4.0"
|
|
30
30
|
},
|
|
31
31
|
"keywords": [
|
|
32
32
|
"transone",
|