Canvas动态折线图需用二次贝塞尔曲线替代lineTo实现平滑:以三点序列(p₀,p₁,p₂)为单元,p₀起点、p₁控制点、p₂终点;坐标须提前映射(y轴翻转+归一化),过滤异常值,动态追加时仅重绘末端并局部清屏,配合lineCap/lineJoin='round'及1.5线宽优化视觉。

Canvas绘制动态折线图时,仅用lineTo()连接数组数据点会显得生硬、拐角尖锐、视觉不连贯。要实现真正自然的平滑插值,核心不是“加滤镜”,而是用贝塞尔曲线替代直线段,并让控制点与数据趋势协同变化——尤其在数据持续追加的场景下,还需兼顾起点连贯性与局部重绘效率。
用二次贝塞尔拟合三点序列
对原始数据点数组 [p0, p1, p2, p3, ...],每组连续三点(如 p₀→p₁→p₂)可生成一段平滑弧线:以 p₁ 为控制点,p₀ 为起点,p₂ 为终点。Canvas 中调用方式为:
- 首次:先
moveTo(p0.x, p0.y),再quadraticCurveTo(p1.x, p1.y, p2.x, p2.y) - 后续:直接
quadraticCurveTo(p2.x, p2.y, p3.x, p3.y),无需重复beginPath()或moveTo() - 至少需 3 个有效点才启动曲线绘制;前两点可用直线过渡,或暂不渲染
- 控制点可微调:例如取
{x: p1.x, y: p1.y + (p2.y - p0.y) * 0.15},避免陡峭折返
坐标映射必须提前完成
Canvas 像素坐标系 y 轴朝下,而图表逻辑 y=0 通常在底部。原始数值不能直接传入绘图函数,否则会出现倒置、越界、比例失真等问题:
- y 坐标转换公式:
pixelY = height - paddingBottom - (value - minY) / (maxY - minY) * usableHeight - x 坐标等距分布:
pixelX = paddingLeft + i / (data.length - 1) * usableWidth(注意保护除零) - 务必在绘图前过滤 NaN、Infinity 或明显异常值,否则
lineTo(NaN, NaN)会静默中断整条路径
动态追加时保持视觉连贯
每 100ms 新增一个点,不能清空重绘整条线——那样会闪烁且丢失“流动感”。应只更新末端一小段,并复用上一帧终点作为新曲线起点:
- 维护一个点数组(推荐双端队列),超出长度时从头部
shift(),保证数据窗口稳定(如最近 100 点) - 每次新增点后,仅对最后三个点(倒数第 2、第 1、新增点)调用一次
quadraticCurveTo - 局部清屏:只擦除旧线末端矩形区域 + 新线段包围盒,避免全屏
clearRect(0,0,w,h) - 设置
ctx.lineCap = 'round'和ctx.lineJoin = 'round',消除线宽 >1 时的尖角断口
性能与视觉细节优化
高密度点或高频刷新下,边缘模糊、卡顿、锯齿容易暴露。这些细节不显眼,但直接影响专业感:
- 线条宽度建议设为
1.5而非整数,提升抗锯齿清晰度 - 避免关闭
imageSmoothingEnabled,它会影响文字和图标;改用离屏 canvas 预绘静态背景(网格、坐标轴)更稳妥 - 若需匀速动画(如小球沿曲线移动),不可直接线性插值 t ∈ [0,1];需预计算弧长表并二分查找对应参数,否则会忽快忽慢
- 所有坐标值必须是数字类型,
quadraticCurveTo不接受字符串或 undefined,传错会导致曲线消失无提示

















