【vue d3 v4】vue2结合d3实现类似企查查的股权穿透图、股权结构图
前言
vue3 框架中使用vue2代码结合d3完成股权穿透图和股权结构图(h5)
(没错听上去很违规,但我懒得把代码从vue2改成vue3了,所以是在vue3框架里用vue2写法完成的)
最终效果:
版本信息:
"d3": "4.13.0",
"vant": "^3.1.5",
"vue": "^3.0.0",
股权穿透图基础功能:
1、默认上下游信息展示,如果没有上下游信息只展示自己
2、点击请求子节点信息展示,收起子节点
3、全屏功能
4、放大器放大缩小(react项目中不知道为啥使用d3.zoom方法不好使,可能跟网页中滚动事件冲突有关,最后选择单独防止放大器进行放大缩小功能)
5、移动功能
股权结构图基础功能:
1、tab切换展示上游或下游信息
2、默认展示一层
3、点击请求子节点信息展示,收起子节点
代码链接:
https://github.com/QiuDaShua/vue2-d3.git
股权穿透图代码:
<template><div class="father-box"><divid="rightPenetrationpage" :style="{ 'transition': 'transform .5s ease', '-ms-transition': 'transform .5s ease', '-moz-transition': 'transform .5s ease','-webkit-transition': 'transform .5s ease','-o-transition': 'transform .5s ease'}"><custom-nav-bar:title="title"left-arrow@on-clickleft="onClickLeft"></custom-nav-bar><!-- <divclass="full"@click.stop="showFullScreen"><div class="full-icon"></div><span>{{isFull ? '退出全屏' :'全屏'}}</span></div> --><divid="penetrateChart":style="{width:'100%',display:'block',margin:'auto'}"></div></div></div></template><script lang="ts">import { defineComponent} from 'vue'import { useStore } from 'vuex'import CustomNavBar from '@/components/common/CustomNavbar.vue'import { fetchCompanySearchDetail, fetchEquityUpperInfo, fetchEquityBelowInfo } from '@/api/companySearch'import { Notify, Toast } from 'vant'import { formatMoney, getBLen } from '@/utils/tool'import { sm2Decrypted } from '@/enrich/crypto-gm'import { GlobalMutation } from '@/store/types/mutation-types'import * as $d3 from 'd3'// 过渡时间const DURATION = 0// 加减符号半径const SYMBOLA_S_R = 9// 公司const COMPANY = '0'// 人const PERSON = '1'//x,y距离// let x0 = 0, y0 = 0, dx = 0, dy = 0export default defineComponent({props: {},components: {CustomNavBar},data () {return {layoutTree: {} as any,diamonds: {} as any,d3: $d3,// hasChildNodeArr: [],originDiamonds: {} as any,diagonalUp: '',diagonalDown: '',tree: {'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': ''}]},rootUp: {} as any,rootDown: {} as any,svg: {} as any,svgW: document.documentElement.clientWidth,svgH: document.documentElement.clientHeight - 44,title: '股权穿透图',isFull: false,name: '',id: '',token: '',regCapi: '',userid: '',parents: [] as any[], // 下游信息children: [] as any[], // 上游信息}},// beforeCreate () {// document.body.style.overflow = 'hidden'// },// beforeDestroy () {// document.body.style.overflow = 'auto'// },// created () {// // window.addEventListener('orientationchange', this.changeOrient)// },mounted () {const store = useStore()const data = this.$route.query.data ? JSON.parse(sm2Decrypted(this.$route.query.data)) : {}const id = data.idconst token = data.token ? data.token : store.state.global.tokenconst userid = data.useridthis.id = idthis.token = tokenthis.userid = useridstore.commit(`global/${GlobalMutation.SET_TOKEN}`, token)Toast.loading({message: '加载中',forbidClick: true,duration: 0,});this.getInit()},beforeUnmount() {this.d3.select('#treesvg').remove()console.log('页面关闭')},methods: {// changeOrient () {// const box = document.getElementById('penetrateChart').children[0]// const g = document.getElementById('penetrateChart').children[0].children[0]// let navbar = document.querySelector('.navbar')// let flag = false// flag = isOrient()// setTimeout(()=>{// if(flag){// navbar?.classList.add('smallBar')// }else{// navbar?.classList.remove('smallBar')// }// console.log(document.documentElement.clientWidth, document.documentElement.clientHeight)// box.setAttribute('width', document.documentElement.clientWidth)// box.setAttribute('height', document.documentElement.clientHeight)// g.setAttribute('transform', 'translate(' + (document.documentElement.clientWidth / 2) + ',' + (document.documentElement.clientHeight / 2) + ')')// }, 100)// },async getDetailInfo(){await fetchCompanySearchDetail({token: this.token,instId: this.id,userId: this.userid}).then((response)=>{const {code =0, records = [] } = responseif (code > 0 && records != null) {this.regCapi = records[0].reg_capithis.name = records[0].chn_full_nm}})},async getUpper(){await fetchEquityUpperInfo({token: this.token,instId: this.id,regCapi: this.regCapi,currentPage: 0,pageSize: 200,}).then((response) => {const {code =0, records = [] } = responseif (code > 0 && records != null) {const dataSource = [] as any[];records.forEach(element =>{// let children = []// // 设置children节点// if(element.list){// element.list.forEach(child =>{// children.push({// money: child.amount ? formatMoney((child.amount / 10000).toFixed(2)) :'--',// scale: child.hold_rati || '--%',// name: child.chn_full_nm || '--',// id: child.inst_cust_id || '--',// type: '0'// })// })// }dataSource.push({// children: children,isHaveChildren: element.dataType === '1' ? true : false,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',regCapi: element.reg_capi})})this.parents = dataSource}})},async getBelow(){await fetchEquityBelowInfo({token: this.token,instId: this.id,currentPage: 0,pageSize: 200,}).then((response) => {const {code =0, records = []} = responseif (code > 0 && records != null) {const dataSource = [] as any[];records.forEach(element =>{// let children = []// // 设置children节点// if(element.list){// element.list.forEach(child =>{// children.push({// money: child.amount ? formatMoney((child.amount / 10000).toFixed(2)) :'--',// scale: child.hold_rati || '--%',// name: child.chn_full_nm || '--',// id: child.inst_cust_id || '--',// type: '0'// })// })// }dataSource.push({// children: children,isHaveChildren: element.dataType === '1' ? true : false,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',})})this.children = dataSource}})},// 获取树状数据getTreeData(){console.log( this.children, this.parents, '111111111')let obj = {id: this.id,name: this.name,tap: '节点',children: this.children,parents: this.parents,}this.tree = {...obj}Toast.clear()},async getInit(){await this.getDetailInfo()// await this.getUpper()// await this.getBelow()Promise.all([this.getUpper(), this.getBelow()]).finally(()=>{this.getTreeData()this.init()})},init () {let d3 = this.d3let svgW = this.svgWlet svgH = this.svgH// x0 = svgW / 2,// y0= svgH / 2// 方块形状this.diamonds = {w: 162,h: 66,intervalW: 182,intervalH: 150}// 源头对象this.originDiamonds = {w: 208,h: 41}this.layoutTree = d3.tree().nodeSize([this.diamonds.intervalW, this.diamonds.intervalH]).separation(() => 1);// 主图this.svg = d3.select('#penetrateChart').append('svg').attr('width', svgW).attr('height', svgH).attr('id', 'treesvg').call(d3.zoom().scaleExtent([0.3, 2]).on('zoom', () => {// 设置缩放位置以及平移初始位// if(isiOS && this.isFull){// // 修改ios手机上才有的移动bug,安卓手机,pc端没有// let x = d3.event.transform.x// d3.event.transform.x = d3.event.transform.y// d3.event.transform.y = -x// console.log('222', '出现移动bug', d3.event.transform)// console.log(isiOS, d3.event.transform.x, d3.event.transform.y)// // dx = d3.event.transform.x - x0// // dy = d3.event.transform.y - y0// // x0 = d3.event.transform.x// // y0 = d3.event.transform.y// // d3.event.transform.x = d3.event.transform.x + dy// // d3.event.transform.y = d3.event.transform.y + dx// // this.svg.attr('transform', 'translate(' + (svgW / 2) + ',' + (svgH / 2) + ') rotate(90)')// }this.svg.attr('transform', d3.event.transform.translate(svgW / 2, svgH / 2));})).on('dblclick.zoom', null).attr('style', 'position: relative;z-index: 2') //background-image:url(${setWatermark({name: this.$store.state.global.user.userName, loginName: this.$store.state.global.user.userId}).toDataURL()}).append('g').attr('id', 'g').attr('transform', `translate(${svgW / 2},${svgH / 2})`) let upTree = {} as anylet downTree = {} as any// 拷贝树的数据Object.keys(this.tree).map(item => {if (item === 'parents') {upTree = JSON.parse(JSON.stringify(this.tree))upTree.children = this.tree[item]upTree.parents = null} else if (item === 'children') {downTree = JSON.parse(JSON.stringify(this.tree))downTree.children = this.tree[item]downTree.parents = null}})// hierarchy 返回新的结构 x0,y0初始化起点坐标this.rootUp = d3.hierarchy(upTree, d => d.children);this.rootUp.x0 = 0this.rootUp.y0 = 0this.rootDown = d3.hierarchy(downTree, d => d.children);this.rootDown.x0 = 0this.rootDown.y0 = 0;// 上 和 下 结构let treeArr = [{data: this.rootUp,type: 'up'},{data: this.rootDown,type: 'down'}]if(!this.tree['children'].length && !this.tree['parents'].length){this.updataSelf()}else{treeArr.map(item => {if (item.data.children) {// item.data.children.forEach(this.collapse);this.update(item.data, item.type, item.data)}})}},updataSelf(){let nodes = this.rootUp.descendants()let node = this.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 ? (this.rootDown.data.children. length ? 0 : 1) : 1// 创建矩形nodeEnter.append('rect').attr('type', d => d.data.id + '_' + d.depth).attr('width', d => d.depth ? this.diamonds.w : (getBLen(d.data.name)/2 * 20 + 20)).attr('height', d => d.depth ? (d.data.type === COMPANY ? this.diamonds.h : this.diamonds.h - 10) : this.originDiamonds.h).attr('x', d => d.depth ? -this.diamonds.w / 2 : -(getBLen(d.data.name)/2 * 20 + 20) / 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', `${this.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')},/**[update 函数描述], [click 函数描述]* @param {[Object]} source 第一次是初始源对象,后面是点击的对象* @param {[String]} showtype up表示向上 down表示向下* @param {[Object]} sourceTree 初始源对象*/update (source, showtype, sourceTree) {// eslint-disable-next-linelet _this = thisif (source.parents === null) {source.isOpen = !source.isOpen}let nodesif (showtype === 'up') {nodes = this.layoutTree(this.rootUp).descendants()} else {nodes = this.layoutTree(this.rootDown).descendants()}let links = nodes.slice(1);nodes.forEach(d => {d.y = d.depth *(d.depth == 1 ? 120 : this.diamonds.intervalH);});let node = this.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 ? (this.rootDown.data.children.length ? 0 : 1) : 1); // 拥有下部分则隐藏初始块// 创建矩形nodeEnter.append('rect').attr('type', d => d.data.id).attr('width', d => d.depth ? this.diamonds.w : (getBLen(d.data.name)/2 * 20 + 20)).attr('height', d => d.depth ? (d.data.type === COMPANY ? this.diamonds.h : this.diamonds.h - 10) : this.originDiamonds.h).attr('x', d => d.depth ? -this.diamonds.w / 2 : -(getBLen(d.data.name)/2 * 20 + 20) / 2).attr('y', d => d.depth ? showtype === 'up' ? -this.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) {_this.click(d, showtype, sourceTree)});circle.append('circle').attr('type', d => d.data.id || '').attr('r', (d) => d.depth ? (d.data.isHaveChildren ? SYMBOLA_S_R : 0) : 0).attr('cy', d => d.depth ? showtype === 'up' ? -(SYMBOLA_S_R + this.diamonds.h / 2) : (this.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 + this.diamonds.h / 2) : this.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');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' ? this.diamonds.h -10 : -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 -this.diamonds.h / 2} else {return 0}} else {if (d.depth) {return 0} else {// if (d.data.name.length > 10) {// return -5// }return 0}}}).attr('dy', d => d.depth ? (d.data.name.length > 10 ? '1.3em' : '1.8em') : `${this.originDiamonds.h/2 - 10}px`).attr('text-anchor', 'middle').attr('fill', d => d.depth ? '#DE4A3C' : '#fff').text(d =>d.depth ? (d.data.name.length > 10) ? d.data.name.substr(0, 10) : d.data.name : d.data.name).style('font-size', d => d.depth ? '14px' : '18px').style('font-family', 'PingFangSC-Medium').style('font-weight', '500').on('click', (d) => {if(d.data.id && d.depth){// 跳转操作之类的}});// 名称过长 第二段nodeEnter.append('text').attr('x', 0).attr('y', d => {// ? (d.depth ? -this.diamonds.h / 2 : 0) : 0if (showtype === 'up') {if (d.depth) {return -this.diamonds.h / 2}return 8} else {if (!d.depth) {return 8}return 0}}).attr('dy', d => d.depth ? '2.5em' : '.3em').attr('text-anchor', 'middle').attr('fill', d => d.depth ? '#DE4A3C' : '#fff').text(d => {// 索引从第19个开始截取有表示超出if(d.depth){if (d.data.name.substr(19, 1)) {return d.data.name.substr(10, 9) + '...'}return d.data.name.substr(10, 9)}else{return null}}).style('font-size', '14px').style('font-family', 'PingFangSC-Medium').style('font-weight', '500');// 认缴金额nodeEnter.append('text').attr('x', 0).attr('y', showtype === 'up' ? -this.diamonds.h / 2 : 0).attr('dy', d => d.data.name.substr(10, d.data.name.length).length ? '4.5em' : '4.1em').attr('text-anchor', 'middle').attr('fill', d => d.depth ? '#445166' : '#fff').text(d => d.data.money ? d.data.money.length > 12 ? `认缴金额:${d.data.money.substr(0, 12)}…` : `认缴金额:${d.data.money}万元` : '').style('font-size', '12px').style('font-family', 'PingFangSC-Regular').style('font-weight', '400').style('color', '#666666');/** 绘制箭头* @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).attr('fill', '#DE4A3C').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', this.diamonds.w).attr('height', this.diamonds.h).attr('stroke', 'black').attr('stroke-width', 1);// 修改线条let link = this.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').attr('d', () => {let o = {x: source.x0, y: source.y0};return _this.diagonal(o, o, showtype)});let linkUpdate = linkEnter.merge(link);// 过渡更新位置.linkUpdate// .transition()// .duration(DURATION).attr('d', d => _this.diagonal(d, d.parent, showtype));// 将退出节点转换到父节点的新位置link.exit()// .transition()// .duration(DURATION).attr('d', () => {let o = {x: source.x,y: source.y};return _this.diagonal(o, o, showtype)}).remove();// 隐藏旧位置方面过渡.nodes.forEach(d => {d.x0 = d.x;d.y0 = d.y});},// 拷贝到_children 隐藏1排以后的树// collapse (source) {// if (source.children) {// source._children = source.children;// source._children.forEach(this.collapse);// source.children = null;// this.hasChildNodeArr.push(source);// }// },// 获取点击上游的上游async fetchUpper (id, regCapi){Toast.loading({message: '加载中',forbidClick: true,duration: 0,});const dataSource = [];try{const response = await fetchEquityUpperInfo({token: this.token,instId: id,currentPage: 0,pageSize: 200,regCapi: regCapi,})const {code =0, records = []} = responseif (code > 0 && records != null) {const dataSource = [] as any[];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'})})Toast.clear()return dataSource}else{Toast.clear()return dataSource}}catch(error){Toast.clear()return dataSource}},// 获取点击下游的下游async fetchBelow (id){Toast.loading({message: '加载中',forbidClick: true,duration: 0,});const dataSource = [];try{const response = await fetchEquityBelowInfo({token: this.token,instId: id,currentPage: 0,pageSize: 200,})const {code =0, records = []} = responseif (code > 0 && records != null) {const dataSource = [] as any[];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'})})Toast.clear()return dataSource}else{Toast.clear()return dataSource}}catch(error){Toast.clear()return dataSource}},async click (source, showType, sourceTree) {// 不是起点才能点// if (source.depth) {// if (source.children) {// source._children = source.children;// source.children = null;// } else {// source.children = source._children;// source._children = null;// }// }if(source.children){// 点击减号source._children = source.children;source.children = null;}else {// 点击加号if(!source._children){let res = [] as any[]if(showType === 'up'){res = await this.fetchUpper(source.data.id, source.data.regCapi)}else {res = await this.fetchBelow(source.data.id)}if(!res.length){Notify({message: '上游或下游企业信息为空!',type: 'warning',duration: 1500})return}res.forEach(item =>{let newNode = this.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;}}this.update(source, showType, sourceTree)},diagonal (s, d, showtype) {// 折线let endMoveNum = 0;let moveDistance = 0;if (d) {if (showtype == 'down') {let downMoveNum = d.depth ? this.diamonds.h/2 : this.originDiamonds.h/2 -10 ;// var downMoveNum = 30;let tmpNum = s.y + (d.y - s.y) / 2;endMoveNum = downMoveNum;moveDistance = tmpNum + endMoveNum;} else {let upMoveNum = d.depth ? 0 : -this.originDiamonds.h/2 + 5 ;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);}},resetSvg () {this.d3.select('#treesvg').remove()this.init()},// 点击全屏showFullScreen(){let width = document.documentElement.clientWidth,height = document.documentElement.clientHeight,wrapper = document.getElementById('rightPenetrationpage') as HTMLElement,style = '';const navbar = document.querySelector('.navbar') as HTMLElementconst fullScreen = document.querySelector('.full') as HTMLElement// const box = document.getElementById('penetrateChart').children[0]// const g = document.getElementById('penetrateChart').children[0].children[0]// setTimeout(()=>{// // box.setAttribute('width', width)// // box.setAttribute('height', height - 44)// g.setAttribute('transform', 'translate(' + (width / 2) + ',' + (height / 2) + ')')// }, 200)if (this.isFull) { // 竖屏console.log('竖过来')this.isFull = falsethis.svgH = height - 44this.svgW = width// 设置按钮和顶部样式fullScreen.classList.remove('fullRight')navbar.classList.remove('smallBar')style += 'width:100%';style += 'height:100%;';style += '-webkit-transform: translateX(0) translateZ(0px) rotate(0); -ms-transform: translateX(0) translateZ(0px) rotate(0); -moz-transform:translateX(0) translateZ(0px) rotate(0); -o-transform: translateX(0) translateZ(0px) rotateY(0); transform: translateX(0) translateZ(0px) rotate(0);';style += '-webkit-transform-origin: 0 0;';style += '-ms-transform-origin: 0 0;';style += '-moz-transform-origin: 0 0;';style += '-o-transform-origin: 0 0;';style += 'transform-origin: 0 0;';} else { // 横屏console.log('横过来')this.isFull = truethis.svgH = width - 44this.svgW = height// 设置按钮和顶部样式fullScreen.classList.add('fullRight')navbar.classList.add('smallBar')style += 'width:' + height + 'px;';// 注意旋转后的宽高切换style += 'height:' + width + 'px;';style += '-webkit-transform: translateX(0) translateZ(0px) rotate(90deg); -ms-transform: translateX(0) translateZ(0px) rotate(90deg); -moz-transform: translateX(0) translateZ(0px) rotate(90deg); -o-transform: translateX(0) translateZ(0px) rotate(90deg); transform:translateX(0) translateZ(0px) rotate(90deg);';// 注意旋转中点的处理style += 'transform-origin: ' + width / 2 + 'px ' + width / 2 + 'px;';style += '-webkit-transform-origin: ' + width / 2 + 'px ' + width / 2 + 'px;';style += '-ms-transform-origin: ' + width / 2 + 'px ' + width / 2 + 'px;';style += '-moz-transform-origin: ' + width / 2 + 'px ' + width / 2 + 'px;';style += '-o-transform-origin: ' + width / 2 + 'px ' + width / 2 + 'px;';}wrapper.style.cssText = style;// 重新渲染图片this.resetSvg()},// 点击返回onClickLeft(){// jsBridge.callHandler('navigationToSkip', {type: '0'}, ()=> {// console.log('11111111111')// })history.back()}}})
</script><style lang="scss" scoped>
.father-box{transform: perspective(1000px);-ms-transform: perspective(1000px);-moz-transform: perspective(1000px);-webkit-transform: perspective(1000px);-o-transform: perspective(1000px);
}.info-icon {width: 16px;height: 16px;background-image: url('../../assets/icon/icon_info.png');background-repeat: no-repeat;background-size: cover;
}
.full {position: absolute;top: 12px;right:20px;font-size: 14px;color: #DE4A3C;display: flex;z-index: 9999;line-height: 24px;.full-icon {width: 24px;height: 24px;background-image: url('../../assets/icon/icon_fullscreen.png');background-repeat: no-repeat;background-size: cover;margin-right: 7px;}
}.fullRight{top: 12px !important;right:35px;
}
.smallBar {:deep(.van-nav-bar__left){display: none;}:deep(.van-nav-bar__content){background-color: transparent;}:deep(.van-nav-bar__title){// font-size: 4.8vh;margin-left:30px;}
}</style>
股权结构图代码:
<template><divid="structureChartIn":style="{width:'100%',display:'block',margin:'auto'}"></div></template><script lang="ts">// import { setWatermark } from '@/utils/tool.js'import { defineComponent} from 'vue'import { formatMoney, getBLen } from '@/utils/tool'import { fetchEquityUpperInfo } from '@/api/companySearch'import { Notify, Toast } from 'vant'import * as $d3 from 'd3'// 过渡时间const DURATION = 400// 加减符号半径const SYMBOLA_S_R = 9// // 公司// const COMPANY = '0'// // 人// const PERSON = '1'export default defineComponent({props: {tree: {type: Object,default: () => {return {
'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',}]}]}}},token: {type: String,default: ''}},components: {},data () {return {diamonds: {} as any,originDiamonds: {} as any,d3: $d3,// hasChildNodeArr: [],root: {} as any,svg: {} as any,svgW: document.documentElement.clientWidth,svgH: document.documentElement.clientHeight - 88,title: '股权结构图',lastClickD: null,}},watch: {tree(newVal){if(newVal.name){this.init()}}},mounted () {// window.addEventListener('orientationchange', this.changeOrient)},// beforeUnmount(){// window.removeEventListener('orientationchange', this.changeOrient)// },methods: {// changeOrient () {// const box = document.getElementById('structureChartIn').children[0]// const g = document.getElementById('structureChartIn').children[0].children[0]// let navbar = document.querySelector('.navbar')// let flag = false// flag = isOrient()// setTimeout(()=>{// if(flag){// navbar?.classList.add('smallBar')// }else{// navbar?.classList.remove('smallBar')// }// console.log(document.documentElement.clientWidth, document.documentElement.clientHeight)// box.setAttribute('width', document.documentElement.clientWidth)// box.setAttribute('height', document.documentElement.clientHeight)// g.setAttribute('transform', 'translate(' + (document.documentElement.clientWidth / 2) + ',' + (document.documentElement.clientHeight / 2) + ')')// }, 100)// },init () {let d3 = this.d3let svgW = this.svgWlet svgH = this.svgHlet margin = {top: 20, right: 20, bottom: 30, left: 10}// 方块形状this.diamonds = {w: 320,h: 60,}// 源头对象this.originDiamonds = {w: 208,h: 36}// 主图this.svg = d3.select('#structureChartIn').append('svg').attr('width', svgW).attr('height', svgH).attr('id', 'treesvgIn').call(d3.zoom().scaleExtent([0.3, 2]).on('zoom', () => {const transform = d3.event.transformthis.svg.attr('transform', transform.translate(margin.left, margin.top));})).on('dblclick.zoom', null).attr('style', 'position: relative;z-index: 2') // background-image:url(${setWatermark({name: this.$store.state.global.user.userName, loginName: this.$store.state.global.user.userId}).toDataURL()}).append('g').attr('id', 'gIn').attr('transform', `translate(${margin.left},${margin.top})`) // 拷贝树的数据let downTree = {} as anyObject.keys(this.tree).map(item => {if (item === 'children') {downTree = JSON.parse(JSON.stringify(this.tree))downTree.children = this.tree[item]}})// hierarchy 返回新的结构 x0,y0初始化起点坐标this.root = d3.hierarchy(downTree);this.root.x0 = 0this.root.y0 = 0if(!this.root.children){this.update(this.root)}else {// this.root.children.forEach(this.collapse)this.update(this.root)}},/**[update 函数描述], [click 函数描述]* @param {[Object]} source 第一次是初始源对象,后面是点击的对象*/update (source) {// eslint-disable-next-linelet _this = thislet nodes= this.root.descendants()let index = -1, count = 0;this.root.eachBefore(function(n) {count+=20;n.style = 'node_' + n.depth;n.x = ++index * _this.diamonds.h + count;n.y = n.depth * 25; // 设置下一层水平位置向后移25px});let node = this.svg.selectAll('g.node').data(nodes, d => {return 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 ? this.diamonds.w : (d.data.children.length ? (getBLen(d.data.name)/2 * 18 + 62) : (getBLen(d.data.name)/2 * 18 + 20))).attr('height', d => d.depth ? this.diamonds.h : this.originDiamonds.h).attr('y', -this.diamonds.h / 2).style('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', -this.diamonds.h / 2).attr('height', d => d.depth ? this.diamonds.h : this.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 : (d.data.children.length ? 36 : 10)).style('font-size', d=> d.depth ? '14px' : '18px').style('font-weight', '500').attr('fill', d => d.depth ? '#333333' : '#fff').text(function(d) {// 名字长度超过进行截取if(d.depth){if(d.data.name.length>20){return d.data.name.substring(0, 19) + '...'; } }return d.data.name; }).on('click', (d) => {if(d.data.id && d.depth){// 跳转操作之类的}});// 持股比例nodeEnter.append('text').attr('dy', 17).attr('dx', 36).style('font-size', '12px').style('fill', '#666666').text(function(d) {if(!d.data.tap){return ('持股比例 ' +':')} });nodeEnter.append('text').attr('dy', 17).attr('dx', 95).style('font-size', '12px').style('fill', '#DE4A3C').text(function(d) {if(!d.data.tap){return (d.data.scale)} });// 认缴金额nodeEnter.append('text').attr('dy', 17).attr('dx', 150).style('font-size', '12px').style('fill', '#666666').text(function(d) {if(!d.data.tap){return ('认缴金额 ' +':')} });nodeEnter.append('text').attr('dy', 17).attr('dx', 210).style('font-size', '12px').style('fill', '#DE4A3C').text(function(d) {if(!d.data.tap){if(d.data.money.length > 14){return d.data.money.substr(0, 14) + '...'}else{return (d.data.money + '万元')}} });// 箭头// nodeEnter.append('text')// .attr('dy', 5.5)// .attr('dx', 200 )// .style('font-size', '20px')// .style('fill', '#000')// .text(function(d) {// if(!d.data.tap){// return '>'// }// });// 创造圆 加减let circle = nodeEnter.append('g').attr('class', 'circle').on('click', _this.click);circle.append('circle').style('fill', '#F9DDD9').style('stroke', '#FCEDEB').style('stroke-width', 1).attr('r', function (d) {if (d.children || d.data.isHaveChildren) {return 9;} else {return 0;}}).attr('cy', d => d.depth ? 0 : (- SYMBOLA_S_R -3)).attr('cx', 20)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.children) {return '-';} else if (d._children || d.data.isHaveChildren) {return '+';} else {return '';}}).style('font-size', '16px');node.select('.fa').text(function (d) {if (d.children) {return '-';} else if (d._children || d.data.isHaveChildren) {return '+';} else {return '';}})/** 绘制箭头* @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 箭头颜色*/// nodeEnter.append('marker')// .attr('id', 'resolvedIn')// .attr('markerUnits', 'strokeWidth')// .attr('markerUnits', 'userSpaceOnUse')// .attr('viewBox', '0 -5 10 10')// .attr('markerWidth', 8)// .attr('markerHeight', 8)// .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();// nodeExit.select('rect')// .attr('width', this.diamonds.w)// .attr('height', this.diamonds.h)// .attr('stroke', 'black')// .attr('stroke-width', 1);// 修改线条let link = this.svg.selectAll('path.link').data(this.root.links(), d => d.target.id);// 在父级前的位置画线。let linkEnter = link.enter().insert('path', 'g').attr('class', d =>{return '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 _this.diagonal({source: o, target: o})})// .transition()// .duration(DURATION).attr('d', _this.diagonal);// 过渡更新位置.link// .transition()// .duration(DURATION).attr('d', _this.diagonal);// 将退出节点转换到父节点的新位置link.exit()// .transition()// .duration(DURATION).attr('d', () => {let o = {x: source.x,y: source.y};return _this.diagonal({source: o, target: o})}).remove();// 隐藏旧位置方面过渡.this.root.each(d => {d.x0 = d.x;d.y0 = d.y});},// 获取点击上游的上游async fetchUpper (id, regCapi){Toast.loading({message: '加载中',forbidClick: true,duration: 0,});const dataSource = [];try{const response = await fetchEquityUpperInfo({token: this.token,instId: id,currentPage: 0,pageSize: 200,regCapi: regCapi,})const {code =0, records = []} = responseif (code > 0 && records != null) {console.log(records)const dataSource = [] as any[];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'})})Toast.clear()return dataSource}else{Toast.clear()return dataSource}}catch(error){Toast.clear()return dataSource}},async click (source) {// if (d.children) {// d._children = d.children;// d.children = null;// } else {// d.children = d._children;// d._children = null;// }// if (this.lastClickD){// this.lastClickD._isSelected = false;// }// d._isSelected = true;// this.lastClickD = d;if(source.children){// 点击减号source._children = source.children;source.children = null;}else {// 点击加号if(!source._children){let res = [] as any[]res = await this.fetchUpper(source.data.id, source.data.regCapi)if(!res.length){Notify({message: '上游或下游企业信息为空!',type: 'warning',duration: 1500})return}res.forEach(item =>{let newNode = this.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;}}this.update(source);},// 拷贝到_children 隐藏1排以后的树// collapse (source) {// if (source.children) {// source._children = source.children;// source._children.forEach(this.collapse);// source.children = null;// this.hasChildNodeArr.push(source);// }// },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}`;},}})
</script>
总结:
前端小白一枚,在之前只使用过echarts进行可视化,在开发这个功能时候发现d3版本中文网站内容较少,基本出现问题讨论也是在外文网站,踩过一堆版本的坑,最终选择稳定且例子比较多的v4版本。还有基本都是默认信息展示,很少有点击请求的功能,进行一个最终功能的整合,如果有问题欢迎大家积极提出讨论,共同进步~
【vue d3 v4】vue2结合d3实现类似企查查的股权穿透图、股权结构图相关推荐
- D3 企业关系图谱 企业构成图谱 股权穿透图 股权结构图 关联方认定图
前言:之前说做了项目,这个项目黄了,公司跑路了,代码就拿出来分享,主要就是实现各种类似企查查的各种图谱,欢迎交流.目前已大致实现了: D3 企业关系图谱 企业构成图谱 股权穿透图 股权结构图 关联方认 ...
- 使用d3实现类似脑图,股权穿透图,关系图谱
前言 最近工作需要完成一个股权穿透图,找了好多文档发现都不满足需求,最终选择d3.js来实现,包含子集的收缩展开,交互以及其他功能.之前由于没做过类似关系图以及不了解d3,踩了很多坑,我会尽可能将代码 ...
- d3.js实现股权穿透图(vue+d3.js)
d3.js实现股权穿透图 业务需求 1.实现的效果图 2.安装依赖 3.全部代码(复制粘贴即可实现) 参考git地址 业务需求 前段时间写过一篇使用relation-graph插件实现的股权穿透图效果 ...
- d3 企业图谱 仿天眼查 企查查
最近接到一个需求,终端要加入企业图谱的功能.能无线穿透下去,之前写过一个类似树形图但是节点长度没有自适应(如下图),样式也不够好看,产品提出做一个类似企查查那种的企业图谱,能更直观的展示企业信息,无奈 ...
- 仿企查查、天眼查 d3关联关系图 力项导图
研究完股权穿透图跟企业图谱 最近又写了一个企业.人物的力项导图 力导向图可以直观看出各个元素之间的关系 具体的代码就不贴了,官网很多 demo 都可以直接拿来用 先看效果 d3灵活的地方就在于它可以 ...
- D3 天眼查 股权穿透 股权结构
效果图如上 大致效果就是仿照天眼查股权穿透图 曲线会出现节点位置不对(曲线的算法是在不会 技术菜解决不了) 最后刀放到产品脖子他同意用折线代替 在下面已经补充 JSON数据 把请求换成请求本地j ...
- d3 v4版本画基本图
感谢 http://www.ourd3js.com/wordpress/ 提供的技术参考,其中地图的文件china.geojson 下载地址http://www.ourd3js.com/wordpre ...
- d3 v4 api interpolate
方法 说明 d3.interpolate interpolate arbitrary values. d3.interpolateArray interpolate arrays of arbitra ...
- 仿企查查、天眼查股权穿透d3
企业图谱做出来了,接下来仿企查查写个股权穿透的图谱 企查查股权穿透 自己的 首先使用的方法以及生成图的方法 跟企业图谱类似 也是用的d3官方demo给出的生成双向树的方法,不过版本是d3.v3 相比企 ...
最新文章
- python生成epub文件_python在内存中生成Zip文件!
- C语言有三个电阻r1r2r3,[VR虚拟现实]ARM硬件试题库及答案(37页)-原创力文档
- Eclipse相关问题总结
- 【渝粤教育】国家开放大学2018年春季 0554-21T立体构成(一) 参考试题
- 图像的灰度级数越多越好_MATLAB-数字图像处理 图像直方图归一化
- 源码时代php中级项目,0526PHP班中级项目评比圆满落幕
- jbutton添加点击事件_electron-vue自定义边框后点击事件失效问题
- socket中使用心跳来检测连接是否断开[ZT]
- linux下文件的相关信息
- 基础面试题——HTML/CSS
- 基于log4net的支持动态文件名、按日期和大小自动分割文件的日志组件
- 计算机操作系统(第四版)课后习题答案西电版V2.0校对版
- linux中安装rpm命令,linux下,如何安装rpm命令?
- 索尼1a dac插电脑用什么驱动。在哪下载,求助
- 阿里CEO张勇:打破各企业边界 联手对抗黑灰产
- 我的python程序_我试着运行我的python程序,但当我运行它时什么也没有发生
- 带你领略Clean架构的魅力,腾讯T3大佬亲自讲解
- 笔记本电脑识别不到WiFi、蓝牙消失
- 【C++】面向对象之继承篇
- 简短加密_神经网络训练中回调的简短实用指南