用 VEGA 資料視覺化
tl;dr
Vega 是一個種描述互動視覺化的文法(JSON 規格),相對於低階的 d3.js,提供了一個不用碰太多 JavaScript 就能編寫圖表的高階宣告式語法。這篇文章簡述 Vega 的概念,然後以 COSCUP 2017 的議程表為題寫了一個 tutorial(完成品)。
另外,如果只有簡單的資料(csv, json…等),又只需要基本的幾種圖,可以考慮使用 vega-lite 這個比 vega 還要高階的語言來做。
VEGA
Vega is a visualization grammar, a declarative language for creating, saving, and sharing interactive visualization designs. With Vega, you can describe the visual appearance and interactive behavior of a visualization in a JSON format, and generate web-based views using Canvas or SVG.
Vega 是 Washington Interactive Data Lab 的另一個專案(就是那個做 d3.js 的 IDL),提出了一個 JSON 格式的文法,用來描述互動式的視覺化圖表,也一併給了 JavaScript 的 Runtime 實作。(背後很理所當然的用了 d3.js 作為 backend)
為什麼不用 d3 就好
雖然不太一樣,但是可以先看看兩者實作 bar chart 的方式: d3, vega。
可以看到幾點:
- vega 的寫法少了很多來自 js 語法的雜訊,
也不用讓變數名稱佔掉大腦可貴的記憶空間https://www.kernel.org/doc/html/v4.10/process/coding-style.html#functions - vega 用了一個 json 敘述來描述視覺化,
但是 d3.js 的版本需要從 html/svg/canvas 等低階的實作細節開始考慮 - 比起 js , JSON 格式更好自動生成,也就是更好串接在其他應用
Vega 團隊有講明 Vega 的目的並不是要取代 d3.js ,而是在 d3.js 的基礎上,建立更高層度的視覺化。
長什麼樣子
官網給的一個 bar chart 的範例長的像這樣,可以發現幾乎看不到程式碼。
下面這個是從 specification 抄來的基本款 outline
- 上半部是除了
$schema
之外,是一些關於整張圖的 top-level 設定 - 下半部
[]
分別還需要填入適當的內容。
{
"$schema": "https://vega.github.io/schema/vega/v3.0.json",
"description": "A specification outline example.",
"width": 500,
"height": 200,
"padding": 5,
"autosize": "pad",
"signals": [],
"data": [],
"scales": [],
"projections": [],
"axes": [],
"legends": [],
"marks": []
}
signals
提供一個 reactive 的方法來做互動事件: 滑鼠移動、內建的按鈕、範圍選擇器之類的data
用來載入、parse、轉換,一些簡單的資料處理 (filter, aggregation 之類的都可以在這裡做掉)。scales
用來映射資料的 domain 跟 輸出的 range (XY 座標/ 顏色等)。projections
用來將經緯度投影到座標平面。axes
可以指定圖的的座標軸的位置、刻度。legends
是圖的圖例。marks
用來畫出折線、面積、矩形…等所有表現資料的部份,是整張圖核心的部份。
畫圖所需要的元素 VEGA 團隊都適當的設計了出來,剩下的就是使用者依照需求填空了。
如何開始
如果想要開始使用,可以先看官方網站上的兩個 tutorials 寫的簡單好懂(當然還是需要看 documentation 就是了)
- Let’s Make A Bar Chart 介紹了最基本的幾個元素: data, scale, axes, marks, signals
- Mapping Airport Connections 進一步應用了 data transform, projections, signals
除此之外,官方網站上也有一個線上的 WYSIWYG 編輯器: Vega Editor,裡面有很多範例可以 塗塗改改 參考。
剩下的就是讀文件啦。
tutorial
因為這次 COSCUP 2017 官網最早沒有釋出時間線形式的議程表,但是有給足自幹需要的資料, 就發現大家都自幹了一份,於是我就用 vega 也兜了一個簡單的版本,也就順便以此為原形,寫了這篇 tutorial。
這篇 tutorial 會簡述怎麼從零開始到做出一個這樣議程表
準備
打開 Vega Editor,就可以開始了。
top-level
首先要先有個最簡單的雛型,宣告 schema 版本、圖的大小等
// vg.json
{"$schema": "https://vega.github.io/schema/vega/v3.0.json",
"width": 480,
"height": 1600,
"padding": 50
}
Data
這次的資料是 COSCUP 2017 官網所使用的 submissions.json,是一個 array of records ,如下:
// submissions.json
[// 我隨便節錄了一個議程,兩天所有的議程都在這個 array 裡面
{"room": "202",
"community": "Chinese 中文",
"subject": "Unicode 不是年年更新嗎,2017 怎麼還在缺字?",
"summary": "維基文庫是維基媒體基金會所屬計畫中有關原始文獻典藏的網站,而中文的維基文庫因此遇到其他維基計畫不曾遇到的問題–暴量的古籍缺字。台灣的維基分會在經手吳守禮國臺對 照活用辭典的現代數位化之計畫中,也遇到了嚴重的缺字問題,好在現在開源動態組字技術已經發展成熟,可以整合到維基網站裡,本議題將分享該專案運用此新式缺字處理技術的點滴、目前瓶頸、未來的展望。",
"start": "2017-08-05T16:20:00+08:00",
"end": "2017-08-05T17:00:00+08:00",
"original_speakerpic": "http://imgur.com/q0j6fda",
"lang": "ZH",
"speaker": {
"name": "張正一",
"avatar": "https://coscup.org/2017-assets/images/program/202-2017-08-05T1515.jpg",
"bio": "維基協會理事,維基吳守禮台語辭典現代數位化計畫 PM,動態組字技術開發長期參與者"
},
}// 還有更多...
]
Vega 支援讀取 array of records 的資料,我們可以直接將他讀進來,然後順便對時間欄位做 parse :
// vg.json
// 前略
{ "data":[
"name": "table",
{ "url": "https://coscup.org/2017-assets/json/submissions.json",
"format": {"type":"json", "parse":{"start":"date", "end": "date"}}
}
] }
如果用的是 Vega Editor 的話,可以打開 developer console (chrome 的話按
.view.data('table')
VEGA_DEBUG// (141) [Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, …]
可以觀察到資料有正確載入。
Scale, Axes
載入資料之後、把圖畫出來之前,需要先將資料的範圍映射圖的X/Y座標:
// vg.json 的片段
"scales": [
{"name": "xscale",
"type": "band",
"domain": {"data": "table", "field": "room", "sort": true},
"range": "width"
,
}
{"name": "yscale",
"type": "time",
"domain": {"data": "table", "fields": ["start", "end"]},
"range": "height",
"reverse": true
} ]
每個 scale 都要有對應的名字 name
跟類型 type
,這邊我分別定義了:
xscale
band
可以做出 categorical 分段的效果(想像一下 bar chart,看一下這張圖)sort
好讓場地依照順序顯示range
設定成width
指的是 top-level 的設定
yscale
time
告訴 vega 我用的資料是個時間fields
指定domain
的最小最大值來自哪個欄位reverse
用來反轉軸的大小,讓先發生的有比較高的值,顯示在圖的上方
我們可以畫幾個座標軸(使用前面定義的scale)來觀察:
// vg.json 的片段
"axes": [
"orient": "top", "scale": "xscale" },
{ "orient": "bottom", "scale": "xscale" },
{ "orient": "left", "scale": "yscale" }
{ ]
Marks
我希望將每個議程用方塊的方式畫出來,做出時間表的的效果。
// vg.json 的片段
"marks": [
{"type": "rect",
"from": {"data":"table"},
"encode": {
"update": {
"xc": {"scale": "xscale", "field": "room", "band":0.5},
"width": {"scale": "xscale", "band": 0.5},
"y": {"scale": "yscale", "field": "start"},
"y2": {"scale": "yscale", "field": "end"}
}
}
} ]
- 每個 mark 都要有對應的
type
,這邊選用方塊(rect
) - 用
from
來指定資料來源
encode
的部份用來指定畫圖時的參數,建議先看一下文件 跟 Thinking with Joins
xc
用來指定 整個方塊的中心座標,band
是為了調整方塊的位置,向右移動半個 bandwidth
用半個band
y
,y2
指定方塊的上界跟下界,感謝 scale ,我們可以直接指定start
跟end
這兩個時間欄位
看起來不糟,不過上點顏色應該會好看點。我用議程屬於的社群(community
) 來上色。
- 先建立一個 scale 從社群類型映射到顏色
- 在 marks 裡面使用這個 scale
// vg.json 的片段,scale 底下
{"name": "color",
"type": "ordinal",
"domain": {"data": "table", "field": "community"},
"range": {"scheme": "category20"}
}
// vg.json 的片段,marks[0]["encode"] 底下
"enter": {
"fill": {"scale": "color", "field": "community"}
}
另外,可以用 tooltip
屬性來顯示議程的主題:
// vg.json 的片段,marks[0]["encode"]["enter"] 裡面
"tooltip": {"field": "subject"}
Transform
可以看到第一天議程結束到第二天議程結束有一大段空閒,可以用 transform 的 filter 來選擇只用哪一部分的資料。
// vg.json 的片段,data 底下
"transform": [
{"type": "filter",
"expr": "datum.start >= datetime(2017, 8-1, 5) && datum.end < datetime(2017, 8-1, 5+1)"
} ]
expr
吃一個 vega 精簡過的 javascript subset,用來給 filter
決定保留與否。
Signals
我不會永遠只想看其中一天,也不希望要看隔天的議程還要做修改這份 vega json。好在 vega 提供了一些方便的 signal,我這邊選用了他現成的 radio input:會在圖上面加上一個互動的 radio button,然後綁定成一個 signal。
// vg.json 的片段
"signals": [
{"name": "day",
"value": 5,
"bind": {"input": "radio", "options": [5, 6]}
} ]
綁定過的 signal 可以作為 expr
內的變數使用
// vg.json 的片段,修改 data["transform"][0]["expr"]
"expr": "datum.start >= datetime(2017, 8-1, day) && datum.end < datetime(2017, 8-1, day)"
完成
一個簡單的議程時間表就完成啦!當然還有很多可以增加/改進的地方,像是:
- 顯示更多情報:講者、議程摘要…
- 讓圖更漂亮:調整滑鼠 hover 時的 style …
- 加上 Scale Break ,直接省去切換兩天的動作
- 想辦法支援行動瀏覽器,手機版的 chrome 就不支援顯示 tooltip
- …
總結
我最後的成果放在這邊,雖然不完美,但是也讓我在很短的時間內完成了一個堪用的成品。
利用 Vega 這個工具,可以迅速得做出簡單好讀寫(相對於 d3.js),但又不失彈性的視覺化成果。
題外話,雖然 JSON 還算是好讀寫,但是實際還是不太適合人類直接編寫,要的話用 YAML 或是再幹一個 DSL 可能會比較方便。