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属性来表示不同节点与点击处节点的距离,具体看代码啦。
体验一下就知道效果了~

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值