d3 v7 股权穿透图

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>
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值