@easyv/charts 1.10.8 → 1.10.9

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.
@@ -261,15 +261,20 @@ var Chart = /*#__PURE__*/(0, _react.memo)(function (_ref) {
261
261
  }, [tickName, JSON.stringify(originData)]);
262
262
  var showTooltip = !!(tooltipData && tooltipData.length && (auto || manual));
263
263
  var isVertical = axisX.direction === "vertical";
264
- var indicatorWidth = indicator.width * (control ? axisX.controlStep : axisX.step) / 100;
265
- var position = axisX.scaler(tickName) - indicatorWidth / 2;
264
+ var axisStep = control ? axisX.controlStep : axisX.step;
265
+ var safeAxisStep = Number.isFinite(axisStep) ? axisStep : 0;
266
+ var indicatorWidth = indicator.width * safeAxisStep / 100;
267
+ var safeIndicatorWidth = Number.isFinite(indicatorWidth) ? indicatorWidth : 0;
268
+ var tickPos = axisX.scaler(tickName);
269
+ var safeTickPos = Number.isFinite(tickPos) ? tickPos : 0;
270
+ var position = safeTickPos - safeIndicatorWidth / 2;
266
271
  var indicatorAttr = isVertical ? {
267
272
  width: chartWidth,
268
- height: indicatorWidth,
273
+ height: safeIndicatorWidth,
269
274
  y: position
270
275
  } : {
271
276
  height: yLineRange,
272
- width: indicatorWidth,
277
+ width: safeIndicatorWidth,
273
278
  x: position
274
279
  };
275
280
  var onInteraction = (0, _react.useCallback)(function (e, type) {
@@ -353,15 +358,18 @@ var Chart = /*#__PURE__*/(0, _react.memo)(function (_ref) {
353
358
  if (controlEl.current) {
354
359
  controlEl.current.style.transform = "translate(".concat(cBarX, "px,0)");
355
360
  //计算出当前位移的百分比
356
- var percent = cBarX / (cWidth - cBarWidth);
357
- percent = isNaN(percent) ? 1 : percent;
358
- var translateX = -(controlEnd + start / cPercent - chartWidth) * percent;
361
+ var slideRange = cWidth - cBarWidth;
362
+ var percent = slideRange > 0 ? cBarX / slideRange : 0;
363
+ if (!Number.isFinite(percent)) percent = 0;
364
+ var safeCPercent = cPercent > 0 && Number.isFinite(cPercent) ? cPercent : 1;
365
+ var span = controlEnd + start / safeCPercent - chartWidth;
366
+ var translateX = Number.isFinite(span) ? -span * percent : 0;
359
367
  curControlPercent.current = percent;
360
- setClipX(-translateX);
368
+ setClipX(Number.isFinite(-translateX) ? -translateX : 0);
361
369
  var _isVertical = axisX.direction === "vertical";
362
370
  var coreOffset = _isVertical ? marginRight : isIOS ? marginLeft : 0;
363
371
  seriesEl.current.style.transform = "translate(".concat(translateX + coreOffset, "px,").concat(marginTop, "px)");
364
- axisElList.current[2].style.transform = "translate(".concat(translateX, "px,", 0, "px)");
372
+ axisElList.current[2] && (axisElList.current[2].style.transform = "translate(".concat(translateX, "px,", 0, "px)"));
365
373
  }
366
374
  }, [controlInfo]);
367
375
  //控制条轮播动画
@@ -457,6 +465,11 @@ var Chart = /*#__PURE__*/(0, _react.memo)(function (_ref) {
457
465
  });
458
466
  var bodyWidth = isVertical ? xLineRange + 100 + marginRight + marginLeft : xLineRange,
459
467
  bodyHeight = isVertical ? yLineRange : yLineRange + marginTop + marginBottom;
468
+ var controlTooltipSpan = function () {
469
+ var p = cPercent > 0 && Number.isFinite(cPercent) ? cPercent : 1;
470
+ var s = axisX.controlEnd + axisX.start / p - chartWidth;
471
+ return Number.isFinite(s) ? s : 0;
472
+ }();
460
473
  var hasDuplicateX = function hasDuplicateX(data) {
461
474
  var xValues = new Set();
462
475
  var _iterator = _createForOfIteratorHelper(data),
@@ -613,13 +626,15 @@ var Chart = /*#__PURE__*/(0, _react.memo)(function (_ref) {
613
626
  }))), /*#__PURE__*/_react["default"].createElement("g", {
614
627
  clipPath: "url(#chart-clip-".concat(id, ")")
615
628
  }, /*#__PURE__*/_react["default"].createElement("g", null, control && zIndex == "bottom" && ctlIndicatorList.map(function (item, index) {
616
- var x = axisX.scaler(item.tick);
629
+ var xRaw = axisX.scaler(item.tick);
630
+ var x = Number.isFinite(xRaw) ? xRaw : 0;
631
+ var iw = safeIndicatorWidth;
617
632
  return /*#__PURE__*/_react["default"].createElement(_.Indicator, (0, _extends2["default"])({
618
633
  key: index
619
634
  }, indicator, {
620
635
  height: yLineRange,
621
- width: indicatorWidth,
622
- x: x - indicatorWidth / 2,
636
+ width: iw,
637
+ x: x - iw / 2,
623
638
  isControlChart: !!control,
624
639
  xName: item.tick,
625
640
  setCtlTip: setCtlTip,
@@ -669,13 +684,15 @@ var Chart = /*#__PURE__*/(0, _react.memo)(function (_ref) {
669
684
  isControlChart: !!control
670
685
  }));
671
686
  }), /*#__PURE__*/_react["default"].createElement("g", null, control && zIndex != "bottom" && ctlIndicatorList.map(function (item, index) {
672
- var x = axisX.scaler(item.tick);
687
+ var xRaw = axisX.scaler(item.tick);
688
+ var x = Number.isFinite(xRaw) ? xRaw : 0;
689
+ var iw = safeIndicatorWidth;
673
690
  return /*#__PURE__*/_react["default"].createElement(_.Indicator, (0, _extends2["default"])({
674
691
  key: index
675
692
  }, indicator, {
676
693
  height: yLineRange,
677
- width: indicatorWidth,
678
- x: x - indicatorWidth / 2,
694
+ width: iw,
695
+ x: x - iw / 2,
679
696
  isControlChart: !!control,
680
697
  xName: item.tick,
681
698
  setCtlTip: setCtlTip,
@@ -686,7 +703,7 @@ var Chart = /*#__PURE__*/(0, _react.memo)(function (_ref) {
686
703
  isVertical: isVertical
687
704
  }, tooltip, {
688
705
  data: controlChartTooltipData,
689
- x: ctlX - marginLeft - (axisX.controlEnd + axisX.start / cPercent - chartWidth) * curControlPercent.current,
706
+ x: (Number.isFinite(ctlX) ? ctlX : 0) - marginLeft - controlTooltipSpan * curControlPercent.current,
690
707
  marginLeft: marginLeft,
691
708
  marginTop: marginTop,
692
709
  tickName: ctlXName,
@@ -77,8 +77,8 @@ var Area = function Area(_ref) {
77
77
  opacity: Areaopacity
78
78
  },
79
79
  stroke: "none",
80
- fill: 'url(#' + id + ')'
81
- }), /*#__PURE__*/_react["default"].createElement("defs", null, type && type == 'pattern' ? /*#__PURE__*/_react["default"].createElement("pattern", {
80
+ fill: "url(#" + id + ")"
81
+ }), /*#__PURE__*/_react["default"].createElement("defs", null, type && type == "pattern" ? /*#__PURE__*/_react["default"].createElement("pattern", {
82
82
  id: id,
83
83
  patternUnits: "userSpaceOnUse",
84
84
  width: patternW,
@@ -132,7 +132,7 @@ var _default = exports["default"] = /*#__PURE__*/(0, _react.memo)(function (_ref
132
132
  return getLineData(sortData, connectNulls);
133
133
  }, [sortData, connectNulls]);
134
134
  var lineGen = (0, _react.useMemo)(function () {
135
- var isVertical = direction === 'vertical';
135
+ var isVertical = direction === "vertical";
136
136
  var lineGen = (isVertical ? (0, _d3v.line)().y(function (_ref9) {
137
137
  var x = _ref9.data.x;
138
138
  return xScaler(x);
@@ -164,9 +164,9 @@ var _default = exports["default"] = /*#__PURE__*/(0, _react.memo)(function (_ref
164
164
  pointerEvents: "none"
165
165
  },
166
166
  fill: "none",
167
- strokeDasharray: lineType === 'dash' ? '3 3' : null,
167
+ strokeDasharray: lineType === "dash" ? "3 3" : null,
168
168
  strokeWidth: lineWidth
169
- }), type == 'area' && /*#__PURE__*/_react["default"].createElement(Area, {
169
+ }), type == "area" && /*#__PURE__*/_react["default"].createElement(Area, {
170
170
  data: _data,
171
171
  config: _objectSpread(_objectSpread({}, area), {}, {
172
172
  curve: curve,
@@ -310,7 +310,7 @@ var _default = exports["default"] = function _default(_ref) {
310
310
  end: end,
311
311
  clipAxisRange: clipAxisRange,
312
312
  lengthWithoutPaddingOuter: lengthWithoutPaddingOuter,
313
- step: [lengthWithoutPaddingOuter / clipAxisAllTicks[0].length, lengthWithoutPaddingOuter / clipAxisAllTicks[1].length],
313
+ step: [clipAxisAllTicks[0].length > 0 ? lengthWithoutPaddingOuter / clipAxisAllTicks[0].length : 0, clipAxisAllTicks[1].length > 0 ? lengthWithoutPaddingOuter / clipAxisAllTicks[1].length : 0],
314
314
  allTicks: clipAxisAllTicks,
315
315
  ticks: clipAxisTicks,
316
316
  clipValue: clipValue
@@ -372,14 +372,19 @@ var _default = exports["default"] = function _default(_ref) {
372
372
  // }
373
373
  // }
374
374
  // }
375
- var _step2 = _lengthWithoutPaddingOuter / allTicks.length;
375
+ var tickLen = allTicks.length;
376
+ var _step2 = tickLen > 0 ? _lengthWithoutPaddingOuter / tickLen : 0;
376
377
  var controlCfg = {
377
378
  controlStep: 0,
378
379
  controlDragScaler: null
379
380
  };
380
381
  if (isC) {
381
- controlCfg.controlStep = _step2 / cPercent;
382
- controlCfg.controlDragScaler = scaler.copy().range([_start / cPercent, _end / cPercent]);
382
+ if (tickLen > 0 && cPercent > 0) {
383
+ controlCfg.controlStep = _step2 / cPercent;
384
+ controlCfg.controlDragScaler = scaler.copy().range([_start / cPercent, _end / cPercent]);
385
+ } else {
386
+ controlCfg.controlDragScaler = scaler.copy();
387
+ }
383
388
  var _getChartsConfig3 = getChartsConfig(orientation, cWidth, height, paddingOuter),
384
389
  start_ = _getChartsConfig3.start,
385
390
  end_ = _getChartsConfig3.end,
@@ -390,7 +395,7 @@ var _default = exports["default"] = function _default(_ref) {
390
395
  scaler = scales[type]().domain(newDomain).range(_range);
391
396
  scaler.type = type;
392
397
  var controlOuter = len - outer;
393
- _step2 = controlOuter / allTicks.length;
398
+ _step2 = tickLen > 0 ? controlOuter / tickLen : 0;
394
399
  }
395
400
  tmp.set(axisType, _objectSpread(_objectSpread(_objectSpread({}, item), {}, {
396
401
  count: _count,
@@ -17,11 +17,11 @@ var initialState = {
17
17
  flag: false // 首次加载标识:true-首次加载不执行动画,false-可执行轮播
18
18
  };
19
19
 
20
- /**
21
- * x轴滚动逻辑
22
- * @param {Object} axis x轴配置项
23
- * @param {Object} config x轴轮播动画的配置项
24
- * @returns {Map} 返回更新后的x轴配置(ticks/scaler/range 变更)
20
+ /**
21
+ * x轴滚动逻辑
22
+ * @param {Object} axis x轴配置项
23
+ * @param {Object} config x轴轮播动画的配置项
24
+ * @returns {Map} 返回更新后的x轴配置(ticks/scaler/range 变更)
25
25
  */
26
26
  var _default = exports["default"] = function _default(axis, config, isHover, controlInfo, active) {
27
27
  var show = config.show,
@@ -40,11 +40,11 @@ var _default = exports["default"] = function _default(axis, config, isHover, con
40
40
  lengthWithoutPaddingOuter = axis.lengthWithoutPaddingOuter;
41
41
  var tickLength = ticks.length;
42
42
  var tickCount = isC ? allTicks.length : count;
43
- var scale = isC ? cPercent : 1;
43
+ var scale = isC && cPercent > 0 ? cPercent : 1;
44
44
  var _start = start / scale;
45
45
  var _end = end / scale;
46
46
  var canCarousel = (0, _react.useMemo)(function () {
47
- return show && active && tickLength > tickCount;
47
+ return show && active && tickCount > 0 && tickLength > tickCount;
48
48
  }, [show, active, tickLength, tickCount]);
49
49
  var _useState = (0, _react.useState)({
50
50
  scaler: scaler,
@@ -120,7 +120,7 @@ var _default = exports["default"] = function _default(axis, config, isHover, con
120
120
 
121
121
  // 初始化:仅在基础条件变化时设置初始索引
122
122
  (0, _react.useEffect)(function () {
123
- if (show && tickLength > tickCount) {
123
+ if (show && tickCount > 0 && tickLength > tickCount) {
124
124
  setStatus({
125
125
  currentIndex: 0,
126
126
  flag: true
@@ -137,10 +137,11 @@ var _default = exports["default"] = function _default(axis, config, isHover, con
137
137
 
138
138
  // 执行条件:显示、非hover暂停、激活、有足够的轮播项
139
139
  var canRun = canCarousel && !(latest.hover && hoverRef.current);
140
- if (currentIndex !== null && canRun) {
140
+ if (currentIndex !== null && canRun && latest.tickCount > 0) {
141
141
  if (flag) {
142
142
  // 首次加载:仅初始化视图,不执行动画
143
143
  var _step = latest.lengthWithoutPaddingOuter / latest.tickCount;
144
+ if (!Number.isFinite(_step)) return;
144
145
  var _ticks = latest.allTicks.slice(currentIndex, latest.tickCount);
145
146
  setState({
146
147
  step: _step,
@@ -168,6 +169,7 @@ var _default = exports["default"] = function _default(axis, config, isHover, con
168
169
  // 非首次加载:执行动画 + 后续逻辑
169
170
  if (!ready) return;
170
171
  var _step2 = latest.lengthWithoutPaddingOuter / latest.tickCount;
172
+ if (!Number.isFinite(_step2)) return;
171
173
  animation = (0, _popmotion.animate)({
172
174
  from: 0,
173
175
  to: -1,
@@ -278,7 +280,7 @@ var _default = exports["default"] = function _default(axis, config, isHover, con
278
280
  setReady(true);
279
281
  setStatus(function () {
280
282
  // 与初始化 effect 保持一致:只要满足条件就从 0 开始重新初始化
281
- if (show && active && tickLength > tickCount) {
283
+ if (show && active && tickCount > 0 && tickLength > tickCount) {
282
284
  return {
283
285
  currentIndex: 0,
284
286
  flag: true
@@ -294,12 +296,12 @@ var _default = exports["default"] = function _default(axis, config, isHover, con
294
296
  controlEnd: _end
295
297
  });
296
298
  };
297
- /**
298
- * 获取指定索引范围的ticks(支持循环)
299
- * @param {Array} ticks 所有ticks
300
- * @param {number} currentIndex 当前起始索引
301
- * @param {number} length 需要的长度
302
- * @returns {Array} 目标ticks数组
299
+ /**
300
+ * 获取指定索引范围的ticks(支持循环)
301
+ * @param {Array} ticks 所有ticks
302
+ * @param {number} currentIndex 当前起始索引
303
+ * @param {number} length 需要的长度
304
+ * @returns {Array} 目标ticks数组
303
305
  */
304
306
  var getTicks = function getTicks(ticks, currentIndex, length) {
305
307
  var _currentIndex = +currentIndex;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@easyv/charts",
3
- "version": "1.10.8",
3
+ "version": "1.10.9",
4
4
  "description": "",
5
5
  "main": "lib/index.js",
6
6
  "scripts": {
@@ -55,4 +55,4 @@
55
55
  "commit": true
56
56
  }
57
57
  }
58
- }
58
+ }
@@ -252,14 +252,20 @@ const Chart = memo(
252
252
 
253
253
  const isVertical = axisX.direction === "vertical";
254
254
 
255
- const indicatorWidth =
256
- (indicator.width * (control ? axisX.controlStep : axisX.step)) / 100;
257
- const position = axisX.scaler(tickName) - indicatorWidth / 2;
255
+ const axisStep = control ? axisX.controlStep : axisX.step;
256
+ const safeAxisStep = Number.isFinite(axisStep) ? axisStep : 0;
257
+ const indicatorWidth = (indicator.width * safeAxisStep) / 100;
258
+ const safeIndicatorWidth = Number.isFinite(indicatorWidth)
259
+ ? indicatorWidth
260
+ : 0;
261
+ const tickPos = axisX.scaler(tickName);
262
+ const safeTickPos = Number.isFinite(tickPos) ? tickPos : 0;
263
+ const position = safeTickPos - safeIndicatorWidth / 2;
258
264
  const indicatorAttr = isVertical
259
- ? { width: chartWidth, height: indicatorWidth, y: position }
265
+ ? { width: chartWidth, height: safeIndicatorWidth, y: position }
260
266
  : {
261
267
  height: yLineRange,
262
- width: indicatorWidth,
268
+ width: safeIndicatorWidth,
263
269
  x: position,
264
270
  };
265
271
 
@@ -341,18 +347,22 @@ const Chart = memo(
341
347
  if (controlEl.current) {
342
348
  controlEl.current.style.transform = `translate(${cBarX}px,0)`;
343
349
  //计算出当前位移的百分比
344
- let percent = cBarX / (cWidth - cBarWidth);
345
- percent = isNaN(percent) ? 1 : percent;
346
- const translateX =
347
- -(controlEnd + start / cPercent - chartWidth) * percent;
350
+ const slideRange = cWidth - cBarWidth;
351
+ let percent = slideRange > 0 ? cBarX / slideRange : 0;
352
+ if (!Number.isFinite(percent)) percent = 0;
353
+ const safeCPercent =
354
+ cPercent > 0 && Number.isFinite(cPercent) ? cPercent : 1;
355
+ const span = controlEnd + start / safeCPercent - chartWidth;
356
+ const translateX = Number.isFinite(span) ? -span * percent : 0;
348
357
  curControlPercent.current = percent;
349
- setClipX(-translateX);
358
+ setClipX(Number.isFinite(-translateX) ? -translateX : 0);
350
359
  const isVertical = axisX.direction === "vertical";
351
360
  const coreOffset = isVertical ? marginRight : isIOS ? marginLeft : 0;
352
361
  seriesEl.current.style.transform = `translate(${
353
362
  translateX + coreOffset
354
363
  }px,${marginTop}px)`;
355
- axisElList.current[2].style.transform = `translate(${translateX}px,${0}px)`;
364
+ axisElList.current[2] &&
365
+ (axisElList.current[2].style.transform = `translate(${translateX}px,${0}px)`);
356
366
  }
357
367
  }, [controlInfo]);
358
368
  //控制条轮播动画
@@ -465,6 +475,11 @@ const Chart = memo(
465
475
  bodyHeight = isVertical
466
476
  ? yLineRange
467
477
  : yLineRange + marginTop + marginBottom;
478
+ const controlTooltipSpan = (() => {
479
+ const p = cPercent > 0 && Number.isFinite(cPercent) ? cPercent : 1;
480
+ const s = axisX.controlEnd + axisX.start / p - chartWidth;
481
+ return Number.isFinite(s) ? s : 0;
482
+ })();
468
483
  const hasDuplicateX = (data) => {
469
484
  const xValues = new Set();
470
485
  for (const item of data) {
@@ -624,15 +639,17 @@ const Chart = memo(
624
639
  {control &&
625
640
  zIndex == "bottom" &&
626
641
  ctlIndicatorList.map((item, index) => {
627
- const x = axisX.scaler(item.tick);
642
+ const xRaw = axisX.scaler(item.tick);
643
+ const x = Number.isFinite(xRaw) ? xRaw : 0;
644
+ const iw = safeIndicatorWidth;
628
645
  return (
629
646
  <Indicator
630
647
  key={index}
631
648
  {...indicator}
632
649
  {...{
633
650
  height: yLineRange,
634
- width: indicatorWidth,
635
- x: x - indicatorWidth / 2,
651
+ width: iw,
652
+ x: x - iw / 2,
636
653
  }}
637
654
  isControlChart={!!control}
638
655
  xName={item.tick}
@@ -695,15 +712,17 @@ const Chart = memo(
695
712
  {control &&
696
713
  zIndex != "bottom" &&
697
714
  ctlIndicatorList.map((item, index) => {
698
- const x = axisX.scaler(item.tick);
715
+ const xRaw = axisX.scaler(item.tick);
716
+ const x = Number.isFinite(xRaw) ? xRaw : 0;
717
+ const iw = safeIndicatorWidth;
699
718
  return (
700
719
  <Indicator
701
720
  key={index}
702
721
  {...indicator}
703
722
  {...{
704
723
  height: yLineRange,
705
- width: indicatorWidth,
706
- x: x - indicatorWidth / 2,
724
+ width: iw,
725
+ x: x - iw / 2,
707
726
  }}
708
727
  isControlChart={!!control}
709
728
  xName={item.tick}
@@ -723,10 +742,9 @@ const Chart = memo(
723
742
  {...tooltip}
724
743
  data={controlChartTooltipData}
725
744
  x={
726
- ctlX -
745
+ (Number.isFinite(ctlX) ? ctlX : 0) -
727
746
  marginLeft -
728
- (axisX.controlEnd + axisX.start / cPercent - chartWidth) *
729
- curControlPercent.current
747
+ controlTooltipSpan * curControlPercent.current
730
748
  }
731
749
  marginLeft={marginLeft}
732
750
  marginTop={marginTop}
@@ -1,23 +1,27 @@
1
1
  /**
2
2
  * 折线图
3
3
  */
4
- import React, { memo, useMemo } from 'react';
5
- import { line as d3Line, area as d3Area, curveCatmullRom, curveMonotoneX } from 'd3v7';
6
- import { getColorList } from '../utils';
7
- import { Lighter, LinearGradient } from '.';
4
+ import React, { memo, useMemo } from "react";
5
+ import {
6
+ line as d3Line,
7
+ area as d3Area,
8
+ curveCatmullRom,
9
+ curveMonotoneX,
10
+ } from "d3v7";
11
+ import { getColorList } from "../utils";
12
+ import { Lighter, LinearGradient } from ".";
8
13
 
9
14
  const defined = (d) => d.data.y != null;
10
- const getLineData = (data, connectNulls) =>{
11
- return data.flatMap(d=>{
15
+ const getLineData = (data, connectNulls) => {
16
+ return data.flatMap((d) => {
12
17
  const y = d.data.y;
13
- return isNaN(y)?
14
- connectNulls?
15
- []:
16
- {...d,data:{...d.data,y:null}}:
17
- d
18
+ return isNaN(y)
19
+ ? connectNulls
20
+ ? []
21
+ : { ...d, data: { ...d.data, y: null } }
22
+ : d;
18
23
  });
19
- }
20
-
24
+ };
21
25
 
22
26
  const Area = ({
23
27
  data,
@@ -30,11 +34,11 @@ const Area = ({
30
34
  opacity,
31
35
  size: { width: patternW, height: patternH },
32
36
  curve,
33
- tension
37
+ tension,
34
38
  },
35
39
  xScaler,
36
40
  yScaler,
37
- opacity:Areaopacity
41
+ opacity: Areaopacity,
38
42
  }) => {
39
43
  const [height] = yScaler.range();
40
44
  const area = useMemo(() => getColorList(fill), [fill]);
@@ -43,7 +47,7 @@ const Area = ({
43
47
  const areaGen = d3Area()
44
48
  .x(({ data: { x } }) => xScaler(x))
45
49
  .y1(({ data: { y } }) => yScaler(y))
46
- .y0(({})=>yScaler(0))
50
+ .y0(({}) => yScaler(0))
47
51
  .defined(defined);
48
52
  curve && areaGen.curve(curveCatmullRom.alpha(tension));
49
53
  curve && areaGen.curve(curveMonotoneX);
@@ -52,11 +56,28 @@ const Area = ({
52
56
 
53
57
  return (
54
58
  <>
55
- <path d={areaGen(data)} style={{pointerEvents:"none",opacity:Areaopacity}} stroke='none' fill={'url(#' + id + ')'} />
59
+ <path
60
+ d={areaGen(data)}
61
+ style={{ pointerEvents: "none", opacity: Areaopacity }}
62
+ stroke="none"
63
+ fill={"url(#" + id + ")"}
64
+ />
56
65
  <defs>
57
- {type && type == 'pattern' ? (
58
- <pattern id={id} patternUnits="userSpaceOnUse" width={patternW} height={patternH}>
59
- {url && <image opacity={opacity} width={patternW} height={patternH} xlinkHref={window.appConfig.ASSETS_URL + url} />}
66
+ {type && type == "pattern" ? (
67
+ <pattern
68
+ id={id}
69
+ patternUnits="userSpaceOnUse"
70
+ width={patternW}
71
+ height={patternH}
72
+ >
73
+ {url && (
74
+ <image
75
+ opacity={opacity}
76
+ width={patternW}
77
+ height={patternH}
78
+ xlinkHref={window.appConfig.ASSETS_URL + url}
79
+ />
80
+ )}
60
81
  </pattern>
61
82
  ) : (
62
83
  <LinearGradient id={id} colors={area} rotate={0} />
@@ -91,32 +112,32 @@ export default memo(
91
112
  }) => {
92
113
  if (!data.length) return null;
93
114
  const ticks = xScaler.domain();
94
-
115
+
95
116
  const sortData = useMemo(() => {
96
117
  const usefulData = data.filter(
97
- ({ data: { x } }) => ticks.indexOf(x) > -1
118
+ ({ data: { x } }) => ticks.indexOf(x) > -1,
98
119
  );
99
120
  return usefulData.sort(
100
121
  ({ data: { x: a } }, { data: { x: b } }) =>
101
- ticks.indexOf(a) - ticks.indexOf(b)
122
+ ticks.indexOf(a) - ticks.indexOf(b),
102
123
  );
103
124
  }, [data, ticks]);
104
-
125
+
105
126
  const _data = useMemo(
106
127
  () => getLineData(sortData, connectNulls),
107
- [sortData, connectNulls]
128
+ [sortData, connectNulls],
108
129
  );
109
130
  const lineGen = useMemo(() => {
110
- const isVertical = direction === 'vertical';
131
+ const isVertical = direction === "vertical";
111
132
 
112
133
  let lineGen = (
113
134
  isVertical
114
135
  ? d3Line()
115
- .y(({ data: { x } }) => xScaler(x))
116
- .x(({ data: { y } }) => yScaler(y))
136
+ .y(({ data: { x } }) => xScaler(x))
137
+ .x(({ data: { y } }) => yScaler(y))
117
138
  : d3Line()
118
- .x(({ data: { x } }) => xScaler(x))
119
- .y(({ data: { y } }) => yScaler(y))
139
+ .x(({ data: { x } }) => xScaler(x))
140
+ .y(({ data: { y } }) => yScaler(y))
120
141
  ).defined(defined);
121
142
  curve && lineGen.curve(curveCatmullRom.alpha(tension));
122
143
  curve && lineGen.curve(curveMonotoneX);
@@ -128,29 +149,31 @@ export default memo(
128
149
  const show = lineShadow && lineShadow.show;
129
150
  const shadow = lineShadow && lineShadow.shadow;
130
151
  return (
131
- <g className='__easyv-line'>
152
+ <g className="__easyv-line">
132
153
  <path
133
154
  d={path}
134
155
  stroke={stroke}
135
156
  style={{
136
- filter:show?`drop-shadow(${shadow.hShadow}px ${shadow.vShadow}px ${shadow.blur}px ${shadow.color})`:"none",
137
- pointerEvents:"none"
157
+ filter: show
158
+ ? `drop-shadow(${shadow.hShadow}px ${shadow.vShadow}px ${shadow.blur}px ${shadow.color})`
159
+ : "none",
160
+ pointerEvents: "none",
138
161
  }}
139
- fill='none'
140
- strokeDasharray={lineType === 'dash' ? '3 3' : null}
162
+ fill="none"
163
+ strokeDasharray={lineType === "dash" ? "3 3" : null}
141
164
  strokeWidth={lineWidth}
142
165
  />
143
- {type == 'area' && (
166
+ {type == "area" && (
144
167
  <Area
145
168
  data={_data}
146
169
  config={{ ...area, curve, tension }}
147
170
  xScaler={xScaler}
148
171
  yScaler={yScaler}
149
- opacity={areaColor?areaColor.linear.opacity:1}
172
+ opacity={areaColor ? areaColor.linear.opacity : 1}
150
173
  />
151
174
  )}
152
175
  {showLighter && <Lighter path={path} config={lighter} />}
153
176
  </g>
154
177
  );
155
- }
178
+ },
156
179
  );
@@ -372,8 +372,12 @@ export default ({
372
372
  clipAxisRange,
373
373
  lengthWithoutPaddingOuter,
374
374
  step: [
375
- lengthWithoutPaddingOuter / clipAxisAllTicks[0].length,
376
- lengthWithoutPaddingOuter / clipAxisAllTicks[1].length,
375
+ clipAxisAllTicks[0].length > 0
376
+ ? lengthWithoutPaddingOuter / clipAxisAllTicks[0].length
377
+ : 0,
378
+ clipAxisAllTicks[1].length > 0
379
+ ? lengthWithoutPaddingOuter / clipAxisAllTicks[1].length
380
+ : 0,
377
381
  ],
378
382
  allTicks: clipAxisAllTicks,
379
383
  ticks: clipAxisTicks,
@@ -454,16 +458,22 @@ export default ({
454
458
  // }
455
459
  // }
456
460
  // }
457
- let step = lengthWithoutPaddingOuter / allTicks.length;
461
+ const tickLen = allTicks.length;
462
+ let step =
463
+ tickLen > 0 ? lengthWithoutPaddingOuter / tickLen : 0;
458
464
  const controlCfg = {
459
465
  controlStep: 0,
460
466
  controlDragScaler: null,
461
467
  };
462
468
  if (isC) {
463
- controlCfg.controlStep = step / cPercent;
464
- controlCfg.controlDragScaler = scaler
465
- .copy()
466
- .range([start / cPercent, end / cPercent]);
469
+ if (tickLen > 0 && cPercent > 0) {
470
+ controlCfg.controlStep = step / cPercent;
471
+ controlCfg.controlDragScaler = scaler
472
+ .copy()
473
+ .range([start / cPercent, end / cPercent]);
474
+ } else {
475
+ controlCfg.controlDragScaler = scaler.copy();
476
+ }
467
477
  const {
468
478
  start: start_,
469
479
  end: end_,
@@ -480,7 +490,7 @@ export default ({
480
490
  scaler = scales[type]().domain(newDomain).range(range);
481
491
  scaler.type = type;
482
492
  const controlOuter = len - outer;
483
- step = controlOuter / allTicks.length;
493
+ step = tickLen > 0 ? controlOuter / tickLen : 0;
484
494
  }
485
495
  tmp.set(axisType, {
486
496
  ...item,
@@ -1,318 +1,320 @@
1
- import { useEffect, useMemo, useRef, useState } from "react";
2
- import { animate, linear } from "popmotion";
3
-
4
- const initialState = {
5
- currentIndex: null,
6
- flag: false, // 首次加载标识:true-首次加载不执行动画,false-可执行轮播
7
- };
8
-
9
- /**
10
- * x轴滚动逻辑
11
- * @param {Object} axis x轴配置项
12
- * @param {Object} config x轴轮播动画的配置项
13
- * @returns {Map} 返回更新后的x轴配置(ticks/scaler/range 变更)
14
- */
15
- export default (axis, config, isHover, controlInfo, active) => {
16
- const { show, interval, duration, hover } = config;
17
- const { isC, cPercent } = controlInfo;
18
-
19
- const {
20
- tickCount: count,
21
- allTicks,
22
- scaler,
23
- start,
24
- end,
25
- step,
26
- ticks,
27
- lengthWithoutPaddingOuter,
28
- } = axis;
29
- const tickLength = ticks.length;
30
- const tickCount = isC ? allTicks.length : count;
31
-
32
- const scale = isC ? cPercent : 1;
33
- const _start = start / scale;
34
- const _end = end / scale;
35
- const canCarousel = useMemo(
36
- () => show && active && tickLength > tickCount,
37
- [show, active, tickLength, tickCount]
38
- );
39
-
40
- const [state, setState] = useState({
41
- scaler,
42
- step,
43
- ticks,
44
- });
45
- const [status, setStatus] = useState(initialState);
46
- const [ready, setReady] = useState(true); // true 才允许开启下一轮动画(用于 interval 等待)
47
- const [wakeToken, setWakeToken] = useState(0);
48
- const hoverRef = useRef(isHover);
49
- const prevHoverRef = useRef(isHover);
50
- const intervalTimerRef = useRef(null);
51
- const isAnimatingRef = useRef(false);
52
- const latestRef = useRef({
53
- allTicks,
54
- scaler,
55
- tickCount,
56
- tickLength,
57
- lengthWithoutPaddingOuter,
58
- _start,
59
- _end,
60
- interval,
61
- duration,
62
- hover,
63
- });
64
-
65
- // 避免 hover 触发 render 时 effect 误重启动画:用 ref 持有最新输入
66
- useEffect(() => {
67
- latestRef.current = {
68
- allTicks,
69
- scaler,
70
- tickCount,
71
- tickLength,
72
- lengthWithoutPaddingOuter,
73
- _start,
74
- _end,
75
- interval,
76
- duration,
77
- hover,
78
- };
79
- }, [
80
- allTicks,
81
- scaler,
82
- tickCount,
83
- tickLength,
84
- lengthWithoutPaddingOuter,
85
- _start,
86
- _end,
87
- interval,
88
- duration,
89
- hover,
90
- ]);
91
-
92
- // hover 变化不打断正在进行的动画:只更新引用、并在需要时取消「下一轮」定时器
93
- useEffect(() => {
94
- const prev = prevHoverRef.current;
95
- prevHoverRef.current = isHover;
96
- hoverRef.current = isHover;
97
- if (latestRef.current.hover && isHover && intervalTimerRef.current) {
98
- clearTimeout(intervalTimerRef.current);
99
- intervalTimerRef.current = null;
100
- }
101
-
102
- // 仅在 hover 从 true -> false 时唤醒轮播(避免在非 hover 状态下反复唤醒导致 interval 失效)
103
- if (
104
- latestRef.current.hover &&
105
- prev === true &&
106
- isHover === false &&
107
- canCarousel &&
108
- status.currentIndex !== null &&
109
- !isAnimatingRef.current
110
- ) {
111
- setReady(true);
112
- setWakeToken((v) => v + 1);
113
- }
114
- }, [isHover, canCarousel, status.currentIndex]);
115
-
116
- // 初始化:仅在基础条件变化时设置初始索引
117
- useEffect(() => {
118
- if (show && tickLength > tickCount) {
119
- setStatus({
120
- currentIndex: 0,
121
- flag: true,
122
- });
123
- } else {
124
- setStatus(initialState);
125
- }
126
- }, [show, tickCount, tickLength]);
127
- useEffect(() => {
128
- let animation;
129
- const { currentIndex, flag } = status;
130
- const latest = latestRef.current;
131
-
132
- // 执行条件:显示、非hover暂停、激活、有足够的轮播项
133
- const canRun = canCarousel && !(latest.hover && hoverRef.current);
134
-
135
- if (currentIndex !== null && canRun) {
136
- if (flag) {
137
- // 首次加载:仅初始化视图,不执行动画
138
- const step = latest.lengthWithoutPaddingOuter / latest.tickCount;
139
- const _ticks = latest.allTicks.slice(currentIndex, latest.tickCount);
140
- setState({
141
- step,
142
- ticks: _ticks,
143
- scaler: latest.scaler
144
- .copy()
145
- .domain(_ticks)
146
- .range([latest._start, latest._end]),
147
- });
148
-
149
- // 首次加载完成后,启动第一轮动画(等待interval后执行)
150
- if (latest.interval >= 0) {
151
- intervalTimerRef.current = setTimeout(() => {
152
- setStatus((prev) => ({ ...prev, flag: false }));
153
- }, latest.interval * 1000);
154
- } else {
155
- setStatus((prev) => ({ ...prev, flag: false }));
156
- }
157
- } else {
158
- // 非首次加载:执行动画 + 后续逻辑
159
- if (!ready) return;
160
- const step = latest.lengthWithoutPaddingOuter / latest.tickCount;
161
-
162
- animation = animate({
163
- from: 0,
164
- to: -1,
165
- duration: latest.duration * 1000,
166
- ease: linear,
167
- onPlay: () => {
168
- isAnimatingRef.current = true;
169
- // 动画开始:更新ticks和scaler初始范围
170
- // 这里需要保证动画是「当前项滚出,下一项滚入」
171
- // 因此后续窗口从 currentIndex + 1 开始,避免首项重复导致视觉上「从第一项滚到第一项」的闪烁
172
- setState((axis) => {
173
- const { ticks, scaler } = axis;
174
- const [tick] = ticks;
175
- const _ticks = [
176
- tick,
177
- ...getTicks(
178
- latest.allTicks,
179
- currentIndex + 1,
180
- latest.tickCount
181
- ),
182
- ];
183
- return {
184
- ...axis,
185
- ticks: _ticks,
186
- scaler: scaler
187
- .copy()
188
- .range([latest._start, latest._end + step])
189
- .domain(_ticks),
190
- };
191
- });
192
- },
193
- onUpdate: (v) => {
194
- // 动画过程:实时更新scaler范围(实现滚动效果)
195
- setState((axis) => {
196
- const { scaler, step } = axis;
197
- return {
198
- ...axis,
199
- scaler: scaler
200
- .copy()
201
- .range([
202
- latest._start + step * v,
203
- latest._end + step + step * v,
204
- ]),
205
- };
206
- });
207
- },
208
- onComplete: () => {
209
- isAnimatingRef.current = false;
210
- // 本轮结束后进入等待态,避免立即开启下一轮(保证 interval 生效)
211
- setReady(false);
212
- // 动画完成:重置ticks和scaler
213
- setState((axis) => {
214
- const { scaler, ticks } = axis;
215
- const _ticks = ticks.slice(1, ticks.length);
216
- return {
217
- ...axis,
218
- ticks: _ticks,
219
- scaler: scaler
220
- .copy()
221
- .range([latest._start, latest._end])
222
- .domain(_ticks),
223
- };
224
- });
225
-
226
- const nextIndex = (currentIndex + 1) % latest.tickLength;
227
-
228
- // 无论是否 hover,都先把 currentIndex 同步到下一屏起点
229
- // 否则在 interval 暂停期触发 hover/离开,会用旧索引重启,视觉上“回滚一格”
230
- setStatus((prev) => ({ ...prev, currentIndex: nextIndex }));
231
-
232
- // hover 触发时:让当前动画跑完后暂停(不再排下一轮)
233
- if (latest.hover && hoverRef.current) return;
234
-
235
- // hover:动画完成后,等待interval时长,再触发下一轮动画
236
- intervalTimerRef.current = setTimeout(() => {
237
- setReady(true);
238
- setWakeToken((v) => v + 1);
239
- }, latest.interval * 1000);
240
- },
241
- });
242
- }
243
- }
244
-
245
- // 清理副作用:停止动画、清除定时器
246
- return () => {
247
- if (animation) {
248
- animation.stop();
249
- isAnimatingRef.current = false;
250
- }
251
- if (intervalTimerRef.current) {
252
- clearTimeout(intervalTimerRef.current);
253
- intervalTimerRef.current = null;
254
- }
255
- };
256
- }, [canCarousel, status.currentIndex, status.flag, wakeToken, ready]);
257
-
258
- // 重置逻辑:当无有效索引时恢复初始状态
259
- useEffect(() => {
260
- if (status.currentIndex === null) {
261
- const _ticks = scaler.type === "linear" ? scaler.domain() : allTicks;
262
- setState({
263
- step,
264
- scaler: scaler.copy().domain(_ticks).range([_start, _end]),
265
- ticks,
266
- });
267
- }
268
- }, [status.currentIndex, scaler, _start, _end, step, ticks, allTicks]);
269
-
270
- // x 轴可见项数量(count)发生变化时,安全地重置轮播节奏
271
- useEffect(() => {
272
- // 清掉当前的 interval 计时器,避免用旧配置继续排队
273
- if (intervalTimerRef.current) {
274
- clearTimeout(intervalTimerRef.current);
275
- intervalTimerRef.current = null;
276
- }
277
-
278
- // 重置动画节奏,让配置变更后「立即」按最新的 tickCount/tickLength 重新初始化
279
- // 不能简单 setStatus(initialState),否则会覆盖上方的初始化 effect(先开轮播再改数量时会被清成 null,导致展示全部且不轮播)
280
- isAnimatingRef.current = false;
281
- setReady(true);
282
- setStatus(() => {
283
- // 与初始化 effect 保持一致:只要满足条件就从 0 开始重新初始化
284
- if (show && active && tickLength > tickCount) {
285
- return { currentIndex: 0, flag: true };
286
- }
287
- return initialState;
288
- });
289
- setWakeToken((v) => v + 1);
290
- }, [count, show, active, tickLength, tickCount]);
291
-
292
- return {
293
- ...axis,
294
- ...state,
295
- controlEnd: _end,
296
- };
297
- };
298
-
299
- /**
300
- * 获取指定索引范围的ticks(支持循环)
301
- * @param {Array} ticks 所有ticks
302
- * @param {number} currentIndex 当前起始索引
303
- * @param {number} length 需要的长度
304
- * @returns {Array} 目标ticks数组
305
- */
306
- const getTicks = (ticks, currentIndex, length) => {
307
- const _currentIndex = +currentIndex;
308
- const ticksLength = ticks.length;
309
- if (ticksLength <= length) return ticks;
310
-
311
- const _end = _currentIndex + length;
312
- if (ticksLength < _end) {
313
- const prev = ticks.slice(_currentIndex, ticksLength);
314
- const next = ticks.slice(0, _end - ticksLength);
315
- return [...prev, ...next];
316
- }
317
- return ticks.slice(_currentIndex, _end);
318
- };
1
+ import { useEffect, useMemo, useRef, useState } from "react";
2
+ import { animate, linear } from "popmotion";
3
+
4
+ const initialState = {
5
+ currentIndex: null,
6
+ flag: false, // 首次加载标识:true-首次加载不执行动画,false-可执行轮播
7
+ };
8
+
9
+ /**
10
+ * x轴滚动逻辑
11
+ * @param {Object} axis x轴配置项
12
+ * @param {Object} config x轴轮播动画的配置项
13
+ * @returns {Map} 返回更新后的x轴配置(ticks/scaler/range 变更)
14
+ */
15
+ export default (axis, config, isHover, controlInfo, active) => {
16
+ const { show, interval, duration, hover } = config;
17
+ const { isC, cPercent } = controlInfo;
18
+
19
+ const {
20
+ tickCount: count,
21
+ allTicks,
22
+ scaler,
23
+ start,
24
+ end,
25
+ step,
26
+ ticks,
27
+ lengthWithoutPaddingOuter,
28
+ } = axis;
29
+ const tickLength = ticks.length;
30
+ const tickCount = isC ? allTicks.length : count;
31
+
32
+ const scale = isC && cPercent > 0 ? cPercent : 1;
33
+ const _start = start / scale;
34
+ const _end = end / scale;
35
+ const canCarousel = useMemo(
36
+ () => show && active && tickCount > 0 && tickLength > tickCount,
37
+ [show, active, tickLength, tickCount]
38
+ );
39
+
40
+ const [state, setState] = useState({
41
+ scaler,
42
+ step,
43
+ ticks,
44
+ });
45
+ const [status, setStatus] = useState(initialState);
46
+ const [ready, setReady] = useState(true); // true 才允许开启下一轮动画(用于 interval 等待)
47
+ const [wakeToken, setWakeToken] = useState(0);
48
+ const hoverRef = useRef(isHover);
49
+ const prevHoverRef = useRef(isHover);
50
+ const intervalTimerRef = useRef(null);
51
+ const isAnimatingRef = useRef(false);
52
+ const latestRef = useRef({
53
+ allTicks,
54
+ scaler,
55
+ tickCount,
56
+ tickLength,
57
+ lengthWithoutPaddingOuter,
58
+ _start,
59
+ _end,
60
+ interval,
61
+ duration,
62
+ hover,
63
+ });
64
+
65
+ // 避免 hover 触发 render 时 effect 误重启动画:用 ref 持有最新输入
66
+ useEffect(() => {
67
+ latestRef.current = {
68
+ allTicks,
69
+ scaler,
70
+ tickCount,
71
+ tickLength,
72
+ lengthWithoutPaddingOuter,
73
+ _start,
74
+ _end,
75
+ interval,
76
+ duration,
77
+ hover,
78
+ };
79
+ }, [
80
+ allTicks,
81
+ scaler,
82
+ tickCount,
83
+ tickLength,
84
+ lengthWithoutPaddingOuter,
85
+ _start,
86
+ _end,
87
+ interval,
88
+ duration,
89
+ hover,
90
+ ]);
91
+
92
+ // hover 变化不打断正在进行的动画:只更新引用、并在需要时取消「下一轮」定时器
93
+ useEffect(() => {
94
+ const prev = prevHoverRef.current;
95
+ prevHoverRef.current = isHover;
96
+ hoverRef.current = isHover;
97
+ if (latestRef.current.hover && isHover && intervalTimerRef.current) {
98
+ clearTimeout(intervalTimerRef.current);
99
+ intervalTimerRef.current = null;
100
+ }
101
+
102
+ // 仅在 hover 从 true -> false 时唤醒轮播(避免在非 hover 状态下反复唤醒导致 interval 失效)
103
+ if (
104
+ latestRef.current.hover &&
105
+ prev === true &&
106
+ isHover === false &&
107
+ canCarousel &&
108
+ status.currentIndex !== null &&
109
+ !isAnimatingRef.current
110
+ ) {
111
+ setReady(true);
112
+ setWakeToken((v) => v + 1);
113
+ }
114
+ }, [isHover, canCarousel, status.currentIndex]);
115
+
116
+ // 初始化:仅在基础条件变化时设置初始索引
117
+ useEffect(() => {
118
+ if (show && tickCount > 0 && tickLength > tickCount) {
119
+ setStatus({
120
+ currentIndex: 0,
121
+ flag: true,
122
+ });
123
+ } else {
124
+ setStatus(initialState);
125
+ }
126
+ }, [show, tickCount, tickLength]);
127
+ useEffect(() => {
128
+ let animation;
129
+ const { currentIndex, flag } = status;
130
+ const latest = latestRef.current;
131
+
132
+ // 执行条件:显示、非hover暂停、激活、有足够的轮播项
133
+ const canRun = canCarousel && !(latest.hover && hoverRef.current);
134
+
135
+ if (currentIndex !== null && canRun && latest.tickCount > 0) {
136
+ if (flag) {
137
+ // 首次加载:仅初始化视图,不执行动画
138
+ const step = latest.lengthWithoutPaddingOuter / latest.tickCount;
139
+ if (!Number.isFinite(step)) return;
140
+ const _ticks = latest.allTicks.slice(currentIndex, latest.tickCount);
141
+ setState({
142
+ step,
143
+ ticks: _ticks,
144
+ scaler: latest.scaler
145
+ .copy()
146
+ .domain(_ticks)
147
+ .range([latest._start, latest._end]),
148
+ });
149
+
150
+ // 首次加载完成后,启动第一轮动画(等待interval后执行)
151
+ if (latest.interval >= 0) {
152
+ intervalTimerRef.current = setTimeout(() => {
153
+ setStatus((prev) => ({ ...prev, flag: false }));
154
+ }, latest.interval * 1000);
155
+ } else {
156
+ setStatus((prev) => ({ ...prev, flag: false }));
157
+ }
158
+ } else {
159
+ // 非首次加载:执行动画 + 后续逻辑
160
+ if (!ready) return;
161
+ const step = latest.lengthWithoutPaddingOuter / latest.tickCount;
162
+ if (!Number.isFinite(step)) return;
163
+
164
+ animation = animate({
165
+ from: 0,
166
+ to: -1,
167
+ duration: latest.duration * 1000,
168
+ ease: linear,
169
+ onPlay: () => {
170
+ isAnimatingRef.current = true;
171
+ // 动画开始:更新ticks和scaler初始范围
172
+ // 这里需要保证动画是「当前项滚出,下一项滚入」
173
+ // 因此后续窗口从 currentIndex + 1 开始,避免首项重复导致视觉上「从第一项滚到第一项」的闪烁
174
+ setState((axis) => {
175
+ const { ticks, scaler } = axis;
176
+ const [tick] = ticks;
177
+ const _ticks = [
178
+ tick,
179
+ ...getTicks(
180
+ latest.allTicks,
181
+ currentIndex + 1,
182
+ latest.tickCount
183
+ ),
184
+ ];
185
+ return {
186
+ ...axis,
187
+ ticks: _ticks,
188
+ scaler: scaler
189
+ .copy()
190
+ .range([latest._start, latest._end + step])
191
+ .domain(_ticks),
192
+ };
193
+ });
194
+ },
195
+ onUpdate: (v) => {
196
+ // 动画过程:实时更新scaler范围(实现滚动效果)
197
+ setState((axis) => {
198
+ const { scaler, step } = axis;
199
+ return {
200
+ ...axis,
201
+ scaler: scaler
202
+ .copy()
203
+ .range([
204
+ latest._start + step * v,
205
+ latest._end + step + step * v,
206
+ ]),
207
+ };
208
+ });
209
+ },
210
+ onComplete: () => {
211
+ isAnimatingRef.current = false;
212
+ // 本轮结束后进入等待态,避免立即开启下一轮(保证 interval 生效)
213
+ setReady(false);
214
+ // 动画完成:重置ticks和scaler
215
+ setState((axis) => {
216
+ const { scaler, ticks } = axis;
217
+ const _ticks = ticks.slice(1, ticks.length);
218
+ return {
219
+ ...axis,
220
+ ticks: _ticks,
221
+ scaler: scaler
222
+ .copy()
223
+ .range([latest._start, latest._end])
224
+ .domain(_ticks),
225
+ };
226
+ });
227
+
228
+ const nextIndex = (currentIndex + 1) % latest.tickLength;
229
+
230
+ // 无论是否 hover,都先把 currentIndex 同步到下一屏起点
231
+ // 否则在 interval 暂停期触发 hover/离开,会用旧索引重启,视觉上“回滚一格”
232
+ setStatus((prev) => ({ ...prev, currentIndex: nextIndex }));
233
+
234
+ // hover 触发时:让当前动画跑完后暂停(不再排下一轮)
235
+ if (latest.hover && hoverRef.current) return;
236
+
237
+ // 非hover:动画完成后,等待interval时长,再触发下一轮动画
238
+ intervalTimerRef.current = setTimeout(() => {
239
+ setReady(true);
240
+ setWakeToken((v) => v + 1);
241
+ }, latest.interval * 1000);
242
+ },
243
+ });
244
+ }
245
+ }
246
+
247
+ // 清理副作用:停止动画、清除定时器
248
+ return () => {
249
+ if (animation) {
250
+ animation.stop();
251
+ isAnimatingRef.current = false;
252
+ }
253
+ if (intervalTimerRef.current) {
254
+ clearTimeout(intervalTimerRef.current);
255
+ intervalTimerRef.current = null;
256
+ }
257
+ };
258
+ }, [canCarousel, status.currentIndex, status.flag, wakeToken, ready]);
259
+
260
+ // 重置逻辑:当无有效索引时恢复初始状态
261
+ useEffect(() => {
262
+ if (status.currentIndex === null) {
263
+ const _ticks = scaler.type === "linear" ? scaler.domain() : allTicks;
264
+ setState({
265
+ step,
266
+ scaler: scaler.copy().domain(_ticks).range([_start, _end]),
267
+ ticks,
268
+ });
269
+ }
270
+ }, [status.currentIndex, scaler, _start, _end, step, ticks, allTicks]);
271
+
272
+ // x 轴可见项数量(count)发生变化时,安全地重置轮播节奏
273
+ useEffect(() => {
274
+ // 清掉当前的 interval 计时器,避免用旧配置继续排队
275
+ if (intervalTimerRef.current) {
276
+ clearTimeout(intervalTimerRef.current);
277
+ intervalTimerRef.current = null;
278
+ }
279
+
280
+ // 重置动画节奏,让配置变更后「立即」按最新的 tickCount/tickLength 重新初始化
281
+ // 不能简单 setStatus(initialState),否则会覆盖上方的初始化 effect(先开轮播再改数量时会被清成 null,导致展示全部且不轮播)
282
+ isAnimatingRef.current = false;
283
+ setReady(true);
284
+ setStatus(() => {
285
+ // 与初始化 effect 保持一致:只要满足条件就从 0 开始重新初始化
286
+ if (show && active && tickCount > 0 && tickLength > tickCount) {
287
+ return { currentIndex: 0, flag: true };
288
+ }
289
+ return initialState;
290
+ });
291
+ setWakeToken((v) => v + 1);
292
+ }, [count, show, active, tickLength, tickCount]);
293
+
294
+ return {
295
+ ...axis,
296
+ ...state,
297
+ controlEnd: _end,
298
+ };
299
+ };
300
+
301
+ /**
302
+ * 获取指定索引范围的ticks(支持循环)
303
+ * @param {Array} ticks 所有ticks
304
+ * @param {number} currentIndex 当前起始索引
305
+ * @param {number} length 需要的长度
306
+ * @returns {Array} 目标ticks数组
307
+ */
308
+ const getTicks = (ticks, currentIndex, length) => {
309
+ const _currentIndex = +currentIndex;
310
+ const ticksLength = ticks.length;
311
+ if (ticksLength <= length) return ticks;
312
+
313
+ const _end = _currentIndex + length;
314
+ if (ticksLength < _end) {
315
+ const prev = ticks.slice(_currentIndex, ticksLength);
316
+ const next = ticks.slice(0, _end - ticksLength);
317
+ return [...prev, ...next];
318
+ }
319
+ return ticks.slice(_currentIndex, _end);
320
+ };