Population Pyramid
聊一聊人口金字塔圖。
人口金字塔是按人口年齡和性別表示人口分佈的特種塔狀條形圖,是形象地表示某一人口的年齡和性別構成的圖形。——百度百科
一般的人口金字塔圖如下圖所示:
例如上圖表示,2011年利比亞男女不同年齡階段的比例分佈情況。
而本篇要講的Population Pyramid圖,將男女人口數據畫在了座標軸的同一邊,通過柱狀圖的覆蓋來看不同年齡階段的男女比例分佈情況,如下圖所示:
圖中用粉色來標識女性的數據、藍色標識男性的數據,數據重疊部分,由於粉色和藍色重疊而呈現出紫色,例如70歲的人羣當中,女性的比例比男性的多;而20歲的人羣當中,男性的比例比女性的多。
接下來詳細解釋D3.js是如何實現這張人口金字塔圖的。
index.html——源碼
<!DOCTYPE html>
<meta charset="utf-8">
<style>
svg {
font: 10px sans-serif;
}
.y.axis path {
display: none;
}
.y.axis line {
stroke: #fff;
stroke-opacity: .2;
shape-rendering: crispEdges;
}
.y.axis .zero line {
stroke: #000;
stroke-opacity: 1;
}
.title {
font: 300 78px Helvetica Neue;
fill: #666;
}
.birthyear,
.age {
text-anchor: middle;
}
.birthyear {
fill: #fff;
}
rect {
fill-opacity: .6;
fill: #e377c2;
}
rect:first-child {
fill: #1f77b4;
}
</style>
<body>
<script src="//d3js.org/d3.v3.min.js"></script>
<script>
// 定義相關尺寸
// margin定義svg畫圖的上 、右、下、左的外邊距
var margin = {top: 20, right: 40, bottom: 30, left: 20},
// 計算寬度
width = 960 - margin.left - margin.right,
// 計算高度
height = 500 - margin.top - margin.bottom,
// 計算柱狀條的寬度,其中19由於分了19個年齡段
barWidth = Math.floor(width / 19) - 1;
// 爲x軸定義線性比例尺,值域range的定義可以看出,x軸的刻度尺都會位於柱狀圖的底部中間位置
var x = d3.scale.linear()
.range([barWidth / 2, width - barWidth / 2]);
// 爲y軸定義線性比例尺,值域爲height到0
var y = d3.scale.linear()
.range([height, 0]);
// 定義y座標軸
var yAxis = d3.svg.axis()
// 設置y軸的比例尺
.scale(y)
// y軸座標刻度文字在右側
.orient("right")
// 這裏設置爲“-width”,個人理解爲,y軸刻度線本應該在軸的右邊,設置爲負數,刻度線繪製在y軸的左邊
// 而且刻度線的長度爲圖形的寬度,表現在圖上就是那些橫穿柱狀條的白色線,看不見白色線的部分是因爲
// 圖背景和刻度線都是白色
.tickSize(-width)
// 設置y軸刻度的格式
.tickFormat(function(d) { return Math.round(d / 1e6) + "M"; });
// An SVG element with a bottom-right origin.
// 定義svg畫布
var svg = d3.select("body").append("svg")
// 設置svg畫布的寬度
.attr("width", width + margin.left + margin.right)
// 設置svg畫布的高度
.attr("height", height + margin.top + margin.bottom)
.append("g")
// 定位svg畫布
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
// A sliding container to hold the bars by birthyear.
// 定義表示 出生年 的元素
var birthyears = svg.append("g")
.attr("class", "birthyears");
// A label for the current year.
// 繪製當年的年份文字,即圖中左上角的 2000字樣
var title = svg.append("text")
.attr("class", "title")
.attr("dy", ".71em")
.text(2000);
// 處理數據
d3.csv("population.csv", function(error, data) {
// Convert strings to numbers.
// 將csv數據文件中的pepole,yaer,age字段的值轉換成數字類型
data.forEach(function(d) {
d.people = +d.people;
d.year = +d.year;
d.age = +d.age;
});
// Compute the extent of the data set in age and years.
// 計算年齡和年份數據集的範圍
// 獲取最大年齡
var age1 = d3.max(data, function(d) { return d.age; }),
// 獲取最小年份
year0 = d3.min(data, function(d) { return d.year; }),
// 獲取最大年份
year1 = d3.max(data, function(d) { return d.year; }),
// 設置year爲最大年份
year = year1;
// Update the scale domains.
// 上面在定義x,y的比例尺時沒有設置“定義域”,此處開始設置
// 設置x比例尺的定義域,可以看出,x軸表示年齡的變化
x.domain([year1 - age1, year1]);
// 設置y比例尺的定義域,可以看出,y軸表示人口數量的變化
y.domain([0, d3.max(data, function(d) { return d.people; })]);
// Produce a map from year and birthyear to [male, female].
// d3.nest()函數用來將數據分組爲任意層次結構
// d3.nest().key(fun)用來對每數據以fun函數返回的鍵值來進行分組,此處以year來進行分組
// 後,返回的是以year作爲鍵的不同的數組;再以year-age作爲鍵值進行第二次分組;
// rollup()函數將用返回的值d.people來替換key所對應的值
// d3.nest().map()返回最終的分組後的層次結構的數據
// 可以通過在瀏覽器中調試狀態下看到最終返回的data數組是以年份進行第一層分組,每個年份下又以
// d.year -d.age進行了第二層的分組,第二層分組對應的數據爲rollup中指定的d.people。
data = d3.nest()
.key(function(d) { return d.year; })
.key(function(d) { return d.year - d.age; })
.rollup(function(v) { return v.map(function(d) { return d.people; }); })
.map(data);
// Add an axis to show the population values.
// 繪製y軸
svg.append("g")
.attr("class", "y axis")
// 將y軸定位到畫布右側
.attr("transform", "translate(" + width + ",0)")
// 對該g元素執行yAxis定義的操作
.call(yAxis)
.selectAll("g")
// 篩選出 value爲空的
.filter(function(value) { return !value; })
// 將篩選出的value爲空的元素,爲期添加zero樣式類
.classed("zero", true);
// Add labeled rects for each birthyear (so that no enter or exit is required).
// 爲表示出生年份的元素綁定數據,定義年份步長爲5年
var birthyear = birthyears.selectAll(".birthyear")
.data(d3.range(year0 - age1, year1 + 1, 5))
.enter().append("g")
.attr("class", "birthyear")
// 定位年份的位置,通過上面定義的x()比例尺函數來計算
.attr("transform", function(birthyear) { return "translate(" + x(birthyear) + ",0)"; });
// 繪製柱狀條
birthyear.selectAll("rect")
// 獲取2000這一年裏,出生年份爲birthyear的分組
.data(function(birthyear) { return data[year][birthyear] || [0, 0]; })
.enter().append("rect")
.attr("x", -barWidth / 2)
.attr("width", barWidth)
// 設置y位置通過y比例尺來計算
.attr("y", y)
// 設置柱狀條的高度
.attr("height", function(value) { return height - y(value); });
// Add labels to show birthyear.
// 添加出生年份文字
birthyear.append("text")
.attr("y", height - 4)
.text(function(birthyear) { return birthyear; });
// Add labels to show age (separate; not animated).
// 添加年齡文字
svg.selectAll(".age")
// 爲年齡文字綁定數據,年齡步長爲5
.data(d3.range(0, age1 + 1, 5))
.enter().append("text")
.attr("class", "age")
.attr("x", function(age) { return x(year - age); })
.attr("y", height + 4)
.attr("dy", ".71em")
.text(function(age) { return age; });
// Allow the arrow keys to change the displayed year.
// 通過方向鍵“←”和“→”來查滑動年份窗口,查看更多年份的人口分佈情況
// 用focus()方法可把鍵盤焦點給予當前窗口
window.focus();
//爲方向鍵“←”和“→”操作綁定動作
d3.select(window).on("keydown", function() {
switch (d3.event.keyCode) {
// 若爲向左←,則將當前年份倒退10年
case 37: year = Math.max(year0, year - 10); break;
// 若爲向右→,則將當前年份向前推進10年
case 39: year = Math.min(year1, year + 10); break;
}
// 對圖進行更新
update();
});
// 定義更改年份窗口後,對圖進行更新的操作
function update() {
// 若更改年份窗口後,data中無當前年份的數據,則不進行任何操作,直接返回
if (!(year in data)) return;
// 託更改年份窗口後,data中有當前年份數據,則首先更新左上角顯示的年份
title.text(year);
// 更新出生年份,此處定義更新過渡動畫
birthyears.transition()
// 動作持續750毫秒
.duration(750)
// 定義更新動作
.attr("transform", "translate(" + (x(year1) - x(year)) + ",0)");
// 更新柱狀條
birthyear.selectAll("rect")
// 綁定新的年份窗口數據
.data(function(birthyear) { return data[year][birthyear] || [0, 0]; })
// 定義過渡動畫
.transition()
.duration(750)
.attr("y", y)
.attr("height", function(value) { return height - y(value); });
}
});
</script>
至此,人口金字塔圖的實現解釋完畢。實現此人口金字塔圖的重點:
一是通過d3.nest()數據處理方法,對官網給出的population.csv中的數據進行分組處理。
二是x,y座標軸的刻度的計算方法。
今天儘管陽光明媚,但是空氣冷涼,適合坐在室內窗邊安安靜靜地就很美好。