「線」是可視化展示中最多見的圖形元素,最直觀的就是折線圖,如圖一。前端
圖一 折線圖git
一條線由多個點來定義,按照點與點之間的鏈接方式,一般將線分爲「折線」和「曲線」,在畫法上又分爲「實線」和「虛線」,如圖二:github
圖二 折線和曲線web
咱們也常用線來繪製閉合的路徑,從而造成可填充區域,好比面積圖和雷達圖,如圖三和圖四。算法
圖三 面積圖sql
圖四 雷達圖canvas
本篇文章在 Canvas API 的基礎上,爲你們講解可視化研發中線的畫法封裝和線的動畫實現方案(總體方案創建在圖形學基礎上,一樣適用於WegGL 和3D場景)。api
前面咱們提到過線的基本組成單位是「點(Point)」,兩個相鄰的點鏈接在一塊兒成爲一個「段(Segment)」,多個段拼裝在一塊兒組成一條線。如圖六,這條線由7個點劃分紅的6個段組合而成。markdown
圖六 點和段echarts
曲線的每一個段的起止點會由於插值算法的不一樣而不一樣,後面咱們會詳細介紹。
圖七所示的僞代碼展現了咱們對線的基本定義。
圖七 線的定義
線的繪製是以段爲單位的,不一樣的形狀的線對段的拆分邏輯和畫法都是有區別的,咱們從最簡單的折線開始。
折線對段的拆分很簡單,根據傳入的點數據,相鄰兩點劃爲一段。
圖八 折線段的拆分
如上面的代碼,實現很簡單,依次遍歷點數據,初始化段對象。這裏有一個計算段的長度的操做,段的長度在動畫場景是必須參數,在非動畫場景則能夠不用關心。折線的段的長度計算,就是計算一個線段的長度(兩點間距離),如圖九所示。
圖九 線段的距離計算
另外圖八的代碼中,有一段是不是空段的判斷邏輯。在實際的線圖應用中,咱們在某些狀況須要隱藏線的某些段,好比傳入了空數據或者用戶指定了過濾條件。
圖十 空段
在Canvas中畫線段只須要兩個api——moveTo 和 lineTo。圖十一展現了鏈接[(0,0),(300,150),(400,150)]三個點的折線。
圖十一 moveTo 與 lineTo
從上面的示例能夠看到,Canvas 中繪製線段,只須要經過moveTo將畫筆(Canvas 繪圖上下文)定位到線段的起點,而後經過lineTo 繪製到線段的終點便可。多個首位相接的線段能夠省略moveTo,直接lineTo。 要實現圖十的空段效果,只須要moveTo到新段的起點便可,例如:
圖十二 繪製空段
理解了基本的api以後,咱們回到咱們的折線上來,看看以段爲單位的繪製方法。
基於上面畫線的方法,咱們只須要遍歷一條線中的全部段,依次鏈接就能夠了。爲了處理空段的繪製,設置一個lineStart的標記變量,若是處於start狀態,會先moveTo到新的點,而不是lineTo。大體的繪圖流程以下:
圖十三 線的繪製基本流程
drawSegment方法以下:
圖十四 drawSegment
這裏你可能要疑惑,這裏將線拆成段並無什麼優點,爲何不直接鏈接各個點呢?分紅段完成了一個線的繪製的骨架,在這個骨架基礎上,不少功能都會很容易的擴展。好比,線的每一段都有不一樣的含義,可視化層面要展示這些不一樣的含義須要給線賦予不一樣的樣式。這裏咱們能夠給LineSegment配置一個LineSegConfig,獨立配置每一個段的樣式,在繪製的過程當中若是發現新的段的樣式發生了變化,就能夠當即進行渲染,而後開始繪製新段,靈活拼裝。好比下圖,末尾的紅色虛線用來表示預測數據。
圖十五 分段渲染不一樣樣式
另外,分段會大大下降動畫效果的實現成本,後面咱們詳細介紹。
瞭解了折線的基本畫法以後,咱們來看看曲線。
曲線有不少種,畫曲線的方法也有不少種。因爲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值,就能夠獲得對應的點。
對應的,三次貝塞爾曲線由四個點組成,經過更多的迭代步驟來肯定曲線的上點,如圖二十一所示。完整的生成若是如圖二十二所示。
圖二十一 三次貝塞爾曲線
圖二十二 三次貝塞爾曲線生成過程
三次貝塞爾曲線的數學公式爲:
在canvas中繪製二次貝塞爾曲線使用的是 quadraticCurveTo 函數,參數定義以下:
函數只定義了控制點和終點,起點須要咱們使用moveTo來肯定,如圖二十三的代碼示例。
圖二十三 canvas繪製二次貝塞爾曲線
三次貝塞爾曲線使用 bezierCurveTo() 方法來繪製,參數定義以下:
和二次曲線的繪製方式相似,如圖二十四。
圖二十四 canvas繪製三次貝塞爾曲線
下面的動圖展現了控制點對貝塞爾曲線形狀的影響。
圖28 控制點對貝塞爾曲線的影響
咱們瞭解瞭如何繪製三次貝塞爾曲線,可是回到咱們的線圖,一個線圖會有不肯定數量的點被平滑的鏈接起來,可是目前三次貝塞爾曲線顯然沒法知足這個需求。咱們前面談到了分段的概念,一條完整的曲線被分紅了多段,若是每一段都是一條三次貝塞爾曲線,問題就解決了。那麼問題就轉化成了如何構造多條能依次平滑拼接的貝塞爾曲線。在圖形學中有個概念叫「樣條曲線」,專業的概念有點難懂,咱們這裏簡單理解就是將一個點的集合,分紅多段曲線,各曲線處的鏈接點處有能夠平滑鏈接(有連續的一次和二次導數)。關於樣條曲線的連續性以及貝塞爾曲線的更多特性,讀者能夠參考《計算機圖形學(第四版)》一書第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個,而後咱們利用以下邏輯進行段的獲取:
下面來看看 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就是第一段的終點,第二段的起點,這樣咱們只須要計算控制點便可。根據咱們以前對貝塞爾曲線繪製過程的理解,咱們能夠得出以下結論:
依據上面的結論,三次貝塞爾曲線拆分的方法就很容易實現了:
圖34 貝塞爾曲線拆分
圖34 所示代碼中 pointAt 方法爲根據t獲取直線上點的方法。以下:
圖35 根據t獲直線上的點
咱們能夠在任意位置對三次貝塞爾曲線進行拆分了,結合二分法,控制迭代次數,結合近似長度計算函數,咱們能夠獲得想要精度的長度值了。如圖36。
圖36 三次貝塞爾曲線的分割
內部細節咱們都梳理清楚了,獲取全部的段也很簡單了。如今須要特殊處理的是最後一個點數據,這裏咱們將第二個點和第三個點都用最後一個點表示。
圖37 basis 最後一段生成方法
關於曲線的全部準備工做都完成了,下面咱們要把它畫出來。和畫折線的方法相似,咱們只須要循環調用"段" 的繪製方法進行繪圖便可。內部,只須要調用bezierCurveTo便可。以下:
圖38 繪製曲線的段
咱們完成了折線和曲線的繪製,想要線經過動畫的方式畫出來,只須要作少許的改動。首先不論直線仍是曲線咱們都分紅了多段,每一段都是和t相關的函數。
動畫和非動畫的本質區別就是一次畫多少的問題,咱們將整條線圖的繪製放置在[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 動畫
實現線和麪積的動畫的方案還有總體Clip和生成點集兩種方案,下面咱們簡單對比一下,以說明咱們的分段繪製的優點。
方案 | 簡介 | 函數調用 | 基於曲線的軌跡動畫 | 不規則線 | 分段擴展 |
預生成點集 | 是利用曲線函數,預生成足夠密度的點,而後將各點鏈接 | 較多。會產生大量的繪圖函數的調用 | 支持 | 支持 | 能夠支持,比較麻煩,也要有段的概念 |
總體clip | 繪製以前設置一個裁剪窗口,調整裁剪窗口的大小來實現動畫 | 較少 | 不支持,不能動態計算當前t值的x,y | 不支持。只能在一個方向上clip,不能照顧x,y座標值無序狀況。 | 不支持 |
分段模型 | 略 | 一個圖最多調用n-1 次 | 支持 | 支持 | 支持 |
上面咱們看到的動畫不一樣的線之間雖然能夠再同一時間到大終點,可是過程當中在x方向的位移是不一樣步的。同步和不一樣步都各有需求,尤爲是在面積圖狀況下,單個面積圖實際被拆分了上下兩組segment。如圖41.
圖41 基本面積圖的segement
咱們觀察上面面積圖的繪圖動畫,它是從左到右推動的,好比當前的t值繪製到圖41的矩形框的位置,那麼首先會繪製第一段,計算第12段應該被繪製的區間,最後填充上下兩段的閉合區間。這裏有一個問題,若是是相同的t值,帶入1和12的函數,產生的x值是不同的,那麼繪製出來的效果就不對了,切面多是斜的。
解決這個問題作法是根據x或者y值反求t值,再帶入目標函數中。對於三次貝塞爾曲線來講,這又是一個大難題,因爲篇幅所限及代碼實現的比較複雜,這裏就再也不講解了,你們能夠參考文後的參考資料。
一個超酷的貝塞爾類庫: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 」
複製代碼