Vue3 中 echarts 力导向图的简略应用和配置
最近有 Vue 我的项目中应用到 Echarts,做一个简略记录。
我的项目实现了一个显示全副节点和局部节点(依据节点长度进行过滤)的性能
做的时候写的一些思考也写在了正文外面
data.json 跟 https://cdn.jsdelivr.net/gh/a… 一样,就不专门贴出来了
<template>
<div id="graph" style="width: 100%; height: 100%">
<div style="padding-top: 15px">
<el-checkbox v-model="config.showAll"> 显示全副 </el-checkbox>
</div>
<div>
<span v-if="!config.showAll"> 节点长度 </span
><el-input-number
size="mini"
v-if="!config.showAll"
v-model="config.length"
:min="1"
:max="10"
/>
</div>
<div>
<el-button size="mini" @click="drawImage()"> 点击重绘 </el-button>
</div>
<div id="chart" ref="scatterMap" class="chart-wrapper" />
</div>
</template>
<script lang="ts">
import * as echarts from "echarts";
import {defineComponent, onMounted, ref, reactive} from "vue";
import cloneDeep from "lodash/cloneDeep";
import {data} from "./data.js";
export default defineComponent({
name: "echarts",
props: {},
setup() {function getCenterPoint() {
return {
x: scatterMap.value.clientWidth / 2,
y: scatterMap.value.clientHeight / 2,
};
}
const config = reactive({
color: 0,
colorOptions: [{ label: "计划一", value: 0},
{label: "计划二", value: 1},
{label: "计划三", value: 2},
],
size: 1,
sizeOptions: [{ label: "小", value: 0},
{label: "中", value: 1},
{label: "大", value: 2},
],
length: 1,
showAll: true,
id: "0", // 随便设置一个, 初始化时依据参数重置
});
const lastNodes = ref();
function setNodes(id, length, position, links) {let existNodes = [id];
// 目标:查找某节点左近的 ID,如果之前曾经查找过,则过滤掉
function findNearNodes(id) {let nodesID = [];
const tempNodeID = [];
// 依据已有的 ID 查找 target 和 source 所对应的 ID
links.forEach((item) => {if (item.source === id) {tempNodeID.push(item.target);
}
if (item.target === id) {tempNodeID.push(item.source);
}
});
// 先剔除本身反复,即查找进去的 nodesID 的反复项
nodesID = [...new Set(tempNodeID)];
// 剔除曾经查找过的点 []
const res = nodesID.filter(function (v) {return existNodes.indexOf(v) == -1;
});
// 把新找进去的点,加到曾经存在的 list 中 existNodes
existNodes = existNodes.concat(res);
// 返回曾经剔除进去的新找进去的节点
return res;
}
const nodeLevel = [];
for (let i = 0; i < length + 1; i++) {nodeLevel.push([]);
}
let res = [];
function setNodeLevel(nodes, levelIndex) {nodes.forEach((item) => {nodeLevel[levelIndex].push(item);
});
// 查找节点左近的点
let nearNodes = [];
nodeLevel[levelIndex].forEach((item) => {nearNodes = nearNodes.concat(findNearNodes(item));
});
if (levelIndex < length) {setNodeLevel(nearNodes, levelIndex + 1);
} else {
// 依据这个 nodeLevel 设置对应 id 的 category 属性
const visibleNodesId = nodeLevel.flat(); // 去括号,获取所有要显示的节点 ID
let coypNodes = cloneDeep(data.nodes);
const visibleNodes = coypNodes.filter((item) => {return visibleNodesId.indexOf(item.id) !== -1;
});
nodeLevel.forEach((nodeIds, index) => {nodeIds.forEach((nodeId) => {
// 多级遍历设置对应的 category 属性
visibleNodes.forEach((node) => {if (node.id === nodeId) {
node.category = index + 1;
node.symbolSize = 15;
}
if (node.id === id) {
node.x = position.x;
node.y = position.y;
node.fixed = true;
}
});
});
});
// 增加上次 查问进去的点
// 遍历 上次查问进去的点
res = visibleNodes;
}
}
// 示例,第一个节点 id 为 '1'
setNodeLevel([id], 0);
return res;
}
function setConfig(nodeId, position) {let nodes = cloneDeep(data.nodes);
// link 能够不扭转,然而 category 要扭转,cloneDeep 避免笼罩
// 是否显示全副,不然有的节点永远选不到,必定要有这么一个选项
// 即:显示全副、显示以后节点
// 如果显示全副节点 category 如何设置?随机!// 如果节点局部设置,选中点 category 为 1,其余依照链路累加
if (config.showAll) {nodes.forEach(function (node) {
node.symbolSize = 15;
node.category = Math.floor(Math.random() * 8);
});
} else {nodes = setNodes(nodeId, config.length, position, data.links);
}
// TODO
// handle 解决 要显示的 node 跟上一次的进行合并
const visibleNodes = cloneDeep(nodes);
// const visibleNodesId = visibleNodes.map((item) => {
// return item.id;
// });
// 把上次的节点也显示进去
// lastNodes.value.forEach((item) => {// if (visibleNodesId.indexOf(item.id) === -1) {
// item.category = 0;
// item.symbolSize = 15;
// visibleNodes.push(item);
// }
// });
// 把以后查出来的 nodes 保留到 lastNodes 中
lastNodes.value = cloneDeep(nodes);
// nodes 跟上次的合并解决
const options = {
title: {
text: "",
subtext: "",
top: "bottom",
left: "right",
},
tooltip: {},
series: [
{// edgeSymbol:["circle","arrow"],
// edgeSymbolSize:10,
name: "Les Miserables",
type: "graph",
layout: "force",
draggable: true,
data: visibleNodes,
links: data.links,
categories: data.categories,
roam: true,
label: {
position: "right",
show: false, // 默认显示 label
formatter: function (params) {
// 连
if (params.data.source) {
// 留神判断,else 是将节点的文字也初始化成想要的格局
return (
params.data.source +
"是【" +
params.data.target +
"】的居间人"
);
} else {return params.name;}
},
clolr: "#fff", // label 色彩,
fontSize: 12, // 字体大小
},
force: {
edgeLength: 160, // TODO 能够由用户设置 边的两个节点之间的间隔,这个间隔也会受 repulsion。// 反对设置成数组表白边长的范畴,此时不同大小的值会线性映射到不同的长度。值越小则长度越长。repulsion: 100, // 节点之间的斥力因子。// 反对设置成数组表白斥力的范畴,此时不同大小的值会线性映射到不同的斥力。值越大则斥力越大
gravity: 0.1, // 节点受到的向核心的引力因子。该值越大节点越往中心点聚拢。},
},
],
};
return options;
}
// 三组配色计划 ok
// 三组间距,球大小 ok
// 点击后,小球居中 ok
// 呈现问题:拖拽后小球地位主动挪动,产生偏移
// filter 链路可调
// 箭头双向过滤
// 对于色彩的应用:因为总色彩无限,中心点设为 category 为 1,其余的顺次相加,0 作为默认色彩
let myChart = ref(null);
const scatterMap = ref();
const initEcharts = async () => {myChart.value = echarts.init(scatterMap.value);
lastNodes.value = data.nodes;
draw();
config.id = data.nodes[0].id;
myChart.value.on("click", function (params) {
// 点击节点时,才会触发绘图
if (params.dataType === "node") {
// 设置以后选中点
config.id = params.data.id;
draw();}
});
};
function drawImage() {draw();
}
function draw() {const position = getCenterPoint();
if (config.showAll) {const option = setConfig(0, position);
myChart.value.setOption(option);
} else {
// TODO 设置是否居中,居中的话不便查看,不居中的话可能会导致局部节点不显示?maybe
const option = setConfig(config.id, position);
// 居中事实的话 setoption 第二个参数为 true
myChart.value.setOption(option);
}
}
onMounted(() => {initEcharts();
});
return {
drawImage,
scatterMap,
myChart,
initEcharts,
config,
};
},
});
</script>
<style scoped>
.chart-wrapper {
width: 100%;
height: 600px;
}
</style>
实现成果
过滤成果
通过设置节点的 category 属性来示意不同节点与点击处节点的间隔,具体看代码啦。
体验一下就晓得成果了~