D3.js 在 2016/7/28 釋出 v4.0.0 版,現在已經更新到 v4.4.1,大部分的書本還是以 v3 為主,因此我們嘗試測試將書本的範例調整為 v4 版本。
D3.js 可以生成及處理資料,處理過程經歷以下的步驟:
- 把資料載入到瀏覽器的 memory
- 把資料綁定到 DOM 的元素,根據需要建立新元素
- 解析每個元素的範圍資料 (bound datum),並為其設置相應的可視化屬性,實現元素的轉換 (transforming)
- 套用使用者輸入的,實現元素狀態的動態過渡 (transitioning)
D3 不適合產生探索型的視覺圖形,擅長產生解釋型的視覺圖形,探索型的視圖工具可以根據相同的資料,產生多個視圖。
D3 擅長處理 SVG 及 GeoJSON,不處理類似 google map 的地圖貼片。
所有的數據資料都必須傳送到客戶端的瀏覽器,如果數據資料有分享的疑慮,就不應該使用 D3.js。
以下的範例都是由 數據可視化實戰:使用D3設計交互式圖表 這本書取得的。
資料處理
- 產生網頁 DOM 元素
d3 // 引用 D3 物件
.select("body") // 取得 body 元素
.append("p") // 產生 p
.text("New paragraph!") // 放入文字到 p 元素中
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<script type="text/javascript" src="../d3/d3-4.2.2.min.js"></script>
</head>
<body>
<script type="text/javascript">
d3.select("body")
.append("p")
.text("New paragraph!");
</script>
</body>
</html>
大部分的 d3 method 會傳回正在操作的 DOM 元素,所以可以連續呼叫 method。
- 載入 csv 資料
d3.csv 是非同步的 method,後面需要一個 callback function 處理接收的資料。如果前面多了一個 error 參數,則是在處理下載 csv 失敗時的狀況。
d3.csv("food.csv", function(data) {
console.log(data);
});
var dataset;
d3.csv("food.csv", function(error, data) {
if (error) {
console.log(error); // 輸出錯誤
} else {
console.log(data); // 輸出資料
dataset = data;
}
});
- D3 的常見問題:如何使用還不存在的元素
var dataset = [ 5, 10, 15, 20, 25 ];
d3.select("body")
.selectAll("p") // 取得 body 裡面所有的 <p>,如果還不存在,就建立一個新的 <p>
.data(dataset) // 根據 dataset 資料的 5 個元素,後面的城市,會執行 5 次
.enter() // 分析目前的<p> 及 dataset,如果資料比 DOM 元素多,就建立一個新的元素,傳給下面的 method
.append("p") // 將 enter 產生的空元素,加入一個 <p>
.text("New paragraph!"); // 在 <p> 裡面加入 text
以 console.log(d3.selectAll("p"))
查詢所有的段落,並找到剛剛的 dataset
- 調整 dataset 的方法
修改最後一行,可以知道如何在 callback function 中使用 dataset 裡面的元素。
d3.select("body").selectAll("p")
.data(dataset)
.enter()
.append("p")
.text(function(d) { return d; });
以 .style("color","red")
把段落文字變成紅色
d3.select("body").selectAll("p")
.data(dataset)
.enter()
.append("p")
.text(function(d) {
return "I can count up to " + d;
})
.style("color", "red");
在 style 的處理中,也可以根據 dataset 的原始資料,進行條件判斷,產生不同的文字顏色
var dataset = [ 5, 10, 15, 20, 25 ];
d3.select("body").selectAll("p")
.data(dataset)
.enter()
.append("p")
.text(function(d) {
return "I can count up to " + d;
})
.style("color", function(d) {
if (d > 15) {
return "red";
} else {
return "black";
}
});
根據資料繪製圖形
準備一個長條矩形的 css style,將 div 變成長條圖
<style type="text/css">
div.bar {
display: inline-block;
width: 20px;
height: 75px; /* 會被 d3 的 style 覆寫 */
margin-right: 2px;
background-color: teal;
}
</style>
<script type="text/javascript">
var dataset = [ 25, 7, 5, 26, 11 ];
d3.select("body").selectAll("div")
.data(dataset)
.enter()
.append("div")
.attr("class", "bar")
.style("height", function(d) {
var barHeight = d * 5;
return barHeight + "px";
});
</script>
以亂數的方式產生 dataset
<script type="text/javascript">
var dataset = []; //Initialize empty array
for (var i = 0; i < 25; i++) { //Loop 25 times
//var newNumber = Math.random() * 30; //New random number (0-30)
var newNumber = Math.floor(Math.random() * 30); //New random integer (0-29)
dataset.push(newNumber); //Add new number to array
}
d3.select("body").selectAll("div")
.data(dataset)
.enter()
.append("div")
.attr("class", "bar")
.style("height", function(d) {
var barHeight = d * 5;
return barHeight + "px";
});
</script>
繪製 SVG 圖形
- 類似剛剛的 dataset 產生長條圖的方法,用同樣的方式產生 svg
<script type="text/javascript">
//Width and height
var w = 500;
var h = 50;
//Data
var dataset = [ 5, 10, 15, 20, 25 ];
// 產生 svg 區塊
var svg = d3.select("body")
.append("svg")
.attr("width", w)
.attr("height", h);
// 在 svg 中產生 circle
var circles = svg.selectAll("circle")
.data(dataset)
.enter()
.append("circle");
// 為每個 circles 設定屬性
circles.attr("cx", function(d, i) {
return (i * 50) + 25;
})
.attr("cy", h/2)
.attr("r", function(d) {
return d;
})
.attr("fill", "yellow")
.attr("stroke", "orange")
.attr("stroke-width", function(d) {
return d/2;
});
</script>
- 以 svg 的方式產生長條圖
<script type="text/javascript">
//Width and height
var w = 500;
var h = 100;
var barPadding = 1;
var dataset = [ 5, 10, 13, 19, 21, 25, 22, 18, 15, 13,
11, 12, 15, 20, 18, 17, 16, 18, 23, 25 ];
//Create SVG element
var svg = d3.select("body")
.append("svg")
.attr("width", w)
.attr("height", h);
// 產生 rect 方塊
svg.selectAll("rect")
.data(dataset)
.enter()
.append("rect")
// 方塊的 x 位置,以 svg 圖片的寬度,跟 dataset 數量計算
.attr("x", function(d, i) {
return i * (w / dataset.length);
})
// 方塊的 y 位置,(x,y) 座標點的計算是由左上角開始的,因此 y 的位置要由 svg 高度 扣掉資料的數值來決定, 資料的 4 倍可以讓圖形的相對差距變大,讓資料的差異在圖形的表現上更大
.attr("y", function(d) {
return h - (d * 4);
})
// 矩形的寬度由 svg 寬度跟 dataset 數量決定,badPadding 是不同矩形之間的空白
.attr("width", w / dataset.length - barPadding)
// 高度由 dataset 數值放大 4 倍決定
.attr("height", function(d) {
return d * 4;
})
// 以 rgb 動態將舉行填滿不同的顏色
.attr("fill", function(d) {
return "rgb(0, 0, " + (d * 10) + ")";
});
// 在 svg 中產生 text 文字區塊,變成長條圖上面的文字標籤
svg.selectAll("text")
.data(dataset)
.enter()
.append("text")
.text(function(d) {
return d;
})
// 讓文字對齊中間
.attr("text-anchor", "middle")
// 設定 text 的 x 座標位置
.attr("x", function(d, i) {
return i * (w / dataset.length) + (w / dataset.length - barPadding) / 2;
})
// 設定 text 的 y 座標位置
.attr("y", function(d) {
return h - (d * 4) + 14;
})
// 因為字看不清楚
.attr("font-family", "sans-serif")
.attr("font-size", "11px")
.attr("fill", "white");
</script>
attr 可以只設定一個屬性或是將多個屬性組合在一起
svg.select("circle")
.attr("cx", 0)
.attr("cy", 0)
.attr("fill", "red");
svg.select("circle")
.attr({
cx: 0,
cy: 0,
fill: "red"
});
在多個屬性中,同時指定 callback functions
svg.selectAll("rect")
.data(dataset)
.enter()
.append("rect")
.attr({
x: function(d, i) { return i * (w / dataset.length); },
y: function(d) { return h - (d * 4); },
width: w / dataset.length - barPadding,
height: function(d) { return d * 4; },
fill: function(d) { return "rgb(0, 0, " + (d * 10) + ")";}
});
繪製散點圖 Scatter Plot
二維的資料,一般就先繪製 scatter plot
<script type="text/javascript">
//Width and height
var w = 500;
var h = 100;
var dataset = [
[5, 20], [480, 90], [250, 50], [100, 33], [330, 95],
[410, 12], [475, 44], [25, 67], [85, 21], [220, 88]
];
//Create SVG element
var svg = d3.select("body")
.append("svg")
.attr("width", w)
.attr("height", h);
// 產生 circle
svg.selectAll("circle")
.data(dataset)
.enter()
.append("circle")
// 圓心的位置
.attr("cx", function(d) {
return d[0];
})
.attr("cy", function(d) {
return d[1];
})
// 圓的半徑
.attr("r", function(d) {
return Math.sqrt(h - d[1]);
});
// 加上標籤
svg.selectAll("text")
.data(dataset)
.enter()
.append("text")
.text(function(d) {
return d[0] + "," + d[1];
})
// 標籤的位置放在圓心的地方
.attr("x", function(d) {
return d[0];
})
.attr("y", function(d) {
return d[1];
})
// 設定標籤的文字 style
.attr("font-family", "sans-serif")
.attr("font-size", "11px")
.attr("fill", "red");
</script>
比例尺 scale
scale 是將輸入資料映射為另一組輸出範圍的函數。
一般來說,原始的資料數據不可能會剛好是圖表中的像素,就像是剛剛的例子一樣,長條圖的高度,是由原始資料計算出來的。這個資料轉換的過程, 就是 scale。
對於線性比例尺來說,概念就像是 normalization 一樣。D3 可以先將原始資料映射到 scale.domain([100, 500]) 的值域,也就是根據值域進行 normalization,然後以 scale.range([10, 350]) 將 normalized 之後的值,對應到 range 的輸出範圍。
在 D3 v3 版是 d3.scale.linear()
,在 v4 版是 d3.scaleLinear()
var scale = d3.scaleLinear().domain([100, 500]).range([10, 350]);
scale(100); // -> 10
scale(300); // -> 180
scale(500); // -> 350
// [100, 300, 500] -> [10, 180, 350]
<script type="text/javascript">
//Width and height
var w = 500;
var h = 100;
var dataset = [
[5, 20], [480, 90], [250, 50], [100, 33], [330, 95],
[410, 12], [475, 44], [25, 67], [85, 21], [220, 88]
];
//Create scale functions
var xScale = d3.scaleLinear()
// 以 d3.max 取得 dataset d[0] 的 max, normalized 為 0 ~ max(d[0])
.domain([0, d3.max(dataset, function(d) { return d[0]; })])
// 映射到 0 ~ w
.range([0, w]);
// 映射的範圍是相反的 (h,0)
var yScale = d3.scaleLinear()
.domain([0, d3.max(dataset, function(d) { return d[1]; })])
.range([h, 0]);
//Create SVG element
var svg = d3.select("body")
.append("svg")
.attr("width", w)
.attr("height", h);
svg.selectAll("circle")
.data(dataset)
.enter()
.append("circle")
.attr("cx", function(d) {
return xScale(d[0]);
})
.attr("cy", function(d) {
return yScale(d[1]);
})
.attr("r", function(d) {
return Math.sqrt(h - d[1]);
});
svg.selectAll("text")
.data(dataset)
.enter()
.append("text")
.text(function(d) {
return d[0] + "," + d[1];
})
.attr("x", function(d) {
return xScale(d[0]);
})
.attr("y", function(d) {
return yScale(d[1]);
})
.attr("font-family", "sans-serif")
.attr("font-size", "11px")
.attr("fill", "red");
</script>
因為接近外框的圓形被切掉了一部分,加上 padding=20,讓 svg 保留一部分外框。
//Create scale functions
var xScale = d3.scaleLinear()
.domain([0, d3.max(dataset, function(d) { return d[0]; })])
.range([padding, w - padding * 2]);
var yScale = d3.scaleLinear()
.domain([0, d3.max(dataset, function(d) { return d[1]; })])
.range([h - padding, padding]);
將圓形的半徑也使用 scale 進行映射到 [2,5]
var rScale = d3.scaleLinear()
.domain([0, d3.max(dataset, function(d) { return d[1]; })])
.range([2, 5]);
所有的數值都是動態計算的,如果把 svg 放大,也可以讓 dataset 依照同樣的方式,利用到 svg 的整個範圍。
<script type="text/javascript">
//Width and height
var w = 500;
var h = 300;
var padding = 20;
var dataset = [
[5, 20], [480, 90], [250, 50], [100, 33], [330, 95],
[410, 12], [475, 44], [25, 67], [85, 21], [220, 88],
[600, 150]
];
//Create scale functions
var xScale = d3.scaleLinear()
.domain([0, d3.max(dataset, function(d) { return d[0]; })])
.range([padding, w - padding * 2]);
var yScale = d3.scaleLinear()
.domain([0, d3.max(dataset, function(d) { return d[1]; })])
.range([h - padding, padding]);
var rScale = d3.scaleLinear()
.domain([0, d3.max(dataset, function(d) { return d[1]; })])
.range([2, 5]);
//Create SVG element
var svg = d3.select("body")
.append("svg")
.attr("width", w)
.attr("height", h);
svg.selectAll("circle")
.data(dataset)
.enter()
.append("circle")
.attr("cx", function(d) {
return xScale(d[0]);
})
.attr("cy", function(d) {
return yScale(d[1]);
})
.attr("r", function(d) {
return rScale(d[1]);
});
svg.selectAll("text")
.data(dataset)
.enter()
.append("text")
.text(function(d) {
return d[0] + "," + d[1];
})
.attr("x", function(d) {
return xScale(d[0]);
})
.attr("y", function(d) {
return yScale(d[1]);
})
.attr("font-family", "sans-serif")
.attr("font-size", "11px")
.attr("fill", "red");
</script>
d3.scaleLinear() 的其他常用 methods
nice() : 在映射到 range() 時,把兩端的值擴展到最接近的短數值,ex: [0.20147987687960267, 0.996679553296417] -> [0.2, 1]
rangeRound() : 取代 range,會將比例尺輸出為最接近的整數
clamp() : scale 預設可以傳回超過範圍之外的值,如果加上 clamp(true),會讓超過範圍的數值,變成範圍的最高或最低值
其他的比例尺
sqrt : 平方根比例尺
pow : 冪比例尺,指數變化的 dataset
log : 對數比例尺
quantize : 輸出範圍為獨立的值的線性比例尺,適合把資料分類的情形
ordinal : 使用非定量值(ex: 類別名稱) 作為輸出的序數比例尺,非常適合比較蘋果和橘子
d3.scale.category10() d3.scale.category20() d3.scale.category20b() d3.scale.category20c() : 能夠輸出10到20種類別顏色的預設序數比例尺
d3.time.scale() : 針對日期和時間值的一個比例尺方法,可以對日期刻度進行特殊處理
軸 x & y axis
呼叫 axis method 不會有回傳值,而是產生與 axis 相關的可見元素,例如 軸線、標籤與刻度。axis method 只適用於 svg 圖形。
- axis 在 v3 跟 v4 的差異
// v3: Define X axis
var xAxis = d3.svg.axis()
.scale(xScale)
.orient("bottom");
// v4: Define X axis
var xAxis = d3.axisBottom()
.scale(xScale);
- 產生 x axis 的方法,先以 xScale 比例尺產生 xAxis,然後在 svg 後面加上 g 元素,並呼叫 call(xAxis)。 g 是個 grouping 元素,有兩種用途 (1) 包含其他元素 (2) 對整個分組應用進行變換
//Define X axis
var xAxis = d3.axisBottom()
.scale(xScale);
//Create X axis
svg.append("g")
.call(xAxis);
也可以合併成一行
//Create X axis
svg.append("g")
.call(d3.axisBottom().scale(xScale));
上面這個產生的 x 軸,是出現在圖形的上方,如果要換到下面,要利用平移 transform 移動
//Define X axis
var xAxis = d3.axisBottom()
.scale(xScale);
svg.append("g")
.attr("class", "axis")
.attr("transform", "translate(0," + (h - padding) + ")")
.call(xAxis);
可以為 axis 利用 css 進行視覺調整
<style type="text/css">
.axis path,
.axis line {
fill: none;
stroke: black;
shape-rendering: crispEdges;
}
.axis text {
font-family: sans-serif;
font-size: 11px;
}
</style>
- tick 刻度
D3 會自動根據 scale 計算 ticks,也可以呼叫 ticks(5) 改為 5 個刻度線,但實際上 tick 只是個參考值,不是絕對值,D3 會自動調整為最適當的又接近 tick 數值的刻度數量。
//Define X axis
var xAxis = d3.axisBottom()
.scale(xScale)
.ticks(5);
- y 軸
為了讓 y 放在圖形的左邊,並產生 padding,還是需要 transform 進行平移
var padding = 30;
//Define X axis
var xAxis = d3.axisBottom()
.scale(xScale)
.ticks(5);
//Define Y axis
var yAxis = d3.axisLeft()
.scale(yScale)
.ticks(5);
//Create X axis
svg.append("g")
.attr("class", "axis")
.attr("transform", "translate(0," + (h - padding) + ")")
.call(xAxis);
//Create Y axis
svg.append("g")
.attr("class", "axis")
.attr("transform", "translate(" + padding + ",0)")
.call(yAxis);
- 格式化軸刻度
".1%" 就是將 200 顯示為 200.0%
var formatAsPercentage = d3.format(".1%");
//Define X axis
var xAxis = d3.axisBottom()
.scale(xScale)
.ticks(5)
.tickFormat(formatAsPercentage);
//Define Y axis
var yAxis = d3.axisLeft()
.scale(yScale)
.ticks(5)
.tickFormat(formatAsPercentage);
- 完整的軸線處理程式碼
<style type="text/css">
.axis path,
.axis line {
fill: none;
stroke: black;
shape-rendering: crispEdges;
}
.axis text {
font-family: sans-serif;
font-size: 11px;
}
</style>
<script type="text/javascript">
//Width and height
var w = 500;
var h = 300;
var padding = 30;
/*
//Static dataset
var dataset = [
[5, 20], [480, 90], [250, 50], [100, 33], [330, 95],
[410, 12], [475, 44], [25, 67], [85, 21], [220, 88],
[600, 150]
];
*/
//Dynamic, random dataset
var dataset = []; //Initialize empty array
var numDataPoints = 50; //Number of dummy data points to create
var xRange = Math.random() * 1000; //Max range of new x values
var yRange = Math.random() * 1000; //Max range of new y values
for (var i = 0; i < numDataPoints; i++) { //Loop numDataPoints times
var newNumber1 = Math.floor(Math.random() * xRange); //New random integer
var newNumber2 = Math.floor(Math.random() * yRange); //New random integer
dataset.push([newNumber1, newNumber2]); //Add new number to array
}
//Create scale functions
var xScale = d3.scaleLinear()
.domain([0, d3.max(dataset, function(d) { return d[0]; })])
.range([padding, w - padding * 2]);
var yScale = d3.scaleLinear()
.domain([0, d3.max(dataset, function(d) { return d[1]; })])
.range([h - padding, padding]);
var rScale = d3.scaleLinear()
.domain([0, d3.max(dataset, function(d) { return d[1]; })])
.range([2, 5]);
//Define X axis
var xAxis = d3.axisBottom()
.scale(xScale)
.ticks(5);
//Define Y axis
var yAxis = d3.axisLeft()
.scale(yScale)
.ticks(5);
//Create SVG element
var svg = d3.select("body")
.append("svg")
.attr("width", w)
.attr("height", h);
//Create circles
svg.selectAll("circle")
.data(dataset)
.enter()
.append("circle")
.attr("cx", function(d) {
return xScale(d[0]);
})
.attr("cy", function(d) {
return yScale(d[1]);
})
.attr("r", function(d) {
return rScale(d[1]);
});
/*
//Create labels
svg.selectAll("text")
.data(dataset)
.enter()
.append("text")
.text(function(d) {
return d[0] + "," + d[1];
})
.attr("x", function(d) {
return xScale(d[0]);
})
.attr("y", function(d) {
return yScale(d[1]);
})
.attr("font-family", "sans-serif")
.attr("font-size", "11px")
.attr("fill", "red");
*/
//Create X axis
svg.append("g")
.attr("class", "axis")
.attr("transform", "translate(0," + (h - padding) + ")")
.call(xAxis);
//Create Y axis
svg.append("g")
.attr("class", "axis")
.attr("transform", "translate(" + padding + ",0)")
.call(yAxis);
</script>
序數比例尺
序數就是有固定順序的一個數列,ex: 週一、週二、週三..,新生、大二、大三、大四
scaleBand 跟 scaleLinear 不同,使用的是離散的數據資料。
domain 用來指定輸入的值域
.domain([" 新生 ", " 大二 ", " 大三 ", " 大四 "])
.domain([0, 1, 2, 3, 4, 5, 6, 7, 8, 9,
10, 11, 12, 13, 14, 15, 16, 17, 18, 19])
.domain(d3.range(20))
.domain(d3.range(dataset.length))
range 用來做映射,前面是映射的範圍,第二個參數指定間距
.range([0,w], 0.2)
D3 的序數比例尺 ordinal,在 v3 跟 v4 有些 API 呼叫的差異。
// v3
var xScale = d3.scale.ordinal()
.domain(d3.range(dataset.length))
.rangeRoundBands([0, w], 0.05);
// v4
var xScale = d3.scaleBand()
.domain(d3.range(dataset.length))
.range([0, w], 0.05);
// v3
xScale.rangeBand()
// v4
xScale.bandwidth()
<script type="text/javascript">
//Width and height
var w = 600;
var h = 250;
var barPadding = 1;
var dataset = [ 5, 10, 13, 19, 21, 25, 22, 18, 15, 13,
11, 12, 15, 20, 18, 17, 16, 18, 23, 25 ];
// var xScale = d3.scale.ordinal()
// .domain(d3.range(dataset.length))
// .rangeRoundBands([0, w], 0.05);
var xScale = d3.scaleBand()
.domain(d3.range(dataset.length))
.range([0, w], 0.05);
var yScale = d3.scaleLinear()
.domain([0, d3.max(dataset)])
.range([0, h]);
//Create SVG element
var svg = d3.select("body")
.append("svg")
.attr("width", w)
.attr("height", h);
//Create bars
svg.selectAll("rect")
.data(dataset)
.enter()
.append("rect")
.attr("x", function(d, i) {
return xScale(i);
})
.attr("y", function(d) {
return h - yScale(d);
})
.attr("width", xScale.bandwidth()-barPadding)
.attr("height", function(d) {
return yScale(d);
})
.attr("fill", function(d) {
return "rgb(0, 0, " + (d * 10) + ")";
});
//Create labels
svg.selectAll("text")
.data(dataset)
.enter()
.append("text")
.text(function(d) {
return d;
})
.attr("text-anchor", "middle")
.attr("x", function(d, i) {
// xScale(i) 是傳回 index 為 i 的原始資料
return xScale(i) + xScale.bandwidth() / 2;
})
.attr("y", function(d) {
return h - yScale(d) + 14;
})
.attr("font-family", "sans-serif")
.attr("font-size", "11px")
.attr("fill", "white");
</script>
References
D3: Data-Driven Documents - Michael Bostock, Vadim Ogievetsky and Jeffrey Heer
《D3 API 詳解》隨書源碼 後面的 Refereces 有很多 D3.js 的網頁資源
用 D3.js v4 看 Pokemon 屬性表 D3.js v3 到 v4 的 migration 差異
沒有留言:
張貼留言