#27-微互动折线图动态!就是要比较才看得出结果啊 (D3.js)

前两天都是展现Data而已,今天来试做看看互动&换资料的动态!
折线图也是满常见的样式,
这次以非洲的通膨为主题,主要功能为

1.可以有三个国家资料做比较
2.点击把其中一个国家隐藏

老样子先看成果:

主要流程如下:

  1. 基本绘制:svg定位、x & y轴
  2. 资料读取&整理
  3. 绘制线段、点、label & legend
  4. 资料互动:监听legend点击
  5. 资料更新的监听:监听selection change
  6. 资料更新:线段等的位置移动

参考
更换data的动态是参考这个:Connected scatter plot with dropdown to select group

隐藏线段是参考这个: Connected scatter plot with interactive legend


1.基本绘制

//1. 基本绘制
const margin = {top: 10, right: 100, bottom: 30, left: 30},
    width = 460 - margin.left - margin.right,
    height = 400 - margin.top - margin.bottom;

//1. 基本绘制: append the svg to the body of the page
const svg = d3.select("#my_data")
    .append("svg")
    .attr("width", width + margin.left + margin.right)
    .attr("height", height + margin.top + margin.bottom)
    .append("g")
    .attr("transform",`translate(${margin.left},${margin.top})`);

// 1. 基本绘制 add X-axis
  const x = d3.scaleLinear()
  .domain([2012, 2020])
  .range([0, width]);
  
  svg.append("g")
  .attr("transform", `translate(0, ${height})`)
  .call(d3.axisBottom(x).tickFormat(d3.format("d")));//.tickFormat(d3.format("d"))是把数字变成字串
  
  //1. 基本绘制 dd Y-axis
  const y = d3.scaleLinear()
      .domain( [-10,50])
      .range([height, 0 ]);//上面是起点,要把-10往下推,就让他y变高高
  
  svg.append("g")
      .call(d3.axisLeft(y));


2. 资料读取&整理

//2. 资料读取&整理
let countriesData = []
let time = []
for(i = 2012; i <=2020; i++){
  time.push(i);
}

//2.资料读取&整理:开始读资料
d3.csv('https://raw.githubusercontent.com/africadatahub/adh-africa-inflation/main/data/africa_inflation_data.csv')
.then(data=>{ 
  
  //2. 资料准备:create countries
 countriesData = data.map(d=>d.country);

  //2. 资料准备:加入下拉选单
  d3.select('#countrySelect')
  .selectAll('option')
  .data(countriesData)
  .join('option')
  .attr('value', d=> d)
  .text(d=> d)
  
  //2. 资料准备:先有个初始资料
  let allGroup = ['Burundi','Chad','Gabon'];
  
  //2. 资料准备:将拿到的资料整理成好读版
	//整理成这样的阵列:[{name: 国家名称, values: [{2012: 0.18}, {2013: 0.25}...]}
	//,{name: 国家, ...}]
  let dataReady = allGroup.map(groupName => {
        return {
          name: groupName,
          values: data.reduce((pre,data)=>{
               if(data.country == groupName){
                  result = time.map(year=>{
                    return {time: year, value: data[year]}
                  })    
                }
              return result
            }, [])
          }
        })
     
  //2. 资料准备:   colorScale 把国家配对成一个一个颜色
  let myColor = d3.scaleOrdinal().domain(countriesData).range(d3.schemeSet2);

3.绘制线段、点、label & legend

//3. 绘制:Line Maker
     const line = d3.line()
        .x(d=>x(+d.time))
        .y(d=>y(+d.value));

    //3. 绘制:drawlines
    let lines = svg.selectAll('myLines')
        .data(dataReady)
        .join("path")
        .attr("class", d => d.name.replace(/\s*/g,""))
				//class name用国家名称,但有些国家名称有空格,所以把空格拔掉
        .attr("d", d => line(d.values))
        .attr("stroke", d => myColor(d.name))
        .style("stroke-width", 4)
        .style("fill", "none");

        //3. 绘制:add points Group
    let pointsGroup = svg.selectAll('myDots')
            .data(dataReady)
            .join('g')
            .style('fill', d=>myColor(d.name))
            .attr('class',d=>d.name.replace(/\s*/g,""))
      
    //3. 绘制:add points
   let points = pointsGroup.selectAll('myPoints')
            .data(d=>d.values)
            .join('circle')
            .attr('cx', d=>x(d.time))
            .attr('cy', d=>y(d.value))
            .attr('stroke','white')
            .attr('r',5);

  
        //3. 绘制:Add labels
    let label = svg.selectAll()
        .data(dataReady)
        .join('g')
        .append('text')
        .attr('class',d=>d.name.replace(/\s*/g,""))
        .datum(d=>{return {name: d.name, value: d.values[d.values.length -1]}})
        .attr('transform', d =>`translate( ${x(d.value.time) + 10} ,${y(d.value.value)} )`)
        .text(d=>d.name)
        .style('height', '30px')
        .style('fill',d=>myColor(d.name))
        .style("font-size", 15);

    //3. 绘制:加上图例
    myLegend = svg.selectAll('myLegend')
        .data(dataReady)
        .join('g')
        .append('text')
        .attr('x', 30)
        .attr('y', (d,i)=> 10 + i*30)
        .text(d=>d.name)
        .style('fill',d=>myColor(d.name))
        .style('font-size',15)

4.资料互动:监听legend点击

//4.资料互动:Interactive: hide line
//监听图标的点击
  myLegend.on('click',function(e,d){
    currentOpacity = d3.selectAll('.'+d.name.replace(/\s*/g,"")).style('opacity');
    d3.selectAll('.'+d.name.replace(/\s*/g,"")).style('opacity',currentOpacity == 1 ? 0 : 1);
  })

5.资料更新的监听:监听selection change

//5.资料change data: 监听器
    d3.select('#countrySelect').on('change', function(){
			//用d3 的nodes()拿到被点击的节点
      let selected = d3.selectAll('#countrySelect>option:checked').nodes();
			//固定只取三个
      if(selected.length != 3) return;
      allGroup = []; 
      d3.selectAll('#countrySelect>option:checked')
      .each((item)=>{
        allGroup.push(item)
      });

		//有变动时我就呼叫update罗,请看下一小节
      update();
    })

6.资料更新:线段等的位置移动

//6. 资料更新
   function update(){
    dataReady = allGroup.map(groupName => {
        return {
          name: groupName,
          values: data.reduce((pre,data)=>{
               if(data.country == groupName){
                  result = time.map(year=>{
                    return {time: year, value: data[year]}
                  })    
                }
              return result
            }, [])
          }
        });
    
    lines.data(dataReady)
          .join()
          .transition()
          .duration(1000)
          .attr("class", d => d.name.replace(/\s*/g,""))
          .attr("d", d => line(d.values))
          .attr("stroke", d => myColor(d.name))
          .style("stroke-width", 4)
          .style("fill", "none");

    pointsGroup.data(dataReady)
          .join()
          .transition()
          .duration(1000)
          .style('fill', d=>myColor(d.name))
          .attr('class',d=>d.name.replace(/\s*/g,""))
    
    points.data(d=>d.values)
          .join()
          .transition()
          .duration(1000)
          .attr('cx', d=>x(d.time))
          .attr('cy', d=>y(d.value))
          .attr('stroke','white')
          .attr('r',5);   
    
    label.data(dataReady)
        .join()
        .attr('class',d=>d.name.replace(/\s*/g,""))
        .datum(d=>{return {name: d.name, value: d.values[d.values.length -1]}})
        .transition()
        .duration(1000)
        .attr('transform', d =>`translate( ${x(d.value.time) + 10} ,${y(d.value.value)} )`)
        .text(d=>d.name)
        .style('height', '30px')
        .style('fill',d=>myColor(d.name))
        .style("font-size", 15);
    
    myLegend.data(dataReady)
            .join()
            .transition()
            .duration(1000)
            .attr('y',(d,i)=> 10 + i*30)
            .attr('x', 30)
            .text(d=>d.name)
            .style('fill',d=>myColor(d.name))
            .style('font-size',15);

    
  }
  
  
  
})

以上!

今天的code在这里
当然还有太多细节可以调整,譬如说资料标签重叠的地方,以及option多选的介面,还有一次强迫选3条...

等之後有时间再回头来!QQ

有任何错误或想法欢迎批评指教!


<<:  TypeScript | namespace 心得纪录

>>:  [全民疯AI系列2.0] 完赛总结

Day 10:Python基本介绍03 | List、Tuple

各位早安阿~ 不知不觉间已经来到铁人赛第十天,也就是过完1/3了呢,想想还真是快。只不过今天就开学了...

Day09 SwiftUI 02 - 在 SwiftUI 上设计画面

预设已经帮我们建立一个 Text 元件"Hello world",且会看到Aut...

【Day19】:PWM输出-模拟类比讯号

PWM-脉冲宽度调变 我相信很多人在使用Arduion的时候还是不清楚PWM到底在干嘛? PWM是一...

30-14 之 Domain Layer - Service

在谈论完书中提的组织 domain 的三种模式 : Transaction Script Domai...

任务排程

本文目标 学习基本的排程演算法 阅读原始码以理解排程器的实际运作 常见的排程演算法 FCFS (Fi...