D3JsDay11 观测时候别铁齿,拿出你的比例尺

前言

想像一下假设今天你的资料的数字是如此庞大,而电脑萤幕的宽和高却是有限的情况之下,不可能以1个人口对应1个萤幕的高或是宽,例如我们的资料是各个国家的人口数,例如台湾两千三百万的数字、美国三亿多、日本一亿多、加拿大三千多万,当我们的资料的数字是如此庞大的情况,想要制作长条图,不太可能是div有4亿px的height或是svg的width设定成两千三百万,因此我们势必得进行资料转换,将这些数字转换对应到我们萤幕上的宽、高。

d3.scaleLinear()线性比例转换

首先先使用d3.scaleLinear()来创造一个线性比例尺,并且由domain()定义原始输入的范围,再由range()定义一个输出後的范围。例如我们一公里等於一千公尺,因此我们可以设定如下的程序码。

let Km_Transform_M = d3.scaleLinear().domain([0,1]).range([0,1000]);
console.log(Km_Transform_M(1.35));

这时候我们将值1.35带入之後就会看到转换後的数字

当然我们甚至也可以拿来做温度的转换,由於华氏和摄氏温度也是线性关系,因此我们方程序改成如下

  let temC_Transform_temF  = d3.scaleLinear().domain([0,100]).range([32,212]);
  console.log(temC_Transform_temF(30));

这时候你就可以告诉大家我用d3Js的资料视觉化套件可以做出温度转换了 (其他人表示:???)
我们也可以引用国中y=ax+b 学到的一元二次方程序理解这段程序码,x代入0、y带入32和x带入100、y代入212解二元一次联立方程序的a和b就可以知道这个线性转换方程序了,当然这些并不是这一章节主要的内容,以上只是帮助你理解整个程序码的运作。

clamp()函式

clamp翻成中文有夹住、强制执行等等的意思,你可以想像一下原本资料进行转换的时候我们设定range()是0到1000正常来说他会依比例进行转换,也就是说如果输入1.35会转换成1350的数字,但是我们的range设定的是1000却超出范围了,因此可以使用这个函式, clamp()可以比喻成把资料夹住在这个范围里面或是强制执行在这个范围里面此时,当你输入的值转换後超出range的最大值的时候,将会一律以最大值呈现,参见以下程序码

let Km_Transform_M = d3.scaleLinear()
                      .domain([0,1])
                      .range([0,1000])
                      .clamp(true);
                      
console.log(Km_Transform_M(12));

clamp()的参数需带入布林值true或是false,如果设置true之後超过的部分将会以最大值来呈现。以刚刚的例子
这时候只要你输入超过1的数字,转换後的数字都会是最大值1000,如果未设定的情况下,表示预设是false,这个函式的作用可以用来避免画出来的图形超出你预期的空间。

接下来我会尝试着导入真实的资料画出长条图,这边的资料用政府开放平台的人口资料参见以下网址

政府开放平台资料各乡镇人口密度

这边先将109年的资料下载下来,之後和html档案与json档案放在同一个资料夹并且改名成populationDensity.json以便比较好识别,由於预计仅画出台北市的资料所以这边接完的资料先进行资料前处理以便操作,这边实际画出图表会使用箭头函式来简化撰写

第一个then所包含的是将主要的各个乡镇内容的阵列取出

第二个then是透过正则表达式(regular expression literal)来过滤资料,把符合正则规则的资料筛选出来

最後一个then处理人口密度、人口总数、乡镇市的面积,由於原始资料的内容是字串,这边使用Number()将它转型成数字site_id资料内容是台北市大安区、台北市士林区,每笔资料内容都包含了台北市,因此希望能够简化它,所以将取第四个字开始,长条图的bar预计留50给之後要放各个区文字的空间,因此先padding设定50

小补充

如果对於箭头函式和正则表达式不熟的人可以参考

箭头函式MDN介绍
正规表达式MDN介绍
Number()MDN函式介绍

d3.json("populationDensity.json")
  .then((data) => {
    return data.result.records;
  })
  .then((data) => {
    const reg = RegExp(/台北市/);
    return data.filter((el) => {
      return reg.test(el.site_id);
    });
  })
  .then((taipei) => {
     const newTaipei = taipei.map((el) => {
      el.people_total = Number(el.people_total);
      el.area = Number(el.area);
      el.population_density = Number(el.population_density);
      el.site_id = el.site_id.substr(3);
      return el;
    });
      let padding = 50;
      const svg = d3.select("body")
                  .append("svg")
                  .attr("width", 800)
                  .attr("height", 450);
    });

绘制比例尺常用的函式min() max()

由於绘制比例尺的时候我们往往不太知道要设domain为多少,所以我们通常必须先知道所有资料当中最大值最小值来构想预计要从多少来缩放比例,这边资料只有十二笔虽然用眼睛稍微扫描一下就可以得知资料最大和最小值是谁,但是如果当资料上万笔的时候不太可能用这方式来找出来,d3也提供了一些函式来处理这个问题以下作介绍

min()函式带入两个参数,第一个参数带入阵列,如果阵列内容是一个物件的话可以带入第二个参数,参数内容是函式可以撰写你要筛选的资料是什麽,这边就以下范例

let min = d3.min(newTaipei, (d) => d.people_total);
let max = d3.max(newTaipei, (d) => d.people_total);
console.log(max);

max()min()大同小异,这边不多述,另外值得一提的地方官方文件内有提到不像Math.min,如果资料当中有一些null或者underfined和NaN的时候将会自动忽略,这对资料遗失时候的情况十分有用。

https://ithelp.ithome.com.tw/upload/images/20210926/20125095UBVKRsGx3j.png

d3官方API Min()

接下来我们得到最大值302644和最小值是118758,因此我们设置大小如下

至於为什麽range()要使用4000的关系,之後下个章节在介绍座标轴的时候会提到原因。

https://ithelp.ithome.com.tw/upload/images/20210926/20125095DQ3UBAry8g.png

let scaleY = d3
  .scaleLinear()
  .domain([0, 320000])
  .range([400, 0]);

绘制长条

接下来我们将会画出长方形来作为资料的大小值程序码如下

   svg.selectAll("rect")
      .data(newTaipei)
      .join("rect")
      .attr("x", (d, i) => {
        return padding + i * 60;
      })
      .attr("y", (d) => {
        return scaleY(d.people_total);
      })
      .attr("width", 50)
      .attr("height", (d) => {
        return 400 - scaleY(d.people_total);
      })
      .attr("fill", "orange");

我们将资料放入rect当中之後的x起始点先加入padding往右移,然後根据索引值再绘制出每个rect长方形的时候再向右移60来当作起始点,而y的部分使用刚刚所做的scaleY()函式来进行资料的转换,转换的数值设为y的起始点,接下来宽设定50,x当时是根据索引值i设置60当起始点,而长方形的宽是设置50所以他们之间的间距自然而然就是60-50=10,最後再渲染出高的时候是使用400-scaleY(d.people_total),因为刚刚的scaleYrange()起始是400结束点是0所以原先的资料越大转换之後的数字越小,所以使用400减去它,自然而然就可以表示原始资料所对应的大小了,最後我们填充橘色作为这个长条图的颜色。

接下来你应该会看到如下图

https://ithelp.ithome.com.tw/upload/images/20210926/20125095AU3XIj9elo.png

绘制乡镇区的名称

接下来我们将各个乡镇区带入,一样先选取整个text然後将资料给放入,里面使用函式参数来return 乡镇区的字串,为了对齐刚刚所制成的长条图,决定x起始点位置和长条图的内容将会是一样,最後减去20的原因是为了让它更靠近长条图,使间距缩小。

svg.selectAll("text")
    .data(newTaipei)
    .join("text")
    .text((d) => {
        return d.site_id;
    })
    .attr("x", (d, i) => {
        return padding + i * 60;
    })
    .attr("y", (y) => {
        return 450 - 20;
    });

完整程序码如下

d3.json("populationDensity.json")
.then((data) => {
    return data.result.records;
})
.then((data) => {
  let reg = RegExp(/台北市/);
  return data.filter((el) => {
      return reg.test(el.site_id);
  });
})
.then((taipei) => {
  const newTaipei = taipei.map((el) => {
      el.site_id = el.site_id.substr(3);
      return el;
  });
  let padding = 50;
  const svg = d3.select("body")
              .append("svg")
              .attr("width", 800)
              .attr("height", 450);
  let min = d3.min(newTaipei, (d) => d.people_total);
  let max = d3.max(newTaipei, (d) => d.people_total);

  const scaleY = d3
  .scaleLinear()
  .domain([0, 320000])
  .range([400, 0]);

  svg.selectAll("rect")
  .data(newTaipei)
  .join("rect")
  .attr("x", (d, i) => {
      return padding + i * 60;
  })
  .attr("y", (d) => {
    console.log(scaleY(d.people_total));
    return scaleY(d.people_total);
  })
  .attr("width", 50)
  .attr("height", (d) => {
    return 400 - scaleY(d.people_total);
  })
  .attr("fill", "orange");

  svg.selectAll("text")
  .data(newTaipei)
  .join("text")
  .text((d) => {
      return d.site_id;
  })
  .attr("x", (d, i) => {
      return padding + i * 60;
  })
  .attr("y", (y) => {
      return 450 -20;
  });
})

最後完成如图
https://ithelp.ithome.com.tw/upload/images/20210926/20125095eZBxEdJp15.png

实际页面如下

gihab页面

gitHubPage页面


<<:  【Day 13】逻辑回归(Logistic Regression)(下)

>>:  [Day24] 在 Codecademy 学 React ~ 终於来到 Hook 的世界 ‧ useState 篇 (1)

#0. 前言+环境配置

前言 Hi!我是SingYo,谢谢你点进来看这个系列! 这是我第一次参加铁人赛。 其实说30个前端「...

I Want To Know React - Context 范例 & 使用技巧

回顾 Context 语法 在上一章节中,我们介绍了 Context 的使用简介与语法。 React...

最短路径问题 (5)

10.6 Dijkstra’s SSSP 演算法 如果我们只在乎从某个点 s 出发到所有点的距离,而...

30天轻松学会unity自制游戏-往前移动

用最简单的方式Ctrl+C&Ctrl+V把场景往上延伸,Ctrl+D也可以直接复制此物件,看...

虹语岚访仲夏夜-23(专业的小四篇)

万里无云时 总觉得喘不过气 要问为什麽 一定是 无云天空下 只有我问我    该往那去 寂静夜深时...