d3.js 构建股权架构图并绘制股权百分比

效果:

代码:

StockStructureChart.js

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

const StockStructureChart = ({ data }) => {
  const ref = useRef()

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

  useEffect(() => {
    if (data && ref.current) {
      const svg = d3.select(ref.current).append("g")

      const tree = d3
        .tree()
        .nodeSize([boxWidth + 20, boxHeight + 20])
        .separation(function (a, b) {
          return a.parent === b.parent ? 1 : 1
        })
      const root = d3.hierarchy(data)
      const treeData = tree(root)

      //节点树垂直居中处理
      //root.height 为节点树的深度, 这里是估算的树的总高度
      const nodeHeight = boxHeight * 4
      const totalTreeHeight = root.height * nodeHeight
      const center = height / 2
      const offset = center - totalTreeHeight / 2
      //调整 y 坐标(注:左上角为坐标原点),使图形垂直居中
      treeData.each((d) => {
        d.y = height - d.depth * nodeHeight - offset
        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}`
      }

      //节点链接的箭头符号
      svg
        .append("defs")
        .append("marker")
        .attr("id", "arrowDown")
        .attr("markerUnits", "userSpaceOnUse")
        .attr("viewBox", "-5 0 10 10")
        .attr("refX", 0)
        .attr("refY", boxHeight / 2 + 13) //箭头与节点的距离: boxHeight/2(15) + markerHeight(10) + delta(3)
        .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")

      //绘制链接
      svg
        .selectAll(".link")
        .data(treeData.links())
        .enter()
        .append("path")
        .attr("class", "link")
        .attr("fill", "none")
        .attr("stroke", "#ccc")
        .attr("stroke-width", "2px")
        .attr("marker-start", "url(#arrowDown)")
        .attr("d", straightLine)

      //链接线上的文字
      svg
        .selectAll(".link-text")
        .data(treeData.links())
        .enter()
        .append("text")
        .attr("text-anchor", "middle")
        .attr("class", "link-text")
        .attr("fill", "black")
        .attr("font-size", "12px")
        .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 = svg
        .selectAll(".node")
        .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", "steelblue")
        .style("fill", "white")

      //节点文字
      nodes
        .append("text")
        .attr("font-size", "14px")
        .style("text-anchor", "middle") // 文字的水平居中
        .style("dominant-baseline", "middle") // 文字的垂直居中
        .attr("fill", "black")
        .text((d) => d.data.name)

      //自动缩放
      const bounds = d3.select("svg 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.5) * 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)
      function zoomed(e) {
        d3.select("svg g").attr("transform", e.transform)
      }
      const zoom = d3.zoom().on("zoom", zoomed)
      d3.select(ref.current).call(zoom).call(zoom.transform, initialTransform)

      return () => {
        d3.select("svg g").remove()
      }
    }
  }, [data])

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

export default StockStructureChart

App.js

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

function App() {
  const data = {
    name: "Root",
    children: [
      {
        name: "Company A",
        percent: 50,
        children: [
          { name: "Subsidiary A1", percent: 100 },
          { name: "Subsidiary A2", percent: 200,
            children: [
              { name: "Subsidiary A2.1", percent: 50 },
              { name: "Subsidiary A2.2", percent: 100 },
            ],
           },
        ],
      },
      {
        name: "Company B",
        percent: 50,
        children: [{ name: "Subsidiary B1", percent: 350 }],
      },
      {
        name: "Company C",
        percent: 50,
        children: [{ name: "Subsidiary C1", percent: 150 }],
      },
      {
        name: "Company C",
        percent: 50,
        children: [{ name: "Subsidiary C1", percent: 150 }],
      },
      {
        name: "Company C",
        percent: 50,
        children: [{ name: "Subsidiary C1", percent: 150 }],
      },
    ],
  }

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

export default App

posted on 2024-06-04 13:58  Lemo_wd  阅读(4)  评论(0编辑  收藏  举报

导航