if
(source?.children?.length) {
showType ===
"down"
?
this
.setNewData(
this
.oldTreeItem, source.nodeId, [])
:
this
.setNewDataUp(
this
.oldTreeItem, source.nodeId, []);
source.children = [];
setTimeout(() => {
this
.init2(
this
.oldTreeItem);
},
0
);
}
else
{
if
(!source.
data
.children.length && source.
data
.investment) {
const
data
= {
id: source.
data
.id,
nodeType: showType ===
"up"
?
1
:
2
,
queryNode(
data
).then((res) => {
showType ===
"down"
?
this
.setNewData(
this
.oldTreeItem, source.nodeId, res.
data
)
:
this
.setNewDataUp(
this
.oldTreeItem, source.nodeId, res.
data
);
source.children = res.
data
;
setTimeout(() => {
this
.init2(
this
.oldTreeItem);
},
0
);
setNewData(oldTree, nodeId,
data
) {
if
(!(oldTree.children instanceof Array))
return
null
;
for
(let i of oldTree.children) {
if
(i.nodeId === nodeId) {
i.children = JSON.parse(JSON.stringify(
data
));
return
;
}
else
{
this
.setNewData(i, nodeId,
data
);
setNewDataUp(oldTree, nodeId,
data
) {
if
(oldTree.nodeId === nodeId) {
oldTree.parents = JSON.parse(JSON.stringify(
data
));
}
else
{
let temptree = oldTree?.parents?.length
? oldTree.parents
: oldTree.children;
for
(let i of temptree) {
if
(i.nodeId === nodeId) {
i.children = JSON.parse(JSON.stringify(
data
));
return
;
}
else
{
this
.setNewDataUp(i, nodeId,
data
);
2. 根节点不能展开收起操作
d3.hierarchy
– 从给定的层次结构数据构造一个根节点并为各个节点指定深度等属性.
treeItem 数据举例
rootUp,rootDown 数据举例
初始化的时候,将全部数据分为
up
,
down
两个类型,在最终渲染后,根节点的type就是
down
。
所以无法区分根节点类型,添加不了扩展符号。
init2(treeItem) {
this.oldTreeItem = JSON.parse(JSON.stringify(treeItem))
let upTree = null
let downTree = null
// 拷贝树的数据
Object.keys(treeItem).map((item) => {
if (item === "parents") {
upTree = JSON.parse(JSON.stringify(treeItem))
upTree.children = treeItem[item]
upTree.parents = null
} else if (item === "children") {
downTree = JSON.parse(JSON.stringify(treeItem))
downTree.children = treeItem[item]
downTree.parents = null
// hierarchy 返回新的结构 x0,y0初始化起点坐标
this.rootUp = d3.hierarchy(upTree, (d) => d.children
this.rootUp.x0 = 0
this.rootUp.y0 = 0
this.rootDown = d3.hierarchy(downTree, (d) => d.children)
this.rootDown.x0 = 0
this.rootDown.y0 = 0
// 上 和 下 结构
let treeArr = [
data: this.rootUp,
type: "up",
data: this.rootDown,
type: "down",
treeArr.map((item) => {
// 控制展示根节点上下两层数据
item.data.data.children.forEach(this.collapse)
// 渲染
this.update(item.data, item.type, item.data)
结论:遇到根节点,如果有可扩展值就默认多展示一层,不去操作这个特殊的根节点。
3. 缩放拖拽抖动问题
根本原因是原来使用的方法在缩放时去记录了原始位置,对原位置进行操作,但拖拽时图片会从当前画布的(0,0)坐标开始滑动,视觉上给人一种抖动的感觉。
this.zoom = d3.zoom()
.scaleExtent([1, 3])
.on("zoom", (e) => {
this.scaleData = d3.event.transform.k;
this.svg.attr(
"transform",
d3.event.transform.translate(svgW / 2, svgH / 2)
this.svg.call(this.zoom);
setRange(type) {
this.zoom.scaleBy(this.svg, type === "big" ? 1.2 : 0.8);
存在的问题:当缩放范围最小值小于1时,拖拽图片图片会在原地轻微抖动
解决方法:缩放最小值设置为1。
4. 图片下载不全
zoom有缩放移动功能,导致下载的图片有2种情况:
下载当前视图的svg(只下载页面呈现部分)
无视缩放,移动位置,下载整个svg
缩放问题引起的缩放值取值不正确,导致下载图片,等比缩放时比例错误,下载图片不全。
参数zoomClassName
: 元素g
, 包含可缩放移动的数据。
svgDownloadAll(svg, zoomClassName) {
//得到svg的真实大小
var box = svg.getBBox(),
x = box.x,
y = box.y,
width = box.width,
height = box.height
if (zoomClassName) {
//查找zoomObj
var zoomObj = svg.getElementById(zoomClassName.replace(/\./g, ""))
if (!zoomObj) {
alert("zoomObj不存在")
return false
/*------这里是处理svg缩放的--------*/
var transformMath = zoomObj.getAttribute("transform"),
scaleMath = zoomObj.getAttribute("transform")
if (transformMath || scaleMath) {
var transformObj = transformMath.match(
/translate\(([^,]*),([^,)]*)\)/
if (transformObj) {
// 原缩放,移动值 反应用到svg的宽高上
var translateX = transformObj[1],
translateY = transformObj[2],
scale = this.scaleData
x = (x - translateX) / scale
y = (y - translateY) / scale
width = width / Number(scale)
height = height / Number(scale)
//克隆svg
var node = svg.cloneNode(true)
//重新设置svg的width,height,viewbox
node.setAttribute("width", width)
node.setAttribute("height", height)
node.setAttribute("viewBox", [x, y, width, height])
if (zoomClassName) {
var zoomObj1 = node.getElementById(zoomClassName.replace(/\./g, ""))
/*-------------清除缩放元素的缩放-------------*/
zoomObj1.setAttribute("transform", "translate(0,0) scale(1)")
this.downloadSvgFn(node)
下载流程:
将svg转成字符串 点击查看
转换之后,将svg字符串变成image的src
用canvas绘制image
将dataurl 转成 blob
模拟点击事件,下载blob
downloadSvgFn(svg) {
let that = this;
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 width = this.naturalWidth,
height = this.naturalHeight;
var canvas = document.createElement("canvas");
canvas.width = width;
canvas.height = height;
var context = canvas.getContext("2d");
context.rect(0, 0, width, height);
context.fillStyle = "#fff";
context.fill();
context.drawImage(image, 0, 0);
var imgSrc = canvas.toDataURL("image/jpg", 1);
that.base64DownloadFile(imgSrc);
base64DownloadFile(content) {
let aLink = document.createElement("a");
let blob = this.base64ToBlob(content);
let evt = document.createEvent("HTMLEvents");
evt.initEvent("click", true, true);
aLink.download = "股权架构";
aLink.href = URL.createObjectURL(blob);
aLink.click();
base64ToBlob(code) {
let parts = code.split(";base64,");
let contentType = parts[0].split(":")[1];
let raw = window.atob(parts[1]);
let rawLength = raw.length;
let uInt8Array = new Uint8Array(rawLength);
for (let i = 0; i < rawLength; ++i) {
uInt8Array[i] = raw.charCodeAt(i);
return new Blob([uInt8Array], { type: contentType });
5. 下载图片的连接线过粗
设置一下fill-opacity
线条透明度即可。
6. 全屏与退出全屏
handleFullScreen() {
let element = document.documentElement;
if (this.fullscreen) {
if (document.exitFullscreen) {
document.exitFullscreen();
} else if (document.webkitCancelFullScreen) {
document.webkitCancelFullScreen();
} else if (document.mozCancelFullScreen) {
document.mozCancelFullScreen();
} else if (document.msExitFullscreen) {
document.msExitFullscreen();
} else {
if (element.requestFullscreen) {
element.requestFullscreen();
} else if (element.webkitRequestFullScreen) {
element.webkitRequestFullScreen();
} else if (element.mozRequestFullScreen) {
element.mozRequestFullScreen();
} else if (element.msRequestFullscreen) {
element.msRequestFullscreen();
this.fullscreen = !this.fullscreen;
7. 连接线被遮挡问题(曲线替换为直线)
最开始使用的是贝塞尔曲线,因为随着层级加深,倾斜角度也越来越高,线条会被矩形模块遮挡,所以替换了直线展示。
M x y : 移动到指定坐标,x,y分别为轴坐标点,即为起点
C x1 y1 x2 y2 x y : 三次贝塞尔曲线
当前点为起点,xy为终点,起点和x1y1控制曲线起始的斜率,终点和x2y2控制结束的斜率。
diagonal (s, d, showtype) {
let path
if (showtype === 'up') {
path = `M ${s.x} ${-s.y + 24}
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;
L x y :在初始位置(M 画的起点)和xy确定的坐标画一条线。
diagonal(s, d, showtype) {
let path;
const halfDistance = (d.y - s.y) / 2;
const halfY = s.y + halfDistance;
if (showtype === "up") {
path = `M${s.x} ${-s.y + 30}
L${s.x},${-halfY} L${d.x},${-halfY} L${d.x}, ${-d.y}`;
} else {
path = `M${s.x} ${s.y}
L${s.x},${halfY} L${d.x},${halfY} L${d.x},${d.y}`;
return path;
svg path路径指令
8. 弹窗展示公司信息
点击公司,展示公司详情。
核心知识点:svgRect.getBoundingClientRect()
showDialog(d) {
if (d.data.subjectType === 1 || d.data.subjectType === 3) {
const FormDatas = new FormData();
FormDatas.append("id", d.data.id);
this.$axios({
url: "/equityrelation/queryCompanyInfoByNodeId",
method: "post",
data: FormDatas,
}).then((res) => {
if (res.data.code === 200) {
const svgRect = document.querySelector(`rect[type="${d.nodeId}"]`);
var rt = svgRect.getBoundingClientRect();
document
.getElementById("dialog")
.setAttribute(
"style",
`display:block;top:${rt.bottom}px;left:${rt.right}px`
const {
companyName,
companyZhName,
companyRegNo,
registerDate,
registerAmount,
registerCurrencyCode,
auth,
} = res.data.data;
this.dialogData = {
auth,
companyName,
companyZhName,
companyRegNo,
registerDate,
registerAmount,
registerCurrencyCode,
} else {
this.$message.error(res.data.message);
四、从0开发一个股权穿透图
举例的代码是来自 d3 股权穿透图干货这篇内容,运行可直接看效果,写的很好。
相关知识点:
d3.tree – 创建一个新的整齐(同深度节点对齐)的树布局.
d3.zoom – 创建一个缩放交互.
d3.select – 从文档中选取一个元素.
d3.hierarchy – 从给定的层次结构数据构造一个根节点并为各个节点指定深度等属性.
zoom.scaleExtent – 设置可缩放系数大小.
transform.translate – 根据指定的值平移当前坐标变换.
transition.remove – 在过渡结束后移除选中的元素.
tree.nodeSize – 设置节点尺寸.
tree.separation – 设置两个相邻的节点之间的间距.
node.descendants – 从当前节点开始返回其后代节点数组.
selection.selectAll -从每个被选中的元素中选择多个后代元素.
selection.data – 将元素与数据绑定.
selection.enter – 获取需要插入的选择集(数据个数大于元素个数)的占位符.
selection.exit – 获取多余的元素的选择集(数据个数小于元素个数).
selection.on – 添加或移除事件监听器.
selection.attr – 设置或获取属性.
selection.style – 获取或设置样式属性.
selection.text – 设置或获取文本内容.
selection.append – 创建、添加并返回一个新的元素.
selection.insert – 创建、插入并返回一个新的元素.
selection.remove – 从文档中移除元素.
transition.duration – 指定每个过渡元素的过渡时间(毫秒).
<template>
<div class="penetrate-chart">
<div class="bt-group">
<button class="reset" @click="resetSvg">重置</button>
</div>
</div>
</template>
<script>
const DURATION = 0
const SYMBOLA_S_R = 9
const COMPANY = 0
const PERSON = 1
export default {
props: {},
components: {},
data () {
return {
layoutTree: '',
diamonds: '',
d3: this.$d3,
i: 0,
hasChildNodeArr: [],
originDiamonds: '',
diagonalUp: '',
diagonalDown: '',
tree: {"name":"多多包","children":[{"name":"一卡通公司","type":0},{"name":"一卡通公司2","type":0,"children":[{"name":"小公司","type":0,"children":[{"name":"小小小","type":0,"children":[{"type":1,"name":"笑小下"}]}]},{"type":0,"name":"小公司2"}]},{"name":"一卡通公司2333","type":0,"children":[{"type":0,"name":"小公司"},{"type":0,"name":"小公司2"}]},{"type":0,"name":"一卡通公司2222"}],"parents":[{"name":"大公司","type":0,"children":[{"name":"发发委","type":0,"money":"780万元","children":[{"type":0,"money":"780万元","name":"123"}]},{"name":"123发发委","money":"780万元","type":0,"children":[{"money":"780万元","type":0,"name":"123"}]}]},{"name":"多多网","money":"780万元","type":0,"children":[{"type":0,"money":"780万元","name":"发哈哈"}]},{"name":"龙龙投资","money":"780万元","type":0,"children":[{"type":1,"money":"780万元","name":"王林"},{"type":1,"money":"780万元","name":"张峰"},{"type":1,"money":"780万元","name":"侯明"}]}]},
rootUp: '',
rootDown: '',
svg: ''
mounted () {
this.init()
methods: {
init () {
let d3 = this.d3
let svgW = document.body.clientWidth
let svgH = 500
this.diamonds = {
w: 145,
h: 68,
intervalW: 200,
intervalH: 150
this.originDiamonds = {
w: 190
this.layoutTree = d3.tree().nodeSize([this.diamonds.intervalW, this.diamonds.intervalH]).separation(() => 1);
this.svg = d3.select('#app').append('svg').attr('width', svgW).attr('height', svgH).attr('id', 'treesvg')
.call(d3.zoom().scaleExtent([0, 5]).on('zoom', () => {
this.svg.attr('transform', d3.event.transform.translate(svgW / 2, svgH / 2));
.attr('style', 'position: relative;z-index: 2;')
.append('g').attr('id', 'g').attr('transform', 'translate(' + (svgW / 2) + ',' + (svgH / 2) + ')');
let upTree = null
let downTree = null
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
this.rootUp = d3.hierarchy(upTree, d => d.children);
this.rootUp.x0 = 0
this.rootUp.y0 = 0
this.rootDown = d3.hierarchy(downTree, d => d.children);
this.rootDown.x0 = 0
this.rootDown.y0 = 0;
let treeArr = [
data: this.rootUp,
type: 'up'
data: this.rootDown,
type: 'down'
treeArr.map(item => {
item.data.children.forEach(this.collapse);
this.update(item.data, item.type, item.data)
*[update 函数描述], [click 函数描述]
* @param {[Object]} source 第一次是初始源对象,后面是点击的对象
* @param {[String]} showtype up表示向上 down表示向下
* @param {[Object]} sourceTree 初始源对象
update (source, showtype, sourceTree) {
let _this = this
if (source.parents === null) {
source.isOpen = !source.isOpen
let nodes
if (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 * this.diamonds.intervalH;
let node = this.svg.selectAll('g.node' + showtype)
.data(nodes, d => d.id || (d.id = showtype + ++this.i));
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 + ')')
nodeEnter.append('rect')
.attr('type', d => d.id)
.attr('width', d => d.depth ? this.diamonds.w : this.originDiamonds.w)
.attr('height', d => d.depth ? (d.type === COMPANY ? this.diamonds.h : this.diamonds.h - 10) : 30)
.attr('x', d => d.depth ? -this.diamonds.w / 2 : -this.originDiamonds.w / 2)
.attr('y', d => d.depth ? showtype === 'up' ? -this.diamonds.h / 2 : 0 : -15)
.attr('stroke', d => d.data.type === COMPANY || !d.depth ? '#FD7D00' : '#7A9EFF')
.attr('stroke-width', 1)
.attr('rx', 5)
.attr('ry', 5)
.style('fill', d => {
if (d.data.type === COMPANY || !d.depth) {
return d._children ? '#FFF1D7' : (d.depth ? '#fff' : '#FD7D00')
} else if (d.data.type === PERSON) {
return d._children ? '#fff' : (d.depth ? '#fff' : '#7A9EFF')
nodeEnter.append('circle')
.attr('type', d => d.id || (d.id = showtype + 'text' + ++this.i))
.attr('r', (d) => d.depth ? (this.hasChildNodeArr.indexOf(d) === -1 ? 0 : SYMBOLA_S_R) : 0)
.attr('cy', d => d.depth ? showtype === 'up' ? -(SYMBOLA_S_R + this.diamonds.h / 2) : this.diamonds.h : 0)
.attr('cx', 0)
.attr('fill', d => d.children ? '#fff' : '#FD7D00')
.attr('stroke', d => d._children || d.children ? '#FD7D00' : '')
.on('click', function (d) {
_this.click(d, showtype, sourceTree)
setTimeout(() => {
if (document.querySelector(`text[type="${d.id}"]`).innerHTML === '-') {
d.isOpen = false
this.innerHTML = '+'
this.setAttribute('fill', '#FD7D00')
document.querySelector(`text[type="${d.id}"]`).setAttribute('fill', '#fff')
document.querySelector(`rect[type="${d.id}"]`).setAttribute('style', 'fill:#FFF1D7')
document.querySelector(`text[type="${d.id}"]`).innerHTML = '+'
} else {
d.isOpen = true
this.setAttribute('fill', '#fff')
document.querySelector(`text[type="${d.id}"]`).setAttribute('fill', '#FD7D00')
document.querySelector(`rect[type="${d.id}"]`).setAttribute('style', 'fill:#fff')
document.querySelector(`text[type="${d.id}"]`).innerHTML = '-'
}, DURATION)
nodeEnter.append('g')
.attr('transform', () => 'translate(0,0)')
.append('text')
.attr('class', d => !d.depth ? 'proportion-hide' : 'proportion')
.attr('x', d => d.x > 0 ? (showtype === 'up' ? -30 : 30) : 30)
.attr('y', showtype === 'up' ? this.diamonds.h : -20)
.attr('text-anchor', 'middle')
.attr('fill', d => d.data.type === COMPANY ? '#FD7D00' : '#7A9EFF')
.text(d => '30%');
nodeEnter.append('text')
.attr('class', 'text-style-name')
.attr('x', 0)
.attr('y', showtype === 'up' ? -this.diamonds.h / 2 : 0)
.attr('dy', d => d.depth ? (d.data.name.length > 9 ? '1.5em' : '2em') : '.3em')
.attr('text-anchor', 'middle')
.attr('fill', d => d.depth ? '#465166' : '#fff')
.text(d => (d.data.name.length > 9) ? d.data.name.substr(0, 9) : d.data.name);
nodeEnter.append('text')
.attr('class', 'text-style-name')
.attr('x', 0)
.attr('y', showtype === 'up' ? -this.diamonds.h / 2 : 0)
.attr('dy', d => d.depth ? '3em' : '.3em')
.attr('text-anchor', 'middle')
.attr('fill', d => d.depth ? '#465166' : '#fff')
.text(d => d.data.name.substr(9, d.data.name.length));
nodeEnter.append('text')
.attr('class', 'text-style-money')
.attr('x', 0)
.attr('y', showtype === 'up' ? -this.diamonds.h / 2 : 0)
.attr('dy', d => d.data.name.substr(9, d.data.name.length).length ? '5em' : '4em')
.attr('text-anchor', 'middle')
.attr('fill', d => d.depth ? '#465166' : '#fff')
.text(d => d.data.money);
* 绘制箭头
* @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' ? '-5' : '15')
.attr('stroke-width', 2)
.attr('fill', 'red')
.append('path')
.attr('d', 'M0,-5L10,0L0,5')
.attr('fill', '#FD7D00');
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' ? '-5' : '15')
.attr('stroke-width', 2)
.attr('fill', 'red')
.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) + ')');
nodeEnter.append('svg:text')
.attr('type', d => d.id || (d.id = showtype + 'text' + ++this.i))
.on('click', function (d) {
_this.click(d, showtype, sourceTree)
setTimeout(() => {
if (this.innerHTML === '-') {
d.isOpen = false
this.innerHTML = '+'
this.setAttribute('fill', '#fff')
document.querySelector(`circle[type="${d.id}"]`).setAttribute('fill', '#FD7D00')
document.querySelector(`rect[type="${d.id}"]`).setAttribute('style', 'fill:#FFF1D7')
} else {
d.isOpen = true
this.innerHTML = '-'
this.setAttribute('fill', '#FD7D00')
document.querySelector(`circle[type="${d.id}"]`).setAttribute('fill', '#fff')
document.querySelector(`rect[type="${d.id}"]`).setAttribute('style', 'fill:#fff')
}, DURATION)
.attr('x', 0)
.attr('dy', d => d.depth ? (showtype === 'up' ? -(SYMBOLA_S_R / 2 + this.diamonds.h / 2) : this.diamonds.h + 4) : 0)
.attr('text-anchor', 'middle')
.attr('fill', d => d._children ? '#fff' : '#FD7D00')
.text(d => this.hasChildNodeArr.indexOf(d) !== -1 ? (source.depth && d.isOpen ? '-' : '+') : '');
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.id);
let linkEnter = link.enter().insert('path', 'g')
.attr('class', 'link' + showtype)
.attr('marker-start', d => `url(#${showtype}resolved${d.data.type})`)
.attr('stroke', d => d.data.type === COMPANY ? '#FD7D00' : '#7A9EFF')
.style('fill-opacity', 1)
.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 });
collapse (source) {
if (source.children) {
source._children = source.children;
source._children.forEach(this.collapse);
source.children = null;
this.hasChildNodeArr.push(source);
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;
this.update(source, showType, sourceTree)
diagonal (s, d, showtype) {
let path
if (showtype === 'up') {
path = `M ${s.x} ${-s.y + 24}
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;
resetSvg () {
this.d3.select('#treesvg').remove()
this.init()
</script>
<style lang="scss">
.penetrate-chart {
.bt-group{
position: fixed;
z-index: 999;
right: 15px;
bottom: 15px;
button{
width:88px;
height:32px;
display: block;
border-radius:18px;
font-size:14px;
font-family:PingFangSC-Medium;
font-weight:500;
line-height:20px;
.save{
background:rgba(255,168,9,1);
color:rgba(255,255,255,1);
.reset{
margin-top: 8px;
color: rgba(255, 168, 9, 1);
background: white;
border:1px solid rgba(255,168,9,1);
#treesvg{
display: block;
margin: auto;
.linkup, .linkdown {
fill: none;
stroke-width: 1px;
.text-style-name{
font-size:12px;
font-family:PingFangSC-Medium;
font-weight:500;
.text-style-money{
font-size:10px;
font-family:PingFangSC-Regular;
font-weight:400;
color:rgba(70,81,102,1)
.proportion{
font-size:10px;
font-family:PingFangSC-Regular;
font-weight:400;
.proportion-hide, .hide-node{
display: none;
</style>
参考文档:
d3.js官网,
d3 股权穿透图干货,
d3.js中svg的下载,
5分钟看懂svg path路径的所有命令