惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

博客园 - 叶小钗
Martin Fowler
Martin Fowler
H
Help Net Security
月光博客
月光博客
Stack Overflow Blog
Stack Overflow Blog
V
Visual Studio Blog
Recent Commits to openclaw:main
Recent Commits to openclaw:main
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
K
Kaspersky official blog
MongoDB | Blog
MongoDB | Blog
L
LINUX DO - 最新话题
S
Schneier on Security
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
C
Cyber Attacks, Cyber Crime and Cyber Security
AI
AI
The Hacker News
The Hacker News
Latest news
Latest news
D
Darknet – Hacking Tools, Hacker News & Cyber Security
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
GbyAI
GbyAI
SecWiki News
SecWiki News
C
Check Point Blog
T
The Exploit Database - CXSecurity.com
J
Java Code Geeks
V2EX - 技术
V2EX - 技术
Y
Y Combinator Blog
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
U
Unit 42
Attack and Defense Labs
Attack and Defense Labs
Security Latest
Security Latest
Help Net Security
Help Net Security
博客园 - Franky
L
LINUX DO - 热门话题
N
News and Events Feed by Topic
Cloudbric
Cloudbric
P
Proofpoint News Feed
Hacker News - Newest:
Hacker News - Newest: "LLM"
博客园 - 【当耐特】
Schneier on Security
Schneier on Security
Google Online Security Blog
Google Online Security Blog
C
Cisco Blogs
Webroot Blog
Webroot Blog
C
CERT Recently Published Vulnerability Notes
Simon Willison's Weblog
Simon Willison's Weblog
A
Arctic Wolf
O
OpenAI News
T
Threat Research - Cisco Blogs
Know Your Adversary
Know Your Adversary
C
CXSECURITY Database RSS Feed - CXSecurity.com
The Last Watchdog
The Last Watchdog

博客园 - Lemo_wd

flutter —— iOS 的构建与分发 flutter —— 理解 iOS 的依赖管理 wireguard 入门 k3s 基础 —— 将 traefik 替换为 ingress-nginx Pr 入门 为什么 TCP 是3次握手4次挥手? 给 ssh 私钥文件配置统一口令 滚动部署零碎笔记1 Clickhouse 物化视图与投影 Clickhouse 主键索引 Clickhouse 核心概念 ClickHouse 案例学习 —— 使用 ClickHouse 探索 GitHub 统计信息 linux Tun/Tap 虚拟网卡 Clickhouse 的 kafka Engine 集成 Clickhouse 表引擎 —— MergeTree 局域网中的设备间的流量转发 final cut pro 入门 d3.js 构建股权架构图并绘制股权百分比 flutter 的滚动控制 —— 滚动类组件的内部与整体滚动
d3.js 构建股权架构图并绘制双向节点树
Lemo_wd · 2024-09-14 · via 博客园 - Lemo_wd

效果:

代码:

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