@easyv/charts 1.10.34 → 1.10.36

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