比例尺是 D3 中很重要的一個概念。繪制圖形時直接用數值的大小來代表像素不是一種好方法,本章正是要解決此問題。
一、為什么需要比例尺
上一章制作了一個柱形圖,當時有一個數組,繪圖時,直接使用 250 給矩形的寬度賦值,即矩形的寬度就是 250 個像素。此方式非常具有局限性,如果數值過大或過小,例如:
var dataset_1 = [ 2.5 , 2.1 , 1.7 , 1.3 , 0.9 ]; var dataset_2 = [ 2500, 2100, 1700, 1300, 900 ];
對以上兩個數組,絕不可能用 2.5 個像素來代表矩形的寬度,那樣根本看不見;也不可能用 2500 個像素來代表矩形的寬度,因為畫布沒有那么長。於是,我們需要一種計算關系,能夠:將某一區域的值映射到另一區域,其大小關系不變。這就是比例尺(Scale)。
二、有哪些比例尺
比例尺,很像數學中的函數。例如,對於一個一元二次函數,有 x 和 y 兩個未知數,當 x 的值確定時,y 的值也就確定了。在數學中,x 的范圍被稱為定義域,y 的范圍被稱為值域。
D3 中的比例尺,也有定義域和值域,分別被稱為 domain 和 range。開發者需要指定 domain 和 range 的范圍,如此即可得到一個計算關系。
D3 提供了多種比例尺,下面介紹最常用的兩種。
1、線性比例尺
線性比例尺,能將一個連續的區間,映射到另一區間。要解決柱形圖寬度的問題,就需要線性比例尺。
假設有以下數組,現有要求如下:將 dataset 中最小的值,映射成 0;將最大的值,映射成 300。代碼如下:
var dataset = [1.2, 2.3, 0.9, 1.5, 3.3]; var min = d3.min(dataset); var max = d3.max(dataset); var linear = d3.scale.linear() .domain([min, max])//注意:domain()/range()里面是個數組形式哦 .range([0, 300]); linear(0.9); //返回 0
linear(2.3); //返回 175
linear(3.3); //返回 300
其中,d3.scale.linear() 返回一個線性比例尺。domain() 和 range() 分別設定比例尺的定義域和值域。在這里還用到了兩個函數,它們經常與比例尺一起出現:
d3.max() 、和d3.min():這兩個函數能夠求數組的最大值和最小值,是 D3 提供的。
按照以上代碼:比例尺的定義域 domain 為:[0.9, 3.3],比例尺的值域 range 為:[0, 300]
因此,當輸入 0.9 時,返回 0;當輸入 3.3 時,返回 300。當輸入 2.3 時呢?返回 175,這是按照線性函數的規則計算的。
有一點請大家記住:d3.scale.linear() 的返回值,是可以當做函數來使用的。因此,才有這樣的用法:linear(0.9)。
2、序數比例尺
有時候,定義域和值域不一定是連續的。例如,有兩個數組:
var index = [0, 1, 2, 3, 4]; var color = ["red", "blue", "green", "yellow", "black"];
我們希望 0 對應顏色 red,1 對應 blue,依次類推。但是,這些值都是離散的,線性比例尺不適合,需要用到序數比例尺。
var ordinal = d3.scale.ordinal() .domain(index) .range(color); ordinal(0); //返回 red
ordinal(2); //返回 green
ordinal(4); //返回 black
序數比例尺:d3.scale.ordinal();用法與線性比例尺是類似的。
三、坐標軸由什么構成
坐標軸,是可視化圖表中經常出現的一種圖形,由一些列線段和刻度組成。坐標軸在 SVG 中是沒有現成的圖形元素的,需要用其他的元素組合構成。D3 提供了坐標軸的組件,如此在 SVG 畫布中繪制坐標軸變得像添加一個普通元素一樣簡單。
在 SVG 畫布的預定義元素里,有六種基本圖形:
- 矩形 <rect>
- 圓形 <circle>
- 橢圓 <ellipse>
- 線段 <line>
- 折線 <polyline>
- 多邊形 <polygon>
另外,還有一種比較特殊,也是功能最強的元素:
- 路徑 <path>
畫布中的所有圖形,都是由以上七種元素組成。
顯然,這里面沒有坐標軸 <axis> 這種元素。如果有的話,我們可以采用類似以下的方式定義:<axis x1="" x2="" ...></axis>,很可惜,沒有這種元素。但是,這種設計是合理的:不可能為每一種圖形都配備一個單獨的元素,那樣 SVG 就會過於龐大。
因此,我們需要用其他元素來組合成坐標軸,最終使其變為類似以下的形式:
<g>
<!-- 第一個刻度 -->
<g>
<line></line> <!-- 第一個刻度的直線 -->
<text></text> <!-- 第一個刻度的文字 -->
</g>
<!-- 第二個刻度 -->
<g>
<line></line> <!-- 第二個刻度的直線 -->
<text></text> <!-- 第二個刻度的文字 -->
</g> ... <!-- 坐標軸的軸線 -->
<path></path>
</g>
分組元素 <g>,是 SVG 畫布中的元素,意思是 group。此元素是將其他元素進行組合的容器,在這里是用於將坐標軸的其他元素分組存放。
如果需要手動添加這些元素就太麻煩了,為此,D3 提供了一個組件:d3.svg.axis()。它為我們完成了以上工作。
四、定義坐標軸
上一章提到了比例尺的概念,要生成坐標軸,需要用到比例尺,它們二者經常是一起使用的。下面,在上一章的數據和比例尺的基礎上,添加一個坐標軸的組件。
//數據
var dataset = [ 2.5 , 2.1 , 1.7 , 1.3 , 0.9 ]; //定義比例尺
var linear = d3.scale.linear() .domain([0, d3.max(dataset)]) .range([0, 250]); var axis = d3.svg.axis() .scale(linear) //指定比例尺
.orient("bottom") //指定刻度的方向
.ticks(7); //指定刻度的數量
主要看下定義坐標軸的方法,其中使用了線性比例尺 linear。其中:
d3.svg.axis():D3 中坐標軸的組件,能夠在 SVG 中生成組成坐標軸的元素。
scale():指定比例尺。
orient():指定刻度的朝向,bottom 表示在坐標軸的下方顯示。
ticks():指定刻度的數量。
五、在 SVG 中添加坐標軸
定義了坐標軸之后,只需要在 SVG 中添加一個分組元素 <g>,再將坐標軸的其他元素添加到這個 <g> 里即可。代碼如下:
svg.append("g") .call(axis);
上面有一個 call() 函數,其參數是前面定義的坐標軸 axis。在 D3 中,call() 的參數是一個函數。調用之后,將當前的選擇集作為參數傳遞給此函數。也就是說,以下兩段代碼是相等的。
function foo(selection) { selection .attr("name1", "value1") .attr("name2", "value2"); } foo(d3.selectAll("div")); //等價於
d3.selectAll("div").call(foo); svg.append("g").call(axis); //等價於
axis(svg.append(g));
六、設定坐標軸的樣式和位置
默認的坐標軸樣式不太美觀,下面提供一個常見的樣式:
<style> .axis path, .axis line{ fill: none; stroke: black; shape-rendering: crispEdges; } .axis text { font-family: sans-serif; font-size: 11px; } </style>
分別定義了類 axis 下的 path、line、text 元素的樣式。接下來,只需要將坐標軸的類設定為 axis 即可。
坐標軸的位置,可以通過 transform 屬性來設定。通常在添加元素的時候就一並設定,寫成如下形式:
svg.append("g") .attr("class","axis") .attr("transform","translate(20,130)") .call(axis);
七、應用
<html>
<head>
<meta charset="utf-8">
<title>比例尺和坐標軸</title>
<style> .axis path, .axis line{ fill: none; stroke: black; shape-rendering: crispEdges; } .axis text { font-family: sans-serif; font-size: 11px; } </style>
</head>
<body>
<script src="http://d3js.org/d3.v3.min.js" charset="utf-8"></script>
<script>
var width = 300; //畫布的寬度
var height = 300; //畫布的高度
var svg = d3.select("body") //選擇文檔中的body元素
.append("svg") //添加一個svg元素
.attr("width", width) //設定寬度
.attr("height", height); //設定高度
var dataset = [ 2.5 , 2.1 , 1.8 , 1.3 , 0.9 ]; //數據(表示矩形的寬度)
var linear = d3.scale.linear() .domain([0,d3.max(dataset)]) .range([0,250]); var rectHeight = 25; //每個矩形所占的像素高度(包括空白)
svg.selectAll("rect") .data(dataset) .enter() .append("rect") .attr("x",20) .attr("y",function(d,i){ return i * rectHeight; }) .attr("width",function(d){ return linear(d); // 此處應用比例尺
}) .attr("height",rectHeight-2) .attr("fill","steelblue"); //添加坐標軸
var axis = d3.svg.axis() .scale(linear) .orient("bottom") .ticks(7); svg.append("g") .attr("class","axis") .attr("transform","translate(20,130)") .call(axis); </script>
</body>
</html>