昨天已經介紹完散佈圖了,大致上與散佈圖的作法大同小異,差別在於氣泡本身也就是circle
,也能呈現一個變數值,在svg裡面我們將設定其屬性r來定義氣泡的半徑大小。
本日預計使用實價登錄網站的台南交易資料來實作一個氣泡圖、其中x軸表示售出時的建物面積、y軸表示售出時的土地面積,其氣泡的大小表示成交價格。
為了肉眼辨別視覺差異,讓使用者觀看面積有總價上的感覺,因此R必須再開平方根
避免渲染出來的圖形造成次方倍的扭曲解讀。
這邊使用數學式子來稍微講解一下關係
關係表格如下假設半徑是r
半徑與面積關係如下
r:πr²
半徑( r ) | 1 | 2 | 3 | 4 |
---|---|---|---|---|
半徑平方r² | 1 | 4 | 9 | 16 |
圓面積πr² | π | 4π | 9π | 16π |
套用到上述的例子當中,
半徑1的半徑平方是1圓面積是π
半徑1的半徑平方是4圓面積是4π
寫數學式子如下
1:π = 4:4π = 9:9π
故圓面積是與半徑平方成正比
input
欄位來輸入x軸、y軸的範圍並加上一個軸線更新的按鈕Math.sqrt()
將其半徑開平方根,以便繪圖完的圓形面積比與房屋價格比相同,與上一篇文章大同小異會是設置scaleX和scaleY來進行資料轉換至svg的座標點位置。<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選取時更為方便。
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]);
}
如果不熟悉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出來的資料如下圖
取得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()
的方法所得到的內容是相等的情形如下圖
因此接下來我們可以撰寫以下的程式碼來得到使用者輸入的值並且將其設置為座標比例尺的上限和下限
具體程式碼如下
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
,先前我們再進行座標渲染的時候都是使用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的方式會回傳物件以便後續給函式繼續接續方法鏈。
這邊也可以觀看官方文件的說明,他表示下列兩種執行方式都是一樣的結果,如下圖
因此我們可以使用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()進行開平方根。
到此為止應當可以映射出圓點如下圖
本日淺談了程式構想,使用一些方法處理資料並且介紹了先前未提及的selection.call()和selection.node(),另外依序將三個數值帶Scale()
後添加動畫,明天將處理掉圓形超出範圍的部分和軸線更新以及行政區切換,先行預告一下可能需要有一點座標平面的概念,以上。