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(() => {
    const chartRef = ref.current
    //移除旧图形
    d3.select("svg g").remove()

    const svg = d3.select(chartRef).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)

    //节点树垂直居中处理
    //注1:root.height 为节点树的深度, 这里是估算的树的总高度
    const nodeHeight = boxHeight * 4
    const totalTreeHeight = root.height * nodeHeight
    const center = height / 2
    const offset = center - totalTreeHeight / 2
    //注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", "#555")
      .attr("stroke-width", 1)
      .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", "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 = 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("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", "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)

    // 默认缩放
    function initialTransform() {
      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.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) => {
      d3.select("svg g").attr("transform", e.transform)
    })

    //初始化缩放
    d3.select(chartRef)
      .call(zoom)
      .call(zoom.transform, initialTransformTranslate)
  }, [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  阅读(91)  评论(0编辑  收藏  举报

导航