Day23-D3 基础图表:复数长条图

本篇大纲:开放资料下载、本次范例的画面与互动效果、复数长条图的绘制关键、绘制复数长条图

今天的一天一图表,我们要来画图表长条图第二部曲 — 复数长条图!
https://ithelp.ithome.com.tw/upload/images/20211005/201349307BuJaRleJg.jpg

复数长条图主要是用来比较 某项目在不同时间的变化,或是 同一时间不同项目的差异。由於有多项资料要比较,因此除了XY轴之外,最好还要有资料的标签,才能让看图表的人更了解这张图想比较什麽东西。

https://ithelp.ithome.com.tw/upload/images/20211005/20134930HrVjCUBiYN.jpg

开放资料下载

为了更贴近真实,我们这次一样使用政府提供的公开资料:台南市劳动人口-依年龄别区分 来当作此次图表的资料!这次的资料一样是csv档案,打开档案的结构如下

https://ithelp.ithome.com.tw/upload/images/20211005/20134930KW86JjxeGy.jpg

由於是 csv 档,所以我们不用进行其他处理,直接拿来用就可以了 (耶比~)

本次范例的画面与互动效果

这次我们要做的范例画面与互动效果有:

  • 多条直条图呈现
  • 滑鼠 hover 到长条图时,会从此长条图拉出一条水平线,连到最左边的Y轴
  • 滑鼠 hover 到长条图时,Y轴会呈现此条资料的数值

https://i.imgur.com/4ZFQ1Zd.gif

复数长条图的绘制关键

绘制复数长条图的关键是 多加一条X轴的比例尺 。一般的长条图只有一条X轴跟一个X轴的比例尺,所有的资料会根据这条X轴去排列;但复数长条图的关键是另外整理一个新的资料阵列放要比较的资料,接着用这个新的资料阵列再多设定一个新的X轴比例尺。这样讲可能很模糊,我们直接来看程序码比较清楚。

绘制复数长条图

首先,我们先将资料取回来,并且建立svg画面

// css
.chart{
    width: 100%;
    min-width: 300px;
    margin: auto;
}
// html
<div class="chart"></div>
let data = []
async function getData() {
    // 取资料
    dataGet = await d3.csv('./data/tainan_labor_force_population.csv')
    data = dataGet
    console.log(data)
    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,
          margin = 20,
          marginBottom = 100

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

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

d3.select(window).on('resize', drawBarChart);

接着,我们先把基本的XY轴、以及XY轴的比例尺建立出来

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

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

const xAxis = d3.axisBottom(xScale)

// 呼叫绘制x轴、调整x轴位置
const xAxisGroup = svg.append("g")
                      .call(xAxis)
                      .attr("transform", `translate(0,${rwdSvgHeight - marginBottom})`)

// 设定要给 Y 轴用的 scale 跟 axis
const yScale = d3.scaleLinear()
                .domain([0, 600])
                .range([rwdSvgHeight - marginBottom, 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)`)

再来!最重要的部分来了!我们把要比较的资料抓出来建立新的阵列,并且建立新的X轴比例尺

const subgroups =  Object.keys(data[0]).slice(1)

// 第二条X轴的比例尺,用来设定多条bar的位置
const xSubgroup = d3.scaleBand()
                    .domain(subgroups)
                    .range([0, xScale.bandwidth()])
                    .padding([0.05])

我们可以把新的资料阵列 console 出来看看
https://ithelp.ithome.com.tw/upload/images/20211005/20134930IiDAioWbIi.jpg

这个就是我们要分组的阵列啦~~接着,我们来设定一下每个组别的颜色

// 设定不同 subgorup bar的颜色
const color = d3.scaleOrdinal()
  .domain(subgroups)
  .range(['#ff2d85','#4a4ae0','#4daf4a', '#f29909'])

然後就是建立长条图啦!

// 开始建立长条图
const bar = svg.append('g')
               .selectAll('g')
               .data(data)
               .join('g')
               .attr('transform',  d => `translate(${xScale(d['年度'])}, 0)`)
               .selectAll('rect')
               .data(d => {
                  return subgroups.map(key=>{
                      return {key:key, value:d[key]};})
                 })
               .join('rect')
                 .attr('x', d => xSubgroup(d.key))
                 .attr("y", d => yScale(d.value))
                 .attr("width", xSubgroup.bandwidth())
                 .attr("height", d =>{
                    return (rwdSvgHeight-marginBottom) - yScale(d.value)})
                 .attr("fill", d => color(d.key))
                 .style('cursor', 'pointer')

这样就建立好长条图罗~刚刚有说过,复数长条图的标签很重要,因此我们还要加上最下方的标签

// 加上下方分类标签
const tagsWrap =  svg.append('g')
     .selectAll('g')
     .attr('class', 'tags')
     .data(subgroups)
     .enter()
     .append('g')
     .attr('transform', "translate(-70,0)")
    
tagsWrap.append('rect')
     .attr('x', (d,i)=> (i+1)*marginBottom*1.3)
     .attr('y', rwdSvgHeight-marginBottom/2)
     .attr('width', 20)
     .attr('height', 20)
     .attr('fill', d => color(d))

tagsWrap.append('text')
        .attr('x', (d,i)=> (i+1)*marginBottom*1.3)
        .attr('y', rwdSvgHeight-10)
        .style('fill', '#000')
        .style('font-size', '12px')
        .style('font-weight', 'bold')
        .style("text-anchor", 'middle')
        .text(d=>d)

这样基本的图表就好罗
https://ithelp.ithome.com.tw/upload/images/20211005/201349308C29fg510e.jpg

再来我们来绑定滑鼠的事件,同时使用 d3.pointer( ) 的方法来建立水平轴线跟资料的标示


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

function handleMouseOver(d, i){
      // console.log(d.target.__data__)
      const pt = d3.pointer(event, svg.node())

      // 加上文字标签
      svg.append('text')
         .attr('class', 'infoText')
         .attr('y', yScale(d.target.__data__['value']))
         .attr("x", margin*2)
         .style('fill', '#000')
         .style('font-size', '18px')
         .style('font-weight', 'bold')
         .style("text-anchor", 'middle')
         .text(d.target.__data__['value'] + '千人')
    
      // 加上轴线
      svg.append('line')
         .attr('class', 'dashed-Y')
         .attr('x1', margin*2)
         .attr('y1', yScale(d.target.__data__['value']))
         .attr('x2', pt[0])
         .attr('y2', yScale(d.target.__data__['value']))
         .style('stroke', 'black')
         .style('stroke-dasharray', '3' )
 }

function handleMouseLeave(){
  // 移除文字、轴线标签
  svg.select('.infoText').remove()
  svg.select('.dashed-Y').remove()
}

大功告成~


Github Page 图表与 Github 程序码

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


<<:  Day_23: 让 Vite 来开启你的Vue 之 <script setup>

>>:  Day21 样式变化(动画)5

apt-get upgrade 和dist-upgrade的差别

Debian/Ubuntu Linux都使用apt,升级时都是: apt-get update ap...

企划实现(10)

FB登入 第一步:到FB官网并创建帐号 https://developers.facebook.co...

自己来一场30天不间断的文章之旅吧

五年前,我从资管系毕业。当时的我告诉自己,未来不会踏上跟写程序相关的工作。往餐饮业、服务业发展什麽...

OpenProject安装在 ubuntu 20.04 LTS

Install OpenProject with DEB/RPM packages Import t...

ASP.NET MVC 从入门到放弃(Day11) -C# 连线资料库介绍( ADO.NET )

接着来讲讲资料库连线的部分.... Mysql 类别Class public class Categ...