学习D3.js(十一)饼图-南丁格尔图

360 阅读3分钟

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

开始绘制

引入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 = 120
    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)

// 线性比例尺 根据值 获取扇形的半径长度
let scaleRadius = d3
  .scaleLinear()
  .domain([0, d3.max(dataArr.map((d) => d.value))])
  .range([0, d3.min([width - 4 * margin, height - 4 * margin]) * 0.5])
  • 创建线性比例,根据值获取对应的半径长度。

绘制扇形

//  饼图(pie)生成器  计算图所需要的角度信息
let drawData = d3
  .pie()
  .value(function (d) {
    return d.value
  })
  .startAngle(0)
  .endAngle(Math.PI * 2)(dataArr)
  • 创建饼图生成器,得到每个扇形的图形数据。
    // 图形绘制组
    const arcs = chart
      .append('g')
      .attr('class', 'pie')
      .attr('transform', 'translate(' + (width - 2 * margin) / 2 + ',' + (height - 2 * margin) / 2 + ')')
      .attr('stroke', 'steelblue')
      .attr('stroke-width', 1)

    // 扇形
    arcs
      .selectAll()
      .data(drawData)
      .enter()
      .append('path')
      .attr('class', (d, i) => 'arc arc-' + i)
      .attr('fill', (d, i) => colorScale(i))
      .transition()
      .duration(1000)
      .attrTween('d', arcTween)

    function arcTween(d) {
      // 半径插值
      const interpolate = d3.interpolate(0, scaleRadius(d.value))
      // 弧度插值
      let fn = d3.interpolate(
        {
          endAngle: d.startAngle
        },
        d
      )
      return function (t) {
        let arc = d3.arc().outerRadius(interpolate(t)).innerRadius(0)
        return arc(fn(t))
      }
    }

2.gif

  • 创建饼图绘制组g,设置好位置和样式。
  • 绑定饼图数据,绘制扇形。
  • 在动画函数中,通过比例尺获取对应值的半径。
  • 在动画函数中创建弧形生成器,保证每个扇形都使用自己的弧形生成器。

绘制标签

    const textOffsetH = 10
    // 文本
    // 线性比例尺 文本 根据弧度获取偏移值
    const scaleTextDx = d3
      .scaleLinear()
      .domain([0, Math.PI / 2])
      .range([textOffsetH, textOffsetH * 3])

    /**
     * 根据 扇形弧度
     * 计算文本水平偏移
     *
     * @param d (pie)生成器的 数据
     * @returns 文本偏移值
     */
    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
    }
  • 创建文本偏移计算函数。这个函数是为了优化文本显示,非必要函数。
    /**
     * @param outerRadius 半径
     * @param d (pie)生成器的 数据
     * @param averageLength 是否放大半径
     * @returns 扇形 中心点
     */
    function getArcCentorid(outerRadius, d, averageLength) {
      if (averageLength) outerRadius = Math.sqrt(outerRadius * 600)

      return d3.arc().outerRadius(outerRadius).innerRadius(0).centroid(d)
    }
  • 因为每个扇形的半径不同,创建中心点获取函数。用于获取扇形中心点或获取文本所在中心点。放大半径为文本所在位置。
// 文本
arcs
  .selectAll()
  .data(drawData)
  .enter()
  .append('text')
  .attr('text-anchor', (d) => {
    return (d.endAngle + d.startAngle) / 2 > Math.PI ? 'end' : 'start'
  })
  .attr('dy', '0.35em')
  .attr('dx', computeTextDx)
  .attr('transform', (d) => {
    return 'translate(' + getArcCentorid(scaleRadius(d.value), d, true) + ')'
  })
  .text((d) => d.data.label + ': ' + d.data.value)

image.png

  • 在饼图绘制组下绘制文本标签。
  • 在设置位置时,先使用scaleRadius()获取当前扇形对象的半径,在调用中心点获取方法。
    // 生成连线的点
    const linePoints = drawData.map((d) => {
      const line = []
      // 文本位置点
      const tempPoint = getArcCentorid(scaleRadius(d.value), d, true)
      const tempDx = computeTextDx(d)
      const dx = tempDx > 0 ? tempDx - textOffsetH : tempDx + textOffsetH
      line.push(getArcCentorid(scaleRadius(d.value) * 2, 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')
      .attr('fill', 'none')
      .attr('stroke', 'steelblue')
      .attr('d', generateLine)

image.png

  • 通过饼图数据,获取扇形对象的连线点数组。第一个点:扇形对象的中心点扩大两倍。第二个点:文本未偏移的位置。第二个点:文本偏移后的位置。当不计算文本偏移时连线就只需要两个点。
  • 创建线生成器,在饼图绘制组下绘制连线。

添加交互

    /**
     * 弧形动画 
     * */
    function arcTweenMouse(type) {
      // 设置缓动函数,为鼠标事件使用
      return function () {
        d3.select(this)
          .transition()
          .attrTween('d', function (d) {
            let interpolate = null
            if (type) {
              interpolate = d3.interpolate(scaleRadius(d.value), scaleRadius(d.value + 20))
            } else {
              interpolate = d3.interpolate(scaleRadius(d.value + 20), scaleRadius(d.value))
            }
            return function (t) {
              let arc = d3.arc().outerRadius(interpolate(t)).innerRadius(0)
              return arc(d)
            }
          })
      }
    }
    d3.selectAll('.arc').on('mouseover', arcTweenMouse(true)).on('mouseout', arcTweenMouse(false))

3.gif

  • 创建弧形动画函数,控制扇形的放大缩小。
  • 代码地址