react?hooks?d3實現(xiàn)企查查股權(quán)穿透圖結(jié)構(gòu)圖效果詳解
前言
umi+antd-admin 框架中使用hooks結(jié)合d3完成類似股權(quán)穿透圖和股權(quán)結(jié)構(gòu)圖(web)
最終效果:
股權(quán)穿透圖
股權(quán)結(jié)構(gòu)圖
版本信息:
"d3": "4.13.0",
"antd": "3.24.2",
"umi": "^2.7.7",
股權(quán)穿透圖基礎(chǔ)功能:
1、默認(rèn)上下游信息展示,如果沒有上下游信息只展示自己
2、點擊請求子節(jié)點信息展示,收起子節(jié)點
3、全屏功能
4、放大器放大縮?。╮eact項目中不知道為啥使用d3.zoom方法不好使,可能跟網(wǎng)頁中滾動事件沖突有關(guān),最后選擇單獨放置放大器進行放大縮小功能)
5、移動功能
股權(quán)結(jié)構(gòu)圖基礎(chǔ)功能:
1、tab切換展示上游或下游信息
2、默認(rèn)展示一層
3、點擊請求子節(jié)點信息展示,收起子節(jié)點
代碼鏈接: github.com/QiuDaShua/r…
股權(quán)穿透圖代碼
俺認(rèn)為的關(guān)鍵都寫在注釋中了
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': '大公司畢節(jié)分公司', // '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': '發(fā)展公司父級公司1', 'scale': 30, 'type': '0', 'oldUrlName': ''}, // {'controlPerson': true, 'money': '3000', 'children': [], 'parentMoney': 3000, 'old': true, 'id': '2-01-1', 'name': '發(fā)展公司父級公司2', 'scale': 70, 'type': '0', 'oldUrlName': ''}, // ], // 'name': '發(fā)展公司', // '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排以后的樹 通過數(shù)組記錄下已經(jīng)展開的節(jié)點 使全屏前后展開的節(jié)點是一樣的 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) =>{ // 數(shù)據(jù)全部請求回來的情況 // 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))})` // ); // } // } // } // 點擊加號時才去請求節(jié)點信息的情況 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('上游或下游企業(yè)信息為空!') 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; } // 點擊后將節(jié)點移動到中間位置 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 函數(shù)描述], [click 函數(shù)描述] * @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 // 創(chuàng)建矩形 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' } } ); // 創(chuàng)建圓 加減 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'); // 認(rèn)繳金額 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 ? `認(rèn)繳金額:${d.data.money.substr(0,20)}…` : `認(rèn)繳金額:${d.data.money}萬元` : '') .style('font-size', '14px') .style('font-family', 'PingFangSC-Regular') .style('font-weight', '400') .style('color', '#3D3D3D'); /* * 繪制箭頭 * @param {string} markerUnits [設(shè)置為strokeWidth箭頭會隨著線的粗細(xì)發(fā)生變化] * @param {string} viewBox 坐標(biāo)系的區(qū)域 * @param {number} markerWidth,markerHeight 標(biāo)識的大小 * @param {string} orient 繪制方向,可設(shè)定為:auto(自動確認(rèn)方向)和 角度值 * @param {number} stroke-width 箭頭寬度 * @param {string} d 箭頭的路徑 * @param {string} fill 箭頭顏色 * @param {string} id resolved0表示公司 resolved1表示個人 * 直接用一個marker達(dá)不到兩種顏色都展示的效果 */ 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'); // 將節(jié)點轉(zhuǎn)換到它們的新位置。 let nodeUpdate = node // .transition() // .duration(DURATION) .attr('transform', d => showtype === 'up' ? 'translate(' + d.x + ',' + -(d.y) + ')' : 'translate(' + d.x + ',' + (d.y) + ')'); // 將退出節(jié)點轉(zhuǎn)換到父節(jié)點的新位置. 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})`)// 根據(jù)箭頭標(biāo)記的id號標(biāo)記箭頭 .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)); // 將退出節(jié)點轉(zhuǎn)換到父節(jié)點的新位置 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添加這個事件的時候,當(dāng)你拖動很快的時候就很快脫離這個onmousemove事件,而不能實時拖動它 document.onmousemove = function (evt) { // 實時改變目標(biāo)元素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; }; } }) // 拷貝樹的數(shù)據(jù) 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 返回新的結(jié)構(gòu) x0,y0初始化起點坐標(biāo) 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; // 上 和 下 結(jié)構(gòu) 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 // 創(chuàng)建矩形 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') } // 設(shè)置圖片水印 const setWatermark = () =>{ // 設(shè)置水印 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() } // 倍數(shù)改變 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> ); }
股權(quán)結(jié)構(gòu)圖代碼
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': '節(jié)點', // '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': '中央?yún)R金資產(chǎn)管理有限責(zé)任公司', // '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('上游或下游企業(yè)信息為空!') 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 函數(shù)描述], [click 函數(shù)描述] * @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; // 設(shè)置下一層水平位置向后移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); // 創(chuàng)建矩形 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) } }); // 認(rèn)繳金額 nodeEnter.append('text') .attr('dy', 17) .attr('dx', 170) .style('font-size', '14px') .style('fill', '#666666') .text(function(d) { if(!d.data.tap){ return ('認(rèn)繳金額' + ':') } }); 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 + '萬元') } } }); // 創(chuàng)建圓 加減 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 [設(shè)置為strokeWidth箭頭會隨著線的粗細(xì)發(fā)生變化] * @param {string} viewBox 坐標(biāo)系的區(qū)域 * @param {number} markerWidth,markerHeight 標(biāo)識的大小 * @param {string} orient 繪制方向,可設(shè)定為:auto(自動確認(rèn)方向)和 角度值 * @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'); // 將節(jié)點轉(zhuǎn)換到它們的新位置。 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'); // 將退出節(jié)點轉(zhuǎn)換到父節(jié)點的新位置. 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)`)// 根據(jù)箭頭標(biāo)記的id號標(biāo)記箭頭 .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); // 將退出節(jié)點轉(zhuǎn)換到父節(jié)點的新位置 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添加這個事件的時候,當(dāng)你拖動很快的時候就很快脫離這個onmousemove事件,而不能實時拖動它 document.onmousemove = function (evt) { // 實時改變目標(biāo)元素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; }; } }) // 拷貝樹的數(shù)據(jù) let downTree = null Object.keys(tree).map(item => { if (item === 'children') { downTree = JSON.parse(JSON.stringify(tree)) downTree.children = tree[item] } }) // hierarchy 返回新的結(jié)構(gòu) x0,y0初始化起點坐標(biāo) 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) } } // 設(shè)置圖片水印 const setWatermark = () =>{ // 設(shè)置水印 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() } // 倍數(shù)改變 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> ); }
總結(jié):
前端小白一枚,在之前只使用過echarts進行可視化,在開發(fā)這個功能時候發(fā)現(xiàn)d3版本中文網(wǎng)站內(nèi)容較少,基本出現(xiàn)問題討論也是在外文網(wǎng)站,踩過一堆版本的坑,最終選擇穩(wěn)定且例子比較多的v4版本。 并且查找時發(fā)現(xiàn)大部分例子基本都是默認(rèn)信息展示,很少有點擊請求子節(jié)點展示的功能,所以最終進行一個最終功能的整合
以上就是react hooks d3實現(xiàn)企查查股權(quán)穿透圖結(jié)構(gòu)圖效果詳解的詳細(xì)內(nèi)容,更多關(guān)于react hooks d3股權(quán)穿透圖結(jié)構(gòu)圖的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
React使用react-sortable-hoc如何實現(xiàn)拖拽效果
這篇文章主要介紹了React使用react-sortable-hoc如何實現(xiàn)拖拽效果問題,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2024-07-07在React頁面重新加載時保留數(shù)據(jù)的實現(xiàn)方法總結(jié)
在React頁面重新加載時保留數(shù)據(jù),可以通過多種方法來實現(xiàn),常見的方法包括使用瀏覽器的本地存儲(Local Storage 或 Session Storage)、URL參數(shù)、以及服務(wù)器端存儲等,本文給大家總結(jié)了一些具體實現(xiàn)方法,需要的朋友可以參考下2024-06-06React使用Echarts/Ant-design-charts的案例代碼
這篇文章主要介紹了React使用Echarts/Ant-design-charts的實例代碼,本文通過實例代碼給大家講解的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2022-11-11基于Node的React圖片上傳組件實現(xiàn)實例代碼
本篇文章主要介紹了基于Node的React圖片上傳組件實現(xiàn)實例代碼,非常具有實用價值,需要的朋友可以參考下2017-05-05React之錯誤邊界 Error Boundaries示例詳解
這篇文章主要為大家介紹了React之錯誤邊界Error Boundaries示例教程,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2022-10-10使用react-activation實現(xiàn)keepAlive支持返回傳參
本文主要介紹了使用react-activation實現(xiàn)keepAlive支持返回傳參,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2022-05-05