目录
  • 前言
    • 最终效果:
    • 版本信息:
    • 股权穿透图基础功能:
    • 股权结构图基础功能:
    • 股权穿透图代码
    • 股权结构图代码
  • 总结:

    前言

    umi+antd-admin 框架中使用hooks结合d3完成类似股权穿透图和股权结构图(web)

    最终效果:

    股权穿透图

    react hooks d3实现企查查股权穿透图结构图效果详解

    股权结构图

    react hooks d3实现企查查股权穿透图结构图效果详解

    版本信息:

    "d3": "4.13.0",

    "antd": "3.24.2",

    "umi": "^2.7.7",

    股权穿透图基础功能:

    1、默认上下游信息展示,如果没有上下游信息只展示自己

    2、点击请求子节点信息展示,收起子节点

    3、全屏功能

    4、放大器放大缩小(react项目中不知道为啥使用d3.zoom方法不好使,可能跟网页中滚动事件冲突有关,最后选择单独放置放大器进行放大缩小功能)

    5、移动功能

    股权结构图基础功能:

    1、tab切换展示上游或下游信息

    2、默认展示一层

    3、点击请求子节点信息展示,收起子节点

    代码链接: github.com/QiuDaShua/r…

    股权穿透图代码

    俺认为的关键都写在注释中了

    import React,{ useEffect,useRef, useState} from 'react';
    import { Col, Row, Slider, Spin,message } from 'antd';
    import * as d3Chart from 'd3';
    import fullScreen from '../../../../../../assets/fullScreen.png'
    import { EncryptBase64 } from '../../../../../Common/Encrypt';
    import { FetchEquityBelowInfo,FetchEquityUpperInfo } from '../../../../../../services/companysearch'
    import { formatMoney } from '../../../../../../utils/splitMoney'
    // 过渡时间
    const DURATION = 0
    // 加减符号半径
    const SYMBOLA_S_R = 9
    // 公司
    const COMPANY = '0'
    // 人
    const PERSON = '1'
    export default function RightPenetration(props){
      let state  = useRef({
          layoutTree: '',
          diamonds: '',
          d3: d3Chart,
          hasChildOpenNodeArr: [],
          originDiamonds: '',
          diagonalUp: '',
          diagonalDown: '',
          rootUp: '',
          rootDown: '',
          svg: '',
          svgH: 500,
          svgW: 1600,
      })
      const isFullRef = useRef()
      const [isFull,setIsFull] = useState(false) 
      const [scaleN,setScaleN] = useState(1) 
      const [tree,setTree] = useState({
        // 'name': '大公司', 
        // 'id': '1',
        // 'children': [{
        //   'children': [], 
        //   'money': 3000,
        //   'scale': 30,
        //   'name': '大公司黔西南分公司', 
        //   'id': '1-1',
        //   'type': '0'
        // }, {
        //   'children': [], 
        //   'money': 3000,
        //   'scale': 30,
        //   'name': '大公司六盘水分公司', 
        //   'id': '1-2',
        //   'type': '0'
        // }, {
        //   'children': [], 
        //   'money': 3000,
        //   'scale': 30,
        //   'name': '大公司贵阳分公司', 
        //   'id': '1-3',
        //   'type': '0'
        // }, {
        //   'children': [], 
        //   'money': 3000,
        //   'scale': 30,
        //   'name': '大公司安顺分公司', 
        //   'id': '1-4',
        //   'type': '0'
        // }, {
        //   'children': [], 
        //   'money': 3000,
        //   'scale': 30,
        //   'name': '大公司毕节分公司', 
        //   'id': '1-5',
        //   'type': '0'
        // }, {
        //   'children': [],
        //   'money': 3000,
        //   'scale': 30,
        //   'name': '大公司遵义分公司', 
        //   'id': '1-6',
        //   'type': '0'
        // }, {
        //   'children': [], 
        //   'money': 3000,
        //   'scale': 30,
        //   'name': '大公司黔东南分公司', 
        //   'id': '1-7',
        //   'type': '0'
        // }, {
        //   'children': [
        //     {'controlPerson': false, 'children': [], 'old': false, 'name': '大公司黔南分公司下属公司1', 'id': '1-8-1', 'money': 200, 'scale': 20, 'type': '0'},
        //     {'controlPerson': false, 'children': [], 'old': false, 'name': '大公司黔南分公司下属公司2', 'id': '1-8-2',  'money': 200, 'scale': 20, 'type': '0'},
        //   ], 
        //   'money': 3000,
        //   'scale': 30,
        //   'name': '大公司铜仁分公司', 
        //   'id': '1-8', 
        //   'type': '0'
        // }, {
        //   'children': [
        //     {'controlPerson': false, 'children': [], 'old': false, 'name': '大公司黔南分公司下属公司1', 'id': '1-9-1',  'money': 200, 'scale': 20, 'type': '0'},
        //     {'controlPerson': false, 'children': [], 'old': false, 'name': '大公司黔南分公司下属公司2', 'id': '1-9-2',  'money': 200, 'scale': 20, 'type': '0'},
        //   ], 
        //   'name': '大公司黔南分公司',
        //   'id': '1-9', 
        //   'money': 3000, 
        //   'scale': 30,
        //   'type': '0'
        //   }
        // ], 
        // 'parents': [
        //   {
        //     'controlPerson': true, 
        //     'money': '3000', 
        //     'children': [
        //       {'controlPerson': true, 'money': '3000', 'children': [], 'parentMoney': 3000, 'old': true, 'id': '1-01-1', 'name': '发展公司父级公司1', 'scale': 30, 'type': '0', 'oldUrlName': ''},
        //       {'controlPerson': true, 'money': '3000', 'children': [], 'parentMoney': 3000, 'old': true, 'id': '2-01-1', 'name': '发展公司父级公司2', 'scale': 70, 'type': '0', 'oldUrlName': ''},
        //     ], 
        //     'name': '发展公司', 
        //     'id': '01-1', 
        //     'scale': 90, 
        //     'type': '0', 
        //     'oldUrlName': ''
        //   }
        // ]
      })
      const [isLoading,setLoading] = useState(false)
      useEffect(() => {
        isFullRef.current = isFull
     }, [isFull])
      // 获取文字长度
      const getStringLength = (str) => {
        let realLength = 0, len = str.length, charCode = -1;
        for (let i = 0; i < len; i++) {
          charCode = str.charCodeAt(i);
          if (charCode >= 0 && charCode < 65){
            realLength += 1;
          }else if(charCode > 90 && charCode <= 128){
            realLength += 1;
          }else if(charCode >= 65 && charCode <= 90){
            realLength += 1.3;
          }else{
            realLength += 2;
          }
        }
        return realLength / 2;
      };
      // 连线
      const diagonal =(s, d, showtype) =>{
        // 曲线
        // if(s.x !== undefined && s.y !== undefined && d.x !== undefined && d.y !== undefined){
        //   let path
        //   if (showtype === 'up') {
        //     path = `M ${s.x} ${-s.y + 35}
        //   C${s.x} -${(s.y + d.y) * 0.45},
        //    ${s.x} -${(s.y + d.y) * 0.45},
        //     ${d.x} -${d.y}`;
        //   } else {
        //     path = `M ${s.x} ${s.y}
        //   C${s.x} ${(s.y + d.y) * 0.45},
        //    ${s.x} ${(s.y + d.y) * 0.45},
        //     ${d.x} ${d.y}`;
        //   }
        //   return path;
        // }
          // 折线
          var endMoveNum = 0;
          var moveDistance = 0;
          if (d) {
            if (showtype == 'down') {
              var downMoveNum =  d.depth ? state.current.diamonds.h/2 : state.current.originDiamonds.h/2 -10 ;
              // var downMoveNum =  30;
              let tmpNum = s.y + (d.y - s.y) / 2;
              endMoveNum = downMoveNum;
              moveDistance = tmpNum + endMoveNum;
            } else {
              var upMoveNum = d.depth ? 0 : -state.current.originDiamonds.h/2 +10 ;
              let tmpNum = d.y + (s.y - d.y) / 2;
              endMoveNum = upMoveNum;
              moveDistance = tmpNum + endMoveNum;
            }
          }
          if (showtype === 'up') {
            return (
              'M' +
              s.x +
              ',' +
              -s.y +
              'L' +
              s.x +
              ',' +
              -moveDistance +
              'L' +
              d.x +
              ',' +
              -moveDistance +
              'L' +
              d.x +
              ',' +
              -d.y
            );
          }else {
            return (
              'M' +
              s.x +
              ',' +
              s.y +
              'L' +
              s.x +
              ',' +
              moveDistance +
              'L' +
              d.x +
              ',' +
              moveDistance +
              'L' +
              d.x +
              ',' +
              d.y
            );
          }
      }
      // 拷贝到_children 隐藏1排以后的树 通过数组记录下已经展开的节点 使全屏前后展开的节点是一样的
      const collapse = (source) => {
        if (!state.current.hasChildOpenNodeArr.includes(source.data.id) && source.children) {
          source._children = source.children;
          // source._children.forEach(collapse);
          source.children = null;
        }
      }
      // 请求获取下游信息
      const getBelow = async (id) =>{
        setLoading(true)
        const dataSource = [];
        try{
          const response = await FetchEquityBelowInfo({
            instId: id,
            currentPage: 0,
            pageSize: 200,
          })
          const { records = [] } = response
          records.forEach(element =>{
          dataSource.push({
              isHaveChildren:null,
              money:element.amount ? formatMoney((element.amount / 10000).toFixed(2)) :'--',
              scale:element.hold_rati || '--%',
              name:element.chn_full_nm || '--',
              id:element.inst_cust_id || '--',
              type:'0'
            })
          })
          setLoading(false)
          return dataSource
        }catch(error){
          return dataSource
        }
      }
      // 请求获取上游信息
      const getUpper = async (id,regCapi) =>{
        setLoading(true)
        const dataSource = [];
        try{
          const response = await FetchEquityUpperInfo({
            instId: id,
            currentPage: 0,
            pageSize: 200,
            regCapi,
          })
          const { records = [] } = response
          records.forEach(element =>{
          dataSource.push({
              isHaveChildren:null,
              money:element.amount ? formatMoney((element.amount / 10000).toFixed(2)) :'--',
              scale:element.hold_rati || '--%',
              name:element.chn_full_nm || '--',
              id:element.inst_cust_id || '--',
              type:'0'
            })
          })
          setLoading(false)
          return dataSource
        }catch(error){
          return dataSource
        }
      }
      // 圆圈点击事件
      const click = async (source, showType,nodes) =>{
        // 数据全部请求回来的情况
        // if (source.depth) {
            // if(source.children){
            //   // 点击减号
            //   source._children = source.children;
            //   source.children = null;
            // }else {
            //   // 点击加号
            //   source.children = source._children;
            //   source._children = null;
            //   let gbox = document.getElementById('penetrateChart').childNodes[0].childNodes[0]
            //   let x = gbox.getAttribute('transform')
            //   const decompose = x.match(/translate((\S+),(\S+))/);
            //   const scale = x.match(/scale((\S+))/)
            //   if (Array.isArray(decompose) && Array.isArray(scale) && decompose[2] && scale[1]) {
            //     gbox.setAttribute(
            //       'transform',
            //       `translate(${parseFloat(decompose[1])},${parseFloat(+decompose[2]+ (showType === 'up'? 200:-200))}) scale(${parseFloat(scale[1])})`
            //     );
            //   }else{
            //     gbox.setAttribute(
            //       'transform',
            //       `translate(${parseFloat(decompose[1])},${parseFloat(+decompose[2]+(showType === 'up'? 200:-200))})`
            //     );
            //   }
            // }
          // }
           // 点击加号时才去请求节点信息的情况
            if(source.children){
              // 点击减号
              source._children = source.children;
              source.children = null;
              state.current.hasChildOpenNodeArr = state.current.hasChildOpenNodeArr.filter(item => item !== source.data.id)
            }else {
              // 点击加号
              state.current.hasChildOpenNodeArr.push(source.data.id);
              if(!source._children){
                let res = []
                if(showType === 'up'){
                  res = await getUpper(source.data.id,source.data.regCapi)
                }else {
                  res = await getBelow(source.data.id)
                }
                if(!res.length){
                  message.warning('上游或下游企业信息为空!')
                  return
                }
                res.forEach(item =>{
                  let newNode = state.current.d3.hierarchy(item)
                  newNode.depth = source.depth + 1; 
                  newNode.height = source.height - 1;
                  newNode.parent = source; 
                  if(!source.children){
                    source.children = [];
                    source.data.children = [];
                  }
                  source.children.push(newNode);
                  source.data.children.push(newNode.data);
                })
              }else{
                source.children = source._children;
                source._children = null;
              }
              // 点击后将节点移动到中间位置
              let gbox = document.getElementById('penetrateChart').childNodes[0].childNodes[0]
              let x = gbox.getAttribute('transform')
              const decompose = x.match(/translate((\S+),(\S+))/);
              const scale = x.match(/scale((\S+))/)
              let dy = showType === 'up' ? state.current.svgH/2 + nodes[0].y + source.y +10 : state.current.svgH/2 + nodes[0].y - source.y - 10
              let dx = state.current.svgW/2 + nodes[0].x - source.x
              if (Array.isArray(decompose) && Array.isArray(scale) && decompose[2] && scale[1]) {
                // gbox.setAttribute(
                //   'transform',
                //   `translate(${parseFloat(decompose[1])},${parseFloat(+decompose[2]+ (showType === 'up'? 200:-200))}) scale(${parseFloat(scale[1])})`
                // );
                state.current.svg.attr('transform', 'translate(' + dx + ',' + dy + ') scale(' + parseFloat(scale[1]) + ')');
              }else{
                // gbox.setAttribute(
                //   'transform',
                //   `translate(${parseFloat(decompose[1])},${parseFloat(+decompose[2]+(showType === 'up'? 200:-200))})`
                // );
                state.current.svg.attr('transform', 'translate(' + dx + ',' + dy + ')');
              }
            }  
            update(source, showType)
        }
      /*
           *[update 函数描述], [click 函数描述]
           *  @param  {[Object]} source 第一次是初始源对象,后面是点击的对象
           *  @param  {[String]} showtype up表示向上 down表示向下
           *  @param  {[Object]} sourceTree 初始源对象
           */
      const update = (source, showtype) => {        
            if (source.parents === null) {
              source.isOpen = !source.isOpen
            }
            let nodes
            if (showtype === 'up') {
              nodes = state.current.layoutTree(state.current.rootUp).descendants()
            } else {
              nodes = state.current.layoutTree(state.current.rootDown).descendants()
            }
            let links = nodes.slice(1);
            nodes.forEach(d => {
              d.y = d.depth * (d.depth == 1 ? 150: state.current.diamonds.intervalH);
            });
            let node = state.current.svg.selectAll('g.node' + showtype)
              .data(nodes, d => d.data.id || '');
            let nodeEnter = node.enter().append('g')
              .attr('class',  d => showtype === 'up' && !d.depth ? 'hide-node' : 'node' + showtype)
              .attr('transform', d => showtype === 'up' ? 'translate(' + d.x + ',' + -(d.y) + ')' : 'translate(' + d.x + ',' + d.y + ')')
              .attr('opacity',d => showtype === 'up' && !d.depth? (state.current.rootDown.data.children.length ? 0 : 1) : 1); // 拥有下部分则隐藏初始块  d => showtype === 'up' && !d.depth ? (state.current.rootDown.data.children.length ? 0 : 1) : 1
            // 创建矩形
            nodeEnter.append('rect')
              .attr('type', d => d.data.id+ '_' +d.depth)
              .attr('width', d => d.depth ? state.current.diamonds.w : getStringLength(d.data.name) * 22)
              .attr('height', d => d.depth ? (d.data.type === COMPANY ? state.current.diamonds.h : state.current.diamonds.h - 10) : state.current.originDiamonds.h)
              .attr('x', d => d.depth ? -state.current.diamonds.w / 2 : -getStringLength(d.data.name) * 22 / 2)
              .attr('y', d => d.depth ? showtype === 'up' ? -state.current.diamonds.h / 2 : 0 : -15)
              .attr('stroke', d => d.data.type === COMPANY || !d.depth ? '#DE4A3C' : '#7A9EFF')
              .attr('stroke-width', 1)
              .attr('rx', 10)
              .attr('ry', 10)
              .style('fill',d => {
                if (d.data.type === COMPANY || !d.depth) {
                  return d.depth ? '#fff' : '#DE4A3C'
                } else if (d.data.type === PERSON) {
                  return '#fff'
                }
              }
              );
            // 创建圆 加减
            let circle = nodeEnter.append('g')
            .attr('class', 'circle')
            .on('click', function (d) {
              click(d, showtype,nodes)
            });
            circle.append('circle')
              .attr('type', d => d.data.id+ '_' +d.depth || '')
              .attr('r', (d) => d.depth ? (d.data.isHaveChildren ? SYMBOLA_S_R : 0) : 0)
              .attr('cy', d => d.depth ? showtype === 'up' ? -(SYMBOLA_S_R + state.current.diamonds.h / 2) : state.current.diamonds.h + SYMBOLA_S_R : 0)
              .attr('cx', 0)
              .attr('fill', '#F9DDD9')
              .attr('stroke', '#FCEDEB')
              .style('stroke-width', 1)
            circle.append('text')
            .attr('x', 0)
            .attr('dy', d => d.depth ? (showtype === 'up' ? -(SYMBOLA_S_R / 2 + state.current.diamonds.h / 2) : state.current.diamonds.h + SYMBOLA_S_R + 4) : 0)
            .attr('text-anchor', 'middle')
            .attr('class', 'fa')
            .style('fill', '#DE4A3C')
            .text(function(d) {
              if(d.depth){
                if (d.children) {
                  return '-';
                } else if (d._children || d.data.isHaveChildren) {
                  return '+';
                } else {
                  return '';
                }
              } else {
                return '';
              }
            })
            .style('font-size', '16px')
            .style('cursor', 'pointer');
            node.select('.fa')
            .text(function (d) {
              if (d.children) {
                return '-';
              } else if (d._children || d.data.isHaveChildren) {
                return '+';
              } else {
                return '';
              }
            })
            // 持股比例
            nodeEnter.append('g')
              .attr('transform', () => 'translate(0,0)')
              .append('text')
              .attr('x', 35)
              .attr('y', showtype === 'up' ? state.current.diamonds.h -20 : -10)
              .attr('text-anchor', 'middle')
              .attr('fill', d => d.data.type === COMPANY ? '#DE4A3C' : '#7A9EFF')
              .attr('opacity', d => !d.depth ? 0 : 1)
              .text(d => d.data.scale)
              .style('font-size', '14px')
              .style('font-family', 'PingFangSC-Regular')
              .style('font-weight', '400');
            // 公司名称
            // y轴 否表源头的字体距离
            nodeEnter.append('text')
              .attr('x', 0)
              .attr('y', d => {
                // 如果是上半部分
                if (showtype === 'up') {
                  // 如果是1层以上
                  if (d.depth) {
                    return -state.current.diamonds.h / 2
                  } else {
                    // 如果名字长度大于12个
                    // if (getStringLength(d.data.name) > 12) {
                    //   return -5
                    // }
                    return 0
                  }
                } else {
                  if (d.depth) {
                    return 0
                  } else {
                    // if (getStringLength(d.data.name) > 12) {
                    //   return -5
                    // }
                    return 0
                  }
                }
              })
              .attr('dy', d => d.depth ? (d.data.name.length > 12 ? '1.5em' : '2em') : `${state.current.originDiamonds.h/2 - 10}px`)
              .attr('text-anchor', 'middle')
              .attr('fill', d => d.depth ? '#DE4A3C' : '#fff')
              .text(d => d.depth ? (d.data.name.length > 12) ? d.data.name.substr(0, 12) : d.data.name :  d.data.name)
              .style('font-size', d => d.depth ? '16px' : '20px')
              .style('font-family', 'PingFangSC-Medium')
              .style('font-weight', '500')
              .style('cursor','pointer')
              .on('click', function (d) {
                if(d.data.id && d.depth){
                  if(isFullRef.current){
                    handleFullScreen()
                  }
                  // 点击操作
                }
              });
            // 名称过长 第二段
            nodeEnter.append('text')
              .attr('x', 0)
              .attr('y', d => {
                 // ? (d.depth ? -this.diamonds.h / 2 : 0) : 0
                if (showtype === 'up') {
                  if (d.depth) {
                    return -state.current.diamonds.h / 2
                  }
                  return 8
                } else {
                  if (!d.depth) {
                    return 8
                  }
                  return 0
                }
              })
              .attr('dy', d => d.depth ? '3em' : '.3em')
              .attr('text-anchor', 'middle')
              .attr('fill', d => d.depth ? '#DE4A3C' : '#fff')
              .text(d => {
                // 索引从第22个开始截取有表示超出
                if(d.depth){
                  if (d.data.name.substr(22, 1)) {
                    return d.data.name.substr(12, 10) + '...'
                  }
                  return d.data.name.substr(12, 10)
                }else {
                  return null
                }
              })
              .style('font-size', '16px')
              .style('font-family', 'PingFangSC-Medium')
              .style('font-weight', '500');
            // 认缴金额
            nodeEnter.append('text')
              .attr('x', 0)
              .attr('y', showtype === 'up' ? -state.current.diamonds.h / 2 : 0)
              .attr('dy', d => d.data.name.substr(12, d.data.name.length).length ? '5.5em' : '4.5em')
              .attr('text-anchor', 'middle')
              .attr('fill', d => d.depth ? '#465166' : '#fff')
              .text(d => d.data.money ? d.data.money.length > 20 ? `认缴金额:${d.data.money.substr(0,20)}…` : `认缴金额:${d.data.money}万元` : '')
              .style('font-size', '14px')
              .style('font-family', 'PingFangSC-Regular')
              .style('font-weight', '400')
              .style('color', '#3D3D3D');
            /*
            * 绘制箭头
            * @param  {string} markerUnits [设置为strokeWidth箭头会随着线的粗细发生变化]
            * @param {string} viewBox 坐标系的区域
            * @param {number} markerWidth,markerHeight 标识的大小
            * @param {string} orient 绘制方向,可设定为:auto(自动确认方向)和 角度值
            * @param {number} stroke-width 箭头宽度
            * @param {string} d 箭头的路径
            * @param {string} fill 箭头颜色
            * @param {string} id resolved0表示公司 resolved1表示个人
            * 直接用一个marker达不到两种颜色都展示的效果
            */
            nodeEnter.append('marker')
              .attr('id', showtype + 'resolved0')
              .attr('markerUnits', 'strokeWidth')
              .attr('markerUnits', 'userSpaceOnUse')
              .attr('viewBox', '0 -5 10 10')
              .attr('markerWidth', 12)
              .attr('markerHeight', 12)
              .attr('orient', '90')
              .attr('refX', () => showtype === 'up' ? '-50' : '10')
              .attr('stroke-width', 2)
              .attr('fill', '#DE4A3C')
              .append('path')
              .attr('d', 'M0,-5L10,0L0,5')
              .attr('fill', '#DE4A3C');
            nodeEnter.append('marker')
              .attr('id', showtype + 'resolved1')
              .attr('markerUnits', 'strokeWidth')
              .attr('markerUnits', 'userSpaceOnUse')
              .attr('viewBox', '0 -5 10 10')
              .attr('markerWidth', 12)
              .attr('markerHeight', 12)
              .attr('orient', '90')
              .attr('refX', () => showtype === 'up' ? '-50' : '10')
              .attr('stroke-width', 2)
              .append('path')
              .attr('d', 'M0,-5L10,0L0,5')
              .attr('fill', '#7A9EFF');
            // 将节点转换到它们的新位置。
            let nodeUpdate = node
              // .transition()
              // .duration(DURATION)
              .attr('transform', d => showtype === 'up' ? 'translate(' + d.x + ',' + -(d.y) + ')' : 'translate(' + d.x + ',' + (d.y) + ')');
            // 将退出节点转换到父节点的新位置.
            let nodeExit = node.exit()
              // .transition()
              // .duration(DURATION)
              .attr('transform', () => showtype === 'up' ? 'translate(' + source.x + ',' + -(source.y) + ')' : 'translate(' + source.x + ',' + (parseInt(source.y)) + ')')
              .remove();
            nodeExit.select('rect')
              .attr('width', state.current.diamonds.w)
              .attr('height', state.current.diamonds.h)
              .attr('stroke', 'black')
              .attr('stroke-width', 1);
            // 修改线条
            let link = state.current.svg.selectAll('path.link' + showtype)
              .data(links, d =>  d.data.id);
            // 在父级前的位置画线。
            let linkEnter = link.enter().insert('path', 'g')
              .attr('class', 'link' + showtype)
              .attr('marker-start', d => `url(#${showtype}resolved${d.data.type})`)// 根据箭头标记的id号标记箭头
              .attr('stroke', d => d.data.type === COMPANY ? '#DE4A3C' : '#7A9EFF')
              .style('fill-opacity', 1)
              .attr('fill', 'none')
              .attr('stroke-width', '1px')
              // .transition()
              // .duration(DURATION)
              .attr('d', () => {
                let o = {x: source.x0, y: source.y0};
                return diagonal(o, o, showtype)
              });
            let linkUpdate = linkEnter.merge(link);
            // 过渡更新位置.
            linkUpdate
              // .transition()
              // .duration(DURATION)
              .attr('d', d => diagonal(d, d.parent, showtype));
            // 将退出节点转换到父节点的新位置
            link.exit()
              // .transition()
              // .duration(DURATION)
              .attr('d', () => {
                let o = {
                  x: source.x,
                  y: source.y
                };
                return diagonal(o, o, showtype)
              }).remove();
            // 隐藏旧位置方面过渡.
            nodes.forEach(d => {
              d.x0 = d.x;
              d.y0 = d.y
            });
      } 
      // 初始化
      const init = () =>{
          let d3 = state.current.d3
          let svgW = state.current.svgW
          let svgH = state.current.svgH
          // console.log('init',svgW, svgH)
          // 方块形状
          state.current.diamonds = {
            w: 240,
            h: 94,
            intervalW: 280,
            intervalH: 180
          }
          // 源头对象
          state.current.originDiamonds = {
            w: 240,
            h: 56,
          }
          state.current.layoutTree = d3.tree().nodeSize([state.current.diamonds.intervalW, state.current.diamonds.intervalH]).separation(() => 1);
          // 主图
          state.current.svg = d3.select('#penetrateChart').append('svg').attr('width', svgW).attr('height', svgH).attr('id', 'treesvg')
            .attr('style', 'position: relative;z-index: 2') // background-image:url(${setWatermark().toDataURL()})
            // .call(d3.zoom().scaleExtent([0.3, 3]).on('zoom', () => {
            //   state.current.svg.attr('transform', d3.event.transform.translate(svgW / 2, svgH / 2));
            // }))
            .append('g').attr('id', 'g').attr('transform', 'translate(' + (svgW / 2) + ',' + (svgH / 2) + ')')
            // 可以被拖动的功能
            var obox = document.getElementById('penetrateChart').childNodes[0];
            var gbox = document.getElementById('penetrateChart').childNodes[0].childNodes[0];
            obox.addEventListener('mousedown', function (evt) {
              // 点击时候停止
              document.onclick = function () {
                document.onmousemove = null;
                document.onmouseup = null;
              };
              var oEvent = evt // 获取事件对象,这个是兼容写法
              var disX = oEvent.clientX;
              var disY = oEvent.clientY;
              // let arr = gbox.getAttribute('transform')
              //   .replace('translate(', '')
              //   .replace(')', '')
              //   .split(',');
              let x = gbox.getAttribute('transform')
              const decompose = x.match(/translate((\S+),(\S+))/)
              const scale = x.match(/scale((\S+))/)
              // 这里就解释为什么要给document添加onmousemove时间,原因是如果你给obox添加这个事件的时候,当你拖动很快的时候就很快脱离这个onmousemove事件,而不能实时拖动它
              document.onmousemove = function (evt) {
                // 实时改变目标元素obox的位置
                  var oEvent = evt
                  if (Array.isArray(decompose) && Array.isArray(scale) && decompose[2] && scale[1]) {
                    gbox.setAttribute(
                      'transform',
                      `translate(${oEvent.clientX - disX + parseFloat(decompose[1])},${oEvent.clientY - disY + parseFloat(decompose[2])}) scale(${parseFloat(scale[1])})`
                    );
                  }else{
                    gbox.setAttribute(
                      'transform',
                      `translate(${oEvent.clientX - disX + parseFloat(decompose[1])},${oEvent.clientY - disY + parseFloat(decompose[2])})`
                    );
                  }
                  // 停止拖动
                  document.onmouseup = function () {
                    document.onmousemove = null;
                    document.onmouseup = null;
                  };
                }
              })
             // 拷贝树的数据
              let upTree = null
              let downTree = null
              Object.keys(tree).map(item => {
                if (item === 'parents') {
                  upTree = JSON.parse(JSON.stringify(tree))
                  upTree.children = tree[item]
                  upTree.parents = null
                } else if (item === 'children') {
                  downTree = JSON.parse(JSON.stringify(tree))
                  downTree.children = tree[item]
                  downTree.parents = null
                }
              })
                // hierarchy 返回新的结构 x0,y0初始化起点坐标
                state.current.rootUp = d3.hierarchy(upTree, d => d.children)
                state.current.rootDown =  d3.hierarchy(downTree, d => d.children)
                state.current.rootUp.x0 = 0
                state.current.rootUp.y0 = 0
                state.current.rootDown.x0 = 0
                state.current.rootDown.y0 = 0;
              // 上 和 下 结构
              let treeArr = [
                {
                  data: state.current.rootUp,
                  type: 'up'
                },
                {
                  data: state.current.rootDown,
                  type: 'down'
                }
              ]
              if(!tree['children'].length && !tree['parents'].length){
                updataSelf()
              }else{
                treeArr.map(item => {
                  if (item.data.children) {
                    item.data.children.forEach(collapse);
                    update(item.data, item.type, item.data)
                  }
                })
              }
      }
      const updataSelf = () =>{
        let nodes = state.current.rootUp.descendants()
        let node = state.current.svg.selectAll('g.node')
          .data(nodes, d => d.data.id || '');
        let nodeEnter = node.enter().append('g')
          .attr('class', d => 'node node_' + d.depth) //d => showtype === 'up' && !d.depth ? 'hide-node' :
          // .attr('transform', 'translate(' + (svgW / 2) + ',' + (svgH / 2) + ')')
          .attr('opacity', 1); // 拥有下部分则隐藏初始块  d => showtype === 'up' && !d.depth ? (state.current.rootDown.data.children.length ? 0 : 1) : 1
        // 创建矩形
        nodeEnter.append('rect')
          .attr('type', d => d.data.id + '_' + d.depth)
          .attr('width', d => d.depth ? state.current.diamonds.w : getStringLength(d.data.name) * 22)
          .attr('height', d => d.depth ? (d.data.type === COMPANY ? state.current.diamonds.h : state.current.diamonds.h - 10) : state.current.originDiamonds.h)
          .attr('x', d => d.depth ? -state.current.diamonds.w / 2 : -getStringLength(d.data.name) * 22 / 2)
          .attr('y', d => d.depth ?  0 : -15)
          .attr('stroke', '#DE4A3C')
          .attr('stroke-width', 1)
          .attr('rx', 10)
          .attr('ry', 10)
          .style('fill',d => {
            if (d.data.type === COMPANY || !d.depth) {
              return d.depth ? '#fff' : '#DE4A3C'
            } else if (d.data.type === PERSON) {
              return '#fff'
            }
          });
          // 文字
          nodeEnter.append('text')
            .attr('x', 0)
            .attr('y', 0)
            .attr('dy', `${state.current.originDiamonds.h/2 - 10}px`)
            .attr('text-anchor', 'middle')
            .attr('fill', d => d.depth ? '#DE4A3C' : '#fff')
            .text(d => d.data.name)
            .style('font-size', d => d.depth ? '16px' : '20px')
            .style('font-family', 'PingFangSC-Medium')
            .style('font-weight', '500')
      }
      // 设置图片水印
      const setWatermark = () =>{
        // 设置水印
        let user = JSON.parse(sessionStorage.getItem('user')) || { name :'' , loginName :''}
        const waterMarkText = `${user.name} ${user.loginName}`
        const canvas = document.createElement('canvas')
        canvas.width = 200
        canvas.height = 150
        const ctx = canvas.getContext('2d')
        ctx.textAlign = 'center'
        ctx.textBaseline = 'middle'
        ctx.globalAlpha = 0.09
        ctx.font = '16px sans-serif'
        ctx.translate(70,90)
        ctx.rotate(-Math.PI / 4)
        ctx.fillText(waterMarkText, 0, 0)
        return canvas
      }
      // 全屏 退出全屏
      const handleFullScreen = () =>{
        const element = document.getElementById('comChartOne');
        if(!isFullRef.current){
          setIsFull(true)
          setScaleN(1)
          if (element.requestFullScreen) { // HTML W3C 提议
            element.requestFullScreen();
          } else if (element.msRequestFullscreen) { // IE11
            element.msRequestFullScreen();
          } else if (element.webkitRequestFullScreen) { // Webkit (works in Safari5.1 and Chrome 15)
            element.webkitRequestFullScreen();
          } else if (element.mozRequestFullScreen) { // Firefox (works in nightly)
            element.mozRequestFullScreen();
          }
          state.current.svgW = document.documentElement.clientWidth
          state.current.svgH = document.documentElement.clientHeight + 300
          element.style.backgroundImage = `url(${setWatermark().toDataURL()})`
        }else {
          // 退出全屏
          setIsFull(false)
          setScaleN(1)
          if (element.requestFullScreen) {
            document.exitFullscreen();
          } else if (element.msRequestFullScreen) {
            document.msExitFullscreen();
          } else if (element.webkitRequestFullScreen) {
            document.webkitCancelFullScreen();
          } else if (element.mozRequestFullScreen) {
            document.mozCancelFullScreen();
          }
          state.current.svgW = 1600
          state.current.svgH = 500
        }
        resetSvg()
      }
      // 倍数改变
      const onScaleChange = (value) => {
        setScaleN(value)
        let gbox = document.getElementById('penetrateChart').childNodes[0].childNodes[0]
        let x = gbox.getAttribute('transform')
        const decompose = x.match(/translate((\S+),(\S+))/);
        if (Array.isArray(decompose) && decompose[2]) {
          gbox.setAttribute('transform',`translate(${parseFloat(decompose[1])},${parseFloat(decompose[2])}) scale(${value})`)
        }
      }
      // 重置画面
      const resetSvg =() =>{
        state.current.d3.select('#treesvg').remove()
        init()
      }
      const { treeData } = props
      useEffect(()=>{
        if(treeData.name){
          setTree(treeData)
        }
      },[treeData])
      useEffect(()=>{
        if(tree.name){
          init()
        }
      },[tree]) // eslint-disable-line react-hooks/exhaustive-deps
        return (
          <div  id="comChartOne" style={{backgroundColor:'white'}}>
            <Spin spinning={isLoading}>
              <Row>
                <Col className="left">
                  <Slider style={{ width: '20rem' }} min={0.3} max={2} step={0.1} defaultValue={1} onChange={onScaleChange} value={scaleN} />
                </Col>
                <Col className="right">
                  <div onClick={handleFullScreen} style={{fontSize: '16px',color: '#DE4A3C', lineHeight:'22px',cursor:'pointer'}}>
                    <img alt="" style={{width: '22px'}} src={fullScreen}/>
                    {isFull ? '退出全屏':'全屏'}
                  </div>
                </Col>
              </Row>
              <div id="penetrateChart" style={{width: '100%', display: 'block', margin:9;auto'}}>
              </div>
            </Spin>
          </div>
        );
      }
    

    股权结构图代码

    import React,{ useEffect,useRef, useState} from 'react';
    import { Col, Row, Slider, Spin,message } from 'antd';
    import * as d3Chart from 'd3';
    import fullScreen from '../../../../../../assets/fullScreen.png'
    import styles from './index.less';
    import { EncryptBase64 } from '../../../../../Common/Encrypt';
    import { FetchEquityUpperInfo } from '../../../../../../services/companysearch'
    import { formatMoney } from '../../../../../../utils/splitMoney'
    
    // 过渡时间
    const DURATION = 400
    // 加减符号半径
    const SYMBOLA_S_R = 9
    // // 公司
    // const COMPANY = '0'
    // // 人
    // const PERSON = '1'
    
    export default function RightStructureUp(props){
      let state  = useRef({
          diamonds: '',
          originDiamonds: '',
          d3: d3Chart,
          hasChildOpenNodeArr: [],
          root: '',
          svg: '',
          svgH: 500,
          svgW: 1600,
          lastClickD:null,
      })
    
      const isFullRef = useRef()
      const [isFull,setIsFull] = useState(false) 
      const [scaleN,setScaleN] = useState(1)
      const [tree,setTree] = useState({
        // 'name': '马云',
        // 'tap': '节点',
        // 'id': '1',
        // 'children': [
        //   {
        //     'name': '中国平安人寿保险股份有限公司自有资金马云的公司厉害得很',
        //     'scale': '2.27',
        //     'id': '1-1',
        //     'money': '3000',
        //     'children': [
        //       {
        //         'name': '中国证券金融股份有限公司',
        //         'scale': '2.27',
        //         'id': '1-1-1',
        //         'money': '3000',
        //         'children': [
        //           {
        //             'name': '中国证券金融股份有限公司',
        //             'scale': '2.27',
        //             'id': '1-1-1-1',
        //             'money': '3000',
        //           }
        //         ]
        //       },
        //       {
        //         'name': '中央汇金资产管理有限责任公司',
        //         'scale': '2.27',
        //         'id': '1-1-2',
        //         'money': '3000',
        //       }
        //     ]
        //   }
        // ]
        })
      const [isLoading,setLoading] = useState(false)
    
      useEffect(() => {
        isFullRef.current = isFull
      }, [isFull])
      
      // 获取文字长度
      const getStringLength = (str) => {
        let realLength = 0, len = str.length, charCode = -1;
        for (let i = 0; i < len; i++) {
          charCode = str.charCodeAt(i);
          if (charCode >= 0 && charCode < 65){
            realLength += 1;
          }else if(charCode > 90 && charCode <= 128){
            realLength += 1;
          }else if(charCode >= 65 && charCode <= 90){
            realLength += 1.2;
          }else{
            realLength += 2;
          }
        }
        return realLength / 2;
      };
    
      const diagonal = (d) =>{
        return `M ${d.source.y} ${d.source.x}
            H ${(d.source.y + (d.target.y-d.source.y)/2)}
            V ${d.target.x}
            H ${d.target.y}`;
      }
    
      // 拷贝到_children 隐藏1排以后的树
      const collapse = (source) => {
        if (!state.current.hasChildOpenNodeArr.includes(source.data.id) && source.children) {
          source._children = source.children;
          // source._children.forEach(collapse);
          source.children = null;
        }
      }
    
      // 获取上游信息
      const getUpper = async (id,regCapi) =>{
        setLoading(true)
        const dataSource = [];
        try{
          const response = await FetchEquityUpperInfo({
            instId: id,
            currentPage: 0,
            pageSize: 200,
            regCapi,
          })
          const { records = [] } = response
          records.forEach(element =>{
          dataSource.push({
              isHaveChildren:null,
              money:element.amount ? formatMoney((element.amount / 10000).toFixed(2)) :'--',
              scale:element.hold_rati || '--%',
              name:element.chn_full_nm || '--',
              id:element.inst_cust_id || '--',
              type:'0'
            })
          })
          setLoading(false)
          return dataSource
        }catch(error){
          return dataSource
        }
      }
    
    
      const click = async(d) =>{
          // if (d.children) {
          //   d._children = d.children;
          //   d.children = null;
          // } else {
          //   d.children = d._children;
          //   d._children = null;
          // }
          // if (state.current.lastClickD){
          //   state.current.lastClickD._isSelected = false;
          // }
          // d._isSelected = true;
          // state.current.lastClickD = d;
          if(d.children){
            // 点击减号
            d._children = d.children;
            d.children = null;
            state.current.hasChildOpenNodeArr = state.current.hasChildOpenNodeArr.filter(item => item !== d.data.id)
          }else {
            // 点击加号
            state.current.hasChildOpenNodeArr.push(d.data.id);
            if(!d._children){
              let res = []
              res = await getUpper(d.data.id,d.data.regCapi)
              if(!res.length){
                message.warning('上游或下游企业信息为空!')
                return
              }
              res.forEach(item =>{
                let newNode = state.current.d3.hierarchy(item)
                newNode.depth = d.depth + 1; 
                newNode.height = d.height - 1;
                newNode.parent = d; 
                if(!d.children){
                  d.children = [];
                  d.data.children = [];
                }
                d.children.push(newNode);
                d.data.children.push(newNode.data);
              })
            }else{
              d.children = d._children;
              d._children = null;
            }
          }  
          update(d)
      }
    
      /*
           *[update 函数描述], [click 函数描述]
           *  @param  {[Object]} source 第一次是初始源对象,后面是点击的对象
           *  @param  {[String]} showtype up表示向上 down表示向下
           *  @param  {[Object]} sourceTree 初始源对象
           */
      const update = (source) => {        
    
            let nodes = state.current.root.descendants()
            
            let index = -1, count = 0;
            state.current.root.eachBefore(function(n) {
              count+=20;
              n.style = 'node_' + n.depth;
              n.x = ++index * state.current.diamonds.h + count;
              n.y = n.depth * 37; // 设置下一层水平位置向后移37px
            });
    
            let node = state.current.svg.selectAll('g.node')
              .data(nodes, d => d.data.id || '');
    
            let nodeEnter = node.enter().append('g')
              .attr('class', d => 'node node_' + d.depth)
              .attr('transform', 'translate(' + source.y0 + ',' + source.x0 + ')')
              .attr('opacity', 0);
            // 创建矩形
            nodeEnter.append('rect')
              .attr('type', d => d.data.id)
              .attr('width', d => d.depth ? state.current.diamonds.w : (getStringLength(d.data.name) * 20 + 20) )
              .attr('height', d => d.depth ? state.current.diamonds.h : state.current.originDiamonds.h)
              .attr('y', -state.current.diamonds.h / 2)
              .attr('stroke', '#DE4A3C')
              .attr('stroke-width', 1)
              .attr('rx', 6)
              .attr('ry', 6)
              .style('fill',d => {
                return d.data.tap ? '#DE4A3C' : '#fff'
              }
              );
              
              nodeEnter.append('rect')
              .attr('y', -state.current.diamonds.h / 2)
              .attr('height', d => d.depth ? state.current.diamonds.h : state.current.originDiamonds.h)
              .attr('width', 6)
              .attr('rx', 6)
              .attr('ry', 6)
              .style('fill', '#DE4A3C')
    
              // 文字
              nodeEnter.append('text')
                .attr('dy', d=> d.depth ? -7 : -5)
                .attr('dx', d=> d.depth ? 36 : 10)
                .style('font-size', d=> d.depth ? '16px' : '20px')
          .style('font-weight', '500')
          .attr('fill', d =>  d.depth ? '#333333' : '#fff')
                .text(function(d) {
            // 名字长度超过进行截取
            if(d.depth){
              if(d.data.name.length>22){
                return    d.data.name.substring(0, 22) + '...'; 
              }
            }
                    return d.data.name; 
                })
          .style('cursor', 'pointer')
          .on('click', function (d) {
            if(d.data.id && d.depth){
              if(isFullRef.current){
                handleFullScreen()
              }
              // 操作点击打开新页面
            }
          });
    
            // 持股比例
                nodeEnter.append('text')
            .attr('dy', 17)
            .attr('dx', 36)
            .style('font-size', '14px')
            .style('fill', '#666666')
            .text(function(d) {
              if(!d.data.tap){
                return ('持股比例' +':')
              } 
            });
          
            nodeEnter.append('text')
            .attr('dy', 17)
            .attr('dx', 98)
            .style('font-size', '14px')
            .style('fill', '#DE4A3C')
            .text(function(d) {
              if(!d.data.tap){
                return (d.data.scale)
              } 
            });
          
            // 认缴金额
            nodeEnter.append('text')
            .attr('dy', 17)
            .attr('dx', 170)
            .style('font-size', '14px')
            .style('fill', '#666666')
            .text(function(d) {
              if(!d.data.tap){
                return ('认缴金额' + ':')
              } 
            });
          
            nodeEnter.append('text')
            .attr('dy', 17)
            .attr('dx', 240)
            .style('font-size', '14px')
            .style('fill', '#DE4A3C')
            .text(function(d) {
              if(!d.data.tap){
                if(d.data.money.length > 20){
                  return  d.data.money.substr(0, 20) + '...'
                }else{
                  return (d.data.money + '万元')
                }
              }  
            });
    
            // 创建圆 加减
            let circle = nodeEnter.append('g')
            .attr('class', 'circle')
            .on('click', click);
      
            circle.append('circle')
                    .style('fill', '#F9DDD9')
                    .style('stroke', '#FCEDEB')
                    .style('stroke-width', 1)
                    .attr('r', function (d) {
                      if(d.depth){
                        if (d.children || d.data.isHaveChildren) {
                          return 9;
                        } else {
                          return 0;
                        }  
                      }else {
                        return 0
                      }
                    })
                    .attr('cy', d => d.depth ? 0 : (-SYMBOLA_S_R -3))
                    .attr('cx', 20)
                    .style('cursor', 'pointer')
                  
            circle.append('text')
              .attr('dy', d => d.depth ? 4.5 : -7)
              .attr('dx', 20)
              .attr('text-anchor', 'middle')
              .attr('class', 'fa')
              .style('fill', '#DE4A3C')
              .text(function(d) {
                if(d.depth){
                  if (d.children) {
                    return '-';
                  } else if (d._children || d.data.isHaveChildren) {
                    return '+';
                  } else {
                    return '';
                  }
                }else {
                  return ''
                }
              })
              .style('font-size', '16px')
              .style('cursor', 'pointer');
            
            node.select('.fa')
            .text(function (d) {
              if(d.depth){
                if (d.children) {
                  return '-';
                } else if (d._children || d.data.isHaveChildren) {
                  return '+';
                } else {
                  return '';
                }
              }else {
                return ''
              }
            })
    
        /*
            * 绘制箭头
            * @param  {string} markerUnits [设置为strokeWidth箭头会随着线的粗细发生变化]
            * @param {string} viewBox 坐标系的区域
            * @param {number} markerWidth,markerHeight 标识的大小
            * @param {string} orient 绘制方向,可设定为:auto(自动确认方向)和 角度值
            * @param {number} stroke-width 箭头宽度
            * @pmarker-endaram {string} d 箭头的路径
            * @param {string} fill 箭头颜色
            */
            // nodeEnter.append('marker')
            //   .attr('id', 'resolvedIn')
            //   .attr('markerUnits', 'strokeWidth')
            //   .attr('markerUnits', 'userSpaceOnUse')
            //   .attr('viewBox', '0 -5 10 10')
            //   .attr('markerWidth', 12)
            //   .attr('markerHeight', 12)
            //   .attr('orient', '0')
            //   .attr('refX', '10')
            //   // .attr('refY', '10')
            //   .attr('stroke-width', 2)
            //   .attr('fill', '#DE4A3C')
            //   .append('path')
            //   .attr('d', 'M0,-5L10,0L0,5')
            //   .attr('fill', '#DE4A3C');
    
            // 将节点转换到它们的新位置。
            nodeEnter
              // .transition()
              // .duration(DURATION)
              .attr('transform', function(d) { return 'translate(' + d.y + ',' + d.x + ')'; })
              .style('opacity', 1);
    
            node
            // .transition()
            // .duration(DURATION)
            .attr('transform', function(d) { return 'translate(' + d.y + ',' + d.x + ')'; })
            .style('opacity', 1)
            .select('rect');
    
            // 将退出节点转换到父节点的新位置.
            let nodeExit = node.exit()
              // .transition()
              // .duration(DURATION)
              .attr('transform', () => 'translate(' + source.y + ',' + (parseInt(source.x)) + ')')
              .style('opacity', 0)
              .remove();
    
            // 修改线条
            let link = state.current.svg.selectAll('path.link')
              .data(state.current.root.links(), d => d.target.id);
    
            // 在父级前的位置画线。
            let linkEnter = link.enter().insert('path', 'g')
              .attr('class', d => 'link link_' + d.target.depth)
              // .attr('marker-end', `url(#resolvedIn)`)// 根据箭头标记的id号标记箭头
              .attr('stroke', '#DE4A3C')
              .style('fill-opacity', 1)
              .attr('fill', 'none')
              .attr('stroke-width', '1px')
              .attr('d', () => {
                let o = {x: source.x0, y: source.y0};
                return diagonal({source: o, target: o})
              })
              // .transition()
              // .duration(DURATION)
              .attr('d', diagonal);
    
            // 过渡更新位置.
            link
              // .transition()
              // .duration(DURATION)
              .attr('d', diagonal);
    
            // 将退出节点转换到父节点的新位置
            link.exit()
              // .transition()
              // .duration(DURATION)
              .attr('d', () => {
                let o = {
                  x: source.x,
                  y: source.y
                };
                return diagonal({source: o, target: o})
              }).remove();
    
            // 隐藏旧位置方面过渡.
            state.current.root.each(d => {
              d.x0 = d.x;
              d.y0 = d.y
            });
      }    
      
    
    
      const init = () =>{
        // console.log('init',tree)
        let d3 = state.current.d3
        // 强制横屏 所以取反
        let svgW = state.current.svgW
        let svgH = state.current.svgH
        let margin = {top: 20, right: 20, bottom: 30, left: 10}
        // 方块形状
        state.current.diamonds = {
          w: 410,
          h: 72,
        }
        // 源头对象
        state.current.originDiamonds = {
          w: 224,
          h: 52
        }
    
        
        // 主图
        state.current.svg = d3.select('#structureChartUp').append('svg').attr('width', svgW).attr('height', svgH).attr('id', 'treesvgUp')
          .attr('style', 'position: relative;z-index: 2') // background-image:url(${setWatermark().toDataURL()})
          // .call(d3.zoom().scaleExtent([0.3, 3]).on('zoom', () => {
          //   state.current.svg.attr('transform', d3.event.transform.translate(svgW / 2, svgH / 2));
          // }))
          .append('g').attr('id', 'gUp').attr('transform', `translate(${svgW / 3},${margin.top})`)
    
          // 可以被拖动的功能
          var obox = document.getElementById('structureChartUp').childNodes[0];
          var gbox = document.getElementById('structureChartUp').childNodes[0].childNodes[0];
          obox.addEventListener('mousedown', function (evt) {
            // 点击时候停止
            document.onclick = function () {
              document.onmousemove = null;
              document.onmouseup = null;
            };
            var oEvent = evt // 获取事件对象,这个是兼容写法
            var disX = oEvent.clientX;
            var disY = oEvent.clientY;
            // let arr = gbox.getAttribute('transform')
            //   .replace('translate(', '')
            //   .replace(')', '')
            //   .split(',');
            let x = gbox.getAttribute('transform')
            const decompose = x.match(/translate((\S+),(\S+))/)
            const scale = x.match(/scale((\S+))/)
              
    
            // 这里就解释为什么要给document添加onmousemove时间,原因是如果你给obox添加这个事件的时候,当你拖动很快的时候就很快脱离这个onmousemove事件,而不能实时拖动它
            document.onmousemove = function (evt) {
              // 实时改变目标元素obox的位置
                var oEvent = evt
                if (Array.isArray(decompose) && Array.isArray(scale) && decompose[2] && scale[1]) {
                  gbox.setAttribute(
                    'transform',
                    `translate(${oEvent.clientX - disX + parseFloat(decompose[1])},${oEvent.clientY - disY + parseFloat(decompose[2])}) scale(${parseFloat(scale[1])})`
                  );
                }else{
                  gbox.setAttribute(
                    'transform',
                    `translate(${oEvent.clientX - disX + parseFloat(decompose[1])},${oEvent.clientY - disY + parseFloat(decompose[2])})`
                  );
                }
                // 停止拖动
                document.onmouseup = function () {
                  document.onmousemove = null;
                  document.onmouseup = null;
                };
              }
            })
    
            // 拷贝树的数据
            let downTree = null
            Object.keys(tree).map(item => {
              if (item === 'children') {
                downTree = JSON.parse(JSON.stringify(tree))
                downTree.children = tree[item]
              }
            })
              // hierarchy 返回新的结构 x0,y0初始化起点坐标
              state.current.root = d3.hierarchy(downTree)
              state.current.root.x0 = 0
              state.current.root.y0 = 0
              if(!state.current.root.children){
                // console.log(tree['children'].length,state.current.root.children)
                update(state.current.root)
              }else {
                state.current.root.children.forEach(collapse);
                update(state.current.root)
              }
      }
    
      // 设置图片水印
      const setWatermark = () =>{
        // 设置水印
        let user = JSON.parse(sessionStorage.getItem('user')) || { name :'' , loginName :''}
        const waterMarkText = `${user.name} ${user.loginName}`
        const canvas = document.createElement('canvas')
        canvas.width = 200
        canvas.height = 150
        const ctx = canvas.getContext('2d')
        ctx.textAlign = 'center'
        ctx.textBaseline = 'middle'
        ctx.globalAlpha = 0.09
        ctx.font = '16px sans-serif'
        ctx.translate(70,90)
        ctx.rotate(-Math.PI / 4)
        ctx.fillText(waterMarkText, 0, 0)
    
        return canvas
      }
    
    
      // 全屏 退出全屏
      const handleFullScreen = () =>{
        const element = document.getElementById('comChartUp');
        if(!isFullRef.current){
          if (element.requestFullScreen) { // HTML W3C 提议
            element.requestFullScreen();
          } else if (element.msRequestFullscreen) { // IE11
            element.msRequestFullScreen();
          } else if (element.webkitRequestFullScreen) { // Webkit (works in Safari5.1 and Chrome 15)
            element.webkitRequestFullScreen();
          } else if (element.mozRequestFullScreen) { // Firefox (works in nightly)
            element.mozRequestFullScreen();
          }
          state.current.svgW = document.documentElement.clientWidth
          state.current.svgH = document.documentElement.clientHeight + 300
          element.style.backgroundImage = `url(${setWatermark().toDataURL()})`
          setIsFull(true)
          setScaleN(1)
        }else {
          // 退出全屏
          if (element.requestFullScreen) {
            document.exitFullscreen();
          } else if (element.msRequestFullScreen) {
            document.msExitFullscreen();
          } else if (element.webkitRequestFullScreen) {
            document.webkitCancelFullScreen();
          } else if (element.mozRequestFullScreen) {
            document.mozCancelFullScreen();
          }
          state.current.svgW = 1600
          state.current.svgH = 500
          setIsFull(false)
          setScaleN(1)
        }
        resetSvg()
      }
    
    
    
      // 重置画面
      const resetSvg =() =>{
        state.current.d3.select('#treesvgUp').remove()
        init()
      }
    
      // 倍数改变
      const onScaleChange = (value) => {
        setScaleN(value)
        let gbox = document.getElementById('structureChartUp').childNodes[0].childNodes[0]
        let x = gbox.getAttribute('transform')
        const decompose = x.match(/translate((\S+),(\S+))/);
        if (Array.isArray(decompose) && decompose[2]) {
          gbox.setAttribute('transform',`translate(${parseFloat(decompose[1])},${parseFloat(decompose[2])}) scale(${value})`)
        }
      }
    
      const { treeData } = props
      useEffect(()=>{
        if(treeData.name){
          // console.log(treeData,'treeData')
          let temp = {...treeData}
          temp.children = temp.parents
          temp.parents = null
          setTree(temp)
        }
      },[treeData])
    
    
      useEffect(()=>{
        if(tree.name){
          init()
        }
      },[tree]) // eslint-disable-line react-hooks/exhaustive-deps
    
    
        return (
          <div  id="comChartUp" style={{backgroundColor:'white'}}>
            <Spin spinning={isLoading}>
              <Row style={{height:'35px'}}>
                <Col className="left">
                  <Slider style={{ width: '20rem' }} min={0.3} max={2} step={0.1} defaultValue={1} onChange={onScaleChange} value={scaleN} />
                </Col>
                <Col className="right">
                  <div onClick={handleFullScreen} style={{fontSize: '16px',color: '#DE4A3C', lineHeight:'22px',cursor:'pointer'}}>
                    <img alt="" style={{width: '22px'}} src={fullScreen}/>
                    {isFull ? '退出全屏':'全屏'}
                  </div>
                </Col>
              </Row>
              <div id="structureChartUp" style={{width: '100%', display: 'block', margin:'auto'}}>
              </div>
            </Spin>
          </div>
        );
      }

    总结:

    前端小白一枚,在之前只使用过echarts进行可视化,在开发这个功能时候发现d3版本中文网站内容较少,基本出现问题讨论也是在外文网站,踩过一堆版本的坑,最终选择稳定且例子比较多的v4版本。 并且查找时发现大部分例子基本都是默认信息展示,很少有点击请求子节点展示的功能,所以最终进行一个最终功能的整合

    以上就是react hooks d3实现企查查股权穿透图结构图效果详解的详细内容,更多关于react hooks d3股权穿透图结构图的资料请关注其它相关文章!

    声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。