相關包
打開搜索引擎,一搜 Vega,發現相關的包有好幾個,Vega, Vega-Lite, Vega-Embed,React-Vega 等等,不免讓人頭暈。
別急,它們之間的關係三四句話就能說明白,以下是極簡介紹:
- Vega:一套數據可視化的語法。它強大、靈活。用 JSON 描述配置,以 Canvas 或者 SVG 出圖。
- Vega-Lite:一套描述 Vega 配置的語法。它簡易、快速。同樣使用 JSON,其結果可以編譯爲 Vega 版本的配置。
- Vega-Embed:一個可以讓你在 web 項目中使用 Vega、 Vega-Lite 的工具。
- React-Vega: 顧名思義,可以讓你在 React 項目中使用 Vega、 Vega-Lite 的工具。
Vega-Lite 是描述 Vega 語法的高階語法(有點類似 React 高階組件的概念),它短平快的風格可以讓你迅速上手,但與 Vega 相比有一些功能上的限制。
在實際使用中,可以先通過 Vega-Lite 快速把想法實現爲圖表,再在其編譯的 Vega 版配置結果上進一步修改,增加複雜功能。
項目案例
現在網上已經有一些用 Vega 實現柱狀圖(bar chart)的文章,本文將主要介紹如何在 React 項目中用 Vega-Lite 語法實現一個 area chart。
圖表
基本圖表
製作一個 area chart,表現國慶七天假期內的用戶數量變化。
(可以粘貼到 Vega editor 中)
{
"$schema": "https://vega.github.io/schema/vega-lite/v4.json",
"mark": {"type": "area", "color": "#0084FF", "interpolate": "monotone"},
"encoding": {
"x": {
"field": "date",
"type": "temporal",
"timeUnit": "yearmonthdate",
"axis": {"title": "Date"}
},
"y": {
"field": "active_users",
"type": "quantitative",
"axis": {"title": "Active Users"}
},
"opacity": {"value": 1}
},
"width": 400,
"height": 300,
"data": {
"values": [
{"active_users": 0, "date": "2019-10-01"},
{"active_users": 2, "date": "2019-10-02"},
{"active_users": 0, "date": "2019-10-03"},
{"active_users": 1, "date": "2019-10-04"},
{"active_users": 0, "date": "2019-10-05"},
{"active_users": 0, "date": "2019-10-06"},
{"active_users": 1, "date": "2019-10-07"}
]
},
"config": {}
}
Y 軸顯示整數
看到這裏,有人會問了,這什麼產品這麼慘,才這麼點活躍用戶數?
這裏數據量寫小是爲了凸顯 y 軸顯示的問題。如果把用戶數據改大,y 軸顯示是很好看的,但如果數很小的話,如圖,就會顯示成小數。
顯然,不存在半個,或者大半個用戶,y 軸應該顯示爲整數。
通過查閱官方文檔,有個 format 參數其中有 ‘d’ 可以設置爲整數。
...
"y": {
"field": "active_users",
"type": "quantitative",
"axis": {
"title": "Active Users",
"format": "d"
}
},
...
圖表發生了變化,但存在重複顯示的問題。
這個時候,就需要使用 values 來直接指定。
...
"y": {
"field": "active_users",
"type": "quantitative",
"axis": {
"title": "Active Users",
"format": "d",
"values": [1,2]
}
},
...
現在,y 軸成了我們想要的效果。顯然,這裏的 values 參數數組是隨着數據變化的,我們可以在 React 組件中動態地傳入,而且數組中不要 0,這樣沒數據時會顯示空圖表。
X 軸 時間軸的合理顯示
現在再看時間軸的參數。field
指定了對應數據中的date
,即按天顯示,然後又通過 timeUnit
規定了顯示格式。
另外還有個 type
參數,其值爲 temporal
。這個詞本身意思和時間有關的,顯示時間時常常用這個類型。
在數據量大的時候,顯示效果是很好的,但當數據量小的時候,就會出現與剛纔 y 軸類似的問題:重複的label。
例如下圖,把長寬改大(web 端正常尺寸),就出現了重複顯示的問題。
"width": 1200,
"height": 600,
此時,type
設爲 ordinal 可以解決這個問題。
...
"x": {
"field": "date",
"type": "ordinal",
"timeUnit": "yearmonthdate",
"axis": {"title": "Date"}
},
...
但當數據量大的時候,x 軸的 label 會擠到一起,黑壓壓一片。
"values": [
{"active_users": 0, "date": "2019-10-01"},
{"active_users": 2, "date": "2019-10-02"},
{"active_users": 0, "date": "2019-10-03"},
{"active_users": 1, "date": "2019-10-04"},
{"active_users": 0, "date": "2019-10-05"},
{"active_users": 0, "date": "2019-10-06"},
{"active_users": 1, "date": "2019-10-07"},
{"active_users": 0, "date": "2019-10-08"},
{"active_users": 2, "date": "2019-10-09"},
{"active_users": 0, "date": "2019-10-10"},
{"active_users": 1, "date": "2019-10-11"},
{"active_users": 0, "date": "2019-10-12"},
{"active_users": 0, "date": "2019-10-13"},
{"active_users": 1, "date": "2019-10-14"},
{"active_users": 0, "date": "2019-10-15"},
{"active_users": 0, "date": "2019-10-16"},
{"active_users": 1, "date": "2019-10-17"},
{"active_users": 0, "date": "2019-10-18"},
{"active_users": 2, "date": "2019-10-19"},
{"active_users": 2, "date": "2019-10-20"},
{"active_users": 0, "date": "2019-10-21"},
{"active_users": 2, "date": "2019-10-22"},
{"active_users": 0, "date": "2019-10-23"},
{"active_users": 1, "date": "2019-10-24"},
{"active_users": 0, "date": "2019-10-25"},
{"active_users": 0, "date": "2019-10-26"},
{"active_users": 1, "date": "2019-10-27"},
{"active_users": 0, "date": "2019-10-28"},
{"active_users": 2, "date": "2019-10-29"}
]
},
所以,我們可以加一個條件判斷,當數據範圍超過一個月,type
設爲 temporal
, 反之用 ordinal
。
爲了用戶的頸椎,再用 labelAngle
調整一下 x 軸 label 的角度。
const getDateXObj = rangeLen => ({
field: 'date',
type: `${rangeLen > 30 ? 'temporal' : 'ordinal'}`,
timeUnit: 'yearmonthdate',
axis: {
title: 'Date',
labelAngle: -45,
},
});
如果僅僅是爲了避免 label 排列過於密集,可讀性差的問題,直接設置 labelAngle
就可以達到類似效果。
temporal
和 ordinal
的真正區別在於:
- 前者把數據放在不可壓縮的時間軸上
- 後者僅對現有數據進行排列。
通過下面這兩張圖可以明顯看出區別,注意數據,只是在之前 19 年國慶節的七天假期後面加了一天 20 年元旦。
"values": [
{"active_users": 0, "date": "2019-10-01"},
{"active_users": 2, "date": "2019-10-02"},
{"active_users": 0, "date": "2019-10-03"},
{"active_users": 1, "date": "2019-10-04"},
{"active_users": 0, "date": "2019-10-05"},
{"active_users": 0, "date": "2019-10-06"},
{"active_users": 1, "date": "2019-10-07"},
{"active_users": 2, "date": "2020-01-01"} // happy new year~
]
temporal
ordinal
在 React 項目中顯示圖表
我們使用 react-vega
包。首先,先顯示最基本的圖表:
裝包
npm install react vega vega-lite react-vega --save
引入項目
...
import { Vega } from 'react-vega';
const spec = {
"$schema": "https://vega.github.io/schema/vega-lite/v4.json",
"mark": {"type": "area", "color": "#0084FF", "interpolate": "monotone"},
"encoding": {
"x": {
"field": "date",
"type": "ordinal",
"timeUnit": "yearmonthdate",
"axis": {
"title": "Date",
"labelAngle": -45
}
},
"y": {
"field": "active_users",
"type": "quantitative",
"axis": {
"title": "Active Users",
"format": "d",
"values": [1,2]
}
},
"opacity": {"value": 1}
},
"config": {}
}
const data = [
{"active_users": 0, "date": "2019-10-01"},
{"active_users": 2, "date": "2019-10-02"},
{"active_users": 0, "date": "2019-10-03"},
{"active_users": 1, "date": "2019-10-04"},
{"active_users": 0, "date": "2019-10-05"},
{"active_users": 0, "date": "2019-10-06"},
{"active_users": 1, "date": "2019-10-07"}
]
...
return (
...
<Vega
spec={{
...spec,
width: 400,
height: 300,
data: { values: data },
}}
/>
...
)
然後,按上文所述,優化顯示。
我們引入 getSpec 函數,用來返回 spec 對象。另外引入用來從 data 數組中取出最大值,以及創建 y 軸相應 values 參數值的函數。
...
const getSpec = (yAxisValues = [], rangeLen = 0) => ({
"$schema": "https://vega.github.io/schema/vega-lite/v4.json",
"mark": {"type": "area", "color": "#0084FF", "interpolate": "monotone"},
"encoding": {
"x": {
"field": "date",
type: `${rangeLen > 30 ? 'temporal' : 'ordinal'}`,
"timeUnit": "yearmonthdate",
"axis": {"title": "Date"}
},
"y": {
"field": "active_users",
"type": "quantitative",
"axis": {
"title": "Active Users",
"format": "d",
"values": yAxisValues
}
},
"opacity": {"value": 1}
},
"config": {}
})
...
function App() {
// get max value from data arary
const yAxisMaxValueFor = (...keys) => {
const maxList = keys.map(key => data.reduce((acc, cur) => (cur[key] > acc[key] ? cur : acc))[key]);
return Math.max(...maxList);
};
const yAxisValues = Array.from(
{ length: yAxisMaxValueFor('active_users') },
).map((v, i) => (i + 1));
const spec = getSpec(yAxisValues, data.length);
return (
<div className="App">
<Vega
spec={{
...spec,
autosize: 'fit',
resize: true,
contains: 'padding',
width: 400,
height: 300,
data: { values: data },
}}
/>
</div>
);
}
...
至此,我們以及成功地在 React 項目中引入了 Vega-Lite 描述的圖表。
圖表右上角有個按鈕,點擊,出現了若干選項,支持導出下載,查看編譯後的 Vega 配置,在 Vega 在線編輯器打開等功能。
顯然,後面這些都是輔助開發的,我們希望僅對用戶顯示導出下載的選項。而且最好能指定下載的文件名(而非一個統一的默認名)。
這就體現了 React-Vega
的一個優點,它支持 Vega-Embed
的若干配置功能,詳見文檔。這裏,我們只需要增加 actions
,downloadFileName
兩個配置。前者通過布爾值,僅打開導出功能,後者指定下載文件名。
...
<Vega
spec={ ... }
actions={{
export: true,
source: false,
compiled: false,
editor: false,
}}
downloadFileName={'1024.avi'}
/>
...
最後,給 spec 對象中加入title
:
...
"title": '1024',
...
resize & legend
圖表已經比較完善。作爲在頁面顯示的完善,需要考慮到用戶 resize 瀏覽器窗口大小的場景。
下一篇再談這個吧。
最後,1024,好人一生… 不對,“程序員節”快樂。
?
更新
數據可視化:在 React 項目中使用 Vega 圖表 (二):
- 多層圖表;
- 圖例;
- React 中使圖表大小始終跟隨瀏覽器窗口。