学习D3.js(十)环形饼图

831 阅读3分钟

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

开始绘制

引入D3模块

  • 引入整个D3模块。
<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/4 用于唤醒半径
    var radius = height / 4

绘制扇形

    // 饼图(pie)生成器  计算图所需要的角度信息
    // .padAngle()  饼图扇形之间的间隔设置
    // .startAngle()  起始角度设置
    // .endAngle()  终止角度设置
    // 最后传入数据 返回饼图数据
    let drawData = d3
      .pie()
      .value(function (d) {
        return d.value
      })
      .padAngle(0.02)
      .startAngle(0)
      .endAngle(Math.PI * 2)(dataArr)

    // .arc() 是shape中的 弧形生成器
    // innerRadius() 设置内半径
    // outerRadius() 设置外半径
    // cornerRadius() 设置拐角圆滑
    let arc = d3.arc().innerRadius(50).outerRadius(radius).cornerRadius(5)
  • 在创建饼图生成器时,传入数据提前准换好饼图数据。
 const arcs = chart
      .append('g')
      .attr('transform', 'translate( ' + (radius * 2 - margin) + ', ' + (radius * 2 - margin) + ' )')

    arcs
      .selectAll()
      .data(drawData)
      .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 fn = d3.interpolate(
          {
            endAngle: d.startAngle
          },
          d
        )
        return function (t) {
          return arc(fn(t))
        }
      })

1.gif

  • 创建饼图绘制组g,设置好组位置。
  • 绑定好饼图数据,通过弧形生成器绘制扇形。
  • 最后添加上弧形过度动画。

绘制文本数据

// 求和
let sum = d3.sum(dataArr, (d) => d.value)
  • 获取总和,用于计算扇形占比。
   arcs
      .selectAll()
      .data(drawData)
      .enter()
      .append('text')
      .attr('transform', function (d) {
        // arc.centroid(d) 将文字平移到弧的中心
        return 'translate(' + arc.centroid(d) + ') '
      })
      // 文字开始点在文字中间
      .attr('text-anchor', 'middle')
      .attr('fill', '#fff')
      // 文字垂直居中
      .attr('dominant-baseline', 'central')
      .attr('font-size', '10px')
      // 格式化文字显示格式
      .text(function (d) {
        return ((d.data.value / sum) * 100).toFixed(2) + '%'
      })

image.png

  • 文本数据属于饼图的,添加在饼图绘制组g下。
  • 绑定饼图数据,获取扇形的中心位置。
  • 最后设置文本时计算占比。

绘制标签

  • 这里和前面的基础饼图一样。创建一个大几倍的扇形生成器,取其中心绘制标签。
  • 计算文本偏移量,设置文本偏移。
// 文本
    const arc2 = d3
      .arc()
      .outerRadius(radius * 2.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)
      .enter()
      .append('text')
      .attr('class', (d) => `text${d.index}`)
      .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)
  • 同理的绘制连线方式也是一样。
  • 根据饼图数据计算出连线点,创建线生成器,绘制连线。
    // 生成连线的点
    const linePoints = drawData.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
    })

    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) {
            if (outerRadius > 0) {
              d3.select(`.text${d.index}`).attr('stroke', '#AAACC2')
            } else {
              d3.select(`.text${d.index}`).attr('stroke', 'steelblue')
            }

            let interpolate = d3.interpolate(radius, radius + outerRadius)
            return function (t) {
              let arcT = d3.arc().innerRadius(50).outerRadius(interpolate(t)).cornerRadius(5)
              return arcT(d)
            }
          })
      }
    }
    d3.selectAll('.pieArc').on('mouseover', arcTween(20)).on('mouseout', arcTween(0))

2.gif

  • 通过扇形标识符获取扇形对象组。
  • 创建交互函数,在监听事件中获取对应的扇形对象。
  • 通过扇形对象获取绑定的饼图数据。
  • 通过唯一标识符修改文本标签颜色,和当前扇形对象的大小。
  • 代码地址