学习D3.js(十九)旭日图

530 阅读2分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第6天,点击查看活动详情

引入D3模块

  • 引入整个D3模块。
<!-- D3模块 -->
<script src="https://d3js.org/d3.v7.min.js"></script>

数据

const data = {
      name: 'A',
      children: [
        {
          name: 'a',
          children: [
            {
              name: 'a-a',
              children: [
                {
                  name: 'a-aa',
                  value: 2
                }
              ]
            },
            {
              name: 'a-b',
              value: 4
            }
          ]
        },
        {
          name: 'b',
          children: [
            {
              name: 'b-a',
              children: [
                {
                  name: 'b-aa',
                  value: 2
                }
              ]
            }
          ]
        },
        {
          name: 'c',
          children: [
            {
              name: 'c-a',
              children: [
                {
                  name: 'c-aa',
                  value: 2
                },
                {
                  name: 'c-ab',
                  value: 5
                },
                {
                  name: 'c-ac',
                  children: [
                    {
                      name: 'c-aca',
                      value: 2
                    },
                    {
                      name: 'c-acb',
                      value: 5
                    }
                  ]
                }
              ]
            }
          ]
        }
      ]
    }

添加画布

  • 初始化画布。
    // 画布
    const width = 500
    const height = 500
    const svg = d3
      .select('.d3Chart')
      .append('svg')
      .attr('width', width)
      .attr('height', height)
      .style('background-color', '#1a3055')
    // 图
    const chart = svg.append('g')

比例尺和配置信息

// 创建颜色比例尺
const color = d3.scaleOrdinal(d3.quantize(d3.interpolateRainbow, data.children.length + 1))
  1. d3.interpolateRainbow(t) 返回一串RGB颜色。t: 取值范围是[0,1]。
  2. d3.quantize(interpolator, n) 返回数组。通过插值器函数(interpolator),返回传入的样本数(n)。
  • 颜色比例尺,参数相同返回相同颜色。
const root = d3
  .hierarchy(data)
  .sum((d) => d.value)
  .sort((a, b) => b.value - a.value)
  • 使用.hierarchy() 构造根节点数据。如节点层级、节点总值等。
// 分区图布局
const partition = (newData) => {
  //  d3.partition() 递归地将节点树  分区为旭日图或者冰柱图
  return d3.partition().size([2 * Math.PI, newData.height + 1])(newData)
}
  1. d3.partition() 分区图布局,对数据在画布上进行布局。
  • 通过.size() 设置布局为极坐标系,以弧度和层级进行布局。

绘制扇形

    const slices = chart.append('g').attr('transform', `translate(${width / 2},${height / 2})`)
  • 创建绘制组。
// 中心点 标识
let currentRoot = 'chart'
  • 处理交互时的唯一标识。图中间圆标识。
function handle_node_click(e, d) {
  if (d.data.name === currentRoot) {
    // 点击中心节点回退
    if (d.parent) {
      const newD = d.parent.copy()
      newD.parent = d.parent.parent
      draw(newD)
    }
  } else {
    // 点击其余节点下钻
    // .copy() 复制层次结构
    const newD = d.copy()
    newD.parent = d.parent
    draw(newD)
  }
}
  1. .copy() 继承d3.hierarchy,复制当前节点信息。
  • 绘图节点点击,修改数据重新绘制图新形成交互。.copy() 只复制节点当前节点信息,手动添加父节点信息。
    function draw(newData) {
      // 获取展示数据
      const nodes = partition(newData)
      nodes.each((d) => (d.current = d))
      const radius = width / 2 / (nodes.height + 2)
      // 中心节点
      currentRoot = nodes.data.name
    }

    draw(root)
  • 创建绘图函数draw()
  • 对数据添加布局信息,根据节点层级计算半径。设置当前数据中心节点标识。
function draw(newData) {
...
    const path = slices
        .selectAll('.node')
        .data(nodes.descendants(), (d) => d.data.name)
        .join(
          (enter) => {
            let $gs = enter.append('path')
            $gs.append('title').text((d) => `${d.data.name}: ${d.value}`)
            return $gs
          },
          (update) => update,
          (exit) => {
            // 多出的节点 删除
            exit.transition().duration(100).attr('opacity', 0).remove()
          }
        )
        .attr('class', 'node')
        .style('cursor', 'pointer')
        .attr('fill', (d) => {
          while (d.depth > 1) d = d.parent
          return color(d.data.name)
        })
        .on('click', handle_node_click)
        .transition()
        .duration(300)
        .attrTween('d', arcTween)

      // path
      function arcTween(item) {
        /**
         * 弧度计算
         * */
        let currentArc = this._current
        if (!currentArc) {
          currentArc = { startAngle: 0, endAngle: 0 }
        }
        const interpolateArc = d3.interpolate(
          //对弧度插值
          currentArc,
          {
            startAngle: item.x0,
            endAngle: item.x1
          }
        )
        this._current = interpolateArc(1)

        /**
         * 半径计算
         * */
        // 不同层级 内半径不同
        const innerRadiusR = item.y0 * radius
        let currentRadius = this._currentR
        if (!currentRadius) {
          currentRadius = innerRadiusR
        }
        const outerRadiusR = d3.interpolate(currentRadius, item.y1 * radius - 1)
        this._currentR = outerRadiusR(1)

        return function (t) {
          const arc = d3
            .arc()
            .padAngle(Math.min((item.x1 - item.x0) / 2, 0.005))
            .innerRadius(innerRadiusR)
            .outerRadius(outerRadiusR(t))
          return arc(interpolateArc(t))
        }
      }
...
}
  • 添加了交互动画,需要对节点设置唯一标识node。对绑定数据设置唯一标识.data(nodes.descendants(), (d) => d.data.name)
  • 第二次通过标识node获取已存在的节点。d3 会通过数据的标识对节点进行修改删除。
  • 使用.attrTween('d', arcTween) 加载动画。这里对弧度和半径进行了过度动画设置。

1.gif

...
  // 文本位置计算
  function labelTransform(d) {
    const x = (((d.x0 + d.x1) / 2) * 180) / Math.PI
    let y = ((d.y0 + d.y1) / 2) * radius
    if (d.y0 === 0) {
      return `translate(0,0)`
    }
    return `rotate(${x - 90}) translate(${y},0) rotate(${x < 180 ? 0 : 180})`
  }
  const label = slices
    .selectAll('text')
    .data(nodes.descendants(), (d) => d.data.name)
    .join(
      (enter) => {
        let $gs = enter.append('text')
        return $gs
      },
      (update) => update,
      (exit) => {
        // 多出的节点删除
        exit.transition().duration(100).attr('opacity', 0).remove()
      }
    )
    .attr('text-anchor', 'middle')
    .attr('pointer-events', 'none')
    .style('user-select', 'none')
    .attr('dy', '0.35em')
    .attr('transform', (d) => labelTransform(d.current))
    .text((d) => d.data.name)
...
  • 创建文本位置计算函数,根据弧度值和层级计算出文本的旋转角度和位移值。
  • 和弧形一样的,通过数据标识来控制节点。

2.gif