tech share
  • tech-share
  • Engineering
    • 登录鉴权
    • SSR 页面路由
    • npm 版本号
    • 缓存
    • 数据库容灾
    • 动态效果导出 gif
    • Chrome-devtools
    • C 端 H5 性能优化
    • Docker
    • Monorepo 最佳实践
    • 技术架构演化
    • 项目规范最佳实践
    • snowpack
    • 静态资源重试
    • 前端页面渲染分析
    • Git
    • 前端重构
    • 微前端
    • 项目依赖分析
    • 前端监控原理
    • webpack
    • BS 架构与 CS 架构
    • HTTPS
    • package-lock.json 生成逻辑
    • SVN(Subversion)
    • 数据库分类
    • gulp
    • 前端架构
    • Bundle & Bundless
    • 控制反转 IoC
  • JavaScript
    • Javascript 性能
    • JavaScript 原型(2) - 原型与原型链
    • JavaScript 原型(1) - 构造函数
    • JavaScript - Promise
    • ES6 解构赋值
    • 前端离线化
    • Proxy
    • Object.defineProperty()简介
    • TypeScript
  • MachineLearning
    • GAN生成对抗网络
    • 虚拟对抗训练
    • 深度度量学习
    • 原型网络
    • PyTorch优化器
    • 隐马尔可夫模型2
    • Shapley Value 算法
    • Embarassingly Autoencoder算法
    • AutoRec算法及其后续发展
    • 深度学习常用激活函数
    • 序列预测ConvTran算法
    • 联邦学习
    • 深度学习推荐系统算法整理
    • 隐马尔可夫模型
    • 黎曼优化方法
    • FM算法
    • 机器学习常见评价指标
    • VAE算法
    • Adam优化器详解
    • Transformer算法
    • Self-attention 推荐算法
    • CNN 卷积神经网络
    • 图嵌入
    • 集成学习算法
    • RecBole开源框架
    • NCE-PLRec
    • 深度学习初始化方法
    • RNN循环神经网络
    • PyTorch数据处理
    • PyTorch安装和基本操作
    • XGBoost算法
    • NCF算法与简单MF的对比
    • 计算最佳传输
  • CSS
    • 什么是BFC
    • 纯CSS实现可拖动布局
    • 滚动穿透解决方案
  • React
    • React 生命周期
    • React Ref
    • React Hooks
    • SWR
    • React 数据流
    • React 函数式组件和类组件的区别
  • 可视化
    • OffscreenCanvas
    • Echarts 平滑曲线端点为什么不平滑
    • 颜色空间
    • 词云布局解析
    • 3D 数学基础
    • Canvas 图片处理
    • GLGL ES
    • WebGL 中绘制直线
    • Graphics API
    • 现代计算机图形学基础
    • Canvas 灰度
  • Vue
    • Vue2.x全局挂载整理
    • Vue2.6.x源码阅读
      • Vue2.6.x源码阅读 - 2.目录结构分析
      • Vue2.6.x源码阅读 - 4.源码阅读-platform
      • Vue2.6.x源码阅读 - 1.准备工作
      • Vue2.6.x源码阅读 - 5.源码阅读-core-Vue构造函数
      • Vue2.6.x源码阅读 - 7.源码阅读-core-响应式原理
      • Vue2.6.x源码阅读 - 3.源码阅读-shared
      • Vue2.6.x源码阅读 - 6.源码阅读-core-组件挂载
    • Vue + TypeScript Web应用实践
    • Vue2.x指令
    • nextTick()的使用
    • vue-cli2.x 的使用与项目结构分析
    • Vue响应式原理及总结
    • VueX的使用
    • Electron-Vue + Python 桌面应用实践
    • Vite
    • Vue组件通信整理
    • 记录一个问题的探索过程
  • Linux
    • memcg
  • GameDev
    • 游戏中的几种投影视图
    • 从零开始写软渲染器06
    • 从零开始写软渲染器05
    • 从零开始写软渲染器04
    • 从零开始写软渲染器03
    • 从零开始写软渲染器02
    • 从零开始写软渲染器01
    • 从零开始写软渲染器00
    • 现代游戏常用的几种寻路方案(一)
  • Node
    • NPM Dependency
    • Node 优势
    • Node Stream
    • Node 模块系统
  • HTML
    • html5语义与结构元素
  • 跨端
    • Flutter 介绍
  • Golang
    • Golang 基础
  • AR
    • SceneKit
由 GitBook 提供支持
在本页
  • 背景
  • 背景知识
  • 贝塞尔曲线
  • 向量
  • Echarts 平滑曲线绘制
  • 常规绘制方法
  • 新算法
  • 参考

这有帮助吗?

  1. 可视化

Echarts 平滑曲线端点为什么不平滑

上一页OffscreenCanvas下一页颜色空间

最后更新于4年前

这有帮助吗?

背景

在实际开发中,偶尔会出现曲线是圆滑的,但是端点连接处不是平滑的现象,这种情况往往在选择日期较少,端点两边的曲线斜率相对值差别较大时出现。

使用 Echarts 的在线编辑可以很容易复现出来。

背景知识

在展开具体分析之前,我们先介绍一下背景知识。 我们先想一下如何在两点之间画一条曲线,比较简单的方法就是画一个圆弧,假设两点在同一个圆上,那么我们就可以求出这个圆的方程,进而在画布上绘制出这个圆弧。但是如果现在有 3 个点,我们想要用曲线来表示这 3 个点之间的变化趋势,使用圆弧来表示这种变化趋势是不合适的,实际上,太过规则的曲线都不适合用来表达这种多个点之间的变化趋势。

贝塞尔曲线

贝塞尔曲线是一种不规则的曲线,它可以在两点之间画出来一条不规则的曲线。在图表可视化中,往往会使用贝塞尔曲线进行绘制平滑曲线。 贝塞尔曲线的绘制过程取决于两个端点以及控制点。

  1. 对于一维贝塞尔方程来说,控制点的个数是 0(维数-1)个,一维贝塞尔曲线是一条直线,假设两个端点为 P0,P1,方程满足 B(t) = P0 + (P1 - P0) * t,t 的取值范围是[0, 1]。

  1. 对于二维贝塞尔曲线来说,控制点的个数是 1 个,在下图中,满足:

  • 由 P0 至 P1 的连续点 Q0,描述一条线性贝塞尔曲线。

  • 由 P1 至 P2 的连续点 Q1,描述一条线性贝塞尔曲线。

  • 由 Q0 至 Q1 的连续点 B(t),描述一条二次贝塞尔曲线。 即 Q0 在 P0P1 上满足一维贝塞尔曲线;Q1 在 P1P2 上满足一维贝塞尔曲线;并且 B 在 Q0Q1 上满足一维贝塞尔曲线,Q0Q1 是曲线的切线。

向量

Echarts 平滑曲线绘制

常规绘制方法

Echarts 在进行常规曲线绘制时,会使用三维贝塞尔曲线进行绘制,所以每次绘制两个点之间的曲线时,需要选定两个控制点。 假设我们现在有 3 个点:P0,P1,P2。P1 点在 P0 点和 P2 点的中间,当绘制 P0 和 P1 点的曲线时,选定两个控制点 M1,M2;在绘制 P1 和 P2 的曲线时,选定两个控制点 N1,N2。那么,如果保证 M2,P1,N1 三点。如下图: 但是 Echarts 在实际处理中,计算得出控制点 M1,M2 后,还会与所有点的中的最大最小 x,y 坐标进行判断并调整,确保控制点不超过这个区域。控制点 M2 与控制点 N1 的计算方式非常相似:

在不对 M2,N1 进行调整的情况下,这两点与 P1 一定在同一条直线上,因此 P1 上的两侧曲线也应该是平滑过度的,不会出现突变的情况。但是由于 Echarts 中多了一步对所有点的中的最大最小 x,y 坐标进行判断并调整的步骤,因而导致 M2,N1 中的其中一个点或者两个点的坐标发生改变,因而造成三点不同线,使得端点两侧线段在端点处的切线发生突变。

新算法

Echarts 新使用的算法是将每个控制点两边都保持水平或竖直,水平还是垂直取决于数据点是在哪个方向(x/y)上单调递增。这样的好处是,每个点都是连续并且存在导数,生成的曲线是可导的,不会产生突变的情况。缺点是没法处理不单调的情况(比如点坐标依次是:[100, 100], [200, 200], [150, 180])。新算法的开启可以使用 smoothMonotone 字段,效果如下:

参考

  1. Echarts 判断使用何种方式进行绘制曲线

function drawSegment(
  ctx,
  points,
  start,
  segLen,
  allLen,
  dir,
  smoothMin,
  smoothMax,
  smooth,
  smoothMonotone,
  connectNulls
) {
  if (smoothMonotone === "none" || !smoothMonotone) {
    // 常规方法
    return drawNonMono.apply(this, arguments);
  } else {
    return drawMono.apply(this, arguments);
  }
}
  1. Echarts 常规绘制圆滑曲线方法

// Draw smoothed line in non-monotone, in may cause undesired curve in extreme
// situations. This should be used when points are non-monotone neither in x or
// y dimension.
function drawNonMono(
  ctx,
  points,
  start,
  segLen,
  allLen,
  dir,
  smoothMin,
  smoothMax,
  smooth,
  smoothMonotone,
  connectNulls
) {
  var prevIdx = 0;
  var idx = start;
  for (var k = 0; k < segLen; k++) {
    // segLen:points长度
    var p = points[idx];
    if (idx >= allLen || idx < 0) {
      // points数组下标越界
      break;
    }
    if (isPointNull(p)) {
      if (connectNulls) {
        //connectNulls为true时会跳过这个值,将前一个值和后一个值相连
        idx += dir; // dir === 1
        continue;
      }
      break;
    }

    if (idx === start) {
      // 起始点:移动到该点;非起始点:画线
      ctx[dir > 0 ? "moveTo" : "lineTo"](p[0], p[1]);
      v2Copy(cp0, p);
    } else {
      if (smooth > 0) {
        // 是否平滑绘制
        var nextIdx = idx + dir;
        var nextP = points[nextIdx];
        if (connectNulls) {
          // Find next point not null
          while (nextP && isPointNull(points[nextIdx])) {
            nextIdx += dir;
            nextP = points[nextIdx];
          }
        }

        var ratioNextSeg = 0.5;
        var prevP = points[prevIdx];
        var nextP = points[nextIdx];
        // Last point
        if (!nextP || isPointNull(nextP)) {
          v2Copy(cp1, p);
        } else {
          // If next data is null in not connect case
          if (isPointNull(nextP) && !connectNulls) {
            nextP = p;
          }

          vec2.sub(v, nextP, prevP); // v = nextP - prevP

          var lenPrevSeg;
          var lenNextSeg;
          if (smoothMonotone === "x" || smoothMonotone === "y") {
            var dim = smoothMonotone === "x" ? 0 : 1;
            lenPrevSeg = Math.abs(p[dim] - prevP[dim]);
            lenNextSeg = Math.abs(p[dim] - nextP[dim]);
          } else {
            lenPrevSeg = vec2.dist(p, prevP);
            lenNextSeg = vec2.dist(p, nextP);
          }

          // Use ratio of seg length
          ratioNextSeg = lenNextSeg / (lenNextSeg + lenPrevSeg);

          scaleAndAdd(cp1, p, v, -smooth * (1 - ratioNextSeg));
        }
        // Smooth constraint,造成不平滑的原因
        vec2Min(cp0, cp0, smoothMax); // cp0 = min(lastP, smoothMax)
        vec2Max(cp0, cp0, smoothMin); // cp0 = max(cp0, smoothMin)
        vec2Min(cp1, cp1, smoothMax); // cp1 = min(modifiedP, smoothMax)
        vec2Max(cp1, cp1, smoothMin); // cp1 = max(cp1, smoothMin)

        ctx.bezierCurveTo(
          // 控制点1: cp0;控制点2: cp1;终点:p
          cp0[0],
          cp0[1],
          cp1[0],
          cp1[1],
          p[0],
          p[1]
        );
        // cp0 of next segment
        scaleAndAdd(cp0, p, v, smooth * ratioNextSeg);
      } else {
        ctx.lineTo(p[0], p[1]);
      }
    }

    prevIdx = idx;
    idx += dir;
  }

  return k;
}
  1. Echarts 计算平滑曲线新算法

// Draw smoothed line in monotone, in which only vertical or horizontal bezier
// control points will be used. This should be used when points are monotone
// either in x or y dimension.
function drawMono(
  ctx,
  points,
  start,
  segLen,
  allLen,
  dir,
  smoothMin,
  smoothMax,
  smooth,
  smoothMonotone,
  connectNulls
) {
  var prevIdx = 0;
  var idx = start;
  for (var k = 0; k < segLen; k++) {
    var p = points[idx];
    if (idx >= allLen || idx < 0) {
      break;
    }
    if (isPointNull(p)) {
      if (connectNulls) {
        idx += dir;
        continue;
      }
      break;
    }

    if (idx === start) {
      ctx[dir > 0 ? "moveTo" : "lineTo"](p[0], p[1]);
    } else {
      if (smooth > 0) {
        var prevP = points[prevIdx];
        var dim = smoothMonotone === "y" ? 1 : 0;
        // Length of control point to p, either in x or y, but not both
        var ctrlLen = (p[dim] - prevP[dim]) * smooth;
        v2Copy(cp0, prevP);
        cp0[dim] = prevP[dim] + ctrlLen;
        v2Copy(cp1, p);
        cp1[dim] = p[dim] - ctrlLen;
        ctx.bezierCurveTo(cp0[0], cp0[1], cp1[0], cp1[1], p[0], p[1]);
      } else {
        ctx.lineTo(p[0], p[1]);
      }
    }

    prevIdx = idx;
    idx += dir;
  }

  return k;
}

对于三维贝塞尔曲线来说,控制点是两个,我们可以用递归的思想来看这个三维的贝塞尔曲线: 首先 Q0,Q1,Q2 分别满足在各自线段上的一维贝塞尔方程,然后,使用 Q0,Q1,Q2 再进行二维贝塞尔曲线的进行线性插值,二维曲线又可细化为一维贝塞尔曲线。