react?hooks?d3實(shí)現(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、點(diǎn)擊請(qǐng)求子節(jié)點(diǎn)信息展示,收起子節(jié)點(diǎn)
3、全屏功能
4、放大器放大縮?。╮eact項(xiàng)目中不知道為啥使用d3.zoom方法不好使,可能跟網(wǎng)頁中滾動(dòng)事件沖突有關(guān),最后選擇單獨(dú)放置放大器進(jìn)行放大縮小功能)
5、移動(dòng)功能
股權(quán)結(jié)構(gòu)圖基礎(chǔ)功能:
1、tab切換展示上游或下游信息
2、默認(rèn)展示一層
3、點(diǎn)擊請(qǐng)求子節(jié)點(diǎn)信息展示,收起子節(jié)點(diǎn)
代碼鏈接: 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'
// 過渡時(shí)間
const DURATION = 0
// 加減符號(hào)半徑
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ā)展公司父級(jí)公司1', 'scale': 30, 'type': '0', 'oldUrlName': ''},
// {'controlPerson': true, 'money': '3000', 'children': [], 'parentMoney': 3000, 'old': true, 'id': '2-01-1', 'name': '發(fā)展公司父級(jí)公司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é)點(diǎn) 使全屏前后展開的節(jié)點(diǎn)是一樣的
const collapse = (source) => {
if (!state.current.hasChildOpenNodeArr.includes(source.data.id) && source.children) {
source._children = source.children;
// source._children.forEach(collapse);
source.children = null;
}
}
// 請(qǐng)求獲取下游信息
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
}
}
// 請(qǐng)求獲取上游信息
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
}
}
// 圓圈點(diǎn)擊事件
const click = async (source, showType,nodes) =>{
// 數(shù)據(jù)全部請(qǐng)求回來的情況
// if (source.depth) {
// if(source.children){
// // 點(diǎn)擊減號(hào)
// source._children = source.children;
// source.children = null;
// }else {
// // 點(diǎn)擊加號(hào)
// 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))})`
// );
// }
// }
// }
// 點(diǎn)擊加號(hào)時(shí)才去請(qǐng)求節(jié)點(diǎn)信息的情況
if(source.children){
// 點(diǎn)擊減號(hào)
source._children = source.children;
source.children = null;
state.current.hasChildOpenNodeArr = state.current.hasChildOpenNodeArr.filter(item => item !== source.data.id)
}else {
// 點(diǎn)擊加號(hào)
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;
}
// 點(diǎn)擊后將節(jié)點(diǎn)移動(dòng)到中間位置
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 第一次是初始源對(duì)象,后面是點(diǎn)擊的對(duì)象
* @param {[String]} showtype up表示向上 down表示向下
* @param {[Object]} sourceTree 初始源對(duì)象
*/
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); // 擁有下部分則隱藏初始?jí)K 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個(gè)
// 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()
}
// 點(diǎn)擊操作
}
});
// 名稱過長 第二段
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個(gè)開始截取有表示超出
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箭頭會(huì)隨著線的粗細(xì)發(fā)生變化]
* @param {string} viewBox 坐標(biāo)系的區(qū)域
* @param {number} markerWidth,markerHeight 標(biāo)識(shí)的大小
* @param {string} orient 繪制方向,可設(shè)定為:auto(自動(dòng)確認(rèn)方向)和 角度值
* @param {number} stroke-width 箭頭寬度
* @param {string} d 箭頭的路徑
* @param {string} fill 箭頭顏色
* @param {string} id resolved0表示公司 resolved1表示個(gè)人
* 直接用一個(gè)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é)點(diǎn)轉(zhuǎn)換到它們的新位置。
let nodeUpdate = node
// .transition()
// .duration(DURATION)
.attr('transform', d => showtype === 'up' ? 'translate(' + d.x + ',' + -(d.y) + ')' : 'translate(' + d.x + ',' + (d.y) + ')');
// 將退出節(jié)點(diǎn)轉(zhuǎn)換到父節(jié)點(diǎn)的新位置.
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);
// 在父級(jí)前的位置畫線。
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號(hào)標(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é)點(diǎn)轉(zhuǎn)換到父節(jié)點(diǎn)的新位置
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
}
// 源頭對(duì)象
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) + ')')
// 可以被拖動(dòng)的功能
var obox = document.getElementById('penetrateChart').childNodes[0];
var gbox = document.getElementById('penetrateChart').childNodes[0].childNodes[0];
obox.addEventListener('mousedown', function (evt) {
// 點(diǎn)擊時(shí)候停止
document.onclick = function () {
document.onmousemove = null;
document.onmouseup = null;
};
var oEvent = evt // 獲取事件對(duì)象,這個(gè)是兼容寫法
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時(shí)間,原因是如果你給obox添加這個(gè)事件的時(shí)候,當(dāng)你拖動(dòng)很快的時(shí)候就很快脫離這個(gè)onmousemove事件,而不能實(shí)時(shí)拖動(dòng)它
document.onmousemove = function (evt) {
// 實(shí)時(shí)改變目標(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])})`
);
}
// 停止拖動(dòng)
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初始化起點(diǎn)坐標(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); // 擁有下部分則隱藏初始?jí)K 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'
// 過渡時(shí)間
const DURATION = 400
// 加減符號(hào)半徑
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é)點(diǎn)',
// 'id': '1',
// 'children': [
// {
// 'name': '中國平安人壽保險(xiǎn)股份有限公司自有資金馬云的公司厲害得很',
// '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){
// 點(diǎn)擊減號(hào)
d._children = d.children;
d.children = null;
state.current.hasChildOpenNodeArr = state.current.hasChildOpenNodeArr.filter(item => item !== d.data.id)
}else {
// 點(diǎn)擊加號(hào)
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 第一次是初始源對(duì)象,后面是點(diǎn)擊的對(duì)象
* @param {[String]} showtype up表示向上 down表示向下
* @param {[Object]} sourceTree 初始源對(duì)象
*/
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) {
// 名字長度超過進(jìn)行截取
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()
}
// 操作點(diǎn)擊打開新頁面
}
});
// 持股比例
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箭頭會(huì)隨著線的粗細(xì)發(fā)生變化]
* @param {string} viewBox 坐標(biāo)系的區(qū)域
* @param {number} markerWidth,markerHeight 標(biāo)識(shí)的大小
* @param {string} orient 繪制方向,可設(shè)定為:auto(自動(dòng)確認(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é)點(diǎn)轉(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é)點(diǎn)轉(zhuǎn)換到父節(jié)點(diǎn)的新位置.
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);
// 在父級(jí)前的位置畫線。
let linkEnter = link.enter().insert('path', 'g')
.attr('class', d => 'link link_' + d.target.depth)
// .attr('marker-end', `url(#resolvedIn)`)// 根據(jù)箭頭標(biāo)記的id號(hào)標(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é)點(diǎn)轉(zhuǎn)換到父節(jié)點(diǎn)的新位置
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
// 強(qiáng)制橫屏 所以取反
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,
}
// 源頭對(duì)象
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})`)
// 可以被拖動(dòng)的功能
var obox = document.getElementById('structureChartUp').childNodes[0];
var gbox = document.getElementById('structureChartUp').childNodes[0].childNodes[0];
obox.addEventListener('mousedown', function (evt) {
// 點(diǎn)擊時(shí)候停止
document.onclick = function () {
document.onmousemove = null;
document.onmouseup = null;
};
var oEvent = evt // 獲取事件對(duì)象,這個(gè)是兼容寫法
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時(shí)間,原因是如果你給obox添加這個(gè)事件的時(shí)候,當(dāng)你拖動(dòng)很快的時(shí)候就很快脫離這個(gè)onmousemove事件,而不能實(shí)時(shí)拖動(dòng)它
document.onmousemove = function (evt) {
// 實(shí)時(shí)改變目標(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])})`
);
}
// 停止拖動(dòng)
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初始化起點(diǎn)坐標(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進(jìn)行可視化,在開發(fā)這個(gè)功能時(shí)候發(fā)現(xiàn)d3版本中文網(wǎng)站內(nèi)容較少,基本出現(xiàn)問題討論也是在外文網(wǎng)站,踩過一堆版本的坑,最終選擇穩(wěn)定且例子比較多的v4版本。 并且查找時(shí)發(fā)現(xiàn)大部分例子基本都是默認(rèn)信息展示,很少有點(diǎn)擊請(qǐng)求子節(jié)點(diǎn)展示的功能,所以最終進(jìn)行一個(gè)最終功能的整合
以上就是react hooks d3實(shí)現(xiàn)企查查股權(quán)穿透圖結(jié)構(gòu)圖效果詳解的詳細(xì)內(nèi)容,更多關(guān)于react hooks d3股權(quán)穿透圖結(jié)構(gòu)圖的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
React使用react-sortable-hoc如何實(shí)現(xiàn)拖拽效果
這篇文章主要介紹了React使用react-sortable-hoc如何實(shí)現(xiàn)拖拽效果問題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2024-07-07
在React頁面重新加載時(shí)保留數(shù)據(jù)的實(shí)現(xiàn)方法總結(jié)
在React頁面重新加載時(shí)保留數(shù)據(jù),可以通過多種方法來實(shí)現(xiàn),常見的方法包括使用瀏覽器的本地存儲(chǔ)(Local Storage 或 Session Storage)、URL參數(shù)、以及服務(wù)器端存儲(chǔ)等,本文給大家總結(jié)了一些具體實(shí)現(xiàn)方法,需要的朋友可以參考下2024-06-06
React使用Echarts/Ant-design-charts的案例代碼
這篇文章主要介紹了React使用Echarts/Ant-design-charts的實(shí)例代碼,本文通過實(shí)例代碼給大家講解的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2022-11-11
基于Node的React圖片上傳組件實(shí)現(xiàn)實(shí)例代碼
本篇文章主要介紹了基于Node的React圖片上傳組件實(shí)現(xiàn)實(shí)例代碼,非常具有實(shí)用價(jià)值,需要的朋友可以參考下2017-05-05
React之錯(cuò)誤邊界 Error Boundaries示例詳解
這篇文章主要為大家介紹了React之錯(cuò)誤邊界Error Boundaries示例教程,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-10-10
學(xué)習(xí)React中ref的兩個(gè)demo示例
這篇文章主要介紹了學(xué)習(xí)React中ref的兩個(gè)demo示例,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2018-08-08
使用react-activation實(shí)現(xiàn)keepAlive支持返回傳參
本文主要介紹了使用react-activation實(shí)現(xiàn)keepAlive支持返回傳參,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2022-05-05
react中ref獲取dom或者組件的實(shí)現(xiàn)方法
這篇文章主要介紹了react中ref獲取dom或者組件的實(shí)現(xiàn)方法,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-05-05

