diff --git a/packages/f2/src/components/zoom/easing.ts b/packages/f2/src/components/zoom/easing.ts new file mode 100644 index 000000000..8051cc115 --- /dev/null +++ b/packages/f2/src/components/zoom/easing.ts @@ -0,0 +1,5 @@ +function quadraticOut(k) { + return k * (2 - k); +} + +export { quadraticOut }; diff --git a/packages/f2/src/components/zoom/index.tsx b/packages/f2/src/components/zoom/index.tsx index 7de6604b8..522b7faf5 100644 --- a/packages/f2/src/components/zoom/index.tsx +++ b/packages/f2/src/components/zoom/index.tsx @@ -3,6 +3,7 @@ import { ChartChildProps } from '../../chart'; import { updateRange, updateFollow } from './zoomUtil'; import { Scale, ScaleConfig } from '../../deps/f2-scale/src'; import { each, isNumberEqual, isArray } from '@antv/util'; +import { quadraticOut as easeing } from './easing'; export type ZoomRange = [number, number]; export type ScaleValues = number[] | string[]; @@ -52,6 +53,10 @@ export interface ZoomProps { * 横扫 */ swipe?: boolean; + /** + * 横扫动画时长 + */ + swipeDuration?: number; /** * 事件回调 */ @@ -174,6 +179,40 @@ export default (View) => { } as S; } + willUpdate(): void { + const { props, state, dims } = this; + const { minCount, range } = props; + let valueLength = Number.MIN_VALUE; + const cacheRange = {}; + + each(dims, (dim) => { + const scale = this._getScale(dim); + // scale 没有变化, 不处理 + if (scale === this.scale[dim]) { + return; + } + const { values } = scale; + valueLength = values.length > valueLength ? values.length : valueLength; + this.scale[dim] = scale; + this.originScale[dim] = cloneScale(scale); + // 让 range 触发更新 + this.state.range[dim] = [0, 1]; + this.updateRange(range, dim); + cacheRange[dim] = range; + }); + + // 有变化 + if (Object.keys(cacheRange).length > 0) { + this.minScale = minCount / valueLength; + const newRange = { + ...state.range, + ...cacheRange, + }; + + this.renderRange(newRange); + } + } + didUnmount(): void { this.loop && cancelAnimationFrame(this.loop); } @@ -284,12 +323,64 @@ export default (View) => { } } + animateSwipe(dim: string, dimRange: ZoomRange, velocity: number) { + const { context, props } = this; + const { requestAnimationFrame } = context.canvas; + const { swipeDuration = 1000 } = props; + + const diff = (dimRange[1] - dimRange[0]) * velocity; + + const startTime = Date.now(); + + const updateRange = (t: number) => { + const newDimRange: ZoomRange = [dimRange[0] + diff * t, dimRange[1] + diff * t]; + const newRange = this.updateRange(newDimRange, dim); + this.renderRange({ + x: newRange, + }); + }; + + // 更新动画 + const update = () => { + // 计算动画已经进行的时间 + const currentTime = Date.now() - startTime; + + // 如果动画已经结束,则清除定时器 + if (currentTime >= swipeDuration) { + updateRange(1); + return; + } + + // 计算缓动值 + const progress = currentTime / swipeDuration; + const easedProgress = easeing(progress); + updateRange(easedProgress); + + requestAnimationFrame(() => { + update(); + }); + }; + update(); + } + onSwipe = (ev) => { - const { swipe } = this.props; - if (this.props.mode.length < 2 || !swipe) return; + const { props, state } = this; + // 滑动速率 + const { velocity, direction, velocityX = 0, velocityY = 0, points } = ev; + const { mode, swipe } = props; + const { range } = state; - const { velocityX = 0, velocityY = 0, points } = ev; - const { range } = this.state; + if (!swipe || !mode) { + return; + } + if (mode.length === 1) { + this.animateSwipe( + mode, + range[mode], + direction === 'right' || direction === 'down' ? -velocity : velocity + ); + return; + } const { x, y } = points[0]; diff --git a/packages/f2/src/components/zoom/zoomUtil.ts b/packages/f2/src/components/zoom/zoomUtil.ts index a4c23a7d3..67a59c882 100644 --- a/packages/f2/src/components/zoom/zoomUtil.ts +++ b/packages/f2/src/components/zoom/zoomUtil.ts @@ -1,6 +1,6 @@ import { ScaleValues, ZoomRange } from './index'; import { Scale, getTickMethod } from '../../deps/f2-scale/src'; -import { getRange } from '@antv/util'; +import { getRange, isArray } from '@antv/util'; import { toTimeStamp } from '../../util'; // 判断新老values是否相等,这里只要判断前后是否相等即可 @@ -91,7 +91,12 @@ function updateFollow(scales: Scale[], mainScale: Scale, data) { data.forEach((item) => { const value = mainType === 'timeCat' ? toTimeStamp(item[mainField]) : item[mainField]; if (mainValuesMap[value]) { - values.push(item[followField]); + const followItemValue = item[followField]; + if (isArray(followItemValue)) { + values.push(...followItemValue); + } else { + values.push(followItemValue); + } } }); return updateScale(scale, values); diff --git a/packages/f2/test/components/candlestick/__image_snapshots__/pan-test-tsx-candlestick-swipe-1-snap.png b/packages/f2/test/components/candlestick/__image_snapshots__/pan-test-tsx-candlestick-swipe-1-snap.png new file mode 100644 index 000000000..02c6b278a Binary files /dev/null and b/packages/f2/test/components/candlestick/__image_snapshots__/pan-test-tsx-candlestick-swipe-1-snap.png differ diff --git a/packages/f2/test/components/candlestick/__image_snapshots__/pan-test-tsx-candlestick-swipe-2-snap.png b/packages/f2/test/components/candlestick/__image_snapshots__/pan-test-tsx-candlestick-swipe-2-snap.png new file mode 100644 index 000000000..a8b252f6d Binary files /dev/null and b/packages/f2/test/components/candlestick/__image_snapshots__/pan-test-tsx-candlestick-swipe-2-snap.png differ diff --git a/packages/f2/test/components/candlestick/__image_snapshots__/pan-test-tsx-candlestick-swipe-3-snap.png b/packages/f2/test/components/candlestick/__image_snapshots__/pan-test-tsx-candlestick-swipe-3-snap.png new file mode 100644 index 000000000..b34c9579e Binary files /dev/null and b/packages/f2/test/components/candlestick/__image_snapshots__/pan-test-tsx-candlestick-swipe-3-snap.png differ diff --git a/packages/f2/test/components/candlestick/basic.test.tsx b/packages/f2/test/components/candlestick/basic.test.tsx index 277f0ad00..689afb0c2 100644 --- a/packages/f2/test/components/candlestick/basic.test.tsx +++ b/packages/f2/test/components/candlestick/basic.test.tsx @@ -99,7 +99,7 @@ describe('candlestick', () => { expect(context).toMatchImageSnapshot(); }); - it.only('tooltip', async () => { + it('tooltip', async () => { const { props } = ( diff --git a/packages/f2/test/components/candlestick/pan.test.tsx b/packages/f2/test/components/candlestick/pan.test.tsx new file mode 100644 index 000000000..2c811f3da --- /dev/null +++ b/packages/f2/test/components/candlestick/pan.test.tsx @@ -0,0 +1,297 @@ +import { jsx } from '../../../src'; +import { Canvas, Chart, Candlestick, Axis, ScrollBar } from '../../../src'; +import { createContext, delay, gestureSimulator } from '../../util'; +const context = createContext(); + +const data = [ + { + value: [559.8867, 556.0103, 551.9092, 566.5159], + symbol: '813701.CNS', + factor: 'kline_day_candle', + time: '2023-06-19', + }, + { + value: [560.3971, 561.7968, 555.862, 570.6991], + symbol: '813701.CNS', + factor: 'kline_day_candle', + time: '2023-06-16', + }, + { + value: [553.6004, 559.8932, 547.7492, 563.5916], + symbol: '813701.CNS', + factor: 'kline_day_candle', + time: '2023-06-15', + }, + { + value: [548.3811, 551.9996, 543.2821, 559.8399], + symbol: '813701.CNS', + factor: 'kline_day_candle', + time: '2023-06-14', + }, + { + value: [550.2547, 548.2714, 542.5419, 556.425], + symbol: '813701.CNS', + factor: 'kline_day_candle', + time: '2023-06-13', + }, + { + value: [554.7497, 550.9158, 542.1922, 559.8616], + symbol: '813701.CNS', + factor: 'kline_day_candle', + time: '2023-06-12', + }, + { + value: [551.3901, 555.6387, 546.2585, 562.8118], + symbol: '813701.CNS', + factor: 'kline_day_candle', + time: '2023-06-09', + }, + { + value: [554.5496, 551.7763, 545.2867, 559.3196], + symbol: '813701.CNS', + factor: 'kline_day_candle', + time: '2023-06-08', + }, + { + value: [559.5284, 554.6605, 550.6879, 565.1796], + symbol: '813701.CNS', + factor: 'kline_day_candle', + time: '2023-06-07', + }, + { + value: [568.3989, 559.5284, 556.1166, 574.7107], + symbol: '813701.CNS', + factor: 'kline_day_candle', + time: '2023-06-06', + }, + { + value: [566.1414, 568.6264, 559.138, 575.2344], + symbol: '813701.CNS', + factor: 'kline_day_candle', + time: '2023-06-05', + }, + { + value: [566.7634, 564.7859, 560.0398, 573.0916], + symbol: '813701.CNS', + factor: 'kline_day_candle', + time: '2023-06-02', + }, + { + value: [565.918, 565.0119, 558.7824, 576.4514], + symbol: '813701.CNS', + factor: 'kline_day_candle', + time: '2023-06-01', + }, + { + value: [577.9214, 566.3144, 561.2328, 584.5622], + symbol: '813701.CNS', + factor: 'kline_day_candle', + time: '2023-05-31', + }, + { + value: [583.5348, 577.4594, 567.5868, 591.1291], + symbol: '813701.CNS', + factor: 'kline_day_candle', + time: '2023-05-30', + }, + { + value: [585.0556, 584.1774, 576.7428, 594.1294], + symbol: '813701.CNS', + factor: 'kline_day_candle', + time: '2023-05-29', + }, + { + value: [575.7788, 585.4068, 571.5702, 592.0946], + symbol: '813701.CNS', + factor: 'kline_day_candle', + time: '2023-05-26', + }, + { + value: [579.1444, 576.5283, 568.68, 584.3184], + symbol: '813701.CNS', + factor: 'kline_day_candle', + time: '2023-05-25', + }, + { + value: [587.1258, 581.3535, 573.9319, 592.1913], + symbol: '813701.CNS', + factor: 'kline_day_candle', + time: '2023-05-24', + }, + { + value: [584.1688, 589.0106, 581.602, 599.1609], + symbol: '813701.CNS', + factor: 'kline_day_candle', + time: '2023-05-23', + }, + { + value: [581.4904, 583.3521, 574.509, 593.1842], + symbol: '813701.CNS', + factor: 'kline_day_candle', + time: '2023-05-22', + }, + { + value: [573.466, 581.7813, 569.0503, 587.688], + symbol: '813701.CNS', + factor: 'kline_day_candle', + time: '2023-05-19', + }, + { + value: [578.0325, 573.466, 569.2464, 583.177], + symbol: '813701.CNS', + factor: 'kline_day_candle', + time: '2023-05-18', + }, + { + value: [580.1243, 578.0325, 569.6653, 584.7727], + symbol: '813701.CNS', + factor: 'kline_day_candle', + time: '2023-05-17', + }, + { + value: [575.972, 581.054, 570.4904, 588.5912], + symbol: '813701.CNS', + factor: 'kline_day_candle', + time: '2023-05-16', + }, + { + value: [561.1129, 571.0043, 554.5373, 574.9384], + symbol: '813701.CNS', + factor: 'kline_day_candle', + time: '2023-05-15', + }, + { + value: [565.9743, 562.0121, 558.2763, 574.6912], + symbol: '813701.CNS', + factor: 'kline_day_candle', + time: '2023-05-12', + }, + { + value: [567.6747, 566.0309, 561.6097, 580.7117], + symbol: '813701.CNS', + factor: 'kline_day_candle', + time: '2023-05-11', + }, + { + value: [565.0152, 566.8245, 558.4564, 573.4964], + symbol: '813701.CNS', + factor: 'kline_day_candle', + time: '2023-05-10', + }, + { + value: [572.4006, 565.411, 560.9995, 578.2443], + symbol: '813701.CNS', + factor: 'kline_day_candle', + time: '2023-05-09', + }, + { + value: [577.0122, 572.9162, 565.4166, 583.7042], + symbol: '813701.CNS', + factor: 'kline_day_candle', + time: '2023-05-08', + }, + { + value: [591.865, 576.8968, 571.4538, 594.5864], + symbol: '813701.CNS', + factor: 'kline_day_candle', + time: '2023-05-05', + }, + { + value: [584.0125, 591.6283, 579.9117, 600.3571], + symbol: '813701.CNS', + factor: 'kline_day_candle', + time: '2023-05-04', + }, + { + value: [583.956, 585.8286, 573.3642, 593.3775], + symbol: '813701.CNS', + factor: 'kline_day_candle', + time: '2023-04-28', + }, + { + value: [573.7591, 585.1849, 568.4768, 592.3619], + symbol: '813701.CNS', + factor: 'kline_day_candle', + time: '2023-04-27', + }, + { + value: [566.4006, 574.161, 560.5094, 581.6949], + symbol: '813701.CNS', + factor: 'kline_day_candle', + time: '2023-04-26', + }, + { + value: [578.2053, 566.4572, 557.6606, 583.0088], + symbol: '813701.CNS', + factor: 'kline_day_candle', + time: '2023-04-25', + }, + { + value: [579.7089, 578.7262, 569.8245, 593.3505], + symbol: '813701.CNS', + factor: 'kline_day_candle', + time: '2023-04-24', + }, + { + value: [581.4119, 578.0326, 573.4298, 591.0253], + symbol: '813701.CNS', + factor: 'kline_day_candle', + time: '2023-04-21', + }, + { + value: [590.0756, 582.6354, 578.3248, 595.6262], + symbol: '813701.CNS', + factor: 'kline_day_candle', + time: '2023-04-20', + }, + { + value: [597.7922, 590.4889, 586.5978, 603.0003], + symbol: '813701.CNS', + factor: 'kline_day_candle', + time: '2023-04-19', + }, +]; + +describe('candlestick', () => { + it('swipe', async () => { + const { props } = ( + + + + + + + + + ); + + const canvas = new Canvas(props); + await canvas.render(); + + await delay(1000); + expect(context).toMatchImageSnapshot(); + + await delay(20); + await gestureSimulator(context.canvas, 'touchstart', { x: 210, y: 169 }); + await gestureSimulator(context.canvas, 'touchmove', { x: 200, y: 169 }); + await gestureSimulator(context.canvas, 'touchend', { x: 200, y: 169 }); + await delay(3000); + expect(context).toMatchImageSnapshot(); + + const { props: nextProps } = ( + + + + + + + + + ); + + await delay(100); + await canvas.update(nextProps); + await delay(1000); + expect(context).toMatchImageSnapshot(); + }); +});