学习D3.js(九)基础饼图

1,126 阅读3分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第3天,点击查看活动详情

开始绘制

引入D3模块

  • 这里引入的是整个D3模块。需要按模块引入可以在github上查找模块CDN地址。
<script src="https://d3js.org/d3.v7.min.js"></script>

数据

  • 自定义数据格式,主要方便后面使用。
    var dataArr = [
      {
        label: '1月',
        value: 10.5
      },
      {
        label: '2月',
        value: 70.5
      },
      {
        label: '3月',
        value: 60.5
      },
      {
        label: '4月',
        value: 10.5
      },
      {
        label: '5月',
        value: 20.5
      },
      {
        label: '6月',
        value: 30.5
      }
    ]

添加画布

  • 初始化画布。
    var width = 700
    var height = 700
    var margin = 60
    var svg = d3
      .select('.d3Chart')
      .append('svg')
      .attr('width', width)
      .attr('height', height)
      .style('background-color', '#1a3055')
    // 图
    var chart = svg.append('g').attr('transform', `translate(${margin}, ${margin})`)

创建比例尺

  • 根据需求创建比例迟。
// 序数比例尺 - 颜色
let colorScale = d3.scaleOrdinal().domain(d3.range(0, dataArr.length)).range(d3.schemeCategory10)
  • 一个简单的饼图只需要颜色比例尺,计算每个扇形颜色。
  1. d3.schemeCategory10 数组,十个D3内置颜色。

绘制扇形

// 半径
var radius = (Math.min(width, height) * 0.5) / 2
// .pie() 是shape中的 饼图(pie)生成器 计算图所需要的角度信息
// .startAngle()  起始角度设置
// .endAngle()  终止角度设置
// .padAngle()  饼图扇形之间的间隔设置
let drawData = d3
  .pie()
  .value(function (d) {
    return d.value
  })
  .startAngle(0)
  .endAngle(Math.PI * 2)

// .arc() 是shape中的 弧形生成器
// innerRadius() 设置内半径
// outerRadius() 设置外半径
// cornerRadius() 设置拐角圆滑
let arc = d3.arc().innerRadius(0).outerRadius(100)
  • 设置好饼图的半径。
  • d3.pie() 创建饼图生成器。把数据转换为饼图数据。
  • d3.arc() 创建弧形生成器,根据饼图数据绘制扇形。
const arcs = chart
  .append('g')
  .attr('transform', 'translate( ' + (radius * 2 - margin) + ', ' + (radius * 2 - margin) + ' )')
  • 创建饼图绘制组g,居中绘制扇形。
    arcs
      .selectAll()
      .data(drawData(dataArr))
      .enter()
      .append('path')
      .attr('class', 'pieArc')
      .attr('stroke', 'steelblue')
      .attr('stroke-width', 1)
      .attr('fill', function (d) {
        return colorScale(d.index)
      })
      .attr('d', function (d, i) {
        // 根据 pie 数据 计算路径
        return arc(d)
      })
      .transition()
      .duration(1000)
      .attrTween('d', function (d) {
        // 初始加载时的过渡效果
        let interpolate = d3.interpolate(
          {
            endAngle: d.startAngle
          },
          d
        )
        return function (t) {
          return arc(interpolate(t))
        }
      })

1.gif

  1. d3.interpolate() 插值器。可以简单理解为一个线性比例尺,输入范围是0 ~ 1,输出为传入的两个对象中,对应字段之间的值。如上,第一个对象就一个字段endAngle,第二个对象是饼图数据,在第二个对象中存在endAngle字段。输出的范围对应两个endAngle字段之间的值。
  • 使用创建好的饼图生成器drawData(dataArr),转换数据并绑定到饼图绘制组g上。
  • 在生成d属性时,使用弧形生成器。绘制扇形。
  • 添加动画,使用d3.interpolate()使弧形长度,从开始值变化到结束值。

绘制标签

// 
const arc2 = d3
  .arc()
  .outerRadius(100 * 3.5)
  .innerRadius(0)
  
/*
 * 计算文本水平偏移
 **/
const textOffsetM = 10
const scaleTextDx = d3
  .scaleLinear()
  .domain([0, Math.PI / 2])
  .range([textOffsetM, textOffsetM * 3])

function computeTextDx(d) {
  // 计算文本水平偏移
  const middleAngle = (d.endAngle + d.startAngle) / 2
  let dx = ''
  if (middleAngle < Math.PI) {
    dx = scaleTextDx(Math.abs(middleAngle - Math.PI / 2))
  } else {
    dx = -scaleTextDx(Math.abs(middleAngle - (Math.PI * 3) / 2))
  }
  return dx
} 
  • 创建新的弧形生成器,扩大弧形半径。用于后面获取文本位置。
  • 创建文本偏移计算方法。对绘制文本影响不大,可不使用。
arcs
  .selectAll()
  .data(drawData(dataArr))
  .enter()
  .append('text')
  .attr('text-anchor', (d) => {
    return (d.endAngle + d.startAngle) / 2 > Math.PI ? 'end' : 'start'
  })
  .attr('stroke', 'steelblue')
  .attr('dy', '0.35em')
  .attr('dx', computeTextDx)
  .attr('transform', (d) => {
    return 'translate(' + arc2.centroid(d) + ')'
  })
  .text((d) => d.data.label + ': ' + d.data.value)
  1. arc.centroid() 获取扇形的中心点。
  • 这里需要注意,绘制文本需要在(arcs)饼图绘制组(g)上,文本的位移才是正确的。
  • 使用translate(' + arc2.centroid(d) + ')获取扩大扇形的中心点,平移文本到对应位置。
  • 通过饼图数据,获取文本信息组合绘制。

image.png

绘制连线

const linePoints = drawData(dataArr).map((d) => {
  const line = []
  const tempPoint = arc2.centroid(d)
  const tempDx = computeTextDx(d)
  const dx = tempDx > 0 ? tempDx - textOffsetM : tempDx + textOffsetM
  line.push(arc.centroid(d))
  line.push(tempPoint)
  line.push([tempPoint[0] + dx, tempPoint[1]])
  return line
})
  • 循环饼图数据,得到arc.centroid(d)饼图中扇形中心点,tempPoint文本所在位置,和文本偏移后的位置。组合为线数据组。
const generateLine = d3
  .line()
  .x((d) => d[0])
  .y((d) => d[1])

arcs
  .selectAll()
  .data(linePoints)
  .enter()
  .insert('path', ':first-child')
  .classed('line', true)
  .attr('fill', 'none')
  .attr('stroke', 'steelblue')
  .attr('d', generateLine)
  • 创建线生成器,绘制线。

image.png

添加交互

/**
 * 弧形动画
 * */
function arcTween(outerRadius) {
  return function () {
    d3.select(this)
      .transition()
      .attrTween('d', function (d) {
        let interpolate = d3.interpolate(100, 100 + outerRadius)
        return function (t) {
          let arcT = d3.arc().outerRadius(interpolate(t)).innerRadius(0)
          return arcT(d)
        }
      })
  }
}

d3.selectAll('.pieArc').on('mouseover', arcTween(20)).on('mouseout', arcTween(0))
  • 创建动画函数,控制扇形的放大缩小。
  • 代码地址