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