由于公司项目需要,要做港股行情的H5版本,经过分析需求,大致有两块难点: 一是行情的推送接收,二是行情K线的生成及相关操作。本文章主要分析行情K线的相关实现,由于我们前端团队之前是没有相关的工作经验的,所以我们第一反应就是去网上搜现成的插件或者相关文档。经过查找我们发现其实网上这方面的资料不多,相关插件也是比较少,比较符合的相关插件有tradingView以及百度团队开发的ECharts, 但是两者插件体积比较大而且在H5移动端的处理并不是特别好。经过讨论我们决定自研开发。

   下面是我们H5线上行情系统的实际操作图, 也可以扫码体验。

    ​                                 

       开发这套行情的K线图表,关键点主要有两点,其一是K线图,其二是手势的处理。K线图难度不是很大在熟悉Canvas画图基础的情况下注意不同区域划分和层级即可,重点在于数据的一些计算和判断;手势的处理就比较麻烦了需要考虑到长按,滑动,放大缩小,惯性滑动,触底加载,横屏等场景。下面就这些关键点进行逐一分析。

1、K线图基于Z轴(可以理解成css样式中的z-index)分成了三层:

       第一层画坐标轴的各种文字和线条包括边框线,XY轴分割线,X轴时间和日期,Y轴价格和成交量成交额等数据文本;

       

       第二层画主体数据图包括分时的走势线,分时的均线,日K的柱状图,MA5,MA10,MA20走势线,最高价,最低价,成交量或成交额的柱状图等K线主体数据图或文本;

       

       第三层画长按K线时出现的十字线及十字线的数据文本。

       

      最后将三层相对定位在同一坐标即可。

2、Z轴每一层基于Y轴分成了三部分:

      第一部分画上方走势图的线条,图形,文本;

      第二部分画中间时间或日期文本;

      第三部分画下方成交量或成交额的线条,图形,文本。

      

      以上canvas的颜色、大小、线条粗细写成插件配置形式即可。

3、几个需要注意的图形画法

      3.1、分时图的画法逻辑:从第一个数据点的坐标(x0, y0)画笔开始(beginPath)移动到(moveTo)下一个点的坐标(x1, y1)依次移动到最后一个点的坐标(xn, yn),到最后一点的坐标后移动到第一部分的最右下方点(width, height)然后再移动到第一部分最左下方点(0, height)最后回到起点(x0, y0)形成闭合填充(fill)渐变色(createLinearGradient)关闭画笔(closePath)。

      3.2、K线柱状图:这里首先介绍下柱状图(可能有点多余)

      转换成canvas画图角度其实就是线条和矩形的结合,线条的画法比较容易,中心柱状图需要注意的地方是如果是空心柱状图需要叠加两层矩形第一层的背景色和你主背景色一致第二层画边框矩形边框颜色画对应的涨跌颜色(这里是红涨绿跌)所以边框颜色设置成红色。下面是一段伪代码:

  1. var rectConf = {
  2. xAxis: 10, // 矩形框x轴坐标
  3. yAxis: 20, // 矩形框y轴坐标
  4. width: 5, // 矩形框宽度
  5. height: 30, // 矩形框高度
  6. }
  7. if (!this.isSolidCandle) { // 如果不是实心蜡烛图
  8. this.ctx.fillStyle = this.COLORS.MAINBG; // 主背景色
  9. this.ctx.fillRect(rectConf.xAxis, rectConf.yAxis, rectConf.width, rectConf.height);
  10. this.ctx.strokeRect(rectConf.xAxis, rectConf.yAxis, rectConf.width, rectConf.height);
  11. this.ctx.fillStyle = lineColor; // 线条颜色
  12. } else {
  13. this.ctx.fillStyle = lineColor; // 线条颜色
  14. this.ctx.fillRect(rectConf.xAxis, rectConf.yAxis, rectConf.width, rectConf.height);
  15. }

      其它可能存在难点的地方更多是涉及到计算,例如最高点最低点坐标位置,第二部分时间文本坐标位置及宽度等,这里就不一一介绍了,有问题可以下方留言。

1、长按事件:

      我们知道js中是没有这个事件的,但是是有触摸事件,所以这里利用触摸事件来模拟长按事件。定义从触摸开始超过200ms不动即为长按,可以在触摸事件中使用setTimeout定时器超过200ms即执行长按事件,并且设置长按标识,但是这里需要注意的是在滑动事件中清除这个定时器,如果长按事件已经执行那么清除了也不会有影响,如果还没执行说明还没到达长按的条件,利用这个特性就能模拟长按事件了,下面是一段伪代码:

  1. // 触摸开始事件
  1. touchStartEvent(e) {
  2. this.longTapTimeout = setTimeout(() => {
  3. this.longTapFlag = true;
  4. this.longTap(touchOne);
  5. }, 200);
  6. },
  7. // 触摸移动事件
  8. touchMoveEvent(e) {
  9. if (this.longTapTimeout) {
  10. clearTimeout(this.longTapTimeout);
  11. this.longTapTimeout = null;
  12. }
  13. if (this.longTapFlag) {
  14. // 长按滑动事件(即十字线滑动事件)
  15. this.touchMove();
  16. } else {
  17. // k线滑动事件
  18. this.swipe();
  19. }
  20. }
 
2、放大缩小事件:

这个事件是双指事件,在js中是可以通过event.targetTouches的长度来判断的。实现放大缩小大体思路是:

      step1:计算两指中间坐标点;

      step2:计算两指间的直线距离;

      step3:根据直线距离以及上一次的直线距离计算需要放大或缩小的刻度;

      step4:计算刻度不变时中间坐标点对应K线数组的索引index1;

      step5:计算刻度变动后中间坐标点对应K线数组的索引index2;

      step6:变动前后的索引值相减可以获得变动的柱状图条数,重新渲染图形即可。

这里有几个点需要注意:1. 缩小时左边数据已经到底则需要加载更多数据,2.刻度粗细应该设置上下限在达到上下限的时候避免再次渲染图形。下面是部分计算代码:

  1. // 放大缩小刻度
  2. const scale = (touchDistance - this.nextTouchDistance) / this.nextTouchDistance;
  3. // 放大缩小事件
  4. this.zoomIn(centerX, scale);

step1:计算两指中间坐标点

  1. // 两指x轴方向距离
    const xLen = Math.abs(
  2. e.targetTouches[1].pageX - e.targetTouches[0].pageX,
  3. );
  4. // 两指y轴方向距离(横屏需要)
  5. const yLen = Math.abs(
  6. e.targetTouches[1].pageY - e.targetTouches[0].pageY,
  7. );
  8. // canvas内容区矩形的边框信息
  9. const clientRect = this.container.getBoundingClientRect();
  10. // 相对于屏幕的中心点
  11. let center;
  12. // 相对于canvas图层的中心点
  13. let centerX;
  14. center = e.targetTouches[1].clientX - (e.targetTouches[1].clientX -
  15. e.targetTouches[0].clientX) / 2;
  16. centerX = center - clientRect.left;

step2:计算两指间的直线距离;

  1. // 两指间距离 (根据勾股定理计算)
  2. const touchDistance = Math.sqrt(xLen * xLen + yLen * yLen);

step3:根据直线距离以及上一次的直线距离计算需要放大或缩小的刻度;

  1. // 需要放大缩小刻度
  2. const scale = (touchDistance - this.nextTouchDistance) / this.nextTouchDistance;
  3. // 放大缩小处理
  4. this.zoomIn(centerX, scale);
  5. // 记住本次两指间距离
  6. this.nextTouchDistance = touchDistance;

step4:计算刻度不变时中间坐标点对应K线数组的索引index1;step5:计算刻度变动后中间坐标点对应K线数组的索引index2;

step6:变动前后的索引值相减可以获得变动的柱状图条数,重新渲染图形即可。

  1. zoomIn(centerX, scale) {
  2. // 中心刻度
  3. const centerScale = Math.ceil(centerX / this.cellWidth); // this.cellWidth为图中柱状图宽度
  4. // K线数组索引(刻度不变时中间坐标点)
  5. const centerIndex = Math.max(Math.min(this.kData.length - 1, centerScale - 1), 0);
  6. const originalScale = this.scale;
  7. this.scale += scale;
  8. if (this.scale <= 0.5) {
  9. this.scale = 0.5;
  10. } else if (this.scale > 4) {
  11. this.scale = 4;
  12. }
  13. if (originalScale !== this.scale) {
  14. // K线条目数
  15. this.count = Math.floor(60 / this.scale)
  16. this.cellWidth = this.canvasWidth / this.count;
  17. // 计算刻度变动后中间坐标点
  18. const centerScale1 = Math.ceil(centerX / this.cellWidth );
  19. const centerIndex1 = Math.max(Math.min(this.kData.length - 1, centerScale1 - 1),0);
  20. const scaleDiff = centerIndex1 - centerIndex;
  21. let index = this.indexStart - scaleDiff;
  22. index = Math.min(index, Math.max(this.allData.length - this.count, 0));
  23. index = Math.max(index, 0);
  24. if (index === 0 && this.loadMore === false) {
  25. this.loadMore = true;
  26. this.loadMoreCallback();
  27. return;
  28. }
  29. this.indexStart = index;
  30. this.indexStartTemp = index;
  31. const data = this.allData.slice(index, index + this.count) || [];
  32. // 重新渲染图形
  33. this.drawKLine();
  34. }
  35. },

以上代码有些方法没有体现出来,因为代码比较长所以只粘贴相应的代码,如果有迷惑的地方,可以下方留言。

3、惯性滑动事件:

      惯性滑动在移动端是个很好的体验,什么时候会触发惯性呢,两次滑动的间隔时间小于一个设定值既可触发惯性滑动。惯性需要考虑加速度,灵敏度等因素。这里惯性是用的js中requestAnimationFrame方法,存在兼容性问题可以用setTimeout模拟这里不多做兼容处理的介绍,因为大部分机型是兼容的。具体实现为:在滑动开始时记录时间,在触摸结束事件中判断时间间隔是否小于100ms,如果小于100ms则执行惯性滑动事件,根据滑动最后时间和滑动开始时间计算滑动速度,然后根据设置的灵敏度来计算加速度,执行惯性动画,然后每执行一次惯性事件减少速度直到速度为0停止惯性事件。以下为部分代码:

  1. // 触摸结束事件
  2. touchendEvent(e) {
  3. this.touchEndTime = new Date().getTime();
  4. // 开始移动事件和触摸结束事件时间间隔
  5. const intervalTime = this.touchEndTime - this.startMoveTime;
  6. // 最后一次滑动结束和开始滑动时间间隔
  7. let timeStamp = this.endMoveTime - this.startMoveTime;
  8. timeStamp = timeStamp > 0 ? timeStamp : 8;
  9. // 停顿时间超过100ms不产生惯性滑动;
  10. if (intervalTime < 100) {
  11. this.speed = Math.abs((this.startX - this.currentX) / timeStamp); // 计算速度
  12. this.acceleration = this.speed / this.sensitivity; // 根据灵敏度(sensitivity)计算加速度
  13. this.frameStartTime = new Date().getTime(); // 动画开始时间
  14. this.inertiaFrame = requestAnimationFrame(this.moveByInertia);
  15. }
  16. }
  17. // 惯性滑动时间
  18. moveByInertia() {
  19. this.frameEndTime = new Date().getTime(); // 每次动画结束时间
  20. this.frameTime = this.frameEndTime - this.frameStartTime; // 动画执行时间
  21. if (this.currentX < this.startX) {
  22. // 向左惯性滑动;
  23. this.roll.dir = 1;
  24. } else {
  25. // 向右惯性滑动;
  26. this.roll.dir = -1;
  27. }
  28. this.speed = Math.max(this.speed - this.acceleration * this.frameTime, 0); // 逐渐降低速度
  29. if (this.speed === 0) {
  30. cancelAnimationFrame(this.inertiaFrame);
  31. this.touchEnd();
  32. return;
  33. }
  34. this.roll.len += this.speed;
  35. this.swipe(this.roll); // 执行滑动K线方法
  36. this.frameStartTime = this.frameEndTime;
  37. this.inertiaFrame = requestAnimationFrame(this.moveByInertia);
  38. }

 

这里也有几点代码中没有写进去的如需要判断是否已经触及边缘、横屏的处理等等。

3、触底回弹事件:

       触底回弹主要是需要判断是否已经到左右两侧的点,设置到达临界点后允许滑动的K线条数结束滑动后进行惯性回弹至临界点,惯性回弹类似惯性滑动的处理。以下为主要逻辑代码:

  1. // 惯性回弹
  2. springbackByInertia() {
  3. // 设置到达临界点后允许滑动K线条数为5根
  4. const roll = { dir: 1, len: 5 };
  5. // indexStartTemp是k线数组开始的标识位
  6. // 如果this.indexStartTemp > this.allData.length - this.count则判断到达了最左侧的临界点
  7. // 如果this.indexStartTemp < 0则判断达到了最右侧的境界点
  8. if (
  9. this.indexStartTemp > this.allData.length - this.count ||
  10. this.indexStartTemp < 0
  11. ) {
  12. this.swipe(roll);
  13. this.springInertiaFrame = requestAnimationFrame(this.springbackByInertia);
  14. } else {
  15. cancelAnimationFrame(this.springInertiaFrame);
  16. this.swipe(roll);
  17. }
  18. },

 

4、横屏事件:

       这里处理横屏事件是通过轻击事件来触发的,如何判断是否为轻击事件呢。在触摸结束事件中判断触摸时间小于100ms且移动距离小于5px即视为轻击事件来触发横屏事件。下面为判断轻击事件的代码:

  1. // 是否为轻击事件(触摸时间小于100ms且移动距离小于5px)
  2. isTap() {
  3. if (this.touchEndTime - this.touchStartTime < 100) {
  4. if (!this.currentX && !this.currentY) {
  5. return true;
  6. }
  7. const xScale = Math.abs(this.currentX - this.startX);
  8. const yScale = Math.abs(this.currentY - this.startY);
  9. if (xScale < 5 && yScale < 5) {
  10. return true;
  11. }
  12. }
  13. return false;
  14. },

 

横屏的实现这里是利用css3的旋转属性对canvas进行90度旋转,旋转后需要注意的是滑动的时候X轴与Y轴要和竖屏的时候替换来处理。

     以上是我们自研行情的K线图部分的处理,难免有些地方存在不足,也希望读者能给予意见和指导。由于以上代码大都为代码片段,所以会存在变量或方法名没有定义的情况,望多谅解。我们的行情推送是使用websocket推送结合protobuf数据格式来完成的,如果有需要可以另外介绍。

 

*转载请附出处。

版权声明:本文为dennis0525原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://www.cnblogs.com/dennis0525/p/10604058.html