增強 D3.js 的視覺化功能
媽祖託夢給我說我一定要寫這篇
前言
在前面的文章中,我們做出了一些圖表,但是很難讀懂,因為我只看到線以及一些數字,可是沒有「數字上的概念」,而且圖表大小都是寫死的,如果裝置的螢幕太大或太小,這個圖表肯定會跑版。所以這篇文章要探討兩件事:透過比例(Scaling)作響應式設計以及座標軸(Axis)來顯示數值。
目前在 D3.js 中, Scale 有分成兩大類,一類是「Quantitative」,主要以數字或是日期等數值做為縮放的依據,另一類則是「Ordinal」,可用自訂的類型做為縮放依據。在本篇中,「Quantitative」會在 Scaling 章節做講解;「Ordinal」部分會搭配座標軸「Axis」介紹。
本篇目前一樣不探討製圖方式(例如如何產生長條圖)、以及視覺化設計(例如 UI/UX、美感等)。
Scaling
D3.js 的發明人 Mike Bostock 針對 d3 的其中一個強大的功能 - Scaling 做了如下的敘述:
All d3.js scales maps input values (the domain) to output values (the range) via a function. As a visualization designer, you need to determine what input domain and output rangeis needed for the data. — Mike Bostock
我們將數值轉換成座標,利用 px 的方式,使我們的資料能夠畫在網頁上,然後把輸入的數值,根據我們定義的範圍轉換成我們定好的輸出數值範圍。
到目前為止,前面的文章都是將高度與寬度固定好,但是我們的資料總是會成長,如果我們還是透過設定固定的數值,將會非常地不彈性,並且有些資料可能因為超過邊界而沒顯示,導致我們資料遺失。透過使 input values (the domain) 進而調整 data values 使我們的 output values (the range) 來符合顯示。
那我們是如何建立 Scale functions 呢?我們先呼叫 d3 底下的函式庫:
let scale = d3.scaleLinear();
接著,設定要變換數值的 domain 跟 range:
let scale = d3.scaleLinear().domain([100,200]).range([1,100])
其中,domain() 跟 range() 都要傳入陣列型態的數字,陣列中每兩兩數字代表著一個區段,來搭配 range 表示比例尺的縮放, 並且原本 domain 內容陣列長度有多長,range 內容陣列的長度也要一樣長度,將上圖的程式碼轉成圖像:
表示當我在 scale 函式傳入 100~200 時,返回的結果會按比例落在 1~100區間,那如果我傳入 99 的時候呢?我傳入 201 呢?
這裡有個簡單的想法供你參考
let scale = d3.scaleLinear().domain([p1,p2]).range([q1,q2]);
其中我傳入一數字 d1
則結果為以下計算式:|d1 - p1|/|p2 - p1| = |x - q1|/|q2 - q1|求出的 x 即為 scale 會回傳的值。
我們一樣回到 jsbin ,把上述的範例做一次練習:
會得到如下的結果:
將 d1 以 150 代入
|150 - 100|/|200 - 100| = |x - 1|/|100 - 1|
得 x = 50.5
看完比例尺之後,我們要將這樣的練習應用到上篇文章做的撈取氣象資料的練習,因為資料集撈取的功能都寫好了,所以我只需要在 buildLineChart() 函式中,重新定義整個繪圖的函式,而我的資料集是二維(日期、濕度),所以為了讓圖表等比例縮放,我的 x 、 y 軸要等比例縮放。
我的畫布大小設定值如下
let height = 100;
let width = 500;
首先,我要定義 x 軸該如何縮放,我透過 d3.min() 跟 d3.max() 函式先找出日期的極大跟極小值,作為我的 domain,然後我要怎麼縮放呢?因為是 x 軸縮放,並且我希望我的圖不超過我定義的大小,又 D3 繪圖是以左上邊為出發點,所以,我的 range 設定從 [0, width]。
y 軸一樣照本宣科,但注意我在 range 這邊的設定是 [height, 0],目的是讓圖表可以從左上角開始畫圖往下畫。
大家可以看一下我在 Jsbin 中的作圖,調整 width 跟 height 的值的時候,圖表會去做調整:
以上面兩張圖為例,大家是否可以看到 Scaling 的威力了?但是你是否發現圖形差異很大,原因是我調整了高度,使兩圖中間的值線的角度有非常大的不同,所以下面要提到的軸,可以更幫助我們釐清圖表的涵義。
另外,可以思考一下,如果今天 domain() 跟 range() 傳入陣列型態的數字各有三個,例如 domain([0, …n]) 跟 range([0, …n]),他會怎麼縮放?
let scale = d3.scaleLinear().domain([p1,p2, ...pn]).range([q1,q2, ...qn]);
其中我傳入一數字 d1
則結果為以下計算式:
第一段:|d1 - p1|/|p2 - p1| = |x - q1|/|q2 - q1|第二段|d1 - p2|/|p3 - p2| = |x - q2|/|q3 - q2|....
第 n 段
|d1 - p(n-1)|/|pn - p(n-1)| = |x - q(n-1)|/|qn - q(n-1)|看看 d1 落在哪個 p 區間,帶入該段。求出的 x 即為 scale 會回傳的值。
Axis
座標的功用在於可指出座標的位置,使之可計算距離和面積,或是說明我們正關注的資料內容。
首先,我們在程式碼中加入
let yAxisGen = d3.axisLeft(yScale)
這段的意思是,我們定義一個 yAxisGen 的工具,並且使軸顯示在左邊(y軸)。
接著,我們針對 y 軸作一些設計:
let yAxis = svg.append("g")
.call(yAxisGen)
append(“g”) 的 g 表示 svg 中的 group element,因為軸的每個標籤都是一個個線段,透過 g 元素讓全部軸元素全部放在一起,其功用類似 html 中的 div tag。
我們執行上面的程式碼,發現出現了一個線段,但這線段看起來沒有座標數值,但是透過檢查元素可發現其實是有數字的。
我們在中間加入 .attr(“transform”, “translate(“ + 20 + “, 0)”) 使座標軸上的數字顯示出來。
let yAxis = svg.append("g")
.call(yAxisGen)
.attr("transform", "translate(" + padding + ", 0)");
截至目前為止,所撰寫的程式碼執行結果如下圖:
由於我們讓座標軸右移 20px ,使圖表的線段超出邊界了,我們可以在原本的 xScale 的 range([0, width]) 增加一個 padding ,range([0+padding, width]),使圖形平移 20px。
我們完成了 y 軸的建立,接著,我們要建立 x 軸,與前面一樣的邏輯,先建立 xAxisGen
let xAxisGen = d3.axisBottom(xScale);
接著定義軸
let xAxis = svg.append("g")
.call(xAxisGen)
我們一樣發現這個 x 軸居然在正上方,可是我們不是用 axisBottom 了嗎?原因是他在繪圖時,類似把上方建立一個區塊,直接在上方的區塊底部作圖,所以即便使用 axisBottom,最後元素一樣呈現在上方,所以我們就透過 transform 函數來調整:
let xAxis = svg.append("g")
.call(xAxisGen)
.attr("transform", "translate(0," +(height-padding)+")");
建立到這邊之後,我們發現座標竟然有重複,並且密密麻麻的很不好看,除了透過 CSS 調整間距之外,我們也可以利用 axis 本身提供的 function : ticks 跟 tickFormat 的函式來讓 x、y 軸資訊不要太複雜:
let xAxisGen = d3.axisBottom(xScale).ticks(7).tickFormat(f);
let yAxisGen = d3.axisLeft(yScale).ticks(2);
ticks 是會按照上面設定的數字進行對應的區隔,例如在 yAxisGen 中傳入 2,程式會把 0~100 區分成二,所以 y 軸會有 0 跟 50 的標籤出現。
另外,你看到 xAxisGen 後面有個 .tickFormat(f),其中 f 的定義是一個函式,它會把 xScale 回傳的值作為f 函式的參數傳入,這個函式的功能是把日期簡寫成 MMDD 顯示。
//Format 20190509 -> 0509
function f(d){
return d.toString().substring(4,8);
}
最終的成果如下:
結論
在這次的研究中,大致掌握了 Scaling 的一些大觀念,了解 domain() 跟 range(),以及透過 Scaling 來操作資料比例縮放,讓圖表可以隨著裝置大小做縮放。
接著透過加入 Axis 元件讓圖表可以數值化,並且研究如何讓標籤可以設定一些格式化的內容。