d3.js-v7左右双向树结构图

效果图如下:

效果图

 使用的是D3的v7版本,需要具有一定svg基础。

HTML代码

<div class="seeTree-page">
      <div id="treeRoot"></div>
    </div>

js代码

树图布局API:

                   d3.layout.tree():创建一个树图布局。

                   tree.size():设置树图的容器的宽高。

                    tree.separation([separation])设置相邻节点间隔。

                    tree.nodes(root)根据root计算获取节点数组。

                    tree.links(nodes)根据nodes计算获取连线数组。

                   总结:根据两点绘制一条线段的结论得出,links 个数总比nodes个数 少一个。

          2.节点(nodes)对象包含以下属性:

                        parent:父节点。

                        children:子节点。

                        depth:节点深度。

                        x:节点的x坐标。

                        y:节点的y坐标。

          3.节点间连线(links)对象,包含以下属性:

                        source:源节点(连线的前半段节点)。

                        target:目标节点(连线的后半段节点)。

先把数据分成左右两组, 选中页面给页面添加svg标签;设置Svg绘制区域的宽和高;添加g元素(svg的group分组标签元素)并设置位置。

          2.生成树状布局,设置树图布局容器尺寸。

          3..对角线生成器,并旋转90度。

          4.请求数据:

              4.1获取nodes节点数组和links连线数组。
              4.2生成连线。
              4.3生成节点。
              4.4给节点添加圆圈,设置半径。
              4.5给节点添加文本,设置文本的样式位置。


import * as d3 from "d3";
import { uid } from "uid";
const children = [
  {
    name: "123",
  },
  {
    name: "神鼎飞丹砂",
  },
  {
    name: "sdfsd胜多负少的",
  },
  {
    name: "水电费水电费是",
  },
];
const treeData = {
  r: {
    name: "",
    children: [
      {
        name: "xx部",
        children: [...children],
      },
      {
        name: "aa部",
        children: [...children],
      },
      {
        name: "cc部",
        children: [...children],
      },
    ],
  },
  l: {
    name: "",
    children: [
      {
        name: "34234",
        children: [...children],
      },
      {
        name: "vv部",
        children: [...children],
      },
      {
        name: "hh部",
        children: [...children],
      },
      {
        name: "rr部",
        children: [...children],
      },
    ],
  },
};
export default {
  data() {
    return {
      loading: false,
      searchModel: {
        year: {
          label: `填报年份`,
          type: "select",
          options: [
            {
              value: 2022,
              label: 2022,
            },
            {
              value: 2023,
              label: 2023,
            },
            {
              value: 2024,
              label: 2024,
            },
          ],
          inputProps: {
            placeholder: `请选择填报年份`,
          },
        },
        promoterName: {
          label: `指标填报标题`,
          type: "input",
          inputProps: {
            placeholder: `请输入指标填报标题`,
          },
        },
      },
      container: null, //容器svg>g
      duration: 750, //动画持续时间
      scaleRange: [0.2, 4], //container缩放范围
      direction: ["r", "l"], //分为左右2个方向
      centralPoint: [0, 0], //画布中心点坐标x,y
      root: { r: {}, l: {} }, //左右2块数据源
      rootNodeLength: 0, //根节点名称长度
      rootName: ["束带结发你上课的今年发就开始你是的", "工作任务清单(修订版)"], //根节点名称
      textSpace: 20, //多行文字间距
      themeColor: "#2196F3", //主色
      titleColor: "#cfe7e8", //标题色
      nodeSize: [70, 150], //节点间距(高/水平)
      fontSize: 16, //字体大小,也是单字所占宽高
      rectMinWidth: 50, //节点方框默认最小,
      textPadding: 5, //文字与方框间距,注:固定值5
      circleR: 5, //圆圈半径
      textWidth: 500, // 文本宽度
    };
  },
  computed: {
    treeMap() {
      //树布局
      return d3
        .tree()
        .nodeSize(this.nodeSize)
        .separation((a, b) => {
          let result =
            a.parent === b.parent && !a.children && !b.children ? 1 : 2;
          if (result > 1) {
            let length = 0;
            length = a.children ? length + a.children.length : length;
            length = b.children ? length + b.children.length : length;
            result = length / 2 + 0.5;
          }
          return result;
        });
    },
  },
  mounted() {
    this.treeInit();
  },
  methods: {
    search() {},
    //初始化
    treeInit() {
      const margin = { top: 0, right: 150, bottom: 150, left: 0 };
      const treeWidth = document.body.clientWidth - margin.left - margin.right; //tree容器宽
      const treeHeight =
        document.body.clientHeight - margin.top - margin.bottom; //tree容器高
      const centralY = treeWidth / 2 + margin.left;
      const centralX = treeHeight / 2 + margin.top;
      this.centralPoint = [centralX, centralY]; //中心点坐标
      //根节点字符所占宽度
      this.rootNodeLength = this.rootName[0].length * this.fontSize + 30;

      //svg标签
      const svg = d3
        .select("#treeRoot")
        .append("svg")
        .attr("class", "tree-svg")
        .attr("width", treeWidth)
        .attr("height", treeHeight)
        .attr("font-size", this.fontSize)
        .attr("fill", "#555");

      //g标签
      this.container = svg
        .append("g")
        .attr("class", "container")
        .attr("transform", `translate(${margin.left},${margin.top}) scale(1)`);
      //画出根节点
      this.drawRoot();

      //指定缩放范围
      const zoom = d3
        .zoom()
        .scaleExtent(this.scaleRange)
        .on("zoom", (e) => {
          this.container.attr("transform", e.transform);
        });

      //动画持续时间
      this.container
        .transition()
        .duration(this.duration)
        .call(zoom.transform, d3.zoomIdentity);

      svg.call(zoom);
      //数据处理
      this.dealData();
    },
    //数据处理
    dealData() {
      this.direction.forEach((item) => {
        this.root[item] = d3.hierarchy(treeData[item]);
        this.root[item].x0 = this.centralPoint[0]; //根节点x坐标
        this.root[item].y0 = this.centralPoint[1]; //根节点Y坐标
        this.root[item].descendants().forEach((d) => {
          d._children = d.children; //添加_children属性,用于实现点击收缩及展开功能
          d.id = item + uid(); //绑定唯一标识ID 随机数,用于绑定id
        });
        this.update(this.root[item], item);
      });
    },
    //画根节点
    drawRoot() {
      const title = this.container
        .append("g")
        .attr("id", "rootTitle")
        .attr(
          "transform",
          `translate(${this.centralPoint[1]},${this.centralPoint[0]})`
        );
      title
        .append("svg:rect")
        .attr("class", "rootTitle")
        .attr("y", 0)
        .attr("x", -this.rootNodeLength / 2)
        .attr("width", this.rootNodeLength)
        .attr("height", 0)
        .attr("rx", 5) //圆角
        .style("fill", this.titleColor);
      this.rootName.forEach((name, index) => {
        title
          .append("text")
          .attr("fill", "black")
          .attr("y", index * this.textSpace - 2)
          .attr("text-anchor", "middle")
          .text(name);

        let lineHeight = (index + 2) * this.textSpace;
        //修改rootTitle rect 的高度
        d3.select("#rootTitle rect")
          .attr("height", lineHeight)
          .attr("y", -lineHeight / 2);
      });
    },
    //开始绘图
    update(source, direction) {
      const dirRight = direction === "r" ? 1 : -1; //方向为右/左
      const className = `${direction}gNode`;
      const tree = this.treeMap(this.root[direction]);
      const nodes = tree.descendants(); //返回后代节点数组,第一个节点为自身,然后依次为所有子节点的拓扑排序
      const links = tree.links(); //返回当前 node 的 links 数组, 其中每个 link 定义了 source父节点, target 子节点属性。
      nodes.forEach((d) => {
        //左右2部分,设置以中心点为圆点(默认左上角为远点)
        d.y = dirRight * (d.y + this.rootNodeLength / 2) + this.centralPoint[1];
        d.x = d.x + this.centralPoint[0];
      });

      //根据class名称获取左或者右的g节点,达到分块更新
      const node = this.container
        .selectAll(`g.${className}`)
        .data(nodes, (d) => d.id);

      //新增节点,tree会根据数据内的children扩展相关节点
      const nodeEnter = node
        .enter()
        .append("g")
        .attr("id", (d) => `g${d.id}`)
        .attr("class", className)
        .attr("transform", (d) => `translate(${source.y0},${source.x0})`)
        .attr("fill-opacity", 0)
        .attr("stroke-opacity", 0)
        .on("click", (e, d) => {
          d.depth !== 0 && this.clickNode(d, direction); //根节点不执行点击事件
        });

      nodeEnter.each((d) => {
        if (d.depth > 0) {
          //非根节点且无子节点
          this.drawText(`g${d.id}`, dirRight, d); //画文本

          this.drawRect(`g${d.id}`, dirRight); //画方框
          // d3.select(`#g${d.id} rect`).attr('stroke-width',15).attr('filter',`url(#fg${d.id})`);//给rect绑定阴影
        }
        if (d.depth > 0 && d._children) {
          //非根节点且有子节点
          const width = Math.max(
            d.data.name.length * (this.fontSize + 2),
            this.rectMinWidth
          );
          let right = dirRight > 0; //右为1,左为-1
          let xDistance = right ? width : -width;
          //修改rect属性
          d3.select(`#g${d.id} rect`)
            .attr("width", width + this.textPadding * 2)
            .attr("height", this.fontSize + this.textPadding * 2)
            .attr("x", right ? 0 : -width)
            .attr("fill", (d) => this.getTsTextColor(d.data.name))
            .style("stroke", (d) => this.getTsTextColor(d.data.name));
          //修改文本属性
          d3.select(`#g${d.id} text`)
            .attr("text-anchor", right ? "end" : "start")
            .attr("font-weight", "bold")
            .attr(
              "x",
              right
                ? xDistance - this.circleR + this.textPadding
                : xDistance + this.circleR + this.textPadding
            )
            .attr("y", this.fontSize / 2)
            .style("cursor", "pointer");

          // //修改圆圈属性
          // d3.select(`#g${d.id} g`).attr(
          //   "transform",
          //   `translate(${xDistance},0)`
          // );
        }
      });

      // 更新节点:节点enter和exit时都会触发tree更新
      const nodeUpdate = node
        .merge(nodeEnter)
        .transition()
        .duration(this.duration)
        .attr(
          "transform",
          (d) => `translate(${d.y - (dirRight * this.rectMinWidth) / 2},${d.x})`
        )
        .attr("fill-opacity", 1)
        .attr("stroke-opacity", 1);

      // 移除节点:tree移除掉数据内不包含的节点(即,children = false)
      const nodeExit = node
        .exit()
        .transition()
        .duration(this.duration)
        .remove()
        .attr("transform", (d) => `translate(${source.y},${source.x})`)
        .attr("fill-opacity", 0)
        .attr("stroke-opacity", 0);

      // Update the links 根据 className来实现分块更新
      const link = this.container
        .selectAll(`path.${className}`)
        .data(links, (d) => d.target.id);

      // Enter any new links at the parent's previous position.
      //insert是在g标签前面插入,防止连接线挡住G节点内容
      const linkEnter = link
        .enter()
        .insert("path", "g")
        .attr("class", className)
        .attr("d", (d) => {
          const o = { x: source.x0, y: source.y0 };
          return this.diagonal({ source: o, target: o });
        })
        .attr("fill", "none")
        .attr("stroke-width", 2)
        .attr("stroke", ({ source, target }) => {
          return this.getTsTextColor(source.data.name || target.data.name);
        });

      // Transition links to their new position.
      link
        .merge(linkEnter)
        .transition()
        .duration(this.duration)
        .attr("d", this.diagonal);

      // Transition exiting nodes to the parent's new position.
      link
        .exit()
        .transition()
        .duration(this.duration)
        .remove()
        .attr("d", (d) => {
          const o = { x: source.x, y: source.y };
          return this.diagonal({ source: o, target: o });
        });

      // Stash the old positions for transition.
      this.root[direction].eachBefore((d) => {
        d.x0 = d.x;
        d.y0 = d.y;
      });
    },

    //画连接线
    diagonal({ source, target }) {
      let s = source,
        d = target;
      let direction = (s.y + d.y) / 2 > s.y ? "r" : "l";
      let xDistance = this.fontSize * 2;
      let lastX = d.x + this.fontSize - 1;
      // console.log(
      //   d.data?.names,
      //   (d.data?.names?.length > 0 ? d.data?.names?.length - 1 : 0) *
      //     this.fontSize
      // );
      if (!d._children && d.data) {
        // 叶节点
        // console.log(source, target);
        return `M ${s.y} ${s.x}
                L ${(s.y + d.y) / 2} ${s.x},
                L ${(s.y + d.y) / 2} ${lastX > s.x ? lastX - 15 : lastX + 15},
                C ${(s.y + d.y) / 2} ${lastX},
                ${(s.y + d.y) / 2} ${lastX}
                ${d.y} ${lastX},
                L ${
                  direction === "r"
                    ? d.y + this.textWidth
                    : d.y - this.textWidth
                } ${lastX}`;
        // return `M ${s.y} ${s.x}
        // C ${(s.y + d.y) / 2} ${s.x},
        // ${(s.y + d.y) / 2} ${lastX},
        // ${
        //   direction === "r"
        //     ? (s.y + d.y) / 2 + xDistance
        //     : (s.y + d.y) / 2 - xDistance + 10
        // } ${lastX}
        // L ${
        //   direction === "r" ? d.y + this.textWidth : d.y - this.textWidth
        // } ${lastX}`;
      } else {
        return `M ${s.y} ${s.x}
                C ${(s.y + d.y) / 2} ${s.x},
                ${(s.y + d.y) / 2} ${lastX},
                ${
                  direction === "r"
                    ? (s.y + d.y) / 2 + xDistance + 20
                    : (s.y + d.y) / 2 - xDistance - 10
                } ${lastX}
                L ${d.y} ${lastX}`;
      }
    },

    //画文本
    drawText(id, dirRight, d) {
      dirRight = dirRight > 0; //右为1,左为-1
      let texts = d3
        .select(`#${id}`)
        .append("text")
        .attr("class", (d) => {
          return `text-id${id} ${!d._children ? "leaf-node" : ""}`;
        })
        .attr("x", (d) =>
          dirRight ? this.textPadding : -this.textPadding - this.textWidth
        )
        .attr("text-anchor", dirRight ? "start" : "start")
        .style("font-size", this.fontSize)
        .style("fill", (d) => {
          if (!d._children && !d.children) {
            //无子节点
            return this.themeColor;
          }
        });

      // 设置text文字自动换行
      texts.each((item) => {
        let text = this.container.selectAll("text.text-id" + id);
        item.data.names = this.insertEnter(item.data.name, this.textWidth);
        let x = +text.attr("x"),
          y = +text.attr("y");
        let lineHight = this.fontSize + 4;
        if (item.data.names.length > 1) {
          item.data.height = lineHight * item.data.names.length - 1 + 10;
          // 需要换行的文字
          for (let i = 0; i < item.data.names.length; i++) {
            text
              .append("tspan")
              .attr("x", x)
              .attr("y", (d) => {
                // 计算每行字的坐标
                let textX = y + lineHight * i - this.textPadding * 2;
                return item.data.names.length > 4
                  ? textX - this.textSpace
                  : textX;
              })
              .style("font-size", this.fontSize - 2)
              .text((d) => {
                if (i <= 2) {
                  if (item.data.names.length > 4 && i == 2) {
                    d.data.names[i] += "...";
                  }
                  return d.data.names[i];
                }
              });
          }
        } else {
          text.text((d) => d.data.name);
        }
      });
      return texts;
    },
    // 拆分文字字符串 (文字字符串,外框宽度)
    insertEnter(name, width) {
      // 文字宽度
      let nameLen = name.length * this.fontSize;
      // 每行字数,超过换行
      let num = 5;
      // 文字宽度大于rect宽度时,计算每行最大字数
      // console.log("nameLen", nameLen, width, name);
      if (nameLen > width) {
        num = Math.floor(width / this.fontSize);
      } else {
        num = Math.floor(nameLen / this.fontSize);
      }
      if (!num) num = 1;
      var s = name,
        reg = new RegExp(`.{1,${num}}`, "g"),
        rs = s.match(reg);

      if (name.length <= num) {
        return [name];
      } else {
        rs.push(s.substring(rs.join("").length));
      }
      return rs;
    },


    //画方框
    drawRect(id, dirRight) {
      let realw = document.getElementById(id).getBBox().width + 10; //获取g实际宽度后,设置rect宽度
      let realh = document.getElementById(id).getBBox().height;
      return d3
        .select(`#${id}`)
        .insert("rect", "text")
        .attr("x", dirRight > 0 ? 0 : -realw)
        .attr("y", (d) => {
          if (!d._children) {
            return 0;
          } else {
            return -this.textSpace + this.textPadding * 2;
          }
        })
        .attr("width", realw)
        .attr("height", realh)
        .attr("rx", 2) //圆角
        .attr("opacity", (d) => {
          if (!d._children) {
            return 0;
          } else {
            return 1;
          }
        });
      // .style("stroke", "#dddddd")
    },

    //点击某个节点
    clickNode(d, direction) {
      if (!d._children && !d.children) {
        //无子节点
        console.log(d);
        this.$router.push({
          name: "accountCheckDetail",
        });
        return;
      }
      //根据当前节点是否有children来判断是展开还是收缩,true收缩,false展开
      //tree会根据节点内是否有children来向下扩展
      d.children = d.children ? null : d._children;
      // d3.select(`#g${d.id} .node-circle .node-circle-vertical`)
      //   .transition()
      //   .duration(this.duration)
      //   .attr("stroke-width", d.children ? 0 : 1); //控制节点伸缩时的标识圆圈
      this.update(d, direction);
    },

    //子文本颜色配置
    getTsTextColor(name) {
      switch (name) {
        default:
          return "#000";
      }
    },
  },
};

style

<style lang="less">
.seeTree-page {
  width: 100%;
  height: 100%;
  overflow: hidden;
  background-color: white;
  .leaf-node {
    cursor: pointer;
    &:hover {
      text-decoration: underline;
    }
  }
}
</style>

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值