学习D3.js(十五)树图

441 阅读3分钟

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

开始绘制

引入D3模块

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

数据

  • 自定义数据格式。
    const dataTree = {
      name: '太刀',
      children: [
        {
          name: '矿石',
          children: [
            {
              name: '结晶矿',
              children: [
                { name: '蓝矿', num: 10 },
                { name: '黑铁矿', num: 3 },
                { name: '白灰矿', num: 4 }
              ]
            }
          ]
        },
        {
          name: '木材',
          children: [
            {
              name: '稀木',
              children: [
                { name: '钴木', num: 4 },
                { name: '黑木', num: 2 }
              ]
            },
            {
              name: '水木',
              children: [{ name: '蓝木', num: 4 }]
            }
          ]
        },
        {
          name: '宝石',
          children: [
            {
              name: '太阳类',
              children: [
                { name: '日金石', num: 6 },
                { name: '熔岩石', num: 1 }
              ]
            },
            {
              name: '深海类',
              children: [
                { name: '寒铁石', num: 2 },
                { name: '金晶石', num: 3 },
                { name: '玄冰结晶', num: 2 }
              ]
            }
          ]
        }
      ]
    }

添加画布

  • 初始化画布。
    var width = 900
    var height = 600
    var margin = 30
    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(${2 * margin}, ${2 * margin})`)

创建比例尺

var colorScale = d3.scaleOrdinal(d3.schemeSet3)
  • 颜色比例尺。
const rootTree = d3
  .hierarchy(dataTree)
  .sum((d) => d.num) // 计算绘图属性value的值  -求和 其子节点所有.num属性的和值
  .sort((a, b) => a.value - b.value) // 根据 上面计算出的value属性 排序
  • 转换数据为d3树形数据结构,并对每个层级排序。
    // 创建一个新的树布局
    // .size() 设置布局的尺寸
    const treeData = d3.tree().size([width - 4 * margin, height - 4 * margin])  
  • 对树形结构数据,生成画布上的布局数据(每个节点在画布上的坐标信息)。

绘制树

    // 绘制组
    const linkChart = chart.append('g')
    const rectChart = chart.append('g')
  • 树节点和线条分开绘制,创建两个绘制组。
    /**
     * @name 处理结点 点击
     * @param {Object} ev 事件
     * @param {Object} d 数据
     */
    function handle_node_click(ev, d) {
      d.sourceX = d.x
      d.sourceY = d.y

      if (d.depth !== 0) {
        if (d.children) {
          d._children = d.children
          d.children = undefined

          draw()
        } else if (d._children) {
          for (let a of d._children) {
            a.originX = a.parent.x
            a.originY = a.parent.y
          }
          d.children = d._children

          draw()
        }
      }
    }
  • 树图点击节点,可以收起子节点,再次点击,展开子节点。
  • 创建点击方法对数据进行操作。
/**
 * init 是否第一次加载
 * */
function draw(init = false) {
  ...
}
draw(true)
  • 为了实现点击节点树图变化的效果,每一次数据变换都需要重新绘制。绘制代码需要放在函数中实时调用。
function draw(init = false) {
  // 为数据添加位置信息
  let root = treeData(rootTree)
  // 获取所有节点数据
  let nodes = root.descendants()

  const rectNode = rectChart
    .selectAll('.node')
    .data(nodes, (d) => d.data.name)
    .join(
      (enter) => {
        /**
         * 绘制节点 和 节点文本
         * */
        let $gs = enter.append('g').attr('transform', (d) => {
          let x, y
          if (d.originX) {
            x = d.originX
            delete d.originX
          } else {
            x = d.x
          }
          if (d.originY) {
            y = d.originY
            delete d.originY
          } else {
            y = d.y
          }

          return `translate(${x}, ${y})`
        })

        $gs
          .append('circle')
          .attr('r', 24)
          .attr('cx', 0)
          .attr('cy', 0)
          .attr('fill', (d) => (d.children || d._children ? colorScale(1) : colorScale(2)))

        $gs
          .append('text')
          .attr('class', 'text')
          .text((d) => (d.data.name.length < 3 ? d.data.name : d.data.name.slice(0, 1) + '...'))
          .attr('fill', '#000000')
          .style('font-size', '12px')
          .attr('dx', (d) => {
            return -12
          })
          .attr('dy', function () {
            return this.getBBox().height / 4
          })

        return $gs
      },
      (update) => update,
      (exit) => {
        // 删除多出 节点 添加动画
        exit
          .transition()
          .duration(init ? 0 : 1000)
          .attr('opacity', 0)
          .attr('transform', (d) => `translate(${d.parent.x},${d.parent.y})`)
          .remove()
      }
    )
    .attr('class', 'node')
    .style('cursor', 'pointer')
    .on('click', handle_node_click)
    // 节点加载 进行动画
    .transition()
    .duration(init ? 0 : 1000)
    .attr('opacity', 1)
    .attr('transform', (d) => `translate(${d.x}, ${d.y})`)
  ...
}

1.gif

  • .join() 有三个参数,每个参数都是一个函数,创建、更新、删除。根据数据和节点绑定的key来判断。数据没有绑定节点,创建新的节点,执行创建函数。数据和节点同时存在且绑定过,执行更新函数。数据被删除,节点存在删除节点,执行删除函数。

  • 监听点击事件,修改树图数据。

  • 通过使用.join()函数,对节点加入和删除增加动画。

  • 使用同样的方式绘制连线。

    ...
      // 获取线条 位置信息
      let links = root.links()
      linkChart
        .selectAll('.link')
        .data(links, (d, i) => d.target.data.name)
        .join(
          (enter) =>
            enter
              .append('path')
              .attr('class', 'link')
              .attr('fill', 'none')
              .attr('stroke', 'gray')
              .attr('d', (d) => {
                let s = d.source
                let origin = `${s.sourceX || s.x},${s.sourceY || s.y}`
                return `M ${origin} L ${origin}`
              }),
          (update) => update,
          (exit) =>
            exit
              .transition()
              .duration(init ? 0 : 1000)
              .attr('d', (d) => {
                let s = d.source
                let origin = `${s.x},${s.y}`

                return `M ${origin} L ${origin}`
              })
              .remove()
        )
        .transition()
        .duration(init ? 0 : 1000)
        .attr('d', (d) => {
          let s = d.source
          let t = d.target
          return `M ${s.x},${s.y} L ${t.x},${t.y}`
        })
    ...

2.gif