All versions of this documentation
X

Visual grouping manual

This is an example of what one could achieve with the grouping feature.

Drag nodes on each other to group them.
Drag a group over a node to add it into the group.
Drag a group over another to merge them.

Open in a new window.
          <!DOCTYPE html>
<html>

<head>
<meta charset="utf-8">
<script src="../build/ogma.min.js"></script>
<style>
  #graph-container {
    top: 0;
    bottom: 0;
    left: 0;
    right: 0;
    position: absolute;
    margin: 0;
    overflow: hidden;
  }
</style>
</head>

<body>
<div id="graph-container"></div>

<script>
  var ogma = new Ogma({
      container: 'graph-container'
  });

  // incremental counter for group ids
  var groupId = 1;
  var groupColors = {};

  function getRandomColor() {
      var letters = '0123456789ABCDEF';
      var color = '#';
      for (var i = 0; i < 6; i++) {
          color += letters[Math.floor(Math.random() * 16)];
      }
      return color;
  }

  function areOnTheSameGroup(nodeA, nodeB) {
      var groupNodeA = nodeA.getData('parent') || nodeA.getData('group');
      var groupNodeB = nodeB.getData('parent') || nodeB.getData('group');
      return groupNodeA && groupNodeB && groupNodeA === groupNodeB;
  }

  function getNodesGroups(nodes) {
      return nodes.reduce(function (groups, node) {
          var groupId = node.getData('parent');
          if (!groupId) {
              // if the node does not have a group, just pushes it
              return groups.concat(node);
          }
          if (
              // avoid pushing twice a group in the result array
              !groups.filter(function (group) {
                  return groupId === group.getId();
              }).length
          ) {
              return groups.concat(
                  ogma
                      .getNodes()
                      .filter(function (node) {
                          return node.getData('group') === groupId;
                      })
                      .get(0)
              );
          }
          return groups;
      }, []);
  }

  function merge(draggedNode, nodesAround) {
      var nodePos = draggedNode.getPositionOnScreen();
      var draggedNodeRadius =
          draggedNode.getAttribute('radius') * ogma.view.getZoom();
      // find the nodes which are intersecting with the draggedNode
      var nodesToMergeWith = nodesAround.reduce(function (bestNodes, candidate) {
          var pos = candidate.getPositionOnScreen();
          var dist =
              Ogma.geometry.distance(nodePos.x, nodePos.y, pos.x, pos.y) -
              candidate.getAttribute('radius') * ogma.view.getZoom();
          return dist < draggedNodeRadius
              ? bestNodes.concat(candidate)
              : bestNodes;
      }, []);

      // no merge candidate found
      if (!nodesToMergeWith.length) return [];
      // merge the candidates with the dragged node
      var nodesToMerge = nodesToMergeWith.concat(draggedNode);
      // if one of the elements of the nodes to merge is a group, reuse its id
      var newGroupId = nodesToMerge
          .map(function (node) {
              return node.getData('group');
          })
          .filter(function (group) {
              return group;
          })[0];
      // if none of the elements is a group, just create a new one
      if (!newGroupId) {
          newGroupId = groupId++;
          groupColors[newGroupId] = getRandomColor();
      }

      nodesToMerge.forEach(function (node) {
          var subNodes = node.getSubNodes();
          // if the node is a group assign the group id to its children
          if (subNodes) {
              subNodes.fillData('parent', newGroupId);
          } else {
              node.setData('parent', newGroupId);
          }
      });
      return nodesToMerge;
  }

  ogma.generate
      .random()
      .then(function (graph) {
          return ogma.setGraph(graph);
      })
      .then(function () {
          return ogma.layouts.force({ locate: true });
      });

  ogma.transformations.addNodeGrouping({
      groupIdFunction: function (node) {
          return node.getData('parent');
      },
      nodeGenerator: function (nodes, groupId) {
          return {
              data: {
                  group: groupId
              }
          };
      },
      showContents: true,
      onCreated: function (metaNode, visible, subNodes) {
          return ogma.layouts.force({ nodes: subNodes });
      }
  });

  ogma.styles.addRule({
      nodeSelector: function (node) {
          return node.getSubNodes() !== null;
      },

      nodeAttributes: {
          color: function (node) {
              return groupColors[node.getData('group')];
          },
          opacity: 0.25,
          layer: 0
      }
  });

  ogma.styles.addRule({
      nodeSelector: function (node) {
          return node.getSubNodes() === null;
      },
      nodeAttributes: {
          layer: 2
      }
  });

  ogma.events.onNodeDragEnd(function (data) {
      var nodes = getNodesGroups(data.nodes);
      var forbiddenSet = nodes.reduce(function (acc, node) {
          acc[node.getId()] = true;
          return acc;
      }, {});
      var nodesAround = ogma.getNodes();
      nodes.forEach(function (draggedNode) {
          var candidateNodesToMerge = nodesAround
              // nodes to merge with cant be the dragged ones
              // nor the nodes which have already been merged innto
              // a previous iteration of the loop
              .filter(function (node) {
                  return (
                      !forbiddenSet[node.getId()] &&
                      // nor the parent of te dragged node
                      !areOnTheSameGroup(node, draggedNode)
                  );
              });

          var nodesMerged = merge(draggedNode, candidateNodesToMerge);
          // add the merged nodes to the forbidden set
          nodesMerged.forEach(function (node) {
              return (forbiddenSet[node.getId()] = true);
          });
      });
  });
</script>
</body>

</html>