zzxworld

使用 D3.js(v7) 绘制饼图

昨天写了一篇介绍 D3.js 的入门级文章,以示例的方式由浅入深的讲述了如何用 D3 绘制出一个带坐标轴的柱形图。今天继续这一话题,来看看如何画饼图。

本文中的图表代码和运行环境和上一篇绘制柱形图的环境相似,所以这里就不再重复叙述。有需要可以通过查看上一篇文章中的「准备工作」部分来了解。

另外,本文延续了上一篇文章的风格,「由浅入深」的总结了如何绘制出一个有不同颜色,以及带图例和鼠标交互功能的饼图。

简单的饼图

先来看看要画出一个最简单的饼图所需要的代码:

var dataset = [66, 10, 9]

var svg = d3.select('svg')
var g = svg.append('g').attr('transform', 'translate(75, 75)')
var pie = d3.pie().value(v => { return v })

g.selectAll('path').data(pie(dataset)).enter()
    .append('path')
    .attr('d', d3.arc().innerRadius(0).outerRadius(70))
    .attr('stroke', 'white')
    .attr('stroke-width', 2)

解释一下上面代码中的流程和关键点:

  • dataset 是要图形化的数据。
  • 饼图通过 SVG 的路径标签 d 来绘制,并使用了 D3 提供的 pie() 函数来创建路径显示数据。
  • strokestroke-width 分别设置了路径边框和宽度。

来看看上面代码画出的饼图:

使用不同的颜色

上面的饼图每块都是全黑的,虽然有白色的分割线来区分,但不够显眼。通常的做法是让每一块都拥有不同的颜色。来看看添加顔色后的代码:

var dataset = [66, 10, 9]

var svg = d3.select('svg')
var g = svg.append('g').attr('transform', 'translate(75, 75)')
var pie = d3.pie().value(v => { return v })
var colors = d3.scaleOrdinal(['#69C', '#FC9', '#9C6'])

g.selectAll('path').data(pie(dataset)).enter()
    .append('path')
    .attr('d', d3.arc().innerRadius(0).outerRadius(70))
    .attr('stroke', 'white')
    .attr('stroke-width', 2)
    .attr('fill', colors)

主要有两处调整:

  • 使用 D3 提供的 scaleOrdinal 方法创建了颜色迭代方法。
  • 使用 fill 属性代入颜色。

来看看添加颜色后的饼图效果:

添加图例

饼图图例的作用和柱形图的坐标轴一样,是锦上添花,或者说是不可或缺的元素。能显著提高图表的直观体验。添加图例后的饼图代码如下:

var dataset = [66, 10, 9]

var svg = d3.select('svg')
var g = svg.append('g').attr('transform', 'translate(75, 75)')
var pie = d3.pie().value(v => { return v })
var colors = d3.scaleOrdinal(['#69C', '#FC9', '#9C6'])

g.selectAll('path').data(pie(dataset)).enter()
    .append('path')
    .attr('d', d3.arc().innerRadius(0).outerRadius(70))
    .attr('stroke', 'white')
    .attr('stroke-width', 2)
    .attr('fill', colors)

// 绘制图例区块
var legend = svg.append('g').attr('transform', 'translate(200, 50)')

// 绘制图例圆点图标
legend.selectAll('rect').data(dataset).enter()
    .append('circle')
    .attr('r', 7)
    .attr('cy', (v, i) => { return i*20+5 })
    .attr('fill', colors)

// 绘制图例文字数值
legend.selectAll('text').data(dataset).enter()
    .append('text')
    .text((v, i) => { return v })
    .attr('x', 15)
    .attr('y', (v, i) => { return i*20+10 })
    .attr('font-size', '0.8em')
    .attr('text-auchor', 'middle')

新增的代码主要在后半段,为了方便了解相关代码的作用,我在每段代码的起始处添加了注释。代码看起来很多,其实就干了两件事:

  1. 用圆形标签显示对应数据集的颜色。
  2. 用文本标签显示对应数据集的数字。

有了图例后的效果:

添加鼠标事件

最后给这个饼图添加一点简单的鼠标效果,当鼠标移入饼图时,显示对应区块的具体数值。来看看最新的代码:

var dataset = [66, 10, 9]

var svg = d3.select('svg')
var g = svg.append('g').attr('transform', 'translate(75, 75)')
var pie = d3.pie().value(v => { return v })
var colors = d3.scaleOrdinal(['#69C', '#FC9', '#9C6'])

// 鼠标移入饼图区域
var handleMouseOver = function (e, v) {
    d3.select(this).attr('opacity', 1)

    var pos = d3.pointer(e)
    var x = pos[0]+95
    var y = pos[1]+75
    var tooltip = svg.append('g').attr('class', 'tooltip').attr('transform', 'translate('+x+','+y+')')
    var text = tooltip.append('text').text(v.value).attr('text-anchor', 'middle')
    var box = text.node().getBBox()

    tooltip.insert('rect', 'text')
        .attr('x', box.x)
        .attr('y', box.y)
        .attr('width', box.width)
        .attr('height', box.height)
        .attr('fill', 'white')
        .attr('opacity', 0.7)
}

// 鼠标在饼图区域移动
var handleMouseMove = function (e) {
    var pos = d3.pointer(e)
    var x = pos[0]+95
    var y = pos[1]+75

    d3.select('.tooltip')
        .attr('transform', 'translate('+x+','+y+')')
}

// 鼠标移出饼图区域
var handleMouseOut = function () {
    d3.select(this).attr('opacity', 0.8)
    d3.select('.tooltip').remove()
}

g.selectAll('path').data(pie(dataset)).enter()
    .append('path')
    .attr('d', d3.arc().innerRadius(0).outerRadius(70))
    .attr('stroke', 'white')
    .attr('stroke-width', 2)
    .attr('fill', colors)
    .attr('opacity', 0.8)
    .on('mouseover', handleMouseOver) // 绑定鼠标移入事件处理方法
    .on('mousemove', handleMouseMove) // 绑定鼠标移动事件处理方法
    .on('mouseout', handleMouseOut) // 绑定鼠标移出事件处理方法

var legend = svg.append('g').attr('transform', 'translate(200, 50)')

legend.selectAll('rect').data(dataset).enter()
    .append('circle')
    .attr('r', 7)
    .attr('cy', (v, i) => { return i*20+5 })
    .attr('fill', colors)

legend.selectAll('text').data(dataset).enter()
    .append('text')
    .text((v, i) => { return v })
    .attr('x', 15)
    .attr('y', (v, i) => { return i*20+10 })
    .attr('font-size', '0.8em')

添加的代码看起来有点长,主要看有注释的部分。也就关联了 3 个鼠标事件,分别如下:

  • mouseover 鼠标移入事件中,创建文本提示框,并根据鼠标位置设置初始坐标。并调整所在区块的透明度,让其看起来更显眼。
  • mousemove 鼠标移动事件中,随时更新提示框的显示位置,实现鼠标跟随效果。
  • mouseout 鼠标移出事件中,销毁提示框。另外还原饼图区块的透明度。

这个饼图最终的效果如下:

最后提供个小彩蛋:把 d3.arc().innerRadius(0) 这个方法中的 0 改成更大的数字,会得到一个环状图哦。