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/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) {
@@ -99,6 +108,7 @@ export abstract class ChartBase<T extends ChartOption> {
99
108
  }
100
109
 
101
110
  const layout = this.computeLayout();
111
+ this.plot = layout.plot;
102
112
  this.drawTitle(layout);
103
113
  this.drawLegend(layout);
104
114
 
@@ -111,10 +121,39 @@ export abstract class ChartBase<T extends ChartOption> {
111
121
 
112
122
  this.drawSeries(ctx, layout, this.option);
113
123
 
124
+ // tooltip 浮层最后绘制,保证在最上层
125
+ if (this.hover) {
126
+ this.drawTooltip(this.hover);
127
+ }
128
+
114
129
  ctx.restore();
115
130
  return this;
116
131
  }
117
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
+
118
157
  public destroy(): void {
119
158
  this.destroyed = true;
120
159
  }
@@ -323,4 +362,126 @@ export abstract class ChartBase<T extends ChartOption> {
323
362
  protected seriesColor(index: number, color?: string): string {
324
363
  return color ?? this.palette[index % this.palette.length];
325
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
+ }
326
487
  }
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
 
@@ -166,6 +200,8 @@ export interface PieChartOption {
166
200
  labelColor?: string;
167
201
  /** 起始角(弧度),默认 -Math.PI / 2(12 点方向),顺时针。 */
168
202
  startAngle?: number;
203
+ /** 悬浮提示配置;默认开启,可 tooltip: { show: false } 关闭。 */
204
+ tooltip?: TooltipOption;
169
205
  backgroundColor?: string;
170
206
  }
171
207
 
@@ -202,6 +238,8 @@ export interface RadarChartOption {
202
238
  radius?: number;
203
239
  /** 起始角(弧度),默认 -Math.PI / 2(12 点方向),顺时针。 */
204
240
  startAngle?: number;
241
+ /** 悬浮提示配置;默认开启,可 tooltip: { show: false } 关闭。 */
242
+ tooltip?: TooltipOption;
205
243
  backgroundColor?: string;
206
244
  }
207
245
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "transone-chart",
3
- "version": "0.1.4",
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.3.0"
29
+ "transone": "0.4.0"
30
30
  },
31
31
  "keywords": [
32
32
  "transone",