d3.js 构建股权架构图并绘制双向节点树

效果:

代码:

StockStructureChart.js

import React, { useEffect, useRef } from "react"
import * as d3 from "d3"

const StockStructureChart = ({ upwardData, downwardData }) => {
  const ref = useRef()

  const width = 800
  const height = 500
  const boxWidth = 220
  const boxHeight = 55

  useEffect(() => {
    const chartRef = ref.current
    const svg = d3.select(chartRef)

    // 删除旧的元素
    svg.selectAll("*").remove()

    // 创建渲染的组
    const g = svg.append("g")

    drawTree(g, upwardData, 1) // 绘制向上的树(位于根节点上方)
    drawTree(g, downwardData, 0) // 绘制向下的树(位于根节点下方)

    // 初始化缩放和变换
    function initialTransform() {
      const bounds = g.node().getBBox()
      const dx = bounds.width,
        dy = bounds.height,
        x = bounds.x,
        y = bounds.y
      const scale = Math.min(width / dx, height / dy, 1.4) * 0.9
      const translate = [
        width / 2 - scale * (x + dx / 2),
        height / 2 - scale * (y + dy / 2),
      ]
      const initialTransform = d3.zoomIdentity
        .translate(translate[0], translate[1])
        .scale(scale)
      return initialTransform
    }
    const initialTransformTranslate = initialTransform()

    // 缩放行为
    const zoom = d3.zoom().on("zoom", (e) => {
      g.attr("transform", e.transform)
    })

    // 应用初始缩放
    svg.call(zoom).call(zoom.transform, initialTransformTranslate)
  }, [upwardData, downwardData])

  /**
   * 根据方向绘制树
   *
   * @param {*} g - 渲染树的组元素
   * @param {*} data - 用于绘制树的数据
   * @param {*} direction - 1 代表向上,0 代表向下
   */
  function drawTree(g, data, direction) {
    const tree = d3
      .tree()
      .nodeSize([boxWidth + 20, boxHeight + 20])
      .separation((a, b) => (a.parent === b.parent ? 1 : 1))

    const treeData = tree(d3.hierarchy(data))

    treeData.each((d) => {
      const spacing = d.depth * boxHeight * 4
      d.y = direction === 1 ? -spacing : spacing
      d.x = d.x + width / 2
    })

    const straightLine = (d) => {
      const sourceX = d.source.x
      const sourceY = d.source.y
      const targetX = d.target.x
      const targetY = d.target.y
      return `M${sourceX},${sourceY}
       V${(targetY - sourceY) / 2 + sourceY}
       H${targetX}
       V${targetY}`
    }

    // 箭头符号
    g.append("defs")
      .append("marker")
      .attr("id", "arrowDown")
      .attr("markerUnits", "userSpaceOnUse")
      .attr("viewBox", "-5 0 10 10")
      .attr("refX", 0)
      .attr("refY", boxHeight / 2 + 15) //箭头与节点的距离: boxHeight/2(15) + markerHeight(10) + delta(5)
      .attr("markerWidth", 10)
      .attr("markerHeight", 10)
      .attr("orient", "0")
      .attr("stroke-width", 1)
      .append("path")
      .attr("d", "M-5,0L5,0L0,10")
      .attr("fill", "#215af3")

    // 连接线
    g.selectAll(".link-" + direction)
      .data(treeData.links())
      .enter()
      .append("path")
      .attr("class", "link")
      .attr("fill", "none")
      .attr("stroke", "#215af3")
      .attr("stroke-width", 1)
      .attr("stroke-opacity", 0.6)
      .attr("marker-start", direction === 1 && "url(#arrowDown)")
      .attr("marker-end", direction === 0 && "url(#arrowDown)")
      .attr("d", straightLine)

    // 连接线上的文字
    g.selectAll(".link-text-" + direction)
      .data(treeData.links())
      .enter()
      .append("text")
      .attr("text-anchor", "middle")
      .attr("class", "link-text")
      .attr("fill", "black")
      .attr("font-size", "13px")
      .attr("x", (d) => d.target.x + 30)
      .attr("y", (d) => d.source.y / 4 + (d.target.y * 3) / 4)
      .attr("dy", "5")
      .text((d) => d.target.data.percent)

    // 节点
    const nodes = g
      .selectAll(".node-" + direction)
      .data(treeData.descendants())
      .enter()
      .append("g")
      .attr("class", "node")
      .attr("transform", (d) => `translate(${d.x},${d.y})`)

    // 节点框
    nodes
      .append("rect")
      .attr("width", boxWidth)
      .attr("height", boxHeight)
      .attr(
        "transform",
        "translate(" + -boxWidth / 2 + "," + -boxHeight / 2 + ")"
      )
      .style("stroke-width", "1")
      .style("stroke", (d) => (d.depth === 0 ? "#215af3" : "#215af3"))
      .style("fill", (d) => (d.depth === 0 ? "#215af3" : "white"))

    // 节点文本
    nodes
      .append("foreignObject")
      .attr("width", boxWidth)
      .attr("height", boxHeight)
      .attr("x", -boxWidth / 2)
      .attr("y", -boxHeight / 2)
      .attr("font-size", (d) => (d.data.name.length > 70 ? "10px" : "14px"))
      .append("xhtml:div")
      .style("color", (d) => (d.depth === 0 ? "white" : "black"))
      .style("display", "flex")
      .style("justify-content", "center")
      .style("align-items", "center")
      .style("text-align", "center")
      .style("line-height", "1.2")
      .style("word-break", "break-word")
      .style("width", "100%")
      .style("height", "100%")
      .style("padding", "5px")
      .html((d) => d.data.name)
  }

  return <svg ref={ref} width={width} height={height} />
}

export default StockStructureChart

App.js

import "./App.css"
import StockStructureChart from "./components/StockStructureChart"

function App() {
  const upwardData = {
    name: "Root",
    children: [
      {
        name: "Company A",
        percent: 50,
      },
      {
        name: "Company B",
        percent: 50,
      },
      {
        name: "Company C",
        percent: 50,
      },
    ],
  }

  const downwardData = {
    name: "Root",
    children: [
      {
        name: "Company L",
        percent: 50,
        children: [
          {
            name: "Company M",
            percent: 100,
          },
        ],
      },
      {
        name: "Company N",
        percent: 50,
      },
    ],
  }

  return (
    <div className="App">
      <h1>Hello React + D3 world!</h1>
      <StockStructureChart
        upwardData={upwardData}
        downwardData={downwardData}
      />
    </div>
  )
}

export default App

posted on 2024-09-14 12:27  Lemo_wd  阅读(18)  评论(0编辑  收藏  举报

导航