D3.js(Data-Driven Documents)是一个用于操作文档数据的JavaScript库,主要用于创建基于数据的动态和交互式数据可视化。它的核心功能包括:
- 数据绑定:D3.js可以将数据与DOM元素绑定,使得数据的变化可以自动反映在页面上。
- SVG支持:D3.js可以创建和操作SVG元素,用于生成图表、地图和其他视觉元素。
- 动画和过渡:D3.js支持丰富的动画效果,可以使数据可视化过程更加生动。
- 灵活性:D3.js提供了很多工具和函数,可以对数据进行操作和转换,支持各种图表和可视化形式的创建。
效果图
演示效果
组件代码
<template>
<div id="flow-box">
<!-- 操作按钮组 -->
<div class="tool-box">
<div class="tool-item" @click="toggleFullScreen">
<el-tooltip
class="box-item"
effect="dark"
content="全屏"
placement="left"
>
<el-icon><FullScreen /></el-icon>
</el-tooltip>
</div>
<div class="tool-item" @click="expandAllNodes()">
<el-tooltip
class="box-item"
effect="dark"
content="全部展开"
placement="left"
>
<el-icon><CaretBottom /></el-icon>
</el-tooltip>
</div>
<div class="tool-item" @click="foldAllNodes()">
<el-tooltip
class="box-item"
effect="dark"
content="全部收起"
placement="left"
>
<el-icon><CaretTop /></el-icon>
</el-tooltip>
</div>
<div class="tool-item" @click="foldAllNodes()">
<el-tooltip
class="box-item"
effect="dark"
content="刷新"
placement="left"
>
<el-icon><Refresh /></el-icon>
</el-tooltip>
</div>
<div class="tool-item" @click="downloadChart">
<el-tooltip
class="box-item"
effect="dark"
content="下载"
placement="left"
>
<el-icon><Download /></el-icon>
</el-tooltip>
</div>
</div>
<!-- 绘制架构图盒子 -->
<div id="chartBox" ref="chartBox" :style="styles"></div>
</div>
</template>
<script setup>
import * as d3 from 'd3'
import { onMounted, onBeforeUnmount, ref } from 'vue'
import { ElTooltip } from 'element-plus'
import {
Refresh,
CaretBottom,
CaretTop,
FullScreen,
Download,
} from '@element-plus/icons-vue'
const props = defineProps({
styles: {
type: Object,
default: () => ({
height: '680px',
}),
},
// 数据源
data: {
type: Object,
// 默认数据格式
default: () => ({
id: '10001',
name: '广东XXX集团公司',
children: [
{
id: '10002',
name: '深圳市XXX投资有限公司',
percent: '100%',
rounds: '天使轮',
},
{
id: '10003',
name: '深圳市XXX技术有限公司',
percent: '100%',
rounds: 'A轮',
},
{
id: '10004',
name: '深圳市XXX光伏材料有限公司',
percent: '100%',
rounds: '被收购',
off: true,
},
{
id: '10005',
name: '深圳市XXX医疗科技有限公司',
percent: '100%',
children: [
{
id: '10006',
name: '深圳市XXX医疗仪器有限公司',
percent: '100%',
children: [
{
id: '10007',
name: '深圳市XXX的子公司一',
percent: '80%',
},
{
id: '10008',
name: '深圳市XXX的子公司二',
percent: '90%',
},
{
id: '10009',
name: '深圳市XXX的子公司三',
percent: '100%',
},
],
},
],
},
{
id: '100010',
name: '上海XXX科技有限公司',
percent: '100%',
children: [
{
id: '100011',
name: '上海XXX通讯设备有限公司',
percent: '100%',
children: [
{
id: '100012',
name: '上海XXX的子公司一',
percent: '100%',
},
{
id: '100013',
name: '上海XXX的子公司二',
percent: '90%',
},
],
},
],
},
{
id: '100014',
name: '北京XXX科技有限公司',
percent: '100%',
children: [
{
id: '100015',
name: '北京XXX网络有限公司',
},
],
},
],
}),
},
});
// 基础配置项
const config = ref({
// 节点的横向距离
dx: 200,
// 节点的纵向距离
dy: 170,
// svg的viewBox的宽度
width: 0,
// svg的viewBox的高度
height: 650,
// 节点的矩形框宽度
rectWidth: 170,
// 节点的矩形框高度
rectHeight: 70,
})
const svgs = ref(null)
const gAlls = ref(null)
const gLinks = ref(null)
const gNodes = ref(null)
// 给树加坐标点的方法
const tree = ref(null)
// 投资公司树的根节点
const rootDom = ref(null)
// 股东树的根节点
const childDom = ref(null)
// 是否全屏
const isFullscreen = ref(true)
onMounted(() => {
drawChart({
type: 'fold',
})
// 监听窗口大小变化
window.addEventListener('resize', handleResize)
})
const handleResize = () => {
// ESC退出全屏改变图表
if (!checkFull()) {
isFullscreen.value = true
}
svg.setAttribute('height', window.innerHeight)
}
// 在组件销毁时移除事件监听
onBeforeUnmount(() => {
window.removeEventListener('resize', handleResize)
})
// 初始化树结构数据
function drawChart(options) {
// 宿主元素的d3选择器对象
let host = d3.select(document.getElementById('chartBox'))
// 宿主元素的DOM,通过node()获取到其DOM元素对象
let dom = host.node()
// 宿主元素的DOMRect
let domRect = dom.getBoundingClientRect()
// svg的宽度和高度
config.value.width = domRect.width
config.value.height = domRect.height
let oldSvg = host.select('svg')
// 如果宿主元素中包含svg标签了,那么则删除这个标签,再重新生成一个
if (!oldSvg.empty()) {
oldSvg.remove()
}
const svg = d3
.create('svg')
.attr('id', 'mysvg')
.attr('viewBox', () => {
let parentsLength = props.data.parents ? props.data.parents.length : 0
return [
-config.value.width / 2,
// 如果有父节点,则根节点居中,否则根节点上浮一段距离
parentsLength > 0 ? -config.value.height / 2 : -config.value.height / 3,
config.value.width,
config.value.height,
]
})
.style('user-select', 'none')
.style('cursor', 'move')
// 包括连接线和节点的总集合
const gAll = svg.append('g').attr('id', 'all')
svg
.call(
d3
.zoom()
.scaleExtent([0.5, 2])
.on('zoom', (e) => {
gAll.attr('transform', () => {
return `translate(${e.transform.x},${e.transform.y}) scale(${e.transform.k})`
})
})
)
.on('dblclick.zoom', null) // 取消默认的双击放大事件
gAlls.value = gAll
// 连接线集合
gLinks.value = gAll.append('g').attr('id', 'linkGroup')
// 节点集合
gNodes.value = gAll.append('g').attr('id', 'nodeGroup')
// 设置好节点之间距离的tree方法
tree.value = d3.tree().nodeSize([config.value.dx, config.value.dy])
rootDom.value = d3.hierarchy(props.data, (d) => d.children)
childDom.value = d3.hierarchy(props.data, (d) => d.parents)
tree.value(rootDom.value)
;[rootDom.value.descendants(), childDom.value.descendants()].forEach(
(nodes) => {
nodes.forEach((node) => {
node._children = node.children || null
if (options.type === 'all') {
//如果是all的话,则表示全部都展开
node.children = node._children
} else if (options.type === 'fold') {
//如果是fold则表示除了父节点全都折叠
// 将非根节点的节点都隐藏掉(其实对于这个组件来说加不加都一样)
if (node.depth) {
node.children = null
}
}
})
}
)
//箭头(下半部分)
svg
.append('marker')
.attr('id', 'markerOfDown')
.attr('markerUnits', 'userSpaceOnUse')
.attr('viewBox', '0 -5 10 10') //坐标系的区域
.attr('refX', 55) //箭头坐标
.attr('refY', 0)
.attr('markerWidth', 10) //标识的大小
.attr('markerHeight', 10)
.attr('orient', '90') //绘制方向,可设定为:auto(自动确认方向)和 角度值
.attr('stroke-width', 2) //箭头宽度
.append('path')
.attr('d', 'M0,-5L10,0L0,5') //箭头的路径
.attr('fill', '#215af3') //箭头颜色
//箭头(上半部分)
svg
.append('marker')
.attr('id', 'markerOfUp')
.attr('markerUnits', 'userSpaceOnUse')
.attr('viewBox', '0 -5 10 10') //坐标系的区域
.attr('refX', -50) //箭头坐标
.attr('refY', 0)
.attr('markerWidth', 10) //标识的大小
.attr('markerHeight', 10)
.attr('orient', '90') //绘制方向,可设定为:auto(自动确认方向)和 角度值
.attr('stroke-width', 2) //箭头宽度
.append('path')
.attr('d', 'M0,-5L10,0L0,5') //箭头的路径
.attr('fill', '#215af3') //箭头颜色
svgs.value = svg
update()
// 将svg置入宿主元素中
host.append(function () {
return svg.node()
})
}
// 更新数据
function update(source) {
if (!source) {
source = {
x0: 0,
y0: 0,
}
// 设置根节点所在的位置(原点)
rootDom.value.x0 = 0
rootDom.value.y0 = 0
childDom.value.x0 = 0
childDom.value.y0 = 0
}
let nodesOfDown = rootDom.value.descendants().reverse()
let linksOfDown = rootDom.value.links()
let nodesOfUp = childDom.value.descendants().reverse()
let linksOfUp = childDom.value.links()
tree.value(rootDom.value)
tree.value(childDom.value)
const myTransition = svgs.value.transition().duration(500)
// 绘制子公司树
const node1 = gNodes.value
.selectAll('g.nodeOfDownItemGroup')
.data(nodesOfDown, (d) => {
return d.data.id
})
const node1Enter = node1
.enter()
.append('g')
.attr('class', 'nodeOfDownItemGroup')
.attr('transform', (d) => {
return `translate(${source.x0},${source.y0})`
})
.attr('fill-opacity', 0)
.attr('stroke-opacity', 0)
.style('cursor', 'pointer')
// 外层的矩形框
node1Enter
.append('rect')
.attr('width', (d) => {
if (d.depth === 0) {
return (d.data.name.length + 2) * 16
}
return config.value.rectWidth
})
.attr('height', (d) => {
if (d.depth === 0) {
return 40
}
return config.value.rectHeight
})
.attr('x', (d) => {
if (d.depth === 0) {
return (-(d.data.name.length + 2) * 16) / 2
}
return -config.value.rectWidth / 2
})
.attr('y', (d) => {
if (d.depth === 0) {
return -15
}
return -config.value.rectHeight / 2
})
.attr('rx', 5)
.attr('stroke-width', 1)
.attr('stroke', (d) => {
if (d.depth === 0) {
return '#5682ec'
}
return '#7A9EFF'
})
.attr('fill', (d) => {
if (d.depth === 0) {
return '#7A9EFF'
}
return '#FFFFFF'
})
.on('click', (e, d) => {
nodeClickEvent(e, d)
})
// 文本主标题
node1Enter
.append('text')
.attr('class', 'main-title')
.attr('x', (d) => {
return 0
})
.attr('y', (d) => {
if (d.depth === 0) {
return 10
}
return -14
})
.attr('text-anchor', (d) => {
return 'middle'
})
.text((d) => {
if (d.depth === 0) {
return d.data.name
} else {
return d.data.name.length > 11
? d.data.name.substring(0, 11)
: d.data.name
}
})
.attr('fill', (d) => {
if (d.depth === 0) {
return '#FFFFFF'
}
return '#000000'
})
.style('font-size', (d) => (d.depth === 0 ? 16 : 14))
.style('font-family', '黑体')
.style('font-weight', 'bold')
.append('svg:title')
.text((d) => d.data.name)
// 副标题
node1Enter
.append('text')
.attr('class', 'sub-title')
.attr('x', (d) => {
return 0
})
.attr('y', (d) => {
return 5
})
.attr('text-anchor', (d) => {
return 'middle'
})
.text((d) => {
if (d.depth !== 0) {
let subTitle = d.data.name.substring(11)
if (subTitle.length > 10) {
return subTitle.substring(0, 10) + '...'
} else {
return subTitle
}
}
})
.style('font-size', (d) => 14)
.style('font-family', '黑体')
.style('font-weight', 'bold')
.append('svg:title')
.text((d) => d.data.name)
// 融资轮次
node1Enter
.append('svg:rect')
.attr('x', '-84')
.attr('y', '14')
.attr('rx', 5)
.attr('width', (d) => {
if (d.depth !== 0 && d.data.rounds) {
return config.value.rectWidth - 2
}
})
.attr('height', '20')
.style('fill', '#E5F3FE')
node1Enter
.append('text')
.attr('x', '0')
.attr('dy', '30')
.attr('text-anchor', 'middle')
.attr('class', 'linkname')
// .style("fill", "#666")
.style('font-size', 12)
.attr('fill', '#008BF8')
.text(function (d) {
if (d.depth !== 0 && d.data.rounds) {
var str = '融资轮次:' + d.data.rounds
return str.length > 13 ? str.substring(0, 18) + '..' : str
}
})
// 控股比例
node1Enter
.append('text')
.attr('class', 'percent')
.attr('x', (d) => {
return 15
})
.attr('y', (d) => {
return -55
})
.text((d) => {
if (d.depth !== 0) {
return d.data.percent
}
})
.attr('fill', '#0084FF')
.style('font-family', '黑体')
.style('font-size', (d) => 14)
node1Enter
.append('svg:rect')
.attr('x', '-40')
.attr('y', '-70')
.attr('rx', 2)
.attr('width', function (d) {
return d.depth !== 0 && d.data.percent ? 30 : 0
})
.attr('height', function (d) {
return d.depth !== 0 && d.data.percent ? 20 : 0
})
.style('fill', '#EBF5FF')
node1Enter
.append('text')
.attr('x', '-37')
.attr('dy', '-55')
.attr('text-anchor', 'start')
.style('fill', '#0084FF')
.style('font-size', 12)
.text(function (d) {
return d.depth !== 0 && d.data.percent ? '控股' : ''
})
// 吊销/注销
node1Enter
.append('svg:rect')
.attr('x', '10')
.attr('y', '-48')
.attr('rx', 2)
.attr('width', function (d) {
return d.depth !== 0 && d.data.off ? 45 : 0
})
.attr('height', function (d) {
return d.depth !== 0 && d.data.off ? 10 : 0
})
.style('fill', '#fde2e2')
node1Enter
.append('text')
.attr('x', '15')
.attr('dy', '-40')
.attr('text-anchor', 'start')
.style('fill', '#f89898')
.style('font-size', 8)
.text(function (d) {
return d.depth !== 0 && d.data.off ? '吊销/注销' : ''
})
// 增加展开按钮
const expandBtnG = node1Enter
.append('g')
.attr('class', 'expandBtn')
.attr('transform', (d) => {
return `translate(${0},${config.value.rectHeight / 2})`
})
.style('display', (d) => {
// 如果是根节点,不显示
if (d.depth === 0) {
return 'none'
}
// 如果没有子节点,则不显示
if (!d._children) {
return 'none'
}
})
.on('click', (e, d) => {
if (d.children) {
d._children = d.children
d.children = null
} else {
d.children = d._children
}
update(d)
})
expandBtnG.append('circle').attr('r', 8).attr('fill', '#7A9EFF').attr('cy', 8)
expandBtnG
.append('text')
.attr('text-anchor', 'middle')
.attr('fill', '#ffffff')
.attr('y', 13)
.style('font-size', 16)
.style('font-family', '微软雅黑')
.text((d) => {
return d.children ? '-' : '+'
})
const link1 = gLinks.value
.selectAll('path.linkOfDownItem')
.data(linksOfDown, (d) => d.target.data.id)
const link1Enter = link1
.enter()
.append('path')
.attr('class', 'linkOfDownItem')
.attr('d', (d) => {
let o = {
source: {
x: source.x0,
y: source.y0,
},
target: {
x: source.x0,
y: source.y0,
},
}
return drawLink(o)
})
.attr('fill', 'none')
.attr('stroke', '#7A9EFF')
.attr('stroke-width', 1)
.attr('marker-end', 'url(#markerOfDown)')
// 有元素update更新和元素新增enter的时候
node1
.merge(node1Enter)
.transition(myTransition)
.attr('transform', (d) => {
return `translate(${d.x},${d.y})`
})
.attr('fill-opacity', 1)
.attr('stroke-opacity', 1)
// 有元素消失时
node1
.exit()
.transition(myTransition)
.remove()
.attr('transform', (d) => {
return `translate(${source.x0},${source.y0})`
})
.attr('fill-opacity', 0)
.attr('stroke-opacity', 0)
link1.merge(link1Enter).transition(myTransition).attr('d', drawLink)
link1
.exit()
.transition(myTransition)
.remove()
.attr('d', (d) => {
let o = {
source: {
x: source.x,
y: source.y,
},
target: {
x: source.x,
y: source.y,
},
}
return drawLink(o)
})
// 绘制股东树
nodesOfUp.forEach((node) => {
node.y = -node.y
})
const node2 = gNodes.value
.selectAll('g.nodeOfUpItemGroup')
.data(nodesOfUp, (d) => {
return d.data.id
})
const node2Enter = node2
.enter()
.append('g')
.attr('class', 'nodeOfUpItemGroup')
.attr('transform', (d) => {
return `translate(${source.x0},${source.y0})`
})
.attr('fill-opacity', 0)
.attr('stroke-opacity', 0)
.style('cursor', 'pointer')
// 外层的矩形框
node2Enter
.append('rect')
.attr('width', (d) => {
if (d.depth === 0) {
return (d.data.name.length + 2) * 16
}
return config.value.rectWidth
})
.attr('height', (d) => {
if (d.depth === 0) {
return 40
}
return config.value.rectHeight
})
.attr('x', (d) => {
if (d.depth === 0) {
return (-(d.data.name.length + 2) * 16) / 2
}
return -config.value.rectWidth / 2
})
.attr('y', (d) => {
if (d.depth === 0) {
return -15
}
return -config.value.rectHeight / 2
})
.attr('rx', 5)
.attr('stroke-width', 1)
.attr('stroke', (d) => {
if (d.depth === 0) {
return '#5682ec'
}
return '#7A9EFF'
})
.attr('fill', (d) => {
if (d.depth === 0) {
return '#7A9EFF'
}
return '#FFFFFF'
})
.on('click', (e, d) => {
nodeClickEvent(e, d)
})
// 文本主标题
node2Enter
.append('text')
.attr('class', 'main-title')
.attr('x', (d) => {
return 0
})
.attr('y', (d) => {
if (d.depth === 0) {
return 10
}
return -14
})
.attr('text-anchor', (d) => {
return 'middle'
})
.text((d) => {
if (d.depth === 0) {
return d.data.name
} else {
return d.data.name.length > 11
? d.data.name.substring(0, 11)
: d.data.name
}
})
.attr('fill', (d) => {
if (d.depth === 0) {
return '#FFFFFF'
}
return '#000000'
})
.style('font-size', (d) => (d.depth === 0 ? 16 : 14))
.style('font-family', '黑体')
.style('font-weight', 'bold')
.append('svg:title')
.text((d) => d.data.name)
// 副标题
node2Enter
.append('text')
.attr('class', 'sub-title')
.attr('x', (d) => {
return 0
})
.attr('y', (d) => {
return 5
})
.attr('text-anchor', (d) => {
return 'middle'
})
.text((d) => {
if (d.depth !== 0) {
let subTitle = d.data.name.substring(11)
if (subTitle.length > 10) {
return subTitle.substring(0, 10) + '...'
}
return subTitle
}
})
.style('font-size', (d) => 14)
.style('font-family', '黑体')
.style('font-weight', 'bold')
.append('svg:title')
.text((d) => d.data.name)
// 控股比例
node2Enter
.append('text')
.attr('class', 'percent')
.attr('x', (d) => {
return 12
})
.attr('y', (d) => {
return 55
})
.text((d) => {
if (d.depth !== 0) {
return d.data.percent
}
})
.attr('fill', function (d) {
return d.data.controlling ? '#FA6B64' : '#000000'
})
.style('font-family', '黑体')
.style('font-size', (d) => 14)
// 地址
node2Enter
.append('svg:rect')
.attr('x', '50')
.attr('y', '-55')
.attr('rx', 2)
.attr('width', function (d) {
return d.depth !== 0 && d.data.address ? 28 : 0
})
.attr('height', function (d) {
return d.depth !== 0 && d.data.address ? 15 : 0
})
.style('fill', ' #e1f3d8')
node2Enter
.append('text')
.attr('x', '53')
.attr('dy', '-44')
.attr('text-anchor', 'start')
.style('fill', '#95d475')
.style('font-size', 10)
.text(function (d) {
return d.depth !== 0 && d.data.address ? d.data.address : ''
})
// 实际控股人
node2Enter
.append('svg:rect')
.attr('x', -50)
.attr('y', -90)
.attr('width', function (d) {
return d.data.controlling ? 100 : 0
})
.attr('height', function (d) {
return d.data.controlling ? 40 : 0
})
.attr('rx', 2)
.style('stroke', function (d) {
return d.data.controlling ? '#FA6B64' : '#F1B03A'
})
.style('fill', function (d) {
return d.data.controlling ? '#FA6B64' : '#F1B03A' //节点背景色
})
node2Enter
.append('svg:path')
.attr('fill', (d) => {
return d.data.controlling ? '#FA6B64' : '#F1B03A'
})
.attr('d', function (d) {
if (d.data.controlling) {
return 'M0 -44 L-10 -54 L10 -54 Z'
} else {
return ''
}
})
node2Enter
.append('text')
.attr('x', '0')
.attr('dy', '-74')
.attr('text-anchor', 'middle')
.style('fill', '#fff')
.style('font-size', 12)
.text(function (d) {
return d.data.controlling ? '实际控制人' : ''
})
node2Enter
.append('text')
.attr('x', '0')
.attr('dy', '-58')
.attr('text-anchor', 'middle')
.style('fill', '#fff')
.style('font-size', 12)
.text(function (d) {
return d.data.controlling ? '受益所有人' : ''
})
// 增加展开按钮
const expandBtnG2 = node2Enter
.append('g')
.attr('class', 'expandBtn')
.attr('transform', (d) => {
return `translate(${0},${-config.value.rectHeight / 2})`
})
.style('display', (d) => {
// 如果是根节点,不显示
if (d.depth === 0) {
return 'none'
}
// 如果没有子节点,则不显示
if (!d._children) {
return 'none'
}
})
.on('click', (e, d) => {
if (d.children) {
d._children = d.children
d.children = null
} else {
d.children = d._children
}
update(d)
})
expandBtnG2
.append('circle')
.attr('r', 8)
.attr('fill', '#7A9EFF')
.attr('cy', -8)
expandBtnG2
.append('text')
.attr('text-anchor', 'middle')
.attr('fill', '#ffffff')
.attr('y', -3)
.style('font-size', 16)
.style('font-family', '微软雅黑')
.text((d) => {
return d.children ? '-' : '+'
})
const link2 = gLinks.value
.selectAll('path.linkOfUpItem')
.data(linksOfUp, (d) => d.target.data.id)
const link2Enter = link2
.enter()
.append('path')
.attr('class', 'linkOfUpItem')
.attr('d', (d) => {
let o = {
source: {
x: source.x0,
y: source.y0,
},
target: {
x: source.x0,
y: source.y0,
},
}
return drawLink(o)
})
.attr('fill', 'none')
.attr('stroke', '#7A9EFF')
.attr('stroke-width', 1)
.attr('marker-end', 'url(#markerOfUp)')
// 有元素update更新和元素新增enter的时候
node2
.merge(node2Enter)
.transition(myTransition)
.attr('transform', (d) => {
return `translate(${d.x},${d.y})`
})
.attr('fill-opacity', 1)
.attr('stroke-opacity', 1)
// 有元素消失时
node2
.exit()
.transition(myTransition)
.remove()
.attr('transform', (d) => {
return `translate(${source.x0},${source.y0})`
})
.attr('fill-opacity', 0)
.attr('stroke-opacity', 0)
link2.merge(link2Enter).transition(myTransition).attr('d', drawLink)
link2
.exit()
.transition(myTransition)
.remove()
.attr('d', (d) => {
let o = {
source: {
x: source.x,
y: source.y,
},
target: {
x: source.x,
y: source.y,
},
}
return drawLink(o)
})
// node数据改变的时候更改一下加减号
const expandButtonsSelection = d3.selectAll('g.expandBtn')
expandButtonsSelection
.select('text')
.transition()
.text((d) => {
return d.children ? '-' : '+'
})
rootDom.value.eachBefore((d) => {
d.x0 = d.x
d.y0 = d.y
})
childDom.value.eachBefore((d) => {
d.x0 = d.x
d.y0 = d.y
})
}
// 直角连接线
function drawLink({ source, target }) {
const halfDistance = (target.y - source.y) / 2
const halfY = source.y + halfDistance
return `M${source.x},${source.y} L${source.x},${halfY} ${target.x},${halfY} ${target.x},${target.y}`
}
// 展开所有的节点
function expandAllNodes() {
drawChart({
type: 'all',
})
}
// 将所有节点都折叠
function foldAllNodes() {
drawChart({
type: 'fold',
})
}
// 点击节点获取节点数据
function nodeClickEvent(e, d) {
// console.log('当前节点的数据:', d)
}
// 全屏-退出全屏
function toggleFullScreen(e) {
isFullscreen.value = !isFullscreen.value
getFullScreen(document.getElementById('flow-box'))
}
// 全屏
function fullele() {
return (
document.fullscreenElement ||
document.webkitFullscreenElement ||
document.msFullscreenElement ||
document.mozFullScreenElement ||
null
)
}
// 判断是否为全屏
function checkFull() {
return !!(document.webkitIsFullScreen || fullele())
}
// 全屏-退出全屏
function getFullScreen(el) {
if (isFullscreen.value) {
//退出全屏
if (document.exitFullscreen) {
document.exitFullscreen()
} else if (document.mozCancelFullScreen) {
document.mozCancelFullScreen()
} else if (document.webkitExitFullscreen) {
document.webkitExitFullscreen()
} else if (!document.msRequestFullscreen) {
document.msExitFullscreen()
}
} else {
//进入全屏
if (el.requestFullscreen) {
el.requestFullscreen()
} else if (el.mozRequestFullScreen) {
el.mozRequestFullScreen()
} else if (el.webkitRequestFullscreen) {
//改变平面图在google浏览器上面的样式问题
el.webkitRequestFullscreen()
} else if (el.msRequestFullscreen) {
isFullscreen.value = true
el.msRequestFullscreen()
}
}
}
// 下载为图片
function downloadChart(chartName) {
// 得到svg的真实大小
let svg = document.getElementById('mysvg')
let box = document.querySelector('svg').getBBox(),
x = box.x,
y = box.y,
width = box.width * 2,
height = box.height * 2
// 克隆svg
var node = svg.cloneNode(true)
//重新设置svg的width,height,viewbox
node.setAttribute('width', width * 2)
node.setAttribute('height', height * 2)
node.setAttribute('viewBox', [x, y, width, height])
downloadSvg(node, width, height, props.data.name)
}
// 下载
function downloadSvg(svg, width, height, rootName) {
var serializer = new XMLSerializer()
var source =
'<?xml version="1.0" standalone="no"?>\r\n' +
serializer.serializeToString(svg)
var image = new Image()
image.src = 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(source)
image.onload = function () {
var canvas = document.createElement('canvas')
canvas.width = width + 40
canvas.height = height + 40
var context = canvas.getContext('2d')
context.rect(0, 0, canvas.width, canvas.height)
context.fillStyle = '#fff'
context.fill()
context.drawImage(image, 20, 20)
var url = canvas.toDataURL('image/png')
var a = document.createElement('a')
a.download = `${rootName}-股权穿透图.png`
a.href = url
a.click()
return
}
}
</script>
<style lang="scss" scoped>
#flow-box {
background: #fff;
position: relative;
}
.tool-box {
padding: 5px;
position: absolute;
right: 10px;
bottom: 50%;
display: flex;
flex-direction: column;
background: #eee;
transition: all linear 0.3s;
border-radius: 10px;
.tool-item {
color: #333;
height: 36px;
width: 36px;
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
border-radius: 6px;
}
tool-item > img {
width: 100%;
height: 100%;
}
.tool-item:hover {
background: rgb(233, 233, 233);
}
}
</style>
组件调用
<template>
<D3Pane :data="chartData" />
</template>
<script setup lang="ts">
import D3Pane from './components/D3Pane.vue'
// 数据源
const chartData = ref({
id: '10001',
// 根节点名称
name: '广东XXX集团公司',
// 子节点列表
children: [
{
id: '10002',
name: '深圳市XXX投资有限公司',
percent: '100%', // 控股比例
rounds: '天使轮', // 融资轮次
},
{
id: '10003',
name: '深圳市XXX技术有限公司',
percent: '100%',
rounds: 'A轮',
},
{
id: '10004',
name: '深圳市XXX光伏材料有限公司',
percent: '100%',
rounds: '被收购',
off: true, // 吊销/注销
},
{
id: '10005',
name: '深圳市XXX医疗科技有限公司',
percent: '100%',
children: [
{
id: '10006',
name: '深圳市XXX医疗仪器有限公司',
percent: '100%',
children: [
{
id: '10007',
name: '深圳市XXX的子公司一',
percent: '80%',
},
{
id: '10008',
name: '深圳市XXX的子公司二',
percent: '90%',
},
{
id: '10009',
name: '深圳市XXX的子公司三',
percent: '100%',
},
],
},
],
},
{
id: '100010',
name: '上海XXX科技有限公司',
percent: '100%',
children: [
{
id: '100011',
name: '上海XXX通讯设备有限公司',
percent: '100%',
children: [
{
id: '100012',
name: '上海XXX的子公司一',
percent: '100%',
},
{
id: '100013',
name: '上海XXX的子公司二',
percent: '90%',
},
],
},
],
},
{
id: '100014',
name: '北京XXX科技有限公司',
percent: '100%',
children: [
{
id: '100015',
name: '北京XXX网络有限公司',
},
],
},
],
})
总结
摸索借鉴文档学习,到这里就解决了效果如图,通过这个效果学习了d3的强大功能之一~