用 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 就是了)

除此之外,官方網站上也有一個線上的 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 的話按 Ctrl + Shift + J) 執行:

VEGA_DEBUG.view.data('table')
// (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 是為了調整方塊的位置,向右移動半個 band
  • width 用半個 band
  • y, y2 指定方塊的上界跟下界,感謝 scale ,我們可以直接指定 startend 這兩個時間欄位

看起來不糟,不過上點顏色應該會好看點。我用議程屬於的社群(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 可能會比較方便。