Day24-D3 基础图表:堆叠长条图

本篇大纲:d3.stack( ) 的用法、本次范例的画面与互动效果、绘制堆积长条图

今天的一天一图表,来到长条图三部曲的终章 — 堆积长条图!
https://ithelp.ithome.com.tw/upload/images/20211006/20134930XGiML8dYYA.jpg

堆积长条图的绘制相对困难一点,我们需要使用到 d3.scaleBand( )d3.stack( ) 这两个API。

d3.stack( ) 的用法

这个方法主要是用来绘制堆积图表,我们会使用 d3.stack( ) 的方法来换算每个数据的占比,接着再把这些数据呈现在长条图上。如果不清楚它要怎麽使用、会生成哪些方法、提供什麽样的数据的话,我在 Day9 的 Layouts 章节有详细的解说。

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

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

  • 基本堆积长条图呈现
  • 滑鼠 hover 时堆积图透明度下降,呈现此堆积图的数值
    https://i.imgur.com/FH02D02.gif

绘制堆积长条图

我们这次沿用昨天的资料(台南市劳动人口)来制作~首先,一样先建立 svg 并把资料取回来

// css
.chart{
    width: 100%;
    min-width: 300px;
    margin: auto;
}
// html
<div class="chart"></div>
// js

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);

接下来,我们把 X轴跟 Y轴要用到的资料分别整理出来,并依此来建立 X轴跟 Y轴。

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

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

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, 1200])
                .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轴分组之外,我们还需要整理一个 subgroups 资料集。这个资料集是把我们想要分组的资料拉出来,之後用来建立堆积图的。以这边的资料来说,我们希望能分成四组:

  • 15-24岁[千人]
  • 25-44岁[千人]
  • 45-64岁[千人]
  • 65岁及以上[千人]

因此,我们用 Object.keys(data[0]).slice(1) 把这四个组别拉出来。分组的资料拉出来後,接着就是用 d3.stack( ) 的方法,把这些资料变成堆积图可以使用的数据。

// 拉出要分组的资料
const subgroups =  Object.keys(data[0]).slice(1)

// 用 d3.stack() 把资料堆叠起来
const stackedData = d3.stack()
                      .keys(subgroups)(data)

接着,我们用 scaleOrdinal 的方法来设定 subgorup 资料的颜色

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

再来就是建立堆积图表啦!我们把用 d3.stack 建立好的资料带进去,并使用它提供的资料去建立 < rect >

const bar = svg.append('g')
                 .selectAll('g')
                 .data(stackedData)
                 .join('g')
                 .attr('fill',  d => color(d.key))
                 .selectAll('rect')
                 .data(d=>d)
                 .join('rect')
                 .attr("x", d => xScale(d.data['年度']))
                 .attr("y", d => yScale(d[1]))
                 .attr("height", d => yScale(d[0]) - yScale(d[1]))
                 .attr("width",xScale.bandwidth())

这样基本的长条堆叠图表就完成了

https://ithelp.ithome.com.tw/upload/images/20211006/20134930plQzaW1esZ.jpg

别走,还没结束!我们还有动画跟下方的标签说明要做呢,先来加动画吧

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

// 设定文字标签
const textTag = svg.append('text')
                  .attr('class', 'infoText')
                  .style('fill', '#000')
                  .style('font-size', '18px')
                  .style('font-weight', 'bold')
                  .style("text-anchor", 'middle')
                  .style('opacity', '0')

function handleMouseOver(d, i){
  const pt = d3.pointer(event, svg.node())

  d3.select(this)
    .style('opacity', '0.5')

  // 加上文字标签
  textTag
     .style('opacity', '1')
     .attr("x",  pt[0])
     .attr('y', pt[1]-20)
     .text((d.target.__data__[1] - d.target.__data__[0]) + '千人')
}

function handleMouseLeave(){
  d3.select(this)
    .style('opacity', '1')
  
  textTag.style('opacity', '0')
}

最後再加上最下方的标签就可以了~

// 加上辨识标签
const tagsWrap =  svg.append('g')
     .selectAll('g')
     .attr('class', 'tags')
     .data(subgroups)
     .enter()
     .append('g')

if(window.innerWidth < 780){
  tagsWrap.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)

完成!一天又平安的结束惹 (下台一鞠躬)~


Github Page 图表与 Github 程序码

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


<<:  {DAY 24} Pandas 学习笔记part.10

>>:  Day 21 - Linux 与服务相关的攻击

[Day27] 第二十七课 Azure巢状虚拟化-2 [进阶]

昨天提到,当Azure Hyper-V需要被其它Azure VM内网存取的时候, 该如何调整呢?内网...

Day 12 强化学习 (Reinforcement Learning)

强化学习什麽是? 简称RL,在没有以往资料的前提下,将模型放到使用环境中,透过一些操作观察环境状态,...

[Day01] 网站一条龙 - 从架站到前端

庞大的需求催生就业机会 在这人手一机,每个人都吃到饱的时代,不管什麽产业的公司行号组织单位,都需要有...

从 IT 技术面细说 Search Console 的 27 组数字 KPI (26) :Search Console 的 Bug

Google 虽然已经是在网路圈是稳定度相当高的一间公司与服务,但事实上挂的机会还是很高,主要也是 ...

CSS-Model 盒模型

前言 在HTML中每一个元素都会被CSS当作一个矩形盒子,CSS可以更改其宽度、高度、跟其他元素的距...