“线”是可视化展现中最常见的图形元素,最直观的就是折线图,如图一。
图一 折线图
一条线由多个点来定义,按照点与点之间的连接方式,通常将线分为“折线”和“曲线”,在画法上又分为“实线”和“虚线”,如图二:
图二 折线和曲线
我们也经常使用线来绘制闭合的路径,从而形成可填充区域,比如面积图和雷达图,如图三和图四。
图三 面积图
图四 雷达图
本篇文章在 Canvas API 的基础上,为大家讲解可视化研发中线的画法封装和线的动画实现方案(整体方案建立在图形学基础上,同样适用于WegGL 和3D场景)。
* 0.1 线的定义
前面我们提到过线的基本组成单位是“点(Point)”,两个相邻的点连接在一起成为一个“段(Segment)”,多个段拼装在一起组成一条线。如图六,这条线由7个点划分成的6个段组合而成。
图六 点和段
曲线的每个段的起止点会因为插值算法的不同而不同,后面我们会详细介绍。
图七所示的伪代码展示了我们对线的基本定义。
图七 线的定义
线的绘制是以段为单位的,不同的形状的线对段的拆分逻辑和画法都是有区别的,我们从最简单的折线开始。
0.2 折线画法
0.2.1 获取段
折线对段的拆分很简单,根据传入的点数据,相邻两点划为一段。
图八 折线段的拆分
如上面的代码,实现很简单,依次遍历点数据,初始化段对象。这里有一个计算段的长度的操作,段的长度在动画场景是必须参数,在非动画场景则可以不用关心。折线的段的长度计算,就是计算一个线段的长度(两点间距离),如图九所示。
图九 线段的距离计算
另外图八的代码中,有一段是否是空段的判断逻辑。在实际的线图应用中,我们在某些情况需要隐藏线的某些段,比如传入了空数据或者用户指定了过滤条件。
图十 空段
0.2.2 Canvas中线段的画法
在Canvas中画线段只需要两个api——moveTo 和 lineTo。图十一展示了连接[(0,0),(300,150),(400,150)]三个点的折线。
图十一 moveTo 与 lineTo
从上面的示例可以看到,Canvas 中绘制线段,只需要通过moveTo将画笔(Canvas 绘图上下文)定位到线段的起点,然后通过lineTo 绘制到线段的终点即可。多个首位相接的线段可以省略moveTo,直接lineTo。 要实现图十的空段效果,只需要moveTo到新段的起点即可,例如:
图十二 绘制空段
理解了基本的api之后,我们回到我们的折线上来,看看以段为单位的绘制方法。
0.2.3 折线绘制
基于上面画线的方法,我们只需要遍历一条线中的所有段,依次连接就可以了。为了处理空段的绘制,设置一个lineStart的标记变量,如果处于start状态,会先moveTo到新的点,而不是lineTo。大致的绘图流程如下:
图十三 线的绘制基本流程
drawSegment方法如下:
图十四 drawSegment
这里你可能要疑惑,这里将线拆成段并没有什么优势,为什么不直接连接各个点呢?分成段完成了一个线的绘制的骨架,在这个骨架基础上,很多功能都会很容易的扩展。比如,线的每一段都有不同的含义,可视化层面要展现这些不同的含义需要给线赋予不同的样式。这里我们可以给LineSegment配置一个LineSegConfig,独立配置每个段的样式,在绘制的过程中如果发现新的段的样式发生了变化,就可以立即进行渲染,然后开始绘制新段,灵活拼装。比如下图,末尾的红色虚线用来表示预测数据。
图十五 分段渲染不同样式
另外,分段会大大降低动画效果的实现成本,后面我们详细介绍。
了解了折线的基本画法之后,我们来看看曲线。
0.3 曲线画法
0.3.1 贝塞尔曲线
曲线有很多种,画曲线的方法也有很多种。由于Canvas 支持贝塞尔二次和三次曲线画法,曲线图表通常使用三次贝塞尔曲线画法,本文也将重点放在三次贝塞尔曲线的应用讲解上。那么什么是贝塞尔曲线呢?
Bézier curve(贝塞尔曲线)是应用于二维图形应用程序的数学曲线。贝塞尔曲线点的数量决定了曲线的阶数,一般N个点构成的N-1阶贝塞尔曲线,即3个点为二阶。一般我们都会要求曲线至少包含3个点,因为两个点的贝塞尔曲线是一条直线。按顺序,第一个点为 起点 ,最后一个点为 终点 ,其余点都为 控制点。
下面我们以二次贝塞尔曲线为例,讨论其生成过程。
二次贝塞尔曲线
给定点P0,P1,P2 ,P0 和 P2 为起点和终点,P1为控制点。从P0到P2的弧线即为一条二次贝塞尔曲线。
图十六 二次贝塞尔曲线
在这里我们要将整个曲线的绘制量化为从0~1
的过程,用t
为当前过程的进度,t
的区间即0~1
。每一条线都需要根据t
生成一个点,如下图,一个点从P0
移动到P1
,这是这条线从0~1
的过程。
下面我们还原一下一个二次贝塞尔曲线的生成过程。
图十七 绘制二次贝塞尔曲线(1)
如图十七,首先我们链接P0P1,P1P2,得到两条线段。然后我们对进度t进行取值,比如0.3,取一个Q0点,使得P0Q0的长度为P0P1总长度的0.3倍。
图十八 绘制二次贝塞尔曲线(2)
同时我们在P1P2上取一点Q1,使得 P0Q0: P0P1 = P1Q1: P1P2。接下来我们再在Q0Q1上取一点B,使得 P0Q0: P0P1 = P1Q1: P1P2 = Q0B:Q0Q1,如图十九。
图十九 绘制二次贝塞尔曲线(3)
现在我们得到的点B就是二次贝塞尔曲线的上的一个点,如果我们使t=0开始取值,逐步递增进行插值,就会得到一系列的点B,进行连接就会形成一条完整的曲线,如图二十。
图二十 二次贝塞尔曲线绘制过程
上面展示了完整的二次贝塞尔曲线的产生过程,这个过程我们经过数学推导,最终可以得到如下公式:
根据这个公式,我们只要变更t值,就可以得到对应的点。
三次贝塞尔曲线
对应的,三次贝塞尔曲线由四个点组成,通过更多的迭代步骤来确定曲线的上点,如图二十一所示。完整的生成如果如图二十二所示。
图二十一 三次贝塞尔曲线
图二十二 三次贝塞尔曲线生成过程
三次贝塞尔曲线的数学公式为:
0.3.2 Canas中如何绘制贝塞尔曲线
在canvas中绘制二次贝塞尔曲线使用的是 quadraticCurveTo 函数,参数定义如下:
函数只定义了控制点和终点,起点需要我们使用moveTo来确定,如图二十三的代码示例。
图二十三 canvas绘制二次贝塞尔曲线
三次贝塞尔曲线使用 bezierCurveTo() 方法来绘制,参数定义如下:
和二次曲线的绘制方式类似,如图二十四。
图二十四 canvas绘制三次贝塞尔曲线
下面的动图展示了控制点对贝塞尔曲线形状的影响。
图28 控制点对贝塞尔曲线的影响
0.3.3 样条曲线 与 获取段
我们了解了如何绘制三次贝塞尔曲线,但是回到我们的线图,一个线图会有不确定数量的点被平滑的连接起来,但是目前三次贝塞尔曲线显然无法满足这个需求。我们前面谈到了分段的概念,一条完整的曲线被分成了多段,如果每一段都是一条三次贝塞尔曲线,问题就解决了。那么问题就转化成了如何构造多条能依次平滑拼接的贝塞尔曲线。在图形学中有个概念叫“样条曲线”,专业的概念有点难懂,我们这里简单理解就是将一个点的集合,分成多段曲线,各曲线处的连接点处有可以平滑连接(有连续的一次和二次导数)。关于样条曲线的连续性以及贝塞尔曲线的更多特性,读者可以参考《计算机图形学(第四版)》一书第14章——《样条表示》,这里我们就不深入解释了,直接看例子。
图29 一段由四条三次贝塞尔曲线拼接而成的曲线
以图29为例,如我我们要将这条曲线分成四条三次贝塞尔曲线,我们要确定两个参数:
- 每条三次贝塞尔曲线的起点和终点
- 每条三次贝塞尔曲线的两个控制点
只有选取合适的起点、终点和控制点,我们才能使得相邻的两条曲线可以平滑连接。样条曲线的拆分算法有很多种,这里也不详细介绍了,感兴趣的同学可以参考图形学相关书籍;JavaScript 实现可以参考 d3-shape 的 Curves 接口(github.com/d3/d3-shape),d3-shape Curves 中的curveBasis、curveBasisClosed、curveBasisOpen、curveBundle、curveCardinal、curveCardinalClosed、curveCardinalOpen、curveCatmullRom、curveCatmullRomClosed、curveCatmullRomOpen、curveNatural、curveMonotoneX和curveMonotoneY都是基于三次贝塞尔曲线的样条实现。
下面我们以Basis 算法的实现为例,进行讲解曲线如何获取“段”。
主流程
Basis 算法要求点集中的点的数量至少为3个,然后我们利用如下逻辑进行段的获取:
- 图30 获取曲线的 “段” 的主流程
- 我们的主流程逻辑很简单,循环给到的点,从当前索引位置开始向后取出3个点,然后根据这三个点以及当前段的起始点计算结束点和控制点。每个新段的起点是上一个相邻段的终点。随后计算当前段的长度。 当前的循环逻辑不会计算到最后一个点,所以会少一个段,最后加个单独的逻辑来处理。
点的计算
下面来看看 Basis 算法点的计算:
- 图31 basis 样条算法
如图31,我们基于很简单的公式来计算各个点的值,这个公式是怎么来的呢?简单说是结合了B样条曲线和三次贝塞尔曲线在端点处的一阶和二阶导出得来的。这里就不深入了,否则本篇文章会严重偏离主题,感兴趣的读者请参阅计算机图形学相关书籍。总之,我们通过公式计算可以得到我们需要的点。
曲线分割与长度计算
计算曲线的长度并不是一件容易的事情,由于贝塞尔函数是插值函数,所以计算方法就是先对曲线进行切割,切割到足够小的范围,然后计算这一小段的曲线近似长度,再累加。0.3.1节给出了三次贝塞尔曲线的函数,我们只需要将变量t取足够小的值,然后计算两个点之间的直线距离进行累加就可以,但是这种方案的性能消耗比较大。我在
community.khronos.org/t/3d-cubic-…
看到一种近似方法,利用该方法可以缩减切割次数。 基于三次贝塞尔曲线的函数,对一个贝塞尔曲线进行切割,很简单。我们再把图21拿来说明一下各点的计算。
图21
第一步:找到连接点
如图21,假设我要在t=0.25的位置将当前曲线切分成两条曲线,首先我们要知道点B的位置。根据公式带入即可:
图33 根据t计算3次贝塞尔的点
第二步:获取控制点
拿到点P之后,P就是第一段的终点,第二段的起点,这样我们只需要计算控制点即可。根据我们之前对贝塞尔曲线绘制过程的理解,我们可以得出如下结论:
- 第一段曲线的第一个控制点的运动轨迹是线段P0P1,和t线性相关
- 第一段曲线的第二个控制点的运动轨迹是线段Q0Q1,和t线性相关
- 第二段曲线的第一个控制点的运动轨迹是线段Q1Q2,和t线性相关
- 第二段曲线的第二个控制点的运动轨迹是线段P2P3,和t线性相关
依据上面的结论,三次贝塞尔曲线拆分的方法就很容易实现了:
图34 贝塞尔曲线拆分
图34 所示代码中 pointAt 方法为根据t获取直线上点的方法。如下:
图35 根据t获直线上的点
第三步 长度计算
我们可以在任意位置对三次贝塞尔曲线进行拆分了,结合二分法,控制迭代次数,结合近似长度计算函数,我们可以得到想要精度的长度值了。如图36。
图36 三次贝塞尔曲线的分割
获取段
内部细节我们都梳理清楚了,获取所有的段也很简单了。现在需要特殊处理的是最后一个点数据,这里我们将第二个点和第三个点都用最后一个点表示。
图37 basis 最后一段生成方法
0.3.4 曲线画法
关于曲线的所有准备工作都完成了,下面我们要把它画出来。和画折线的方法类似,我们只需要循环调用"段" 的绘制方法进行绘图即可。内部,只需要调用bezierCurveTo即可。如下:
图38 绘制曲线的段
0.4 动画
我们完成了折线和曲线的绘制,想要线通过动画的方式画出来,只需要做少量的改动。首先不论直线还是曲线我们都分成了多段,每一段都是和t相关的函数。
0.4.1 基本方案
动画和非动画的本质区别就是一次画多少的问题,我们将整条线图的绘制放置在[0,1]区间内,启动一个动画循环,每次绘图的时候更新的t的值,在我们上面循环绘制segment 的代码中,将整条线图的t转化为每一个段内部的t值。段 内部根据传入的t值,对自身进行切割,只画应该绘制的那部分。
图39 t值换算
因为我们已经计算了每个段的长度,和总长度,所以每个段的占比由长度可以获得,此占比在和整个线图的t值进行换算即可。
以图39为例,比如我们传入的t值为0.1,整条线图的0.1 换算到第一个段是0.4,那么第一个段只需绘制前40% 部分即可。我们在图39的基础上,做少量的改动。
图40 支持局部绘制
如图40,我们将外部计算的t(percent)传入绘制段的方法内,该方法会使用我们之前介绍过的 divideCubic 方法对当前曲线进行切割,然后进行局部绘制。效果如下:
图41 动画
0.4.2 和其他动画方案的对比
实现线和面积的动画的方案还有整体Clip和生成点集两种方案,下面我们简单对比一下,以说明我们的分段绘制的优势。
方案 | 简介 | 函数调用 | 基于曲线的轨迹动画 | 不规则线 | 分段扩展 | 预生成点集 | 是利用曲线函数,预生成足够密度的点,然后将各点连接 | 较多。会产生大量的绘图函数的调用 | 支持 | 支持 | 可以支持,比较麻烦,也要有段的概念 | 整体clip | 绘制之前设置一个裁剪窗口,调整裁剪窗口的大小来实现动画 | 较少 | 不支持,不能动态计算当前t值的x,y | 不支持。只能在一个方向上clip,不能照顾x,y坐标值无序情况。 | 不支持 | 分段模型 | 略 | 一个图最多调用n-1 次 | 支持 | 支持 | 支持 |
0.4.3 动画同步
上面我们看到的动画不同的线之间虽然可以再同一时间到大终点,但是过程中在x方向的位移是不同步的。同步和不同步都各有需求,尤其是在面积图情况下,单个面积图实际被拆分了上下两组segment。如图41.
图41 基本面积图的segement
我们观察上面面积图的绘图动画,它是从左到右推进的,比如当前的t值绘制到图41的矩形框的位置,那么首先会绘制第一段,计算第12段应该被绘制的区间,最后填充上下两段的闭合区间。这里有一个问题,如果是相同的t值,带入1和12的函数,产生的x值是不一样的,那么绘制出来的效果就不对了,切面可能是斜的。
解决这个问题做法是根据x或者y值反求t值,再带入目标函数中。对于三次贝塞尔曲线来说,这又是一个大难题,由于篇幅所限及代码实现的比较复杂,这里就不再讲解了,大家可以参考文后的参考资料。
0.5 参考资料:
一个超酷的贝塞尔类库:pomax.github.io/bezierjs/
一本超级棒的贝塞尔电子书 pomax.github.io/bezierinfo/
关于根据x或y反算t的讨论:www.zhihu.com/question/30…
图形学必读书物:《计算机图形学》
本文例子来源(字节跳动自研图表库):bytecharts.web.bytedance.net/
数据平台前端团队,在公司内负责风神、TEA、Libra、Dorado等大数据相关产品的研发。我们在前端技术上保持着非常强的热情,除了数据产品相关的研发外,在数据可视化、海量数据处理优化、web excel、sql编辑器、私有化部署、工程工具都方面都有很多的探索和积累,有兴趣可以与我们联系。对产品有任何建议和反馈也可以直接找我们进行反馈~
欢迎关注「 字节前端 ByteFE 」简历投递联系邮箱「 tech@bytedance.com 」
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!