• 数据可视化 gojs 实践关系图 demo:节点分组


    本文是关于如何使用可视化库 gojs 完成节点分组关系展示的,从零基础到实现最终效果。希望对使用 gojs 的小伙伴有帮助。

    1. 节点分组需求及 demo 展示

    demo

    需求
    • 能正确展示组的层次,以及节点之间的关系。
    • 单选节点、多选节点,获取到节点信息
    • 选中组,能选中组中的节点,能获取到组中的节点信息
    • 选中节点,当前节点视为根节点,能选中根节点连线下的所有节点,并获取到节点信息

    2. 准备

    • 从后端获取到的接口数据:
    const data = {
      "properties": [
        { "key": "t-2272", "parentKey": "j-1051", "name": "哈哈" },
        { "key": "p-344", "parentKey": "g--1586357764", "name": "test" },
        { "key": "t-2271", "parentKey": "j-1051", "name": "查询" },
        { "key": "t-2275", "parentKey": "j-1052", "name": "开开心心" },
        { "key": "j-1054", "parentKey": "p-344", "name": "嘻嘻" },
        { "key": "t-2274", "parentKey": "j-1052", "name": "查询" },
        { "key": "j-1051", "parentKey": "p-444", "name": "hello" },
        { "key": "j-1052", "parentKey": "p-444", "name": "编辑" },
        { "key": "t-2281", "parentKey": "j-1054", "name": "嘻嘻" },
        { "key": "p-444", "parentKey": "g--1586357624", "name": "test" },
        { "key": "g--1586357624", "name": "数据组1" },
        { "key": "g--1586357764", "name": "数据组2" },
        { "key": "t-2273", "parentKey": "j-1051", "name": "新建" }
      ],
      "dependencies": [
        { "sourceKey": "t-2272", "targetKey": "t-2274" },
        { "sourceKey": "t-2274", "targetKey": "t-2275" },
        { "sourceKey": "t-2273", "targetKey": "t-2272" },
        { "sourceKey": "t-2271", "targetKey": "t-2272" },
        { "sourceKey": "t-2272", "targetKey": "t-2281" }
      ]
    }
    

    3. 实现步骤1:数据组建

    • gojs 图表实例所需数据结构如下:

      diagram.model = new go.GraphLinksModel(
          [ // node data
              { key: "A"},
              { key: "F", group: "Omega"},
              { key: "G"},
              { key: "Chi", isGroup: true },
          ],
          [  // link data
              { from: "A", to: "A" },
              { from: "F", to: "G" },
              { from: "G", to: "Chi"}
          ]
      );
      
    • 根据接口数据构建出的最终数据如下:

      node

    [
      { "key": "g--1586357624", "text": "数据组1", "type": "g", "isGroup": true },
      { "key": "p-444", "text": "test", "type": "p", "isGroup": true, "group": "g--1586357624" },
      { "key": "j-1051", "text": "hello", "type": "j", "isGroup": true, "group": "p-444" },
      { "key": "t-2272", "text": "哈哈", "type": "t", "group": "j-1051" },
      { "key": "t-2271", "text": "查询", "type": "t", "group": "j-1051" },
      { "key": "t-2273", "text": "新建", "type": "t", "group": "j-1051" },
      { "key": "j-1052", "text": "编辑", "type": "j", "isGroup": true, "group": "p-444" },
      { "key": "t-2275", "text": "开开心心", "type": "t", "group": "j-1052" },
      { "key": "t-2274", "text": "查询", "type": "t", "group": "j-1052" },
      { "key": "g--1586357764", "text": "数据组2", "type": "g", "isGroup": true },
      { "key": "p-344", "text": "test", "type": "p", "isGroup": true, "group": "g--1586357764" },
      { "key": "j-1054", "text": "嘻嘻", "type": "j", "isGroup": true, "group": "p-344" },
      { "key": "t-2281", "text": "嘻嘻", "type": "t", "group": "j-1054" }
    ]
    

    link

    [
      { "from": "t-2272", "to": "t-2274", "nextLinks": [ "t-2274", "t-2275" ] },
      { "from": "t-2274", "to": "t-2275", "nextLinks": [ "t-2275" ] },
      { "from": "t-2273", "to": "t-2272", "nextLinks": [ "t-2272", "t-2274", "t-2275" ] },
      { "from": "t-2271", "to": "t-2272", "nextLinks": [ "t-2272", "t-2274", "t-2275" ] },
      { "from": "t-2272", "to": "t-2281", "nextLinks": [ "t-2281" ] }
    ]
    

    如何根据接口数据组装出所需数据就不介绍了。text字段用于显示组及节点的标题,nextLinks是为后面做选中当前节点,能选中节点连线下的所有节点做数据准备。

    大家如果感兴趣,可以先不读后面的,自己根据组装出的数据自己实现下后面的交互。

    4. 实现步骤2:构建图表容器、实例,自定义布局、节点、连线、组的样式等属性。

    • 容器
    <div class="diagram" id="diagram"></div>  
    
    • 去除水印、画布蓝色边框,参考前篇
    • 构建图表实例
    import * as go from './go-module.js';
    
    const $ = go.GraphObject.make;
    
        const diagram = $(go.Diagram,
            'diagram', // diagram 绘图容器的 id
            {
              layout: $(go.TreeLayout, // 布局方式
                {
                  angle: 90, // 自上而下,0 从左到右
                  arrangement: go.TreeLayout.ArrangementHorizontal
                }
              )
            }
        );
    
    • 自定义节点、连线、组
    const config = {
      borderColor: '#d1d9e2',
      groupTextColor: '#444',
      nodeTextColor: '#585858',
      linkColor: '#666',
      selectedLinkColor: '#2090ff',
    }
    

    图表颜色值统一管理。

    定义节点

        diagram.nodeTemplate = $(go.Node,
            "Auto",
            $(go.Shape, "Rectangle", // 节点形状:矩形
              { stroke: config.borderColor, // 边框颜色
                strokeWidth: 1, // 边框宽度
                fill: "white", // 形状填充颜色
              },
            ),
            $(go.TextBlock, // 节点文本
              { margin: 4,
                stroke: config.nodeTextColor // 文本颜色
              },
              new go.Binding("text", "text"), // 将 model 中的 text 属性进行绑定,用于节点显示文本
            ),
            { doubleClick: nodeDblClick, // 节点双击事件,选中节点下的所有节点
            },
        );
    

    定义边

        diagram.linkTemplate = $(go.Link,
            {
              curve: go.Link.Bezier // 贝塞尔曲线
            },
            // 连线
            $(go.Shape, { name: 'link', strokeWidth: 1, stroke: config.linkColor }),
            // 连线的箭头
            $(go.Shape, { name: 'linkArrow', toArrow: "OpenTriangle", stroke: config.linkColor })
        );
    

    定义组

        diagram.groupTemplate = $(go.Group,
            "Auto",
            { // 定义分组的内部布局
              layout: $(go.TreeLayout,
                { angle: 90, arrangement: go.TreeLayout.ArrangementHorizontal }),
              isSubGraphExpanded: false, // 默认展开true、折叠false
              // 分组单击事件
              click: (e, group) => {
                // todo 实现组选中,选中组中所有节点
              }
            },
            $(go.Shape, // 定义分组形状及描述
              "Rectangle",
              {
                parameter1: 14,
                fill: "rgba(2, 153, 255, .2)", // 填充色
                stroke: config.borderColor, // 边框色
                strokeWidth: 1,
              },
            ),
            $(go.Panel, "Vertical",
              { defaultAlignment: go.Spot.Left, margin: 4 },
              $(go.Panel, "Horizontal",
                { defaultAlignment: go.Spot.Top, margin: 4 },
                $("SubGraphExpanderButton"), // 设置收缩按钮,用于展开折叠子图
                $(go.TextBlock, // 定义文本
                  {
                    alignment: go.Spot.TopLeft,
                    font: "Bold 12px Sans-Serif",
                    stroke: config.groupTextColor,
                  },
                  new go.Binding("text"), // 将 model 中的 text 属性进行绑定,用于节点显示文本
                )
              ),
              // 创建占位符来表示组内容所在的区域
              $(go.Placeholder, { padding: new go.Margin(5, 10) })
            )
        )
    
    • 绑定数据
    diagram.model = new go.GraphLinksModel(
        [], // nodes
        [] // links
    )
    

    5. 实现步骤3:交互处理

    • 选中分组交互相对简单,就不附上代码了。

    • 选中节点,选中节点连线下的所有节点

        function nodeDblClick (e, node) {
          // 遍历每一条边进行设置
          let goneNodes = []; // 记录遍历过的,避免再次遍历它
          const forEdges = (edges, isSelected) => {
            edges.forEach(edge => {
              if (edge && edge.nextLinks) { // 当前节点下面有多个节点
                edge.nextLinks.forEach((id, i) => {
                  if (!goneNodes.includes(id)) { // 避免遍历过的
                    goneNodes.push(id);
                    const node = diagram.findNodeForKey(id);
                    node.isSelected = isSelected;
                    highlightLink(node, node.isSelected);
                    // 递归设置节点连线上下游的每一个节点选中及连线高亮,linkArr 为前面组装出的图的边数据
                    forEdges(linkArr.filter(e => e.from === id), isSelected)
                  }
                })
              }
            })
          }
          
          const {key: nodeId} = node.data;
          // 存在多条边,linkArr 为前面组装出的图的边数据
          const edges = linkArr.filter(e => e.from === nodeId);
          // 先清除上次高亮的连线
          clearHightLink();
          // 高亮当前节点的连线
          highlightLink(node, node.isSelected);
          
          // 循环设置当前节点连线上下游的每一个节
          点选中及连线高亮
          forEdges(edges, node.isSelected);
        }
    

    优化:思路:先统计数据,再对统计数据进行UI处理。职责分明,增强可读性。

    function nodeDblClick (e, node) {
      // 遍历每一条边
      let allNodes = []; // 统计连线上的所有节点
      const forEdges = (edges) => {
        edges.forEach(edge => {
          if (edge && edge.nextLinks) { // 当前节点下面有多个节点
            edge.nextLinks.forEach((id, i) => {
              allNodes.push(id);
              // 递归节点连线上下游的每一个节点,linkArr 为前面组装出的图的边数据
              forEdges(linkArr.filter(e => e.from === id))
            })
          }
        })
      }
     
      const {key: nodeId} = node.data;
      // 存在多条边,linkArr 为前面组装出的图的边数据
      const edges = linkArr.filter(e => e.from === nodeId);
      // 循环统计当前节点连线上下游的每一个节点
      forEdges(edges);
    
      // 先清除上次高亮的连线
      clearHightLink();
      
      // 先统计出所有的,再去重,再对节点进行处理
      // 设置统计出的连线上的所有节点及边高亮
      allNodes.push(nodeId);
      allNodes = [...new Set(allNodes)]; // 去重
      allNodes.forEach(id => {
        const node = diagram.findNodeForKey(id);
        node.isSelected = true;
        highlightLink(node, true);
      })
    }
    

    clearHightLink 方法:清除连线高亮

    highlightLink 方法:根据node获取到对应的id找出node的出去的线设置颜色等高亮。

    最后

    完成这个效果难点在哪,我自己的感受是:

    • 组装数据:如何组装出图表所需的数据,特别是选中节点要选中节点连线下的所有节点,怎么组装数据才方便后面的处理。
    • 在自定义布局、节点、连线、组属性及样式上,特别是细节处理,需要大量翻看文档指南或api,查看案例是等,确定哪个属性的哪个值改了才是需要的。
    • 在做交互时,需要理清思路,看文档事件相关的部分。特别难的是,节点信息打印出来查看时,看不到具体的,只能看出来是 迭代器,可以遍历,但看不出具体的数据,只能通过相应 api 才能得到。

    关于本文的代码,只放了核心部分的。

    最后的最后,有不到位的地方或者错误的地方,亦或是更好的意见,欢迎指出。

    非常感谢!!!

  • 相关阅读:
    用Repeater控件显示主从关系数据表
    transactsql高级查询(上)
    利用动态加载模板,配合ajax实现无刷新操作
    如何使用Repeater控件的模板
    总结一下DataGrid,DataList,Repeater
    ajax实现dropdownlist与datagrid或Repeater无联动刷新
    Asp.net2.0 VS 2005下的repeater控件本功能分页实例(共有 条记录 共有几页 当前第 页 首页,上一页,下一页,尾页 DropDownList跳转)
    SQLSERVER存储过程
    我的java 的实用代码
    各种数据库对应的jar包、驱动类名和URL格式
  • 原文地址:https://www.cnblogs.com/EnSnail/p/12444499.html
Copyright © 2020-2023  润新知