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

半径R圆心座标(x,y)与x,y轴的关系

昨天我们渲染了图表出来,但是出现的问题是超出了座标轴的范围,这边先解释一下接下来要解决的方式的原理

我们预计使用者可以操作座标轴的最小值和最大值,因此目前的做法是如果绘制出的圆超过座标轴的话就将其消失。

这边要考量的地方是要如何经过筛选机制将会超出范围的圆形给过滤掉呢?
首先需要有些座标平面(又称笛卡尔座标系)的概念,可以参见下图

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

如图在座标平面上面圆心分别为(1,1)和(4,0)的两圆,半径R为3.5,试着以变数(x,y)和R表示圆形超出座标轴的可能性?

尝试着看左边的圆
换句话说,将圆心的x座标(座标数字1)减去半径R(半径数字3.5)会是负值(小於零)
同理将圆心的y座标(座标数字1)减去半径R(半径数字3.5)也会是负值(小於零)

用数学式子表示在x座标的状况是 1-3.5<0-2.5<0的情况
同理用数学式子表示在y座标的状况是 1-3.5<0-2.5<0的情况

在代成变数就会变成x-R<0y-R<0的情况

现在回到程序码
我们可以对使用scale函式映射完在座标平面的x值y值r值的开平方根相减是正数的情形就绘制半径出来,否则半径就等於0(也就是不绘制)

gCircle.selectAll("circle")
.data(house)
//中间省略
//中间省略
//中间省略
.attr("r",d=>{
     let R = Math.sqrt(scaleR(d["总价元"]));
    if(
       (scaleX(d["土地移转总面积平方公尺"])-R>0)
        &&(scaleY(d["建物移转总面积平方公尺"])-R>0)
        &&(scaleX(d["土地移转总面积平方公尺"])+R<800)
        &&(scaleY(d["建物移转总面积平方公尺"])+R<800)
    )
    {
        return Math.sqrt(scaleR(d["总价元"]));
    }
    else {
        return 0;
    }

另外要考量的地方是除了圆形会超出在svg的负数位置上面,还可能超出svg的宽高,因此也必须判断任何圆形绘制完後超出宽为800高为800的圆形
因此程序码如下

//这有程序码先前提过故省略
//这有程序码先前提过故省略
//这有程序码先前提过故省略
.attr("r",d=>{
if(
(scaleX(d["土地移转总面积平方公尺"])-scaleR(d["总价元"]))>0
&&(scaleY(d["建物移转总面积平方公尺"])-scaleR(d["总价元"]))>0
&&(scaleX(d["土地移转总面积平方公尺"])+scaleR(d["总价元"]))<800
&&(scaleY(d["建物移转总面积平方公尺"])+scaleR(d["总价元"]))<800
){
return scaleR(d["总价元"])    
}
else {
return 0;
}
})

这边先制作渲染样板的函式做为等等在滑鼠移入的时候会执行的事情程序码如下

function tooltip(city){
d3.select(".wrap-data").append("div").classed("tooltip",true).html(
`<p>交易标的:${city[0]["交易标的"]}</p>
<p>建物型态:${city[0]["建物型态"]}</p>
<p>主要用途:${city[0]["主要用途"]}</p>
<p>移转层次:${city[0]["移转层次"]}</p>
<p>建物总面积${parseInt(city[0]["建物移转总面积平方公尺"])}m²</p>
<p>土地总面积${parseInt(city[0]["土地移转总面积平方公尺"])}m²</p>
<p>总价钱${city[0]["总价元"]}元</p>
`)
}

如上述函式中所接收的变数city,等等将会接收到滑鼠移入的事件做为渲染画面的资料。

接下来我们要进行滑鼠移入时所执行的function,在mouseentercallbackFunction 里面我们预计将所移入的圆点变换颜色,撰写attr("fill","blue"),另外将所选到的圆点的data代入到一个tooltip的function执行,在方法链串接当中使用mouseleave事件,并且在其callbackFuntion里面让原本被改变的颜色恢复成透明度0.1的红色。这边使用d3.select(".tooltip").remove();在mouseenter的用意主要是希望我再移入下一个圆点之前房屋价格、土地面积等等的资料继续出现直到下一个滑鼠移入圆点事件被触发。

//方法链串接程序码省略
.on("mouseenter",function(){
d3.select(".tooltip").remove();
d3.select(this).attr("fill","blue");
tooltip(d3.select(this).data());
}).on("mouseleave",function(){
d3.select(this)
.attr("fill","rgba(255,0,0,.1)");
});

目前成果如下图

最後我们进行轴线更新以及切换行政区的时候画面更新,也就是当下拉式选单被选择到某个行政区的时候重新渲染图形,另外输入土地和房屋面积的范围值时按下轴线更新可以切换范围。如下图的按钮及选单。

https://ithelp.ithome.com.tw/upload/images/20211009/20125095SCN0DmqMcU.png

因此我们回到先前的程序码的下方添加change事件,程序码如下

const groupData = d3.group(data,d=>d["乡镇市区"]);
//省略
//省略
//省略
for (let i=0;i<districtAry.length;i++) {
d3.select("#district").append("option").text(districtAry[i]);
}
d3.select("#district").on("change", function(e) {
    defaultDistrict=e.target.value
    update();
})

然後将接下来的程序码使用updata的函式包住,如果被触发事件的时候使用d3.selectAll("svg g").remove();先移除先前的轴线和圆型後重新绘制,另外在画面载入的时候执行一次updata()函式程序码如下

function update(){
d3.selectAll("svg g").remove();
const house = groupData.get(defaultDistrict).filter(function (d) {
if (d["交易标的"] !== "土地" && d["交易标的"] !== "车位") {
return d;
}
});
//中间省略
//中间省略
//中间省略
.on("mouseenter",function(){
d3.select(".tooltip").remove();
d3.select(this).attr("fill","blue");
tooltip(d3.select(this).data());
// console.log(d3.select(this).data());
}).on("mouseleave",function(){
d3.select(this)
.attr("fill","rgba(255,0,0,.1)");
});
}
update();

最後应当可以看到成果如下图

本日githubPage连结

githubPage

本日完整程序码如下

<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>
<div class="wrap-data">
  <div class="bulid-div">建物面积m²</div>
  <div class="land-div">土地面积m²</div>
</div>
<script>
let width = 900;
let height = 900;

const svg = d3.select(".wrap-data")
            .append("svg")
            .attr("width", width)
            .attr("height", height);
    d3.csv("tainan11009.csv").then(function(data) {
        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]);
        }
        d3.select("#district").on("change", function(e) {
            defaultDistrict=e.target.value
            update();
        })
        d3.select("#btn").on("click",update);
        function update(){
            d3.selectAll("svg g").remove();
            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["总价元"];
            });
            let minPrice = 0;
            let maxPrice = 10000000;
            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);
            const scaleR = d3.scaleLinear().domain([minPrice,maxPrice]).range([5,900]).clamp(false);
            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);                    
            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=>{
                        let R = Math.sqrt(scaleR(d["总价元"]));
                        if(
                           (scaleX(d["土地移转总面积平方公尺"])-R>0)
                            &&(scaleY(d["建物移转总面积平方公尺"])-R>0)
                            &&(scaleX(d["土地移转总面积平方公尺"])+R<800)
                            &&(scaleY(d["建物移转总面积平方公尺"])+R<800)
                        )
                        {
                            return Math.sqrt(scaleR(d["总价元"]));
                        }
                        else {
                            return 0;
                        }
                    })
                    .on("mouseenter",function(){
                        d3.select(".tooltip").remove();
                        d3.select(this).attr("fill","blue");
                        tooltip(d3.select(this).data());
                        // console.log(d3.select(this).data());
                    }).on("mouseleave",function(){
                        d3.select(this)
                            .attr("fill","rgba(255,0,0,.1)");
                    });
        }
        update();
        function tooltip(city){
            d3.select(".wrap-data").append("div").classed("tooltip",true).html(
                `<p>交易标的:${city[0]["交易标的"]}</p>
                <p>建物型态:${city[0]["建物型态"]}</p>
                <p>主要用途:${city[0]["主要用途"]}</p>
                <p>移转层次:${city[0]["移转层次"]}</p>
                <p>建物总面积${parseInt(city[0]["建物移转总面积平方公尺"])}m²</p>
                <p>土地总面积${parseInt(city[0]["土地移转总面积平方公尺"])}m²</p>
                <p>总价钱${city[0]["总价元"]}元</p>
            `)
        }

    })
</script>

备注说明和题外话

HTML部分左下角加上文字说明此轴是土地面积还是建物面剂并且使用CSS调整了文字垂直排列、由於最後一个x和y轴的刻度的数字可能会被切掉、因此使用了CSS选取器将其设置不显示,其他的css样式撰写就不多做说明,有兴趣的人可以再使用devtool观看。
另外笔者原本是带入台北109的第四季资料试试看,但是渲染出来的图片都是大圈圈(房价颇高),而且变异数太大(代表高房价和低房价的资料颇多),因此改使用变异数较少和资料量较少的台南110年9月份的资料让,整个其呈现比较能看得出差异

有兴趣者的人可以台北109第四季如下图!https://ithelp.ithome.com.tw/upload/images/20211009/20125095dcklPdOQrC.jpg


<<:  [Day 25] keep-alive状态保留

>>:  Day 24:如何还原备份到 Github 另外一个分支的 Hexo 资料?

[2021铁人赛 Day05] General Skills 02

引言 昨天完成了最基本的一题,学了一些基本操作。 cat, wget 等,都是必备的工具,一定要学...

[Day 05] 当我~们同在一起在17在17 (k-means 理论篇)

前言 有一说一,表情辨识到底还是个分类任务。 如果我说有一种演算法可以在不需要标签的情况下自动帮我们...

连续 30 天 玩玩看 ProtoPie - Day 9

做出左右滑动的互动行为 今天要来操作这个 Container ,其实就可以把它想成「一组」东西就好了...

Day 04 Azure Web App- 方便部署服务

Azure Web App- 方便部署服务 Azure Web App 提供一个方便部署服务的做法,...

鼠年全马铁人挑战 WEEK 37:封包测试工具 - Charles (二)

           Photo on charlesproxy.com 前言 上个礼拜简单的分享...