數據可視化:在 React 項目中使用 Vega 圖表 (一)

相關包

打開搜索引擎,一搜 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。

圖表

1024

基本圖表

製作一個 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"
        }
    },
...

y 軸 labels 重複
圖表發生了變化,但存在重複顯示的問題。

這個時候,就需要使用 values 來直接指定。

...
 "y": {
      "field": "active_users",
      "type": "quantitative",
      "axis": {
        "title": "Active Users",
        "format": "d",
        "values": [1,2]
        }
    },
...

y 軸

現在,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-axis de-du

但當數據量大的時候,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"}
    ]
  },

x-axis too many

所以,我們可以加一個條件判斷,當數據範圍超過一個月,type 設爲 temporal, 反之用 ordinal

爲了用戶的頸椎,再用 labelAngle 調整一下 x 軸 label 的角度。

const getDateXObj = rangeLen => ({
  field: 'date',
  type: `${rangeLen > 30 ? 'temporal' : 'ordinal'}`,
  timeUnit: 'yearmonthdate',
  axis: {
    title: 'Date',
    labelAngle: -45,
  },
});

x-axis better

如果僅僅是爲了避免 label 排列過於密集,可讀性差的問題,直接設置 labelAngle 就可以達到類似效果。

temporalordinal 的真正區別在於:

  • 前者把數據放在不可壓縮的時間軸上
  • 後者僅對現有數據進行排列。

通過下面這兩張圖可以明顯看出區別,注意數據,只是在之前 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
temporal

ordinal
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 在線編輯器打開等功能。

顯然,後面這些都是輔助開發的,我們希望僅對用戶顯示導出下載的選項。而且最好能指定下載的文件名(而非一個統一的默認名)。

5

這就體現了 React-Vega 的一個優點,它支持 Vega-Embed 的若干配置功能,詳見文檔。這裏,我們只需要增加 actionsdownloadFileName 兩個配置。前者通過布爾值,僅打開導出功能,後者指定下載文件名。

  ...
  <Vega
    spec={ ... }
    actions={{
        export: true,
        source: false,
        compiled: false,
        editor: false,
      }}
      downloadFileName={'1024.avi'}
    />
  ...

最後,給 spec 對象中加入title:

  ...
  "title": '1024',
  ...

1024

resize & legend

圖表已經比較完善。作爲在頁面顯示的完善,需要考慮到用戶 resize 瀏覽器窗口大小的場景。

下一篇再談這個吧。

最後,1024,好人一生… 不對,“程序員節”快樂。

?

更新

數據可視化:在 React 項目中使用 Vega 圖表 (二)

  • 多層圖表
  • 圖例
  • React 中使圖表大小始終跟隨瀏覽器窗口
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章