画画的baby~画了个树

521 阅读2分钟

背景

画个🌲用echarts和g2,d3不是有手就行?,嗯嗯好的行ok没问题嗯嗯好ok就这样,全剧终~ 但是要是节点间要有能标识流向的箭头呢。然后树的最长路径所经过的节点都在一条线上作为主轴呢,实际上要画一个树形路径图

技术选型

1.charts不支持
2.g2,有路径图,但是节点间不能用箭头标识关系流向
3.无敌d3,太麻烦啦,而且要处理动态数据的话比较费劲
4.g6,这个。。。可
5.html+css+svg+canvas,自己画(俺办不了~太太太太太麻烦)
结论: 采用g2提供的节点和边(箭头)进行绘制。但是有个关键点,节点的位置需要自己计算

支持功能

1.根据数据自动绘制(废话?)
2.播放暂停
3.重播
4.切换图形
5.放大缩小
6.事件传递

绝活展示

发错了,不是这个是下面这个嘿嘿

丑是丑了点,毕竟是个demo,实话告诉你,不光图有点丑,代码还没粗糙呢,也没封装啥的,但是!!!

关键算法

·树的深度优选和广度优先

		/**
		 * 改进更新: 主轴确认
		 * Max(child.length);
		 * 树的最大深度所经过的节点
		 * 
		 **/
		function getMainNodes(root) {
			const node = Array.isArray(root) ? root : [root];
			const result = [];
			const deepTraveler = (node, nodeName = '') => {
				if (!node) {
					return nodeName;
				}
				node.forEach(item => {
					const id = item.id + `|${nodeName}`;
					if (item.children) {
						const nodeIdList = deepTraveler(item.children, id);
						return id;
					}
					result.push(id);
				})
			}
			deepTraveler(node)
			return result.sort((a, b) => b.length - a.length)[0].split('|').reverse();
		}



		// 拿到深度测试
		let height = 0;
		const transfer = (root, mainNodes, deepHeight = 1000000) => {
			if (!root) return [];
			const result = [];
			const x = 500; // 横坐标
			const y = 100; // 纵坐标
			// 存储队列
			const queue = JSON.parse(JSON.stringify(root));
			// 深度
			let height = 0;
			console.log()

			while (queue.length && height < deepHeight) {
				height++;
				const level = queue.length; // 当前节点数
				let index = 0;
				console.log(queue[index].id)
				while (mainNodes[height] !== queue[index].id) {
					index++
				}
				for (let i = 0; i < level; i++) {
					const currentNode = queue.shift();
					if (currentNode.children) {
						queue.push(...currentNode.children);
					}
					currentNode.x = x + (i - index) * 80;
					currentNode.y = y * height;
					result.push({
						id: currentNode.id,
						label: currentNode.label,
						x: currentNode.x,
						y: currentNode.y
					});
				}
			}

			return result;
		}

引入g6

<script src="https://gw.alipayobjects.com/os/antv/pkg/_antv.g6-3.1.0/build/g6.js"></script>
<div id="mountNode"></div>
// 定义节点数据
const nodes = [{			id: '1',			label: '321312321',			children: [{				id: '2',				label: 'hhhh',				children: [{					id: '3',					label: '6adada',				},				{					id: '666',					label: 'hhhh',					children: [{						id: '12'					}]
				},
				{
					id: '4',
					label: 'hhhh',
				}
				]
			},
			{
				id: '5',
				label: 'hhhh',
				children: [{
					id: '6',
					label: 'hhhh',
				}, {
					id: '7',
					label: 'hhhh',
				}, {
					id: '8',
					label: 'hhhh',
				}]
			}
			]
		}];
		const nodes2 = [{			id: '1',			label: '321312321',			children: [{				id: '9',				label: 'hhhh',			}, {				id: '10'			}, {				id: '11',				children: [{					id: '12'				}]
			}]
		}]
		// 定义数据源
		const edges = {
			// 边集
			edges: [
				// 表示一条从 node1 节点连接到 node2 节点的边
				{
					source: '1',
					target: '2',
					label: 'wwdasas'
				},
				{
					source: '2',
					target: '1'
				},
				{
					source: '1',
					target: '3'
				},
				{
					source: '2',
					target: '4'
				},
				{
					source: '2',
					target: '5'
				},
				{
					source: '4',
					target: '8'
				},
				{
					source: '4',
					target: '6'
				},
				{
					source: '4',
					target: '9'
				},
				{
					source: '7',
					target: '8'
				},
			]
		};

全部代码

<!DOCTYPE html>
<html lang="en">
	<head>
		<meta charset="UTF-8">
		<title>Tutorial Demo</title>
		<style type="text/css">
			.btn::before {
				content: attr(data);

			}
		</style>
	</head>
	<body>
		<div id="mountNode"></div>

		<button>点击切换</button>
		<div data="播放" class="btn"></div>

		<input type="button" name="" id="" value="重播" />
		<script src="https://gw.alipayobjects.com/os/antv/pkg/_antv.g6-3.1.0/build/g6.js"></script>

		<script>
			const nodes = [{
				id: '1',
				label: '321312321',
				children: [{
						id: '2',
						label: 'hhhh',
						children: [{
								id: '3',
								label: '6adada',
							},
							{
								id: '666',
								label: 'hhhh',
								children: [{
									id: '12'
								}]
							},
							{
								id: '4',
								label: 'hhhh',
							}
						]
					},
					{
						id: '5',
						label: 'hhhh',
						children: [{
							id: '6',
							label: 'hhhh',
						}, {
							id: '7',
							label: 'hhhh',
						}, {
							id: '8',
							label: 'hhhh',
						}]
					}
				]
			}];
			const nodes2 = [{
				id: '1',
				label: '321312321',
				children: [{
					id: '9',
					label: 'hhhh',
				}, {
					id: '10'
				}, {
					id: '11',
					children: [{
						id: '12'
					}]
				}]
			}]

			/**
			 * 改进更新: 主轴确认
			 * Max(child.length);
			 * 树的最大深度所经过的节点
			 * 
			 **/
			function getMainNodes(root) {
				const node = Array.isArray(root) ? root : [root];
				const result = [];
				const deepTraveler = (node, nodeName = '') => {
					if (!node) {
						return nodeName;
					}
					node.forEach(item => {
						const id = item.id + `|${nodeName}`;
						if (item.children) {
							const nodeIdList = deepTraveler(item.children, id);
							return id;
						}
						result.push(id);
					})
				}
				deepTraveler(node)
				return result.sort((a, b) => b.length - a.length)[0].split('|').reverse();
			}



			// 拿到深度测试
			let height = 0;
			const transfer = (root, mainNodes, deepHeight = 1000000) => {
				if (!root) return [];
				const result = [];
				const x = 500; // 横坐标
				const y = 100; // 纵坐标
				// 存储队列
				const queue = JSON.parse(JSON.stringify(root));
				// 深度
				let height = 0;
				console.log()

				while (queue.length && height < deepHeight) {
					height++;
					const level = queue.length; // 当前节点数
					let index = 0;
					console.log(queue[index].id)
					while (mainNodes[height] !== queue[index].id) {
						index++
					}
					for (let i = 0; i < level; i++) {
						const currentNode = queue.shift();
						if (currentNode.children) {
							queue.push(...currentNode.children);
						}
						currentNode.x = x + (i - index) * 80;
						currentNode.y = y * height;
						result.push({
							id: currentNode.id,
							label: currentNode.label,
							x: currentNode.x,
							y: currentNode.y
						});
					}
				}

				return result;
			}

			// 定义数据源
			const edges = {
				// 边集
				edges: [
					// 表示一条从 node1 节点连接到 node2 节点的边
					{
						source: '1',
						target: '2',
						label: 'wwdasas'
					},
					{
						source: '2',
						target: '1'
					},
					{
						source: '1',
						target: '3'
					},
					{
						source: '2',
						target: '4'
					},
					{
						source: '2',
						target: '5'
					},
					{
						source: '4',
						target: '8'
					},
					{
						source: '4',
						target: '6'
					},
					{
						source: '4',
						target: '9'
					},
					{
						source: '7',
						target: '8'
					},
				]
			};
			const mainNodes = getMainNodes(nodes);
			let data = Object.assign(edges, {
				nodes: transfer(nodes, mainNodes)
			});
			let change = 0;
			let graph = null;
			let timer = null;
			graph = renderTool(data);
			graph.render();
			const countObj = {
				count: 0
			}
			// 自动播放
			function start() {
				timer = setInterval(() => {
					const mainNodes = getMainNodes(nodes);
					let len = mainNodes.length;
					if (countObj.count > len) {
						clearInterval(timer);
						return;
					}
					if (graph) {
						graph.destroy();
						graph = null;
					}
					const data = Object.assign(edges, {
						nodes: transfer(nodes, mainNodes, countObj.count)
					});
					graph = renderTool(data);
					graph.render();
					countObj.count = countObj.count + 1;
				}, 1000);
			}
			document.querySelector('input').onclick = function() {
				this.setAttribute('data', '暂停');
				clearInterval(timer)
				start();
				countObj.count = 0;

			}
			document.querySelector('.btn').onclick = function() {
				const data = this.getAttribute('data');
				let text = '';
				if (data == '播放') {
					text = '暂停';
					start(countObj.count);
				} else {
					text = '播放';
					clearInterval(timer);

				}
				this.setAttribute('data', text);

			}



			//切换
			document.querySelector('button').onclick = function() {
				if (graph) {
					graph.destroy();
					graph = null;
				}
				change = change ? 0 : 1;
				const node = change ? nodes2 : nodes;
				const mainNodes = getMainNodes(node);
				data = Object.assign(edges, {
					nodes: transfer(node, mainNodes)
				});
				graph = renderTool(data);
				graph.render();
			}
			// 绘制
			function renderTool(data) {
				// 创建 G6 图实例
				graph = new G6.Graph({
					container: 'mountNode', // 指定图画布的容器 id,与第 9 行的容器对应
					// 画布宽高
					width: 1000,
					height: 600,
					modes: {
						default: ['zoom-canvas'],
					},
					defaultEdge: {
						//shape: 'cubic', // 指定边的形状为二阶贝塞尔曲线
						style: {
							endArrow: true,
							stroke: '#e2e2e2',
						},
						labelCfg: {
							refY: 30,
						},
					},
				});

				// 读取数据
				graph.data(data);
				// 渲染图
				graph.on('node:click', ev => {
					const shape = ev.target;
					const node = ev.item;
				});
				return graph;
			}
		</script>
	</body>
</html>