天天看點

D3.js中Population Pyramid詳解Population Pyramidindex.html——源碼

Population Pyramid

聊一聊人口金字塔圖。

人口金字塔是按人口年齡和性别表示人口分布的特種塔狀條形圖,是形象地表示某一人口的年齡和性别構成的圖形。——百度百科

一般的人口金字塔圖如下圖所示:

D3.js中Population Pyramid詳解Population Pyramidindex.html——源碼

例如上圖表示,2011年利比亞男女不同年齡階段的比例分布情況。

而本篇要講的Population Pyramid圖,将男女人口資料畫在了坐标軸的同一邊,通過柱狀圖的覆寫來看不同年齡階段的男女比例分布情況,如下圖所示:

D3.js中Population Pyramid詳解Population Pyramidindex.html——源碼

圖中用粉色來辨別女性的資料、藍色辨別男性的資料,資料重疊部分,由于粉色和藍色重疊而呈現出紫色,例如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坐标軸的刻度的計算方法。

今天盡管陽光明媚,但是空氣冷涼,适合坐在室内窗邊安安靜靜地就很美好。