Day22-D3 基础图表:长条图

本篇大纲:开放资料下载、绘制长条图

今天的一天一图表,我们来画图表世界中最常见的 长条图 吧!长条图系列总共分成三部曲,分别是「基础长条图、复数长条图、堆积长条图」,今天先来看看基础的第一部曲~

开放资料下载

为了让我们画的图表更接近真实社会的资料,我这次选用台湾电力公司提供的开放资料 — 各县市售电资讯。台电目前只提供 xls 档案,但由於D3无法接受这种档案格式,因此我们要把载下来的 xls 档另存成 csv 档。

我们先把2021/07跟2021/08的售电资讯载下来并打开,档案里面第一行跟最後一行使用了合并储存格的方式去记录资料,但这样的方式另存成csv档时会出错,所以我们先将第一行跟最後一行都删掉
https://ithelp.ithome.com.tw/upload/images/20211004/20134930A5bu9hibo6.jpg

接着再把这两份资料另存成csv档。其实这个不是很好的做法,如果你要替公司的专案制作图表的话,记得跟後端沟通好你需要的资料格式,而不是用这种直接修改资料的方式。这边因为只是示范,所以就先用偷吃步的方式来处理~

本次图表的画面与互动效果

确定了要使用哪些资料後,我们先来看看这次要做的图表以及互动效果吧!我们这次要做的图表如下:
https://i.imgur.com/h9kDRSg.gif

画面与互动效果包含:

  • 滑鼠 hover 时,长条图会变色+呈现用电度数
  • 资料切换时图表更改

绘制长条图

现在我们开始画图吧!首先,一样先建立图表区块与svg,并且将刚刚下载的资料用 d3.csv的方式取得

// css
.chart{
    width: 100%;
    min-width: 300px;
    margin: auto;
}
//html
<h4 class="mt-3">台湾各县市每月住宅售电</h4>
<div class="chart"></div>
<div class="btnWrap">
  <button class="btn btn-primary July">2021 7月</button>
  <button class="btn btn-primary August">2021 8月</button>
</div>
// js

let data = []
async function getData() {
  // 取资料
  dataGet = await d3.csv('./data/202107_Electric.csv')
  console.log(dataGet)
  data = dataGet
  drawBarChart()
};
getData()

// RWD
function drawBarChart(){
  // 删除原本的svg.charts,重新渲染改变宽度的svg
  d3.select('.chart svg').remove();

  // RWD 的svg 宽高
  const rwdSvgWidth = parseInt(d3.select('.chart').style('width')),
        rwdSvgHeight = rwdSvgWidth*0.8,
        margin = 40;

  const svg = d3.select('.chart')
                .append('svg')
                .attr('width', rwdSvgWidth)
                .attr('height', rwdSvgHeight);

	// 接下来的程序码放这边...
	// 接下来的程序码放这边...
	// 接下来的程序码放这边...
}

我们先把目前得到的资料印出来看一下
https://ithelp.ithome.com.tw/upload/images/20211004/20134930xqhgfMUn5d.jpg

这是我们拿到的资料~A 代表的是「家用住宅的售电度数」,我们的图表就先来比较各县市家用售电度数吧!要画图表前要先确定XY轴分别使用哪些资料,我决定X轴放县市,Y轴就放家用售电度数,因此我要把XY轴需要的资料都分别拉出来整理成阵列,这样才能带给比例尺去做运算

// map 资料集
const  xData = data.map((i) => i['县市']);

//由於台电提供的A售电数度是带有千分号的字串,我们要先把它处理成数字
const  yData = data.map((i) => parseInt(i['A.售电量(度)'].split(',').join('')));

整理出XY轴的资料集後,接着来画XY轴的比例尺与轴线吧

// 设定要给 X 轴用的 scale 跟 axis
const xScale = d3.scaleBand()
                .domain(xData)
                .range([margin*2, rwdSvgWidth - margin/2]) // 宽度
                .padding(0.2)

const xAxis = d3.axisBottom(xScale)

// 呼叫绘制x轴、调整x轴位置
const xAxisGroup = svg.append("g")
                      .call(xAxis)
                      .attr("transform", `translate(0,${rwdSvgHeight - margin})`)
                      .selectAll("text") // 调整刻度文字标签倾斜
                      .attr("transform", "translate(-10,0)rotate(-45)")
                      .style("text-anchor", "end");

// 设定要给 Y 轴用的 scale 跟 axis
const yScale = d3.scaleLinear()
                .domain([0, d3.max(yData)])
                .range([rwdSvgHeight - margin, margin]) // 数值要颠倒,才会从低往高排
                .nice() // 补上终点值

const yAxis = d3.axisLeft(yScale)
                .ticks(5)
                .tickSize(3)

// 呼叫绘制y轴、调整y轴位置
const yAxisGroup = svg.append("g")
                      .call(yAxis)
                      .attr("transform", `translate(${margin*2},0)`)

再来就是画长条图啦!长条图我们会使用< rect >来建立,要特别注意的是 height 的部分。由於svg都是由原点从上而下绘制,如果我们想绘制正确高度的长条图,就要把高度设定成从「图表高度 — 直条图高度」的地方开始绘制,才能画出正确高度的图表
https://ithelp.ithome.com.tw/upload/images/20211004/20134930efYrrWCFZ0.jpg

// 开始建立长条图
const bar = svg.selectAll("rect")
              .data(data)
              .join("rect")
              .attr("x", d => xScale(d['县市'])) // 让长条图在刻度线中间
              .attr("y", d => yScale(parseInt(d['A.售电量(度)'].split(',').join(''))))
              .attr("width", xScale.bandwidth())
              .attr("height", d => {
                  return (rwdSvgHeight - margin) - yScale(parseInt(d['A.售电量(度)'].split(',').join('')))
                })
              .attr("fill", "#69b3a2")
              .attr('cursor', 'pointer')

这样就建立完长条图啦!长条图真的算是最基本间单的图表~
https://ithelp.ithome.com.tw/upload/images/20211004/20134930KzI1qlzi77.jpg

为了增加点难度,我们来加上 mousehover 跟 mouseleave 的效果。 < rect > 绑定的资料藏在 d.target.__data__ 中,找到之後就能把文字标签设定成每个DOM元素各别绑定的资料

bar.on("mouseover", handleMouseOver)
   .on("mouseleave", handleMouseLeave)

function handleMouseOver(d, i){
      // console.log(d)
      // console.log(d.target.__data__)
      d3.select(this)
        .attr('fill', 'red')
      
      // 加上文字标签
      svg.append('text')
         .attr('class', 'infoText')
         .attr('y', yScale(parseInt(d.target.__data__['A.售电量(度)'].split(',').join('')))-20)
         .attr("x", xScale(d.target.__data__['县市']) + 50)
         .style('fill', '#000')
         .style('font-size', '18px')
         .style('font-weight', 'bold')
         .style("text-anchor", 'middle')
         .text(d.target.__data__['A.售电量(度)'] + '度');
    }

    function handleMouseLeave(){
      d3.select(this)
        .attr('fill', '#69b3a2')

      // 移除文字标签
      svg.select('.infoText').remove()
    }

这样就完成啦!基础的长条图范例就到这边~明天再来看看长条图二部曲:复数长条图吧!


Github Page 图表与 Github 程序码

最後附上本章的程序码:想看完整程序码的请上 Github,想直接操作图表的则去 Github Page 吧!请自行取用~


<<:  【Day 21】卷积神经网路(Convolutional Neural Network, CNN)(上)

>>:  Day21:人生跑马灯

菜鸟日记Day 28-如何为时钟刻上数字?

昨天我们以setInterval的方式,将指针放到时钟上并设定间隔,每秒钟执行1次setTime函式...

如何简单快捷找到被误删的日历事件

突然发现日历事件被误删?我们曾丢失/误删过 iPhone 的行事历档案。那麽我们如何才能从 iPho...

[PoEAA] Data Source Architectural Pattern - Data Mapper

本篇同步发布於个人Blog: [PoEAA] Data Source Architectural P...

【在 iOS 开发路上的大小事-Day11】透过 CocoaPods 来管理第三方套件

前情提要 一般在开发的时候,有些功能可能自己写不出来,但是网路上已经有别人写好的,那我们只需要将其引...

Re-architect - StickyNoteView

上一次我们完成了 ContextMenu 的部分,ContextMenu 也有了属於自己的 View...