@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@easyv/charts",
3
- "version": "1.10.33",
3
+ "version": "1.10.35",
4
4
  "description": "",
5
5
  "main": "lib/index.js",
6
6
  "scripts": {
@@ -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
- show &&
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
- y2 +
1425
+ finalY +
1291
1426
  "L" +
1292
1427
  x3 +
1293
1428
  ", " +
1294
- y2
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={y2 + translateY}
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 + y2 + translateY}px))`
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
- show &&
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
- y2 +
1713
+ finalY +
1524
1714
  "L" +
1525
1715
  x3 +
1526
1716
  ", " +
1527
- y2
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={y2 + translateY}
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 + y2 + translateY}px))`
1746
+ }px),calc(-50% + ${top + finalY + translateY}px))`
1553
1747
  : "translate(0,-50%)",
1554
1748
  whiteSpace: "nowrap",
1555
1749
  float: x3 >= 0 ? "left" : "right",