zzxworld

使用 D3.js(v7) 绘制柱形图

D3.js 是一个使用 JavaScript 的数据可视化工具库。和其他一些专注于图表类的 JS 库不同,它的功能更加丰富,可自定义性也更强。本文是一篇入门级教程和指南,记录并总结了如何通过 D3.js 一步步来实现一个柱形图表的过程。

准备工作

首先需要下载 D3.js,下载地址如下:

下载后解压文件,在 dist 目录中找到 d3.js 文件,复制到项目目录备用。然后创建一个 HTML 文件,并引入 d3.js,文件代码如下:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>D3.js Demo</title>
</head>
<body>
    <div style="text-align: center; padding-top: 2em;">
        <svg style="box-shadow:0 0 0.5em rgba(0, 0, 0, 0.2)"></svg>
    </div>
<script src="/assets/d3/d3.js"></script>
<script charset="utf-8">
    // 在此处写图表代码
</script>
</body>
</html>

注意上面代码中第二个 script 标签,后续所有图表的代码都会放在这个位置。在浏览器中访问这个 HTML 文件,应该会显示一个带阴影的空白框:

通过浏览器开发工具,确认 d3.js 被成功加载。

准备好 D3.js 的图表开发环境后,接下来将由浅人深的展示如何实现一个带数值显示以及坐标的柱形图。

极简的柱形图

数据可视化离不开数据,先准备一组简单的数据:180, 80, 100, 280。现在的目的就是通过柱形图直观的来反映出它们的大小,来看看实现这一目的的图表代码:

var dataset = [180, 80, 100, 280]
var svg = d3.select('svg')

svg.selectAll('rect').data(dataset).enter()
    .append('rect')
    .attr('height', 30-5)
    .attr('width', v => { return v })
    .attr('x', 0)
    .attr('y', (v, i) => { return 30*i+5 })
    .attr('fill', 'blue')

把这些代码放到 HTML 文件中图表代码的位置,然后刷新浏览器,会看到柱形图显示出来了。

稍微解释一下上面的代码:

  • dataset 定义了要可视化的数据集。
  • svg 是 D3.js 的实例化对象,通过 select 方法关联到了 HTML 中的 <svg> 这个标签上。后面的代码都是使用「连缀方法」定义了这个对象的各种数据和属性。
  • data 方法很好理解,就是绑定数据集。
  • selectAll 这个方法有点特殊,它定义了图表中柱形体的选择规则,需要和后面的 append 方法结合来使用。按照正常的使用逻辑,应该是先 append 柱形节点,然后再通过 selectAll 来选择。这里要把 selectAll 当作延迟绑定方法来理解。
  • attr 方法用来定义图表柱形体的各种属性。heightwidth 定义了高和宽。xy 定义位置。fill 设置了填充颜色。这里唯一要注意的是后面的值支持「闭包」方法迭代数据集中的每个对象来对每个柱形体进行细微调整。

柱形体的自适应显示

上面的柱形图虽然能显示了,但存在一个问题:当数据集中有超过 <svg> 标签的长度的数字,或是追加了更多数字后,会无法正确显示相关的柱形图。比如把 dataset 的值改成下面这样:

var dataset = [180, 80, 100, 300, 500, 150]

显示结果就会是这样:

300 和 500 这两个值的柱形图长度完全看不出区别。而最后一个数字 150 则是完全没有显示了。这是因为目前柱形图的长度和宽度都是使用的绝对值。无法在有限的 <svg> 区域中显示它们。解决这个问题的办法是采用相对值来显示。

来看看修改后的图表代码:

var dataset = [180, 80, 100, 300, 500, 150]
var barHeight = 150/dataset.length
var barLength = d3.scaleLinear()
    .domain([0, d3.max(dataset)])
    .range([0, 300])
var barX = d3.scaleLinear()
    .domain([0, dataset.length])
    .range([0, 150])
var svg = d3.select('svg')

svg.selectAll('rect').data(dataset).enter()
    .append('rect')
    .attr('height', barHeight-5)
    .attr('width', barLength)
    .attr('x', 0)
    .attr('y', (v, i) => { return barX(i) })
    .attr('fill', 'blue')

这次主要的变动如下:

  • 添加了 barHeight 变量。用来设置每个柱体的高度,它通过图表可显示高度(150)与数据集的数量计算而来。通过这个方式实现了柱体的高度自适应显示。
  • 添加了 barLength 变量。用到了 D3.js 提供的线性缩放工具方法 scaleLinear。通过它的 domain 函数设置数据集数值的最小和最大值,还有 range 函数用来设置可显示范围。最后会返回一个函数对象,用来动态计算柱体的显示长度。
  • 添加了 barX 变量。用到的工具和 barLength 类似。用来实现动态计算柱体的纵轴显示位置。

来看看调整后的显示效果:

完美的通过柱体显示了所有数值的大小差异。有兴趣可以试着调整下 dataset 中任意数值的大小,增加或删除几个数字,都能完整并正常显示。

显示数值

通过柱形图例虽然已经能很直观的看出数值大小差异了,但加上实际数值会更有利于确认这种差异。所以继续完善这个图表,看看如何在每个柱形图的末端把实际数值显示出来。下面是修改后的图表代码:

var dataset = [180, 80, 100, 300, 500, 150]
var barHeight = 150/dataset.length
var barLength = d3.scaleLinear()
    .domain([0, d3.max(dataset)])
    .range([0, 300])
var barX = d3.scaleLinear()
    .domain([0, dataset.length])
    .range([0, 150])
var svg = d3.select('svg')

svg.selectAll('rect').data(dataset).enter()
    .append('rect')
    .attr('height', barHeight-5)
    .attr('width', barLength)
    .attr('x', 0)
    .attr('y', (v, i) => { return barX(i) })
    .attr('fill', 'blue')

svg.selectAll('text').data(dataset).enter()
    .append('text')
    .text(v => { return v })
    .attr('x', v => { return barLength(v)-5 })
    .attr('y', (v, i) => { return barX(i)+barHeight/1.6 })
    .attr('fill', 'white')
    .attr('font-size', '0.85em')
    .attr('text-anchor', 'end')

这段代码的前半部分没有任何变化,就最后追加了一段看起来和绘制柱形体结构相似的代码。不过 selectAllappend 方法使用的是 text。这在 SVG 中是专门用来处理文本的标签。结构和使用的方法与上面绘制柱形的功能相似,这里就不一一解释了。直接来看效果:

显示坐标轴

坐标轴是一个完整柱形图必不可少的元素,接下来就以添加坐标轴作为本文的结尾吧。添加坐标轴后的最新图表代码如下:

var dataset = [180, 80, 100, 300, 500, 150]
var barHeight = 150/dataset.length
var barLength = d3.scaleLinear()
    .domain([0, d3.max(dataset)])
    .range([0, 260])
var barX = d3.scaleLinear()
    .domain([0, dataset.length])
    .range([0, 125])
var svg = d3.select('svg')
var g = svg.append('g')
    .attr('transform', 'translate(20, 20)')

g.selectAll('rect').data(dataset).enter()
    .append('rect')
    .attr('height', barHeight-5)
    .attr('width', barLength)
    .attr('x', 0)
    .attr('y', (v, i) => { return barX(i) })
    .attr('fill', 'blue')

g.selectAll('text').data(dataset).enter()
    .append('text')
    .text(v => { return v })
    .attr('x', v => { return barLength(v)-5 })
    .attr('y', (v, i) => { return barX(i)+barHeight/1.6})
    .attr('fill', 'white')
    .attr('font-size', '0.85em')
    .attr('text-anchor', 'end')

svg.append('g')
    .attr('transform', 'translate(20, 20)')
    .call(d3.axisTop(barLength))

var labelScale = d3.scaleBand().range([0, 125]).domain(dataset.keys())

svg.append('g')
    .attr('transform', 'translate(20, 20)')
    .call(d3.axisLeft(labelScale))

图表代码整体有了些调整,关键就两点:

  • 新增了一个 g 元素标签,并且把柱状图放到了这个标签里。这么做的原因是为了给坐标轴腾出显示空间。
  • 通过最后插入的两个 g 元素标签来显示坐标轴。坐标轴的绘制借助了 D3.js 的 axisTopaxisLeft 方法。看它们的名字就能明白,一个是顶部坐标轴,一个是左侧坐标轴。另外左侧坐标轴使用 D3.js 的 scaleBand 方法来创建数据集 key 值的刻度值。

依赖于 D3.js 提供的工具,很简单的就完成了坐标轴的添加。来看看最终效果: