數據可視化之 Sankey 桑基圖的實現

原文地址: https://geekplux.com/2018/08/28/how-to-implement-sankey-diagram.html

什麼是桑基圖

Google 搜索桑基圖,能夠搜到一大堆定義。簡而言之,桑基圖是一種數據流圖,展現了數據是如何從左到右流向最後的節點,每條邊表明一條數據流,寬度表明數據流的大小。桑基圖經常使用於流量分析,能夠很清楚的看出數據是如何漸漸分流的。本文着重講解如何實現,理論方面的東西各位能夠自行了解。html

實現桑基圖的關鍵點

關鍵點有兩個:node

1. 座標計算

桑基圖要展示的數據流,算是圖(拓撲類、網絡型或關係型)數據的一種。實現一個數據可視化圖,最重要的就是拆解元素。而實現一個圖數據可視化,則最重要的是分清「節點」和「邊」。git

拆解元素以後,最重要的即是座標的計算,這裏包括點和邊的座標。而圖形中,任何的元素均可以看做是點連線而成,因此元素座標的計算實際上就成了點座標的計算。好比桑基圖中,節點是一個矩形,那麼只需計算兩個點(左上和右下)的座標(x0, y0),(x1, y1)即可肯定;邊是一個帶形,須要計算四個端點才能肯定,帶形的弧度則可由簡單的三次貝塞爾曲線計算得來。github

由此觀之,實現桑基圖的核心在於計算出以上的這些點座標。其實實現任意一種可視化都是計算點的座標。算法

2. 減小邊交叉

當數據量到必定程度的時候, 桑基圖中的邊會出現重疊現象,形成必定的視覺混亂。如何減小能夠閱讀本文第二節。後端

1、座標計算的實現

準備工做

設計數據結構

經典的圖數據結構通常是鄰接矩陣和鄰接表,咱們也能夠本身設計。我在作拓撲數據可視化的時候,會先和後端或數據同窗商定好我須要拿到的數據結構,一般是這個樣子:網絡

{
    nodes: [
        { foo: bar },
        { foo: baz },
        ...
    ],
    links: [
        { source: 0, target: 1, value: 100 },
        { source: 1, target: 2, value: 10 },
        ...
    ]
}

而我拿到以後要作的第一步就是先把 nodes 和 links 串聯起來,這裏每一個 link 的 source 和 target 分別是 nodes 的下標,固然你也能夠設置其餘的引用(指針),總之經過引用講二者串聯起來,變成:數據結構

{
    nodes: [
        {
            foo: bar,
            column: 0, // 節點所在第幾列
            row: 0, // 節點所在第幾行
            value: 100, // 節點數據流大小
            sourceLinks: [
                {
                    source: 0,
                    target: 1,
                    ...
                }
            ],
            targetLinks: [
                ...
            ]
        },
        ...
    ],
    links: [
        {
            source: 0,
            target: 1,
            value: 100,
            sourceNode: {
                foo: bar,
                column: 0,
                row: 0,
                ...
            },
            targetNode: {
                ...
            }
        },
        ...
    ]
}

這樣,對於某個節點來講,能夠直接用 O(1) 的時間複雜度訪問到它的任意相鄰節點。ide

計算節點數據流大小

這裏的計算方法可本身定,一般是取該節點入邊和出邊的數據流大小之和的最小值。函數

計算節點所在行列

在桑基圖的計算中,咱們還須要進行一個關鍵的計算——計算節點在桑基圖中的第幾行第幾列。

第幾列的計算,即爲節點在圖中的深度計算:

  • 入度爲 0 的節點深度爲 0,在第一列
  • 出度爲 0 的節點深度最大,在最後一列
  • 其他節點的深度爲他相連源節點的最大深度加 1

第幾行的計算,涉及到排序的問題,一般某一列中的節點都是按節點數據流大小,從大到小排序。

節點座標計算

剛纔咱們說過,座標計算能夠分爲兩部分:節點和邊。其中,邊的座標位置依賴於節點的座標,因此應該先計算節點座標。

但在計算座標以前,首先要明確一個問題:是否限定視圖的寬高。這個問題引伸出兩種節點座標的計算方式。

不限定視圖寬高

若是不限定寬高,那麼節點座標的計算步驟很簡單:

  • 設置一個節點的寬度
  • 設置節點的水平間距
  • 從左至右,根據剛纔計算出的節點所在第幾列,計算出節點的橫座標(x0, x1),初始的 x0 爲 0
  • 設定一個比例尺函數(多大的數據流對應屏幕上的多少像素,一般是首先設定一個節點最小高度和一個節點最大高度,而後找出全部節點數據流的最小和最大值,映射成一個定義域爲節點數據流大小,值域爲節點高度的函數)
  • 經過比例尺計算出節點高度
  • 設置一個節點垂直間距
  • 從上至下,根據剛纔計算出的節點所在第幾行,計算出節點的縱座標 (y0, y1),初始的 y0 爲 0

大體是這個思路,橫座標的計算取決於兩個值,節點寬度節點水平間距;縱座標的計算取決於 節點的數據流大小節點垂直間距

具體的計算代碼,可根據你本身的數據結構來調整。

限定視圖寬高

若是限定寬高,那麼計算步驟須要換個思路:

  • 節點的寬度和節點的水平間距須要根據節點的列數和視圖寬度來計算,你能夠本身手動調整也能夠設計個算法來算
  • 從左只有,根據節點寬度和節點水平間距,計算出節點橫座標
  • 設定一個比例尺函數,計算出節點的高度
  • 設置一個節點垂直間距
  • 經過高斯-賽德爾迭代(Gauss–Seidel method)計算出縱座標(大體的思路是,先根據前兩步的數值算出一個初始節點座標,若是整體佈局超出視圖的下界,則節點高度和節點垂直間距都按比例縮小(如 0.95),並同時上移 n 個像素,若是整體佈局超出視圖上界,則節點高度和垂直間距都按比例縮小,並同時下移 n 個像素,直到整體的桑基圖佈局適應一開始限定的視圖寬高)

這個思路是 d3-sankey 的實現思路。若是你有限定視圖寬高的需求,那麼能夠直接使用 d3-sankey。

邊的座標計算

只要肯定了節點座標,邊的座標能夠根據它源節點和目標節點的座標來算出:

  • 對於一個節點,將它的出邊和入邊進行排序(排序方法一般是根據相連節點在第幾行從上到下排,也能夠經過一些其餘排序方法減小邊的交叉,具體在第二節介紹)
  • 計算每一個節點中單位數據流佔節點高度的比例
  • 根據出邊入邊的數據流大小,乘上一步計算出的比例,則可獲得每條邊左右兩邊的高度
  • 從上到下,計算每條邊的縱座標
  • 每條邊四個端點的橫座標分別對應源節點和目標節點的橫座標

以上操做能夠經過遍歷每一個 node 的 sourceLinks 和 targetLinks 來計算。獲得邊的四個端點之後,就能夠算出三次貝塞爾曲線的控制點了:

2、如何減小交叉

一般要減小邊的交叉,能夠採用下面兩種方法:

均值排序這個名字是我本身起的。。不過這個方法很實用有效。

對於每一個源節點來講,都有相連的目標節點。這裏的「均值」指的是全部相連目標節點所在行數的平均值(全部目標節點的行數相加,除以目標節點個數),這個平均值能夠大體描述該節點每一個出邊的位置。每條出邊都有這樣一個值,這個值越小,則說明該出邊要鏈接的目標節點的位置越靠上,反之越靠下。因此可根據這個值,從小到大排出出邊在該節點上從上到下的位置。

3、具體項目中的交互

我參與的 UBA (User Behavior Analytics 內部項目) 項目中,正好用到了桑基圖。除了上述的圖形繪製以外,主要複雜的是交互。



如圖所示,除了基本的 hover 交互以外,項目中主要還有

  • minimap 拖拽和刷選
  • 主視圖的拖拽和縮放
  • 左下角的過濾器
  • 點擊交互,高亮只通過選中節點的路徑,而且邊上高亮的部分由最後一個選中節點懈怠的數據流值肯定,其他部分半透明

整個桑基圖實現下來發現繪製只是一些計算,交互纔是更難抽象和處理的部分。

綜上,桑基圖是一個 展示數據流很是好用的視圖,感興趣的同窗能夠本身實現一個試試。除了我文章中這些基本的桑基圖佈局,你還能夠試試其餘變種,另外交互方面也能夠突破剛纔我提到的那些,好比我以前實現過點擊節點進行摺疊/展開的交互。整體來講可視化仍是一個比較有意思的方向。


本做品採用知識共享 署名-非商業性使用-禁止演繹 4.0 國際 許可協議進行許可。

相關文章
相關標籤/搜索