transone-chart 0.1.4 → 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 +3 -2
- 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 +11 -1
- package/dist/core/chart.d.ts +22 -1
- package/dist/index.js +4 -4
- package/dist/index.js.map +10 -10
- package/dist/types.d.ts +34 -0
- package/lib/charts/bar.ts +83 -2
- package/lib/charts/line.ts +62 -3
- package/lib/charts/pie.ts +56 -1
- package/lib/charts/radar.ts +52 -1
- package/lib/component.ts +65 -3
- package/lib/core/chart.ts +161 -0
- package/lib/types.ts +38 -0
- package/package.json +2 -2
package/dist/types.d.ts
CHANGED
|
@@ -61,6 +61,32 @@ export interface CategoryAxisOption {
|
|
|
61
61
|
/** 是否显示类目网格竖线,默认 false。 */
|
|
62
62
|
showGrid?: boolean;
|
|
63
63
|
}
|
|
64
|
+
/** —— Tooltip(悬浮提示)—— */
|
|
65
|
+
/** tooltip 中一条数据(一个系列 / 一个扇区)。 */
|
|
66
|
+
export interface TooltipDatum {
|
|
67
|
+
/** 系列名 / 扇区名。 */
|
|
68
|
+
name: string;
|
|
69
|
+
/** 显示值(已格式化)。 */
|
|
70
|
+
value: number | string;
|
|
71
|
+
/** 色块颜色,用于 tooltip 前的小圆点。 */
|
|
72
|
+
color?: string;
|
|
73
|
+
}
|
|
74
|
+
/** tooltip 触发时命中的一组数据。 */
|
|
75
|
+
export interface TooltipParams {
|
|
76
|
+
/** 触发位置(逻辑像素)。 */
|
|
77
|
+
x: number;
|
|
78
|
+
y: number;
|
|
79
|
+
/** 类目名 / 扇区名。 */
|
|
80
|
+
name: string;
|
|
81
|
+
/** 该类目 / 扇区下的所有数据项。 */
|
|
82
|
+
items: readonly TooltipDatum[];
|
|
83
|
+
}
|
|
84
|
+
export interface TooltipOption {
|
|
85
|
+
/** 是否显示 tooltip,默认 true。 */
|
|
86
|
+
show?: boolean;
|
|
87
|
+
/** 自定义展示内容;返回文本行数组。缺省自动生成(首行标题 + 每行一个色块数据项)。 */
|
|
88
|
+
formatter?: (params: TooltipParams) => string | readonly string[];
|
|
89
|
+
}
|
|
64
90
|
/** —— 折线图 —— */
|
|
65
91
|
export interface LineSeries {
|
|
66
92
|
name?: string;
|
|
@@ -83,6 +109,8 @@ export interface LineChartOption {
|
|
|
83
109
|
series: readonly LineSeries[];
|
|
84
110
|
/** 数值轴是否从 0 开始(数据全为正时强制含 0),默认 false(按数据范围紧凑显示)。 */
|
|
85
111
|
startFromZero?: boolean;
|
|
112
|
+
/** 悬浮提示配置;默认开启,可 tooltip: { show: false } 关闭。 */
|
|
113
|
+
tooltip?: TooltipOption;
|
|
86
114
|
/** 背景色(如 'rgba(22,119,255,0.06)'),默认无。 */
|
|
87
115
|
backgroundColor?: string;
|
|
88
116
|
}
|
|
@@ -107,6 +135,8 @@ export interface BarChartOption {
|
|
|
107
135
|
series: readonly BarSeries[];
|
|
108
136
|
/** 横向柱状图(类目轴转纵向、数值轴转横向),默认 false。 */
|
|
109
137
|
horizontal?: boolean;
|
|
138
|
+
/** 悬浮提示配置;默认开启,可 tooltip: { show: false } 关闭。 */
|
|
139
|
+
tooltip?: TooltipOption;
|
|
110
140
|
backgroundColor?: string;
|
|
111
141
|
}
|
|
112
142
|
/** —— 饼图 —— */
|
|
@@ -138,6 +168,8 @@ export interface PieChartOption {
|
|
|
138
168
|
labelColor?: string;
|
|
139
169
|
/** 起始角(弧度),默认 -Math.PI / 2(12 点方向),顺时针。 */
|
|
140
170
|
startAngle?: number;
|
|
171
|
+
/** 悬浮提示配置;默认开启,可 tooltip: { show: false } 关闭。 */
|
|
172
|
+
tooltip?: TooltipOption;
|
|
141
173
|
backgroundColor?: string;
|
|
142
174
|
}
|
|
143
175
|
/** —— 雷达图 —— */
|
|
@@ -170,6 +202,8 @@ export interface RadarChartOption {
|
|
|
170
202
|
radius?: number;
|
|
171
203
|
/** 起始角(弧度),默认 -Math.PI / 2(12 点方向),顺时针。 */
|
|
172
204
|
startAngle?: number;
|
|
205
|
+
/** 悬浮提示配置;默认开启,可 tooltip: { show: false } 关闭。 */
|
|
206
|
+
tooltip?: TooltipOption;
|
|
173
207
|
backgroundColor?: string;
|
|
174
208
|
}
|
|
175
209
|
export type ChartOption = LineChartOption | BarChartOption | PieChartOption | RadarChartOption;
|
package/lib/charts/bar.ts
CHANGED
|
@@ -12,7 +12,7 @@ import { ChartBase } from '../core/chart';
|
|
|
12
12
|
import type { CategoryScale } from '../core/scale';
|
|
13
13
|
import type { LayoutResult } from '../core/layout';
|
|
14
14
|
import type { LegendItem } from '../core/legend';
|
|
15
|
-
import type { BarChartOption, BarSeries } from '../types';
|
|
15
|
+
import type { BarChartOption, BarSeries, TooltipParams } from '../types';
|
|
16
16
|
|
|
17
17
|
/** 柱体在类目带内的水平偏移与宽度(纵向)或垂直偏移与高度(横向)。 */
|
|
18
18
|
interface Slot {
|
|
@@ -95,6 +95,7 @@ export class BarChart extends ChartBase<BarChartOption> {
|
|
|
95
95
|
|
|
96
96
|
const stackGroups = groupStacks(option.series);
|
|
97
97
|
const categoryCount = option.xAxis.labels.length;
|
|
98
|
+
const hoverIndex = this.getHoverCategoryIndex();
|
|
98
99
|
|
|
99
100
|
for (let i = 0; i < categoryCount; i += 1) {
|
|
100
101
|
// 每个类目带内:无堆叠时按系列分组并排;有堆叠时按 stack 组并排
|
|
@@ -104,6 +105,7 @@ export class BarChart extends ChartBase<BarChartOption> {
|
|
|
104
105
|
.map((s) => s.barWidth)
|
|
105
106
|
.find((w) => w !== undefined && w > 0);
|
|
106
107
|
const slot = this.computeSlot(category, seriesCount, explicitWidth);
|
|
108
|
+
const isHovered = hoverIndex === i;
|
|
107
109
|
|
|
108
110
|
groups.forEach((group, groupIndex) => {
|
|
109
111
|
const slotOffset = slot.offset + groupIndex * slot.size;
|
|
@@ -111,10 +113,11 @@ export class BarChart extends ChartBase<BarChartOption> {
|
|
|
111
113
|
|
|
112
114
|
group.forEach((series) => {
|
|
113
115
|
const raw = series.data[i] ?? 0;
|
|
114
|
-
const
|
|
116
|
+
const baseColor = this.seriesColor(
|
|
115
117
|
option.series.indexOf(series),
|
|
116
118
|
series.color
|
|
117
119
|
);
|
|
120
|
+
const color = isHovered ? lightenColor(baseColor, 0.25) : baseColor;
|
|
118
121
|
|
|
119
122
|
if (horizontal) {
|
|
120
123
|
const x0 = value.scale(0);
|
|
@@ -154,6 +157,46 @@ export class BarChart extends ChartBase<BarChartOption> {
|
|
|
154
157
|
}
|
|
155
158
|
}
|
|
156
159
|
|
|
160
|
+
/** 命中检测:点落在某类目的柱体带内即显示该类目所有系列值。 */
|
|
161
|
+
protected hitTestSeries(x: number, y: number): TooltipParams | null {
|
|
162
|
+
const scales = this.cartesian;
|
|
163
|
+
if (!scales) {
|
|
164
|
+
return null;
|
|
165
|
+
}
|
|
166
|
+
const horizontal = this.option.horizontal ?? false;
|
|
167
|
+
const labels = this.getCategoryLabels();
|
|
168
|
+
|
|
169
|
+
// 命中整个类目带(含柱间间隙):鼠标在类目带内即命中,避免间隙处 tooltip 消失
|
|
170
|
+
for (let i = 0; i < labels.length; i += 1) {
|
|
171
|
+
const start = scales.category.bandStart(i);
|
|
172
|
+
const band = scales.category.bandWidth;
|
|
173
|
+
const hit = horizontal
|
|
174
|
+
? y >= start && y <= start + band
|
|
175
|
+
: x >= start && x <= start + band;
|
|
176
|
+
if (!hit) {
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
// 该类目下所有系列(堆叠时为各段值)
|
|
180
|
+
const items = this.option.series.map((s, si) => ({
|
|
181
|
+
name: s.name ?? `系列${si + 1}`,
|
|
182
|
+
value: s.data[i] ?? 0,
|
|
183
|
+
color: this.seriesColor(si, s.color),
|
|
184
|
+
}));
|
|
185
|
+
return { x, y, name: labels[i], items };
|
|
186
|
+
}
|
|
187
|
+
return null;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** 从 hover 状态解析命中的类目索引,未命中返回 -1。 */
|
|
191
|
+
private getHoverCategoryIndex(): number {
|
|
192
|
+
const hover = this.hover;
|
|
193
|
+
if (!hover) {
|
|
194
|
+
return -1;
|
|
195
|
+
}
|
|
196
|
+
const labels = this.getCategoryLabels();
|
|
197
|
+
return labels.indexOf(hover.name);
|
|
198
|
+
}
|
|
199
|
+
|
|
157
200
|
private computeSlot(
|
|
158
201
|
category: CategoryScale,
|
|
159
202
|
groupCount: number,
|
|
@@ -274,3 +317,41 @@ function roundRectPath(
|
|
|
274
317
|
}
|
|
275
318
|
ctx.closePath();
|
|
276
319
|
}
|
|
320
|
+
|
|
321
|
+
/** 将颜色向白色方向混合 amount(0~1),支持 #rrggbb 与 rgba()。 */
|
|
322
|
+
function lightenColor(color: string, amount: number): string {
|
|
323
|
+
const a = Math.max(0, Math.min(1, amount));
|
|
324
|
+
|
|
325
|
+
// #rrggbb / #rgb
|
|
326
|
+
const hex = color.match(/^#([0-9a-f]{3,8})$/i);
|
|
327
|
+
if (hex) {
|
|
328
|
+
let r: number;
|
|
329
|
+
let g: number;
|
|
330
|
+
let b: number;
|
|
331
|
+
const h = hex[1];
|
|
332
|
+
if (h.length === 3) {
|
|
333
|
+
r = parseInt(h[0] + h[0], 16);
|
|
334
|
+
g = parseInt(h[1] + h[1], 16);
|
|
335
|
+
b = parseInt(h[2] + h[2], 16);
|
|
336
|
+
} else {
|
|
337
|
+
r = parseInt(h.slice(0, 2), 16);
|
|
338
|
+
g = parseInt(h.slice(2, 4), 16);
|
|
339
|
+
b = parseInt(h.slice(4, 6), 16);
|
|
340
|
+
}
|
|
341
|
+
r = Math.round(r + (255 - r) * a);
|
|
342
|
+
g = Math.round(g + (255 - g) * a);
|
|
343
|
+
b = Math.round(b + (255 - b) * a);
|
|
344
|
+
return `rgb(${r}, ${g}, ${b})`;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// rgba(r, g, b, a?) / rgb(r, g, b)
|
|
348
|
+
const rgba = color.match(/rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)/);
|
|
349
|
+
if (rgba) {
|
|
350
|
+
const r = Math.round(parseInt(rgba[1]) + (255 - parseInt(rgba[1])) * a);
|
|
351
|
+
const g = Math.round(parseInt(rgba[2]) + (255 - parseInt(rgba[2])) * a);
|
|
352
|
+
const b = Math.round(parseInt(rgba[3]) + (255 - parseInt(rgba[3])) * a);
|
|
353
|
+
return `rgb(${r}, ${g}, ${b})`;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
return color;
|
|
357
|
+
}
|
package/lib/charts/line.ts
CHANGED
|
@@ -7,7 +7,7 @@ import type { ICanvas2D } from '../core/canvas';
|
|
|
7
7
|
import { ChartBase } from '../core/chart';
|
|
8
8
|
import type { LayoutResult } from '../core/layout';
|
|
9
9
|
import type { LegendItem } from '../core/legend';
|
|
10
|
-
import type { LineChartOption } from '../types';
|
|
10
|
+
import type { LineChartOption, TooltipParams } from '../types';
|
|
11
11
|
|
|
12
12
|
interface Point {
|
|
13
13
|
x: number;
|
|
@@ -68,6 +68,7 @@ export class LineChart extends ChartBase<LineChartOption> {
|
|
|
68
68
|
}
|
|
69
69
|
const { plot } = layout;
|
|
70
70
|
const { value, category } = scales;
|
|
71
|
+
const hoverIndex = this.getHoverCategoryIndex();
|
|
71
72
|
|
|
72
73
|
const pointsPerSeries: Point[][] = option.series.map((series) =>
|
|
73
74
|
series.data.map((d, i) => ({
|
|
@@ -76,6 +77,20 @@ export class LineChart extends ChartBase<LineChartOption> {
|
|
|
76
77
|
}))
|
|
77
78
|
);
|
|
78
79
|
|
|
80
|
+
// 悬浮参考线:在命中类目中心画一条纵向虚线
|
|
81
|
+
if (hoverIndex >= 0) {
|
|
82
|
+
const hx = category.center(hoverIndex);
|
|
83
|
+
ctx.save();
|
|
84
|
+
ctx.strokeStyle = '#c8c9cc';
|
|
85
|
+
ctx.lineWidth = 1;
|
|
86
|
+
ctx.setLineDash([4, 3]);
|
|
87
|
+
ctx.beginPath();
|
|
88
|
+
ctx.moveTo(hx, plot.y);
|
|
89
|
+
ctx.lineTo(hx, plot.y + plot.height);
|
|
90
|
+
ctx.stroke();
|
|
91
|
+
ctx.restore();
|
|
92
|
+
}
|
|
93
|
+
|
|
79
94
|
option.series.forEach((series, index) => {
|
|
80
95
|
const points = pointsPerSeries[index];
|
|
81
96
|
if (points.length === 0) {
|
|
@@ -111,11 +126,14 @@ export class LineChart extends ChartBase<LineChartOption> {
|
|
|
111
126
|
|
|
112
127
|
// 数据点标记
|
|
113
128
|
if (series.showSymbol !== false) {
|
|
129
|
+
const baseR = Math.max(2.5, lineWidth + 0.5);
|
|
114
130
|
ctx.save();
|
|
115
131
|
ctx.fillStyle = color;
|
|
116
|
-
for (
|
|
132
|
+
for (let i = 0; i < points.length; i += 1) {
|
|
133
|
+
const p = points[i];
|
|
134
|
+
const r = i === hoverIndex ? baseR * 1.6 : baseR;
|
|
117
135
|
ctx.beginPath();
|
|
118
|
-
ctx.arc(p.x, p.y,
|
|
136
|
+
ctx.arc(p.x, p.y, r, 0, Math.PI * 2);
|
|
119
137
|
ctx.fill();
|
|
120
138
|
}
|
|
121
139
|
ctx.restore();
|
|
@@ -123,6 +141,47 @@ export class LineChart extends ChartBase<LineChartOption> {
|
|
|
123
141
|
});
|
|
124
142
|
}
|
|
125
143
|
|
|
144
|
+
/** 命中检测:x 落在某类目带内即显示该类目所有系列值(y 在绘图区内即可)。 */
|
|
145
|
+
protected hitTestSeries(x: number, y: number): TooltipParams | null {
|
|
146
|
+
const scales = this.cartesian;
|
|
147
|
+
const plot = this.plot;
|
|
148
|
+
if (!scales || !plot) {
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
if (y < plot.y || y > plot.y + plot.height) {
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
154
|
+
const labels = this.option.xAxis.labels;
|
|
155
|
+
let best = -1;
|
|
156
|
+
let bestDist = Infinity;
|
|
157
|
+
for (let i = 0; i < labels.length; i += 1) {
|
|
158
|
+
const d = Math.abs(scales.category.center(i) - x);
|
|
159
|
+
if (d < bestDist) {
|
|
160
|
+
bestDist = d;
|
|
161
|
+
best = i;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
if (best < 0 || bestDist > scales.category.bandWidth / 2) {
|
|
165
|
+
return null;
|
|
166
|
+
}
|
|
167
|
+
const items = this.option.series.map((s, i) => ({
|
|
168
|
+
name: s.name ?? `系列${i + 1}`,
|
|
169
|
+
value: s.data[best] ?? 0,
|
|
170
|
+
color: this.seriesColor(i, s.color),
|
|
171
|
+
}));
|
|
172
|
+
return { x, y, name: labels[best], items };
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** 从 hover 状态解析命中的类目索引,未命中返回 -1。 */
|
|
176
|
+
private getHoverCategoryIndex(): number {
|
|
177
|
+
const hover = this.hover;
|
|
178
|
+
if (!hover) {
|
|
179
|
+
return -1;
|
|
180
|
+
}
|
|
181
|
+
const labels = this.option.xAxis.labels;
|
|
182
|
+
return labels.indexOf(hover.name);
|
|
183
|
+
}
|
|
184
|
+
|
|
126
185
|
/** 折线路径:直线或 Catmull-Rom 平滑插值(三次贝塞尔)。 */
|
|
127
186
|
private tracePath(ctx: ICanvas2D, points: readonly Point[], smooth: boolean): void {
|
|
128
187
|
if (points.length === 0) {
|
package/lib/charts/pie.ts
CHANGED
|
@@ -8,7 +8,7 @@ import { ChartBase } from '../core/chart';
|
|
|
8
8
|
import type { LayoutResult } from '../core/layout';
|
|
9
9
|
import type { LegendItem } from '../core/legend';
|
|
10
10
|
import { applyFont } from '../core/text';
|
|
11
|
-
import type { PieChartOption } from '../types';
|
|
11
|
+
import type { PieChartOption, TooltipParams } from '../types';
|
|
12
12
|
|
|
13
13
|
const TWO_PI = Math.PI * 2;
|
|
14
14
|
|
|
@@ -24,6 +24,61 @@ export class PieChart extends ChartBase<PieChartOption> {
|
|
|
24
24
|
}));
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
+
/** 命中检测:点落在某扇区(环形环带内)即显示该扇区名与数值占比。 */
|
|
28
|
+
protected hitTestSeries(x: number, y: number): TooltipParams | null {
|
|
29
|
+
const plot = this.plot;
|
|
30
|
+
if (!plot) {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
const centerX = plot.x + plot.width / 2;
|
|
34
|
+
const centerY = plot.y + plot.height / 2;
|
|
35
|
+
const maxRadius = Math.min(plot.width, plot.height) / 2;
|
|
36
|
+
const outerRadius = resolveRadius(this.option.radius ?? '60%', maxRadius);
|
|
37
|
+
const innerRadius = resolveRadius(this.option.innerRadius ?? 0, maxRadius);
|
|
38
|
+
const startAngle = this.option.startAngle ?? -Math.PI / 2;
|
|
39
|
+
|
|
40
|
+
const dx = x - centerX;
|
|
41
|
+
const dy = y - centerY;
|
|
42
|
+
const dist = Math.hypot(dx, dy);
|
|
43
|
+
if (dist < innerRadius || dist > outerRadius) {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const data = this.option.data.filter((d) => d.value > 0);
|
|
48
|
+
const total = data.reduce((sum, d) => sum + d.value, 0);
|
|
49
|
+
if (total <= 0) {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// 命中角(canvas 系 y 向下,与绘制时角度递增方向一致)
|
|
54
|
+
let a = Math.atan2(dy, dx);
|
|
55
|
+
while (a < startAngle) {
|
|
56
|
+
a += TWO_PI;
|
|
57
|
+
}
|
|
58
|
+
let cur = startAngle;
|
|
59
|
+
for (let i = 0; i < data.length; i += 1) {
|
|
60
|
+
const d = data[i]!;
|
|
61
|
+
const sweep = (d.value / total) * TWO_PI;
|
|
62
|
+
if (a >= cur && a <= cur + sweep) {
|
|
63
|
+
const percent = Math.round((d.value / total) * 100);
|
|
64
|
+
return {
|
|
65
|
+
x,
|
|
66
|
+
y,
|
|
67
|
+
name: d.name,
|
|
68
|
+
items: [
|
|
69
|
+
{
|
|
70
|
+
name: d.name,
|
|
71
|
+
value: `${percent}%`,
|
|
72
|
+
color: d.color ?? this.seriesColor(i),
|
|
73
|
+
},
|
|
74
|
+
],
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
cur += sweep;
|
|
78
|
+
}
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
|
|
27
82
|
protected drawSeries(
|
|
28
83
|
ctx: ICanvas2D,
|
|
29
84
|
layout: LayoutResult,
|
package/lib/charts/radar.ts
CHANGED
|
@@ -8,7 +8,7 @@ import { ChartBase } from '../core/chart';
|
|
|
8
8
|
import type { LayoutResult } from '../core/layout';
|
|
9
9
|
import type { LegendItem } from '../core/legend';
|
|
10
10
|
import { applyFont } from '../core/text';
|
|
11
|
-
import type { RadarChartOption } from '../types';
|
|
11
|
+
import type { RadarChartOption, TooltipParams } from '../types';
|
|
12
12
|
|
|
13
13
|
interface PolarPoint {
|
|
14
14
|
x: number;
|
|
@@ -141,6 +141,57 @@ export class RadarChart extends ChartBase<RadarChartOption> {
|
|
|
141
141
|
});
|
|
142
142
|
}
|
|
143
143
|
|
|
144
|
+
/** 命中检测:靠近任一数据顶点(阈值 24px)即显示该指标下所有系列值。 */
|
|
145
|
+
protected hitTestSeries(x: number, y: number): TooltipParams | null {
|
|
146
|
+
const plot = this.plot;
|
|
147
|
+
if (!plot) {
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
const centerX = plot.x + plot.width / 2;
|
|
151
|
+
const centerY = plot.y + plot.height / 2;
|
|
152
|
+
const radius =
|
|
153
|
+
this.option.radius ?? (Math.min(plot.width, plot.height) / 2) * 0.6;
|
|
154
|
+
const count = this.option.indicators.length;
|
|
155
|
+
if (count === 0) {
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
const startAngle = this.option.startAngle ?? -Math.PI / 2;
|
|
159
|
+
const step = TWO_PI / count;
|
|
160
|
+
|
|
161
|
+
let best = -1;
|
|
162
|
+
let bestDist = 24; // 命中阈值(px)
|
|
163
|
+
for (let i = 0; i < count; i += 1) {
|
|
164
|
+
const max =
|
|
165
|
+
this.option.indicators[i].max ?? this.indicatorMax(this.option, i);
|
|
166
|
+
const angle = startAngle + i * step;
|
|
167
|
+
for (let si = 0; si < this.option.series.length; si += 1) {
|
|
168
|
+
const value = this.option.series[si].data[i] ?? 0;
|
|
169
|
+
const ratio = max > 0 ? Math.max(0, Math.min(1, value / max)) : 0;
|
|
170
|
+
const px = centerX + Math.cos(angle) * radius * ratio;
|
|
171
|
+
const py = centerY + Math.sin(angle) * radius * ratio;
|
|
172
|
+
const d = Math.hypot(x - px, y - py);
|
|
173
|
+
if (d < bestDist) {
|
|
174
|
+
bestDist = d;
|
|
175
|
+
best = i;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
if (best < 0) {
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
const items = this.option.series.map((s, si) => ({
|
|
183
|
+
name: s.name ?? `系列${si + 1}`,
|
|
184
|
+
value: s.data[best] ?? 0,
|
|
185
|
+
color: this.seriesColor(si, s.color),
|
|
186
|
+
}));
|
|
187
|
+
return {
|
|
188
|
+
x,
|
|
189
|
+
y,
|
|
190
|
+
name: this.option.indicators[best]!.name,
|
|
191
|
+
items,
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
|
|
144
195
|
private indicatorMax(option: RadarChartOption, index: number): number {
|
|
145
196
|
let max = 0;
|
|
146
197
|
for (const series of option.series) {
|
package/lib/component.ts
CHANGED
|
@@ -62,6 +62,8 @@ export class TcChart extends Component<TcChartProps, TcChartState> {
|
|
|
62
62
|
private resizeDebounced: Debounced<() => void> | null = null;
|
|
63
63
|
private resizeObserver: ResizeObserver | null = null;
|
|
64
64
|
private mpResizeHandler: (() => void) | null = null;
|
|
65
|
+
/** Web 端 hover 事件句柄(mousemove / mouseleave),卸载时移除。 */
|
|
66
|
+
private hoverHandlers: Array<[string, (e: Event) => void]> = [];
|
|
65
67
|
|
|
66
68
|
protected initState(): TcChartState {
|
|
67
69
|
return { canvasId: CANVAS_ID };
|
|
@@ -89,6 +91,7 @@ export class TcChart extends Component<TcChartProps, TcChartState> {
|
|
|
89
91
|
protected onMounted(): void {
|
|
90
92
|
this.attachChart();
|
|
91
93
|
this.setupAutoResize();
|
|
94
|
+
this.setupHoverEvents();
|
|
92
95
|
}
|
|
93
96
|
|
|
94
97
|
protected onUpdated(): void {
|
|
@@ -101,6 +104,7 @@ export class TcChart extends Component<TcChartProps, TcChartState> {
|
|
|
101
104
|
|
|
102
105
|
protected onUnmounted(): void {
|
|
103
106
|
this.teardownAutoResize();
|
|
107
|
+
this.teardownHoverEvents();
|
|
104
108
|
this.destroyed = true;
|
|
105
109
|
this.chart?.destroy();
|
|
106
110
|
this.chart = null;
|
|
@@ -129,9 +133,67 @@ export class TcChart extends Component<TcChartProps, TcChartState> {
|
|
|
129
133
|
});
|
|
130
134
|
}
|
|
131
135
|
|
|
132
|
-
/**
|
|
136
|
+
/**
|
|
137
|
+
* Web 端返回根 canvas;小程序端此方法不会被调用(走 SelectorQuery)。
|
|
138
|
+
* 注意:方法体不能写 super.X() —— transone-cli 编译小程序时会把组件方法拍平为
|
|
139
|
+
* Component options 的普通函数属性,super 在普通函数里非法(JSCore 直接报错)。
|
|
140
|
+
* 基类 el 为 protected,此处直接取用;返回类型收窄在使用点做断言。
|
|
141
|
+
*/
|
|
133
142
|
public getElement(): HTMLCanvasElement | null {
|
|
134
|
-
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 = [];
|
|
135
197
|
}
|
|
136
198
|
|
|
137
199
|
/* —— 自动重绘:容器 / 窗口尺寸变化时防抖重绘 —— */
|
|
@@ -155,7 +217,7 @@ export class TcChart extends Component<TcChartProps, TcChartState> {
|
|
|
155
217
|
|
|
156
218
|
// Web:ResizeObserver 观察 canvas 自身(CSS 宽高 100%,容器变化即触发)。
|
|
157
219
|
// SSR / 构建期无真实元素与 ResizeObserver,跳过。
|
|
158
|
-
const element = this.getElement();
|
|
220
|
+
const element = this.getElement() as HTMLCanvasElement | null;
|
|
159
221
|
if (
|
|
160
222
|
element &&
|
|
161
223
|
typeof element.getContext === 'function' &&
|