GoJs关系图的全部路径和最短路径

863 阅读3分钟

前言

上篇文章中说到了节点和节点之间的关系用连线来串联,然后他们之间的关系用文字或者其他绘图模板进行信息说明。但是如果两个节点之间不是直接关系,需要查找间接关系的话。就需要对中间经过的路线进行一个标识,从而知道两个节点的间接关系,称之为路径。

准备工作

我们需要准备一些节点和连线,然后准备全部路径和最短路径的按钮。由于我们为了突出路径,因此在连线上就不添加文字说明了。

//data
nodes:[
      { key: "1", color: "#99FFFF",text:"三国",figure:"Rectangle" },
      { key: "1-1", color: "#FF0000",text:"魏",figure:"Circle" },
      { key: "1-1-1", color: "#FF0000",text:"曹操",figure:"Circle" },
      { key: "1-1-2", color: "#FF0000",text:"荀彧",figure:"Circle" },
      { key: "1-1-3", color: "#FF0000",text:"许褚",figure:"Circle" },
      { key: "1-2", color: "#FFFF66",text:"蜀",figure:"Circle"},
      { key: "1-2-1", color: "#FFFF66",text:"刘备",figure:"Circle"},
      { key: "1-2-2", color: "#FFFF66",text:"诸葛亮",figure:"Circle"},
      { key: "1-2-3", color: "#FFFF66",text:"关羽",figure:"Circle"},
      { key: "1-3", color: "#0000FF",text:"吴",figure:"Circle" },
      { key: "1-3-1", color: "#0000FF",text:"孙权",figure:"Circle" },
      { key: "1-3-2", color: "#0000FF",text:"周瑜",figure:"Circle" },
      { key: "1-3-3", color: "#0000FF",text:"鲁肃",figure:"Circle" },
    ],
    links:[
      {
        from:"1",
        to:"1-1"
      },
      {
        from:"1-1",
        to:"1-1-1"
      },
      {
        from:"1-1",
        to:"1-1-2"
      },
      {
        from:"1-1",
        to:"1-1-3"
      },
      {
        from:"1",
        to:"1-2"
      },
      {
        from:"1-2",
        to:"1-2-1"
      },
      {
        from:"1-2",
        to:"1-2-2"
      },
      {
        from:"1-2",
        to:"1-2-3"
      },
      {
        from:"1",
        to:"1-3"
      },
      {
        from:"1-3",
        to:"1-3-1"
      },
      {
        from:"1-3",
        to:"1-3-2"
      },
      {
        from:"1-3",
        to:"1-3-3"
      },
      {
        from:"1-1-2",
        to:"1-2-2"
      },
      {
        from:"1-2-2",
        to:"1-3-2"
      },
    ]
    //methods
    this.myDiagram.nodeTemplate =
      $$(go.Node, "Vertical",
      $$(go.Shape, "Circle",
          { width: 30, height: 30,name:"ICON" },
          new go.Binding("fill", "color"),
          new go.Binding("figure", "figure"),
        ),
      $$(go.TextBlock, 
          new go.Binding("text", "text")),
     );

image.png

为了展示路径的不同,这次示例选用了力导向布局,并且增加了好几组的相互之间的关系。 首先我们需要对选中的节点进行一个获取,然后才能对选中的节点处理对应的路径。 选中方法如下

getSelectedNodes() {
  var selectObjIterator = this.myDiagram.selection.iterator;
  var resultNodes = [];
  while (selectObjIterator.next()) {
    var selectedObj = selectObjIterator.value;
    if (selectedObj instanceof go.Node) {
      resultNodes.push(selectedObj);
    }
  }
  return resultNodes;
},

路径上的选择连线高亮

获取到选中的节点之后,然后可以对到时候需要最短路径或者全部路径进行一个高亮的显示,其方法如下。

// 路径高亮显示
lightPath(path, color) {
  var nodesLen = path.length,
    mapKey = {},
    links = [];
  for (var i = 0; i < nodesLen - 1; i++) {
    for (var j = 1 + i; j < nodesLen; j++) {
      var node1 = path[i],
        node2 = path[j],
        linksb = node1.findLinksBetween(node2);
      while (linksb.hasNext()) {
        var link = linksb.value;
        links.push(link);
      }
    }
  }
  this.lightLinks(links, color);
},
// 高亮一组关系
lightLinks(links, color) {
  var len = links.length;
  for (var i = 0; i < len; i++) {
    var link = links[i];
    this.lightLink(link, color);
  }
},
// 高亮一条关系
lightLink(link, color) {
  if (link instanceof go.Link) {
    var linkData = link.data;
    if (color) {
      linkData.bgColor = color;
    } else {
      linkData.bgColor = "#DB5C2F";
    }
    link.updateTargetBindings();
  }
},

获取到需要处理的路径上的连线可以根据bgColor的属性进行一个颜色的更换,然后通过updateTargetBindings来更新连线更改显示属性变化之后的的颜色变化,然后由高亮一条关系的的方法之后可以更新出获取一组关系的方法。然后我们可以根据获取的到的路径path来处理到底哪些关系需要显示高亮。其对应的处理方法如上面所示。

清除高亮显示连线

因为有了显示关系的高亮,因此在处理下一个需要高亮的关系的时候,需要把之前的已经选择高亮的的连线给清除掉,然后才能去处理下一个问题。 其代码如下

// 清除路径
clearAllLinkLight() {
  var allLinks = this.myDiagram.links;
  while (allLinks.next()) {
    var link = allLinks.value;
    this.clearLinkLight(link);
  }
},
//清除连线高亮
clearLinkLight(link) {
  if (link instanceof go.Link) {
    var linkData = link.data;
    linkData.weight = 3;
    linkData.bgColor = "";
    link.selected = false;
    link.updateTargetBindings();
  }
},

清除路径和清除连线高亮的方法和选择增加的时候相似,都是获取到对应的需要修改的路径连线然后修改bgColor之后再更新连线的数据。

全部路径

准备好上面的这些处理方法之后,就可以对全部路径信息进行处理了。其代码如下

// 全部路径的点击方法
lightAllRelationPath() {
  if (!this.allpath) {
    var selectedNodes = this.getSelectedNodes(),
      selectedNodesLen = selectedNodes.length;
    _this.lightNodes = selectedNodes;
    if (selectedNodesLen === 2) {
      this.allpath = !this.allpath;
      _this.lightNodes.forEach((item) => {
        let searchNodeTemp = item.data;
        searchNodeTemp.light = 2;
        _this.myDiagram.model.updateTargetBindings(searchNodeTemp);
      });
      var fromNode = selectedNodes[0],
        toNode = selectedNodes[1],
        toNodeData = toNode.data,
        toKey = toNodeData.key,
        paths = this.findNodesByAllWay(fromNode, toKey),
        pathsLen = paths.length;
      for (var i = 0; i < pathsLen; i++) {
        var path = paths[i];
        this.lightPath(path, "FF9900");
      }
    } else {
      this.$Message.warning("需要选择2个节点,才能进行此操作");
      // alert("需要选择2个节点之后,才能进行此操作");
    }
  } else {
    this.allpath = !this.allpath;
    this.clearAllLinkLight();
    _this.lightNodes.forEach((item) => {
      let searchNodeTemp = item.data;
      searchNodeTemp.light = 1;
      _this.myDiagram.model.updateTargetBindings(searchNodeTemp);
    });
  }
},
// 查找全部路径的方法
findNodesByAllWay(fromNode, toKey) {
  var nodeKeyMap = {},
    fromNodeData = fromNode.data,
    fromNodeKey = fromNodeData.key,
    tempNodeArray = [fromNode],
    flag = true,
    path = [fromNode];
  nodeKeyMap[fromNodeKey] = 1;
  while (flag) {
    var tempNodeLen = tempNodeArray.length,
      newTempNodeArray = [];
    for (var i = 0; i < tempNodeLen; i++) {
      var node = tempNodeArray[i],
        connectedNodes = node.findNodesConnected();
      while (connectedNodes.hasNext()) {
        var connectedNode = connectedNodes.value,
          connectedNodeData = connectedNode.data,
          connectedNodeDataKey = connectedNodeData.key;
        if (connectedNode.visible && !nodeKeyMap[connectedNodeDataKey]) {
          var connectedLinks = connectedNode.findLinksConnected();
          nodeKeyMap[connectedNodeDataKey] = 1;
          if (connectedLinks.count > 1 || connectedNodeDataKey === toKey) {
            newTempNodeArray.push(connectedNode);
            path.push(connectedNode);
          }
        }
      }
    }
    if (newTempNodeArray.length) {
      tempNodeArray = newTempNodeArray;
    } else {
      flag = false;
    }
  }
  if (nodeKeyMap[toKey]) {
    return [path];
  } else {
    return [];
  }
},

最短路径

其对应的获取最短路径和最短路径按钮的点击方法如下。

// 最短路径的点击方法
lightShotestPath() {
  if (!this.shortpath) {
    var selectedNodes = this.getSelectedNodes(),
      selectedNodesLen = selectedNodes.length,
      fromNode = null,
      toNodesKey = {};
    _this.lightNodes = selectedNodes;
    if (selectedNodesLen === 2) {
      this.shortpath = !this.shortpath;
      _this.lightNodes.forEach((item) => {
        let searchNodeTemp = item.data;
        searchNodeTemp.light = 2;
        _this.myDiagram.model.updateTargetBindings(searchNodeTemp);
      });
      var fromNode = selectedNodes[0],
        toNode = selectedNodes[1],
        toNodeData = toNode.data,
        toNodeKey = toNodeData.key,
        nodes = this.findNodesByOneWay(fromNode, toNodeKey);
      this.lightPath(nodes, "FF9900");
    } else {
      this.$Message.warning("需要选择2个节点,才能进行此操作");
    }
  } else {
    this.shortpath = !this.shortpath;
    this.clearAllLinkLight();
    _this.lightNodes.forEach((item) => {
      let searchNodeTemp = item.data;
      searchNodeTemp.light = 1;
      _this.myDiagram.model.updateTargetBindings(searchNodeTemp);
    });
  }
},
// 查找最短路径
    findNodesByOneWay(fromNode, toKey) {
      var tempNodes = [fromNode],
        mapKeys = {},
        flag = true,
        pathsMap = {},
        pathNodes = [];
      pathsMap[fromNode.data.key] = [fromNode];
      while (flag) {
        var tempNodesLen = tempNodes.length,
          newTempNodes = [];
        for (var i = 0; i < tempNodesLen; i++) {
          var node = tempNodes[i],
            nodeData = node.data,
            nodeKey = nodeData.key;
          if (!mapKeys[nodeKey]) {
            mapKeys[nodeKey] = 1;
            if (toKey === nodeKey) {
              var cPaths = pathsMap[nodeKey];
              cPaths.push(node);
              pathNodes = cPaths;
              newTempNodes = [];
              break;
            } else {
              var connectedNodes = node.findNodesConnected(),
                cPaths = pathsMap[nodeKey];
              if (cPaths) {
                cPaths.push(node);
                while (connectedNodes.hasNext()) {
                  var connectedNode = connectedNodes.value,
                    connectedNodeData = connectedNode.data,
                    connectedNodeKey = connectedNodeData.key,
                    newPaths = cPaths.concat();
                  if (connectedNode.visible) {
                    if (!pathsMap[connectedNodeKey]) {
                      pathsMap[connectedNodeKey] = newPaths;
                    }
                    newTempNodes.push(connectedNode);
                  }
                }
                delete pathsMap[nodeKey];
              }
            }
          } else {
            delete pathsMap[nodeKey];
          }
        }
        if (newTempNodes.length) {
          tempNodes = newTempNodes;
        } else {
          flag = false;
        }
      }
      return pathNodes;
    },
    // 查找全部路径
findNodesByAllWay(fromNode, toKey) {
  var nodeKeyMap = {},
    fromNodeData = fromNode.data,
    fromNodeKey = fromNodeData.key,
    tempNodeArray = [fromNode],
    flag = true,
    path = [fromNode];
  nodeKeyMap[fromNodeKey] = 1;
  while (flag) {
    var tempNodeLen = tempNodeArray.length,
      newTempNodeArray = [];
    for (var i = 0; i < tempNodeLen; i++) {
      var node = tempNodeArray[i],
        connectedNodes = node.findNodesConnected();
      while (connectedNodes.hasNext()) {
        var connectedNode = connectedNodes.value,
          connectedNodeData = connectedNode.data,
          connectedNodeDataKey = connectedNodeData.key;
        if (connectedNode.visible && !nodeKeyMap[connectedNodeDataKey]) {
          var connectedLinks = connectedNode.findLinksConnected();
          nodeKeyMap[connectedNodeDataKey] = 1;
          if (connectedLinks.count > 1 || connectedNodeDataKey === toKey) {
            newTempNodeArray.push(connectedNode);
            path.push(connectedNode);
          }
        }
      }
    }
    if (newTempNodeArray.length) {
      tempNodeArray = newTempNodeArray;
    } else {
      flag = false;
    }
  }
  if (nodeKeyMap[toKey]) {
    return [path];
  } else {
    return [];
  }
},

最短路径和全部路径的演示

在上面的代码完成之后,可以对最短路径和全部路径进行一个尝试。其对应的效果如下

5.gif

总结

在较为复杂的关系图当中,一般都是选择力导向布局进行布局.但是图数据量确实比较大的话,也会让中间的关系变的很复杂,因此寻找到全部的路径和最短的路径更加方便处理分析出不同的节点也就是角色之间的关系。

本文章只在掘金一个平台发布,未经允许,禁止转载。