Population Pyramid
聊一聊人口金字塔圖。
人口金字塔是按人口年齡和性别表示人口分布的特種塔狀條形圖,是形象地表示某一人口的年齡和性别構成的圖形。——百度百科
一般的人口金字塔圖如下圖所示:
例如上圖表示,2011年利比亞男女不同年齡階段的比例分布情況。
而本篇要講的Population Pyramid圖,将男女人口資料畫在了坐标軸的同一邊,通過柱狀圖的覆寫來看不同年齡階段的男女比例分布情況,如下圖所示:
圖中用粉色來辨別女性的資料、藍色辨別男性的資料,資料重疊部分,由于粉色和藍色重疊而呈現出紫色,例如70歲的人群當中,女性的比例比男性的多;而20歲的人群當中,男性的比例比女性的多。
接下來詳細解釋D3.js是如何實作這張人口金字塔圖的。
index.html——源碼
<!DOCTYPE html>
<meta charset="utf-8">
<style>
svg {
font: px sans-serif;
}
.y.axis path {
display: none;
}
.y.axis line {
stroke: #fff;
stroke-opacity: .;
shape-rendering: crispEdges;
}
.y.axis .zero line {
stroke: #000;
stroke-opacity: ;
}
.title {
font: px Helvetica Neue;
fill: #666;
}
.birthyear,
.age {
text-anchor: middle;
}
.birthyear {
fill: #fff;
}
rect {
fill-opacity: .;
fill: #e377c2;
}
rect:first-child {
fill: #1f77b4;
}
</style>
<body>
<script src="//d3js.org/d3.v3.min.js"></script>
<script>
// 定義相關尺寸
// margin定義svg畫圖的上 、右、下、左的外邊距
var margin = {top: , right: , bottom: , left: },
// 計算寬度
width = - margin.left - margin.right,
// 計算高度
height = - margin.top - margin.bottom,
// 計算柱狀條的寬度,其中19由于分了19個年齡段
barWidth = Math.floor(width / ) - ;
// 為x軸定義線性比例尺,值域range的定義可以看出,x軸的刻度尺都會位于柱狀圖的底部中間位置
var x = d3.scale.linear()
.range([barWidth / , width - barWidth / ]);
// 為y軸定義線性比例尺,值域為height到0
var y = d3.scale.linear()
.range([height, ]);
// 定義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 / ) + "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();
// 處理資料
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([, 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 + , ))
.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] || [, ]; })
.enter().append("rect")
.attr("x", -barWidth / )
.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 - )
.text(function(birthyear) { return birthyear; });
// Add labels to show age (separate; not animated).
// 添加年齡文字
svg.selectAll(".age")
// 為年齡文字綁定資料,年齡步長為5
.data(d3.range(, age1 + , ))
.enter().append("text")
.attr("class", "age")
.attr("x", function(age) { return x(year - age); })
.attr("y", height + )
.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 : year = Math.max(year0, year - ); break;
// 若為向右→,則将目前年份向前推進10年
case : year = Math.min(year1, year + ); break;
}
// 對圖進行更新
update();
});
// 定義更改年份視窗後,對圖進行更新的操作
function update() {
// 若更改年份視窗後,data中無目前年份的資料,則不進行任何操作,直接傳回
if (!(year in data)) return;
// 托更改年份視窗後,data中有目前年份資料,則首先更新左上角顯示的年份
title.text(year);
// 更新出生年份,此處定義更新過渡動畫
birthyears.transition()
// 動作持續750毫秒
.duration()
// 定義更新動作
.attr("transform", "translate(" + (x(year1) - x(year)) + ",0)");
// 更新柱狀條
birthyear.selectAll("rect")
// 綁定新的年份視窗資料
.data(function(birthyear) { return data[year][birthyear] || [, ]; })
// 定義過渡動畫
.transition()
.duration()
.attr("y", y)
.attr("height", function(value) { return height - y(value); });
}
});
</script>
至此,人口金字塔圖的實作解釋完畢。實作此人口金字塔圖的重點:
一是通過d3.nest()資料處理方法,對官網給出的population.csv中的資料進行分組處理。
二是x,y坐标軸的刻度的計算方法。
今天盡管陽光明媚,但是空氣冷涼,适合坐在室内窗邊安安靜靜地就很美好。