可视化—AntV G6实现节点连线及展开收缩分组

本文介绍如何使用 AntV G6 数据可视化图表库创建流程与关系分析图,并通过 Vue 组件实现分组节点的动态展示与交互,包括分组的形状切换、展开与收缩效果。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

AntV 是蚂蚁金服全新一代数据可视化解决方案,主要包含数据驱动的高交互可视化图形语法G2,专注解决流程与关系分析的图表库 G6、适于对性能、体积、扩展性要求严苛的场景。

demo使用数字模拟真实的节点及分组数据。combo内的nodes亦是使用随机数生成,节点之前的连线edges由节点从小到大依次连接 ,大家在用的时候,可以注意一下连线对象的 sourcetarget 两个属性即可

安装模块依赖 :npm install @antv/g6

main.js 中引入,并绑定Vue原型方法

import G6 from '@antv/g6';
import Vue from 'vue';

Vue.prototype.G6 = G6;

创建Graph.vue

<template>
  <div>
    <div id="drawGraph"></div>
  </div>
</template>

<script>
let graphG = null
export default {
  mounted() {
    this.initData();
  },
  methods: {
    initData() {
      let combos = [
        { id: '100-600', label: '100-600', type: 'root' },
        { id: '100-200', label: '100-200' },
        { id: '200-300', label: '200-300' },
        { id: '300-400', label: '300-400' },
        { id: '400-500', label: '400-500' },
        { id: '500-600', label: '500-600' },
      ]
      let edges = [
        { source: '100-200', target: '100-600' },
        { source: '200-300', target: '100-600' },
        { source: '300-400', target: '100-600' },
        { source: '400-500', target: '100-600' },
        { source: '500-600', target: '100-600' },
      ]
      let data = { combos, edges }
      this.makeRelationData(data);
    },
    // 分组 点 连线处理
    makeRelationData(data) {
      if (graphG) {
        graphG.destroy();
      }
      let drawGraph = document.getElementById("drawGraph");
      this.graphWidth = drawGraph.scrollWidth;
      this.graphHeight = drawGraph.scrollHeight || 1200;
      let origin = [this.graphWidth / 2, 100];
      let row = 150, clo = 180;
      let combos = data.combos
      let row_clo = Math.floor(Math.sqrt(combos.length));
      for (let i = 0; i < combos.length; i++) {
        let rowindex = Math.floor(i / row_clo) + 1;
        let cloindex = (i % row_clo) + 1;
        // 分组默认样式设置
        if (combos[i].type === 'root') {
          combos[i].x = this.graphWidth / 3
          combos[i].y = this.graphHeight / 3
          combos[i].style = {
            fill: "#B19693",
            opacity: 0.4,
            cursor: "pointer",
          };
        } else {
          // 分组定位
          combos[i].x = origin[0] + clo * cloindex;
          combos[i].y = origin[1] + row * rowindex;
          if (i % 2 === 1) {
            combos[i].y += 40;
          }
          combos[i].style = {
            fill: "#FAD069",
            opacity: 0.5,
            cursor: "pointer",
          }
        }
      }
      this.drawQfast(data)
    },
    drawQfast(data) {
      graphG = new this.G6.Graph({
        container: "drawGraph",
        width: this.graphWidth,
        height: this.graphHeight,
        groupByTypes: false,
        modes: {
          default: [
            { type: "zoom-canvas", enableOptimize: true, optimizeZoom: 0.2 },
            { type: "drag-canvas", enableOptimize: true },
            { type: "drag-node", enableOptimize: true, onlyChangeComboSize: true },
            { type: "drag-combo", enableOptimize: true, onlyChangeComboSize: true },
            { type: "brush-select", enableOptimize: true },
          ],
        },
        defaultEdge: {
          type: 'cubic-horizontal',
          lineWidth: 1,
          style: {
            endArrow: true,
            stroke: "#FAD069",
          },
        },
        edgeStateStyles: {
          hover: {
            lineWidth: 2,
          },
        },
        defaultNode: {
          type: "circle",
          size: 15,
          labelCfg: {
            position: "bottom",
            style: {
              fontSize: 15,
            },
          },
        },
        defaultCombo: {
          type: "circle",
          opacity: 0,
          lineWidth: 1,
          collapsed: true,
          labelCfg: {
            position: "top",
            refY: 5,
            style: {
              fontSize: 16,
            },
          },
        },
      });
      graphG.data(data);
      graphG.render(); // 渲染图
      graphG.zoom(0.8);  // 如果觉得节点大,可以缩放整个图
      graphG.on("edge:mouseenter", (e) => {
        graphG.setItemState(e.item, "hover", true);
      });

      graphG.on("edge:mouseleave", (e) => {
        graphG.setItemState(e.item, "hover", false);
      });

      graphG.on("combo:dblclick", (e) => {
        e.item._cfg.model.type = e.item._cfg.model.type === "rect" ? "circle" : "rect";  // 分组形状,方圆切换
        e.item._cfg.model.labelCfg.refY = e.item._cfg.model.type === "rect" ? -20 : 5;   // 切换形状,改变label定位

        const comboId = e.item._cfg.model.id
        graphG.collapseExpandCombo(comboId);

        // 分组收缩时,删除分组内的连线和节点
        if (e.item._cfg.model.collapsed) {   // 收缩 
          let newedges = e.item.getEdges();
          let newNodes = e.item.getNodes();
          for (let j = 0; j < newedges.length; j++) {
            graphG.removeItem(newedges[j]);
          }
          for (let i = 0; i < newNodes.length; i++) {
            graphG.removeItem(newNodes[i]);
          }
          data.edges.forEach(edge => {
            graphG.addItem("edge", edge);
          });
        } else {   // 展开
          // 分组展开时, 添加节点和连线,并给分组内的节点 添加位置信息
          let origin = [e.item._cfg.model.x, e.item._cfg.model.y];  // 获取当前分组combs的坐标
          let row = 110, clo = 150;
          // 生成(10-20)随机数个 随机数 模拟展开分组内的节点
          let randomCount = Math.floor(Math.random() * 10) + 10;
          let row_clo = Math.floor(Math.sqrt(randomCount));
          let nodes = []
          for (let i = 0; i < randomCount; i++) {
            let min = comboId.split('-')[0] - 0
            let max = comboId.split('-')[1] - 0
            let randomNum = Math.floor(Math.random() * (max - min)) + min;
            if (nodes.indexOf(randomNum) > -1) {
              i--
              continue;
            }
            nodes.push(randomNum)
            let rowindex = Math.floor(i / row_clo);
            let cloindex = i % row_clo;
            let y = origin[1] + row * rowindex
            let node = {
              label: randomNum,
              id: randomNum.toString(),
              comboId: comboId,
              style: {
                fillOpacity: 0.5,
                cursor: "pointer",
                fill: randomNum % 5 == 0 ? "#81C7D4" : "#986DB2"
              },
              x: origin[0] + clo * cloindex,
              y: i % 2 == 0 ? y + 40 : y
            }
            graphG.addItem("node", node); // 将节点添加至分组 
          }     
          nodes.sort((a, b) => a - b)  // 将分组内的数字排序,从小到大依次连接,模拟真实数据
          for (let i = 0; i < nodes.length - 1; i++) {
            let edge = {
              source: nodes[i].toString(),
              target: nodes[i + 1].toString(),
              lineWidth: 1,
              style: {
                lineDash: [2, 2],
                lineWidth: 0.5,
                stroke: "#00AA90"
              }
            }
            graphG.addItem("edge", edge);  // 添加连线   将分组内的数字排序,从小到大依次连接
          }
        }
      });
    },
  }
};
</script>

收缩图:

展开图:

要搭建一个基于servlet-jdbc与MySql的教师工资管理系统,首先需要理解系统的基本架构和开发环境配置。系统通常由前端页面、后端逻辑以及数据库三个部分构成。前端负责展示信息和用户交互,后端处理业务逻辑,数据库存储数据。 参考资源链接:[JavaEE教师工资管理系统开发:基于servlet-jdbc与MySql](https://wenku.csdn.net/doc/3bt1wqp7r2?spm=1055.2569.3001.10343) 在开发环境搭建方面,推荐使用MyEclipse作为IDE工具,它提供了一套完整的开发环境,包括了对Java EE的支持和对Tomcat的集成。MyEclipse对servlet和jdbc有良好的支持,可以方便地编写和调试代码。接下来,需要配置Tomcat服务器,它是一个轻量级的Web服务器,用于部署和运行Java Web应用。对于数据库的搭建,使用MySQL,作为开源的关系型数据库管理系统,它支持SQL语言,并提供了稳定的数据库服务。 在项目构建上,建议采用MVC(Model-View-Controller)设计模式,将数据模型、业务逻辑和用户界面分离,以提高代码的可维护性和可扩展性。前端使用JSP技术来动态生成网页内容,而后端的servlet充当控制器的角色,处理来自前端的请求,并与Model进行交互,Model部分则涉及与MySql数据库通过jdbc进行数据交互的代码。 具体到代码层面,你需要创建一个servlet类来处理HTTP请求,通过jdbc与MySql数据库建立连接,执行SQL语句进行数据的增删改查操作。例如,创建一个TeacherServlet类,用于处理教师信息的添加、查询、更新和删除操作,这些操作最终都需要与数据库进行交互。 此外,为保证数据的一致性和安全性,在实际开发中,还需要考虑事务管理、错误处理以及SQL注入防护等安全问题。 《JavaEE教师工资管理系统开发:基于servlet-jdbc与MySql》一书提供了从系统设计到实现的详细指南,适合希望全面掌握开发过程的读者。通过学习这本书,你可以获得更深入的理解和实践中的操作指导。 参考资源链接:[JavaEE教师工资管理系统开发:基于servlet-jdbc与MySql](https://wenku.csdn.net/doc/3bt1wqp7r2?spm=1055.2569.3001.10343)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值