D3JsDay14不想图表被冰冻,那就做一点互动—事件互动

什麽是互动?简单说希望能够让使用者允许监听和分派事件,用比较白话的一点方式举例就是当我们滑鼠按下某个元素的时候,图表会呈现某些样貌,监听就是滑鼠按下的意思,委派就是让图表呈现某些样貌,

对应到的原生Js说明可以参考
Event reference-MDN
其他更多说明也可以参考d3JSAPI文件
官方API事件处理

监听器

首先我们要有一个所选取的元素,在後面透过方法链的方式添加要执行的函式,这边可以直接就范例学习即可,例如我们预做出一个当按钮按下去就会产生随机的长条图,我们可以先把按钮和长条图准备好
创建好的范例如下

<style>
.btn {
      padding: 8px;
      background-color: orange;
      border: none;
      border-radius: 4px;
    }
.btn:hover {
  background-color: rgba(255, 166, 0, 0.664);
  cursor: pointer;
}
</style>
<body>
  <script>
    const btn = d3.select("body")
                .append("button")
                .text("按钮")
                .style("display", "block")
                .classed("btn", true);
    let randomIntFun = d3.randomInt(50, 400);
    let randomArr = [];

    for (let index = 0; index < 20; index++) {
      randomArr.push(randomIntFun());
    }
    let padding = 40;
    const svg = d3
      .select("body")
      .append("svg")
      .attr("width", 800)
      .attr("height", 450);
    svg.selectAll("rect")
      .data(randomArr)
      .join("rect")
      .attr("x", (d, i) => {
        return padding + i * 30;
      })
      .attr("y", () => 400 + padding)
      .attr("width", 20)
      .attr("height", 0)
      .attr("fill", "green")
      .transition()
      .duration("1000")
      .attr("y", (d) => {
        return 400 - d + padding;
      })
      .attr("height", (d) => {
        return d;
      });

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

    const g = svg.append("g");
    axisY(g);
    g.attr("transform", `translate(0,40)`);
  </script>
</body>

d3Day14-1

此时画面还不会动,之後将会加入监听事件

randomInt()、svg属性transform() 补充说明

这里用到了 randomInt()的方法,简单说是创建一个函式,这个函式执行後会回传随机的整数,randomInt()里面填的数值是最小值和最大值,根据范例就可以产生出最小值50,最大值400的随机乱数,然後使用for回圈将产生的数值使用array push到阵列当中,总共执行二十次。
d3官方文件randomInt

补充二第58行的地方 g.attr("transform", "translate(0,40)")的地方使用了transform,有时候我们渲染出来的座标与图表位置没有对应到的时候可以使用这个属性来调整它,可以参考MDN-SVG-transform

加入on监听

接下来我们预计使用滑鼠点击的事件因此在 on()内第一个参数填入click,第二个参数则是填入要执行的function,我们预计每次按下按钮的时候画出一个长条图,因此先取名叫做render函式

     const btn = d3.select("body")
        .append("button")
        .text("按钮")
        .style("display", "block")
        .classed("btn", true)
        .on("click", render);

这时候我们将刚刚所建立的长条图和随机产生数字的程序码用function render()包住此时大概会长这样

    function render() {
        let randomIntFun = d3.randomInt(50, 400);
        let randomArr = [];
        
        //中间如上面的程序码故省略......
        //中间如上面的程序码故省略......
        //中间如上面的程序码故省略......
        
        let scaleY = d3.scaleLinear().domain([0, 400]).range([400, 0]);
        let axisY = d3.axisRight(scaleY);

        const g = svg.append("g");
        axisY(g);
        g.attr("transform", `translate(0,40)`);
    }

这时候按下按钮会发现它将会再次新增一笔,所以我们一直按的话会看到下图

https://ithelp.ithome.com.tw/upload/images/20210929/20125095FiKkRLsbXJ.png

因此我们在执行render function的时候要先把先前的svg给移除掉如下程序码d3.select("body").select("svg").remove();这一行,另外希望画面第一次载入的时候有一个长条图,因此可以先执行一次render()

    function render() {
       d3.select("body").select("svg").remove();
        let randomIntFun = d3.randomInt(50, 400);
        let randomArr = [];
        
        //中间如上面的程序码故省略......
        //中间如上面的程序码故省略......
        //中间如上面的程序码故省略......
        
        let scaleY = d3.scaleLinear().domain([0, 400]).range([400, 0]);
        let axisY = d3.axisRight(scaleY);

        let g = svg.append("g");
        axisY(g);
        g.attr("transform", `translate(0,40)`);
    }
    render();

完整程序码参考如下的codepen

d3Day14-2

实际样貌如下图

使用实际资料做范例

延续前几天的程序码我们希望可以再滑鼠滑入到某个 <rect>的时候列出对应到的人口实际数字,另外添加过渡动画改变长条图的颜色,当滑鼠移出该rect的时候变回来。

因此我们再前几天的程序码後面添加以下片段

svg.selectAll("rect")                                                                  
  .on("mouseenter", function () {  
    const thisRectX = d3.select(this).attr("x"); //存取该 rect的 x属性位置
    const thisRectY = d3.select(this).attr("y"); //存取该 rect的 y属性位置
    let text = d3.select(this).data()[0].people_total;   
    d3.select(this).transition().duration(800).attr("fill","blue");  
    svg.append("text")   //添加文字和id以及thisRectX和
    .attr("id","people-total")   
    .attr("x",thisRectX)   
    .attr("y", ()=>(scaleY(text)-10))  
    .style("fill","blue")  
    .text(text);                                                                     
  })

上述程序码主要是在滑鼠移入到对应的 rect长条时先存取该rectx属性位置和y属性的位置如第3、4行,这边的this指向的是你所触发滑鼠移入事件的元素,另外也宣告text变数来储存你所指的元素的资料,第7~12行就是在svg底下添加text元素,然後各个属性的值来自於刚刚第3、4、5行所取的值,另外为了之後方便移除,所以在text元素添加了一个id属性,到目前为止只有由於执行mouseenter Event也就是滑鼠移入事件,目前当我们滑鼠离开 rect元素的时候,颜色没有变回来,上方的数值也没有消失

如下图

https://ithelp.ithome.com.tw/upload/images/20210929/201250953ueEZtT2fe.png

所以我们得在添加关於离开元素的时候的程序码,程序码如下

svg.selectAll("rect")
  .on("mouseenter", function () {
    const thisRectX = d3.select(this).attr("x");  
    const thisRectY = d3.select(this).attr("y"); 
    let text = d3.select(this).data()[0].people_total;
    d3.select(this).transition().duration(800).attr("fill","blue");
    svg.append("text")
    .attr("id","people-total")
    .attr("x",thisRectX)
    .attr("y", ()=>(scaleY(text)-10))
    .style("fill","blue")
    .text(text);
}).on("mouseleave",function () {
  d3.select(this)
  .transition()
  .duration(800)
  .attr("fill","orange");
    svg.select("#people-total").remove(this);
})

这边的this来自於事件的触发也就是你当滑入进去的元素,方法链後面继续接续一个事件表示滑鼠离开该元素时所要执行的事情,这边执行的事情直接把刚刚添加的text用id选取起来并移除它,另外也将颜色改变回来。

另外值得注意的是先前程序码,如果原先的长条图载入的Bar慢慢变高的动画三秒钟期间还没被执行完的时候,滑鼠移入会导致on事件触发让该Bar变成蓝色,其中bar的高度就再不会再变化,因此可以包装一个function 使用setTimeout来延迟执行

完整程序码如下

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);
let min = d3.min(newTaipei, (d) => d.people_total);
let max = d3.max(newTaipei, (d) => d.people_total);
console.log(newTaipei);
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", 400)
.attr("height", 0)
.attr("width", 50)
.attr("fill", "orange")
.transition()
.duration(3000) //由於这里会载入时间三秒之後的on事件需要等待三秒再执行避免被触发
.attr("y", (d) => {
  return scaleY(d.people_total);
})
.attr("height", (d) => {
  return 400 - scaleY(d.people_total);
})
;
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;
  });

const axisY = d3.axisRight(scaleY)
              .ticks(5)
              .tickFormat(function (d) {
              return d / 10000 + "万";
              });
const g = svg.append("g");
axisY(g);
setTimeout(() =>{addBlueEffect()},3000); //避免Bar的高度还没生长完就触发变色效果
function addBlueEffect() {
  svg.selectAll("rect")
  .on("mouseenter", function () {
  const thisRectX = d3.select(this).attr("x");
  const thisRectY = d3.select(this).attr("y");
  const text = d3.select(this).data()[0].people_total;
  d3.select(this)   
  .transition().duration(800).attr("fill","blue")
  ;
  svg.append("text")
  .attr("id","people-total")
  .attr("x",thisRectX)
  .attr("y", ()=>(scaleY(text)-10))
  .style("fill","blue")
  .text(text);

  }).on("mouseleave",function () {
  d3.select(this)
  .transition()
  .duration(800)
  .attr("fill","orange");
    svg.select("#people-total").remove(this);
  })
}

呈现效果如下图

githubPage如下

githubPage


<<:  【Day 14】Google Apps Script - API 篇 - Document Service - 文件服务介绍

>>:  # Day15 CSS基础设定_5

Git 综合笔记

1. 推资料进新分支 (建立新分支 + 推资料进新分支) (1)建立新专案 命名新专案,并记录网址 ...

[Day26] - Django-REST-Framework API 期末专案实作 (一)

不知不觉,铁人赛慢慢要进入尾声了,感谢过程中队友们彼此提携,互相提醒。 在前几天中,和大家介绍了 D...

冒险村05 - Release Drafter

05 - Release Drafter 每当专案 merge & deploy 完毕时,都...

在国外的我如何跟客户联系

关於联系方式,我分成三种等级: 即时:line/messenger 通话、电话、已读的 line 等...

[Day1] 简单介绍 Google Assistant 语音应用程序

大家好,我是Hank。 目前就读於台科大资工所的研究生。 很高兴有机会向大家分享我在开发Google...