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