基於Flutter 的圖形語法庫,通過跨端 Canvas ,將基於 Javascript 的圖形語法庫 ChartSpace 擴展至 Flutter 端
作者:字節跳動終端技術——胡珀
背景
數據平臺有個基於圖形語法的圖表庫 ChartSpace,支持 web/h5/mini program,現在收到業務訴求,要支持到 Flutter 端。
爲方便理解,稍微解釋下圖形語法的概念,已經瞭解的小夥伴可以跳過這一段。
圖形語法
圖形語法(grammar of graphics) 是通過一套語法來描述任意圖形,主要來自 Wilkinson 的《The Grammar of Graphics》,可參考文章:https://zhuanlan.zhihu.com/p/47550015
圖形語法與一般的圖表主要區別在於:圖形語法只要修改下語法描述,就能得到完全不同的圖形,而一般的圖表需要增加圖表類型。圖形語法可描述的圖形是近乎無限的,而圖表類型是有限的。
舉個例子(截取自:https://segmentfault.com/a/1190000041004457):
如果我們基於圖形語法繪製了柱狀圖
將語法中的座標系換成極座標後,會變成玫瑰圖
語法中調整座標度量,並增加不同顏色後,變成了更完善的玫瑰圖
繼續調整語法參數,最終可得到餅圖
在這個例子中,如果用一般圖表(如 ECharts),需要至少4個圖表類型,圖表數據的格式也可能存在區別。但使用圖形語法描述,只需要調整不同的語法參數,就能得到不同的圖形。
圖形語法通過調整語法參數,得到不同的圖形,給數據的表達提供了更大的空間,屬於更專業的圖表引擎,但同樣也帶來了較爲複雜的語法規則。
基於圖形語法,前端(JS語法)常用的圖表庫:
- G2:螞蟻金服基於圖形語法的圖表庫,圖形語法通過 js 語法使用
const ds = new DataSet();
const chart = new Chart({
...
});
...
const dv2 = ds.createView().source(dv1.rows);
dv2.transform({
type: 'regression',
method: 'polynomial',
fields: ['year', 'death'],
bandwidth: 0.1,
as: ['year', 'death']
});
const view2 = chart.createView();
view2.axis(false);
view2.data(dv2.rows);
...
chart.render();
- Vega:開源的圖形語法框架,圖形語法通過 json 配置使用
{
width : 500,
height : 200,
config : {
axis : {
grid : true,
gridColor : #dedede
}
},
...
}
- ChartSpace:字節跳動基於圖形語法的圖表庫,圖形語法通過 json 配置使用,語法與 Vega 相近
{
type : line ,
data : [],
labels : {
visible : false
},
axes : [
{
orient : left
},
{
orient : bottom
}
],
xField : x ,
yField : y
}
在跨端,跨語言的情況下,json 配置的語法擁有更好的多端一致性。後端保存一套相同的 json 配置,可以在多端繪製出相同的圖形。
圖表庫 ChartSpace
ChartSpace 是字節數據平臺基於圖形語法的圖表庫,已支持 web/h5/mini program,現在要支持到 flutter 端。
業務上期望多端協同,同一份數據在不同端上有一致性的表現,以折線圖爲例:
方案
常規的方案是實現一套 flutter 版的圖形語義,解析 chartspace 的語義配置,繪製成相同規格的圖形。但這種方案帶來的開發成本比較高,所以我們選擇了另一套方案:跨端 canvas。
原理就是將 chartspace (js) 所使用的 web canvas 上繪製的內容,通過跨端技術給呈現到 flutter canvas 上來。
實現這個方案,要解決兩個問題:
- 把東西畫出來
- 把交互串起來
把東西畫出來
核心思想:將 chartspace(js) 的 canvas 繪製指令執行從 js 轉移到 flutter 執行,目標是對齊 Flutter Canvas 和 Web Canvas。
實現方式是:在 JS 中通過構造 Mock Canvas 對象,錄製 canvas 指令,然後發送到 Flutter 側,通過 Flutter Canvas 來實現這些指令。
主要工作量在於用 Flutter Canvas 實現一套 Web Canvas 的 API。
把交互串起來
用戶交互的輸入是 touch 事件,只需要將 Flutter PointerEvent 轉換爲 Web TouchEvent,輸入到 chartspace 即可。
之後 chartspace 會產生新的 canvas 指令,在 Flutter Canvas 中繪製出新的內容,流程和首次渲染一樣,至此交互就完整了。
效果
完成後效果如下,tooltip 的效果是手指點擊後產生的。
取得的收益是:低成本實現,低成本維護,跨端一致性。
渲染性能對比:
開發期間做過很多優化,graph 渲染時間從80ms優化到50ms,我們還在持續優化,爭取做到接近原生的體驗。後續我們其他小夥伴會分享優化的思路和實踐。
跨端 Canvas | 純 Flutter | |
---|---|---|
graph 渲染 | 52ms | 20ms |
tooltip 渲染 | 9ms | 0ms |
跨端 Canvas 的數據是從用戶輸入開始,到渲染圖形結束,包含了 bridge 傳輸,chartspace (js) 生成 canvas 指令的時間。
純 Flutter 是將相同的 canvas 指令變成 Flutter 代碼後的執行時間。
可以看到渲染性能與純 Flutter 模式有一定差距,但也在可接受範圍內,正常圖表交互時,用戶很難感知到區別。
我們相信,相同的圖表如果自己繪製,應該能有更好的性能,在 canvas 的指令優化 和 Web Canvas API 的實現上,還有一定的優化空間。
踩坑 & 解決方案
實踐過程中,遇到了很多問題,這裏選取幾類有代表性的分享一下
Canvas 生命週期不同
生命週期區別如下:
Flutter Canvas | Web Canvas |
---|---|
渲染不會保存畫布 | 渲染會保存畫布 |
每次都是重新繪製 | 在上一次的基礎上繼續繪製 |
我們的解決方案是,保存渲染後的結果,在上一次的渲染結果上繼續繪製
@override
void paint(Canvas canvas, Size size) {
final paintList = _repaint.consume();
ui.Picture picture = canvasRecorder.record(canvasId, size, _repaint.reverse, paintList);
if (picture != null) {
canvas.drawPicture(picture);
}
}
Canvas Context 不同
Context 區別如下:
Flutter Canvas | Web Canvas |
---|---|
canvas.save 保存的內容:Saves a copy of the current transform and clip on the save stack. | ctx.save 保存的內容: |
每次 paint 都是全新的 canvas 實例 | canvas 創建後,實例不變 |
針對第一個問題,save / restore 的內容不一致,我們創建了 WebCanvas 對象以模擬 Web 上的 Canvas,手動管理 save / restore 的內容
class WebCanvas {
...
SaveStack saveStack = SaveStack();
SaveInfo get current => saveStack.current;
...
}
針對第二個問題,我們創建了 CanvasRecorder 對象,並在該對象中持有 WebCanvas 實例,與 Web 上的 Canvas 實例的生命週期保持一致
class CanvasRecorder {
...
CanvasHistory getCanvasHistory(String canvasId) {
if (!hisMap.containsKey(canvasId)) {
hisMap.putIfAbsent(canvasId, () => CanvasHistory(canvasId));
}
return hisMap[canvasId];
}
...
}
class CanvasHistory {
...
final ChartSpaceCanvas chartSpaceCanvas = ChartSpaceCanvas();
...
}
class ChartSpaceCanvas {
...
final WebCanvas webcanvas = WebCanvas();
...
}
Canvas 默認值不同
Canvas 默認值不同的地方較多,我們直接按 Web Canvas 的標準設置了默認值,沒有仔細統計過差異,粗略來說有以下屬性有區別:
- transform
- fillStyle
- strokeStyle
- strokeMiterLimit
- font
以 transform 爲例,transform 實際維護的是一個 4 * 4 的變換矩陣(DOMMatrix 對象),web 上 setTransform 方法設置的是變換矩陣不同位置的值
Flutter 上是直接操作這個變換矩陣
但是 Web Canvas 和 Flutter Canvas 的變換矩陣默認值不一致
Flutter Canvas | Web Canvas |
---|---|
0 0 0 00 0 0 00 0 0 00 0 0 0 | 1 0 0 00 1 0 00 0 1 00 0 0 1 |
所以解決方案如下:
class Matrix4Builder {
static Matrix4 webDefault() {
final matrix4 = Matrix4.zero();
matrix4.setEntry(0, 0, 1.0);
matrix4.setEntry(1, 1, 1.0);
matrix4.setEntry(2, 2, 1.0);
matrix4.setEntry(3, 3, 1.0);
return matrix4;
}
}
Bridge 需要同步 API
我們通過 Mock CanvasRenderdingContext 對象,來達到錄製 canvas 指令的目的,但是 CanvasRenderdingContext 對象上有很多方法需要同步 API,比較高頻的比如 measureText。
但是常規的 Bridge 通信是
其中 Flutter 與 iOS/Android 的通信是異步的,所以這裏使用 FFI 直接與 JS Runtime 通信才能保證同步
截取部分代碼實現:
Pointer<Utf8> funcMeasureTextCString = Utf8.toUtf8('measureText');
var measureTextFunctionObject = jSObjectMakeFunctionWithCallback(
_globalContext,
jSStringCreateWithUTF8CString(funcMeasureTextCString),
Pointer.fromFunction(measureTextFunction));
jSObjectSetProperty(
_globalContext,
_globalObject,
jSStringCreateWithUTF8CString(funcMeasureTextCString),
measureTextFunctionObject,
jsObject.JSPropertyAttributes.kJSPropertyAttributeNone,
nullptr);
free(funcMeasureTextCString);
總結 & 展望
總結一下,我們通過跨端 Canvas 的方式,低成本實現了 Flutter ChartSpace,實踐下來取得了不錯的性能表現。
這也得益於 ChartSpace 本身合理的架構設計,通過 json 配置來定義圖形語義,能有效屏蔽不同平臺,語言的差異。
由於 ChartSpace 是基於圖形語義的實現,相比定製化的圖表類型,需要更大的計算量,會影響渲染性能。但現在也支持了分步渲染,在大數據和複雜的圖形下,能以漸進式的效果逐步呈現完整圖形,對用戶體驗並沒有損害。
Flutter ChartSpace 暫時還沒支持分步渲染,當前的方案還有很大的優化空間,我們會繼續探索。
未來考慮在兩個方向上繼續拓展:
設計易用性更高的 API
圖形語法雖然很強大,也帶來了使用上的複雜度,我們可以在圖形語法上包裝一層 API,將常用的圖形給剝離出來,降低使用成本。
比如螞蟻集團的 g2plot 就是在 g2 基礎上的封裝,提供了更簡潔的語法,引用 g2plot 的一段描述
const line = new Line('container', {
data,
xField: 'year',
yField: 'value',
});
line.render();
大家可以對比下 g2plot 的語法示例和 g2 的語法示例,g2 的語法在文章的圖形語法一節。
拓展更多的端/技術棧
實踐下來後,我們發現,相同的技術可以拓展至更多的技術棧,比如:iOS/Android/RN
開源
ChartSpace 和 Flutter ChartSpace 都是字節內部的產品。ChartSpace 在大量的數據產品,和其他業務中不斷打磨,經受了不同場景的考驗,包括抖音的數據分析,現在已經有開源計劃了。Flutter ChartSpace 也需要在內部場景打磨後,再考慮開源。
Flutter ChartSpace 會在 ChartSpace 之後開源,預期是今年年底。
🔥 火山引擎 APMPlus 應用性能監控是火山引擎應用開發套件 MARS 下的性能監控產品。我們通過先進的數據採集與監控技術,爲企業提供全鏈路的應用性能監控服務,助力企業提升異常問題排查與解決的效率。目前我們面向中小企業特別推出「APMPlus 應用性能監控企業助力行動」,爲中小企業提供應用性能監控免費資源包。現在申請,有機會獲得60天免費性能監控服務,最高可享6000萬條事件量。
👉 點擊這裏,立即申請