D3JsDay23 三枪侠的电磁炮,三个变数的气泡—气泡图(上)

气泡图介绍

昨天已经介绍完散布图了,大致上与散布图的作法大同小异,差别在於气泡本身也就是circle,也能呈现一个变数值,在svg里面我们将设定其属性r来定义气泡的半径大小。

本日预计使用实价登录网站的台南交易资料来实作一个气泡图、其中x轴表示售出时的建物面积、y轴表示售出时的土地面积,其气泡的大小表示成交价格。

注意显示资料的半径R值

为了肉眼辨别视觉差异,让使用者观看面积有总价上的感觉,因此R必须再开平方根避免渲染出来的图形造成次方倍的扭曲解读。

这边使用数学式子来稍微讲解一下关系

关系表格如下假设半径是r

半径与面积关系如下
r:πr²

半径( r ) 1 2 3 4
半径平方r² 1 4 9 16
圆面积πr² π 16π

套用到上述的例子当中,
半径1的半径平方是1圆面积是π
半径1的半径平方是4圆面积是4π
写数学式子如下

1:π = 4:4π = 9:9π

故圆面积是与半径平方成正比

功能说明

  • 可以使用下拉式选单切换行政区域後重绘成该行政区的图表
  • 使用者可以输入土地面积和建物面积的范围来後,点下轴线更新按钮重绘图表
  • 当滑鼠滑入到某个圆点的时候可以显示该笔资料的相关资讯

程序构想浅谈

  • 我们要有一个下拉式选单和四个input栏位来输入x轴、y轴的范围并加上一个轴线更新的按钮
  • 我们下拉式选单的行政区内容是经由d3Js筛选出乡镇市地区後动态新增至选单里
  • 我们多设置一个ScaleR将总价格转换成圆的半径,之後再以Math.sqrt()将其半径开平方根,以便绘图完的圆形面积比与房屋价格比相同,与上一篇文章大同小异会是设置scaleX和scaleY来进行资料转换至svg的座标点位置。

撰写html点击按钮

<div class="wrap">
    <select id="district">
    </select>
    <div class="area-str">建物面积最小值</div>
    <input type="number" id="min-bulid" value=0>
    <span>平方公尺</span>
    <div class="area-str">建物面积最大值</div>
    <input type="number" id="max-bulid" value=300>
    <span>平方公尺</span>
    <div class="area-str">土地面积最大值</div>
    <input type="number" id="max-land" value=500>
    <span>平方公尺</span>
    <div class="area-str">土地面积最小值</div>
    <input type="number" id="min-land" value=0>
    <span>平方公尺</span>
    <button id="btn">轴线更新</button>
</div>

这边在input使用id属性以便日後要使用js选取时更为方便。

动态插入至select选单

const groupData = d3.group(data,d=>d["乡镇市区"]);
groupData.delete("The villages and towns urban district");
console.log(groupData);
const districtAry = [...groupData.keys()];
let defaultDistrict = districtAry[0];
    for (let i=0;i<districtAry.length;i++) {
        d3.select("#district").append("option").text(districtAry[i]);
}
  1. 将资料根据乡镇市区的栏位进行群组化
  2. 由於群组化後的资料第一笔是实价登录网用来说明的内容,因此使用Map.delet()方法将其删除
  3. 可以观看console.log()观看群组化後的资料样貌如下图
  4. 其透过Map.keys()的方法将资料展开後再次包装成阵列的样貌存入districtAry变数里面,另外选定阵列当中的第一笔当作预设行政区预计让使用者载入的时候可以先行看到画面
  5. 最後使用for回圈将option内容插入至id为district的元素

如果不熟悉map或展开运算不熟的话可以参考MDN
Map物件操作MDN介绍
展开运算子

资料筛选与转型

可以console.log(groupData.get(defaultDistrict))查看资料内容
为了确保他是否为一个阵列再次使用Array.isArray()方法检查
确认无误後使用阵列的操作方法进行资料筛选,本次希望以交易标的是房子为主,因此过滤掉交易标的为车位和土地的资料
使用forEach()转换将土地、建物、总价的资料从字串型态转换成数字型态
程序码如下

console.log(groupData.get(defaultDistrict));
console.log(Array.isArray(groupData.get(defaultDistrict)));
const house = groupData.get(defaultDistrict).filter(function (d) {
if (d["交易标的"] !== "土地" && d["交易标的"] !== "车位") {
    return d;
}
});
house.forEach(el => {
    el["建物移转总面积平方公尺"] = +(el["建物移转总面积平方公尺"]);
    el["土地移转总面积平方公尺"] = +(el["土地移转总面积平方公尺"]);
    el["总价元"] = +el["总价元"];
});

console出来的资料如下图

https://ithelp.ithome.com.tw/upload/images/20211008/20125095TvATbSoWcY.png

selection.node()介绍

取得HTML元素虽然可以使用原生Js的getElementById,不过这边介绍一个在d3的select底下的一个方法,selection.node()
尝试着撰写以下程序码

console.log(d3.select("#min-bulid").node());
console.log(document.getElementById("min-bulid"));
console.log(d3.select("#min-bulid").node()===document.getElementById("min-bulid"));

这边可以发现透过d3的selecttion.node()的API和document.getElementById()的方法所得到的内容是相等的情形如下图

https://ithelp.ithome.com.tw/upload/images/20211008/20125095ejaP1NDfhA.png

selection.node()

因此接下来我们可以撰写以下的程序码来得到使用者输入的值并且将其设置为座标比例尺的上限和下限
具体程序码如下

let minBuildArea =d3.select("#min-bulid").node().value;
let maxBuildArea =d3.select("#max-bulid").node().value;
let minLandArea = d3.select("#min-land").node().value;
let maxLandArea =d3.select("#max-land").node().value;
const scaleX =  d3.scaleLinear().domain([minBuildArea,maxBuildArea]).range([0,800]).clamp(true);
const scaleY =  d3.scaleLinear().domain([maxLandArea,minLandArea]).range([0,800]).clamp(true);

圆形半径比例尺

与昨天不同的是这次我们要再加入一个ScaleR来做为圆点的半径,我们将其范围映射到5到900,换句话说希望圆点的半径最小有5,由於之後会开平方根,因此最大值也顶多占座标轴的√900=30而已
然後房价输入价位最大值设定1亿(这样范围应该可以容纳大多数的房价了吧???),
因此添加以下程序码

let minPrice = 0;
let maxPrice = 10000000;
const scaleR = d3.scaleLinear().domain([minPrice,maxPrice]).range([5,900]).clamp(false);

最後我们将刚刚所做出来的比例尺配合axisAPI制作座标轴程序码如下

const axisX = d3.axisBottom(scaleX)
                .ticks(15)
                .tickFormat(d=>(d+"m²"))
                .tickSize(-800); 
const axisY = d3.axisLeft(scaleY)
                .ticks(15)
                .tickFormat(d=>(d+"m²"))
                .tickSize(-800); 
const gX = svg.append("g")
                .attr("transform",`translate(50,850)`)
                .classed("xAxis",true);
const gY = svg.append("g")
                .attr("transform",`translate(50,50)`)
                .classed("yAxis",true);                    

selection.call()介绍

这边简单介绍一下selection.call,先前我们再进行座标渲染的时候都是使用axis(selection)的方式,例如axisBottom( svg.append("g")),然而d3Js是大量使用方法链所形成的一个套件,因此如果使用axisY(gY)的方式来渲染座标轴的话,不便将方法链形成(由於回传的内容不能传递给下一个函式使用。)

我们可以撰写以下程序码来观看差异

console.log(axisY(gY));
console.log(gY.call(axisY));

这边观看回传的东西可以发现axisY(gY)回传的是undefined,但是Y.call(axisY)可以回传一个物件,所以当我们执行axisY(gY)gY.call(axisY)的时候虽然都可以将座标轴绘制到网页画面上,但是使用call的方式会回传物件以便後续给函式继续接续方法链。

https://ithelp.ithome.com.tw/upload/images/20211008/20125095fB6aiZbLcp.png

这边也可以观看官方文件的说明,他表示下列两种执行方式都是一样的结果,如下图

https://ithelp.ithome.com.tw/upload/images/20211008/20125095jvRJ0XD8ar.png

参考selction.call

添加动画

因此我们可以使用call将座标轴添加至画面上并且加入动画如以下程序码

gX.transition().duration(1000).call(axisX);
gY.transition().duration(1000).call(axisY);

呈现结果如下图

绘制圆圈圈

接下来我们进行绘制圆型
程序码如下

const gCircle = svg.append("g");
gCircle.selectAll("circle")
        .data(house)
        .join("circle")
        .attr("transform", "translate(50,50)")
        .attr("fill","rgba(255,0,0,.1)")
        .attr("cx",d=>(scaleX(d["土地移转总面积平方公尺"])))
        .attr("cy",d=>(scaleY(d["建物移转总面积平方公尺"])))
        .attr("r",d=>{return Math.sqrt(scaleR(d["总价元"]))})

基本上资料绑定的data和透过scale函式将资料转换到圆点的x和y的位置与昨天的制作方式大同小异。
这边比较不一样的地方是R需要带入ScaleR()将房屋价格带入比例尺函式映射出圆形的半径,记得再使用Math.sqrt()进行开平方根。

到此为止应当可以映射出圆点如下图

https://ithelp.ithome.com.tw/upload/images/20211009/20125095YLMq4fmVV6.jpg

小总结

本日浅谈了程序构想,使用一些方法处理资料并且介绍了先前未提及的selection.call()selection.node(),另外依序将三个数值带Scale()後添加动画,明天将处理掉圆形超出范围的部分和轴线更新以及行政区切换,先行预告一下可能需要有一点座标平面的概念,以上。


<<:  #23 数据中中的特徵相关性(2)

>>:  23 - 建立结构化的 Log (1/4) - Elastic Common Schema 结构化 Log 的规范

Day 17 To Do List - 切版 2

第 17 天! 昨天我们建立了, To Do List 专案 这是我们预期的画面, 昨天做到 今天我...

[Day18] JavaScript - Fetch

fetch() 是 ES6 的新语法,主要是搭配 Promise来执行请求网站和请求後获取 Resp...

第一次的爬虫(2)

那我就延续上一篇的实作吧! 已经将会用到的套件装上,并且在网站的控制室找到所需的资讯位置,接下来就是...

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

我慢慢的告诉小路,我记得的事,包括那个R... 小路一直强调,只有我一个人参加,然後,不会有什麽NP...

Day 25 - [Android APP] 03-Android 的 STT 与 TTS

用键盘输入讯息,对年轻人或许稀松平常,但对长者而言,使用语音的方式或许更轻松。所以除了画面字体放大外...