iT邦幫忙

2021 iThome 鐵人賽

DAY 28
0
Modern Web

這個網站也太嗨!30 個網頁動態提案系列 第 30

#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

有任何錯誤或想法歡迎批評指教!


上一篇
#26-圓餅圖動起來!玩轉D3.js漸變功能Transition+Interpolate
下一篇
#28- D3.js 地圖動起來!用SVG viewbox/D3 fitExtent讓地圖置中
系列文
這個網站也太嗨!30 個網頁動態提案33

1 則留言

0
juck30808
iT邦新手 3 級 ‧ 2021-10-14 12:33:03

恭喜即將邁入完賽階段~

liawyudye iT邦新手 5 級 ‧ 2021-10-14 15:54:56 檢舉

謝謝~

我要留言

立即登入留言