D3最新版本实现组织型tree结构,包含拖拽、缩放、平移、折叠、异步加载节点自定义渲染等功能

徐秋月
2023-12-01


import { PureComponent } from 'react'
import { D3 } from "@weapp/ui"
let d3: any = null;

//辅助函数: 递归获取tree的节点数量
function getCount(parent: any) {
  var count = 0;
  if (Array.isArray(parent.children)) {
    count += parent.children.length;
    parent.children.forEach(function (child: any) {
      if (Array.isArray(child.children)) {
        count += getCount(child);
      }
    });
  }
  return count;
}

//数据源
var root = {
  "x0": null,
  "y0": null,
  'name': '中国',
  'children': [{
    'name': '浙江',
    "_name": 'zhejiang',
    'children': []
  },
  {
    'name': '广西',
    'children': [],
    "_name": 'guangxi',
  },
  {
    'name': '黑龙江',
    "_name": 'heilongjiang',
  }
  ]
}

function createTree() {
  //布局和尺寸参数
  var margin = {
    top: 20,
    right: 120,
    bottom: 20,
    left: 120
  },
    height = 800 - margin.top - margin.bottom;

  //常规参数
  var i = 0,
    duration = 750,
    rectW = 120,
    rectH = 60;

  //使用hierarchy层级布局生成树结构
  var tree = d3.tree().nodeSize([130, 60]);
  var hierarchyData = d3.hierarchy(root)

  //path连线
  function linkVertical(d: any) {
    // return "M" + d.source.x + "," + d.source.y       //曲线
    //     + "C" + (d.source.x + d.target.x) / 2 + "," + d.source.y
    //     + " " + (d.source.x + d.target.x) / 2 + "," + d.target.y
    //     + " " + d.target.x + "," + d.target.y;
    let sourceX = d.source.x, //折线
      sourceY = d.source.y + rectH,
      targetX = d.target.x,
      targetY = d.target.y;
    return "M" + sourceX + "," + sourceY +
      "V" + ((targetY - sourceY) / 2 + sourceY) +
      "H" + targetX +
      "V" + targetY;
  }

  //在页面中添加svg 支持拖拽和缩放
  var svg = d3.select("#body").append("svg").attr("width", 1000).attr("height", 1000)
    .call(d3.zoom().scaleExtent([1, 3]).on("zoom",
      function redraw(event: any) {
        svg.attr("transform", event.transform);
      }
    )).append("g").attr("transform", "translate(" + 350 + "," + 20 + ")").append("g");

  (root as any).x0 = 0;
  (root as any).y0 = height / 2;

  // root.children.forEach(collapse); 
  update(root);
  d3.select("#body").style("height", "800px");

  function update(source: any) {
    let treeData = tree(hierarchyData);
    let nodes = treeData.descendants().reverse();
    let links = treeData.links()
    // Normalize for fixed-depth.
    nodes.forEach(function (d: any) {
      d.y = d.depth * 180;
    });

    // Update the nodes…
    var node = svg.selectAll("g.node")
      .data(nodes, function (d: any) {
        return d.id || (d.id = ++i);
      });

    // Enter any new nodes at the parent's previous position.
    var nodeEnter = node.enter().append("g")
      .attr("class", "node")
      .attr("transform", function (d: any) {
        return "translate(" + source.x0 + "," + source.y0 + ")";
      })

    nodeEnter.append("rect")
      .attr("width", rectW)
      .attr("height", rectH)
      .attr("stroke", "black")
      .attr('y', 0)
      .attr('x', -rectW / 2)
      .attr("stroke-width", 1)
      .style("fill", function (d: any) {
        return d._children ? "lightsteelblue" : "rgb(93,156,236)";
      }).on("click", function (e: any, node: any) {
        click(node)
      });

    nodeEnter.append("text")
      .attr("x", rectW / 2 - rectW / 2)
      .attr("y", rectH / 2)
      .attr("dy", ".35em")
      .attr("text-anchor", "middle")
      .text(function (d: any) {
        return d.data.name;
      });

    // Transition nodes to their new position.

    let nodeUpdate = nodeEnter.merge(node);
    nodeUpdate.transition()
      .duration(750)
      .attr("transform", function (d: any) {
        return "translate(" + d.x + "," + d.y + ")";
      });

    nodeUpdate.select("rect")
      .attr("width", rectW)
      .attr("height", rectH)
      .attr("stroke", "black")
      .attr("stroke-width", 1)
      .style("fill", function (d: any) {
        return d._children ? "lightsteelblue" : "rgb(93,156,236)";
      });

    nodeUpdate.select("text")
      .style("fill-opacity", 1);

    // Transition exiting nodes to the parent's new position.
    var nodeExit = node.exit().transition()
      .duration(duration)
      .attr("transform", function (d: any) {
        return "translate(" + source.x + "," + source.y + ")";
      })
      .remove();

    nodeExit.select("rect")
      .attr("width", rectW)
      .attr("height", rectH)
      //.attr("width", bbox.getBBox().width)""
      //.attr("height", bbox.getBBox().height)
      .attr("stroke", "black")
      .attr("stroke-width", 1);

    nodeExit.select("text");

    // Update the links…
    var link = svg.selectAll("path.link")
      .data(links, function (d: any) {
        return d.target.id;
      });

    // Enter any new links at the parent's previous position.
    let linkEnter = link.enter().insert("path", "g")
      .attr("class", "link")
      .attr("x", rectW / 2)
      .attr("y", rectH / 2)
      .attr("d", function (d: any) {
        var o = {
          x: source.x0,
          y: source.y0
        };
        return linkVertical({
          source: o,
          target: o
        });
      });


    let linkUpdate = linkEnter.merge(link);

    // Transition links to their new position.
    linkUpdate.transition()
      .duration(duration)
      .attr("d", linkVertical);

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

    // Stash the old positions for transition.
    nodes.forEach(function (d: any) {
      d.x0 = d.x;
      d.y0 = d.y;
    });

    if (source.id === getCount(root) + 1) {
      //折叠区域渲染 使用延迟渲染优化显示效果
      setTimeout(function () {
        renderCollapseArea();
      }, 200)
    }

  }


  function renderCollapseArea() {

    function handleClick(e: any) {
      let target = e.target.dataset;
      let {
        mclass,
        type,
        collapsed,
        self
      } = target;
      if (type !== "btn") {
        return
      }
      if (collapsed === "false") { //展开
        d3.select(`.${mclass}`).transition().style("display", "none");
        e.target.dataset.collapsed = "true";
        document.getElementsByClassName(`${self}`)[0].innerHTML = "+"
      } else {
        d3.select(`.${mclass}`).transition().style("display", "block")
        e.target.dataset.collapsed = "false";
        document.getElementsByClassName(`${self}`)[0].innerHTML = "-"
      }
    }
    window.handleClick = handleClick;
  }

  renderCollapseArea();
  // Toggle children on click.
  function click(d: any) {
    if (d.id === getCount(root) + 1) {
      //折叠前隐藏div区域
      setTimeout(function () {
        d3.selectAll("foreignObject").style("display", "none");
      }, 200)
    }
    if (d.children) {
      d._children = d.children;
      d.children = null;
      update(d);
    } else if (d._children) {
      d.children = d._children;
      d._children = null;
      update(d);
    } else if (!d.children && !d._children) {
      let _y = (d.depth + 1) * 2;
      getNode(d, function (children: any) {
        children.forEach((node: any) => {
          node.depth = d.depth + 1;
          node.height = 0;
          node.x = d.x;
          node.x0 = d.x0;
          node.y = _y;
          node.y0 = _y;
          node.parent = d;
        })
        d.children = children;
        d._children = d.children;
        update(d);
      })
    }
  }

  //异步请求node
  function getNode(d: any, callback: Function) {
    //d是被点击的节点 将这个作为参数去获取不同的子节点
    //模拟接口
    setTimeout(() => {
      let res: any = {
        zhejiang: [{ data: { name: "杭州" } },
        { data: { name: "宁波" } },
        { data: { name: "温州" } }],
        guangxi: [
          { data: { name: "桂林" } },
          { data: { name: "南宁" } },
          { data: { name: "柳州" } },
          { data: { name: "防城港" } }
        ]
      }
      let _res = res[`${d.data._name}`];
      _res && callback(_res);
    }, 100);
  }
}


export default class tree extends PureComponent {
  render() {
    D3().then(Instance => {
      const { selectAll, zoom, select, tree, hierarchy, linkHorizontal } = Instance;
      d3 = {
        selectAll, zoom, select, tree, hierarchy, linkHorizontal
      };
      d3 && createTree()
    });
    return (
      <div id='svgContainer'>
        <div id="body"></div>
      </div>
    )
  }
}

 类似资料: