K线图

技术难点

value 对应的纵坐标

已知数据里的最大值maxValue、最小值minValue、y 轴的高度yAxisHeight,那么就可以得到 value 与yAxisHeight的比例ratio。想要得到刻度对应的数值,只需要将最小值+刻度间距iratio即可。
比如最大值 1000,最小值 500,y 轴高度为 100px,那么 1px 所代表(1000 - 500) / 100,即 5。
如果 y 轴刻度间距为 30px,那么对应的数值为500 + 30*i*ratio

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* 计算Y轴刻度对应的数值:根据最大最小值动态变化
* @param {number} i 刻度对应下标
* @param {number} maxValue 最大值
* @param {number} minValue 最小值
* @param {number} yAxisHeight y轴高度
* @param {number} yAxisTickSpace y轴刻度间距
* @returns number 刻度对应的数值
*/
export const yAxisTickText = (
i,
maxValue,
minValue,
yAxisHeight,
yAxisTickSpace,
) => {
// ratio表示value 与 y轴高度的比例
const ratio = (maxValue - minValue) / yAxisHeight;
const value = (minValue + yAxisTickSpace * i * ratio).toFixed(2);
return value;
};

x 轴元素动态隔点展示

x 轴元素的数量是处于变化的,但 x 轴宽度是已知的,这样我们就能算出元素之间的间距xAxisItemSpace是多少。知道了xAxisItemSpace,那我们就能知道 x 轴刻度横坐标。

1
2
3
4
5
6
7
8
9
10
/**
* 求x轴刻度横坐标
* @param {number} i 下标
* @param {number} xAxisPointX x轴原点横坐标
* @param {number} xAxisItemSpace x轴刻度间距
* @returns number x轴刻度横坐标
*/
export const xAxisTickPointX = (i, xAxisPointX, xAxisItemSpace) => {
return xAxisPointX + i * xAxisItemSpace;
};

缩放时,考虑当元素太多时,如果展示所有的元素,会出现拥挤的情况。所以我们只能展示部分元素。
隔点展示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 绘制x轴刻度与文字
const xAxisItemMaxShowNumber = 4; // 展示个数
const remainder = Math.ceil(xAxisItemLength / (xAxisItemMaxShowNumber - 1));
for (let i = 0; i < xAxisItemLength; i++) {
const xAxisTickX = xAxisTickPointX(i, originalPointX, xAxisItemSpace);

// 隔点展示
if (i % remainder === 0 || i === xAxisItemLength - 1) {
renderText(
ctx,
xAxisTickX,
yAxisOriginPointY + tickWidth + 10,
myDataSource.current.map((x) => x.date)[i],
'center',
TEXT_COLOR.PRIMARY,
);
renderLine(
ctx,
xAxisTickX,
yAxisOriginPointY,
xAxisTickX,
yAxisOriginPointY + tickWidth,
COLOR.LINE,
);
}
}

三次贝塞尔曲线前后控制点

受前后元素纵坐标影响,由前后两个点和当前点的纵坐标构成一个平行四边形,即可得到当前元素的前后控制点。

还需要考虑首尾元素没有前后控制点的边界问题,所以要加入两个虚拟点。

辅助线

在 canvas 里,更新画布既是重选渲染整个画布,所以辅助线的绘制采用分层处理,创建一个新的画布覆盖上去,独立开来,不影响展示画布。

  1. 监听鼠标移动事件mousemove
  2. 清除画布
  3. 如果在 gird 区域,绘制辅助线和提示框

拖拽

  1. 监听鼠标按下事件mousedown,并创建拖动元素。可以做记忆化处理(优化手段)
  2. 监听开始拖动目标元素事件dragstart,并记录光标位置,即event.offsetX
  3. 监听拖动事件drag,拖动过程中,达到一定距离,清除画布(clearRect),然后更新要展示的数据,重新渲染即可
  4. 拖动结束时,需要隐藏拖动元素,并且如果左侧临时集合数据小于页数pageSize请求接口数据

缩放

  1. 监听滚轮事件wheel
  2. 放大event.deltaY > 0时,删除展示集合dataSource前后数据,并分别扔到存储被删除数据的临时集合里,直到最小展示条数
  3. 缩小时「尽可能多的展示数据」,分两个情况,
    1. 当已展示条数大于最大展示条数时 或 左侧临时集合条数小于最小展示条数时,请求接口并将请求数据合并到左侧临时集合中
    2. 删除临时集合的数据,并扔到展示集合中
  4. 处理完数据,清除画布,重新渲染

应用层的思考

数据如何来:
初始时,默认展示为 10 条pageSize=10,最大展示条数为 20 条maxShowSize = pageSize*2,所以需要预准备 30 条数据pageSize+maxShowSize,也就是首次加载 30 条数据。
更新时

  1. 拖拽什么时候请求新数据:拖拽结束后请求新数据

因为一次最多拖maxShowSize当左侧临时集合数据小于**maxShowSize**,请求接口数据,请求maxShowSize

  1. 缩放什么时候请求新数据:缩放结束后,当左侧临时集合数据小于**maxShowSize**

因为没有缩放结束事件,我们可以观察下缩放时的时间间隔,在wheel事件里打印Date.now(),发现滚动时间间隔在 200ms 以内,保险起见我们取个 500ms。
当滚动时间超过了 500ms,我们就判断为滚动结束:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
let timer = null;

kWrapNode.addEventListener('wheel', function (e) {
// 1. 先判断是否停止 2.停止后做什么
if (timer) {
clearTimeout(timer);
}
// 模拟缩放结束事件
const wheelStop = () => {
// 滚动停止时执行的代码
console.warn('wheelStop');
if (leftDataSource.length < maxShowSize) {
// 请求数据
loadData(maxShowSize, dataSource[0].date).then((res) => {
leftDataSource = [...res, ...leftDataSource];
});
}
};
timer = setTimeout(wheelStop, 500);
});
Your browser is out-of-date!

Update your browser to view this website correctly.&npsb;Update my browser now

×