d3.js官网:https://d3js.org/what-is-d3
下载
d3.v7.js:https://d3js.org/d3.v7.js
d3.v7.min.js:https://d3js.org/d3.v7.min.js
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>股权穿透图</title>
<script src="d3.min.js"></script>
</head>
<body>
<div id="appc" style="height: 650px"></div>
<script>
window.onload = function() {
constructor();
};
var originTreeData = null;
var el = null;
var config = null;
var svg = null;
var gAll = null;
var gLinks = null;
var gNodes = null;
var tree = null;
var rootOfDown = null;
var rootOfUp = null;
var data = {
id: "abc1005",
// 根节点名称
name: "山东吠舍科技有限责任公司",
// 子节点列表
children: [{
id: "abc1006",
name: "山东第一首陀罗科技服务有限公司",
percent: "100%",
},
{
id: "abc1007",
name: "山东第二首陀罗程技术有限公司",
percent: "100%",
},
{
id: "abc1008",
name: "山东第三首陀罗光伏材料有限公司",
percent: "100%",
},
{
id: "abc1009",
name: "山东第四首陀罗科技发展有限公司",
percent: "100%",
children: [{
id: "abc1010",
name: "山东第一达利特瑞利分析仪器有限公司",
percent: "100%",
children: [{
id: "abc1011",
name: "山东瑞利的子公司一",
percent: "80%",
},
{
id: "abc1012",
name: "山东瑞利的子公司二",
percent: "90%",
},
{
id: "abc1013",
name: "山东瑞利的子公司三",
percent: "100%",
},
],
}, ],
},
{
id: "abc1014",
name: "山东第五首陀罗电工科技有限公司",
percent: "100%",
children: [{
id: "abc1015",
name: "山东第二达利特低自动化设备有限公司",
percent: "100%",
children: [{
id: "abc1016",
name: "山东敬业的子公司一",
percent: "100%",
},
{
id: "abc1017",
name: "山东敬业的子公司二",
percent: "90%",
},
],
}, ],
},
{
id: "abc1020",
name: "山东第六首陀罗分析仪器(集团)有限责任公司",
percent: "100%",
children: [{
id: "abc1021",
name: "山东第三达利特分气体工业有限公司",
}, ],
},
],
// 父节点列表
parents: [{
id: "abc2001",
name: "山东刹帝利集团有限责任公司",
percent: "60%",
parents: [{
id: "abc2000",
name: "山东婆罗门集团有限公司",
percent: "100%",
}, ],
},
{
id: "abc2002",
name: "吴小远",
percent: "40%",
parents: [{
id: "abc1010",
name: "山东第一达利特瑞利分析仪器有限公司",
percent: "100%",
parents: [{
id: "abc1011",
name: "山东瑞利的子公司一",
percent: "80%",
},
{
id: "abc1012",
name: "山东瑞利的子公司二",
percent: "90%",
},
{
id: "abc1013",
name: "山东瑞利的子公司三",
percent: "100%",
},
],
}, ],
},
{
id: "abc2003",
name: "测试数据",
percent: "40%"
}
]
};
// 股权树
function constructor(options) {
// 树的源数据
originTreeData = data;
// 宿主元素选择器
el = document.getElementById("appc");
// 一些配置项
config = {
// 节点的横向距离
dx: 200,
// 节点的纵向距离
dy: 170,
// svg的viewBox的宽度
width: 0,
// svg的viewBox的高度
height: 500,
// 节点的矩形框宽度
rectWidth: 170,
// 节点的矩形框高度
rectHeight: 70,
};
svg = null;
gAll = null;
gLinks = null;
gNodes = null;
// 给树加坐标点的方法
tree = null;
// 投资公司树的根节点
rootOfDown = null;
// 股东树的根节点
rootOfUp = null;
drawChart({
type: "fold",
});
}
// 初始化树结构数据
function drawChart(options) {
// 宿主元素的d3选择器对象
let host = d3.select(el);
// 宿主元素的DOM,通过node()获取到其DOM元素对象
let dom = host.node();
// 宿主元素的DOMRect
let domRect = dom.getBoundingClientRect();
// svg的宽度和高度
config.width = domRect.width;
config.height = domRect.height;
let oldSvg = d3.select("svg");
// 如果宿主元素中包含svg标签了,那么则删除这个标签,再重新生成一个
if (!oldSvg.empty()) {
oldSvg.remove();
}
const svg2 = d3
.create("svg")
.attr("viewBox", () => {
let parentsLength = originTreeData.parents ?
originTreeData.parents.length :
0;
return [
-config.width / 2,
// 如果有父节点,则根节点居中,否则根节点上浮一段距离
parentsLength > 0 ?
-config.height / 2 :
-config.height / 3,
config.width,
config.height,
];
})
.style("user-select", "none")
.style("cursor", "move");
// 包括连接线和节点的总集合
const gAll2 = svg2.append("g").attr("id", "all");
svg2
.call(
d3.zoom()
.scaleExtent([0.2, 5])
.on("zoom", (e) => {
gAll2.attr("transform", () => {
return `translate(${e.transform.x},${e.transform.y}) scale(${e.transform.k})`;
});
})
)
.on("dblclick.zoom", null); // 取消默认的双击放大事件
gAll = gAll2;
// 连接线集合
gLinks = gAll2.append("g").attr("id", "linkGroup");
// 节点集合
gNodes = gAll2.append("g").attr("id", "nodeGroup");
// 设置好节点之间距离的tree方法
tree = d3.tree().nodeSize([config.dx, config.dy]);
rootOfDown = d3.hierarchy(
originTreeData,
(d) => d.children
);
rootOfUp = d3.hierarchy(originTreeData, (d) => d.parents);
tree(rootOfDown);
[rootOfDown.descendants(), rootOfUp.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;
}
}
});
}
);
//箭头(下半部分)
svg2
.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"); //箭头颜色
//箭头(上半部分)
svg2
.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"); //箭头颜色
svg = svg2;
update();
// 将svg置入宿主元素中
host.append(function() {
return svg2.node();
});
}
// 更新数据
function update(source) {
if (!source) {
source = {
x0: 0,
y0: 0,
};
// 设置根节点所在的位置(原点)
rootOfDown.x0 = 0;
rootOfDown.y0 = 0;
rootOfUp.x0 = 0;
rootOfUp.y0 = 0;
}
let nodesOfDown = rootOfDown.descendants().reverse();
let linksOfDown = rootOfDown.links();
let nodesOfUp = rootOfUp.descendants().reverse();
let linksOfUp = rootOfUp.links();
tree(rootOfDown);
tree(rootOfUp);
const myTransition = svg.transition().duration(500);
/*** 绘制子公司树 ***/
const node1 = gNodes
.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.rectWidth;
})
.attr("height", (d) => {
if (d.depth === 0) {
return 30;
}
return config.rectHeight;
})
.attr("x", (d) => {
if (d.depth === 0) {
return (-(d.data.name.length + 2) * 16) / 2;
}
return -config.rectWidth / 2;
})
.attr("y", (d) => {
if (d.depth === 0) {
return -15;
}
return -config.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 5;
}
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");
// 副标题
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) + "...";
}
return subTitle;
}
})
.style("font-size", (d) => 14)
.style("font-family", "黑体")
.style("font-weight", "bold");
// 控股比例
node1Enter
.append("text")
.attr("class", "percent")
.attr("x", (d) => {
return 12;
})
.attr("y", (d) => {
return -45;
})
.text((d) => {
if (d.depth !== 0) {
return d.data.percent;
}
})
.attr("fill", "#000000")
.style("font-family", "黑体")
.style("font-size", (d) => 14);
// 增加展开按钮
const expandBtnG = node1Enter
.append("g")
.attr("class", "expandBtn")
.attr("transform", (d) => {
return `translate(${0},${config.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
.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
.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.rectWidth;
})
.attr("height", (d) => {
if (d.depth === 0) {
return 30;
}
return config.rectHeight;
})
.attr("x", (d) => {
if (d.depth === 0) {
return (-(d.data.name.length + 2) * 16) / 2;
}
return -config.rectWidth / 2;
})
.attr("y", (d) => {
if (d.depth === 0) {
return -15;
}
return -config.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 5;
}
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");
// 副标题
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");
// 控股比例
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", "#000000")
.style("font-family", "黑体")
.style("font-size", (d) => 14);
// 增加展开按钮
const expandBtnG2 = node2Enter
.append("g")
.attr("class", "expandBtn")
.attr("transform", (d) => {
return `translate(${0},${-config.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
.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 ? "-" : "+";
});
rootOfDown.eachBefore((d) => {
d.x0 = d.x;
d.y0 = d.y;
});
rootOfUp.eachBefore((d) => {
d.x0 = d.x;
d.y0 = d.y;
});
}
// 直角连接线 by wushengyuan
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);
}
</script>
</body>
</html>