Canvas 高性能K线图的架构方案
当前位置:点晴教程→知识管理交流
→『 技术文档交流 』
前言证券行业,最难的前端组件,也就是k线图了。 1、H5 K线图,支持无限左右滑动、样式可随意定制; 注意:以上的 demo 还有一些 bug, 没时间修复, 预览地址是直接在 github 上部署的, 所以最好通过 vpn科学上网,否则可能访问不了,然后再在移动端打开页面。 另外, 上面的股票详情页, 还没有做自适应,等我有时间再改。 一、先看最终的效果1、GIF动图如下
2、支持样式自定义用可以屏幕取色器,获取东方财富的配色 codeinword.com/eyedropper 图一、图二,是参考东方财富黑白皮肤的配色, 图三是参考腾讯自选股的配色。
二、canvas 注意事项1、整数坐标,会导致模糊canvas 在画线段, 通常会出现以下代码:
假设上面的两个点是(1,10)和(5,10),那么画出来的实际上是一条横线, 但是 canvas 不是这样处理的, canvas 默认线条会与整数对齐, 并且由于粗度被拉伸,颜色也会被淡化,那怎么解决这个问题呢? 处理方式也很简单, 通过 cxt.translate(0.5, 0.5) 将坐标往下移动 0.5 个像素, 典型的代码如下:
在我的代码中, 也体现了类似的处理。 2、如何处理高像素比带来的模糊设备像素比越高,理论上应该越清晰,因为原来用一个小方块来渲染1px, 现在用2个(dpr=2的情况)小方块来渲染,应该更清晰才对,但是canvas不是这样的。 例如,通过js获取父容器 div 的宽度是 width, 这时候如果设置 canvas.width = width,在设备像素比为2的时候, canvas 画出来的宽度为css对应宽度的一半, 如果强制通过 css 将 canvas 宽度设置为 width, 则 canvas 会被拉长一倍, 导致出现锯齿模糊。 注意了吗?上面所说的 canvas.width=width 与 css 设置的 #canvas { width: width } 起到的效果是不一样的。不要随便通过 css 去设置 canvas 的宽高, 容易被拉伸变形或者导致模糊。 通用的处理方式是:
三、样式配置为了方便样式自定义, 我独立出一个默认的配置对象 defaultKlineConfig, 参数的含义如下图所示,其实下图这个风格的标注, 是通过 excalidraw 这个软件画的, 也是 canvas 做的开源软件, 可见 canvas 在前端可视化领域的重要性, 这个扯远了,打住。 如上图, 整个canvas 画板, 分成 5 部分, 十字交叉线的颜色, X轴 与 Y轴 的 tooltip 背景色、字体大小的参数如下: 四、均线计算从上面的图可以看出, 需要画 5日均线、10日均线、20日均线, 成交量快线(10日)、成交量慢线(20日) 但是, 接口没有给出当日的均线值, 需要自己计算。 5日均线 = (过去4个成交日的收盘价总和 + 今日收盘价)/ 5 10日均线 = (过去9个成交日的收盘价总和 + 今日收盘价)/ 10 20日均线 = (过去19个成交日的收盘价总和 + 今日收盘价)/ 20 成交量快线 = (过去9日成交量 + 今日成交量)/ 10 成交量慢线 = (过去19日成交量 + 今日成交量)/ 20 所以, 当获取 lmt(一屏的蜡烛图个数)个数据时, 为了计算均线, 需要至少将前 19 个(我的代码写20)数据都获取到。当前一个均线已经获取到, 下一个均线就不需要再累加20个值再得平均数, 可以省一点计算: 今日20日均线值 = (昨日均线值 * 20 - 前面第20个的收盘价 + 今日收盘价)/ 20; 五、分层渲染为了减少重绘,提高性能,可以将K线图做分层渲染。那分几层合适?我认为是三层。
不动层首先, 网格是固定的, 也就是说,当页面拖拽、或者长按出现十字交叉的时候,底部的网格线是不变的,如果每次拖拽,都需要重绘网格,那这个其实是没有必要的开销,可以将网格放在最底层(例如 z-index:0),一次性绘制后,就不要再重绘。 变动层由于拖拽的时候,蜡烛柱体,均线,Y轴刻度, X轴刻度, 都需要重绘, 这一块是无法改变的事实, 所以, 变动层放在中间层(例如 z-index:1),也是最繁忙的一层,并且该层不响应触摸事件。 交互层最后, 交互层由于要捕捉用户的触摸行为, 所以,这一层要在最上层(例如 z-index:2)。 交互层监听触摸事件:当页面快速滑动, 则响应拖拽事件, 即K线图的时间线会左右滑动;当用户长按之后才滑动, 则出现十字交叉浮层。 交互层的好处是, 当响应十字交叉浮层时, 只需要绘制横线、竖线、对应X轴和Y轴的值,而不需要重绘蜡烛柱体和均线, 可以减少重绘,最大程度减少渲染压力。 六、基础几何绘制网格线首先计算出主图的高度 this.mainChartHeight, 将主图从上到下等分为4部分,再在左右两边画出竖线,形成主图的网格,副图是成交量图, 只需画一个矩形边框即可,用 strokeRect 即可画出。
画各类均线1、首先计算出一屏的股价最大值 max , 股价最小值 min ,成交量最大值 maxAmount。 2、当某一个点的均线为 value, 根据最大值、最小值、索引index, 计算出坐标点(x, y), 画均线的时候, 第一个点用 moveTo(x0, y0),其他点用 lineTo(xn yn), 最后 stroke 连起来即可。 3、当然, 每一条线设置下颜色, 即 stokeStyle。
画出蜡烛柱体
当收盘价大于等于开盘价, 选用上面左边红色的样式; 当收盘价小于开盘价, 选用上面右边绿色的样式。 以红色蜡烛为例, 最高点 A(x0, y0),最低点是 B(x1, y1), 为了画出红色蜡烛, 先后顺序别搞混:
按照上面这个顺序, 竖线会被覆盖掉一部分,同时,矩形内部的白色填充不会挤压矩形的红色边框, 如果先 stroke 再 fill,容易出现白色填充覆盖红色边框,矩形可能会变模糊,或者使得红色变淡,极其不友好,所以按照我上面的顺序,可以减少不必要的麻烦。 画出文字canvas 画出文字, 典型的代码如下
注意textBaseline 默认对齐方式是 alphabetic, 但 middle 往往更好用, 能实现垂直居中,但我发现垂直居中也不是很居中,所以会特意加减1、2个像素; 当然还有个textAlign, 能实现水平对齐方式, 左右对齐都可以, 例如上图最左、最右的时间标签。 七、交互设计根据上面的GIF动图, 可以知道, 本次做的移动端 K 线图, 最重要的两个交互是:
上面的交互,其实是比较复杂的,所以需要先设计一个简单的数据结构:
当用户往右快速拖拽时, startIndex 根据用户拖拽的距离, 适当变小; 当用户往左快速拖拽时, startIndex 根据用户拖拽的距离, 适当变大。 那 arrayList 到底多长合适, 因为股票可能有十几年的数据, 甚至上百年的数据, 我不能一次性拉取这个股票的所有数据吧? 当然,站在软件性能、消耗等角度,也不应该一次性拉取所有的数据, 我的答案是 arraylist 最多保存5屏的数据量,用户看到的屏幕, 应该是接近中间这一屏,也就是第3屏的数据, 左右两边各保存2屏数据,这样,用户拖拽的时候,可以比较流畅,而不是每次拖拽都要等拉取数据再去渲染。 那什么时候拉取新的数据呢? 用户触摸完后,当startIndex左边的数据少于2屏,开始拉取左边的数据; 用户触摸完后,当startIndex右边的数据少于2屏,开始拉取右边的数据; 那如果用户一直往右拖拽, 是不是就一直往左边添加数据, 这个 arraylist 是不是会变得很长? 当然不是,例如,当我往 arraylist 的左边添加数据的时候,startIndex 也会跟着变动, 因为用户看到的第一条柱体,在 arraylist 的索引已经变了。当我往 arraylist 的某一边添加数据后, arraylist 的另一边如果数据超过 2 屏, 要适当裁掉一些数据, 这样 arraylist 的总数, 始终保持在 5 屏左右,就不会占用太多的存放空间。 总体思想是, 从 startIndex 开始渲染屏幕的第一条柱体, 当前屏幕的左右两边, 都预留2屏数据,防止用户拖拽频繁调用接口, 导致卡顿; 同时也控制了 arraylist 的长度, 这是虚拟列表的变形,这样设计,可以做一个 高性能 的k线图。 八、触摸事件解耦根据上面的分析:
以上两种拖拽,都在 touchmove 事件中触发, 那怎么区分开呢? 典型的 touchstart、 touchmove 、 touchend 解耦如下:
根据上面的框架, 再详细补充下代码就可以了。 然后再在 touchend 事件中, 新增或减少 arraylist 的数据量。 九、性能优化其实, 做到上面的设计,性能已经很好了,可以监控帧率来看下滑动的流畅程度。 总结下我做了什么操作,来提高整体的性能: 1、分层渲染将K线图画在3个canvas上。
2、离屏渲染当需要在K线上标注一些icon时, 这些 icon 可以先离屏渲染, 需要的时候, 再copy到变动层对应的位置,这样比临时抱佛脚去画,要省很多时间,也能提高新能。 3、设置数据缓冲区就是屏幕只渲染一屏数据, 但是在当前屏的左右两边,各缓存了2屏数据, 超过5屏数据的时候,及时裁掉多余的数据, 这样arraylist的数据量始终保持在5屏, 控制了数据量,有效的控制了占用空间。 4、节流防抖touchmove 会很频繁触发, 可通过节流来控制,减少不必要的渲染。 十、部署到GitHub Pages1、安装gh-pages包
2、package.json 添加如下配置注意, Stock 这个需要对应github的仓库名
3、运行部署命令
最后, 访问上面的链接 这样, github pages 部署成功, 访问上面链接, 可以看到如下效果。 github page 的部署需要将仓库设置为 public, 这个我挺反感的, 可以用 vercel 部署, 也就是将 github 账号与 vercel 关联起来, 项目的 package.js 的 homepage 设置为 “.” , 然后 vercel 可以点击一下, 一键部署, 常见的命令行如下:
转自https://juejin.cn/post/7556154928059334666 该文章在 2025/10/10 11:31:22 编辑过 |
关键字查询
相关文章
正在查询... |