Vue3中echarts力导向图的使用和简单配置

# Vue3中echarts力导向图的简单使用和配置

最近有Vue项目中使用到Echarts,做一个简单记录。

项目实现了一个显示全部节点和部分节点(根据节点长度进行过滤)的功能

做的时候写的一些思考也写在了注释里面

data.json 跟 https://cdn.jsdelivr.net/gh/apache/echarts-website@asf-site/examples/data/asset/data/les-miserables.json 一样,就不专门贴出来了

```

<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属性来表示不同节点与点击处节点的距离,具体看代码啦。

体验一下就知道效果了~

?著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,128评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,316评论 3 388
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事?!?“怎么了?”我有些...
    开封第一讲书人阅读 159,737评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,283评论 1 287
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,384评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,458评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,467评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,251评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,688评论 1 306
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,980评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,155评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,818评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,492评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,142评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,382评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,020评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,044评论 2 352

推荐阅读更多精彩内容