K 线图高性能窗口化渲染

项目仓库:

Gitee:https://gitee.com/yyt363045841/klinechart

Github:https://github.com/363045841/klinechart

NPM:https://www.npmjs.com/package/@363045841yyt/klinechart

安装:npm i @363045841yyt/klinechart

项目持续更新中!欢迎提出宝贵建议!如果对您有帮助可以给个 Star!

本项目采用 Canvas 绘制 K 线图,行情数据通过 AKTools 获取。

布局

html 复制代码
<template>
  <div class="chart-container" ref="containerRef" @scroll.passive="scheduleRender">
    <div class="scroll-content" :style="{ width: totalWidth + 'px' }">
      <canvas class="chart-canvas" ref="canvasRef"></canvas>
    </div>
  </div>
</template>

窗口化渲染的关键在于,每次不重绘所有的 K 线,而是计算出当前可视范围内的所有 K 线,每次仅重绘这部分区域。这就带来一个问题,Canvas 绘制区域不像以往绘制整片区域一样,可以撑开 chart-container 的宽度,使之可以左右滚动。

因此替代方案是创建一个宽度为计算出的所有 K 线宽度的总和的一个 scroll-content div,用来撑开容器。Canvas 正常在窗口区域绘制区域 K 线数据,而不是每次绘制全部区域。

区域绘制

外层 chart-container 容器响应 scroll 事件,触发区域重绘节流函数 scheduleRender。

节流限制

js 复制代码
let rafId: number | null = null

function scheduleRender() {
  if (rafId !== null) cancelAnimationFrame(rafId)
  rafId = requestAnimationFrame(() => {
    rafId = null
    render()
  })
}

由于用户一次滚动会触发多次滚动事件,不节流的话会产生大量重绘开销。节流函数会取消掉之前的排队渲染帧,因为已经过时,没必要再渲染。

requestAnimationFrame 调用后会返回一个 rafId,标记当前的渲染帧,这里渲染前置 null 是为了避免当前渲染帧被取消导致渲染不完全。

渲染计算

拿到外层 container 的 ref 引用,然后通过 getBoundingClientRect() 拿到视口宽高,进而动态设置 Canvas 宽高为视口区域,避免全部重绘。

接下来确定重绘哪些 K 线和对应的重绘坐标
基础数据:

符号 含义 对应代码
W v i e w W_{view} Wview 视口宽度(屏幕能看到的宽度) viewWidth
S l e f t S_{left} Sleft 当前滚动距离( scrollLeft ) scrollLeft
W k W_k Wk 单根 K 线宽度 kWidth
W g W_g Wg K 线间隙宽度 kGap
N t o t a l N_{total} Ntotal 数据总数量 n
U U U 单元宽度(1 根 K 线 + 1 个间隙) unit
  1. 通过 container 的 scrollLeft DOM 属性拿到水平移动距离
  2. 确定渲染 K 线范围:
    单根 K 线宽度: U = W k + W g U = W_k + W_g U=Wk+Wg,那么渲染起始区域为
    S t a r t = max ⁡ ( 0 , ⌊ S l e f t U ⌋ − 1 ) Start = \max(0, \lfloor \frac{S_{left}}{U} \rfloor - 1) Start=max(0,⌊USleft⌋−1)
    注意这里要 -1,为左侧区域提供缓冲区,避免边缘闪烁。
    终止区域为:
    E n d = min ⁡ ( N t o t a l , ⌈ S l e f t + W v i e w U ⌉ + 1 ) End = \min(N_{total}, \lceil \frac{S_{left} + W_{view}}{U} \rceil + 1) End=min(Ntotal,⌈USleft+Wview⌉+1)
    +1 同理。
  3. 移动 Canvas 坐标系
javascript 复制代码
/* 重置变换并缩放 */
ctx.setTransform(1, 0, 0, 1, 0, 0)
ctx.scale(dpr, dpr)
ctx.clearRect(0, 0, viewWidth, height) // 擦除上一帧

/* 保存状态,平移坐标系 */
ctx.save() // 压入状态栈
ctx.translate(-scrollLeft, 0)

/* 恢复状态 */
ctx.restore() // 将栈顶状态覆盖当前状态

首先,要理解 Canvas 绘图的底层机制:变换矩阵,每一次对 ctx 上下文的操作都是基于上一个变换结果的,为了确保每次都是从原来的坐标原点变换,每次做坐标变换之后都要重置回当前状态,否则会导致累积变换导致绘制错误。

将整个 Canvas 坐标系向左移动 scrollLeft 距离
( 0 , s c r o l l L e f t ) ← ( 0 , 0 ) (0,scrollLeft )\leftarrow(0,0) (0,scrollLeft)←(0,0)

此时 Canvas 坐标系原点就和滚动 DOM 左上角对齐,统一了 DOM 内部坐标和 Canvas 坐标系,实现了坐标系对齐

接下来调用封装好的绘制函数绘制 K 线和 MA 线:

javascript 复制代码
/* 画K线 */
  kLineDraw(ctx, kdata, opt, height, dpr, start, end, priceRange)

  /* 画MA */
  if (props.showMA.ma5) {
    drawMA5Line(ctx, kdata, opt, height, dpr, start, end, priceRange)
  }
  if (props.showMA.ma10) {
    drawMA10Line(ctx, kdata, opt, height, dpr, start, end, priceRange)
  }
  if (props.showMA.ma20) {
    drawMA20Line(ctx, kdata, opt, height, dpr, start, end, priceRange)
  }

Canvas 高清适配

核心在于联系 CSS 逻辑像素屏幕物理像素

先了解为什么不特殊处理会导致 Canvas 绘制模糊:

在右边 dpr 为 2 的屏幕中,屏幕实际上有 640 px,但是 Canvas 内部的逻辑像素只分配了 320 个像素,那么浏览器就必须将 320 px 拉伸到 640 px 上显示,此时边缘被线性插值来平滑过渡,导致边缘看起来模糊。
解决方法:

分配和屏幕像素相等的 CSS 逻辑像素,放大画布的同时放大坐标系:

javascript 复制代码
canvas.width = Math.round(viewWidth * dpr)
canvas.height = Math.round(height * dpr)

ctx.scale(dpr, dpr) // x,y 轴的绘制宽度和高度都乘以 dpr

相当于抵消了放大 CSS 逻辑像素带来的影响,实际上就是 CSS 逻辑像素和屏幕像素的对齐

当然,如果现在要画 K 线影线,我们以屏幕像素为结果导向,要画一个 2 px 宽度的影线,但是如果画 2 px CSS 逻辑像素,就会画出实际 4 px 的影线,因此 CSS 绘制实际想要的屏幕像素,需要绘制除以 dpr 的宽度,即 2 d p r = 1    C S S    p x \frac{2}{dpr}=1\; CSS \; px dpr2=1CSSpx 宽度的影线。

相关推荐
XiaoYu20021 天前
第5章 Nest.js精进-IOC控制反转
前端
LV技术派1 天前
适合很多公司和团队的 AI Coding 落地范式(二)
前端·aigc·ai编程
IT_陈寒1 天前
Redis性能翻倍的5个冷门技巧:从每秒10万到20万的实战优化之路
前端·人工智能·后端
ss2731 天前
高版本node启动RuoYi-Vue若依前端ruoyi-ui
前端·javascript·vue.js
饼干,1 天前
模拟试卷2
前端·javascript·easyui
南雨北斗1 天前
js 严格模式
前端
聪明的Levi1 天前
FRONT END REVIEW
前端·css·html
winfredzhang1 天前
零依赖的艺术:用原生 JS 打造“ZenReader”沉浸式阅读器
javascript·frontend·uxdesign·productivity·#webdevelopment
仙人掌一号1 天前
React 白屏机制原理分析[共1500字,阅读时长8min]
前端·javascript·面试