目录
- 前言
- 最终效果:
- 版本信息:
- 股权穿透图基础功能:
- 股权结构图基础功能:
- 股权穿透图代码
- 股权结构图代码
- 总结:
前言
umi+antd-admin 框架中使用hooks结合d3完成类似股权穿透图和股权结构图(web)
最终效果:
股权穿透图

股权结构图

版本信息:
"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股权穿透图结构图的资料请关注其它相关文章!
声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。

评论(0)