@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.
- package/lib/components/CartesianChart.js +33 -16
- package/lib/components/Line.js +5 -5
- package/lib/hooks/useAxes.js +10 -5
- package/lib/hooks/useCarouselAxisX.js +18 -16
- package/package.json +2 -2
- package/src/components/CartesianChart.js +38 -20
- package/src/components/Line.js +61 -38
- package/src/hooks/useAxes.js +18 -8
- package/src/hooks/useCarouselAxisX.js +320 -318
|
@@ -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
|
|
265
|
-
var
|
|
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:
|
|
273
|
+
height: safeIndicatorWidth,
|
|
269
274
|
y: position
|
|
270
275
|
} : {
|
|
271
276
|
height: yLineRange,
|
|
272
|
-
width:
|
|
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
|
|
357
|
-
percent =
|
|
358
|
-
|
|
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
|
|
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:
|
|
622
|
-
x: x -
|
|
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
|
|
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:
|
|
678
|
-
x: x -
|
|
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
|
|
706
|
+
x: (Number.isFinite(ctlX) ? ctlX : 0) - marginLeft - controlTooltipSpan * curControlPercent.current,
|
|
690
707
|
marginLeft: marginLeft,
|
|
691
708
|
marginTop: marginTop,
|
|
692
709
|
tickName: ctlXName,
|
package/lib/components/Line.js
CHANGED
|
@@ -77,8 +77,8 @@ var Area = function Area(_ref) {
|
|
|
77
77
|
opacity: Areaopacity
|
|
78
78
|
},
|
|
79
79
|
stroke: "none",
|
|
80
|
-
fill:
|
|
81
|
-
}), /*#__PURE__*/_react["default"].createElement("defs", null, type && type ==
|
|
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 ===
|
|
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 ===
|
|
167
|
+
strokeDasharray: lineType === "dash" ? "3 3" : null,
|
|
168
168
|
strokeWidth: lineWidth
|
|
169
|
-
}), type ==
|
|
169
|
+
}), type == "area" && /*#__PURE__*/_react["default"].createElement(Area, {
|
|
170
170
|
data: _data,
|
|
171
171
|
config: _objectSpread(_objectSpread({}, area), {}, {
|
|
172
172
|
curve: curve,
|
package/lib/hooks/useAxes.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
382
|
-
|
|
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 /
|
|
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
|
@@ -252,14 +252,20 @@ const Chart = memo(
|
|
|
252
252
|
|
|
253
253
|
const isVertical = axisX.direction === "vertical";
|
|
254
254
|
|
|
255
|
-
const
|
|
256
|
-
|
|
257
|
-
const
|
|
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:
|
|
265
|
+
? { width: chartWidth, height: safeIndicatorWidth, y: position }
|
|
260
266
|
: {
|
|
261
267
|
height: yLineRange,
|
|
262
|
-
width:
|
|
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
|
-
|
|
345
|
-
percent =
|
|
346
|
-
|
|
347
|
-
|
|
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]
|
|
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
|
|
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:
|
|
635
|
-
x: x -
|
|
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
|
|
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:
|
|
706
|
-
x: x -
|
|
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
|
-
|
|
729
|
-
curControlPercent.current
|
|
747
|
+
controlTooltipSpan * curControlPercent.current
|
|
730
748
|
}
|
|
731
749
|
marginLeft={marginLeft}
|
|
732
750
|
marginTop={marginTop}
|
package/src/components/Line.js
CHANGED
|
@@ -1,23 +1,27 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* 折线图
|
|
3
3
|
*/
|
|
4
|
-
import React, { memo, useMemo } from
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
|
|
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
|
|
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 ==
|
|
58
|
-
<pattern
|
|
59
|
-
|
|
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 ===
|
|
131
|
+
const isVertical = direction === "vertical";
|
|
111
132
|
|
|
112
133
|
let lineGen = (
|
|
113
134
|
isVertical
|
|
114
135
|
? d3Line()
|
|
115
|
-
|
|
116
|
-
|
|
136
|
+
.y(({ data: { x } }) => xScaler(x))
|
|
137
|
+
.x(({ data: { y } }) => yScaler(y))
|
|
117
138
|
: d3Line()
|
|
118
|
-
|
|
119
|
-
|
|
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=
|
|
152
|
+
<g className="__easyv-line">
|
|
132
153
|
<path
|
|
133
154
|
d={path}
|
|
134
155
|
stroke={stroke}
|
|
135
156
|
style={{
|
|
136
|
-
filter:show
|
|
137
|
-
|
|
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=
|
|
140
|
-
strokeDasharray={lineType ===
|
|
162
|
+
fill="none"
|
|
163
|
+
strokeDasharray={lineType === "dash" ? "3 3" : null}
|
|
141
164
|
strokeWidth={lineWidth}
|
|
142
165
|
/>
|
|
143
|
-
{type ==
|
|
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
|
);
|
package/src/hooks/useAxes.js
CHANGED
|
@@ -372,8 +372,12 @@ export default ({
|
|
|
372
372
|
clipAxisRange,
|
|
373
373
|
lengthWithoutPaddingOuter,
|
|
374
374
|
step: [
|
|
375
|
-
|
|
376
|
-
|
|
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
|
-
|
|
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
|
-
|
|
464
|
-
|
|
465
|
-
.
|
|
466
|
-
|
|
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 /
|
|
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
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
.
|
|
146
|
-
.
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
//
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
const
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
latest.
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
.
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
//
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
.
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
*
|
|
303
|
-
* @param {
|
|
304
|
-
* @
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
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
|
+
};
|