@publishfx/publish-chart 2.0.0 → 2.0.1

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/CHANGELOG.md CHANGED
@@ -1,3 +1,10 @@
1
+ **2.0.1** feat(chart): 使用svg重写gauge图表组件
2
+ - 2026-02-04T11:18:21+08:00 [feat(chart): 使用svg重写gauge图表组件](http://lf.git.oa.mt/publish_platform/web/publish/commit/47a981e483d2a4df79d473e1053d9f9ea927ee10)
3
+ - 2026-02-03T19:25:53+08:00 [feat(chart): 修改ts错误](http://lf.git.oa.mt/publish_platform/web/publish/commit/40e4e8f44c7ff02b079974eca52ece225180329c)
4
+ - 2026-02-03T19:13:19+08:00 [feat: g2LineChart](http://lf.git.oa.mt/publish_platform/web/publish/commit/86e769b63b237c178a82de0c8dcf3c8d9ffdbab6)
5
+ - 2026-02-02T14:37:21+08:00 [feat(chart): 修改ts错误问题](http://lf.git.oa.mt/publish_platform/web/publish/commit/023b12647850619e340164e02a1f86e884eccf35)
6
+ - 2026-02-02T11:12:14+08:00 [feat(chart): 新增G2图表组件](http://lf.git.oa.mt/publish_platform/web/publish/commit/cfec9e945ceb9d8c0645dcea3d9adfe43c8d0bdc)
7
+
1
8
  **2.0.0** feat(chart): 修改ts错误问题
2
9
  - 2026-02-02T14:37:21+08:00 [feat(chart): 修改ts错误问题](http://lf.git.oa.mt/publish_platform/web/publish/commit/023b12647850619e340164e02a1f86e884eccf35)
3
10
  - 2026-02-02T11:12:14+08:00 [feat(chart): 新增G2图表组件](http://lf.git.oa.mt/publish_platform/web/publish/commit/cfec9e945ceb9d8c0645dcea3d9adfe43c8d0bdc)
@@ -1,38 +1,28 @@
1
1
  /**
2
2
  * G2@5 仪表盘(Gauge)图表组件
3
- * 支持自定义 scale 和阈值区间,可基于目标值设置刻度
3
+ * 使用SVG直接绘制,不依赖G2的canvas渲染
4
4
  */
5
- import React from 'react';
5
+ import React from "react";
6
6
  export interface G2GaugeChartProps {
7
- /** 当前值 */
8
7
  value: number;
9
- /** 最小值 */
10
8
  min?: number;
11
- /** 最大值 */
12
9
  max: number;
13
- /** 目标值(用于设置阈值区间和刻度) */
14
10
  target?: number;
15
- /** 自定义阈值区间配置(优先级高于 target) */
16
11
  thresholds?: number[] | Array<{
17
- /** 阈值点 */
18
12
  value: number;
19
- /** 区间颜色 */
20
13
  color: string;
21
14
  }>;
22
- /** 自定义刻度点(优先级高于 target) */
23
15
  ticks?: number[];
24
- /** 图表高度 */
25
16
  height?: number;
26
- /** 是否显示百分比 */
27
17
  showPercent?: boolean;
28
- /** 自定义格式化函数 */
29
18
  formatter?: (value: number, percent: number) => string;
30
- /** 刻度标签格式化函数(用于格式化刻度上的数值,如 100000 -> "100.00k") */
31
19
  tickFormatter?: (value: number) => string;
32
- /** 指针颜色 */
33
20
  pointerColor?: string;
34
- /** 阈值颜色数组 */
35
21
  thresholdColors?: string[];
22
+ tooltip?: {
23
+ label?: string;
24
+ formatter?: (value: number) => string;
25
+ };
36
26
  }
37
27
  declare const G2GaugeChart: React.FC<G2GaugeChartProps>;
38
28
  export default G2GaugeChart;
@@ -1,13 +1,11 @@
1
1
  import { jsx } from "react/jsx-runtime";
2
2
  import { useEffect, useMemo, useRef } from "react";
3
- import { Chart } from "@antv/g2";
4
- const G2GaugeChart = ({ value, min = 0, max, target, thresholds, ticks, height = 300, showPercent = true, formatter, tickFormatter, thresholdColors = [
5
- '#F4664A',
6
- '#FAAD14',
7
- '#52C41A'
8
- ] })=>{
3
+ const G2GaugeChart = ({ value, min = 0, max, target, thresholds, ticks, height = 300, showPercent = true, formatter, tickFormatter, pointerColor = "#c5c5c5", thresholdColors = [
4
+ "#9596d9",
5
+ "#FF9F4C",
6
+ "#56c2f3"
7
+ ], tooltip })=>{
9
8
  const containerRef = useRef(null);
10
- const chartRef = useRef(null);
11
9
  const scaleTicks = useMemo(()=>{
12
10
  if (ticks && ticks.length > 0) return ticks;
13
11
  if (target) return [
@@ -32,21 +30,27 @@ const G2GaugeChart = ({ value, min = 0, max, target, thresholds, ticks, height =
32
30
  ticks
33
31
  ]);
34
32
  const thresholdValues = useMemo(()=>{
33
+ let baseThresholds = [];
35
34
  if (thresholds) {
36
- if (Array.isArray(thresholds) && thresholds.length > 0 && 'number' == typeof thresholds[0]) return thresholds;
37
- if (Array.isArray(thresholds) && thresholds.length > 0 && 'object' == typeof thresholds[0]) return thresholds.map((t)=>t.value);
38
- }
39
- if (target) return [
35
+ if (Array.isArray(thresholds) && thresholds.length > 0 && "number" == typeof thresholds[0]) baseThresholds = [
36
+ ...thresholds
37
+ ];
38
+ else if (Array.isArray(thresholds) && thresholds.length > 0 && "object" == typeof thresholds[0]) baseThresholds = thresholds.map((t)=>t.value);
39
+ } else if (target) baseThresholds = [
40
40
  Math.round(0.6 * target),
41
41
  Math.round(0.9 * target),
42
42
  target
43
43
  ];
44
- const step = (max - min) / 3;
45
- return [
46
- Math.round(min + step),
47
- Math.round(min + 2 * step),
48
- max
49
- ];
44
+ else {
45
+ const step = (max - min) / 3;
46
+ baseThresholds = [
47
+ Math.round(min + step),
48
+ Math.round(min + 2 * step),
49
+ max
50
+ ];
51
+ }
52
+ if (baseThresholds.length > 0 && baseThresholds[baseThresholds.length - 1] !== max) baseThresholds.push(max);
53
+ return baseThresholds;
50
54
  }, [
51
55
  min,
52
56
  max,
@@ -54,7 +58,7 @@ const G2GaugeChart = ({ value, min = 0, max, target, thresholds, ticks, height =
54
58
  thresholds
55
59
  ]);
56
60
  const thresholdColorArray = useMemo(()=>{
57
- if (thresholds && Array.isArray(thresholds) && thresholds.length > 0 && 'object' == typeof thresholds[0]) return thresholds.map((t)=>t.color);
61
+ if (thresholds && Array.isArray(thresholds) && thresholds.length > 0 && "object" == typeof thresholds[0]) return thresholds.map((t)=>t.color);
58
62
  return thresholdColors;
59
63
  }, [
60
64
  thresholds,
@@ -62,73 +66,300 @@ const G2GaugeChart = ({ value, min = 0, max, target, thresholds, ticks, height =
62
66
  ]);
63
67
  useEffect(()=>{
64
68
  if (!containerRef.current) return;
65
- if (chartRef.current) chartRef.current.destroy();
66
- const chart = new Chart({
67
- container: containerRef.current,
68
- autoFit: true,
69
- height: height
70
- });
71
- chart.options({
72
- type: 'gauge',
73
- data: {
74
- value: {
75
- target: value,
76
- total: max,
77
- name: 'value',
78
- thresholds: thresholdValues
79
- }
80
- },
81
- scale: {
82
- color: {
83
- range: thresholdColorArray
84
- },
85
- value: {
86
- formatter: tickFormatter || ((d)=>{
87
- const matchedTick = scaleTicks.find((tick)=>Math.abs(d - tick) < 0.01);
88
- return void 0 !== matchedTick ? String(matchedTick) : '';
89
- }),
90
- domain: [
91
- min,
92
- max
93
- ],
94
- tickCount: scaleTicks.length,
95
- ticks: scaleTicks
69
+ const startAngle = -1.2 * Math.PI;
70
+ const endAngle = 0.2 * Math.PI;
71
+ const colors = [
72
+ ...thresholdColorArray
73
+ ];
74
+ while(colors.length < thresholdValues.length)colors.push(colors[colors.length - 1] || "#ccc");
75
+ requestAnimationFrame(()=>{
76
+ if (!containerRef.current) return;
77
+ const rect = containerRef.current.getBoundingClientRect();
78
+ const width = rect.width || containerRef.current.offsetWidth;
79
+ const height = rect.height || containerRef.current.offsetHeight;
80
+ const centerX = width / 2;
81
+ const centerY = height / 2;
82
+ const radius = Math.min(width, height) / 2;
83
+ const innerRadius = 0.85 * radius;
84
+ const outerRadius = 0.95 * radius;
85
+ const totalAngle = endAngle - startAngle;
86
+ const padding = 50;
87
+ let svgOverlay = containerRef.current.querySelector('svg.gauge-arc-overlay');
88
+ if (svgOverlay) {
89
+ svgOverlay.innerHTML = '';
90
+ svgOverlay.setAttribute('width', String(width + 2 * padding));
91
+ svgOverlay.setAttribute('height', String(height + 2 * padding));
92
+ svgOverlay.setAttribute('viewBox', `0 0 ${width + 2 * padding} ${height + 2 * padding}`);
93
+ svgOverlay.style.top = `-${padding}px`;
94
+ svgOverlay.style.left = `-${padding}px`;
95
+ } else {
96
+ svgOverlay = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
97
+ svgOverlay.setAttribute('class', 'gauge-arc-overlay');
98
+ svgOverlay.setAttribute('width', String(width + 2 * padding));
99
+ svgOverlay.setAttribute('height', String(height + 2 * padding));
100
+ svgOverlay.setAttribute('viewBox', `0 0 ${width + 2 * padding} ${height + 2 * padding}`);
101
+ svgOverlay.style.pointerEvents = 'none';
102
+ svgOverlay.style.overflow = 'visible';
103
+ containerRef.current.appendChild(svgOverlay);
104
+ }
105
+ for(let i = 0; i < thresholdValues.length; i++){
106
+ const start = 0 === i ? min : thresholdValues[i - 1];
107
+ const end = thresholdValues[i];
108
+ const color = colors[i] || '#ccc';
109
+ const startAngleValue = startAngle + (start - min) / (max - min) * totalAngle;
110
+ const endAngleValue = startAngle + (end - min) / (max - min) * totalAngle;
111
+ const largeArcFlag = Math.abs(endAngleValue - startAngleValue) > Math.PI ? 1 : 0;
112
+ const sweepFlag = 1;
113
+ const startX = centerX + padding + outerRadius * Math.cos(startAngleValue);
114
+ const startY = centerY + padding + outerRadius * Math.sin(startAngleValue);
115
+ const endX = centerX + padding + outerRadius * Math.cos(endAngleValue);
116
+ const endY = centerY + padding + outerRadius * Math.sin(endAngleValue);
117
+ const innerStartX = centerX + padding + innerRadius * Math.cos(startAngleValue);
118
+ const innerStartY = centerY + padding + innerRadius * Math.sin(startAngleValue);
119
+ const innerEndX = centerX + padding + innerRadius * Math.cos(endAngleValue);
120
+ const innerEndY = centerY + padding + innerRadius * Math.sin(endAngleValue);
121
+ const pathString = [
122
+ `M ${startX} ${startY}`,
123
+ `A ${outerRadius} ${outerRadius} 0 ${largeArcFlag} ${sweepFlag} ${endX} ${endY}`,
124
+ `L ${innerEndX} ${innerEndY}`,
125
+ `A ${innerRadius} ${innerRadius} 0 ${largeArcFlag} ${1 - sweepFlag} ${innerStartX} ${innerStartY}`,
126
+ 'Z'
127
+ ].join(' ');
128
+ const pathElement = document.createElementNS('http://www.w3.org/2000/svg', 'path');
129
+ pathElement.setAttribute('d', pathString);
130
+ pathElement.setAttribute('fill', color);
131
+ pathElement.setAttribute('stroke', color);
132
+ pathElement.setAttribute('stroke-width', '0');
133
+ svgOverlay.appendChild(pathElement);
134
+ }
135
+ const tickLength = 0.05 * radius;
136
+ const textOffset = 0.15 * radius;
137
+ const startAngleValue = startAngle;
138
+ const startX = centerX + padding + innerRadius * Math.cos(startAngleValue);
139
+ const startY = centerY + padding + innerRadius * Math.sin(startAngleValue);
140
+ const startTickEndX = centerX + padding + (outerRadius + tickLength) * Math.cos(startAngleValue);
141
+ const startTickEndY = centerY + padding + (outerRadius + tickLength) * Math.sin(startAngleValue);
142
+ const startTextX = centerX + padding + (outerRadius + textOffset) * Math.cos(startAngleValue);
143
+ const startTextY = centerY + padding + (outerRadius + textOffset) * Math.sin(startAngleValue);
144
+ const startTickLine = document.createElementNS('http://www.w3.org/2000/svg', 'line');
145
+ startTickLine.setAttribute('x1', String(startX));
146
+ startTickLine.setAttribute('y1', String(startY));
147
+ startTickLine.setAttribute('x2', String(startTickEndX));
148
+ startTickLine.setAttribute('y2', String(startTickEndY));
149
+ startTickLine.setAttribute('stroke', '#cccccc');
150
+ startTickLine.setAttribute('stroke-width', '2');
151
+ svgOverlay.appendChild(startTickLine);
152
+ const startText = document.createElementNS('http://www.w3.org/2000/svg', 'text');
153
+ startText.setAttribute('x', String(startTextX));
154
+ startText.setAttribute('y', String(startTextY));
155
+ startText.setAttribute('fill', '#1d2129');
156
+ startText.setAttribute('font-size', '14');
157
+ startText.setAttribute('text-anchor', 'middle');
158
+ startText.setAttribute('dominant-baseline', 'middle');
159
+ startText.textContent = tickFormatter ? tickFormatter(min) : String(min);
160
+ svgOverlay.appendChild(startText);
161
+ const thresholdTextOffset = 0.12 * radius;
162
+ const minThresholdDistance = (max - min) * 0.1;
163
+ const textOffsets = [];
164
+ for(let i = 0; i < thresholdValues.length - 1; i++){
165
+ let currentTextOffset = thresholdTextOffset;
166
+ if (i > 0) {
167
+ const prevThresholdValue = thresholdValues[i - 1];
168
+ const thresholdValue = thresholdValues[i];
169
+ const thresholdDistance = Math.abs(thresholdValue - prevThresholdValue);
170
+ if (thresholdDistance < minThresholdDistance) {
171
+ const prevThresholdAngle = startAngle + (prevThresholdValue - min) / (max - min) * totalAngle;
172
+ const thresholdAngle = startAngle + (thresholdValue - min) / (max - min) * totalAngle;
173
+ const angleDiff = Math.abs(thresholdAngle - prevThresholdAngle);
174
+ if (angleDiff < Math.PI / 6) {
175
+ const prevOffset = textOffsets[i - 1] || thresholdTextOffset;
176
+ currentTextOffset = Math.max(prevOffset + 0.08 * radius, thresholdTextOffset + 0.08 * radius);
177
+ } else currentTextOffset = thresholdTextOffset + 0.04 * radius;
178
+ }
96
179
  }
97
- },
98
- style: {
99
- textContent: (target, total)=>{
100
- const percent = (target / total * 100).toFixed(3);
101
- if (formatter) return formatter(target, parseFloat(percent));
102
- return showPercent ? `${target}\n占比: ${percent}%` : String(target);
180
+ textOffsets.push(currentTextOffset);
181
+ }
182
+ for(let i = 0; i < thresholdValues.length - 1; i++){
183
+ const thresholdValue = thresholdValues[i];
184
+ const currentTextOffset = textOffsets[i];
185
+ const thresholdAngle = startAngle + (thresholdValue - min) / (max - min) * totalAngle;
186
+ const thresholdTextX = centerX + padding + (outerRadius + currentTextOffset) * Math.cos(thresholdAngle);
187
+ const thresholdTextY = centerY + padding + (outerRadius + currentTextOffset) * Math.sin(thresholdAngle);
188
+ const rotationAngle = (180 * thresholdAngle / Math.PI + 90) % 360;
189
+ const thresholdText = document.createElementNS('http://www.w3.org/2000/svg', 'text');
190
+ thresholdText.setAttribute('x', String(thresholdTextX));
191
+ thresholdText.setAttribute('y', String(thresholdTextY));
192
+ thresholdText.setAttribute('fill', '#1d2129');
193
+ thresholdText.setAttribute('font-size', '14');
194
+ thresholdText.setAttribute('text-anchor', 'middle');
195
+ thresholdText.setAttribute('dominant-baseline', 'middle');
196
+ thresholdText.setAttribute('transform', `rotate(${rotationAngle} ${thresholdTextX} ${thresholdTextY})`);
197
+ thresholdText.textContent = tickFormatter ? tickFormatter(thresholdValue) : String(thresholdValue);
198
+ svgOverlay.appendChild(thresholdText);
199
+ }
200
+ const endAngleValue = endAngle;
201
+ const endX = centerX + padding + innerRadius * Math.cos(endAngleValue);
202
+ const endY = centerY + padding + innerRadius * Math.sin(endAngleValue);
203
+ const endTickEndX = centerX + padding + (outerRadius + tickLength) * Math.cos(endAngleValue);
204
+ const endTickEndY = centerY + padding + (outerRadius + tickLength) * Math.sin(endAngleValue);
205
+ const endTextX = centerX + padding + (outerRadius + textOffset) * Math.cos(endAngleValue);
206
+ const endTextY = centerY + padding + (outerRadius + textOffset) * Math.sin(endAngleValue);
207
+ const endTickLine = document.createElementNS('http://www.w3.org/2000/svg', 'line');
208
+ endTickLine.setAttribute('x1', String(endX));
209
+ endTickLine.setAttribute('y1', String(endY));
210
+ endTickLine.setAttribute('x2', String(endTickEndX));
211
+ endTickLine.setAttribute('y2', String(endTickEndY));
212
+ endTickLine.setAttribute('stroke', '#cccccc');
213
+ endTickLine.setAttribute('stroke-width', '2');
214
+ svgOverlay.appendChild(endTickLine);
215
+ const endText = document.createElementNS('http://www.w3.org/2000/svg', 'text');
216
+ endText.setAttribute('x', String(endTextX));
217
+ endText.setAttribute('y', String(endTextY));
218
+ endText.setAttribute('fill', '#1d2129');
219
+ endText.setAttribute('font-size', '14');
220
+ endText.setAttribute('text-anchor', 'middle');
221
+ endText.setAttribute('dominant-baseline', 'middle');
222
+ endText.textContent = tickFormatter ? tickFormatter(max) : String(max);
223
+ svgOverlay.appendChild(endText);
224
+ const pointerValue = Math.min(Math.max(value, min), max);
225
+ const pointerAngle = startAngle + (pointerValue - min) / (max - min) * totalAngle;
226
+ const pointerStartCircleRadius = 4;
227
+ const pointerLength = 0.6 * outerRadius;
228
+ const pointerStartX = centerX + padding + pointerStartCircleRadius * Math.cos(pointerAngle);
229
+ const pointerStartY = centerY + padding + pointerStartCircleRadius * Math.sin(pointerAngle);
230
+ const pointerEndX = centerX + padding + (pointerStartCircleRadius + pointerLength) * Math.cos(pointerAngle);
231
+ const pointerEndY = centerY + padding + (pointerStartCircleRadius + pointerLength) * Math.sin(pointerAngle);
232
+ const pointerStartCircle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
233
+ pointerStartCircle.setAttribute('cx', String(centerX + padding));
234
+ pointerStartCircle.setAttribute('cy', String(centerY + padding));
235
+ pointerStartCircle.setAttribute('r', String(pointerStartCircleRadius));
236
+ pointerStartCircle.setAttribute('fill', '#fff');
237
+ pointerStartCircle.setAttribute('stroke', pointerColor);
238
+ pointerStartCircle.setAttribute('stroke-width', '1.5');
239
+ svgOverlay.appendChild(pointerStartCircle);
240
+ const pointerLine = document.createElementNS('http://www.w3.org/2000/svg', 'line');
241
+ pointerLine.setAttribute('x1', String(pointerStartX));
242
+ pointerLine.setAttribute('y1', String(pointerStartY));
243
+ pointerLine.setAttribute('x2', String(pointerEndX));
244
+ pointerLine.setAttribute('y2', String(pointerEndY));
245
+ pointerLine.setAttribute('stroke', pointerColor);
246
+ pointerLine.setAttribute('stroke-width', '3');
247
+ pointerLine.setAttribute('stroke-linecap', 'round');
248
+ svgOverlay.appendChild(pointerLine);
249
+ const displayValue = formatter ? formatter(value, value / max * 100) : tickFormatter ? tickFormatter(value) : String(value);
250
+ const percent = (value / max * 100).toFixed(2);
251
+ const textCenterX = centerX + padding;
252
+ const textCenterY = centerY + padding + 0.7 * radius;
253
+ const valueTextY = textCenterY - 10;
254
+ const percentTextY = textCenterY + 10;
255
+ const valueText = document.createElementNS('http://www.w3.org/2000/svg', 'text');
256
+ valueText.setAttribute('x', String(textCenterX));
257
+ valueText.setAttribute('y', String(valueTextY));
258
+ valueText.setAttribute('fill', colors[0] || '#9596d9');
259
+ valueText.setAttribute('font-size', '18');
260
+ valueText.setAttribute('font-weight', 'bold');
261
+ valueText.setAttribute('text-anchor', 'middle');
262
+ valueText.setAttribute('dominant-baseline', 'middle');
263
+ valueText.textContent = displayValue;
264
+ svgOverlay.appendChild(valueText);
265
+ if (showPercent) {
266
+ const percentText = document.createElementNS('http://www.w3.org/2000/svg', 'text');
267
+ percentText.setAttribute('x', String(textCenterX));
268
+ percentText.setAttribute('y', String(percentTextY));
269
+ percentText.setAttribute('fill', '#8c8c8c');
270
+ percentText.setAttribute('font-size', '14');
271
+ percentText.setAttribute('text-anchor', 'middle');
272
+ percentText.setAttribute('dominant-baseline', 'middle');
273
+ percentText.textContent = `占比: ${percent}%`;
274
+ svgOverlay.appendChild(percentText);
275
+ }
276
+ if (tooltip) {
277
+ let tooltipElement = containerRef.current.querySelector('.gauge-tooltip');
278
+ if (!tooltipElement) {
279
+ tooltipElement = document.createElement('div');
280
+ tooltipElement.className = 'gauge-tooltip';
281
+ tooltipElement.style.position = 'absolute';
282
+ tooltipElement.style.pointerEvents = 'none';
283
+ tooltipElement.style.display = 'none';
284
+ tooltipElement.style.backgroundColor = '#fff';
285
+ tooltipElement.style.borderRadius = '4px';
286
+ tooltipElement.style.padding = '8px 12px';
287
+ tooltipElement.style.boxShadow = '0 2px 8px rgba(0, 0, 0, 0.15)';
288
+ tooltipElement.style.fontSize = '12px';
289
+ tooltipElement.style.lineHeight = '1.5';
290
+ tooltipElement.style.zIndex = '1000';
291
+ tooltipElement.style.whiteSpace = 'nowrap';
292
+ containerRef.current.appendChild(tooltipElement);
103
293
  }
104
- },
105
- legend: false
294
+ const tooltipLabel = tooltip.label || '值';
295
+ const tooltipValue = tooltip.formatter ? tooltip.formatter(value) : value.toLocaleString();
296
+ tooltipElement.innerHTML = `
297
+ <div style="color: #666; margin-bottom: 4px;">${tooltipLabel}</div>
298
+ <div style="color: #1d2129; font-weight: 500;">${tooltipValue}</div>
299
+ `;
300
+ const handleMouseMove = (e)=>{
301
+ if (!tooltipElement || !containerRef.current) return;
302
+ const svgOverlayRect = svgOverlay.getBoundingClientRect();
303
+ const mouseX = e.clientX;
304
+ const mouseY = e.clientY;
305
+ if (mouseX >= svgOverlayRect.left && mouseX <= svgOverlayRect.right && mouseY >= svgOverlayRect.top && mouseY <= svgOverlayRect.bottom) {
306
+ const containerRect = containerRef.current.getBoundingClientRect();
307
+ console.log('containerRect', containerRect, mouseX, mouseY);
308
+ const relativeX = mouseX - containerRect.left;
309
+ const relativeY = mouseY - containerRect.top;
310
+ const offsetX = 10;
311
+ const offsetY = -10;
312
+ tooltipElement.style.display = 'block';
313
+ tooltipElement.style.left = `${relativeX + offsetX}px`;
314
+ tooltipElement.style.top = `${relativeY + offsetY}px`;
315
+ tooltipElement.style.transform = 'translateY(-100%)';
316
+ } else tooltipElement.style.display = 'none';
317
+ };
318
+ const handleMouseLeave = ()=>{
319
+ if (tooltipElement) tooltipElement.style.display = 'none';
320
+ };
321
+ containerRef.current.addEventListener('mousemove', handleMouseMove);
322
+ containerRef.current.addEventListener('mouseleave', handleMouseLeave);
323
+ const cleanup = ()=>{
324
+ if (containerRef.current) {
325
+ containerRef.current.removeEventListener('mousemove', handleMouseMove);
326
+ containerRef.current.removeEventListener('mouseleave', handleMouseLeave);
327
+ }
328
+ };
329
+ containerRef.current._tooltipCleanup = cleanup;
330
+ }
106
331
  });
107
- chart.render();
108
- chartRef.current = chart;
109
332
  return ()=>{
110
- if (chartRef.current) {
111
- chartRef.current.destroy();
112
- chartRef.current = null;
333
+ if (containerRef.current) {
334
+ const container = containerRef.current;
335
+ if (container._tooltipCleanup) {
336
+ container._tooltipCleanup();
337
+ delete container._tooltipCleanup;
338
+ }
339
+ const svgOverlay = containerRef.current.querySelector('svg.gauge-arc-overlay');
340
+ if (svgOverlay) svgOverlay.remove();
341
+ const tooltipElement = containerRef.current.querySelector('.gauge-tooltip');
342
+ if (tooltipElement) tooltipElement.remove();
113
343
  }
114
344
  };
115
345
  }, [
116
346
  value,
117
347
  min,
118
348
  max,
119
- target,
120
349
  thresholdValues,
121
350
  thresholdColorArray,
122
351
  scaleTicks,
123
352
  height,
124
- showPercent,
125
353
  formatter,
126
- tickFormatter
354
+ tickFormatter,
355
+ showPercent,
356
+ pointerColor,
357
+ tooltip
127
358
  ]);
128
359
  return /*#__PURE__*/ jsx("div", {
129
360
  ref: containerRef,
130
361
  style: {
131
- width: '100%',
362
+ width: "100%",
132
363
  height: `${height}px`
133
364
  }
134
365
  });
@@ -0,0 +1,8 @@
1
+ /**
2
+ * G2@5 基础折线图组件
3
+ * 复用现有架构,仅在渲染层面使用 G2@5 API
4
+ */
5
+ import React from 'react';
6
+ import type { BaseChartProps } from '../../../core/ChartTypes';
7
+ declare const G2LineChart: React.FC<BaseChartProps>;
8
+ export default G2LineChart;
@@ -0,0 +1,151 @@
1
+ import { jsx } from "react/jsx-runtime";
2
+ import { useEffect, useMemo, useRef } from "react";
3
+ import { Chart } from "@antv/g2";
4
+ import { useChartContext } from "../../../core/ChartContext.js";
5
+ import { getAxisFormat, splitTextToMultiline } from "../../../utils/chartHelpers.js";
6
+ import { DataAdapter } from "../../../adapters/DataAdapter.js";
7
+ const G2LineChart = ({ height = 300, data, x = '', y = '', indicatorMap, onChartClick, legend = '', config, nodeSetting = {
8
+ showType: 0,
9
+ type: []
10
+ }, auxiliaryLineData })=>{
11
+ const { formatter, dataTransform, config: contextConfig } = useChartContext();
12
+ const safeIndicatorMap = indicatorMap || contextConfig.indicatorMap || {};
13
+ const safeLegend = legend || y;
14
+ const chartConfig = config || {};
15
+ const { isDataTag = false } = chartConfig;
16
+ const transformedData = useMemo(()=>{
17
+ const result = DataAdapter.transform(data, 'line', {
18
+ type: 'line',
19
+ x,
20
+ y
21
+ });
22
+ return result.map((item)=>dataTransform.processNodeInfo(item, contextConfig.nodeMap));
23
+ }, [
24
+ data,
25
+ x,
26
+ y,
27
+ dataTransform,
28
+ contextConfig.nodeMap
29
+ ]);
30
+ const maxY = useMemo(()=>{
31
+ const values = transformedData.filter((item)=>'-' !== item[y] && void 0 !== item[y] && null !== item[y]).map((item)=>Number(item[y]));
32
+ const maxData = Math.max(...values.length > 0 ? values : [
33
+ 0
34
+ ]);
35
+ const maxAuxiliary = auxiliaryLineData?.length ? Math.max(...auxiliaryLineData.map((item)=>item.value)) : 0;
36
+ return Math.max(maxData, maxAuxiliary);
37
+ }, [
38
+ transformedData,
39
+ y,
40
+ auxiliaryLineData
41
+ ]);
42
+ const formatAxis = (val)=>getAxisFormat(val, safeIndicatorMap, safeLegend);
43
+ const containerRef = useRef(null);
44
+ const chartRef = useRef(null);
45
+ useEffect(()=>{
46
+ if (!containerRef.current || !transformedData.length) return;
47
+ if (chartRef.current) chartRef.current.destroy();
48
+ const chart = new Chart({
49
+ container: containerRef.current,
50
+ height,
51
+ autoFit: true
52
+ });
53
+ chart.data(transformedData);
54
+ const line = chart.line().encode('x', x).encode('y', y).encode('shape', 'smooth').style({
55
+ lineWidth: 2
56
+ });
57
+ chart.axis('y', {
58
+ title: false,
59
+ grid: true,
60
+ gridStroke: '#e6e6e6',
61
+ gridLineWidth: 2,
62
+ gridStrokeOpacity: 0.7,
63
+ gridLineDash: [
64
+ 0,
65
+ 0
66
+ ],
67
+ labelFormatter: (val)=>formatAxis(val)
68
+ });
69
+ chart.axis('x', {
70
+ title: false,
71
+ labelAutoHide: 'greedy'
72
+ });
73
+ chart.scale({
74
+ y: {
75
+ type: 'linear',
76
+ domainMax: maxY,
77
+ nice: true
78
+ }
79
+ });
80
+ line.interaction('tooltip', {
81
+ render: (_event, { title, items })=>`<div>
82
+ <div>${title}</div>
83
+ ${items?.map(({ color, name, value })=>`
84
+ <div style="display:flex;justify-content:space-between;line-height:12px;margin-top:5px;">
85
+ <div>
86
+ <span
87
+ style="display:inline-block;width:8px;height:8px;margin-right:5px;background-color:${color};border-radius:50%;"
88
+ ></span>
89
+ ${safeIndicatorMap[name]?.indicatorName || ''}:
90
+ </div>
91
+ <div style="padding-left:10px;font-weight:bold;">
92
+ ${formatter.formatIndicator(value, safeIndicatorMap[name])}
93
+ </div>
94
+ </div>
95
+ `).join('')}
96
+ ${auxiliaryLineData?.map(({ name, value })=>`<div
97
+ style="display:flex;justify-content:space-between;line-height:12px;margin-top:10px;margin-left:16px;"
98
+ >
99
+ <div style="margin-right:8px">${name}:</div>
100
+ <div>${formatter.formatIndicator(value, safeIndicatorMap[safeLegend])}</div>
101
+ </div>`).join('')}
102
+ </div>`
103
+ });
104
+ if (auxiliaryLineData && auxiliaryLineData.length > 0) auxiliaryLineData.forEach((auxLine)=>{
105
+ chart.lineY().encode('y', auxLine.value).style('stroke', '#F4664A').style('strokeOpacity', 1).label({
106
+ dy: -3,
107
+ text: splitTextToMultiline(auxLine.name),
108
+ position: 'bottom-left',
109
+ style: {
110
+ fill: '#F4664A'
111
+ }
112
+ });
113
+ });
114
+ if (isDataTag) line.label({
115
+ text: (d)=>formatter.formatIndicator(d[y], safeIndicatorMap[y])
116
+ });
117
+ chart.render();
118
+ chartRef.current = chart;
119
+ return ()=>{
120
+ if (chartRef.current) {
121
+ chartRef.current.off('element:click');
122
+ chartRef.current.off('element:mouseenter');
123
+ chartRef.current.off('element:mouseleave');
124
+ chartRef.current.destroy();
125
+ chartRef.current = null;
126
+ }
127
+ };
128
+ }, [
129
+ transformedData,
130
+ height,
131
+ x,
132
+ y,
133
+ safeLegend,
134
+ isDataTag,
135
+ maxY,
136
+ safeIndicatorMap,
137
+ formatter,
138
+ onChartClick,
139
+ nodeSetting.showType,
140
+ formatAxis
141
+ ]);
142
+ return /*#__PURE__*/ jsx("div", {
143
+ ref: containerRef,
144
+ style: {
145
+ width: '100%',
146
+ height: `${height}px`
147
+ }
148
+ });
149
+ };
150
+ const base_G2LineChart = G2LineChart;
151
+ export { base_G2LineChart as default };
package/dist/index.d.ts CHANGED
@@ -21,6 +21,7 @@ export { default as GroupLineCompare } from './components/composite/GroupLineCom
21
21
  export { default as MultipleLine } from './components/composite/MultipleLine';
22
22
  export { default as MultipleCompareLine } from './components/composite/MultipleCompareLine';
23
23
  export { default as G2BarChart } from './components/g2/base/G2BarChart';
24
+ export { default as G2LineChart } from './components/g2/base/G2LineChart';
24
25
  export { default as G2GaugeChart } from './components/g2/base/G2GaugeChart';
25
26
  export type { G2GaugeChartProps } from './components/g2/base/G2GaugeChart';
26
27
  export { createLazyComponent, createLazyComponents } from './utils/lazyHelpers';
package/dist/index.js CHANGED
@@ -15,6 +15,7 @@ import GroupLineCompare from "./components/composite/GroupLineCompare.js";
15
15
  import MultipleLine from "./components/composite/MultipleLine.js";
16
16
  import MultipleCompareLine from "./components/composite/MultipleCompareLine.js";
17
17
  import G2BarChart from "./components/g2/base/G2BarChart.js";
18
+ import G2LineChart from "./components/g2/base/G2LineChart.js";
18
19
  import G2GaugeChart from "./components/g2/base/G2GaugeChart.js";
19
20
  import { createLazyComponent, createLazyComponents } from "./utils/lazyHelpers.js";
20
21
  import { TypeAdapter } from "./adapters/TypeAdapter.js";
@@ -25,4 +26,4 @@ import { BigNumberUtil, formatBigNumber, formatIndicatorV2, formatNumberV2, form
25
26
  import { calTextWidth, calculateBarWidth, createStripePatternCanvas, getAxisFormat, getAxisHorPaddingByText, markerStyle, truncateText } from "./utils/chartHelpers.js";
26
27
  import { getIntervalRatio, groupArray, transformIndicatorList } from "./utils/dataTransform.js";
27
28
  import { getIndicator, getIndicatorCompareName, getIndicatorName } from "./utils/indicatorHelpers.js";
28
- export { AuxiliaryLine, BarChart, BarLineAdapter, BarLineChart, BarLineCompareWeekend, BigNumberUtil, ChartProvider, DataAdapter, DefaultDataTransformService, DefaultFormatterService, G2BarChart, G2GaugeChart, GroupBar, GroupBarLine, GroupCompare, GroupLine, GroupLineCompare, LineChart, MultipleCompareLine, MultipleLine, TypeAdapter, XAxisBackground, calTextWidth, calculateBarWidth, createDataTransformService, createFormatterService, createLazyComponent, createLazyComponents, createStripePatternCanvas, defaultChartConfig, formatBigNumber, formatIndicatorV2, formatNumberV2, formatPercentage, getAxisFormat, getAxisFormatFromContext, getAxisHorPaddingByText, getIndicator, getIndicatorCompareName, getIndicatorName, getIntervalRatio, getThemeColors, groupArray, markerStyle, mergeChartConfig, transformIndicatorList, truncateText, useChartContext };
29
+ export { AuxiliaryLine, BarChart, BarLineAdapter, BarLineChart, BarLineCompareWeekend, BigNumberUtil, ChartProvider, DataAdapter, DefaultDataTransformService, DefaultFormatterService, G2BarChart, G2GaugeChart, G2LineChart, GroupBar, GroupBarLine, GroupCompare, GroupLine, GroupLineCompare, LineChart, MultipleCompareLine, MultipleLine, TypeAdapter, XAxisBackground, calTextWidth, calculateBarWidth, createDataTransformService, createFormatterService, createLazyComponent, createLazyComponents, createStripePatternCanvas, defaultChartConfig, formatBigNumber, formatIndicatorV2, formatNumberV2, formatPercentage, getAxisFormat, getAxisFormatFromContext, getAxisHorPaddingByText, getIndicator, getIndicatorCompareName, getIndicatorName, getIntervalRatio, getThemeColors, groupArray, markerStyle, mergeChartConfig, transformIndicatorList, truncateText, useChartContext };
@@ -50,3 +50,4 @@ export declare const markerStyle: (baseColor: string, isCompare: boolean) => {
50
50
  */
51
51
  export declare const createStripePatternCanvas: (baseColor: string, spaceUnit?: number) => HTMLCanvasElement;
52
52
  export declare const getChartKey: (config: any) => string;
53
+ export declare const splitTextToMultiline: (text: string, maxLength?: number) => string;
@@ -109,4 +109,9 @@ const createStripePatternCanvas = (baseColor, spaceUnit = 24)=>{
109
109
  return patternCanvas;
110
110
  };
111
111
  const getChartKey = (config)=>JSON.stringify(config);
112
- export { calTextWidth, calculateBarWidth, createStripePatternCanvas, getAxisFormat, getAxisHorPaddingByText, getChartKey, markerStyle, truncateText };
112
+ const splitTextToMultiline = (text, maxLength = 5)=>{
113
+ let result = '';
114
+ for(let i = 0; i < text.length; i += maxLength)result += text.substring(i, i + maxLength) + '\n';
115
+ return result.trim();
116
+ };
117
+ export { calTextWidth, calculateBarWidth, createStripePatternCanvas, getAxisFormat, getAxisHorPaddingByText, getChartKey, markerStyle, splitTextToMultiline, truncateText };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@publishfx/publish-chart",
3
- "version": "2.0.0",
3
+ "version": "2.0.1",
4
4
  "description": "A React chart component library for the Publish platform, including BarChart, LineChart, BarLineChart and other visualization components",
5
5
  "type": "module",
6
6
  "keywords": [