@easyv/charts 1.10.36 → 1.10.37

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.
@@ -1,1835 +1,1838 @@
1
- /**
2
- * 饼环图
3
- */
4
- import React, {
5
- memo,
6
- useMemo,
7
- useCallback,
8
- useRef,
9
- useState,
10
- useEffect,
11
- useContext,
12
- useLayoutEffect,
13
- Fragment,
14
- } from "react";
15
- import {
16
- ChartContainer,
17
- Carousel,
18
- Legend,
19
- ConicalGradient,
20
- Mapping,
21
- TextOverflow,
22
- } from ".";
23
- import { chartContext } from "../context";
24
- import {
25
- getFontStyle,
26
- sortPie,
27
- getDataWithPercent,
28
- getColorList,
29
- } from "../utils";
30
- import { pie, arc, extent, scaleLinear } from "d3v7";
31
- import { animate, linear } from "popmotion";
32
- import LinearGradient from "./LinearGradient";
33
- import { pieLegendFormatter as legendFormatter } from "../formatter";
34
- import ringCss from "../css/piechart.module.css";
35
- import { useAiDataOfPie } from "../hooks";
36
- import { PieTooltip } from "./PieTooltip";
37
- import { getPieAdaptiveMarginPreset } from "../utils/legendPlacement";
38
-
39
- const PI = Math.PI;
40
-
41
- const PIE_ADAPTIVE_MARGINS = {
42
- right: { marginTop: 24, marginBottom: 24, marginLeft: 0, marginRight: 200 },
43
- left: { marginTop: 24, marginBottom: 24, marginLeft: 200, marginRight: 0 },
44
- top: { marginTop: 110, marginBottom: 24, marginLeft: 24, marginRight: 24 },
45
- bottom: { marginTop: 24, marginBottom: 110, marginLeft: 24, marginRight: 24 },
46
- hidden: { marginTop: 24, marginBottom: 24, marginLeft: 24, marginRight: 24 },
47
- };
48
-
49
- const getPieAdaptiveMargin = (show, alignment) =>
50
- getPieAdaptiveMarginPreset(show, alignment, PIE_ADAPTIVE_MARGINS);
51
-
52
- const defaultChart = {
53
- outerRadius: 1,
54
- innerRadius: 0,
55
- cornerRadius: 0,
56
- rose: false,
57
- roseType: "radius",
58
- baseRadius: 0,
59
- padAngle: 0,
60
- };
61
- const defaultAngle = { startAngle: 0, endAngle: 360, antiClockwise: false };
62
-
63
- // const nameDy = (showValue, showPercent, mode, dir) => {
64
- // if (showValue || showPercent) {
65
- // if (mode == "vertical") {
66
- // return dir == 1 ? "1.1em" : "-2.6em";
67
- // } else {
68
- // return 0;
69
- // }
70
- // } else {
71
- // if (mode == "vertical") {
72
- // return dir * 1.1 + "em";
73
- // } else {
74
- // return 0;
75
- // }
76
- // }
77
- // };
78
- // const valueDy = (value1, mode, dir) => {
79
- // if (value1) {
80
- // if (mode == "vertical") {
81
- // return "1.5em";
82
- // } else {
83
- // return 0;
84
- // }
85
- // } else {
86
- // if (mode == "vertical") {
87
- // return dir == 1 ? "1.1em" : "-1.1em";
88
- // } else {
89
- // return 0;
90
- // }
91
- // }
92
- // };
93
-
94
- // const percentDy_ = (showName, showValue, mode, dir) => {
95
- // if (showValue) {
96
- // return 0;
97
- // }
98
- // if (showName) {
99
- // if (mode == "vertical") {
100
- // return "1.5em";
101
- // } else {
102
- // return 0;
103
- // }
104
- // } else {
105
- // if (mode == "vertical") {
106
- // return dir * 1.1 + "em";
107
- // } else {
108
- // return 0;
109
- // }
110
- // }
111
- // };
112
-
113
- // const percentX = (showName, showValue, mode, x) => {
114
- // if (showValue) {
115
- // return "";
116
- // }
117
- // if (showName) {
118
- // if (mode == "vertical") {
119
- // return x;
120
- // } else {
121
- // return "";
122
- // }
123
- // } else {
124
- // return x;
125
- // }
126
- // };
127
-
128
- // const percentDx = (showName, showValue, mode) => {
129
- // if (showValue) {
130
- // return "0.5em";
131
- // }
132
- // if (showName) {
133
- // if (mode == "vertical") {
134
- // return 0;
135
- // } else {
136
- // return "0.5em";
137
- // }
138
- // } else {
139
- // return 0;
140
- // }
141
- // };
142
-
143
- // const percentDy = (showName, showValue, mode) => {
144
- // if (showValue) {
145
- // return 0;
146
- // }
147
- // if (showName) {
148
- // if (mode == "vertical") {
149
- // return "1.5em";
150
- // } else {
151
- // return 0;
152
- // }
153
- // } else {
154
- // return 0;
155
- // }
156
- // };
157
-
158
- // const valueDx = (showName, mode) => {
159
- // if (!showName) {
160
- // return "";
161
- // }
162
- // if (mode == "vertical") {
163
- // return "";
164
- // } else {
165
- // return "0.5em";
166
- // }
167
- // };
168
-
169
- const getCoord = (deg, radius) => {
170
- var x = Math.cos(deg) * radius,
171
- y = Math.sin(deg) * radius;
172
- return [x, y];
173
- };
174
-
175
- const parseFontSize = (value, fallback = 12) => {
176
- const n = parseFloat(value);
177
- return Number.isFinite(n) ? n : fallback;
178
- };
179
-
180
- // 与 getFontStyle 一致:实际行盒高度取 lineHeight,否则用 fontSize
181
- const getFontLineHeight = (font) => {
182
- const fontSize = parseFontSize(font?.fontSize);
183
- const lineHeight = parseFontSize(font?.lineHeight, 0);
184
- return Math.max(lineHeight, fontSize);
185
- };
186
-
187
- const getLabelRowHeight = ({
188
- showName,
189
- showValue,
190
- showPercent,
191
- nameFont,
192
- valueFont,
193
- percentFont,
194
- }) => {
195
- const sizes = [];
196
- if (showName) sizes.push(getFontLineHeight(nameFont));
197
- if (showValue) sizes.push(getFontLineHeight(valueFont));
198
- if (showPercent) sizes.push(getFontLineHeight(percentFont));
199
- return sizes.length ? Math.max(...sizes) : 12;
200
- };
201
-
202
- // 上下布局:名称/数值/百分比纵向排列,高度 = 各可见行的行高之和
203
- const getLabelColumnHeight = ({
204
- showName,
205
- showValue,
206
- showPercent,
207
- nameFont,
208
- valueFont,
209
- percentFont,
210
- }) => {
211
- let height = 0;
212
- if (showName) height += getFontLineHeight(nameFont);
213
- if (showValue) height += getFontLineHeight(valueFont);
214
- if (showPercent) height += getFontLineHeight(percentFont);
215
- return height || 12;
216
- };
217
-
218
- // 大屏 transform:scale 后 getBoundingClientRect 会带缩放;offsetHeight 不受 transform 影响,与 SVG 坐标一致
219
- const getLabelDomHeight = (el) => {
220
- if (!el) return 0;
221
- if (el.offsetHeight) return el.offsetHeight;
222
- const rect = el.getBoundingClientRect();
223
- if (!rect.height) return 0;
224
- // offsetHeight 不可用时,用宽高比估算当前 scaleY 再还原
225
- const scaleY =
226
- el.offsetWidth > 0 && rect.width > 0 ? rect.width / el.offsetWidth : 1;
227
- return Math.round(rect.height / (scaleY || 1));
228
- };
229
-
230
- // 水平布局:同侧重叠时按半高之和推开
231
- const resolveLabelY = (y2, side, placedYBySide, labelHeight) => {
232
- const placed = placedYBySide[side];
233
- let finalY = y2;
234
- const stackDown = y2 >= 0;
235
- const isOverlap = (y, py, h) => Math.abs(y - py) < (labelHeight + h) / 2;
236
- let guard = 0;
237
- while (
238
- guard++ < 50 &&
239
- placed.some(({ y: py, height: h }) => isOverlap(finalY, py, h))
240
- ) {
241
- const hits = placed.filter(({ y: py, height: h }) =>
242
- isOverlap(finalY, py, h),
243
- );
244
- finalY = stackDown
245
- ? Math.max(
246
- ...hits.map(({ y: py, height: h }) => py + (labelHeight + h) / 2),
247
- )
248
- : Math.min(
249
- ...hits.map(({ y: py, height: h }) => py - (labelHeight + h) / 2),
250
- );
251
- }
252
- placed.push({ y: finalY, height: labelHeight });
253
- return finalY;
254
- };
255
-
256
- // 上下布局:过近时只按「自身标签高度」相对上一个已放置位置偏移
257
- const resolveLabelYVertical = (y2, side, placedYBySide, labelHeight) => {
258
- const placed = placedYBySide[side];
259
- const stackDown = y2 >= 0;
260
- let finalY = y2;
261
- if (placed.some(({ y: py }) => Math.abs(finalY - py) < labelHeight)) {
262
- finalY = stackDown
263
- ? Math.max(...placed.map(({ y: py }) => py)) + labelHeight
264
- : Math.min(...placed.map(({ y: py }) => py)) - labelHeight;
265
- }
266
- placed.push({ y: finalY, height: labelHeight });
267
- return finalY;
268
- };
269
-
270
- const getRoseRadius = ({ innerRadius, baseRadius }) =>
271
- innerRadius + (1 - innerRadius) * baseRadius;
272
-
273
- const getAngle = ({ startAngle, endAngle, antiClockwise, ...rest }) => {
274
- if (antiClockwise)
275
- return {
276
- ...rest,
277
- startAngle: endAngle - 180,
278
- endAngle: startAngle - 180,
279
- };
280
- return { ...rest, startAngle, endAngle };
281
- };
282
-
283
- const getArc = (
284
- radius,
285
- {
286
- padAngle = 0,
287
- innerRadius = 0,
288
- outerRadius = 1,
289
- cornerRadius = 0,
290
- startAngle = 0,
291
- endAngle = 360,
292
- ...rest
293
- },
294
- series_,
295
- index,
296
- ) => {
297
- const series =
298
- series_.find((s) => s.fieldName == rest.data.s) ||
299
- series_[index % series_.length];
300
- return {
301
- ...rest,
302
- type: "pie",
303
- fieldName: series.fieldName,
304
- displayName: series.displayName || rest.data.s,
305
- series: series,
306
- innerRadius: innerRadius * radius,
307
- outerRadius: outerRadius * radius,
308
- arc: arc()
309
- .innerRadius(innerRadius * radius)
310
- .outerRadius(outerRadius * radius)
311
- .cornerRadius(cornerRadius)
312
- .startAngle(startAngle)
313
- .endAngle(endAngle)
314
- .padAngle(padAngle),
315
- };
316
- };
317
-
318
- const getCircleScale = ({ count, color, width, length } = tick, radius) => {
319
- let data = [],
320
- arcs = [],
321
- centroids = [];
322
- for (let i = 0; i < count; i++) {
323
- data.push(1);
324
- }
325
- let scaleData = pie()(data);
326
- scaleData.map((data) => {
327
- let _arc = arc()
328
- .innerRadius(radius + length / 2)
329
- .outerRadius(radius + length / 2)
330
- .startAngle(data.startAngle)
331
- .endAngle(data.endAngle);
332
- arcs.push(_arc());
333
- centroids.push(_arc.centroid());
334
- });
335
- return (
336
- <g>
337
- {centroids.map((center, index) => {
338
- let x = center[0],
339
- y = center[1];
340
- let rate = length / Math.sqrt(Math.pow(x, 2) + Math.pow(y, 2));
341
- return (
342
- <path
343
- key={index}
344
- d={`M${x},${y}l${x * rate},${y * rate}`}
345
- strokeWidth={width}
346
- stroke={color}
347
- />
348
- );
349
- })}
350
- </g>
351
- );
352
- };
353
-
354
- const Component = memo(
355
- ({
356
- width,
357
- height,
358
- config: {
359
- chart: {
360
- label,
361
- legend: { formatter = legendFormatter, ...legend },
362
- animation: { ringDuration, labelDuration } = {},
363
- margin: { marginLeft, marginTop, marginRight, marginBottom },
364
- },
365
- fan: {
366
- chart = defaultChart,
367
- chart: { outerRadius = defaultChart.outerRadius, padAngle },
368
- angle = defaultAngle,
369
- stroke: { show, strokeWidth, color } = { show: false },
370
- decorate,
371
- decorate2,
372
- categoryText,
373
- outerDecorate,
374
- current,
375
- } = {},
376
- order,
377
- columnsSeries,
378
- series,
379
- animation: {
380
- on,
381
- current: {
382
- widthen = 0,
383
- heighten = 0,
384
- opacity = 0,
385
- width: radiusWidthAdd = 0,
386
- color: animateColor,
387
- textStyle: animateCTS,
388
- gap = 0,
389
- },
390
- rotate = 0,
391
- },
392
- tooltip = {},
393
- },
394
- config,
395
- state: { currentIndex, trigger },
396
- onEvent,
397
- hoverEvent,
398
- data: originData = [],
399
- }) => {
400
- const data = originData.map((d) => ({ ...d, y: d.y < 0 ? 0 : d.y }));
401
- const prevIndex = useRef(null);
402
- const { precision: legendPrecision } = legend.config.percent;
403
- const {
404
- id,
405
- isIOS,
406
- width: chartWidth,
407
- height: chartHeight,
408
- triggerOnRelative,
409
- onEmit,
410
- updateConfig,
411
- } = useContext(chartContext);
412
- const { show: legendShow, name: { layoutMode: legendLayoutMode } = {} } =
413
- legend.config;
414
- const legendAlignment = legend.config?.layout?.alignment;
415
- const prevLegendLayoutKey = useRef(null);
416
- useEffect(() => {
417
- if (legendLayoutMode !== "Adaptive") {
418
- prevLegendLayoutKey.current = null;
419
- return;
420
- }
421
- if (!updateConfig) return;
422
-
423
- const layoutKey = `${legendShow}-${legendAlignment}`;
424
- const prev = prevLegendLayoutKey.current;
425
- prevLegendLayoutKey.current = layoutKey;
426
-
427
- // 挂载/刷新:只记录当前图例布局,margin 始终读配置项
428
- if (prev === null) return;
429
- if (prev === layoutKey) return;
430
-
431
- const target = getPieAdaptiveMargin(legendShow, legendAlignment);
432
- updateConfig({
433
- id,
434
- type: "config",
435
- payload: [
436
- {
437
- path: ["chart", "margin", "marginTop"],
438
- field: "value",
439
- value: target.marginTop,
440
- },
441
- {
442
- path: ["chart", "margin", "marginBottom"],
443
- field: "value",
444
- value: target.marginBottom,
445
- },
446
- {
447
- path: ["chart", "margin", "marginLeft"],
448
- field: "value",
449
- value: target.marginLeft,
450
- },
451
- {
452
- path: ["chart", "margin", "marginRight"],
453
- field: "value",
454
- value: target.marginRight,
455
- },
456
- ],
457
- });
458
- }, [id, updateConfig, legendLayoutMode, legendShow, legendAlignment]);
459
- const [y, setY] = useState(1);
460
- const radius = (Math.min(chartWidth, chartHeight) / 2) * outerRadius;
461
-
462
- const arcsFunc = useMemo(() => {
463
- const { startAngle = 0, endAngle = 360 } = getAngle(angle);
464
- const arcsFunc = pie()
465
- .startAngle((startAngle * PI) / 180)
466
- .endAngle((endAngle * PI) / 180)
467
- .value((d) => d.y);
468
- return arcsFunc;
469
- }, [angle]);
470
- //此处创建arcsFuncTwo的原因是为了兼容数据全为零
471
- const arcsFuncTwo = useMemo(() => {
472
- const { startAngle = 0, endAngle = 360 } = getAngle(angle);
473
- const arcsFunc = pie()
474
- .startAngle((startAngle * PI) / 180)
475
- .endAngle((endAngle * PI) / 180)
476
- .value((d) => (d.y == 0 ? 1 : d.y));
477
- return arcsFunc;
478
- }, [angle]);
479
- let judgeData = 0; //此处声明全局变量是为了父子组件传递值来判断数据是否都为零
480
- const arcs = useMemo(() => {
481
- const _chart = Object.assign(defaultChart, chart);
482
- const {
483
- innerRadius,
484
- outerRadius,
485
- rose,
486
- cornerRadius,
487
- padAngle,
488
- roseType,
489
- } = _chart;
490
- const _padAngle = (padAngle * Math.PI) / 180;
491
-
492
- switch (order) {
493
- case "":
494
- arcsFunc.sort(null);
495
- break;
496
- case "desc":
497
- arcsFunc.sort((a, b) => b.y - a.y);
498
- break;
499
- case "asc":
500
- arcsFunc.sort((a, b) => a.y - b.y);
501
- break;
502
- }
503
-
504
- //此处判断data中的y是否都为零,方便饼图都为零时展示
505
-
506
- let arcs = 0;
507
- data.forEach(function (item) {
508
- judgeData += item.y;
509
- });
510
- if (judgeData == 0) {
511
- arcs = arcsFuncTwo(data);
512
- } else {
513
- arcs = arcsFunc(data);
514
- }
515
-
516
- //const arcs = arcsFunc(data); 此处是原本的传输饼图data流程
517
- const legendDataWithPercent = getDataWithPercent(arcs, legendPrecision);
518
- const _legendDataWithPercent = sortPie(legendDataWithPercent, order);
519
-
520
- if (rose) {
521
- const domain = extent(_legendDataWithPercent, (d) => d.value);
522
- const roseRadius = getRoseRadius(_chart);
523
- const scaler = scaleLinear().domain(domain).range([roseRadius, 1]);
524
-
525
- const angle = (PI * 2) / _legendDataWithPercent.length;
526
- return _legendDataWithPercent.map(
527
- ({ startAngle, endAngle, ...arc }, index) => ({
528
- ...arc,
529
- id: id + "_linear_" + index,
530
- startAngle: roseType == "area" ? angle * index : startAngle,
531
- endAngle: roseType == "area" ? angle * (index + 1) : endAngle,
532
- cornerRadius,
533
- padAngle: _padAngle,
534
- innerRadius,
535
- outerRadius: scaler(arc.value),
536
- }),
537
- );
538
- }
539
- return _legendDataWithPercent.map((arc, index) => ({
540
- ...arc,
541
- id: id + "_linear_" + index,
542
- cornerRadius,
543
- padAngle: _padAngle,
544
- innerRadius,
545
- outerRadius,
546
- }));
547
- }, [data, arcsFunc, arcsFuncTwo, chart, legendPrecision, order]);
548
-
549
- const _arcs = useMemo(() => {
550
- const seriesLength = series.size;
551
- if (!seriesLength) return [];
552
- const _series = [...series.values()];
553
- if (_series.length < arcs.length)
554
- console.warn("请检查数据中是否存在相同的s");
555
- return arcs.map((arc, index) => getArc(radius, arc, _series, index));
556
- }, [series, arcs, radius]);
557
-
558
- const onClick = useCallback(
559
- (e) => {
560
- const _data = arcs[+e.currentTarget.dataset.index].data;
561
- triggerOnRelative("onClick", _data);
562
- onEmit("onClick", _data);
563
- onEvent({
564
- currentIndex: +e.currentTarget.dataset.index,
565
- type: "onClick",
566
- });
567
- },
568
- [onEvent],
569
- );
570
-
571
- const onMouseEnter = useCallback(
572
- (e) => {
573
- const _data = arcs[+e.currentTarget.dataset.index].data;
574
- triggerOnRelative("mousehover", _data);
575
- onEmit("mousehover", _data);
576
- onEvent({
577
- currentIndex: +e.currentTarget.dataset.index,
578
- type: "onMouseEnter",
579
- });
580
- },
581
- [onEvent, triggerOnRelative, onEmit],
582
- );
583
-
584
- const onMouseLeave = useCallback(
585
- (e) => {
586
- setMousePos({
587
- x: 0,
588
- y: 0,
589
- });
590
- onEvent({
591
- currentIndex: +e.currentTarget.dataset.index,
592
- type: "onMouseLeave",
593
- });
594
- },
595
- [onEvent],
596
- );
597
-
598
- useLayoutEffect(() => {
599
- let animation;
600
- if (!!on) {
601
- animation = animate({
602
- from: 0,
603
- to: 1,
604
- duration: 500,
605
- ease: linear,
606
- onPlay: () => {},
607
- onUpdate: (v) => {
608
- setY(v);
609
- },
610
- onComplete: () => {
611
- const _data = arcs[+currentIndex] ? arcs[+currentIndex].data : {};
612
- triggerOnRelative("carousel", _data);
613
- onEmit("carousel", _data);
614
- },
615
- });
616
- } else {
617
- if (currentIndex !== null && trigger === "onClick") {
618
- const _data = arcs[+currentIndex] ? arcs[+currentIndex].data : {};
619
- triggerOnRelative("click", _data);
620
- onEmit("click", _data);
621
- }
622
- }
623
- return () => {
624
- prevIndex.current = currentIndex;
625
- animation && animation.stop();
626
- animation = null;
627
- };
628
- }, [
629
- JSON.stringify(arcs),
630
- on,
631
- currentIndex,
632
- trigger,
633
- onEmit,
634
- triggerOnRelative,
635
- ]);
636
- const aiData = useAiDataOfPie(_arcs, legend);
637
- useEffect(() => {
638
- if (aiData.length) {
639
- if (!window.aiData) {
640
- window.aiData = {};
641
- }
642
- window.aiData[id] = {
643
- getAI: () => {
644
- return aiData;
645
- },
646
- };
647
- }
648
- return () => {
649
- window.aiData && window.aiData[id] && delete window.aiData[id];
650
- };
651
- }, [JSON.stringify(aiData), id]);
652
-
653
- const halfChartWidth = chartWidth / 2;
654
- const halfChartHeight = chartHeight / 2;
655
-
656
- const rotate_ = decorate2
657
- ? (-(arcs[+currentIndex].startAngle + arcs[+currentIndex].endAngle) *
658
- 90) /
659
- Math.PI +
660
- rotate
661
- : 0;
662
- let maxRadius = 0;
663
- _arcs.map((d) => {
664
- maxRadius = Math.max(maxRadius, d.outerRadius);
665
- });
666
- let centerRadius = 0.5 * maxRadius + 0.5 * _arcs[0].innerRadius;
667
- const [mousePos, setMousePos] = useState({ x: 0, y: 0 });
668
- const [hoverData, setHoverData] = useState(null);
669
- const pieWarpEl = useRef(null);
670
- const domRef = useRef();
671
- return outerDecorate ? (
672
- <div ref={domRef}>
673
- <ChartContainer //用于生成甜甜圈图,判断依据是外环装饰这个配置项(outerDecorate)
674
- width={width}
675
- height={height}
676
- marginLeft={marginLeft}
677
- marginTop={marginTop}
678
- ref={pieWarpEl}
679
- >
680
- <g
681
- style={{
682
- "--labelDuration": labelDuration + "ms",
683
- "--ringDuration": ringDuration + "ms",
684
- transition: "transform ease-in-out 0.3s",
685
- transform:
686
- "translate(" +
687
- halfChartWidth +
688
- "px, " +
689
- halfChartHeight +
690
- "px) rotate(" +
691
- rotate_ +
692
- "deg)",
693
- }}
694
- >
695
- {
696
- //用于生成外环装饰的刻度
697
- outerDecorate.tick.show &&
698
- getCircleScale(outerDecorate.tick, maxRadius)
699
- }
700
- <circle //外环装饰
701
- cx="0"
702
- cy="0"
703
- r={maxRadius + 2}
704
- fill="none"
705
- stroke={outerDecorate.color}
706
- strokeWidth={outerDecorate.width}
707
- />
708
- {_arcs.map(
709
- ({ id, value, series, arc, innerRadius, outerRadius }, index) => {
710
- const arcWidth = outerRadius - innerRadius;
711
- const path = arc
712
- .innerRadius(centerRadius)
713
- .outerRadius(centerRadius)(value);
714
- const dashLength = Math.ceil(
715
- (Math.PI * centerRadius * 2) / _arcs.length,
716
- );
717
- const pie = getColorList(series.color);
718
- return (
719
- <Fragment key={index}>
720
- <path
721
- className={ringCss["inner-arc"]}
722
- style={{
723
- strokeDasharray: `${dashLength},${2 * dashLength}`,
724
- strokeDashoffset: dashLength,
725
- animationDelay: `${index * ringDuration}ms`,
726
- }}
727
- data-index={index}
728
- onClick={onClick}
729
- onMouseEnter={onMouseEnter}
730
- onMouseLeave={onMouseLeave}
731
- onMouseMove={(e) => {
732
- const _data = arcs[+e.currentTarget.dataset.index];
733
- const warpBoxPos = {
734
- x: pieWarpEl.current.getBoundingClientRect().x,
735
- y: pieWarpEl.current.getBoundingClientRect().y,
736
- };
737
- setMousePos({
738
- x: e.clientX - warpBoxPos.x,
739
- y: e.clientY - warpBoxPos.y,
740
- });
741
- setHoverData(_data);
742
- }}
743
- d={path.split("L")[0]}
744
- stroke={"url(#" + id + ")"}
745
- strokeWidth={arcWidth}
746
- fill="none"
747
- />
748
- <defs>
749
- <LinearGradient
750
- id={id}
751
- colors={pie}
752
- rotate={series.color.linear.angle + 180}
753
- // gradientUnits='objectBoundingBox'
754
- />
755
- </defs>
756
- </Fragment>
757
- );
758
- },
759
- )}
760
- {label && (
761
- <RingLabel
762
- config={{
763
- ...label,
764
- maxRadius: maxRadius + 2,
765
- ringDuration,
766
- labelDuration,
767
- }}
768
- iosStyle={{
769
- isIOS,
770
- left: halfChartWidth + marginLeft,
771
- top: halfChartHeight + marginTop,
772
- }}
773
- arcs={_arcs}
774
- judge={judgeData}
775
- />
776
- )}
777
- </g>
778
- </ChartContainer>
779
-
780
- <Legend
781
- {...legend}
782
- height={chartHeight}
783
- componentWidth={width}
784
- marginLeft={marginLeft}
785
- marginRight={marginRight}
786
- isPieChart
787
- columnsSeries={columnsSeries}
788
- data={data}
789
- series={_arcs.map((arc) => ({
790
- ...arc,
791
- percent: arc.percent.toFixed(legendPrecision),
792
- }))}
793
- pieClick={onClick}
794
- formatter={formatter}
795
- judge={judgeData}
796
- />
797
- {tooltip &&
798
- mousePos &&
799
- mousePos.x != 0 &&
800
- mousePos.y != 0 &&
801
- tooltip.manual && (
802
- <div
803
- style={{
804
- position: "absolute",
805
- pointerEvents: "none",
806
- }}
807
- >
808
- <PieTooltip
809
- series={series}
810
- domRef={domRef}
811
- data={hoverData}
812
- config={tooltip}
813
- pieCenter={{
814
- x: halfChartWidth,
815
- y: maxRadius + marginTop,
816
- }}
817
- mousePos={mousePos}
818
- />
819
- </div>
820
- )}
821
- </div>
822
- ) : (
823
- <div ref={domRef}>
824
- <ChartContainer
825
- width={width}
826
- height={height}
827
- marginLeft={marginLeft}
828
- marginTop={marginTop}
829
- onMouseEnter={() => {
830
- hoverEvent(true);
831
- }}
832
- onMouseLeave={() => {
833
- hoverEvent(false);
834
- }}
835
- ref={pieWarpEl}
836
- >
837
- <g
838
- style={{
839
- transition: "transform ease-in-out 0.3s",
840
- transform:
841
- "translate(" +
842
- halfChartWidth +
843
- "px, " +
844
- halfChartHeight +
845
- "px) rotate(" +
846
- rotate_ +
847
- "deg)",
848
- }}
849
- >
850
- {_arcs.map(
851
- (
852
- {
853
- id,
854
- value,
855
- series,
856
- arc,
857
- innerRadius,
858
- outerRadius,
859
- index: dataIndex,
860
- },
861
- index,
862
- ) => {
863
- const current = index == currentIndex;
864
- const prev = index == prevIndex.current;
865
- const offset = current ? y : prev ? 1 - y : 0;
866
-
867
- const fillOpacity = animateColor
868
- ? 1
869
- : current
870
- ? opacity / 100
871
- : 1;
872
- const deltaWidthen = offset * widthen;
873
- const deltaHeighten = offset * heighten;
874
- const path = arc
875
- .innerRadius(innerRadius + deltaWidthen)
876
- .outerRadius(outerRadius + deltaHeighten + deltaWidthen)(
877
- value,
878
- );
879
- const pie = getColorList(series.color);
880
- const currentPie = animateColor
881
- ? getColorList(animateColor)
882
- : getColorList(series.color);
883
- let textPath = "",
884
- categoryTextStyle = {};
885
- if (categoryText && categoryText.show) {
886
- //如果有类目文本,则需要计算文字路径
887
- //let offsetWidth=decorate2.radiusWidth/2 + radiusWidthAdd/2; //当前文字需生成在装饰物内,故而半径需要减小
888
- let textArc = arc
889
- .innerRadius(
890
- outerRadius + (current ? gap : categoryText.gap),
891
- )
892
- .outerRadius(
893
- outerRadius + (current ? gap : categoryText.gap),
894
- )(value);
895
- let lastA = textArc.lastIndexOf("A");
896
- textPath = textArc.slice(
897
- 0,
898
- lastA > 0 ? lastA : textArc.length,
899
- ); //文字路径
900
- categoryTextStyle = current
901
- ? animateCTS
902
- : categoryText.textStyle; //这里把textstyle拿出来
903
- }
904
-
905
- return (
906
- <Fragment key={index}>
907
- <path
908
- data-index={index}
909
- onClick={onClick}
910
- onMouseEnter={onMouseEnter}
911
- onMouseLeave={onMouseLeave}
912
- onMouseMove={(e) => {
913
- const _data = arcs[+e.currentTarget.dataset.index];
914
- const warpBoxPos = {
915
- x: pieWarpEl.current.getBoundingClientRect().x,
916
- y: pieWarpEl.current.getBoundingClientRect().y,
917
- };
918
- setMousePos({
919
- x: e.clientX - warpBoxPos.x,
920
- y: e.clientY - warpBoxPos.y,
921
- });
922
- setHoverData(_data);
923
- }}
924
- d={path}
925
- stroke={show ? color : "none"}
926
- strokeWidth={show ? strokeWidth : "0"}
927
- fill={"url(#" + id + ")"}
928
- fillOpacity={fillOpacity}
929
- />
930
- {
931
- //装饰物2,产生于每个弧的外部
932
- decorate2 && decorate2.show && (
933
- <path
934
- data-index={index}
935
- onClick={onClick}
936
- onMouseEnter={onMouseEnter}
937
- onMouseLeave={onMouseLeave}
938
- d={arc
939
- .innerRadius(outerRadius)
940
- .outerRadius(
941
- outerRadius +
942
- decorate2.radiusWidth +
943
- (current ? radiusWidthAdd : 0),
944
- )(value)}
945
- stroke={show ? color : "none"}
946
- strokeWidth={show ? strokeWidth : "0"}
947
- fill={"url(#" + id + ")"}
948
- fillOpacity={decorate2.opacity / 100}
949
- />
950
- )
951
- }
952
- {
953
- //类目文本
954
- value && categoryText && categoryText.show && (
955
- <g>
956
- <path
957
- onClick={onClick}
958
- onMouseEnter={onMouseEnter}
959
- onMouseLeave={onMouseLeave}
960
- id={id + "_text_" + index}
961
- d={textPath}
962
- fill="none"
963
- stroke="none"
964
- />
965
- <text
966
- textAnchor="middle"
967
- style={{
968
- ...categoryTextStyle,
969
- fontWeight: categoryTextStyle.bold
970
- ? "bold"
971
- : "normal",
972
- fontStyle: categoryTextStyle.italic
973
- ? "italic"
974
- : "normal",
975
- pointerEvents: "none",
976
- }}
977
- fill={categoryTextStyle.color}
978
- >
979
- <textPath
980
- startOffset="50%"
981
- href={"#" + id + "_text_" + index}
982
- >
983
- {_arcs[index].displayName ||
984
- _arcs[index].fieldName}
985
- </textPath>
986
- </text>
987
- </g>
988
- )
989
- }
990
- <defs>
991
- {/* 此处是环的发生地 */}
992
- <LinearGradient
993
- id={id}
994
- colors={current ? currentPie : pie}
995
- rotate={
996
- current
997
- ? animateColor
998
- ? animateColor.linear.angle + 180
999
- : series.color.linear.angle + 180
1000
- : series.color.linear.angle + 180
1001
- }
1002
- // gradientUnits='objectBoundingBox'
1003
- />
1004
- </defs>
1005
- </Fragment>
1006
- );
1007
- },
1008
- )}
1009
- {label && (
1010
- <Label
1011
- config={label}
1012
- iosStyle={{
1013
- isIOS,
1014
- left: halfChartWidth + marginLeft,
1015
- top: halfChartHeight + marginTop,
1016
- }}
1017
- arcs={_arcs}
1018
- judge={judgeData}
1019
- />
1020
- )}
1021
- {current && (
1022
- <g
1023
- fillOpacity={y}
1024
- style={{ transform: "rotate(" + -rotate_ + "deg)" }}
1025
- >
1026
- <Current
1027
- config={current}
1028
- width={chartWidth}
1029
- height={chartHeight}
1030
- iosStyle={{
1031
- marginLeft,
1032
- marginTop,
1033
- isIOS,
1034
- }}
1035
- data={_arcs}
1036
- judge={judgeData}
1037
- currentIndex={+currentIndex}
1038
- />
1039
- </g>
1040
- )}
1041
- </g>
1042
- </ChartContainer>
1043
- {decorate && (
1044
- <ConicalGradient
1045
- width={width}
1046
- height={height}
1047
- centerX={halfChartWidth + marginLeft}
1048
- centerY={halfChartHeight + marginTop}
1049
- config={decorate}
1050
- arcs={_arcs}
1051
- radius={radius}
1052
- />
1053
- )}
1054
-
1055
- <Legend
1056
- {...legend}
1057
- height={chartHeight}
1058
- componentWidth={width}
1059
- marginLeft={marginLeft}
1060
- marginRight={marginRight}
1061
- isPieChart
1062
- data={data}
1063
- columnsSeries={columnsSeries}
1064
- series={_arcs.map((arc) => ({
1065
- ...arc,
1066
- percent: arc.percent.toFixed(legendPrecision),
1067
- }))}
1068
- pieClick={onClick}
1069
- formatter={formatter}
1070
- judge={judgeData}
1071
- />
1072
- {tooltip &&
1073
- mousePos &&
1074
- mousePos.x != 0 &&
1075
- mousePos.y != 0 &&
1076
- tooltip.manual && (
1077
- <div
1078
- style={{
1079
- position: "absolute",
1080
- pointerEvents: "none",
1081
- }}
1082
- >
1083
- <PieTooltip
1084
- series={series}
1085
- domRef={domRef}
1086
- data={hoverData}
1087
- config={tooltip}
1088
- pieCenter={{
1089
- x: halfChartWidth,
1090
- y: maxRadius + marginTop,
1091
- }}
1092
- mousePos={mousePos}
1093
- />
1094
- </div>
1095
- )}
1096
- </div>
1097
- );
1098
- },
1099
- );
1100
-
1101
- const Current = ({
1102
- config: {
1103
- show,
1104
- gap,
1105
- name: {
1106
- show: showName,
1107
- sameColor: nameColor,
1108
- font: nameFont,
1109
- translate = { x: 0, y: 0 },
1110
- maxWidth,
1111
- textOverflow,
1112
- speed,
1113
- },
1114
- percent: {
1115
- show: showPercent,
1116
- sameColor: percentColor,
1117
- font: percentFont,
1118
- precision,
1119
- translate: { x: translatePercentX, y: translatePercentY },
1120
- },
1121
- value: {
1122
- show: showValue,
1123
- sameColor: valueColor,
1124
- font: valueFont,
1125
- translate: { x: translateValueX, y: translateValueY },
1126
- suffix: {
1127
- show: showSuffix,
1128
- fontSize,
1129
- text,
1130
- translate: { x: translateSuffixX, y: translateSuffixY },
1131
- },
1132
- },
1133
- },
1134
- iosStyle: { isIOS, marginLeft, marginTop },
1135
- width,
1136
- height,
1137
- data,
1138
- judge,
1139
- currentIndex,
1140
- }) => {
1141
- const _data = useMemo(() => {
1142
- const legendDataWithPercent = getDataWithPercent(data, precision);
1143
- return sortPie(legendDataWithPercent, "");
1144
- }, [data, precision]);
1145
-
1146
- //数据容错,当data都为零那么需要进行以下容错
1147
- if (judge == 0) {
1148
- _data.forEach((d) => {
1149
- ((d.percent = 0), (d.value = 0));
1150
- });
1151
- }
1152
-
1153
- const currentData = _data[currentIndex];
1154
-
1155
- if (!currentData) return null;
1156
-
1157
- const { displayName, fieldName, value, percent } = currentData;
1158
- let nameTemp = displayName ? displayName : fieldName; //类目名
1159
-
1160
- let foreignStyle = {
1161
- //foreignObject标签样式,
1162
- width,
1163
- height,
1164
- position: "relative",
1165
- overflow: "visible",
1166
- pointerEvents: "none",
1167
- },
1168
- boxStyle = {
1169
- //弹性盒子样式,用于当前值的上下居中对齐等
1170
- width,
1171
- height,
1172
- position: "absolute",
1173
- display: "flex",
1174
- flexDirection: "column",
1175
- justifyContent: "center",
1176
- alignItems: "center",
1177
- transform: isIOS
1178
- ? `translate(${marginLeft}px,${marginTop}px)`
1179
- : `translate(-${width / 2}px,-${height / 2}px)`,
1180
- };
1181
- let seriesColor = currentData.series.color;
1182
- seriesColor =
1183
- seriesColor.type == "pure"
1184
- ? seriesColor.pure
1185
- : seriesColor.linear.stops[0].color;
1186
- return (
1187
- show && (
1188
- <foreignObject style={foreignStyle}>
1189
- <div style={boxStyle}>
1190
- {showName && (
1191
- <TextOverflow
1192
- type={textOverflow}
1193
- value={nameTemp}
1194
- speed={speed}
1195
- style={{
1196
- width: "100%",
1197
- maxWidth,
1198
- textAlign: "center",
1199
- display: textOverflow == "marquee" ? "flex" : "bolck",
1200
- justifyContent: "center",
1201
- ...getFontStyle(nameFont),
1202
- margin: gap / 2 + "px 0",
1203
- color: nameColor ? seriesColor : nameFont.color,
1204
- transform: `translate(${translate.x}px,${translate.y}px)`,
1205
- }}
1206
- ></TextOverflow>
1207
- )}
1208
- {
1209
- //真实值
1210
- showValue && (
1211
- <span
1212
- style={{
1213
- ...getFontStyle(valueFont),
1214
- transform:
1215
- "translate(" +
1216
- translateValueX +
1217
- "px," +
1218
- translateValueY +
1219
- "px)",
1220
- margin: gap / 2 + "px 0",
1221
- color: valueColor ? seriesColor : valueFont.color,
1222
- }}
1223
- >
1224
- {value}
1225
- {showSuffix && text && (
1226
- <span
1227
- style={{
1228
- display: "inline-block",
1229
- transform:
1230
- "translate(" +
1231
- translateSuffixX +
1232
- "px," +
1233
- translateSuffixY +
1234
- "px)",
1235
- fontSize: fontSize,
1236
- }}
1237
- >
1238
- {text}
1239
- </span>
1240
- )}
1241
- </span>
1242
- )
1243
- }
1244
- {
1245
- //百分比值
1246
- showPercent && (
1247
- <span
1248
- style={{
1249
- transform:
1250
- "translate(" +
1251
- translatePercentX +
1252
- "px," +
1253
- translatePercentY +
1254
- "px)",
1255
- ...getFontStyle(percentFont),
1256
- margin: gap / 2 + "px 0",
1257
- color: percentColor ? seriesColor : percentFont.color,
1258
- }}
1259
- >
1260
- {percent + "%"}
1261
- </span>
1262
- )
1263
- }
1264
- </div>
1265
- </foreignObject>
1266
- )
1267
- );
1268
- };
1269
-
1270
- const Label = ({
1271
- config: {
1272
- maxRadius = 0,
1273
- lineLength,
1274
- lineColor,
1275
- distance,
1276
- mode,
1277
- align,
1278
- show,
1279
- translate: { x: translateX, y: translateY },
1280
- name: {
1281
- show: showName,
1282
- font: nameFont,
1283
- maxWidth,
1284
- textOverflow,
1285
- speed,
1286
- translate: NameTranslate,
1287
- },
1288
- value: {
1289
- show: showValue,
1290
- font: valueFont,
1291
- sameColor: valueSameColor,
1292
- suffix: {
1293
- show: showSuffix,
1294
- text,
1295
- fontSize: suffixFontSize,
1296
- translate: { x: suffixTranslateX, y: suffixTranslateY },
1297
- },
1298
- translate: ValueTranslate,
1299
- },
1300
- percent: {
1301
- show: showPercent,
1302
- sameColor: percentSameColor,
1303
- font: percentFont,
1304
- precision,
1305
- translate: PercentTranslate,
1306
- },
1307
- },
1308
- iosStyle: { isIOS, left, top },
1309
- arcs,
1310
- judge,
1311
- animation,
1312
- }) => {
1313
- const _arcs = useMemo(
1314
- () => getDataWithPercent(arcs, precision),
1315
- [arcs, precision],
1316
- );
1317
- //数据做出容错
1318
- if (judge == 0) {
1319
- _arcs.forEach((d) => {
1320
- d.percent = 0;
1321
- });
1322
- }
1323
- const isHorizontal = mode === "horizontal";
1324
- const labelElsRef = useRef({});
1325
- const [measuredHeights, setMeasuredHeights] = useState({});
1326
-
1327
- // 上下布局:渲染后读取每个标签真实高度(仅在高度变化时更新,避免死循环)
1328
- useLayoutEffect(() => {
1329
- if (isHorizontal) {
1330
- setMeasuredHeights((prev) => (Object.keys(prev).length ? {} : prev));
1331
- return;
1332
- }
1333
- setMeasuredHeights((prev) => {
1334
- const next = {};
1335
- let changed = false;
1336
- Object.keys(labelElsRef.current).forEach((key) => {
1337
- const el = labelElsRef.current[key];
1338
- if (!el) return;
1339
- const h = getLabelDomHeight(el);
1340
- if (!h) return;
1341
- next[key] = h;
1342
- if (prev[key] !== h) changed = true;
1343
- });
1344
- const prevKeys = Object.keys(prev);
1345
- const nextKeys = Object.keys(next);
1346
- if (prevKeys.length !== nextKeys.length) changed = true;
1347
- return changed ? next : prev;
1348
- });
1349
- }, [isHorizontal, _arcs, mode, showName, showValue, showPercent, showSuffix]);
1350
-
1351
- const placedYBySide = { left: [], right: [] };
1352
- return (
1353
- <g>
1354
- {_arcs.map(
1355
- (
1356
- {
1357
- series: {
1358
- color: {
1359
- type,
1360
- pure,
1361
- linear: { stops },
1362
- },
1363
- },
1364
- data,
1365
- displayName,
1366
- value,
1367
- percent,
1368
- arc,
1369
- outerRadius,
1370
- index: actualIndex,
1371
- },
1372
- index,
1373
- ) => {
1374
- const [x, y] = arc.centroid();
1375
- const midAngle = Math.atan2(y, x);
1376
-
1377
- const [x1, y1] = getCoord(
1378
- midAngle,
1379
- maxRadius ? maxRadius : outerRadius,
1380
- );
1381
-
1382
- const radius = (maxRadius ? maxRadius : outerRadius) + distance;
1383
- const [x2, y2] = getCoord(midAngle, radius);
1384
-
1385
- const direction = x2 < 0 ? -1 : 1;
1386
- const x3 = x2 + lineLength * direction;
1387
-
1388
- const _x = x3 + (translateX + 6) * direction;
1389
-
1390
- const _showName = showName && displayName;
1391
- const _showValue = showValue && (value || showSuffix);
1392
- const nameStyle = getFontStyle(nameFont);
1393
- const shouldShow = show && (_showName || showPercent || showValue);
1394
- // 与真实渲染一致:showValue 开启时值为 0 也会渲染
1395
- const labelSizeOptions = {
1396
- showName: _showName,
1397
- showValue,
1398
- showPercent,
1399
- nameFont,
1400
- valueFont,
1401
- percentFont,
1402
- };
1403
- const estimatedHeight = isHorizontal
1404
- ? getLabelRowHeight(labelSizeOptions)
1405
- : getLabelColumnHeight(labelSizeOptions);
1406
- // 上下布局优先用 DOM 实测高度
1407
- const labelHeight =
1408
- !isHorizontal && measuredHeights[index] != null
1409
- ? measuredHeights[index]
1410
- : estimatedHeight;
1411
- const side = x2 < 0 ? "left" : "right";
1412
- const finalY = shouldShow
1413
- ? isHorizontal
1414
- ? resolveLabelY(y2, side, placedYBySide, labelHeight)
1415
- : resolveLabelYVertical(y2, side, placedYBySide, labelHeight)
1416
- : y2;
1417
- return (
1418
- shouldShow && (
1419
- <g key={index}>
1420
- <path
1421
- className={animation ? ringCss["label-line"] : ""}
1422
- style={{
1423
- animationDelay: `${
1424
- animation
1425
- ? (actualIndex + 1) * ringDuration - labelDuration
1426
- : 0
1427
- }ms`,
1428
- }}
1429
- d={
1430
- "M" +
1431
- x1 +
1432
- ", " +
1433
- y1 +
1434
- "L" +
1435
- x2 +
1436
- ", " +
1437
- finalY +
1438
- "L" +
1439
- x3 +
1440
- ", " +
1441
- finalY
1442
- }
1443
- stroke={
1444
- lineColor
1445
- ? lineColor
1446
- : type == "pure"
1447
- ? pure
1448
- : stops[0].color
1449
- }
1450
- fill="none"
1451
- />
1452
- <foreignObject
1453
- width="1"
1454
- height="1"
1455
- x={_x}
1456
- y={finalY + translateY}
1457
- style={{ overflow: "visible", position: "relative" }}
1458
- >
1459
- <div
1460
- ref={(el) => {
1461
- if (el) labelElsRef.current[index] = el;
1462
- else delete labelElsRef.current[index];
1463
- }}
1464
- className={animation ? ringCss["label-text"] : ""}
1465
- style={{
1466
- position: isIOS ? "absolute" : "relative",
1467
- transform: isIOS
1468
- ? `translate(calc(${x3 < 0 ? "-100%" : "0px"} + ${
1469
- left + _x
1470
- }px),calc(-50% + ${top + finalY + translateY}px))`
1471
- : "translate(0,-50%)",
1472
- whiteSpace: "nowrap",
1473
- float: x3 >= 0 ? "left" : "right",
1474
- width: "max-content",
1475
- display: "flex",
1476
- flexDirection: mode == "horizontal" ? "row" : "column",
1477
- alignItems:
1478
- align == "left"
1479
- ? "flex-start"
1480
- : align == "center"
1481
- ? "center"
1482
- : "flex-end",
1483
- justifyContent: "center",
1484
- }}
1485
- >
1486
- {_showName && (
1487
- <TextOverflow
1488
- type={textOverflow}
1489
- value={
1490
- displayName + (showValue || showPercent ? ":" : "")
1491
- }
1492
- speed={speed}
1493
- style={{
1494
- maxWidth,
1495
- ...nameStyle,
1496
- float: mode == "horizontal" ? "left" : "none",
1497
- transform: `translate(${NameTranslate.x}px, ${NameTranslate.y}px)`,
1498
- }}
1499
- ></TextOverflow>
1500
- )}
1501
- {showValue && (
1502
- <span
1503
- style={{
1504
- ...getFontStyle(valueFont),
1505
- color: valueSameColor ? pure : valueFont.color,
1506
- transform: `translate(${ValueTranslate.x}px, ${ValueTranslate.y}px)`,
1507
- }}
1508
- >
1509
- {data.y}
1510
- {showSuffix && (
1511
- <span
1512
- style={{
1513
- position: "relative",
1514
- fontSize: suffixFontSize,
1515
- marginLeft: suffixTranslateX,
1516
- top: suffixTranslateY,
1517
- }}
1518
- >
1519
- {text}
1520
- </span>
1521
- )}
1522
- </span>
1523
- )}
1524
- {showPercent && (
1525
- <span
1526
- style={{
1527
- ...getFontStyle(percentFont),
1528
- color: percentSameColor ? pure : percentFont.color,
1529
- transform: `translate(${PercentTranslate.x}px, ${PercentTranslate.y}px)`,
1530
- }}
1531
- >
1532
- {(_showValue ? "(" : "") +
1533
- percent +
1534
- "%" +
1535
- (_showValue ? "" : "")}
1536
- </span>
1537
- )}
1538
- </div>
1539
- </foreignObject>
1540
- </g>
1541
- )
1542
- );
1543
- },
1544
- )}
1545
- </g>
1546
- );
1547
- };
1548
-
1549
- function getAlign(align, reverse) {
1550
- if (align == "center") return "center";
1551
- if (align == "left") return reverse ? "flex-end" : "flex-start";
1552
- return reverse ? "flex-start" : "flex-end";
1553
- }
1554
- function getTranslate(translate, reverse) {
1555
- const { x, y } = translate;
1556
- return `translate(${reverse ? -x : x}px, ${y}px)`;
1557
- }
1558
- const RingLabel = ({
1559
- config: {
1560
- ringDuration,
1561
- labelDuration,
1562
- maxRadius = 0,
1563
- lineLength,
1564
- lineColor,
1565
- distance,
1566
- mode,
1567
- align = "center",
1568
- show,
1569
- translate: { x: translateX, y: translateY },
1570
- name: {
1571
- show: showName,
1572
- font: nameFont,
1573
- maxWidth,
1574
- textOverflow,
1575
- speed,
1576
- translate: nameTranslate,
1577
- },
1578
- value: {
1579
- show: showValue,
1580
- font: valueFont,
1581
- sameColor: valueSameColor,
1582
- suffix: {
1583
- show: showSuffix,
1584
- text,
1585
- fontSize: suffixFontSize,
1586
- translate: { x: suffixTranslateX, y: suffixTranslateY },
1587
- },
1588
- translate: valueTranslate,
1589
- },
1590
- percent: {
1591
- show: showPercent,
1592
- sameColor: percentSameColor,
1593
- font: percentFont,
1594
- precision,
1595
- translate: percentTranslate,
1596
- },
1597
- },
1598
- iosStyle: { isIOS, left, top },
1599
- judge,
1600
- arcs,
1601
- }) => {
1602
- const _arcs = useMemo(
1603
- () => getDataWithPercent(arcs, precision),
1604
- [arcs, precision],
1605
- );
1606
-
1607
- //数据做出容错
1608
- if (judge == 0) {
1609
- _arcs.forEach((d) => {
1610
- d.percent = 0;
1611
- });
1612
- }
1613
-
1614
- const isHorizontal = mode === "horizontal";
1615
- const labelElsRef = useRef({});
1616
- const [measuredHeights, setMeasuredHeights] = useState({});
1617
-
1618
- // 上下布局:渲染后读取每个标签真实高度(仅在高度变化时更新,避免死循环)
1619
- useLayoutEffect(() => {
1620
- if (isHorizontal) {
1621
- setMeasuredHeights((prev) => (Object.keys(prev).length ? {} : prev));
1622
- return;
1623
- }
1624
- setMeasuredHeights((prev) => {
1625
- const next = {};
1626
- let changed = false;
1627
- Object.keys(labelElsRef.current).forEach((key) => {
1628
- const el = labelElsRef.current[key];
1629
- if (!el) return;
1630
- const h = getLabelDomHeight(el);
1631
- if (!h) return;
1632
- next[key] = h;
1633
- if (prev[key] !== h) changed = true;
1634
- });
1635
- const prevKeys = Object.keys(prev);
1636
- const nextKeys = Object.keys(next);
1637
- if (prevKeys.length !== nextKeys.length) changed = true;
1638
- return changed ? next : prev;
1639
- });
1640
- }, [isHorizontal, _arcs, mode, showName, showValue, showPercent, showSuffix]);
1641
-
1642
- const placedYBySide = { left: [], right: [] };
1643
- return (
1644
- <g>
1645
- {_arcs.map(
1646
- (
1647
- {
1648
- series: {
1649
- color: {
1650
- type,
1651
- pure,
1652
- linear: { stops },
1653
- },
1654
- },
1655
- data: realData,
1656
- displayName,
1657
- value,
1658
- percent,
1659
- arc,
1660
- outerRadius,
1661
- index: actualIndex,
1662
- },
1663
- index,
1664
- ) => {
1665
- const [x, y] = arc.centroid();
1666
- const midAngle = Math.atan2(y, x);
1667
-
1668
- const [x1, y1] = getCoord(
1669
- midAngle,
1670
- maxRadius ? maxRadius : outerRadius,
1671
- );
1672
-
1673
- const radius = (maxRadius ? maxRadius : outerRadius) + distance;
1674
- const [x2, y2] = getCoord(midAngle, radius);
1675
-
1676
- const directionX = x2 < 0 ? -1 : 1;
1677
- const x3 = x2 + lineLength * directionX;
1678
- const _x = x3 + (translateX + 6) * directionX;
1679
-
1680
- const _showName = showName && displayName;
1681
- const _showValue = showValue && (value || showSuffix);
1682
- const shouldShow = show && (_showName || showPercent || _showValue);
1683
- // 与真实渲染一致:showValue 开启时值为 0 也会渲染
1684
- const labelSizeOptions = {
1685
- showName: _showName,
1686
- showValue,
1687
- showPercent,
1688
- nameFont,
1689
- valueFont,
1690
- percentFont,
1691
- };
1692
- const estimatedHeight = isHorizontal
1693
- ? getLabelRowHeight(labelSizeOptions)
1694
- : getLabelColumnHeight(labelSizeOptions);
1695
- // 上下布局优先用 DOM 实测高度
1696
- const labelHeight =
1697
- !isHorizontal && measuredHeights[index] != null
1698
- ? measuredHeights[index]
1699
- : estimatedHeight;
1700
- const side = x2 < 0 ? "left" : "right";
1701
- const finalY = shouldShow
1702
- ? isHorizontal
1703
- ? resolveLabelY(y2, side, placedYBySide, labelHeight)
1704
- : resolveLabelYVertical(y2, side, placedYBySide, labelHeight)
1705
- : y2;
1706
-
1707
- return (
1708
- shouldShow && (
1709
- <g key={index}>
1710
- <path
1711
- className={ringCss["label-line"]}
1712
- style={{
1713
- animationDelay: `${
1714
- (actualIndex + 1) * ringDuration - labelDuration
1715
- }ms`,
1716
- }}
1717
- d={
1718
- "M" +
1719
- x1 +
1720
- ", " +
1721
- y1 +
1722
- "L" +
1723
- x2 +
1724
- ", " +
1725
- finalY +
1726
- "L" +
1727
- x3 +
1728
- ", " +
1729
- finalY
1730
- }
1731
- stroke={
1732
- lineColor
1733
- ? lineColor
1734
- : type == "pure"
1735
- ? pure
1736
- : stops[0].color
1737
- }
1738
- fill="none"
1739
- />
1740
- <foreignObject
1741
- width="1"
1742
- height="1"
1743
- x={_x}
1744
- y={finalY + translateY}
1745
- style={{ overflow: "visible", position: "relative" }}
1746
- >
1747
- <div
1748
- ref={(el) => {
1749
- if (el) labelElsRef.current[index] = el;
1750
- else delete labelElsRef.current[index];
1751
- }}
1752
- className={ringCss["label-text"]}
1753
- style={{
1754
- position: isIOS ? "absolute" : "relative",
1755
- transform: isIOS
1756
- ? `translate(calc(${x3 < 0 ? "-100%" : "0px"} + ${
1757
- left + _x
1758
- }px),calc(-50% + ${top + finalY + translateY}px))`
1759
- : "translate(0,-50%)",
1760
- whiteSpace: "nowrap",
1761
- float: x3 >= 0 ? "left" : "right",
1762
- width: "max-content",
1763
- animationDelay: `${
1764
- (actualIndex + 1) * ringDuration - labelDuration
1765
- }ms`,
1766
- display: "flex",
1767
- flexDirection: mode == "horizontal" ? "row" : "column",
1768
- alignItems: getAlign(align, x3 >= 0),
1769
- justifyContent: "center",
1770
- }}
1771
- >
1772
- {_showName && (
1773
- <TextOverflow
1774
- type={textOverflow}
1775
- value={
1776
- displayName + (showValue || showPercent ? ":" : "")
1777
- }
1778
- speed={speed}
1779
- style={{
1780
- maxWidth,
1781
- ...getFontStyle(nameFont),
1782
- float: mode == "horizontal" ? "left" : "none",
1783
- transform: getTranslate(nameTranslate, x3 >= 0),
1784
- }}
1785
- ></TextOverflow>
1786
- )}
1787
- {showValue && (
1788
- <span
1789
- style={{
1790
- ...getFontStyle(valueFont),
1791
- transform: getTranslate(valueTranslate, x3 >= 0),
1792
- color: valueSameColor ? pure : valueFont.color,
1793
- }}
1794
- >
1795
- {realData.y}
1796
- {showSuffix && (
1797
- <span
1798
- style={{
1799
- position: "relative",
1800
- fontSize: suffixFontSize,
1801
- marginLeft: suffixTranslateX,
1802
- top: suffixTranslateY,
1803
- }}
1804
- >
1805
- {text}
1806
- </span>
1807
- )}
1808
- </span>
1809
- )}
1810
- {showPercent && (
1811
- <span
1812
- style={{
1813
- ...getFontStyle(percentFont),
1814
- transform: getTranslate(percentTranslate, x3 >= 0),
1815
- color: percentSameColor ? pure : percentFont.color,
1816
- }}
1817
- >
1818
- {(_showValue ? "(" : "") +
1819
- percent +
1820
- "%" +
1821
- (_showValue ? "" : "")}
1822
- </span>
1823
- )}
1824
- </div>
1825
- </foreignObject>
1826
- </g>
1827
- )
1828
- );
1829
- },
1830
- )}
1831
- </g>
1832
- );
1833
- };
1834
-
1835
- export default Mapping(Carousel(Component));
1
+ /**
2
+ * 饼环图
3
+ */
4
+ import React, {
5
+ memo,
6
+ useMemo,
7
+ useCallback,
8
+ useRef,
9
+ useState,
10
+ useEffect,
11
+ useContext,
12
+ useLayoutEffect,
13
+ Fragment,
14
+ } from "react";
15
+ import {
16
+ ChartContainer,
17
+ Carousel,
18
+ Legend,
19
+ ConicalGradient,
20
+ Mapping,
21
+ TextOverflow,
22
+ } from ".";
23
+ import { chartContext } from "../context";
24
+ import {
25
+ getFontStyle,
26
+ sortPie,
27
+ getDataWithPercent,
28
+ getColorList,
29
+ } from "../utils";
30
+ import { pie, arc, extent, scaleLinear } from "d3v7";
31
+ import { animate, linear } from "popmotion";
32
+ import LinearGradient from "./LinearGradient";
33
+ import { pieLegendFormatter as legendFormatter } from "../formatter";
34
+ import ringCss from "../css/piechart.module.css";
35
+ import { useAiDataOfPie } from "../hooks";
36
+ import { PieTooltip } from "./PieTooltip";
37
+ import { getPieAdaptiveMarginPreset } from "../utils/legendPlacement";
38
+
39
+ const PI = Math.PI;
40
+
41
+ const PIE_ADAPTIVE_MARGINS = {
42
+ right: { marginTop: 24, marginBottom: 24, marginLeft: 0, marginRight: 200 },
43
+ left: { marginTop: 24, marginBottom: 24, marginLeft: 200, marginRight: 0 },
44
+ top: { marginTop: 110, marginBottom: 24, marginLeft: 24, marginRight: 24 },
45
+ bottom: { marginTop: 24, marginBottom: 110, marginLeft: 24, marginRight: 24 },
46
+ hidden: { marginTop: 24, marginBottom: 24, marginLeft: 24, marginRight: 24 },
47
+ };
48
+
49
+ const getPieAdaptiveMargin = (show, alignment) =>
50
+ getPieAdaptiveMarginPreset(show, alignment, PIE_ADAPTIVE_MARGINS);
51
+
52
+ const defaultChart = {
53
+ outerRadius: 1,
54
+ innerRadius: 0,
55
+ cornerRadius: 0,
56
+ rose: false,
57
+ roseType: "radius",
58
+ baseRadius: 0,
59
+ padAngle: 0,
60
+ };
61
+ const defaultAngle = { startAngle: 0, endAngle: 360, antiClockwise: false };
62
+
63
+ // const nameDy = (showValue, showPercent, mode, dir) => {
64
+ // if (showValue || showPercent) {
65
+ // if (mode == "vertical") {
66
+ // return dir == 1 ? "1.1em" : "-2.6em";
67
+ // } else {
68
+ // return 0;
69
+ // }
70
+ // } else {
71
+ // if (mode == "vertical") {
72
+ // return dir * 1.1 + "em";
73
+ // } else {
74
+ // return 0;
75
+ // }
76
+ // }
77
+ // };
78
+ // const valueDy = (value1, mode, dir) => {
79
+ // if (value1) {
80
+ // if (mode == "vertical") {
81
+ // return "1.5em";
82
+ // } else {
83
+ // return 0;
84
+ // }
85
+ // } else {
86
+ // if (mode == "vertical") {
87
+ // return dir == 1 ? "1.1em" : "-1.1em";
88
+ // } else {
89
+ // return 0;
90
+ // }
91
+ // }
92
+ // };
93
+
94
+ // const percentDy_ = (showName, showValue, mode, dir) => {
95
+ // if (showValue) {
96
+ // return 0;
97
+ // }
98
+ // if (showName) {
99
+ // if (mode == "vertical") {
100
+ // return "1.5em";
101
+ // } else {
102
+ // return 0;
103
+ // }
104
+ // } else {
105
+ // if (mode == "vertical") {
106
+ // return dir * 1.1 + "em";
107
+ // } else {
108
+ // return 0;
109
+ // }
110
+ // }
111
+ // };
112
+
113
+ // const percentX = (showName, showValue, mode, x) => {
114
+ // if (showValue) {
115
+ // return "";
116
+ // }
117
+ // if (showName) {
118
+ // if (mode == "vertical") {
119
+ // return x;
120
+ // } else {
121
+ // return "";
122
+ // }
123
+ // } else {
124
+ // return x;
125
+ // }
126
+ // };
127
+
128
+ // const percentDx = (showName, showValue, mode) => {
129
+ // if (showValue) {
130
+ // return "0.5em";
131
+ // }
132
+ // if (showName) {
133
+ // if (mode == "vertical") {
134
+ // return 0;
135
+ // } else {
136
+ // return "0.5em";
137
+ // }
138
+ // } else {
139
+ // return 0;
140
+ // }
141
+ // };
142
+
143
+ // const percentDy = (showName, showValue, mode) => {
144
+ // if (showValue) {
145
+ // return 0;
146
+ // }
147
+ // if (showName) {
148
+ // if (mode == "vertical") {
149
+ // return "1.5em";
150
+ // } else {
151
+ // return 0;
152
+ // }
153
+ // } else {
154
+ // return 0;
155
+ // }
156
+ // };
157
+
158
+ // const valueDx = (showName, mode) => {
159
+ // if (!showName) {
160
+ // return "";
161
+ // }
162
+ // if (mode == "vertical") {
163
+ // return "";
164
+ // } else {
165
+ // return "0.5em";
166
+ // }
167
+ // };
168
+
169
+ const getCoord = (deg, radius) => {
170
+ var x = Math.cos(deg) * radius,
171
+ y = Math.sin(deg) * radius;
172
+ return [x, y];
173
+ };
174
+
175
+ const parseFontSize = (value, fallback = 12) => {
176
+ const n = parseFloat(value);
177
+ return Number.isFinite(n) ? n : fallback;
178
+ };
179
+
180
+ // 与 getFontStyle 一致:实际行盒高度取 lineHeight,否则用 fontSize
181
+ const getFontLineHeight = (font) => {
182
+ const fontSize = parseFontSize(font?.fontSize);
183
+ const lineHeight = parseFontSize(font?.lineHeight, 0);
184
+ return Math.max(lineHeight, fontSize);
185
+ };
186
+
187
+ const getLabelRowHeight = ({
188
+ showName,
189
+ showValue,
190
+ showPercent,
191
+ nameFont,
192
+ valueFont,
193
+ percentFont,
194
+ }) => {
195
+ const sizes = [];
196
+ if (showName) sizes.push(getFontLineHeight(nameFont));
197
+ if (showValue) sizes.push(getFontLineHeight(valueFont));
198
+ if (showPercent) sizes.push(getFontLineHeight(percentFont));
199
+ return sizes.length ? Math.max(...sizes) : 12;
200
+ };
201
+
202
+ // 上下布局:名称/数值/百分比纵向排列,高度 = 各可见行的行高之和
203
+ const getLabelColumnHeight = ({
204
+ showName,
205
+ showValue,
206
+ showPercent,
207
+ nameFont,
208
+ valueFont,
209
+ percentFont,
210
+ }) => {
211
+ let height = 0;
212
+ if (showName) height += getFontLineHeight(nameFont);
213
+ if (showValue) height += getFontLineHeight(valueFont);
214
+ if (showPercent) height += getFontLineHeight(percentFont);
215
+ return height || 12;
216
+ };
217
+
218
+ // 大屏 transform:scale 后 getBoundingClientRect 会带缩放;offsetHeight 不受 transform 影响,与 SVG 坐标一致
219
+ const getLabelDomHeight = (el) => {
220
+ if (!el) return 0;
221
+ if (el.offsetHeight) return el.offsetHeight;
222
+ const rect = el.getBoundingClientRect();
223
+ if (!rect.height) return 0;
224
+ // offsetHeight 不可用时,用宽高比估算当前 scaleY 再还原
225
+ const scaleY =
226
+ el.offsetWidth > 0 && rect.width > 0 ? rect.width / el.offsetWidth : 1;
227
+ return Math.round(rect.height / (scaleY || 1));
228
+ };
229
+
230
+ // 水平布局:同侧重叠时按半高之和推开
231
+ const resolveLabelY = (y2, side, placedYBySide, labelHeight) => {
232
+ const placed = placedYBySide[side];
233
+ let finalY = y2;
234
+ const stackDown = y2 >= 0;
235
+ const isOverlap = (y, py, h) => Math.abs(y - py) < (labelHeight + h) / 2;
236
+ let guard = 0;
237
+ while (
238
+ guard++ < 50 &&
239
+ placed.some(({ y: py, height: h }) => isOverlap(finalY, py, h))
240
+ ) {
241
+ const hits = placed.filter(({ y: py, height: h }) =>
242
+ isOverlap(finalY, py, h),
243
+ );
244
+ finalY = stackDown
245
+ ? Math.max(
246
+ ...hits.map(({ y: py, height: h }) => py + (labelHeight + h) / 2),
247
+ )
248
+ : Math.min(
249
+ ...hits.map(({ y: py, height: h }) => py - (labelHeight + h) / 2),
250
+ );
251
+ }
252
+ placed.push({ y: finalY, height: labelHeight });
253
+ return finalY;
254
+ };
255
+
256
+ // 上下布局:过近时只按「自身标签高度」相对上一个已放置位置偏移
257
+ const resolveLabelYVertical = (y2, side, placedYBySide, labelHeight) => {
258
+ const placed = placedYBySide[side];
259
+ const stackDown = y2 >= 0;
260
+ let finalY = y2;
261
+ if (placed.some(({ y: py }) => Math.abs(finalY - py) < labelHeight)) {
262
+ finalY = stackDown
263
+ ? Math.max(...placed.map(({ y: py }) => py)) + labelHeight
264
+ : Math.min(...placed.map(({ y: py }) => py)) - labelHeight;
265
+ }
266
+ placed.push({ y: finalY, height: labelHeight });
267
+ return finalY;
268
+ };
269
+
270
+ const getRoseRadius = ({ innerRadius, baseRadius }) =>
271
+ innerRadius + (1 - innerRadius) * baseRadius;
272
+
273
+ const getAngle = ({ startAngle, endAngle, antiClockwise, ...rest }) => {
274
+ if (antiClockwise)
275
+ return {
276
+ ...rest,
277
+ startAngle: endAngle - 180,
278
+ endAngle: startAngle - 180,
279
+ };
280
+ return { ...rest, startAngle, endAngle };
281
+ };
282
+
283
+ const getArc = (
284
+ radius,
285
+ {
286
+ padAngle = 0,
287
+ innerRadius = 0,
288
+ outerRadius = 1,
289
+ cornerRadius = 0,
290
+ startAngle = 0,
291
+ endAngle = 360,
292
+ ...rest
293
+ },
294
+ series_,
295
+ index,
296
+ ) => {
297
+ const series =
298
+ series_.find((s) => s.fieldName == rest.data.s) ||
299
+ series_[index % series_.length];
300
+ return {
301
+ ...rest,
302
+ type: "pie",
303
+ fieldName: series.fieldName,
304
+ displayName: series.displayName || rest.data.s,
305
+ series: series,
306
+ innerRadius: innerRadius * radius,
307
+ outerRadius: outerRadius * radius,
308
+ arc: arc()
309
+ .innerRadius(innerRadius * radius)
310
+ .outerRadius(outerRadius * radius)
311
+ .cornerRadius(cornerRadius)
312
+ .startAngle(startAngle)
313
+ .endAngle(endAngle)
314
+ .padAngle(padAngle),
315
+ };
316
+ };
317
+
318
+ const getCircleScale = ({ count, color, width, length } = tick, radius) => {
319
+ let data = [],
320
+ arcs = [],
321
+ centroids = [];
322
+ for (let i = 0; i < count; i++) {
323
+ data.push(1);
324
+ }
325
+ let scaleData = pie()(data);
326
+ scaleData.map((data) => {
327
+ let _arc = arc()
328
+ .innerRadius(radius + length / 2)
329
+ .outerRadius(radius + length / 2)
330
+ .startAngle(data.startAngle)
331
+ .endAngle(data.endAngle);
332
+ arcs.push(_arc());
333
+ centroids.push(_arc.centroid());
334
+ });
335
+ return (
336
+ <g>
337
+ {centroids.map((center, index) => {
338
+ let x = center[0],
339
+ y = center[1];
340
+ let rate = length / Math.sqrt(Math.pow(x, 2) + Math.pow(y, 2));
341
+ return (
342
+ <path
343
+ key={index}
344
+ d={`M${x},${y}l${x * rate},${y * rate}`}
345
+ strokeWidth={width}
346
+ stroke={color}
347
+ />
348
+ );
349
+ })}
350
+ </g>
351
+ );
352
+ };
353
+
354
+ const Component = memo(
355
+ ({
356
+ width,
357
+ height,
358
+ config: {
359
+ chart: {
360
+ label,
361
+ legend: { formatter = legendFormatter, ...legend },
362
+ animation: { ringDuration, labelDuration } = {},
363
+ margin: { marginLeft, marginTop, marginRight, marginBottom },
364
+ },
365
+ fan: {
366
+ chart = defaultChart,
367
+ chart: { outerRadius = defaultChart.outerRadius, padAngle },
368
+ angle = defaultAngle,
369
+ stroke: { show, strokeWidth, color } = { show: false },
370
+ decorate,
371
+ decorate2,
372
+ categoryText,
373
+ outerDecorate,
374
+ current,
375
+ } = {},
376
+ order,
377
+ columnsSeries,
378
+ series,
379
+ animation: {
380
+ on,
381
+ current: {
382
+ widthen = 0,
383
+ heighten = 0,
384
+ opacity = 0,
385
+ width: radiusWidthAdd = 0,
386
+ color: animateColor,
387
+ textStyle: animateCTS,
388
+ gap = 0,
389
+ },
390
+ rotate = 0,
391
+ },
392
+ tooltip = {},
393
+ },
394
+ config,
395
+ state: { currentIndex, trigger },
396
+ onEvent,
397
+ hoverEvent,
398
+ data: originData = [],
399
+ }) => {
400
+ const data = originData.map((d) => ({ ...d, y: d.y < 0 ? 0 : d.y }));
401
+ const prevIndex = useRef(null);
402
+ const { precision: legendPrecision } = legend.config.percent;
403
+ const {
404
+ id,
405
+ isIOS,
406
+ width: chartWidth,
407
+ height: chartHeight,
408
+ triggerOnRelative,
409
+ onEmit,
410
+ updateConfig,
411
+ } = useContext(chartContext);
412
+ const { show: legendShow, name: { layoutMode: legendLayoutMode } = {} } =
413
+ legend.config;
414
+ const legendAlignment = legend.config?.layout?.alignment;
415
+ const prevLegendLayoutKey = useRef(null);
416
+ useEffect(() => {
417
+ if (legendLayoutMode !== "Adaptive") {
418
+ prevLegendLayoutKey.current = null;
419
+ return;
420
+ }
421
+ if (!updateConfig) return;
422
+
423
+ const layoutKey = `${legendShow}-${legendAlignment}`;
424
+ const prev = prevLegendLayoutKey.current;
425
+ prevLegendLayoutKey.current = layoutKey;
426
+
427
+ // 挂载/刷新:只记录当前图例布局,margin 始终读配置项
428
+ if (prev === null) return;
429
+ if (prev === layoutKey) return;
430
+
431
+ const target = getPieAdaptiveMargin(legendShow, legendAlignment);
432
+ updateConfig({
433
+ id,
434
+ type: "config",
435
+ payload: [
436
+ {
437
+ path: ["chart", "margin", "marginTop"],
438
+ field: "value",
439
+ value: target.marginTop,
440
+ },
441
+ {
442
+ path: ["chart", "margin", "marginBottom"],
443
+ field: "value",
444
+ value: target.marginBottom,
445
+ },
446
+ {
447
+ path: ["chart", "margin", "marginLeft"],
448
+ field: "value",
449
+ value: target.marginLeft,
450
+ },
451
+ {
452
+ path: ["chart", "margin", "marginRight"],
453
+ field: "value",
454
+ value: target.marginRight,
455
+ },
456
+ ],
457
+ });
458
+ }, [id, updateConfig, legendLayoutMode, legendShow, legendAlignment]);
459
+ const [y, setY] = useState(1);
460
+ const radius = Math.max(
461
+ 0,
462
+ (Math.min(chartWidth, chartHeight) / 2) * outerRadius,
463
+ );
464
+
465
+ const arcsFunc = useMemo(() => {
466
+ const { startAngle = 0, endAngle = 360 } = getAngle(angle);
467
+ const arcsFunc = pie()
468
+ .startAngle((startAngle * PI) / 180)
469
+ .endAngle((endAngle * PI) / 180)
470
+ .value((d) => d.y);
471
+ return arcsFunc;
472
+ }, [angle]);
473
+ //此处创建arcsFuncTwo的原因是为了兼容数据全为零
474
+ const arcsFuncTwo = useMemo(() => {
475
+ const { startAngle = 0, endAngle = 360 } = getAngle(angle);
476
+ const arcsFunc = pie()
477
+ .startAngle((startAngle * PI) / 180)
478
+ .endAngle((endAngle * PI) / 180)
479
+ .value((d) => (d.y == 0 ? 1 : d.y));
480
+ return arcsFunc;
481
+ }, [angle]);
482
+ let judgeData = 0; //此处声明全局变量是为了父子组件传递值来判断数据是否都为零
483
+ const arcs = useMemo(() => {
484
+ const _chart = Object.assign(defaultChart, chart);
485
+ const {
486
+ innerRadius,
487
+ outerRadius,
488
+ rose,
489
+ cornerRadius,
490
+ padAngle,
491
+ roseType,
492
+ } = _chart;
493
+ const _padAngle = (padAngle * Math.PI) / 180;
494
+
495
+ switch (order) {
496
+ case "":
497
+ arcsFunc.sort(null);
498
+ break;
499
+ case "desc":
500
+ arcsFunc.sort((a, b) => b.y - a.y);
501
+ break;
502
+ case "asc":
503
+ arcsFunc.sort((a, b) => a.y - b.y);
504
+ break;
505
+ }
506
+
507
+ //此处判断data中的y是否都为零,方便饼图都为零时展示
508
+
509
+ let arcs = 0;
510
+ data.forEach(function (item) {
511
+ judgeData += item.y;
512
+ });
513
+ if (judgeData == 0) {
514
+ arcs = arcsFuncTwo(data);
515
+ } else {
516
+ arcs = arcsFunc(data);
517
+ }
518
+
519
+ //const arcs = arcsFunc(data); 此处是原本的传输饼图data流程
520
+ const legendDataWithPercent = getDataWithPercent(arcs, legendPrecision);
521
+ const _legendDataWithPercent = sortPie(legendDataWithPercent, order);
522
+
523
+ if (rose) {
524
+ const domain = extent(_legendDataWithPercent, (d) => d.value);
525
+ const roseRadius = getRoseRadius(_chart);
526
+ const scaler = scaleLinear().domain(domain).range([roseRadius, 1]);
527
+
528
+ const angle = (PI * 2) / _legendDataWithPercent.length;
529
+ return _legendDataWithPercent.map(
530
+ ({ startAngle, endAngle, ...arc }, index) => ({
531
+ ...arc,
532
+ id: id + "_linear_" + index,
533
+ startAngle: roseType == "area" ? angle * index : startAngle,
534
+ endAngle: roseType == "area" ? angle * (index + 1) : endAngle,
535
+ cornerRadius,
536
+ padAngle: _padAngle,
537
+ innerRadius,
538
+ outerRadius: scaler(arc.value),
539
+ }),
540
+ );
541
+ }
542
+ return _legendDataWithPercent.map((arc, index) => ({
543
+ ...arc,
544
+ id: id + "_linear_" + index,
545
+ cornerRadius,
546
+ padAngle: _padAngle,
547
+ innerRadius,
548
+ outerRadius,
549
+ }));
550
+ }, [data, arcsFunc, arcsFuncTwo, chart, legendPrecision, order]);
551
+
552
+ const _arcs = useMemo(() => {
553
+ const seriesLength = series.size;
554
+ if (!seriesLength) return [];
555
+ const _series = [...series.values()];
556
+ if (_series.length < arcs.length)
557
+ console.warn("请检查数据中是否存在相同的s");
558
+ return arcs.map((arc, index) => getArc(radius, arc, _series, index));
559
+ }, [series, arcs, radius]);
560
+
561
+ const onClick = useCallback(
562
+ (e) => {
563
+ const _data = arcs[+e.currentTarget.dataset.index].data;
564
+ triggerOnRelative("onClick", _data);
565
+ onEmit("onClick", _data);
566
+ onEvent({
567
+ currentIndex: +e.currentTarget.dataset.index,
568
+ type: "onClick",
569
+ });
570
+ },
571
+ [onEvent],
572
+ );
573
+
574
+ const onMouseEnter = useCallback(
575
+ (e) => {
576
+ const _data = arcs[+e.currentTarget.dataset.index].data;
577
+ triggerOnRelative("mousehover", _data);
578
+ onEmit("mousehover", _data);
579
+ onEvent({
580
+ currentIndex: +e.currentTarget.dataset.index,
581
+ type: "onMouseEnter",
582
+ });
583
+ },
584
+ [onEvent, triggerOnRelative, onEmit],
585
+ );
586
+
587
+ const onMouseLeave = useCallback(
588
+ (e) => {
589
+ setMousePos({
590
+ x: 0,
591
+ y: 0,
592
+ });
593
+ onEvent({
594
+ currentIndex: +e.currentTarget.dataset.index,
595
+ type: "onMouseLeave",
596
+ });
597
+ },
598
+ [onEvent],
599
+ );
600
+
601
+ useLayoutEffect(() => {
602
+ let animation;
603
+ if (!!on) {
604
+ animation = animate({
605
+ from: 0,
606
+ to: 1,
607
+ duration: 500,
608
+ ease: linear,
609
+ onPlay: () => {},
610
+ onUpdate: (v) => {
611
+ setY(v);
612
+ },
613
+ onComplete: () => {
614
+ const _data = arcs[+currentIndex] ? arcs[+currentIndex].data : {};
615
+ triggerOnRelative("carousel", _data);
616
+ onEmit("carousel", _data);
617
+ },
618
+ });
619
+ } else {
620
+ if (currentIndex !== null && trigger === "onClick") {
621
+ const _data = arcs[+currentIndex] ? arcs[+currentIndex].data : {};
622
+ triggerOnRelative("click", _data);
623
+ onEmit("click", _data);
624
+ }
625
+ }
626
+ return () => {
627
+ prevIndex.current = currentIndex;
628
+ animation && animation.stop();
629
+ animation = null;
630
+ };
631
+ }, [
632
+ JSON.stringify(arcs),
633
+ on,
634
+ currentIndex,
635
+ trigger,
636
+ onEmit,
637
+ triggerOnRelative,
638
+ ]);
639
+ const aiData = useAiDataOfPie(_arcs, legend);
640
+ useEffect(() => {
641
+ if (aiData.length) {
642
+ if (!window.aiData) {
643
+ window.aiData = {};
644
+ }
645
+ window.aiData[id] = {
646
+ getAI: () => {
647
+ return aiData;
648
+ },
649
+ };
650
+ }
651
+ return () => {
652
+ window.aiData && window.aiData[id] && delete window.aiData[id];
653
+ };
654
+ }, [JSON.stringify(aiData), id]);
655
+
656
+ const halfChartWidth = chartWidth / 2;
657
+ const halfChartHeight = chartHeight / 2;
658
+
659
+ const rotate_ = decorate2
660
+ ? (-(arcs[+currentIndex].startAngle + arcs[+currentIndex].endAngle) *
661
+ 90) /
662
+ Math.PI +
663
+ rotate
664
+ : 0;
665
+ let maxRadius = 0;
666
+ _arcs.map((d) => {
667
+ maxRadius = Math.max(maxRadius, d.outerRadius);
668
+ });
669
+ let centerRadius = 0.5 * maxRadius + 0.5 * _arcs[0].innerRadius;
670
+ const [mousePos, setMousePos] = useState({ x: 0, y: 0 });
671
+ const [hoverData, setHoverData] = useState(null);
672
+ const pieWarpEl = useRef(null);
673
+ const domRef = useRef();
674
+ return outerDecorate ? (
675
+ <div ref={domRef}>
676
+ <ChartContainer //用于生成甜甜圈图,判断依据是外环装饰这个配置项(outerDecorate)
677
+ width={width}
678
+ height={height}
679
+ marginLeft={marginLeft}
680
+ marginTop={marginTop}
681
+ ref={pieWarpEl}
682
+ >
683
+ <g
684
+ style={{
685
+ "--labelDuration": labelDuration + "ms",
686
+ "--ringDuration": ringDuration + "ms",
687
+ transition: "transform ease-in-out 0.3s",
688
+ transform:
689
+ "translate(" +
690
+ halfChartWidth +
691
+ "px, " +
692
+ halfChartHeight +
693
+ "px) rotate(" +
694
+ rotate_ +
695
+ "deg)",
696
+ }}
697
+ >
698
+ {
699
+ //用于生成外环装饰的刻度
700
+ outerDecorate.tick.show &&
701
+ getCircleScale(outerDecorate.tick, maxRadius)
702
+ }
703
+ <circle //外环装饰
704
+ cx="0"
705
+ cy="0"
706
+ r={maxRadius + 2}
707
+ fill="none"
708
+ stroke={outerDecorate.color}
709
+ strokeWidth={outerDecorate.width}
710
+ />
711
+ {_arcs.map(
712
+ ({ id, value, series, arc, innerRadius, outerRadius }, index) => {
713
+ const arcWidth = outerRadius - innerRadius;
714
+ const path = arc
715
+ .innerRadius(centerRadius)
716
+ .outerRadius(centerRadius)(value);
717
+ const dashLength = Math.ceil(
718
+ (Math.PI * centerRadius * 2) / _arcs.length,
719
+ );
720
+ const pie = getColorList(series.color);
721
+ return (
722
+ <Fragment key={index}>
723
+ <path
724
+ className={ringCss["inner-arc"]}
725
+ style={{
726
+ strokeDasharray: `${dashLength},${2 * dashLength}`,
727
+ strokeDashoffset: dashLength,
728
+ animationDelay: `${index * ringDuration}ms`,
729
+ }}
730
+ data-index={index}
731
+ onClick={onClick}
732
+ onMouseEnter={onMouseEnter}
733
+ onMouseLeave={onMouseLeave}
734
+ onMouseMove={(e) => {
735
+ const _data = arcs[+e.currentTarget.dataset.index];
736
+ const warpBoxPos = {
737
+ x: pieWarpEl.current.getBoundingClientRect().x,
738
+ y: pieWarpEl.current.getBoundingClientRect().y,
739
+ };
740
+ setMousePos({
741
+ x: e.clientX - warpBoxPos.x,
742
+ y: e.clientY - warpBoxPos.y,
743
+ });
744
+ setHoverData(_data);
745
+ }}
746
+ d={path.split("L")[0]}
747
+ stroke={"url(#" + id + ")"}
748
+ strokeWidth={arcWidth}
749
+ fill="none"
750
+ />
751
+ <defs>
752
+ <LinearGradient
753
+ id={id}
754
+ colors={pie}
755
+ rotate={series.color.linear.angle + 180}
756
+ // gradientUnits='objectBoundingBox'
757
+ />
758
+ </defs>
759
+ </Fragment>
760
+ );
761
+ },
762
+ )}
763
+ {label && (
764
+ <RingLabel
765
+ config={{
766
+ ...label,
767
+ maxRadius: maxRadius + 2,
768
+ ringDuration,
769
+ labelDuration,
770
+ }}
771
+ iosStyle={{
772
+ isIOS,
773
+ left: halfChartWidth + marginLeft,
774
+ top: halfChartHeight + marginTop,
775
+ }}
776
+ arcs={_arcs}
777
+ judge={judgeData}
778
+ />
779
+ )}
780
+ </g>
781
+ </ChartContainer>
782
+
783
+ <Legend
784
+ {...legend}
785
+ height={chartHeight}
786
+ componentWidth={width}
787
+ marginLeft={marginLeft}
788
+ marginRight={marginRight}
789
+ isPieChart
790
+ columnsSeries={columnsSeries}
791
+ data={data}
792
+ series={_arcs.map((arc) => ({
793
+ ...arc,
794
+ percent: arc.percent.toFixed(legendPrecision),
795
+ }))}
796
+ pieClick={onClick}
797
+ formatter={formatter}
798
+ judge={judgeData}
799
+ />
800
+ {tooltip &&
801
+ mousePos &&
802
+ mousePos.x != 0 &&
803
+ mousePos.y != 0 &&
804
+ tooltip.manual && (
805
+ <div
806
+ style={{
807
+ position: "absolute",
808
+ pointerEvents: "none",
809
+ }}
810
+ >
811
+ <PieTooltip
812
+ series={series}
813
+ domRef={domRef}
814
+ data={hoverData}
815
+ config={tooltip}
816
+ pieCenter={{
817
+ x: halfChartWidth,
818
+ y: maxRadius + marginTop,
819
+ }}
820
+ mousePos={mousePos}
821
+ />
822
+ </div>
823
+ )}
824
+ </div>
825
+ ) : (
826
+ <div ref={domRef}>
827
+ <ChartContainer
828
+ width={width}
829
+ height={height}
830
+ marginLeft={marginLeft}
831
+ marginTop={marginTop}
832
+ onMouseEnter={() => {
833
+ hoverEvent(true);
834
+ }}
835
+ onMouseLeave={() => {
836
+ hoverEvent(false);
837
+ }}
838
+ ref={pieWarpEl}
839
+ >
840
+ <g
841
+ style={{
842
+ transition: "transform ease-in-out 0.3s",
843
+ transform:
844
+ "translate(" +
845
+ halfChartWidth +
846
+ "px, " +
847
+ halfChartHeight +
848
+ "px) rotate(" +
849
+ rotate_ +
850
+ "deg)",
851
+ }}
852
+ >
853
+ {_arcs.map(
854
+ (
855
+ {
856
+ id,
857
+ value,
858
+ series,
859
+ arc,
860
+ innerRadius,
861
+ outerRadius,
862
+ index: dataIndex,
863
+ },
864
+ index,
865
+ ) => {
866
+ const current = index == currentIndex;
867
+ const prev = index == prevIndex.current;
868
+ const offset = current ? y : prev ? 1 - y : 0;
869
+
870
+ const fillOpacity = animateColor
871
+ ? 1
872
+ : current
873
+ ? opacity / 100
874
+ : 1;
875
+ const deltaWidthen = offset * widthen;
876
+ const deltaHeighten = offset * heighten;
877
+ const path = arc
878
+ .innerRadius(innerRadius + deltaWidthen)
879
+ .outerRadius(outerRadius + deltaHeighten + deltaWidthen)(
880
+ value,
881
+ );
882
+ const pie = getColorList(series.color);
883
+ const currentPie = animateColor
884
+ ? getColorList(animateColor)
885
+ : getColorList(series.color);
886
+ let textPath = "",
887
+ categoryTextStyle = {};
888
+ if (categoryText && categoryText.show) {
889
+ //如果有类目文本,则需要计算文字路径
890
+ //let offsetWidth=decorate2.radiusWidth/2 + radiusWidthAdd/2; //当前文字需生成在装饰物内,故而半径需要减小
891
+ let textArc = arc
892
+ .innerRadius(
893
+ outerRadius + (current ? gap : categoryText.gap),
894
+ )
895
+ .outerRadius(
896
+ outerRadius + (current ? gap : categoryText.gap),
897
+ )(value);
898
+ let lastA = textArc.lastIndexOf("A");
899
+ textPath = textArc.slice(
900
+ 0,
901
+ lastA > 0 ? lastA : textArc.length,
902
+ ); //文字路径
903
+ categoryTextStyle = current
904
+ ? animateCTS
905
+ : categoryText.textStyle; //这里把textstyle拿出来
906
+ }
907
+
908
+ return (
909
+ <Fragment key={index}>
910
+ <path
911
+ data-index={index}
912
+ onClick={onClick}
913
+ onMouseEnter={onMouseEnter}
914
+ onMouseLeave={onMouseLeave}
915
+ onMouseMove={(e) => {
916
+ const _data = arcs[+e.currentTarget.dataset.index];
917
+ const warpBoxPos = {
918
+ x: pieWarpEl.current.getBoundingClientRect().x,
919
+ y: pieWarpEl.current.getBoundingClientRect().y,
920
+ };
921
+ setMousePos({
922
+ x: e.clientX - warpBoxPos.x,
923
+ y: e.clientY - warpBoxPos.y,
924
+ });
925
+ setHoverData(_data);
926
+ }}
927
+ d={path}
928
+ stroke={show ? color : "none"}
929
+ strokeWidth={show ? strokeWidth : "0"}
930
+ fill={"url(#" + id + ")"}
931
+ fillOpacity={fillOpacity}
932
+ />
933
+ {
934
+ //装饰物2,产生于每个弧的外部
935
+ decorate2 && decorate2.show && (
936
+ <path
937
+ data-index={index}
938
+ onClick={onClick}
939
+ onMouseEnter={onMouseEnter}
940
+ onMouseLeave={onMouseLeave}
941
+ d={arc
942
+ .innerRadius(outerRadius)
943
+ .outerRadius(
944
+ outerRadius +
945
+ decorate2.radiusWidth +
946
+ (current ? radiusWidthAdd : 0),
947
+ )(value)}
948
+ stroke={show ? color : "none"}
949
+ strokeWidth={show ? strokeWidth : "0"}
950
+ fill={"url(#" + id + ")"}
951
+ fillOpacity={decorate2.opacity / 100}
952
+ />
953
+ )
954
+ }
955
+ {
956
+ //类目文本
957
+ value && categoryText && categoryText.show && (
958
+ <g>
959
+ <path
960
+ onClick={onClick}
961
+ onMouseEnter={onMouseEnter}
962
+ onMouseLeave={onMouseLeave}
963
+ id={id + "_text_" + index}
964
+ d={textPath}
965
+ fill="none"
966
+ stroke="none"
967
+ />
968
+ <text
969
+ textAnchor="middle"
970
+ style={{
971
+ ...categoryTextStyle,
972
+ fontWeight: categoryTextStyle.bold
973
+ ? "bold"
974
+ : "normal",
975
+ fontStyle: categoryTextStyle.italic
976
+ ? "italic"
977
+ : "normal",
978
+ pointerEvents: "none",
979
+ }}
980
+ fill={categoryTextStyle.color}
981
+ >
982
+ <textPath
983
+ startOffset="50%"
984
+ href={"#" + id + "_text_" + index}
985
+ >
986
+ {_arcs[index].displayName ||
987
+ _arcs[index].fieldName}
988
+ </textPath>
989
+ </text>
990
+ </g>
991
+ )
992
+ }
993
+ <defs>
994
+ {/* 此处是环的发生地 */}
995
+ <LinearGradient
996
+ id={id}
997
+ colors={current ? currentPie : pie}
998
+ rotate={
999
+ current
1000
+ ? animateColor
1001
+ ? animateColor.linear.angle + 180
1002
+ : series.color.linear.angle + 180
1003
+ : series.color.linear.angle + 180
1004
+ }
1005
+ // gradientUnits='objectBoundingBox'
1006
+ />
1007
+ </defs>
1008
+ </Fragment>
1009
+ );
1010
+ },
1011
+ )}
1012
+ {label && (
1013
+ <Label
1014
+ config={label}
1015
+ iosStyle={{
1016
+ isIOS,
1017
+ left: halfChartWidth + marginLeft,
1018
+ top: halfChartHeight + marginTop,
1019
+ }}
1020
+ arcs={_arcs}
1021
+ judge={judgeData}
1022
+ />
1023
+ )}
1024
+ {current && (
1025
+ <g
1026
+ fillOpacity={y}
1027
+ style={{ transform: "rotate(" + -rotate_ + "deg)" }}
1028
+ >
1029
+ <Current
1030
+ config={current}
1031
+ width={chartWidth}
1032
+ height={chartHeight}
1033
+ iosStyle={{
1034
+ marginLeft,
1035
+ marginTop,
1036
+ isIOS,
1037
+ }}
1038
+ data={_arcs}
1039
+ judge={judgeData}
1040
+ currentIndex={+currentIndex}
1041
+ />
1042
+ </g>
1043
+ )}
1044
+ </g>
1045
+ </ChartContainer>
1046
+ {decorate && (
1047
+ <ConicalGradient
1048
+ width={width}
1049
+ height={height}
1050
+ centerX={halfChartWidth + marginLeft}
1051
+ centerY={halfChartHeight + marginTop}
1052
+ config={decorate}
1053
+ arcs={_arcs}
1054
+ radius={radius}
1055
+ />
1056
+ )}
1057
+
1058
+ <Legend
1059
+ {...legend}
1060
+ height={chartHeight}
1061
+ componentWidth={width}
1062
+ marginLeft={marginLeft}
1063
+ marginRight={marginRight}
1064
+ isPieChart
1065
+ data={data}
1066
+ columnsSeries={columnsSeries}
1067
+ series={_arcs.map((arc) => ({
1068
+ ...arc,
1069
+ percent: arc.percent.toFixed(legendPrecision),
1070
+ }))}
1071
+ pieClick={onClick}
1072
+ formatter={formatter}
1073
+ judge={judgeData}
1074
+ />
1075
+ {tooltip &&
1076
+ mousePos &&
1077
+ mousePos.x != 0 &&
1078
+ mousePos.y != 0 &&
1079
+ tooltip.manual && (
1080
+ <div
1081
+ style={{
1082
+ position: "absolute",
1083
+ pointerEvents: "none",
1084
+ }}
1085
+ >
1086
+ <PieTooltip
1087
+ series={series}
1088
+ domRef={domRef}
1089
+ data={hoverData}
1090
+ config={tooltip}
1091
+ pieCenter={{
1092
+ x: halfChartWidth,
1093
+ y: maxRadius + marginTop,
1094
+ }}
1095
+ mousePos={mousePos}
1096
+ />
1097
+ </div>
1098
+ )}
1099
+ </div>
1100
+ );
1101
+ },
1102
+ );
1103
+
1104
+ const Current = ({
1105
+ config: {
1106
+ show,
1107
+ gap,
1108
+ name: {
1109
+ show: showName,
1110
+ sameColor: nameColor,
1111
+ font: nameFont,
1112
+ translate = { x: 0, y: 0 },
1113
+ maxWidth,
1114
+ textOverflow,
1115
+ speed,
1116
+ },
1117
+ percent: {
1118
+ show: showPercent,
1119
+ sameColor: percentColor,
1120
+ font: percentFont,
1121
+ precision,
1122
+ translate: { x: translatePercentX, y: translatePercentY },
1123
+ },
1124
+ value: {
1125
+ show: showValue,
1126
+ sameColor: valueColor,
1127
+ font: valueFont,
1128
+ translate: { x: translateValueX, y: translateValueY },
1129
+ suffix: {
1130
+ show: showSuffix,
1131
+ fontSize,
1132
+ text,
1133
+ translate: { x: translateSuffixX, y: translateSuffixY },
1134
+ },
1135
+ },
1136
+ },
1137
+ iosStyle: { isIOS, marginLeft, marginTop },
1138
+ width,
1139
+ height,
1140
+ data,
1141
+ judge,
1142
+ currentIndex,
1143
+ }) => {
1144
+ const _data = useMemo(() => {
1145
+ const legendDataWithPercent = getDataWithPercent(data, precision);
1146
+ return sortPie(legendDataWithPercent, "");
1147
+ }, [data, precision]);
1148
+
1149
+ //数据容错,当data都为零那么需要进行以下容错
1150
+ if (judge == 0) {
1151
+ _data.forEach((d) => {
1152
+ ((d.percent = 0), (d.value = 0));
1153
+ });
1154
+ }
1155
+
1156
+ const currentData = _data[currentIndex];
1157
+
1158
+ if (!currentData) return null;
1159
+
1160
+ const { displayName, fieldName, value, percent } = currentData;
1161
+ let nameTemp = displayName ? displayName : fieldName; //类目名
1162
+
1163
+ let foreignStyle = {
1164
+ //foreignObject标签样式,
1165
+ width,
1166
+ height,
1167
+ position: "relative",
1168
+ overflow: "visible",
1169
+ pointerEvents: "none",
1170
+ },
1171
+ boxStyle = {
1172
+ //弹性盒子样式,用于当前值的上下居中对齐等
1173
+ width,
1174
+ height,
1175
+ position: "absolute",
1176
+ display: "flex",
1177
+ flexDirection: "column",
1178
+ justifyContent: "center",
1179
+ alignItems: "center",
1180
+ transform: isIOS
1181
+ ? `translate(${marginLeft}px,${marginTop}px)`
1182
+ : `translate(-${width / 2}px,-${height / 2}px)`,
1183
+ };
1184
+ let seriesColor = currentData.series.color;
1185
+ seriesColor =
1186
+ seriesColor.type == "pure"
1187
+ ? seriesColor.pure
1188
+ : seriesColor.linear.stops[0].color;
1189
+ return (
1190
+ show && (
1191
+ <foreignObject style={foreignStyle}>
1192
+ <div style={boxStyle}>
1193
+ {showName && (
1194
+ <TextOverflow
1195
+ type={textOverflow}
1196
+ value={nameTemp}
1197
+ speed={speed}
1198
+ style={{
1199
+ width: "100%",
1200
+ maxWidth,
1201
+ textAlign: "center",
1202
+ display: textOverflow == "marquee" ? "flex" : "bolck",
1203
+ justifyContent: "center",
1204
+ ...getFontStyle(nameFont),
1205
+ margin: gap / 2 + "px 0",
1206
+ color: nameColor ? seriesColor : nameFont.color,
1207
+ transform: `translate(${translate.x}px,${translate.y}px)`,
1208
+ }}
1209
+ ></TextOverflow>
1210
+ )}
1211
+ {
1212
+ //真实值
1213
+ showValue && (
1214
+ <span
1215
+ style={{
1216
+ ...getFontStyle(valueFont),
1217
+ transform:
1218
+ "translate(" +
1219
+ translateValueX +
1220
+ "px," +
1221
+ translateValueY +
1222
+ "px)",
1223
+ margin: gap / 2 + "px 0",
1224
+ color: valueColor ? seriesColor : valueFont.color,
1225
+ }}
1226
+ >
1227
+ {value}
1228
+ {showSuffix && text && (
1229
+ <span
1230
+ style={{
1231
+ display: "inline-block",
1232
+ transform:
1233
+ "translate(" +
1234
+ translateSuffixX +
1235
+ "px," +
1236
+ translateSuffixY +
1237
+ "px)",
1238
+ fontSize: fontSize,
1239
+ }}
1240
+ >
1241
+ {text}
1242
+ </span>
1243
+ )}
1244
+ </span>
1245
+ )
1246
+ }
1247
+ {
1248
+ //百分比值
1249
+ showPercent && (
1250
+ <span
1251
+ style={{
1252
+ transform:
1253
+ "translate(" +
1254
+ translatePercentX +
1255
+ "px," +
1256
+ translatePercentY +
1257
+ "px)",
1258
+ ...getFontStyle(percentFont),
1259
+ margin: gap / 2 + "px 0",
1260
+ color: percentColor ? seriesColor : percentFont.color,
1261
+ }}
1262
+ >
1263
+ {percent + "%"}
1264
+ </span>
1265
+ )
1266
+ }
1267
+ </div>
1268
+ </foreignObject>
1269
+ )
1270
+ );
1271
+ };
1272
+
1273
+ const Label = ({
1274
+ config: {
1275
+ maxRadius = 0,
1276
+ lineLength,
1277
+ lineColor,
1278
+ distance,
1279
+ mode,
1280
+ align,
1281
+ show,
1282
+ translate: { x: translateX, y: translateY },
1283
+ name: {
1284
+ show: showName,
1285
+ font: nameFont,
1286
+ maxWidth,
1287
+ textOverflow,
1288
+ speed,
1289
+ translate: NameTranslate,
1290
+ },
1291
+ value: {
1292
+ show: showValue,
1293
+ font: valueFont,
1294
+ sameColor: valueSameColor,
1295
+ suffix: {
1296
+ show: showSuffix,
1297
+ text,
1298
+ fontSize: suffixFontSize,
1299
+ translate: { x: suffixTranslateX, y: suffixTranslateY },
1300
+ },
1301
+ translate: ValueTranslate,
1302
+ },
1303
+ percent: {
1304
+ show: showPercent,
1305
+ sameColor: percentSameColor,
1306
+ font: percentFont,
1307
+ precision,
1308
+ translate: PercentTranslate,
1309
+ },
1310
+ },
1311
+ iosStyle: { isIOS, left, top },
1312
+ arcs,
1313
+ judge,
1314
+ animation,
1315
+ }) => {
1316
+ const _arcs = useMemo(
1317
+ () => getDataWithPercent(arcs, precision),
1318
+ [arcs, precision],
1319
+ );
1320
+ //数据做出容错
1321
+ if (judge == 0) {
1322
+ _arcs.forEach((d) => {
1323
+ d.percent = 0;
1324
+ });
1325
+ }
1326
+ const isHorizontal = mode === "horizontal";
1327
+ const labelElsRef = useRef({});
1328
+ const [measuredHeights, setMeasuredHeights] = useState({});
1329
+
1330
+ // 上下布局:渲染后读取每个标签真实高度(仅在高度变化时更新,避免死循环)
1331
+ useLayoutEffect(() => {
1332
+ if (isHorizontal) {
1333
+ setMeasuredHeights((prev) => (Object.keys(prev).length ? {} : prev));
1334
+ return;
1335
+ }
1336
+ setMeasuredHeights((prev) => {
1337
+ const next = {};
1338
+ let changed = false;
1339
+ Object.keys(labelElsRef.current).forEach((key) => {
1340
+ const el = labelElsRef.current[key];
1341
+ if (!el) return;
1342
+ const h = getLabelDomHeight(el);
1343
+ if (!h) return;
1344
+ next[key] = h;
1345
+ if (prev[key] !== h) changed = true;
1346
+ });
1347
+ const prevKeys = Object.keys(prev);
1348
+ const nextKeys = Object.keys(next);
1349
+ if (prevKeys.length !== nextKeys.length) changed = true;
1350
+ return changed ? next : prev;
1351
+ });
1352
+ }, [isHorizontal, _arcs, mode, showName, showValue, showPercent, showSuffix]);
1353
+
1354
+ const placedYBySide = { left: [], right: [] };
1355
+ return (
1356
+ <g>
1357
+ {_arcs.map(
1358
+ (
1359
+ {
1360
+ series: {
1361
+ color: {
1362
+ type,
1363
+ pure,
1364
+ linear: { stops },
1365
+ },
1366
+ },
1367
+ data,
1368
+ displayName,
1369
+ value,
1370
+ percent,
1371
+ arc,
1372
+ outerRadius,
1373
+ index: actualIndex,
1374
+ },
1375
+ index,
1376
+ ) => {
1377
+ const [x, y] = arc.centroid();
1378
+ const midAngle = Math.atan2(y, x);
1379
+
1380
+ const [x1, y1] = getCoord(
1381
+ midAngle,
1382
+ maxRadius ? maxRadius : outerRadius,
1383
+ );
1384
+
1385
+ const radius = (maxRadius ? maxRadius : outerRadius) + distance;
1386
+ const [x2, y2] = getCoord(midAngle, radius);
1387
+
1388
+ const direction = x2 < 0 ? -1 : 1;
1389
+ const x3 = x2 + lineLength * direction;
1390
+
1391
+ const _x = x3 + (translateX + 6) * direction;
1392
+
1393
+ const _showName = showName && displayName;
1394
+ const _showValue = showValue && (value || showSuffix);
1395
+ const nameStyle = getFontStyle(nameFont);
1396
+ const shouldShow = show && (_showName || showPercent || showValue);
1397
+ // 与真实渲染一致:showValue 开启时值为 0 也会渲染
1398
+ const labelSizeOptions = {
1399
+ showName: _showName,
1400
+ showValue,
1401
+ showPercent,
1402
+ nameFont,
1403
+ valueFont,
1404
+ percentFont,
1405
+ };
1406
+ const estimatedHeight = isHorizontal
1407
+ ? getLabelRowHeight(labelSizeOptions)
1408
+ : getLabelColumnHeight(labelSizeOptions);
1409
+ // 上下布局优先用 DOM 实测高度
1410
+ const labelHeight =
1411
+ !isHorizontal && measuredHeights[index] != null
1412
+ ? measuredHeights[index]
1413
+ : estimatedHeight;
1414
+ const side = x2 < 0 ? "left" : "right";
1415
+ const finalY = shouldShow
1416
+ ? isHorizontal
1417
+ ? resolveLabelY(y2, side, placedYBySide, labelHeight)
1418
+ : resolveLabelYVertical(y2, side, placedYBySide, labelHeight)
1419
+ : y2;
1420
+ return (
1421
+ shouldShow && (
1422
+ <g key={index}>
1423
+ <path
1424
+ className={animation ? ringCss["label-line"] : ""}
1425
+ style={{
1426
+ animationDelay: `${
1427
+ animation
1428
+ ? (actualIndex + 1) * ringDuration - labelDuration
1429
+ : 0
1430
+ }ms`,
1431
+ }}
1432
+ d={
1433
+ "M" +
1434
+ x1 +
1435
+ ", " +
1436
+ y1 +
1437
+ "L" +
1438
+ x2 +
1439
+ ", " +
1440
+ finalY +
1441
+ "L" +
1442
+ x3 +
1443
+ ", " +
1444
+ finalY
1445
+ }
1446
+ stroke={
1447
+ lineColor
1448
+ ? lineColor
1449
+ : type == "pure"
1450
+ ? pure
1451
+ : stops[0].color
1452
+ }
1453
+ fill="none"
1454
+ />
1455
+ <foreignObject
1456
+ width="1"
1457
+ height="1"
1458
+ x={_x}
1459
+ y={finalY + translateY}
1460
+ style={{ overflow: "visible", position: "relative" }}
1461
+ >
1462
+ <div
1463
+ ref={(el) => {
1464
+ if (el) labelElsRef.current[index] = el;
1465
+ else delete labelElsRef.current[index];
1466
+ }}
1467
+ className={animation ? ringCss["label-text"] : ""}
1468
+ style={{
1469
+ position: isIOS ? "absolute" : "relative",
1470
+ transform: isIOS
1471
+ ? `translate(calc(${x3 < 0 ? "-100%" : "0px"} + ${
1472
+ left + _x
1473
+ }px),calc(-50% + ${top + finalY + translateY}px))`
1474
+ : "translate(0,-50%)",
1475
+ whiteSpace: "nowrap",
1476
+ float: x3 >= 0 ? "left" : "right",
1477
+ width: "max-content",
1478
+ display: "flex",
1479
+ flexDirection: mode == "horizontal" ? "row" : "column",
1480
+ alignItems:
1481
+ align == "left"
1482
+ ? "flex-start"
1483
+ : align == "center"
1484
+ ? "center"
1485
+ : "flex-end",
1486
+ justifyContent: "center",
1487
+ }}
1488
+ >
1489
+ {_showName && (
1490
+ <TextOverflow
1491
+ type={textOverflow}
1492
+ value={
1493
+ displayName + (showValue || showPercent ? ":" : "")
1494
+ }
1495
+ speed={speed}
1496
+ style={{
1497
+ maxWidth,
1498
+ ...nameStyle,
1499
+ float: mode == "horizontal" ? "left" : "none",
1500
+ transform: `translate(${NameTranslate.x}px, ${NameTranslate.y}px)`,
1501
+ }}
1502
+ ></TextOverflow>
1503
+ )}
1504
+ {showValue && (
1505
+ <span
1506
+ style={{
1507
+ ...getFontStyle(valueFont),
1508
+ color: valueSameColor ? pure : valueFont.color,
1509
+ transform: `translate(${ValueTranslate.x}px, ${ValueTranslate.y}px)`,
1510
+ }}
1511
+ >
1512
+ {data.y}
1513
+ {showSuffix && (
1514
+ <span
1515
+ style={{
1516
+ position: "relative",
1517
+ fontSize: suffixFontSize,
1518
+ marginLeft: suffixTranslateX,
1519
+ top: suffixTranslateY,
1520
+ }}
1521
+ >
1522
+ {text}
1523
+ </span>
1524
+ )}
1525
+ </span>
1526
+ )}
1527
+ {showPercent && (
1528
+ <span
1529
+ style={{
1530
+ ...getFontStyle(percentFont),
1531
+ color: percentSameColor ? pure : percentFont.color,
1532
+ transform: `translate(${PercentTranslate.x}px, ${PercentTranslate.y}px)`,
1533
+ }}
1534
+ >
1535
+ {(_showValue ? "" : "") +
1536
+ percent +
1537
+ "%" +
1538
+ (_showValue ? ")" : "")}
1539
+ </span>
1540
+ )}
1541
+ </div>
1542
+ </foreignObject>
1543
+ </g>
1544
+ )
1545
+ );
1546
+ },
1547
+ )}
1548
+ </g>
1549
+ );
1550
+ };
1551
+
1552
+ function getAlign(align, reverse) {
1553
+ if (align == "center") return "center";
1554
+ if (align == "left") return reverse ? "flex-end" : "flex-start";
1555
+ return reverse ? "flex-start" : "flex-end";
1556
+ }
1557
+ function getTranslate(translate, reverse) {
1558
+ const { x, y } = translate;
1559
+ return `translate(${reverse ? -x : x}px, ${y}px)`;
1560
+ }
1561
+ const RingLabel = ({
1562
+ config: {
1563
+ ringDuration,
1564
+ labelDuration,
1565
+ maxRadius = 0,
1566
+ lineLength,
1567
+ lineColor,
1568
+ distance,
1569
+ mode,
1570
+ align = "center",
1571
+ show,
1572
+ translate: { x: translateX, y: translateY },
1573
+ name: {
1574
+ show: showName,
1575
+ font: nameFont,
1576
+ maxWidth,
1577
+ textOverflow,
1578
+ speed,
1579
+ translate: nameTranslate,
1580
+ },
1581
+ value: {
1582
+ show: showValue,
1583
+ font: valueFont,
1584
+ sameColor: valueSameColor,
1585
+ suffix: {
1586
+ show: showSuffix,
1587
+ text,
1588
+ fontSize: suffixFontSize,
1589
+ translate: { x: suffixTranslateX, y: suffixTranslateY },
1590
+ },
1591
+ translate: valueTranslate,
1592
+ },
1593
+ percent: {
1594
+ show: showPercent,
1595
+ sameColor: percentSameColor,
1596
+ font: percentFont,
1597
+ precision,
1598
+ translate: percentTranslate,
1599
+ },
1600
+ },
1601
+ iosStyle: { isIOS, left, top },
1602
+ judge,
1603
+ arcs,
1604
+ }) => {
1605
+ const _arcs = useMemo(
1606
+ () => getDataWithPercent(arcs, precision),
1607
+ [arcs, precision],
1608
+ );
1609
+
1610
+ //数据做出容错
1611
+ if (judge == 0) {
1612
+ _arcs.forEach((d) => {
1613
+ d.percent = 0;
1614
+ });
1615
+ }
1616
+
1617
+ const isHorizontal = mode === "horizontal";
1618
+ const labelElsRef = useRef({});
1619
+ const [measuredHeights, setMeasuredHeights] = useState({});
1620
+
1621
+ // 上下布局:渲染后读取每个标签真实高度(仅在高度变化时更新,避免死循环)
1622
+ useLayoutEffect(() => {
1623
+ if (isHorizontal) {
1624
+ setMeasuredHeights((prev) => (Object.keys(prev).length ? {} : prev));
1625
+ return;
1626
+ }
1627
+ setMeasuredHeights((prev) => {
1628
+ const next = {};
1629
+ let changed = false;
1630
+ Object.keys(labelElsRef.current).forEach((key) => {
1631
+ const el = labelElsRef.current[key];
1632
+ if (!el) return;
1633
+ const h = getLabelDomHeight(el);
1634
+ if (!h) return;
1635
+ next[key] = h;
1636
+ if (prev[key] !== h) changed = true;
1637
+ });
1638
+ const prevKeys = Object.keys(prev);
1639
+ const nextKeys = Object.keys(next);
1640
+ if (prevKeys.length !== nextKeys.length) changed = true;
1641
+ return changed ? next : prev;
1642
+ });
1643
+ }, [isHorizontal, _arcs, mode, showName, showValue, showPercent, showSuffix]);
1644
+
1645
+ const placedYBySide = { left: [], right: [] };
1646
+ return (
1647
+ <g>
1648
+ {_arcs.map(
1649
+ (
1650
+ {
1651
+ series: {
1652
+ color: {
1653
+ type,
1654
+ pure,
1655
+ linear: { stops },
1656
+ },
1657
+ },
1658
+ data: realData,
1659
+ displayName,
1660
+ value,
1661
+ percent,
1662
+ arc,
1663
+ outerRadius,
1664
+ index: actualIndex,
1665
+ },
1666
+ index,
1667
+ ) => {
1668
+ const [x, y] = arc.centroid();
1669
+ const midAngle = Math.atan2(y, x);
1670
+
1671
+ const [x1, y1] = getCoord(
1672
+ midAngle,
1673
+ maxRadius ? maxRadius : outerRadius,
1674
+ );
1675
+
1676
+ const radius = (maxRadius ? maxRadius : outerRadius) + distance;
1677
+ const [x2, y2] = getCoord(midAngle, radius);
1678
+
1679
+ const directionX = x2 < 0 ? -1 : 1;
1680
+ const x3 = x2 + lineLength * directionX;
1681
+ const _x = x3 + (translateX + 6) * directionX;
1682
+
1683
+ const _showName = showName && displayName;
1684
+ const _showValue = showValue && (value || showSuffix);
1685
+ const shouldShow = show && (_showName || showPercent || _showValue);
1686
+ // 与真实渲染一致:showValue 开启时值为 0 也会渲染
1687
+ const labelSizeOptions = {
1688
+ showName: _showName,
1689
+ showValue,
1690
+ showPercent,
1691
+ nameFont,
1692
+ valueFont,
1693
+ percentFont,
1694
+ };
1695
+ const estimatedHeight = isHorizontal
1696
+ ? getLabelRowHeight(labelSizeOptions)
1697
+ : getLabelColumnHeight(labelSizeOptions);
1698
+ // 上下布局优先用 DOM 实测高度
1699
+ const labelHeight =
1700
+ !isHorizontal && measuredHeights[index] != null
1701
+ ? measuredHeights[index]
1702
+ : estimatedHeight;
1703
+ const side = x2 < 0 ? "left" : "right";
1704
+ const finalY = shouldShow
1705
+ ? isHorizontal
1706
+ ? resolveLabelY(y2, side, placedYBySide, labelHeight)
1707
+ : resolveLabelYVertical(y2, side, placedYBySide, labelHeight)
1708
+ : y2;
1709
+
1710
+ return (
1711
+ shouldShow && (
1712
+ <g key={index}>
1713
+ <path
1714
+ className={ringCss["label-line"]}
1715
+ style={{
1716
+ animationDelay: `${
1717
+ (actualIndex + 1) * ringDuration - labelDuration
1718
+ }ms`,
1719
+ }}
1720
+ d={
1721
+ "M" +
1722
+ x1 +
1723
+ ", " +
1724
+ y1 +
1725
+ "L" +
1726
+ x2 +
1727
+ ", " +
1728
+ finalY +
1729
+ "L" +
1730
+ x3 +
1731
+ ", " +
1732
+ finalY
1733
+ }
1734
+ stroke={
1735
+ lineColor
1736
+ ? lineColor
1737
+ : type == "pure"
1738
+ ? pure
1739
+ : stops[0].color
1740
+ }
1741
+ fill="none"
1742
+ />
1743
+ <foreignObject
1744
+ width="1"
1745
+ height="1"
1746
+ x={_x}
1747
+ y={finalY + translateY}
1748
+ style={{ overflow: "visible", position: "relative" }}
1749
+ >
1750
+ <div
1751
+ ref={(el) => {
1752
+ if (el) labelElsRef.current[index] = el;
1753
+ else delete labelElsRef.current[index];
1754
+ }}
1755
+ className={ringCss["label-text"]}
1756
+ style={{
1757
+ position: isIOS ? "absolute" : "relative",
1758
+ transform: isIOS
1759
+ ? `translate(calc(${x3 < 0 ? "-100%" : "0px"} + ${
1760
+ left + _x
1761
+ }px),calc(-50% + ${top + finalY + translateY}px))`
1762
+ : "translate(0,-50%)",
1763
+ whiteSpace: "nowrap",
1764
+ float: x3 >= 0 ? "left" : "right",
1765
+ width: "max-content",
1766
+ animationDelay: `${
1767
+ (actualIndex + 1) * ringDuration - labelDuration
1768
+ }ms`,
1769
+ display: "flex",
1770
+ flexDirection: mode == "horizontal" ? "row" : "column",
1771
+ alignItems: getAlign(align, x3 >= 0),
1772
+ justifyContent: "center",
1773
+ }}
1774
+ >
1775
+ {_showName && (
1776
+ <TextOverflow
1777
+ type={textOverflow}
1778
+ value={
1779
+ displayName + (showValue || showPercent ? ":" : "")
1780
+ }
1781
+ speed={speed}
1782
+ style={{
1783
+ maxWidth,
1784
+ ...getFontStyle(nameFont),
1785
+ float: mode == "horizontal" ? "left" : "none",
1786
+ transform: getTranslate(nameTranslate, x3 >= 0),
1787
+ }}
1788
+ ></TextOverflow>
1789
+ )}
1790
+ {showValue && (
1791
+ <span
1792
+ style={{
1793
+ ...getFontStyle(valueFont),
1794
+ transform: getTranslate(valueTranslate, x3 >= 0),
1795
+ color: valueSameColor ? pure : valueFont.color,
1796
+ }}
1797
+ >
1798
+ {realData.y}
1799
+ {showSuffix && (
1800
+ <span
1801
+ style={{
1802
+ position: "relative",
1803
+ fontSize: suffixFontSize,
1804
+ marginLeft: suffixTranslateX,
1805
+ top: suffixTranslateY,
1806
+ }}
1807
+ >
1808
+ {text}
1809
+ </span>
1810
+ )}
1811
+ </span>
1812
+ )}
1813
+ {showPercent && (
1814
+ <span
1815
+ style={{
1816
+ ...getFontStyle(percentFont),
1817
+ transform: getTranslate(percentTranslate, x3 >= 0),
1818
+ color: percentSameColor ? pure : percentFont.color,
1819
+ }}
1820
+ >
1821
+ {(_showValue ? "" : "") +
1822
+ percent +
1823
+ "%" +
1824
+ (_showValue ? ")" : "")}
1825
+ </span>
1826
+ )}
1827
+ </div>
1828
+ </foreignObject>
1829
+ </g>
1830
+ )
1831
+ );
1832
+ },
1833
+ )}
1834
+ </g>
1835
+ );
1836
+ };
1837
+
1838
+ export default Mapping(Carousel(Component));