likes
comments
collection
share

d3.js实现树形结构过渡展开、折叠

作者站长头像
站长
· 阅读数 106

d3相关api

本文为一个最简单的树形结构过渡效果展开的核心功能,没有添加连接线等逻辑

hierarchy

family = d3.hierarchy({
  name: "root",
  children: [
    {name: "child #1"},
    {
      name: "child #2",
      children: [
        {name: "grandchild #1"},
        {name: "grandchild #2"},
        {name: "grandchild #3"}
      ]
    }
  ]
})

hierarchy接收一个树形结构的对象,d3会根据这个树形结构生成一个新的树形结构,这个新的树形结构中有几个主要的属性

  • data: 原始树形结构对应位置的数据
  • parent:父级节点的引用
  • depth:当前节点的层级,根节点为0
  • height:当前节点到当前路径叶子节点的节点数,叶子节点的height为0

d3.js实现树形结构过渡展开、折叠 可以看到上面的图片则为新构建树形结构。hierarchy的第二个参数是一个函数,函数接收原始数据为参数,返回一个数组类型,默认为children,也就是利用children属性构建层级结构,如果我们返回其他字段,就会利用相应字段构建层级结构。

  const family = hierarchy(
    {
      name: "root",
      parents: [
        { name: "parent #1" },
        {
          name: "parent #2",
          children: [
            { name: "grandparent#1" },
            { name: "grandparent#2" },
            { name: "grandparent#3" },
          ],
        },
      ],
    },
    (d) => {
      return d.parents;
    }
  );

d3.js实现树形结构过渡展开、折叠 从上图可以看出,新结构的children是利用原始数据的parents构建的

d3.tree()

根据配置给树形结构添加坐标

tree.nodeSize() 参数为一个数组,分别是每个节点的水平距离和垂直距离

transition

    selection.transition().duration(毫秒过渡时间)

为当前selection节点设置属性时添加过渡效果

开始绘制

定义一个插件类DrawGraphPlugin,参数为数据data和el挂载目标对象

class DrawGraphPlugin {
    constructor(config) {
        this.el = config.el;
        this.daddta = config.data;
    }
}

在onMounted生命周期函数中调用

<template>
  <div class="svgContainer"></div>
</template>
<script setup>
import { onMounted, ref } from "vue";
    const data = {
      name: "根节点",
      children: [
        {
          name: "一级节点 1",
          children: [{ name: "二级节点 1-1" }, { name: "二级节点 1-2" }],
        },
        {
          name: "一级节点 2",
          children: [{ name: "二级节点 2-1" }, { name: "二级节点 2-2" }],
        },
      ],
    };
    onMounted(() => {
      new DrawGraphPlugin({
        el: ".svgContainer",
        data,
      });
    });
</script>

开始编写绘制插件DrawGraphPlugin

初始化dom结构

  initDomStructure() {
    // 生成svg
    const svg = create("svg")
      .attr("xmlns", "http://www.w3.org/2000/svg")
      .attr("height", this.gInfo.height)
      .attr("width", this.gInfo.width)
      .attr("viewBox", () => {
        return [
          -this.gInfo.width / 2,
          -200,
          this.gInfo.width,
          this.gInfo.height,
        ];
      })

      .style("user-select", "none")
      .style("cursor", "move");

    this.rootContainer = svg.append("g").attr("class", "containerG");

    // 添加zoom,放大、伸缩的效果
    svg
      .call(
        zoom()
          .scaleExtent([0.2, 5])
          .on("zoom", () => {
            const { x, y, k } = event.transform;
            this.rootContainer.attr("transform", () => {
              return `translate(${x},${y}) scale(${k})`;
            });
          })
      )
      // 取消双击放大的事件
      .on("dblclick.zoom", null);

    // 将新生成的svg添加到el节点中
    select(this.el).node().appendChild(svg.node());
    this.svg = svg;
  }

初始化树结构数据

class DrawGraphPlugin {
  constructor(config) {
      ...
  }
  // 初始化树结构数据
  initTreeStructure(data) {
     // 添加层级关系
    const hierarchyData = hierarchy(data);
    // 添加位置坐标
    const descendantsData = tree().nodeSize([100, 200])(hierarchyData);
    return descendantsData;
  }
}

更新方法

  // 绘制、更新 方法
  // source为点击时记录的位置,用于从点击的位置展开或者折叠到点击的位置
  draw(source) {
    // 给rootContainer绑定数据
    const nodesSelection = this.rootContainer
      .selectAll(".itemG")
      .data(this.treeData.descendants(), (d) => {
        return d.data.name;
      });
    const that = this;
    const itemG = nodesSelection.enter().append("g");

    // 先移动到点击点位置
    itemG.attr("class", "itemG").attr("transform", (d) => {
      // 先直接移动到点击点的位置
      return `translate(${source.x},${source.y})`;
    });

    // 再从点击点开始 动画移动到目标点
    itemG
      .transition()
      .duration(200)
      .attr("transform", function (d) {
        return `translate(${d.x},${d.y})`;
      });

    // 添加点击事件 折叠、展开
    itemG.on("click", function (d) {
      // 如果存在children则需要折叠,将children设置为null,同时用_children保存
      if (d.children) {
        d._children = d.children;
        d.children = null;
      }

      // 不存在children需要展开,将children恢复为_children
      else {
        d.children = d._children;
        d._children = null;
      }
      // 重新绘制,将当前节点信息作为参数传入(需要当前节点的x、y坐标)
      that.draw(d);
    });

    // 绘制节点中的内容 方块、文本
    itemG
      .append("rect")
      .attr("width", 80)
      .attr("height", 50)
      .attr("stroke", "rgb(64, 137, 230)")
      .attr("fill", "#fff");

    itemG
      .append("text")
      .text((d) => d.data.name)
      .attr("font-size", 12)
      .attr("text-anchor", "middle")
      .attr("transform", (d) => {
        return `translate(${40},${30})`;
      });

    // 退出状态动画,将点击节点的子节点移动到点击的节点,隐藏
    nodesSelection
      .exit()
      .transition()
      .duration(200)
      .attr("transform", function () {
        return `translate(${source.x},${source.y})`;
      })
      .style("opacity", 0)
      .remove();
  }

完整代码

<template>
  <div class="svgContainer"></div>
</template>

<script setup>
import { onMounted } from "vue";
import { create, select, tree, hierarchy, zoom, event } from "d3";
const data = {
  name: "根节点",
  children: [
    {
      name: "一级节点 1",
      children: [{ name: "二级节点 1-1" }, { name: "二级节点 1-2" }],
    },
    {
      name: "一级节点 2",
      children: [{ name: "二级节点 2-1" }, { name: "二级节点 2-2" }],
    },
  ],
};

onMounted(() => {
  new DrawGraphPlugin({
    el: ".svgContainer",
    data,
  });
});
class DrawGraphPlugin {
  constructor(config) {
    this.el = config.el;
    this.data = config.data;
    this.svg = null;
    this.gInfo = {
      height: document.documentElement.clientHeight,
      width: document.documentElement.clientWidth,
    };

    this.treeData = this.initTreeStructure(this.data);
    this.initDomStructure();

    this.draw({ x: 0, y: 0 });
  }
  initDomStructure() {
    // 生成svg
    const svg = create("svg")
      .attr("xmlns", "http://www.w3.org/2000/svg")
      .attr("height", this.gInfo.height)
      .attr("width", this.gInfo.width)
      .attr("viewBox", () => {
        return [
          -this.gInfo.width / 2,
          -200,
          this.gInfo.width,
          this.gInfo.height,
        ];
      })

      .style("user-select", "none")
      .style("cursor", "move");

    this.rootContainer = svg.append("g").attr("class", "containerG");

    // 添加zoom,放大、伸缩的效果
    svg
      .call(
        zoom()
          .scaleExtent([0.2, 5])
          .on("zoom", () => {
            const { x, y, k } = event.transform;
            this.rootContainer.attr("transform", () => {
              return `translate(${x},${y}) scale(${k})`;
            });
          })
      )
      // 取消双击放大的事件
      .on("dblclick.zoom", null);

    // 将新生成的svg添加到el节点中
    select(this.el).node().appendChild(svg.node());
    this.svg = svg;
  }
  initTreeStructure(data) {
    const hierarchyData = hierarchy(data);
    const descendantsData = tree().nodeSize([100, 200])(hierarchyData);
    return descendantsData;
  }

  // 绘制、更新 方法
  // source为点击时记录的位置,用于从点击的位置展开或者折叠到点击的位置
  draw(source) {
    // 给rootContainer绑定数据
    const nodesSelection = this.rootContainer
      .selectAll(".itemG")
      .data(this.treeData.descendants(), (d) => {
        return d.data.name;
      });
    const that = this;
    const itemG = nodesSelection.enter().append("g");

    // 先移动到点击点位置
    itemG.attr("class", "itemG").attr("transform", (d) => {
      // 先直接移动到点击点的位置
      return `translate(${source.x},${source.y})`;
    });

    // 再从点击点开始 动画移动到目标点
    itemG
      .transition()
      .duration(200)
      .attr("transform", function (d) {
        return `translate(${d.x},${d.y})`;
      });

    // 添加点击事件 折叠、展开
    itemG.on("click", function (d) {
      // 如果存在children则需要折叠,将children设置为null,同时用_children保存
      if (d.children) {
        d._children = d.children;
        d.children = null;
      }

      // 不存在children需要展开,将children恢复为_children
      else {
        d.children = d._children;
        d._children = null;
      }
      // 重新绘制,将当前节点信息作为参数传入(需要当前节点的x、y坐标)
      that.draw(d);
    });

    // 绘制节点中的内容 方块、文本
    itemG
      .append("rect")
      .attr("width", 80)
      .attr("height", 50)
      .attr("stroke", "rgb(64, 137, 230)")
      .attr("fill", "#fff");

    itemG
      .append("text")
      .text((d) => d.data.name)
      .attr("font-size", 12)
      .attr("text-anchor", "middle")
      .attr("transform", (d) => {
        return `translate(${40},${30})`;
      });

    // 退出状态动画,将点击节点的子节点移动到点击的节点,隐藏
    nodesSelection
      .exit()
      .transition()
      .duration(200)
      .attr("transform", function () {
        return `translate(${source.x},${source.y})`;
      })
      .style("opacity", 0)
      .remove();
  }
}
</script>

转载自:https://juejin.cn/post/7142017273195134983
评论
请登录