知識(shí)
不管是網(wǎng)站,軟件還是小程序,都要直接或間接能為您產(chǎn)生價(jià)值,我們?cè)谧非笃湟曈X表現(xiàn)的同時(shí),更側(cè)重于功能的便捷,營(yíng)銷的便利,運(yùn)營(yíng)的高效,讓網(wǎng)站成為營(yíng)銷工具,讓軟件能切實(shí)提升企業(yè)內(nèi)部管理水平和效率。優(yōu)秀的程序?yàn)楹笃谏?jí)提供便捷的支持!
您當(dāng)前位置>首頁(yè) » 新聞資訊 » 小程序相關(guān) >
一起學(xué)習(xí)可視化圖表庫(kù)及字節(jié)小程序適配
發(fā)表時(shí)間:2021-1-4
發(fā)布人:葵宇科技
瀏覽次數(shù):123
這篇文章是對(duì)螞蟻金服可視化團(tuán)隊(duì) Antvis 的開源圖表方案 F2 的源碼閱讀,還有該庫(kù)的一大背景——圖形語(yǔ)法的了解學(xué)習(xí)。我們會(huì)從實(shí)際需求背景出發(fā),談到圖表方案大的背景、圖形語(yǔ)法這個(gè)陣營(yíng)的基本思想,然后進(jìn)入 F2 源碼的簡(jiǎn)要分析,其大致的學(xué)習(xí)分析思路是:整體架構(gòu)劃分 —— 各模塊分工 —— 重要模塊設(shè)計(jì)實(shí)現(xiàn)。源碼分析過后,又回到需求背景,記錄了在特定的需求場(chǎng)景中,如何運(yùn)用了解到的知識(shí)和背景解決實(shí)際問題——圖表庫(kù)的小程序環(huán)境適配。
希望這篇文章在使大家對(duì) F2、圖形語(yǔ)法有基本了解的同時(shí),能成為源碼閱讀過程中的一個(gè)助手,給大家的技術(shù)選型、技術(shù)實(shí)踐帶來幫助。
1.1 npm install @antv/f2寫下本文的起因、初衷
本文起始于一個(gè)業(yè)務(wù)上的需求,從該需求我們可以提煉出三個(gè)關(guān)鍵詞:
- 移動(dòng)端數(shù)據(jù)看板
- 字節(jié)小程序為載體
- 交互式圖表
業(yè)界有非常多優(yōu)秀的圖表庫(kù)方案,如百度的 ECharts,但是對(duì)標(biāo)移動(dòng)端小程序的成熟圖表庫(kù)幾乎還沒有。根據(jù)這三個(gè)關(guān)鍵詞,我們可以鎖定螞蟻金服可視化團(tuán)隊(duì) Antvis 的開源圖表方案 F2。
會(huì)涉及的
- 圖形語(yǔ)法之九牛一毛:一組概念、很多圖表
- F2 設(shè)計(jì)、圖形語(yǔ)法封裝簡(jiǎn)析
- 模塊/能力實(shí)現(xiàn)方案(交互式、動(dòng)畫)
- *Canvas 實(shí)踐、優(yōu)化策略
不會(huì)涉及的
- F2/G2 API 羅列或總結(jié)
- 業(yè)界圖表庫(kù)的橫向?qū)Ρ?/li>
- Canvas 更底層的渲染原理
- 可視化中的數(shù)據(jù)處理
本文大部分內(nèi)容出于自己在項(xiàng)目啟動(dòng)和實(shí)現(xiàn)過程中,對(duì)圖表庫(kù)的接入和源碼學(xué)習(xí)。同時(shí),會(huì)稍微拓展到一些通用的可視化基礎(chǔ)、圖形語(yǔ)法概念、還有從通用到 F2 實(shí)際實(shí)踐的繪圖策略,以及主要依賴的渲染引擎 Canvas。
文章中不會(huì)直接羅列大段代碼,項(xiàng)目的維護(hù)者都是螞蟻可視化團(tuán)隊(duì)的同學(xué),許多注釋是中文的,加上庫(kù)本身的設(shè)計(jì)和模塊劃分清晰簡(jiǎn)潔,看起來比較直觀易懂,所以只要把各個(gè)模塊還有邏輯講清楚,實(shí)際上已經(jīng)達(dá)到學(xué)習(xí)和初步了解的目的。
在此也應(yīng)該劃定本文不會(huì)涉及的內(nèi)容,首先本文不會(huì)聊很多具體的庫(kù) API,這不太具有通用性和普適性;同時(shí),因?yàn)橐仓皇菆D表庫(kù)的入門用戶,體驗(yàn)不深不廣,還沒有足夠的資質(zhì)去做深入客觀的橫向比對(duì)和評(píng)價(jià);另外,我們的視角會(huì)放在 Canvas 渲染引擎這一層以上,所以也不會(huì)涉及 Canvas 更底層的內(nèi)容;最后,我們還不會(huì)聊到數(shù)據(jù)處理,包括數(shù)據(jù)載入時(shí)的預(yù)處理、adjust 功能等,這也是一個(gè)大話題,并且涉及到的數(shù)學(xué)和統(tǒng)計(jì)學(xué)成分過高。
大致的學(xué)習(xí)背景和范圍圈定之后,我們接下來了解一下關(guān)于數(shù)據(jù)可視化、數(shù)據(jù)圖表的業(yè)界大背景,還有「圖形語(yǔ)法」這個(gè)重要概念。
項(xiàng)目歷史、設(shè)計(jì)初衷、架構(gòu)演進(jìn)
在 F2 的前輩 —— G2 的設(shè)計(jì)文檔中有這么一句話: “將數(shù)據(jù)映射到圖形,同時(shí)增加一些輔助信息,讓用戶讀懂?dāng)?shù)據(jù)?!?/strong>
這反映了可視化的本質(zhì),我們還可以從其中梳理出可視化的一般性流程:
- 數(shù)據(jù)處理:對(duì)數(shù)據(jù)的加工,使數(shù)據(jù)更具有內(nèi)在邏輯,對(duì)人類本身、或者算法、或者圖像處理需求提高可理解性;
- 圖形映射:從數(shù)據(jù),到位置 / 大小 / 顏色等具有區(qū)分度的信息的 map;
- 圖形展示:點(diǎn)線面,在各種媒介上的渲染;
- 輔助信息:視覺通道跟數(shù)據(jù)之間的映射關(guān)系,可能是一些參照、一些標(biāo)準(zhǔn),如坐標(biāo)軸、圖表中的圖例、更進(jìn)一步的輔助文本等。
可視化領(lǐng)域中的一大陣營(yíng)扛著 「圖形語(yǔ)法」 大旗,我們先對(duì)其進(jìn)行大致體會(huì)、感性認(rèn)識(shí)。
它是一本想要為圖形學(xué)、數(shù)學(xué)、計(jì)算機(jī)科學(xué)這個(gè)交叉領(lǐng)域建立系統(tǒng)化描述的著作,同時(shí)也是學(xué)術(shù)界非常具有分量的作品。作者 Leland Wilkinson 是一位統(tǒng)計(jì)學(xué)家,所以使用了非常嚴(yán)密的、形式化的系統(tǒng)描述,來建立模型和自我論證,內(nèi)容因此會(huì)比較不好懂,個(gè)人也沒能完整看完,但有一些關(guān)鍵概念我們可以有大致的感性體會(huì)。
單拎我們的主題,圖表這一塊來講,圖形語(yǔ)法將圖表的這個(gè)數(shù)據(jù)呈現(xiàn)概念看作「數(shù)據(jù)」和「幾何圖形的視覺特征」綁定的結(jié)果,圖表本身,可以被認(rèn)為是相互正交的特征組合而成的結(jié)果。這個(gè)概念也是我們?cè)谑褂妙愃?G2、F2 這樣的圖形語(yǔ)法封裝庫(kù)中能很直觀體會(huì)到的。
直接來認(rèn)識(shí)一下兩個(gè)正交的概念:
Visual Cue 數(shù)據(jù)點(diǎn)特征:- Position 位置
- Length 長(zhǎng)度
- Angle 角度
- Direction 方向
- Shapes 形狀
- Area or Volume 面/體積
- Color 顏色(Saturation 飽和度、Hue 色相)
Coordinate 坐標(biāo)系:Cartesian 笛卡爾坐標(biāo)系、Polar 極坐標(biāo)系、*Geographic 地理坐標(biāo)系(地理坐標(biāo)系在我們的圖表庫(kù)中常常單獨(dú)抽出,比如同是螞蟻 antvis 的 L7)
將上面兩者進(jìn)行 正交 ,組成描述能力豐富的圖表們:
以上三張圖源:Data Points 講義 (英文版正交圖被分開故使用中文版素材圖)
這樣的設(shè)計(jì)和歸納,使基于圖形語(yǔ)法封裝的圖表庫(kù)不同于其他的“零售”式、“授人以魚”式的圖表(即按直觀的圖表類型作為直接接口,進(jìn)行配置化的圖表開發(fā),如大哥 ECharts),G2、F2 有一套自己的表示系統(tǒng),圖表開發(fā)者可以用其進(jìn)行圖表描述。
但并不是說這兩種陣營(yíng)有孰優(yōu)孰劣之分:
- 圖表語(yǔ)法本身是比較完備的一種思想、一個(gè)描述體系,并非實(shí)現(xiàn)標(biāo)準(zhǔn),事實(shí)上其本身的學(xué)習(xí)成本也是不低的;
- 基于此的圖表庫(kù)也一樣,一開始上手使用的時(shí)候有點(diǎn)反直覺,學(xué)習(xí)曲線比較陡峭,跟想象中的配置化圖表差別較大,同時(shí)若無圖形語(yǔ)法相關(guān)背景,前期也常常很難從文檔找到想了解的內(nèi)容;
- 圖表庫(kù)也是產(chǎn)品,也要考慮用戶體驗(yàn)、使用門檻,所以需要找到設(shè)計(jì)哲學(xué)與學(xué)習(xí)成本之間的平衡點(diǎn);
- 類型化的圖表庫(kù)相比之下或許顯得啰嗦、不通用、描述能力稍差,但又更容易做到體積壓縮,符合用戶直覺地簡(jiǎn)易按需引入和針對(duì)性優(yōu)化;
- 大家都在聊 noCode, lowCode,當(dāng)前業(yè)界一些可視化平臺(tái)使用的圖表依賴還是傾向于 ECharts 等,會(huì)不會(huì)配置化的圖表在這樣的應(yīng)用場(chǎng)景下更受青睞,或者反之是具有系統(tǒng)描述性的語(yǔ)法更容易接入?可以留為我們的開放性問題。
介紹過圖形語(yǔ)法這個(gè)陣營(yíng)之后,就可以開始認(rèn)識(shí)該陣營(yíng)中的優(yōu)秀產(chǎn)品了。螞蟻金服可視化團(tuán)隊(duì)的 G2、F2 將依次登場(chǎng)。
要聊 F2,先說說 G2。G2 是 antvis 的第一代圖形語(yǔ)法封裝庫(kù),G2 就取自 The Grammar of Graphic 里面兩個(gè) G。2014 年啟動(dòng),17 年底開源的 G2,其引以為傲的圖形語(yǔ)法理論基礎(chǔ),也是受到上面提到的 The Grammar of Graphic 作者 Leland Wilkinson 的直接肯定的。
它在螞蟻內(nèi)部的前身也是常規(guī)的配置化圖表庫(kù),后來大家發(fā)現(xiàn)這樣的圖表庫(kù)和已有的巨頭 ECharts、HighCharts 定位重疊,同時(shí)自身的開發(fā)拓展也很麻煩,容易遭遇瓶頸,加之?dāng)?shù)據(jù)分析大戶 R 語(yǔ)言 ggplot2 流行,圖形語(yǔ)法的概念為人了解,antvis 希望調(diào)整方向,這時(shí)其初衷和目標(biāo),就變成了一個(gè)圖形語(yǔ)法封裝的可視化工具庫(kù)了。
G2 不是本文的重點(diǎn),我們簡(jiǎn)單看看其發(fā)展,14 年下旬啟動(dòng)圖形語(yǔ)法庫(kù) G2 的正式開發(fā),G2 的基礎(chǔ)架構(gòu)經(jīng)歷了如下的演進(jìn):
以上圖片整理自 G2 官方各階段架構(gòu)圖
經(jīng)過至今 (2020) 六年的發(fā)展,G2 本身也有很多調(diào)整,比如底層引擎從單一 Canvas 到 SVG、Webgl 的支持,交互、數(shù)據(jù)處理模塊的抽離、頂層圖表應(yīng)用庫(kù)的進(jìn)一步封裝,“螺旋上升”到后面又想要做整合,F(xiàn)2 可以從 G2 打包產(chǎn)出,直接從底層渲染引擎徹底兼容多端等,設(shè)想和設(shè)計(jì)都是非常有野心的,期待能有 ECharts zRender 那樣優(yōu)秀的更多設(shè)計(jì)出現(xiàn)。
F2 也是來自需求,服務(wù)于需求。15-16 年支付寶錢包業(yè)務(wù)發(fā)展,移動(dòng)端的資金展示需求冒出。移動(dòng)端錢包,在代碼大小上非常受限,所以 AntV 團(tuán)隊(duì)開始基于已有的 G2,自研面向移動(dòng)端的圖表庫(kù),一開始叫 G2-Mobile,跟前輩 G2 用的是同一套架構(gòu),只是上層實(shí)現(xiàn)有一些差異,但這也帶來了較高維護(hù)成本和一些麻煩。一直到 3.0 時(shí)代,G2-Mobile 才成熟起來,設(shè)計(jì)、架構(gòu)方面都進(jìn)行升級(jí),更名 F2,“復(fù)用”了 G2 的命名語(yǔ)義,取自 Fast 和 Flexible。至于為什么是 Fast 和 Flexible,后面的設(shè)計(jì)例子和 demo 使用中我們可以體會(huì)一下。
發(fā)展至今,我們?cè)陧?xiàng)目中用到的 3.7,其架構(gòu)大概是這樣的。如果說到更遠(yuǎn),就是跟上面 G2 一起談的整合了。
直接使用官方可在線編輯的 demo 體會(huì)使用效果:F2 官網(wǎng) - Examples 示例(https://antv-f2.gitee.io/zh/examples/basic)
在上面的地址中,大家可以動(dòng)手一起直觀體會(huì)一下 F2 的使用。
// 前面引包、創(chuàng)建 Chart 實(shí)例、載入數(shù)據(jù)都不消多說,直接看到最關(guān)鍵的圖形描述語(yǔ)法
// 一行代碼畫圖表
// 常規(guī)柱狀圖
chart.interval().position('genre*sold').color('genre');
// 改成二維坐標(biāo)散點(diǎn)
chart.point().position('genre*sold');
// 配置:給全部散點(diǎn)加上統(tǒng)一的顏色
chart.point().position('genre*sold').color('#face15');
// 上面提到的都是我們的 Visual Cue 概念,它是很容易改變的,再來個(gè)折線
chart.line().position('genre*sold').color('#ffee15');
// 只有 Visual Cue,Coordinate 如何變動(dòng)呢
chart.coord('polar');
// 如果覺得觀感不佳,還可以做更詳細(xì)的配置
chart.source(data, {
sold: {
min: 0,
max: 500,
}
});
復(fù)制代碼
對(duì)項(xiàng)目背景和歷史都有所了解,也已經(jīng)直觀體會(huì)到這個(gè)庫(kù)的使用過程,應(yīng)該就對(duì)其 philosophy 有大致的把握了。接下來會(huì)進(jìn)入具體的源碼學(xué)習(xí)過程。
架構(gòu)模塊簡(jiǎn)析、瀏覽代碼、看看圖表庫(kù)是如何畫圖的
架構(gòu)圖信息量可能有點(diǎn)不足,直接讀一讀各個(gè)模塊里面的代碼,看看分別負(fù)責(zé)了什么工作,模塊之間的數(shù)據(jù)流又是怎么樣的。為了方便理解,我們需要自下而上地看這張架構(gòu)圖:
圖源:F2 開發(fā)團(tuán)隊(duì)關(guān)于 F2 架構(gòu)演進(jìn)的文章
這一層是不同環(huán)境下的繪圖引擎,整個(gè)庫(kù)對(duì)對(duì)其的依賴主要是兩件事情:
-
通過繪圖命令進(jìn)行渲染,各個(gè)環(huán)境下的適配也首先在這一步完成;
-
事件系統(tǒng),在第四部分我們會(huì)仔細(xì)聊到一個(gè)設(shè)計(jì)。
Shape
這個(gè)模塊直接連接了單個(gè)數(shù)據(jù)點(diǎn)到 Canvas 上的繪制過程,可能稍顯復(fù)雜啰嗦。如果看源碼,會(huì)發(fā)現(xiàn) F2 的 Shape 有兩種:
- Graphic 基礎(chǔ)圖形中的 Shape —— 渲染引擎中的 Shape 實(shí)現(xiàn)。
這是 3.x 版本中 F2 對(duì)渲染引擎的完全改造,跟原有的 G 是匹配的。Graphic 層的 Shape 是底層的“圖形”概念,提供了與繪圖引擎 Canvas 的連接橋梁。整套 Shape 使用 OOP 中的常用來舉例子的那種典型繼承關(guān)系進(jìn)行設(shè)計(jì),大致層次為:
基類 Element 維護(hù)了繪圖元素這個(gè)概念,維護(hù)自身層級(jí)、可見性屬性,實(shí)現(xiàn)元素繪制中如矩陣變換、移動(dòng)的方法,通過重新計(jì)算其繪圖點(diǎn)坐標(biāo)實(shí)現(xiàn)(父類維護(hù)公共屬性、基本方法)。
擴(kuò)展 Element 實(shí)現(xiàn)的有兩個(gè)概念:
- Shape: 內(nèi)置圖形(包括圖片)的基類,做這一層包裝是對(duì)基礎(chǔ)圖形的統(tǒng)一,對(duì)外暴露統(tǒng)一的繪制和包圍盒獲取方法,但方法中調(diào)用的具體的路徑創(chuàng)建、包圍盒計(jì)算方法交給各個(gè)具體的 Shape 如 rect, circle, line 去重寫。具體 Shape:矩形、圓形、線等具體圖形,會(huì)按照自身形狀特征 override 繪制路徑、包圍盒計(jì)算接口。
- Group: 組合圖形類,是對(duì)基礎(chǔ)圖形的“打包”處理,包圍盒合并、統(tǒng)一創(chuàng)建銷毀。
Graphic 的 Shape 還允許用戶擴(kuò)展添加自定義圖形,豐富繪圖引擎能直接支持的繪制基礎(chǔ)圖形。用戶只需要定義圖形的繪制路徑和包圍盒計(jì)算即可,與具體內(nèi)置圖形的方式相同,graphic 會(huì)使其繼承自 Shape 創(chuàng)建,可以在繪圖引擎中和其他圖形一樣直接使用。
- Geometry 中的 Shape —— 數(shù)據(jù)的直接映射(圖中的數(shù)據(jù)條柱、數(shù)據(jù)散點(diǎn))。
這個(gè) Shape 使用工廠模式的設(shè)計(jì),各個(gè)具體圖形通過在這個(gè)工廠中注冊(cè),代理 Geometry 中的具體圖形(數(shù)據(jù)點(diǎn)特征 Line/Point/Interval 等)接口,然后實(shí)現(xiàn)對(duì)應(yīng) geom 的繪制方法,這個(gè)繪制方法就是調(diào)用 Graphic 中具體 Shape 的路徑繪制,添加到畫布上。Geom 部分內(nèi)容在下面的 Geometry 模塊還會(huì)具體涉及。
Animation
動(dòng)畫實(shí)現(xiàn),F(xiàn)2 提供了注冊(cè)式的動(dòng)畫入口,支持自定義動(dòng)畫處理。同時(shí)內(nèi)置了許多默認(rèn)動(dòng)畫,支持對(duì)圖表中的數(shù)據(jù)元素 Shape 出現(xiàn)、更新、隱藏的過程添加動(dòng)畫,且支持許多內(nèi)置的緩動(dòng)函數(shù)。是一個(gè)相對(duì)獨(dú)立的模塊,在第四部分中我們也會(huì)討論一個(gè)設(shè)計(jì)細(xì)節(jié)
Component
顧名思義,組件。這部分的內(nèi)容比較雜,實(shí)現(xiàn)的是一些繪制圖表過程中可以抽象出來固化的通用成分,稱為組件。
如坐標(biāo)軸、HTML(為用戶可以自行添加的 HTML 輔助元素提供一個(gè)配置入口,主要做的是幫忙創(chuàng)建節(jié)點(diǎn)、映射 CSS、畫布中布局)、文本、列表、Tooltip 懸窗展示的視圖組件。
聽起來應(yīng)該是 Shape 或者 Plugin 呀?
- Shape 應(yīng)該是數(shù)據(jù)在圖中的體現(xiàn)和代表,是能體現(xiàn)數(shù)據(jù)特征的,雖然 component 也有自己的渲染、包圍盒計(jì)算邏輯,但應(yīng)該跟 shape 有概念上的區(qū)分;
- Plugin 應(yīng)該要有自己獨(dú)立的狀態(tài)管理邏輯,注冊(cè)并寄生于 Chart 主體之上,而非一個(gè)純視圖組件,跟 component 有別;
- 這些 component 的使用方正是圖表組成如坐標(biāo)軸和 plugins 如 Tooltip、Legend。
度量,它表示的是數(shù)據(jù)在圖表上的展示方式,這部分內(nèi)容也已經(jīng)抽離出 F2 本身,作為額外的工具倉(cāng)庫(kù),但是作為支持繪圖的一個(gè)中間模塊,應(yīng)該了解一下。
度量提供的是可視化圖表中數(shù)據(jù)信息對(duì)圖像信息的映射標(biāo)準(zhǔn),可以把源數(shù)據(jù)某個(gè)維度的空間范圍比作定義域,轉(zhuǎn)換后的圖形屬性空間比作值域,其間的轉(zhuǎn)換橋梁就是 scale??梢灾С治覀儗?duì)分類、時(shí)間(離散或連續(xù))、連續(xù)數(shù)據(jù)、分段數(shù)據(jù)等進(jìn)行轉(zhuǎn)換和過濾。
通俗來說,就是最后圖表上面某個(gè)軸向應(yīng)該對(duì)數(shù)據(jù)做怎樣的增刪改,我們?cè)谘菔局休d入數(shù)據(jù),設(shè)定了 sold 字段值的最大最小值,就是在手動(dòng)更改它的 scale。抽離這個(gè)概念,有利于實(shí)現(xiàn)坐標(biāo)軸和數(shù)據(jù)圖的對(duì)應(yīng)聯(lián)動(dòng)。
Coordinate
坐標(biāo)軸類,拓展實(shí)現(xiàn)了 cartesian 笛卡爾直角坐標(biāo)系和 polar 極坐標(biāo)系。
兩種坐標(biāo)系各自實(shí)現(xiàn)不同的 convertPoint 和 invertPoint 函數(shù),根據(jù)坐標(biāo)系規(guī)則,處理歸一化后的點(diǎn)數(shù)據(jù)并返回結(jié)果坐標(biāo),滿足對(duì)應(yīng)的繪圖需求(歸一化也是一個(gè)重要的設(shè)計(jì),我們?cè)诹?Geometry 時(shí)將提到)。
是所有視覺通道的管理類,視覺通道,是可視化編碼的關(guān)鍵。點(diǎn)線面這樣的幾何形狀可以實(shí)現(xiàn)一種標(biāo)記,標(biāo)記幫助人眼進(jìn)行分類、聚合識(shí)別。
- 而如坐標(biāo)軸(一維、二維)位置、顏色、形狀、角度、長(zhǎng)度這樣的視覺通道是實(shí)際數(shù)值在視覺上的映射,實(shí)現(xiàn)定性/定量效果;分類 + 定量,才產(chǎn)生了可視化的效果。
attrs 類,主要是提供一個(gè)基類,實(shí)現(xiàn)數(shù)據(jù)到視覺通道的映射,對(duì)外暴露獲取繪圖數(shù)據(jù)的接口,如控制位置、透明度、顏色等,使用方是后面要提到的 Geometry,它控制著每個(gè)數(shù)據(jù)到我們所見的圖案的映射。
Adjust
顧名思義是一種“調(diào)整”工具,這是 antv 系數(shù)據(jù)處理能力的一種數(shù)據(jù)工具封裝,主體也已經(jīng)拆分出去了,主要用于將原始數(shù)據(jù)做可視化層次上的一些修整,使其繪制出來的圖表更具有可看性。
為滿足不同圖表的數(shù)據(jù)呈現(xiàn)需求,當(dāng)前支持四種方法:
- stack 層疊: 如層疊面積圖、層疊柱狀圖
- dodge 分組散開: 分組并在范圍內(nèi)均勻分布,如分組柱狀圖
- jitter 擾動(dòng)散開: 可以使原本的分類數(shù)據(jù)中同一類的多個(gè)數(shù)據(jù)實(shí)體打散,使其保留分組效果但不重疊,如分組散點(diǎn)
- symmetric 對(duì)稱: 使數(shù)據(jù)對(duì)稱呈現(xiàn),如漏斗圖,河流圖
先看看這個(gè)大類,它是繪制各種圖形的基礎(chǔ)。圖形語(yǔ)法兩個(gè)正交概念中,數(shù)據(jù)的直接映射——圖形就是由 Geometry 控制。
Chart 初始化的時(shí)候,會(huì)將各種 geoms 初始化,并把其中每個(gè)基礎(chǔ) geom 的繪圖命令掛載到 Chart 上,成為 Chart 的可調(diào)用方法,如剛才的示例中我們用 line 方法創(chuàng)建折線圖、用 point 方法創(chuàng)建散點(diǎn)圖,都是直接調(diào)用了 Geometry 中通過 Line、Point 提供給 Chart 的方法。
Chart 初始化 geoms 的時(shí)候也維護(hù)了當(dāng)前 geoms 列表,后續(xù) Chart 對(duì)數(shù)據(jù)在圖表中的映射元素的維護(hù),都通過這個(gè) geom 實(shí)例列表進(jìn)行。Chart 會(huì)告知 geom 數(shù)據(jù)信息、用戶配置、坐標(biāo)軸信息等繪制的必要數(shù)據(jù),而且會(huì)將 geom 的 container 限制為其繪制層次中的 middlePlot,中間層中。后面會(huì)介紹到這個(gè)分層模型。
完成了數(shù)據(jù)和配置的下發(fā)、方法到 Chart 上的掛載,geom 在數(shù)據(jù)初次載入或者變化時(shí)會(huì)有一些處理工序:
- attrs 處理,一些繪圖屬性的設(shè)置和維護(hù)
- 數(shù)據(jù)處理,包括:
- 從基礎(chǔ)數(shù)字字面量到 [0, 1] 范圍的歸一化,從而實(shí)現(xiàn)圖表整體的數(shù)據(jù)統(tǒng)一,方便繪制;歸一化是一個(gè)比較巧妙的設(shè)計(jì),獲取到數(shù)據(jù)并通過 scale 確定繪制范圍后,會(huì)得到數(shù)據(jù)維度上的值域,將每個(gè)數(shù)據(jù)進(jìn)行歸一化操作,得到一種類似“比例”的數(shù)據(jù),再根據(jù)畫布區(qū)域大小映射成具體的像素位置,這個(gè)簡(jiǎn)要的過程實(shí)現(xiàn)了數(shù)據(jù)的準(zhǔn)備,將數(shù)據(jù)范圍限定為一個(gè) 0-1 間的數(shù),為各個(gè)模塊消費(fèi)數(shù)據(jù)、坐標(biāo)軸轉(zhuǎn)化、繪圖轉(zhuǎn)化提供了方便
- adjust 數(shù)據(jù)調(diào)整,以滿足不同的圖表數(shù)據(jù)呈現(xiàn)需求;
- 數(shù)據(jù)排序。
Interaction
移動(dòng)端主要的交互是 touch 系列,這個(gè)模塊對(duì)事件系統(tǒng)做了進(jìn)一步包裝(事件系統(tǒng)的底層模擬在后面的第四部分會(huì)詳細(xì)介紹),對(duì)比如 touch 事件組合形成的 pan 平移、pinch 縮放、swipe 輕掃等事件的具體處理做了包裝,方便按需引入。
Chart
圖表主體基礎(chǔ),數(shù)據(jù)載入、語(yǔ)法運(yùn)用的入口。Chart 類從一個(gè) F2 自己實(shí)現(xiàn)的事件系統(tǒng)擴(kuò)展而來,通過簡(jiǎn)單的 eventListener 維護(hù)還有 on/off 和 emit 對(duì)應(yīng)掛載卸載、觸發(fā)事件,實(shí)現(xiàn)整個(gè)圖表的通信和各個(gè)模塊的聯(lián)動(dòng)。
對(duì)于除了圖表主體之外的各種輔助插件,如 Tooltip 懸窗、Legend 圖例等采用注冊(cè)機(jī)制。
- register 的時(shí)候把 chart 對(duì)象傳入,插件可以自己獨(dú)立配置、維護(hù)狀態(tài)、完成渲染,像 Tooltip 的交互也通過事件系統(tǒng)來實(shí)現(xiàn),各個(gè)插件自行注冊(cè)處理函數(shù)到 Chart 上一起處理
- Chart 只需要維護(hù) plugins 列表,Chart 可以通過這個(gè)列表在事件鉤子觸發(fā)時(shí)對(duì)各個(gè) plugin 進(jìn)行 notify,插件自己對(duì)全局的這些事件進(jìn)行自定義的響應(yīng),更新狀態(tài)
當(dāng)我們創(chuàng)建一個(gè) Chart 實(shí)例、載入數(shù)據(jù)、創(chuàng)建圖形語(yǔ)法命令、最后觸發(fā) render 的時(shí)候,發(fā)生了什么?
3.2.1 new Chart()- initCanvas 根據(jù)傳入信息,包括 canvas 實(shí)體 / 上下文 / DOM 元素,還有寬高、pixelRatio、字體族,創(chuàng)建 Canvas 對(duì)象,這個(gè)對(duì)象封裝了實(shí)際的 Canvas 對(duì)象,實(shí)現(xiàn)事件通信,暴露繪制、resize、銷毀接口,后續(xù)的底層繪制都在此基礎(chǔ)上進(jìn)行
- initLayout 布局初始化,Canvas 作為圖表整體基類,需要管理圖表的數(shù)據(jù)映射、坐標(biāo)軸、輔助信息、標(biāo)題、圖例等的布局,這一步結(jié)合用戶自定義的和默認(rèn)的配置進(jìn)行布局
- initLayers 創(chuàng)建 Chart 的分層模型,通過在 Canvas 上添加繪制組,實(shí)現(xiàn)圖表畫布三層的分層結(jié)構(gòu)
- 坐標(biāo)軸主要在底層,數(shù)據(jù)映射部分(折線、散點(diǎn)、柱)在中間層,輔助信息如輔助線、浮窗,主要在頂層
- 這只是概念上的分層,實(shí)際上 F2 依賴的還是單一的 Canvas,沒有分層次,這樣只是給概念上的操作帶來了方便。在移動(dòng)端小圖的場(chǎng)景下,同時(shí)維護(hù)三層 Canvas,還要保持對(duì)齊、處理重疊,成本比處理交叉重繪更高
- initEvent 全局 Chart 需要管控的事件注冊(cè),主要是數(shù)據(jù)更新和圖表整體 resize 時(shí)的觸發(fā)和必要的重新計(jì)算,其他事件會(huì)由其他組件管控
- initScaleController 為數(shù)據(jù)各個(gè)維度的值域映射專門創(chuàng)建了 controller,初始化時(shí)應(yīng)該掛載上,并維護(hù) Chart 的 scale 配置
- initAxisController 坐標(biāo)軸這個(gè)實(shí)體的 controller 掛載,為的是將初始化完畢的三層結(jié)構(gòu)中頂部和底部告知坐標(biāo)軸管理器,以便后續(xù)繪制
- Chart 本身 data 狀態(tài) set
- 如果同時(shí)傳入了 scale 配置,會(huì)調(diào)用 scaleController 對(duì)各字段 scale 配置進(jìn)行增量的修改,并更新所有受影響的配置,更新方式是全量的,拿到每個(gè) field 數(shù)據(jù),重新進(jìn)行映射
- 數(shù)據(jù)歸一化處理會(huì)在具體的 Geometry 中,上面我們已經(jīng)提及了
完整的 Geometry API 示例應(yīng)該是這樣的:
// Type 可以是 point 點(diǎn),line 線,interval 柱等等,其實(shí)概念應(yīng)該是更抽象的 Visual Cue
chart.()
// 字段解析,通俗易懂:x*y / [x, y] 表示以數(shù)據(jù)的 x 維度和 y 維度分別為橫縱軸
.position()
// 繪圖屬性:大小、顏色、形狀
.size()
.color()
.shape()
// 其他工具:數(shù)據(jù)調(diào)整、Canvas 繪圖屬性、動(dòng)畫
.adjust()
.style()
.animate();
復(fù)制代碼
- Geometry 接管創(chuàng)建數(shù)據(jù)映射圖形的命令,以上面的語(yǔ)句為例:
- 初始化 Interval 柱狀圖實(shí)例,解析帶進(jìn)來的參數(shù),與默認(rèn)配置 mix 生成繪制配置
- 解析字段 field,確定以數(shù)據(jù)源中什么字段名作為數(shù)據(jù)的各個(gè)維度,同時(shí)還會(huì)更新 scale,它直接影響拿到數(shù)據(jù)后的繪制范圍、粒度
- 綁定更多配置化的繪圖屬性,如圖案大小,顏色,填充形狀
- 進(jìn)行數(shù)據(jù)調(diào)整、繪圖屬性、動(dòng)畫添加
- 這些 API 操作的過程,其實(shí)都是維護(hù)該 geom 內(nèi)部 config 對(duì)象的過程,它本質(zhì)就是一份配置數(shù)據(jù),更新完畢確定之后,在 render 的時(shí)候才根據(jù)這些配置項(xiàng)目進(jìn)行繪制
- 初次渲染的初始化操作:觸發(fā)數(shù)據(jù)過濾器,過濾器是一個(gè)列表,由交互定義添加,比如 Legend 插件,在點(diǎn)擊條目的時(shí)候會(huì)觸發(fā)數(shù)據(jù)維度的顯示或隱藏,這時(shí)候需要維護(hù)過濾邏輯;初始化坐標(biāo)軸并掛載到 Chart 上;額外操作如多坐標(biāo)軸對(duì)齊、數(shù)據(jù) adjust 等
- 渲染坐標(biāo)軸,Chart 會(huì)從 Geometry 這個(gè)管理數(shù)據(jù)映射的部件中拿到當(dāng)前數(shù)據(jù)的 scale 信息,比如笛卡爾坐標(biāo)系中會(huì)拿 X Y 兩軸的基本信息,這保證了坐標(biāo)系和數(shù)據(jù)的聯(lián)動(dòng)性。過后 axisController 自己根據(jù)數(shù)據(jù)創(chuàng)建坐標(biāo)軸,使用前面提到的 axis 組件,按需渲染,將需要渲染的圖形添加到 Canvas 上
- 開始處理中間層,為中間層創(chuàng)建 canvas clip,將后續(xù) Geometry 的繪制限制在這個(gè)子畫布內(nèi),過后逐個(gè)觸發(fā) geoms 的 paint,這里的 paint 其實(shí)也是假繪制,并沒有真實(shí)開始渲染,而是將每個(gè)圖形的路徑和繪圖屬性都配置好,添加到 canvas 上
- 開始處理頂層,首先要對(duì)頂層內(nèi)容進(jìn)行一遍排序。這里的排序 F2 通過對(duì)每個(gè)圖元自身的 zIndex 進(jìn)行維護(hù),同時(shí)在繪制之前進(jìn)行排序,zIndex 升序,后面繪制的會(huì)自動(dòng)覆蓋前面的
- 如此就完成了三層內(nèi)容的層疊添加,并且保證了順序,最后會(huì)調(diào)用 canvas.draw 開始繪制,呈現(xiàn)完整圖表
在上面的流程中可以看到,F(xiàn)2 在圖表渲染過程中統(tǒng)一開了一些口子(比如一些事件全局 emit、主動(dòng)檢查的部分共享配置數(shù)據(jù)等),方便插件或者交互、動(dòng)畫能力引入干預(yù)繪制過程和繪制效果。所以圖表更新和交互渲染也是不難理解的:
- 圖表數(shù)據(jù)更新流程大致相同,因?yàn)橹丿B,基本都要觸發(fā)挺多重繪的,但是 AntV 有一些重繪時(shí)的優(yōu)化,如局部渲染,可以減少損失
- 交互產(chǎn)生的更新,通過插件實(shí)現(xiàn):
例1:Tooltip 插件通過配置項(xiàng)告知 Chart 是否需要在頂層添加 Tooltip 元素,并且通過交互判斷控制自身的生命周期。
比如在監(jiān)測(cè)到手指點(diǎn)按的時(shí)候,拿到事件,計(jì)算點(diǎn)擊區(qū)域獲取應(yīng)該展示的詳細(xì)信息,使用 Component 提供的基本繪圖組件載入數(shù)據(jù)進(jìn)行獲取繪制信息,將繪制信息載入 canvas 圖層配置中,同時(shí)觸發(fā) canvas.draw 重繪,重新計(jì)算頂層需要渲染的元素,此時(shí)新增的項(xiàng)目也被加入,就完成了繪制。
例2:Legend 點(diǎn)擊篩選,在 3.2.4 中提到了。chart render 時(shí)會(huì)走一層數(shù)據(jù)過濾器,這層過濾器是 Legend 插件可以干預(yù)的,Legend 在發(fā)生交互(如用戶點(diǎn)擊下圖的 men 條目使其置灰,當(dāng)前的篩選狀態(tài)就被更新到數(shù)據(jù)過濾器中,chart 重新渲染時(shí)數(shù)據(jù)即被剔除。
了解過架構(gòu)上和整個(gè)繪圖流程的宏觀實(shí)現(xiàn),接下來從兩個(gè)重要的細(xì)節(jié)設(shè)計(jì)來深入探討 F2 是如何解決一些關(guān)鍵問題的。
細(xì)看兩個(gè)關(guān)鍵的設(shè)計(jì)細(xì)節(jié):交互 & 動(dòng)畫
圖源:F2 倉(cāng)庫(kù)中 README 示例
Overview first, Zoom and Filter, then Details-on-Demand.
F2:概覽第一,聚焦過濾,再按需查看詳情。這是數(shù)據(jù)的展示鏈路,而交互就是其中的重要串聯(lián)線索。
F2 默認(rèn)依賴的渲染引擎是 Canvas (畢竟只是二維圖表,更復(fù)雜的 WebGL 不談,Canvas 和 SVG 的抉擇可以看到 AntV 關(guān)于引擎選擇的討論,鏈接同樣可見文末),Canvas API 標(biāo)準(zhǔn)提供的是一種 immediate-mode API,與之相對(duì)的是 retained-mode API,簡(jiǎn)單理解這兩種模式就是,前者就像白紙,API 調(diào)用就像一筆一筆往上面畫圖案,畫上去了就拆不開了;后者就像樂高,一張圖可能有其本身的層次結(jié)構(gòu),內(nèi)部的結(jié)構(gòu)狀態(tài)是被保留下來的,可以類比我們更熟悉的 DOM 結(jié)構(gòu)。
在 Canvas 這種“即時(shí)”繪圖的環(huán)境下,并不能知道當(dāng)前畫布上面用戶動(dòng)作意欲交互的節(jié)點(diǎn)/元素,其實(shí)根本沒有所謂節(jié)點(diǎn)和元素的概念,整張畫布就是一張 bitmap,所以元素的概念需要圖表庫(kù)來維護(hù)。
移動(dòng)端最基礎(chǔ)的交互事件是“點(diǎn)擊”“觸摸”,如何擴(kuò)展出一套完整的,支持包括圖表交互需要的:縮放、平移、輕掃、手指停留預(yù)覽、選中等交互,也是需要解決的問題。
所以要實(shí)現(xiàn)交互,需要解決兩個(gè)問題:
- 如何接受原生的交互事件,包裝成圖表庫(kù)可以消費(fèi)的更完整的事件系統(tǒng)——EventController;
- 如何將交互事件映射到實(shí)際并不存在的元素概念;
我們先看比較直觀易懂的第二個(gè)問題可以怎么解決。
具體事件觸發(fā)先不管,先看看已知某事件觸發(fā),知道熱點(diǎn)坐標(biāo),如何處理
計(jì)算
- 畫幾個(gè)不重疊的千姿百態(tài)的圓
- 維護(hù)一個(gè)數(shù)組保存圓的信息
- 點(diǎn)擊時(shí)從整個(gè) Canvas 元素的點(diǎn)擊事件拿到點(diǎn)擊熱點(diǎn)在元素內(nèi)的坐標(biāo) clientX/Y
- 逐個(gè)判斷圓心到熱點(diǎn)的距離和半徑的關(guān)系可知是否點(diǎn)擊了某個(gè)圓
- 知道是某個(gè)圓了,交互就好說啦
- 重疊怎么辦?底層優(yōu)先?頂層優(yōu)先?
- 只需要控制遍歷圖形數(shù)組時(shí)的順序、是否“懶遍歷”(找到一個(gè)就返回)
Trick
- 一個(gè) Canvas 畫布其實(shí)是一張 bitmap,對(duì)于一個(gè)點(diǎn)擊位置,該像素的色值是最直接的信息,需要的計(jì)算量很少
- 獲取顏色,然后可以認(rèn)為用戶點(diǎn)擊了該顏色的圖形
- 顏色碰撞?可以用一層看不見的離屏 Canvas 同步繪制同位圖做交互層,每個(gè)元素生成一個(gè)隨機(jī)不重復(fù)的顏色,透明度可以在離屏 Canvas 屏蔽
- 重疊問題?只能實(shí)現(xiàn)頂層優(yōu)先的交互事件,或者再維護(hù)重疊顏色;透明度?難頂;多層 Canvas?難頂;
- 但是可以支持更復(fù)雜的不規(guī)則圖形
各有優(yōu)劣——結(jié)合使用
-
簡(jiǎn)單圖形:算
-
復(fù)雜圖形:能簡(jiǎn)化?簡(jiǎn)化,算;不能簡(jiǎn)化?取色
- 取色的代價(jià)只在繪制時(shí)要渲染兩層,后續(xù)交互速度很快
F2 中使用的主要還是第一種方式,因?yàn)?F2 的移動(dòng)端交互情況都不太復(fù)雜,交互精度要求低,圖表 Canvas 實(shí)體也不會(huì)太大,簡(jiǎn)單的幾何圖形包圍盒計(jì)算就可以滿足需求了。
原本是依賴于 hammer.js,這個(gè)“錘子”就是專門處理移動(dòng)端手勢(shì)交互的,特別是多指觸控等的交互支持。但是因?yàn)樗€依賴瀏覽器環(huán)境,這對(duì)希望全平臺(tái)適用的 F2 來說是一個(gè)缺陷,所以 F2 也在慢慢脫離這個(gè)庫(kù)。自己實(shí)現(xiàn)了一套多指觸控。目前應(yīng)該還是逐步遷移的階段。但是其實(shí)現(xiàn)思路比較簡(jiǎn)單清晰。
理論上,只要可以解決判斷當(dāng)前鼠標(biāo)/手勢(shì)交互發(fā)生在哪個(gè)“元素”上,其他事件都可以模擬了。同時(shí),面向移動(dòng)端的 F2 其實(shí)比常規(guī)的 PC 端圖表要處理的交互判斷更簡(jiǎn)單:
- PC 端有虛擬指針,所以有 hover、move、click、drag 等事件需要處理;
- 移動(dòng)端只需要處理 click 和 touch;
- touch(start, *move, end, *cancel x) --> mouse (1move, down, up) --> click:
- 先觸發(fā) touch 事件,其中有起始 start,可能有 move,終止的 end 或者異常終止的 cancel,cancel 將 block 后續(xù)的事件觸發(fā);
- 后觸發(fā) mouse 事件,分別是唯一的 move 和后續(xù)緊跟的 down 和 up;
- 最后觸發(fā) click 事件。
F2 自己實(shí)現(xiàn)一個(gè) Controller,通過 touch 系列事件監(jiān)聽,模擬出完整的手勢(shì)操作。沒有太多高深的技術(shù),設(shè)計(jì)邏輯也比較清晰,但代碼比較難提煉,而且現(xiàn)在還是逐步脫離 hammer.js 的兼容階段,可能有點(diǎn)難理清楚,所以這里畫了一個(gè)交互流程的時(shí)序圖,一起看看怎么模擬手勢(shì)交互:
一些細(xì)節(jié)、要點(diǎn)筆記:
- 這個(gè) Controller 幫 Chart 層屏蔽了 Canvas 本身的事件處理,包括交互序列、速度、點(diǎn)數(shù)不同導(dǎo)致的不同操作判定,都封裝成單個(gè)事件,這樣處理響應(yīng)會(huì)非常方便
- 所謂代理,其實(shí)是對(duì)每個(gè)事件添加自定義的 EventListener,代理后原事件會(huì)再被拋出,這里就省略了
- 多點(diǎn)觸控事件中,會(huì)有 touches 屬性保存所有點(diǎn)信息;單點(diǎn)事件則只有一個(gè)單一的 point
- 拓展事件,通過庫(kù)自身實(shí)現(xiàn)的簡(jiǎn)易事件管理拋出的。在這一層只需要處理好整個(gè) controller 邏輯和事件流程,剩下的交給上層消費(fèi)這些事件:press、pan、pinch、swipe。觸發(fā)這些事件的時(shí)候 Controller 會(huì)按序觸發(fā) start 和 end,并在 processEvent 標(biāo)記該事件的進(jìn)行。
我們已經(jīng)解決了落點(diǎn)計(jì)算、事件包裝,那當(dāng)交互事件發(fā)生,我們的事件消費(fèi)就可以進(jìn)行了:針對(duì)需要響應(yīng)的事件進(jìn)行監(jiān)聽,過后經(jīng)過判斷進(jìn)行額外渲染即可。
4.2 「動(dòng)畫」怎么做
通俗易懂:動(dòng)畫在我們這樣的數(shù)據(jù)展示類日常圖表中,可能對(duì)于數(shù)據(jù)的實(shí)際展示、分析輔助作用并不是很大,但它是提升用戶體驗(yàn),提高圖表本身的高級(jí)感的重要工具。
F2 中動(dòng)畫的主體是表示數(shù)據(jù)映射圖形的 Shape,我們前面說過 Shape,維護(hù)的其實(shí)是自身的位置、關(guān)鍵點(diǎn)、繪圖屬性等數(shù)據(jù),動(dòng)畫,就是通過逐幀改變 Shape 的屬性來實(shí)現(xiàn)的。
很多關(guān)于自定義動(dòng)畫、默認(rèn)動(dòng)畫的內(nèi)容,邏輯比較復(fù)雜就不詳細(xì)講了。我們討論一個(gè)關(guān)鍵問題:在前面提到的渲染模式中,動(dòng)畫這個(gè)過程是如何勾入整個(gè) Chart 的 render 中的?
要解決這個(gè)問題,有兩個(gè)關(guān)鍵的背景知識(shí)需要了解:
動(dòng)畫的實(shí)現(xiàn)中,動(dòng)畫速度常常是控制動(dòng)畫效果的一個(gè)重要維度,需要有多種方法支持動(dòng)畫進(jìn)行的速度跟時(shí)間的關(guān)系映射,這樣的方法就是緩動(dòng)函數(shù)。為了支持多樣性的動(dòng)畫,F(xiàn)2 自身內(nèi)置了一系列緩動(dòng)函數(shù)。當(dāng)前大致包括:
- 一次線性緩動(dòng)(可以理解為勻速);
- 二次、三次緩動(dòng)(包含淡入、淡出、淡入淡出);
- 彈性緩動(dòng)(包括回彈、定位抖動(dòng)、反彈);
關(guān)于緩動(dòng)效果的展示,可以在文末參考鏈接看到緩動(dòng)曲線還有動(dòng)畫效果,鼠標(biāo) hover 在對(duì)應(yīng)的函數(shù)圖之上,會(huì)有一個(gè)光標(biāo),它的移動(dòng)速度可以表示該緩動(dòng)函數(shù)實(shí)現(xiàn)的速度控制效果。
插值器也是動(dòng)畫領(lǐng)域一個(gè)比較重要的概念了,不詳細(xì)展開,而且其在我們簡(jiǎn)單的二維動(dòng)畫中并不復(fù)雜,大致了解一下其背景和用途即可。插值器常常是用于創(chuàng)建所謂“補(bǔ)間動(dòng)畫”的工具函數(shù)。在 F2 中,主要支持了兩種類型的插值:
- 純數(shù)值的插值,用于創(chuàng)建如透明度、大小、一維位置之間變化的補(bǔ)間;
- 矩陣插值,用于創(chuàng)建如散點(diǎn)位置變化、矩陣變化的補(bǔ)間。
插值器的形式大概是接受起始和終止兩個(gè)狀態(tài),生成一個(gè)可以接收一個(gè)表示整個(gè)動(dòng)畫進(jìn)度的參數(shù) t,返回該進(jìn)度下數(shù)據(jù)中間值的函數(shù),用最簡(jiǎn)單的數(shù)值插值器來舉例就好了(下面代碼片段來自 F2 源代碼):
function interpolateNumber(a, b) {
a = +a;
b -= a;
return function(t) {
return a + b * t;
};
}
復(fù)制代碼
返回的函數(shù)接收一個(gè) t,表示當(dāng)前進(jìn)度,這個(gè)函數(shù)可以用于計(jì)算 a ---> b 的數(shù)值變化中,經(jīng)過時(shí)間 t 之后的數(shù)值情況。這里的 t 是用當(dāng)前動(dòng)畫已經(jīng)執(zhí)行的時(shí)間 t' / duration 算出來的比值。如果用 4.2.1 中提到的緩動(dòng)函數(shù)再對(duì) t 包裝一層,就實(shí)現(xiàn)了數(shù)值變化的緩動(dòng)。
幀的概念,由 requestAnimationFrame 來觸發(fā)
- 這里 F2 做了一個(gè)適配,瀏覽器環(huán)境下,存在 requestAnimationFrame 方法,則直接使用,不存在的話會(huì) fallback 使用 setTimeout(fn, 16) 代替,近似達(dá)到每秒 60 幀效果。
當(dāng)我們初始化一個(gè)動(dòng)畫時(shí),是創(chuàng)建了一個(gè) Animator 類,需要三個(gè)參數(shù),可以幫我們大致理解動(dòng)畫的執(zhí)行框架
- 動(dòng)畫的對(duì)象 Shape(是各個(gè)代表數(shù)據(jù)的圖形在進(jìn)行著動(dòng)畫)
- Shape 自身屬性 Attrs 作為動(dòng)畫的內(nèi)容(動(dòng)畫內(nèi)容是圖形的大小、形狀、顏色、位置等隨時(shí)間的逐幀改變)
- 需要?jiǎng)赢嬓Ч闹黧w自己保留的 timeline 對(duì)象,這個(gè)對(duì)象可以實(shí)現(xiàn)動(dòng)畫生命周期控制
Timeline 對(duì)象是控制動(dòng)畫執(zhí)行的最小單元,受到全局默認(rèn)注冊(cè)的動(dòng)畫管理器的控制。管理器會(huì)默認(rèn)將全局的 Shapes 動(dòng)畫添加到 Timeline 的動(dòng)畫隊(duì)列中,當(dāng)監(jiān)聽 Canvas 將開始刷新,就會(huì)開始“播放動(dòng)畫”,動(dòng)畫播放完畢,達(dá)到改變化階段的最終狀態(tài),canvas 更新也就結(jié)束了,就會(huì)“停止播放”
-
動(dòng)畫事件,是由 Animator 對(duì)象的 to 方法發(fā)出的,常見的形式是將某個(gè) Shape 的當(dāng)前狀態(tài) animate to 某個(gè)新的狀態(tài),創(chuàng)建動(dòng)畫的時(shí)候可以從此獲知起始和終止?fàn)顟B(tài),明確這個(gè)動(dòng)畫的過程是要改變哪個(gè) Shape 的什么屬性,耗時(shí)多久,這就創(chuàng)建了一個(gè)動(dòng)畫事件
-
Timeline 控制器,就是對(duì)動(dòng)畫事件的消費(fèi)。首先它會(huì)維護(hù)時(shí)間軸,動(dòng)畫播放過程中通過 requestAnimationFrame 不斷調(diào)用自身的 update 函數(shù),update 函數(shù)更新當(dāng)前時(shí)間,檢查動(dòng)畫隊(duì)列,逐個(gè)取出事件,判斷動(dòng)畫執(zhí)行進(jìn)度,如果還在進(jìn)度中,則使用上面提到的緩動(dòng)函數(shù)和插值器計(jì)算動(dòng)畫進(jìn)度,將動(dòng)畫對(duì)象的屬性更新為獲取到的插值,這時(shí)候 Canvas 中的圖形信息就更新了,整個(gè)動(dòng)畫事件隊(duì)列遍歷完之后,調(diào)用 Canvas 的 draw 方法進(jìn)行更新,就實(shí)現(xiàn)了動(dòng)畫效果
- 這個(gè)過程中 Timeline 也承擔(dān)了部分動(dòng)畫隊(duì)列的維護(hù)工作,如果消費(fèi)過程中,發(fā)現(xiàn)有些 Shape 已經(jīng)被銷毀了,則其動(dòng)畫也不應(yīng)該再維護(hù),Timeline 會(huì)將其從列表中移除
- 如果 Timeline 檢查進(jìn)度發(fā)現(xiàn)某動(dòng)畫已經(jīng)執(zhí)行完畢,則還是保留動(dòng)畫事件,下次更新可能還要執(zhí)行,但本次 update 可以跳過了
所以,其實(shí)我一開始的想法不準(zhǔn)確,不是動(dòng)畫過程“勾入” render 過程,而是動(dòng)畫作為 canvas 繪制過程中的“點(diǎn)綴”,在圖形變動(dòng)時(shí)作為一個(gè)中間過程,由 Timeline 進(jìn)行控制和渲染。
為什么?怎么做?做得如何?
適配包 tt-f2 (命名取自 F2 官方的微信適配包 wx-f2,針對(duì)頭條/字節(jié)跳動(dòng)小程序的適配) Github 地址可見文末。
從這一部分開始,我們回到了實(shí)際的落地場(chǎng)景,來看看要在字節(jié)小程序上使用 F2 遇到了什么問題,可以如何解決。
5.1 癥結(jié)當(dāng)我們?cè)谧止?jié)小程序上直接引入并開始使用 F2 這個(gè)圖表語(yǔ)法庫(kù)來畫圖表,我們會(huì)遇到以下問題:
官方 Demo
實(shí)際引入情況
控制臺(tái)錯(cuò)誤
既然圖形、大致的圖表框架,甚至圖例、數(shù)據(jù)映射都已經(jīng)畫出來了,說明圖表庫(kù)本身的運(yùn)行是基本 OK 的,并且按照我們前面的源碼學(xué)習(xí),F(xiàn)2 基本是純 JS 環(huán)境下運(yùn)作的,相關(guān)依賴也在逐漸減少,所以繪制表現(xiàn)異常,顏色丟失,很有可能是底層繪圖 API 的問題。實(shí)際上官方在對(duì)微信小程序做的適配中也提到,理論上只要提供了同樣的 Canvas 運(yùn)行環(huán)境,就可以正常完成圖表繪制。
帶著這個(gè)問題,我們先看看 HTML5 標(biāo)準(zhǔn)和字節(jié)跳動(dòng)小程序給出的 CanvasContext 有沒有什么 GAP:
(注:以下數(shù)據(jù)對(duì)比是截止 2020.10 的數(shù)據(jù)。HTML5 Canvas 標(biāo)準(zhǔn)來自 MDN,字節(jié)小程序接口來自小程序官方文檔。同時(shí),HTML5 標(biāo)準(zhǔn)中實(shí)驗(yàn)性、已廢棄、未標(biāo)準(zhǔn)化的屬性或方法已被忽略)
做完如上整理之后,我們基本可以斷定 demo 圖表樣式出現(xiàn)問題,是繪制的時(shí)候可寫屬性全部失效導(dǎo)致。至于小程序環(huán)境下方法上的一些 GAP,調(diào)研之后發(fā)現(xiàn)影響不大,主要是環(huán)形漸變、紋理繪制、文字描邊這三塊功能有無法避免的缺失,其他沒有對(duì)齊的 API 在 F2 源代碼中都沒有用到。
至于這樣的沒有與 HTML5 標(biāo)準(zhǔn)對(duì)齊的 API 設(shè)計(jì)的初衷,尤其是繪圖屬性不支持可寫的問題,通過了解和咨詢,得知這樣的設(shè)計(jì)出于以下的一些考慮:
- Canvas 組件從基礎(chǔ)庫(kù) 1.0.0 開始支持,為了降低小程序開發(fā)者的遷移成本,基礎(chǔ)能力的設(shè)計(jì)基本和微信小程序?qū)R,微信小程序的 Canvas Context 繪圖屬性可寫特性也是 18 年 2 月左右,基礎(chǔ)庫(kù) v1.9.90 之后才支持的。
- 這樣的設(shè)計(jì)可能也和小程序與 Web 的差異有關(guān)。我們知道小程序視圖組件和 JS 邏輯分別運(yùn)行在渲染層 (webview) 和邏輯層 (jscore) 中,Canvas 繪圖指令的實(shí)現(xiàn)涉及從 webview 到 jscore 的通信,提高運(yùn)行性能,做了屬性設(shè)置和繪制的分離,set[[propName]] 只是將操作放入 action 列表,真正的繪制要調(diào)用 draw API,統(tǒng)一執(zhí)行,以此減少頻繁通信的情況。
另外,還有一個(gè)小問題。剛才控制臺(tái)輸出了一片紅色報(bào)錯(cuò),都是形如 不支持顏色:#fff 的報(bào)錯(cuò)信息。這是因?yàn)樽止?jié)小程序不支持 RGB 顏色的 HEX 簡(jiǎn)寫。這個(gè)問題根源比較簡(jiǎn)單。
5.3 偏方
5.3.1 先“挑軟柿子捏”
RGB 的 HEX 簡(jiǎn)寫(形如 #ffff00 ---> #ff0),在庫(kù)源碼各處都有,在 ECharts 之類的其他庫(kù)也是如此。沒辦法通過什么配置項(xiàng)目來覆蓋掉。但我們可以進(jìn)行代碼替換:
- 這樣的 HEX 簡(jiǎn)寫以字符串的形式出現(xiàn)在庫(kù)源碼中,并且都是以 # 開頭,緊跟三個(gè) HEX 字符
- 正則表達(dá)式匹配:/#([0-9a-fA-F])([0-9a-fA-F])([0-9a-fA-F])(['"])/g
- 將其替換為:'#$1$1$2$2$3$3$4' 的形式
- 其實(shí)就是很粗暴地把中間的三組單個(gè)縮寫拆出來還原為兩位,并保留原來的井號(hào)和結(jié)尾引號(hào)
- replace 插件支持對(duì)指定文件夾內(nèi)容進(jìn)行掃描替換,使用上方正則匹配替換即可
- npm 在 script 中提供了 postinstall 關(guān)鍵字,可以在這個(gè)鉤子中加入命令,運(yùn)行替換腳本
// package.json
"scripts": {
"postinstall": "node ./util/hexFormatter.js"
}
// hexFormatter.js
const replace = require('replace');
replace({
regex: /#([0-9a-fA-F])([0-9a-fA-F])([0-9a-fA-F])(['"])/g,
replacement: '#$1$1$2$2$3$3$4',
paths: ['../@antv/f2'],
recursive: true,
silent: true,
});
復(fù)制代碼
這個(gè)做法有點(diǎn)小問題還沒有深究:npm 中正常運(yùn)作,如果使用 yarn 作為包管理器,在更新包或者安裝其他包之后,原有的替換操作會(huì)被還原回去,大致推測(cè)是 yarn 會(huì)判斷 node_modules/* 是否被篡改,并且還原時(shí)不會(huì)觸發(fā) postinstall 腳本。
5.3.2 抹平 Canvas 差異經(jīng)過上面的 API 調(diào)研和 diff,清楚了 Canvas 上下文環(huán)境的差異所在。方法上的 GAP 目前沒有比較好的方案去填平,但是因?yàn)樵S多接口 F2 并沒有使用,而少數(shù)缺陷產(chǎn)生的影響,在常見場(chǎng)景中基本可以忽略,所以小程序 Canvas 的方法基本是可以承載繪圖需求的。
當(dāng)務(wù)之急是解決小程序完全沒有提供可寫屬性的問題。所以希望能夠?qū)?duì) CanvasContext 中屬性的寫請(qǐng)求變成對(duì)小程序 CanvasContext 中一系列 set 接口的調(diào)用,答案就比較明顯了,就是將寫請(qǐng)求代理出來,手動(dòng)調(diào)用接口。說到代理,那 ES6 的 Proxy、Reflect,或者是最原生的 defineProperty 都可以做。其實(shí)這幾者基本都是對(duì) getter setter 的劫持,為了最簡(jiǎn)化,也為了不考慮 ES6 兼容的問題,我們直接用 defineProperty 試試看。有了上面的 diff 根據(jù),適配起來并不難:
const MAP = {
fillStyle: "setFillStyle",
font: "setFontSize",
globalAlpha: "setGlobalAlpha",
lineCap: "setLineCap",
lineJoin: "setLineJoin",
lineWidth: "setLineWidth",
miterLimit: "setMiterLimit",
shadowOffsetX: "setShadow",
shadowOffsetY: "setShadow",
shadowBlur: "setShadow",
shadowColor: "setShadow",
strokeStyle: "setStrokeStyle",
textAlign: "setTextAlign",
textBaseline: "setTextBaseline",
};
// 提取可配置的 fontSize
const fontSizeReg = /(\d*)px/;
export default (ctx) => {
Object.keys(MAP).forEach((key) => {
Object.defineProperty(ctx, key, {
set(val) {
const setter = MAP[key];
if (!ctx[setter]) {
return;
}
// 對(duì)設(shè)置 font 的請(qǐng)求,只使用 setFontSize 設(shè)置字號(hào)
if (key === "font" && fontSizeReg.test(val)) {
const match = fontSizeReg.exec(val);
ctx[setter](match[1]);
return;
}
// 考慮自行添加變量保存 shadow 各字段
// 每次都調(diào)用 setShadow 進(jìn)行全量設(shè)置
if (key === "shadow" && Array.isArray(val)) {
ctx[`_${key}`] = val;
ctx[setter](
ctx._shadowOffsetX || 0,
ctx._shadowOffsetY || 0,
ctx._shadowBlur || 0,
ctx._shadowColor || ""
);
return;
}
ctx[setter](val);
},
});
});
return ctx;
};
復(fù)制代碼
因?yàn)槌跏蓟?Chart 對(duì)象的時(shí)候,總要傳入一個(gè) CanvasContext 或者其元素。我們可以對(duì) Chart 進(jìn)行一次繼承,其子類 TTChart 在初始化的時(shí)候,拿到 CanvasContext 先回做一層包裝劫持,使用上面的方法設(shè)置各個(gè)屬性的 setter,而后再交給父類,原 Chart 做初始化操作,后續(xù) Chart 基于這個(gè) Canvas 上下文繪圖時(shí),對(duì)接的 API 就是我們適配過的了。
適配效果如下,基本可以做到官方 demo 的表現(xiàn)了,只是寬高沒對(duì)應(yīng)設(shè)置而已,數(shù)據(jù)展示、動(dòng)畫、交互已經(jīng)可以實(shí)現(xiàn)(交互上需要給小程序中的 canvas 節(jié)點(diǎn)綁定 touch 系列的事件監(jiān)聽,手動(dòng) dispatch)。
5.4 復(fù)診
- 官方也在做適配努力,未來可期;
- Canvas 可以適配,GAP 是可以接受的。字節(jié)系小程序的 Canvas 2D 上下文基礎(chǔ)庫(kù)有上面這些 API 與 HTML5 標(biāo)準(zhǔn)存在 GAP,適配上有一些漏洞,但不會(huì)帶來很大的根本性影響;
- 庫(kù)內(nèi)少量依賴了瀏覽器 BOM、DOM 的環(huán)境,但因?yàn)楣俜降挠幸膺w移在慢慢淡出,比如前面提到的手勢(shì)操作,自己實(shí)現(xiàn),脫離 hammer.js,這是好的進(jìn)展;
- 但是類似原來提供的允許用戶引入自定義 HTML 的功能由于小程序沒有 DOM 接口將會(huì)一直有缺陷??梢匀绾芜m配?提前預(yù)留節(jié)點(diǎn)位置?自定義節(jié)點(diǎn)完全轉(zhuǎn)譯畫出來?
- F2 官方可否提供更高程度的自定義口子,允許用戶自己拓展一些插件?
- 事實(shí)上類似這樣的操作是可以實(shí)現(xiàn)的,對(duì)具體數(shù)據(jù)的交互發(fā)生時(shí) F2 提供了回調(diào)入口,我的項(xiàng)目中就通過這樣的口子自己實(shí)現(xiàn)了自定義的 Tooltip。
回顧總結(jié)
簡(jiǎn)單回顧一下本文的內(nèi)容。
因?yàn)榭梢暬@個(gè)話題還是比較大的,即使具體到某個(gè)特征鮮明的庫(kù),也還是有許多內(nèi)容會(huì)涉及到。在這次分享中,主要涉及了以下幾大塊內(nèi)容:
- 可視化話題上基礎(chǔ)概念的了解,從 2.1 可視化概念介紹、圖形語(yǔ)法引入,在此基礎(chǔ)上講到基于圖形語(yǔ)法的 AntV 家的圖表庫(kù)和各自定位發(fā)展。然后在 2.2 有了大致的上手體會(huì),了解使用圖表庫(kù)的日常場(chǎng)景是怎樣的,感受圖形語(yǔ)法的體現(xiàn)。
- 走馬觀花地瀏覽了現(xiàn)有框架下 F2 是怎么設(shè)計(jì)的,主要有多少個(gè)功能模塊和認(rèn)知上的實(shí)體,通過簡(jiǎn)單瀏覽代碼和數(shù)據(jù)流動(dòng),明確各自職責(zé),而后再回到具體的使用場(chǎng)景中,看看這個(gè)庫(kù)是怎么 work 的。
- 接下來比較詳細(xì)地了解了兩個(gè)功能模塊的具體實(shí)現(xiàn),挑選這兩個(gè)功能模塊也是因?yàn)橛X得其比較獨(dú)立,容易抽離出來弄明白,而且有一定設(shè)計(jì)構(gòu)思,學(xué)習(xí)參考的價(jià)值較高。
- 回到最初的起點(diǎn),回到這次學(xué)習(xí)的啟動(dòng)背景——需求,看到在明白原理之后,遇到問題我們可以如何適配的一個(gè)小 case。
7.1 更看好、更愿意使用標(biāo)榜圖形語(yǔ)法的 G2(-Plot)、F2,還是配置化、功能化的老大哥 ECharts 之類的呢?開放性話題
大哥還是大哥,用戶量大和社區(qū)活躍度高。畢竟現(xiàn)在也是 Apache 旗下開源產(chǎn)品了,而且接入門檻相形之下會(huì)更低。作為前輩級(jí)的庫(kù),ECharts 也積累了很多優(yōu)化實(shí)踐和經(jīng)驗(yàn),也有許多強(qiáng)大工具如 ZRender,它實(shí)現(xiàn)了 SVG 和 Canvas 兩大陣營(yíng)的底層抹平,個(gè)人感覺是非常厲害的設(shè)計(jì),把上面提到的 immediate-mode 和 retained-mode 這兩個(gè)模式的標(biāo)準(zhǔn)做到統(tǒng)一封裝,光這一點(diǎn)在適配性、優(yōu)化上就能造就很大優(yōu)勢(shì)。
剛才提到了 noCode, lowCode,配置化確實(shí)是不用 code,但是 noCode, lowCode 本身的一個(gè)難點(diǎn)要點(diǎn)是定制性夠不夠強(qiáng),雖然之前了解到的 360 團(tuán)隊(duì)的一個(gè)可視化編輯器,用的圖表庫(kù)部分就主要是 ECharts,但圖形語(yǔ)法的描述能力可以說具有更高的靈活性和擴(kuò)展性,會(huì)不會(huì)發(fā)展到某種程度的時(shí)候其實(shí)能做得更好。
作者:字節(jié)前端
來源:掘金
著作權(quán)歸作者所有。商業(yè)轉(zhuǎn)載請(qǐng)聯(lián)系作者獲得授權(quán),非商業(yè)轉(zhuǎn)載請(qǐng)注明出處。
相關(guān)案例查看更多
相關(guān)閱讀
- 云南微信小程序開發(fā)
- 云南小程序開發(fā)公司
- 云南etc小程序
- 網(wǎng)站優(yōu)化哪家好
- 昆明小程序開發(fā)
- 微信小程序
- 網(wǎng)站搭建
- 麗江小程序開發(fā)
- 百度小程序公司
- 云南電商網(wǎng)站建設(shè)
- 云南小程序商城
- 云南網(wǎng)站優(yōu)化公司
- 貴州小程序開發(fā)
- 報(bào)廢車
- 網(wǎng)絡(luò)公司哪家好
- 重慶網(wǎng)站建設(shè)公司
- 霸屏推廣
- 云南小程序開發(fā)推薦
- 云南小程序開發(fā)制作公司
- 云南小程序制作
- 昆明小程序公司
- 網(wǎng)站建設(shè)高手
- 做小程序被騙
- 云南etc微信小程序
- 網(wǎng)站建設(shè)哪家強(qiáng)
- 網(wǎng)絡(luò)公司聯(lián)系方式
- 云南網(wǎng)站建設(shè)公司排名
- 云南小程序公司
- 網(wǎng)站排名
- 小程序開發(fā)公司