@easyv/charts 1.10.33 → 1.10.35
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/components/Legend.js +79 -6
- package/lib/components/PieChart.js +494 -287
- package/package.json +1 -1
- package/src/components/Legend.js +91 -0
- package/src/components/PieChart.js +207 -13
package/package.json
CHANGED
package/src/components/Legend.js
CHANGED
|
@@ -29,6 +29,7 @@ export default memo(
|
|
|
29
29
|
layout: {
|
|
30
30
|
alignment = "right center",
|
|
31
31
|
gridTemplateColumns,
|
|
32
|
+
layoutType,
|
|
32
33
|
gridGap: { gridColumnGap, gridRowGap },
|
|
33
34
|
translate: { x, y },
|
|
34
35
|
},
|
|
@@ -247,6 +248,11 @@ export default memo(
|
|
|
247
248
|
const isFixedWidth = isPieChart
|
|
248
249
|
? isPieFixedWidthGrid
|
|
249
250
|
: LegendType == "FixedWidth";
|
|
251
|
+
const isAxisFixedWidth =
|
|
252
|
+
!isPieChart &&
|
|
253
|
+
(LegendType == "FixedWidth" ||
|
|
254
|
+
rootLayoutMode === "FixedWidth" ||
|
|
255
|
+
layoutType === "FixedWidth");
|
|
250
256
|
const isSideLegend = isPieAdaptive && isSidePlacement;
|
|
251
257
|
const isTopBottomAdaptive = isPieAdaptive && isCenterTopOrBottom;
|
|
252
258
|
const fullWidth = componentWidth ?? chartWidth ?? 0;
|
|
@@ -507,6 +513,79 @@ export default memo(
|
|
|
507
513
|
);
|
|
508
514
|
}
|
|
509
515
|
|
|
516
|
+
if (isAxisFixedWidth) {
|
|
517
|
+
const axisLegendTextWidth = normalizeCssSize(maxWidth);
|
|
518
|
+
const axisGridColumns = Math.min(
|
|
519
|
+
Math.max(1, Number(gridTemplateColumns) || 1),
|
|
520
|
+
length,
|
|
521
|
+
);
|
|
522
|
+
return (
|
|
523
|
+
<div
|
|
524
|
+
className="__easyv-legend-wrapper"
|
|
525
|
+
style={{
|
|
526
|
+
position: "absolute",
|
|
527
|
+
display: "flex",
|
|
528
|
+
...getPosition(position, _alignment, x, y),
|
|
529
|
+
height: loopHeight,
|
|
530
|
+
overflowY: loop.show ? "scroll" : "auto",
|
|
531
|
+
}}
|
|
532
|
+
ref={ref_container}
|
|
533
|
+
>
|
|
534
|
+
<ul
|
|
535
|
+
style={{
|
|
536
|
+
display: "grid",
|
|
537
|
+
gridGap: gridRowGap + "px " + gridColumnGap + "px",
|
|
538
|
+
gridTemplateColumns: "repeat(" + axisGridColumns + ", 1fr)",
|
|
539
|
+
}}
|
|
540
|
+
>
|
|
541
|
+
{_series.map((series, i) => {
|
|
542
|
+
const {
|
|
543
|
+
type,
|
|
544
|
+
name,
|
|
545
|
+
displayName,
|
|
546
|
+
fieldName,
|
|
547
|
+
icon,
|
|
548
|
+
selected,
|
|
549
|
+
index,
|
|
550
|
+
} = series;
|
|
551
|
+
const _icon = getIcon(type, icon, series?.config?.line?.type);
|
|
552
|
+
return (
|
|
553
|
+
<li
|
|
554
|
+
key={i}
|
|
555
|
+
onClick={onClick(fieldName)}
|
|
556
|
+
data-name={displayName || name}
|
|
557
|
+
data-index={index}
|
|
558
|
+
style={{
|
|
559
|
+
display: "flex",
|
|
560
|
+
opacity: selected === false ? opacity / 100 : 1,
|
|
561
|
+
alignItems: "center",
|
|
562
|
+
cursor: "pointer",
|
|
563
|
+
gap: _icon.gap,
|
|
564
|
+
}}
|
|
565
|
+
>
|
|
566
|
+
<span style={{ ..._icon, flexShrink: 0 }} />
|
|
567
|
+
<TextOverflow
|
|
568
|
+
type={textOverflow}
|
|
569
|
+
value={displayName || name}
|
|
570
|
+
style={{
|
|
571
|
+
...font,
|
|
572
|
+
width: axisLegendTextWidth,
|
|
573
|
+
minWidth: axisLegendTextWidth,
|
|
574
|
+
flexShrink: 0,
|
|
575
|
+
boxSizing: "border-box",
|
|
576
|
+
fontStyle: italic ? "italic" : "normal",
|
|
577
|
+
fontWeight: bold ? "bold" : "normal",
|
|
578
|
+
}}
|
|
579
|
+
speed={speed}
|
|
580
|
+
/>
|
|
581
|
+
</li>
|
|
582
|
+
);
|
|
583
|
+
})}
|
|
584
|
+
</ul>
|
|
585
|
+
</div>
|
|
586
|
+
);
|
|
587
|
+
}
|
|
588
|
+
|
|
510
589
|
return isFixedWidth ? (
|
|
511
590
|
<div
|
|
512
591
|
className="__easyv-legend-wrapper"
|
|
@@ -723,6 +802,18 @@ export default memo(
|
|
|
723
802
|
},
|
|
724
803
|
);
|
|
725
804
|
|
|
805
|
+
const normalizeCssSize = (value) => {
|
|
806
|
+
if (value == null || value === "") return value;
|
|
807
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
808
|
+
return `${value}px`;
|
|
809
|
+
}
|
|
810
|
+
const str = String(value).trim();
|
|
811
|
+
if (/^-?\d+(\.\d+)?$/.test(str)) {
|
|
812
|
+
return `${str}px`;
|
|
813
|
+
}
|
|
814
|
+
return str;
|
|
815
|
+
};
|
|
816
|
+
|
|
726
817
|
const getPosition = (position, alignment, x = 0, y = 0) => {
|
|
727
818
|
switch (position) {
|
|
728
819
|
case "top":
|
|
@@ -172,6 +172,89 @@ const getCoord = (deg, radius) => {
|
|
|
172
172
|
return [x, y];
|
|
173
173
|
};
|
|
174
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
|
+
|
|
175
258
|
const getRoseRadius = ({ innerRadius, baseRadius }) =>
|
|
176
259
|
innerRadius + (1 - innerRadius) * baseRadius;
|
|
177
260
|
|
|
@@ -1225,6 +1308,35 @@ const Label = ({
|
|
|
1225
1308
|
d.percent = 0;
|
|
1226
1309
|
});
|
|
1227
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: [] };
|
|
1228
1340
|
return (
|
|
1229
1341
|
<g>
|
|
1230
1342
|
{_arcs.map(
|
|
@@ -1266,9 +1378,32 @@ const Label = ({
|
|
|
1266
1378
|
const _showName = showName && displayName;
|
|
1267
1379
|
const _showValue = showValue && (value || showSuffix);
|
|
1268
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;
|
|
1269
1405
|
return (
|
|
1270
|
-
|
|
1271
|
-
(_showName || showPercent || showValue) && (
|
|
1406
|
+
shouldShow && (
|
|
1272
1407
|
<g key={index}>
|
|
1273
1408
|
<path
|
|
1274
1409
|
className={animation ? ringCss["label-line"] : ""}
|
|
@@ -1287,11 +1422,11 @@ const Label = ({
|
|
|
1287
1422
|
"L" +
|
|
1288
1423
|
x2 +
|
|
1289
1424
|
", " +
|
|
1290
|
-
|
|
1425
|
+
finalY +
|
|
1291
1426
|
"L" +
|
|
1292
1427
|
x3 +
|
|
1293
1428
|
", " +
|
|
1294
|
-
|
|
1429
|
+
finalY
|
|
1295
1430
|
}
|
|
1296
1431
|
stroke={
|
|
1297
1432
|
lineColor
|
|
@@ -1306,17 +1441,21 @@ const Label = ({
|
|
|
1306
1441
|
width="1"
|
|
1307
1442
|
height="1"
|
|
1308
1443
|
x={_x}
|
|
1309
|
-
y={
|
|
1444
|
+
y={finalY + translateY}
|
|
1310
1445
|
style={{ overflow: "visible", position: "relative" }}
|
|
1311
1446
|
>
|
|
1312
1447
|
<div
|
|
1448
|
+
ref={(el) => {
|
|
1449
|
+
if (el) labelElsRef.current[index] = el;
|
|
1450
|
+
else delete labelElsRef.current[index];
|
|
1451
|
+
}}
|
|
1313
1452
|
className={animation ? ringCss["label-text"] : ""}
|
|
1314
1453
|
style={{
|
|
1315
1454
|
position: isIOS ? "absolute" : "relative",
|
|
1316
1455
|
transform: isIOS
|
|
1317
1456
|
? `translate(calc(${x3 < 0 ? "-100%" : "0px"} + ${
|
|
1318
1457
|
left + _x
|
|
1319
|
-
}px),calc(-50% + ${top +
|
|
1458
|
+
}px),calc(-50% + ${top + finalY + translateY}px))`
|
|
1320
1459
|
: "translate(0,-50%)",
|
|
1321
1460
|
whiteSpace: "nowrap",
|
|
1322
1461
|
float: x3 >= 0 ? "left" : "right",
|
|
@@ -1460,6 +1599,35 @@ const RingLabel = ({
|
|
|
1460
1599
|
});
|
|
1461
1600
|
}
|
|
1462
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: [] };
|
|
1463
1631
|
return (
|
|
1464
1632
|
<g>
|
|
1465
1633
|
{_arcs.map(
|
|
@@ -1494,16 +1662,38 @@ const RingLabel = ({
|
|
|
1494
1662
|
const [x2, y2] = getCoord(midAngle, radius);
|
|
1495
1663
|
|
|
1496
1664
|
const directionX = x2 < 0 ? -1 : 1;
|
|
1497
|
-
const directionY = y2 < 0 ? -1 : 1;
|
|
1498
1665
|
const x3 = x2 + lineLength * directionX;
|
|
1499
1666
|
const _x = x3 + (translateX + 6) * directionX;
|
|
1500
1667
|
|
|
1501
1668
|
const _showName = showName && displayName;
|
|
1502
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;
|
|
1503
1694
|
|
|
1504
1695
|
return (
|
|
1505
|
-
|
|
1506
|
-
(_showName || showPercent || _showValue) && (
|
|
1696
|
+
shouldShow && (
|
|
1507
1697
|
<g key={index}>
|
|
1508
1698
|
<path
|
|
1509
1699
|
className={ringCss["label-line"]}
|
|
@@ -1520,11 +1710,11 @@ const RingLabel = ({
|
|
|
1520
1710
|
"L" +
|
|
1521
1711
|
x2 +
|
|
1522
1712
|
", " +
|
|
1523
|
-
|
|
1713
|
+
finalY +
|
|
1524
1714
|
"L" +
|
|
1525
1715
|
x3 +
|
|
1526
1716
|
", " +
|
|
1527
|
-
|
|
1717
|
+
finalY
|
|
1528
1718
|
}
|
|
1529
1719
|
stroke={
|
|
1530
1720
|
lineColor
|
|
@@ -1539,17 +1729,21 @@ const RingLabel = ({
|
|
|
1539
1729
|
width="1"
|
|
1540
1730
|
height="1"
|
|
1541
1731
|
x={_x}
|
|
1542
|
-
y={
|
|
1732
|
+
y={finalY + translateY}
|
|
1543
1733
|
style={{ overflow: "visible", position: "relative" }}
|
|
1544
1734
|
>
|
|
1545
1735
|
<div
|
|
1736
|
+
ref={(el) => {
|
|
1737
|
+
if (el) labelElsRef.current[index] = el;
|
|
1738
|
+
else delete labelElsRef.current[index];
|
|
1739
|
+
}}
|
|
1546
1740
|
className={ringCss["label-text"]}
|
|
1547
1741
|
style={{
|
|
1548
1742
|
position: isIOS ? "absolute" : "relative",
|
|
1549
1743
|
transform: isIOS
|
|
1550
1744
|
? `translate(calc(${x3 < 0 ? "-100%" : "0px"} + ${
|
|
1551
1745
|
left + _x
|
|
1552
|
-
}px),calc(-50% + ${top +
|
|
1746
|
+
}px),calc(-50% + ${top + finalY + translateY}px))`
|
|
1553
1747
|
: "translate(0,-50%)",
|
|
1554
1748
|
whiteSpace: "nowrap",
|
|
1555
1749
|
float: x3 >= 0 ? "left" : "right",
|