背景
画个🌲用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>