手寫(xiě)圖表指南,你學(xué)會(huì)了嗎?
1、前言
說(shuō)到數(shù)據(jù)可視化,大家應(yīng)該都不陌生。它旨在借助于圖形化手段,清晰有效的傳達(dá)與溝通信息。廣義的數(shù)據(jù)可視化涉及信息技術(shù)、自然科學(xué)、統(tǒng)計(jì)分析、圖形學(xué)等多種學(xué)科。
圖例來(lái)源網(wǎng)絡(luò)
我們熟知的圖形、圖表以及地圖等都屬于數(shù)據(jù)可視化的范疇。今天我們主要討論數(shù)據(jù)可視化中的圖表,像柱狀圖、折線圖、面積圖、餅圖、熱力圖都是使用頻率非常高的圖表。
圖例來(lái)源網(wǎng)絡(luò)
如果要在移動(dòng)端繪制一個(gè)類(lèi)似于下圖,使用真實(shí)數(shù)據(jù)渲染的簡(jiǎn)單面積圖表,我們應(yīng)該如何實(shí)現(xiàn)它呢?相信大家腦子里應(yīng)該都有各種方案了,那么接下來(lái)我們就來(lái)一步步實(shí)現(xiàn)它。
2、技術(shù)選型
需求
- 圖表樣式定制化圖表樣式為我司設(shè)計(jì)師獨(dú)立設(shè)計(jì),最終實(shí)現(xiàn)效果應(yīng)該做到100%還原設(shè)計(jì)細(xì)節(jié);
- 交互效果默認(rèn)情況下數(shù)據(jù)游標(biāo)只顯示當(dāng)前數(shù)據(jù)點(diǎn),如需查看其他月份或者時(shí)刻數(shù)據(jù),需要用戶手動(dòng)點(diǎn)擊切換;
- 曲線面積圖最終需要繪制出一個(gè)面積圖,也就是用真實(shí)數(shù)據(jù)繪制出的曲線與坐標(biāo)軸相交而形成的一個(gè)區(qū)域;
明確了具體的需求之后,我們就可以考慮技術(shù)方案選型了。
2.1 圖表庫(kù)
目前業(yè)界有很多成熟的圖表庫(kù),像我們熟知的highcharts、echarts,Bizcharts,G2,更高階的three.js等等。如果采用現(xiàn)有圖表庫(kù)來(lái)實(shí)現(xiàn)上述圖表的話,會(huì)存在以下一些問(wèn)題。
- 無(wú)法100%還原圖表樣式
- 包體積大,引入會(huì)造成項(xiàng)目性能問(wèn)題
引入現(xiàn)有圖表庫(kù)的方案固然非常簡(jiǎn)單,大大節(jié)省了前端同學(xué)的開(kāi)發(fā)量。但是存在著以上兩個(gè)比較突出的問(wèn)題。
圖表庫(kù)的圖表樣式都是通過(guò)配置完成,實(shí)現(xiàn)出來(lái)的效果在某些細(xì)節(jié)上難以完全還原設(shè)計(jì)稿,并且翻文檔測(cè)試配置項(xiàng)的過(guò)程也比較繁瑣。而且如果后續(xù)設(shè)計(jì)同學(xué)需要優(yōu)化圖表樣式,并且此優(yōu)化難以通過(guò)現(xiàn)有圖表庫(kù)配置項(xiàng)實(shí)現(xiàn)的話,那可能就需要二次開(kāi)發(fā)圖表庫(kù),對(duì)我們來(lái)說(shuō),也是一個(gè)不小的工作量;
通常C端的圖表需求并不是那么通用,可能一個(gè)項(xiàng)目也就實(shí)現(xiàn)這么一兩個(gè)圖表,如果引入圖表庫(kù)的話,對(duì)項(xiàng)目本身來(lái)說(shuō),無(wú)形中又增加了一些打包成本。那有些同學(xué)可能會(huì)說(shuō),現(xiàn)在的某些圖表庫(kù)已經(jīng)可以按需引用了,這樣增加打包體積這個(gè)問(wèn)題可能就不是問(wèn)題了,雖然現(xiàn)在的某些比較成熟的圖表庫(kù)可以按需引用,但是在引用某個(gè)圖表文件之前還是要引入一些核心文件,這些核心文件依然會(huì)占據(jù)不小的包體積??偨Y(jié)來(lái)說(shuō),引入現(xiàn)有圖表庫(kù)的方案成本高、靈活性差。
2.2 canvas
canvas相信對(duì)每一個(gè)前端開(kāi)發(fā)者來(lái)說(shuō)都不陌生,如果我們采用canvas來(lái)繪制圖表的話,有兩個(gè)問(wèn)題比較棘手,上文中有提到過(guò),我們要實(shí)現(xiàn)的圖表是有交互效果的,當(dāng)用戶點(diǎn)擊數(shù)據(jù)點(diǎn)的時(shí)候,則需要顯示當(dāng)前數(shù)據(jù)點(diǎn)的數(shù)據(jù)游標(biāo),再點(diǎn)擊其他數(shù)據(jù)點(diǎn)的時(shí)候,數(shù)據(jù)游標(biāo)也要相應(yīng)的切換。大家都知道,使用原生canvas來(lái)實(shí)現(xiàn)事件系統(tǒng)異常麻煩,并且canvas的重繪機(jī)制也是我非常不喜歡的一點(diǎn)??偨Y(jié)一下,原生canvas沒(méi)有完備的事件系統(tǒng),重繪機(jī)制繁瑣;
當(dāng)然,現(xiàn)在也有很多優(yōu)秀的canvas框架能夠解決上述問(wèn)題,比如fabric.js和konva.js,尤其是fabric.js,讓我們使用canvas不再別扭,感興趣的同學(xué)也可以嘗試一下。
2.3 svg
svg是一種基于XML語(yǔ)法的圖像格式,是可縮放的矢量圖形。那什么是矢量圖形呢?矢量圖是計(jì)算機(jī)圖形學(xué)中用點(diǎn)、直線或者多邊形等基于數(shù)學(xué)方程的幾何圖元表示的圖像,所以矢量圖具有無(wú)論放大多少倍都不會(huì)失真的特性。而與之相對(duì)應(yīng)的則是位圖,位圖是用像素陣列表示的圖像。svg在繪制圖表上有天然的優(yōu)勢(shì),
- 開(kāi)發(fā)成本低svg基于XML語(yǔ)法,XML語(yǔ)法是一種類(lèi)似于HTML語(yǔ)法的可擴(kuò)展標(biāo)記語(yǔ)言,也就是說(shuō)svg是使用一系列的元素(line、circle,polygon等)來(lái)描述圖形的。那svg元素和dom元素之間是不是存在著某種關(guān)聯(lián)呢?
我們由元素間的繼承關(guān)系可以得出的結(jié)論是:svg元素和dom元素基本相似,因此對(duì)于svg元素,完全可以從dom元素的角度去理解和應(yīng)用,上手成本幾乎就可以忽略不計(jì)了。并且svg和css,javascript等其他網(wǎng)絡(luò)標(biāo)準(zhǔn)無(wú)縫銜接。本質(zhì)上,svg相對(duì)于圖像,就好比html相對(duì)于文本;
- 完備的事件系統(tǒng)由于svg元素與dom元素類(lèi)似,因此dom元素中的事件系統(tǒng)對(duì)于svg同樣適用;
- 文件體積小,兼容性好前文已經(jīng)介紹過(guò),svg繪制出來(lái)的是一種矢量圖形,而矢量圖形都是使用點(diǎn)、直線等幾何圖元構(gòu)成的圖形,是對(duì)圖像的圖形描述,本質(zhì)上依然是文本文件,所以它具有體積小的天然優(yōu)勢(shì)。svg是由萬(wàn)維網(wǎng)聯(lián)盟(W3C)自1999年開(kāi)始開(kāi)發(fā)的開(kāi)放標(biāo)準(zhǔn)。兼容性方面幾乎所有主流瀏覽器都支持。
因此,最終我選擇了使用svg來(lái)繪制圖表。
3、svg基礎(chǔ)
在我們正式繪制圖表之前,首先需要了解一些svg的基礎(chǔ)知識(shí)。
3.1 svg元素
svg圖像就是使用不同的svg元素來(lái)創(chuàng)建的,svg元素常用的主要分為動(dòng)畫(huà)元素,形狀元素,字體元素,圖形元素,文本元素等。
- 形狀元素<circle>, <ellipse>, <line>, <mesh>, <path>, <polygon>, <polyline>, <rect>形狀元素是繪制svg圖像最常用的,path元素是svg中一個(gè)非常強(qiáng)大的元素,它類(lèi)似于canvas中的path,利用它能夠繪制出任何你想要的圖形。在我們本次繪制圖表過(guò)程中,path元素亦不可或缺;
- 動(dòng)畫(huà)元素<animate>,<animateColor>,<animateMotion>,<animateTransform>,<discard>,<mpath>,<set>想要給svg元素添加動(dòng)畫(huà),最簡(jiǎn)單的方式是使用動(dòng)畫(huà)元素,即用動(dòng)畫(huà)元素包裹住svg圖形,即可添加動(dòng)畫(huà);
其他元素就不再贅述。
3.2 svg應(yīng)用場(chǎng)景
- iconfont圖標(biāo)庫(kù)和字體庫(kù)iconfont圖標(biāo)庫(kù)應(yīng)該是svg最常見(jiàn)的一個(gè)使用場(chǎng)景,svg矢量圖、文件小的特性使得它非常適合來(lái)繪制小圖標(biāo),像我們轉(zhuǎn)轉(zhuǎn)的圖標(biāo)庫(kù)也是使用svg來(lái)繪制的。svg繪制圖標(biāo)也有一些小小的缺點(diǎn),比如它只能繪制純色或者css漸變色圖標(biāo),從顏色方面來(lái)說(shuō)沒(méi)有圖片色系豐富,層次分明。
- 業(yè)務(wù)動(dòng)畫(huà)我們業(yè)務(wù)中一些常用的動(dòng)畫(huà)場(chǎng)景也會(huì)使用svg實(shí)現(xiàn),比如loading效果,圓環(huán)進(jìn)度條,商品添加購(gòu)物車(chē)特效等;像商品添加購(gòu)物車(chē)的特效在電商網(wǎng)站是非常常見(jiàn)的,一般我們的實(shí)現(xiàn)思路是使用js+css動(dòng)畫(huà)實(shí)現(xiàn);其實(shí)svg中的路徑動(dòng)畫(huà)更適用于這個(gè)場(chǎng)景,我們可以在需要加購(gòu)的商品和購(gòu)物車(chē)之間繪制一條隱形的path,當(dāng)用戶觸發(fā)加購(gòu)操作的時(shí)候觸發(fā)路徑動(dòng)畫(huà),即animateMotion,這樣也可以實(shí)現(xiàn)同樣的功能。
4、svg如何繪制圖表?
通過(guò)以上對(duì)背景以及一些前置知識(shí)的介紹,相信大家已經(jīng)對(duì)svg有了一個(gè)初步的了解,接下來(lái)我們就回到最初的問(wèn)題,如何通過(guò)svg來(lái)從頭開(kāi)始繪制一個(gè)曲線面積圖?我主要分了以下幾個(gè)步驟,下文會(huì)對(duì)每個(gè)步驟逐一進(jìn)行說(shuō)明。
4.1 坐標(biāo)系
計(jì)算機(jī)繪圖使用的坐標(biāo)系統(tǒng)都是網(wǎng)格坐標(biāo)系。其以左上角作為坐標(biāo)系的原點(diǎn),X軸正方形向右逐漸開(kāi)始增大,Y軸正方向向下逐漸開(kāi)始增大。
圖例來(lái)源于網(wǎng)絡(luò)
了解了svg的坐標(biāo)系之后,我們來(lái)繪制曲線面積圖中的坐標(biāo)系,坐標(biāo)系其實(shí)就是由兩條線相交而成,svg中的line元素就是用來(lái)繪制直線的,所以使用line元素就可以繪制出X軸和Y軸。需要注意的是svg的坐標(biāo)系原點(diǎn)在左上角,而我們需要實(shí)現(xiàn)的圖表中坐標(biāo)系原點(diǎn)在左下角,所以在實(shí)現(xiàn)的時(shí)候要對(duì)y軸的實(shí)際坐標(biāo)進(jìn)行處理。
4.2 網(wǎng)格
在我們需要實(shí)現(xiàn)的兩個(gè)圖表中,圖表背景處均有網(wǎng)格,網(wǎng)格的實(shí)現(xiàn)原理也是使用line元素,只要標(biāo)記好起點(diǎn)以及終點(diǎn),就可以完美繪制。此處不再展開(kāi)。
4.3 數(shù)據(jù)點(diǎn)和數(shù)據(jù)游標(biāo)
數(shù)據(jù)點(diǎn):即用來(lái)標(biāo)記當(dāng)前數(shù)據(jù)位置的小原點(diǎn),數(shù)據(jù)點(diǎn)有兩種狀態(tài),分別是未點(diǎn)擊態(tài)和點(diǎn)擊態(tài),實(shí)現(xiàn)數(shù)據(jù)點(diǎn)我們使用svg中的circle元素即可。當(dāng)數(shù)據(jù)點(diǎn)被點(diǎn)擊時(shí),我們只需要更改circle元素的填充屬性。
數(shù)據(jù)游標(biāo):數(shù)據(jù)游標(biāo)在我們的圖表里是一個(gè)不規(guī)則圖形,其有點(diǎn)類(lèi)似于會(huì)話氣泡。我們要實(shí)現(xiàn)數(shù)據(jù)游標(biāo)有兩種方式,第一種方式是使用svg的path元素來(lái)繪制,那path元素的參數(shù)具體應(yīng)該怎么設(shè)置呢?其實(shí)可以跟設(shè)計(jì)師同學(xué)溝通,一般設(shè)計(jì)同學(xué)在用設(shè)計(jì)軟件導(dǎo)出的時(shí)候,設(shè)計(jì)軟件會(huì)攜帶path元素的具體參數(shù),這是方案一;還有第二種比較簡(jiǎn)單的方案是利用svg中的image元素,也就是將數(shù)據(jù)游標(biāo)當(dāng)作一個(gè)圖片繪制到圖表中,這種方案比較簡(jiǎn)單省事,我采用的也是此方案。
4.4 曲線
接下來(lái)就要繪制圖表中最重要的一個(gè)部分,也就是用真實(shí)數(shù)據(jù)渲染出來(lái)的一條曲線,繪制曲線我們依然是利用path元素繪制貝塞爾曲線,貝塞爾曲線只需要少量的點(diǎn)就可以繪制一條光滑曲線。在svg中,path元素用來(lái)繪制貝塞爾曲線的命令有兩組,第一組是C,S命令,用來(lái)繪制三次貝塞爾曲線;第二組是Q,T命令,用來(lái)繪制二次貝塞爾曲線。
我繪制圖表使用的是三次貝塞爾曲線,那首先了解一下三次貝塞爾曲線。
其中,t代表斜率,取值為0-1;p0代表起始點(diǎn)坐標(biāo)(x0,y0);p1代表第一個(gè)控制點(diǎn)坐標(biāo)(x1,y1);p2代表第二個(gè)控制點(diǎn)坐標(biāo)(x2,y2);p3代表終點(diǎn)坐標(biāo)(x3,y3);pt代表這條曲線上的任意一個(gè)點(diǎn)坐標(biāo)(xt,yt)。當(dāng)t由0-1逐漸變化的時(shí)候,可以得到一系列的(xt,yt),這一系列(xt,yt)就組成了一條三次貝塞爾曲線,這就是三次貝塞爾曲線的定義。
通過(guò)以上介紹可知,繪制三次貝塞爾曲線必須得知道起始點(diǎn)、兩個(gè)控制點(diǎn)以及終點(diǎn)。后端會(huì)返回給我們相應(yīng)的幾個(gè)數(shù)據(jù)點(diǎn),也就是說(shuō)這幾個(gè)數(shù)據(jù)點(diǎn)的坐標(biāo)是已知的,現(xiàn)在的問(wèn)題就成了給定一組已知數(shù)據(jù)點(diǎn),如何擬合成一條曲線?其實(shí)思路很簡(jiǎn)單,假如說(shuō)有已知的5個(gè)點(diǎn),那么我們將第一個(gè)點(diǎn)作為起始點(diǎn),第二個(gè)點(diǎn)作為終點(diǎn),計(jì)算出他們之間的控制點(diǎn),繪制一條曲線,同樣的,又以第二個(gè)點(diǎn)作為起點(diǎn),第三個(gè)點(diǎn)作為終點(diǎn),再重復(fù)以上過(guò)程,最終即繪制出一條橫穿五個(gè)點(diǎn)的平滑曲線。
此處附上算法源碼
4.5 面積
最后一步就是繪制曲線與X軸和Y軸相交而形成的面積部分。假如說(shuō)這條曲線不是一條曲線而是一條折線的話,那么其實(shí)很容易就能實(shí)現(xiàn)。我們將這條折線與X軸和Y軸連接起來(lái)形成一個(gè)閉合圖形polygon,然后通過(guò)給polygon進(jìn)行填充即可得到折線的面積圖。
我們利用這個(gè)思路,如果一條折線上的點(diǎn)足夠多的話,那么這條折線就會(huì)無(wú)限趨近于一條曲線。反之,一條曲線也可以看成是無(wú)限多的點(diǎn)構(gòu)成的折線,所以我們利用svg中的getTotalLength()和getPointAtLength()這兩個(gè)方法就可以將path轉(zhuǎn)換為多邊形,最后再填充多邊形即可得到最終的面積圖。
5、結(jié)語(yǔ)
通過(guò)以上5個(gè)步驟,我們就能夠基于svg從頭開(kāi)始實(shí)現(xiàn)一個(gè)簡(jiǎn)單的曲線面積圖。svg的使用場(chǎng)景還是非常豐富的,并且兼容性一直都不錯(cuò),如果需要實(shí)現(xiàn)這種相對(duì)不那么復(fù)雜且交互少的圖形,svg還是一個(gè)不錯(cuò)的方案。如果要實(shí)現(xiàn)復(fù)雜圖層、復(fù)雜動(dòng)效以及復(fù)雜交互,canvas框架可能會(huì)是一個(gè)更好的選擇。
最后,開(kāi)年第一篇,祝大家新年快樂(lè),2023突(兔)飛猛進(jìn),大展鴻圖(兔),前途(兔)無(wú)量!
參考
??https://www.infoq.cn/article/ogwddr4u8x0s*5aaytsh??? ??https://gist.github.com/mingzhi22/be3324ffd9765687ea2f??