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