D3.js中Population Pyramid詳解

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座標軸的刻度的計算方法。
今天儘管陽光明媚,但是空氣冷涼,適合坐在室內窗邊安安靜靜地就很美好。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章