All versions of this documentation
X

Transformation API

Graphs representation can change during the visualization: multiple nodes can be aggregated to a single node representation, same for links, or indirect relationships can be represented by a simple and direct relationship. In all these cases the graph can be transformed in another graph. Ogma provides multiple ways to alter the graph in any way described above with the Transformation API.

Basics

A transformation object is the outcome of any Ogma transformation operation: the object controls the state of the transformation, so that it is possible to toggle it, control its execution and destroy it.

var transformationObject = ogma.transformation.addNodeFilter( ... );

transformationObject
  .whenApplied()
  .then(function(){
    // here I am sure the trasformation is complete
  });

// ...later on
// turn off the transformation
transformationObject.disable();

// and turn it on again
transformationObject.enable();

// ...and now destroy it completely
transformationObject.destroy();
// from this moment on the transformationObject
// has no longer effect on the graph

Transformation are immediate in general, but it is possible to animate them during creation with the duration parameter:

// use filter here, but same apply to other transformation as well
var transformationObject = ogma.transformation.addNodeFilter({..., duration: 1000});

// same applies when disabling/destroying the transformation:
// ...later on
// turn off the transformation with animation
transformationObject.disable(1500);

// and turn it on again with animation
transformationObject.enable(1500);

// ...and now destroy it completely with animation
transformationObject.destroy(1500);

Filtering Transformation

Both nodes and edges can be dynamically filtered from the graph with a simple operation:

// Keep only the nodes with "country" is set to France
var nodeFilter = ogma.transformations.addNodeFilter(function(node){
  return node.getData("country") === "France";
});
// Keep only the edges which property "country" is set to "France"
var edgeFilter = ogma.transformations.addEdgeFilter(function(edge){
  return edge.getData("country") === "France";
});

Criteria used in the transformations above must return true for nodes/edges to show and false for nodes/edges to hide.

It is also possible to animate the filtering transformation:

var nodeFilter = ogma.transformations.addNodeFilter({
  duration: 500,
  criteria: function(node){
    return node.getData("country") === "France";
  }
});

var edgeFilter = ogma.transformations.addEdgeFilter({
  duration: 500,
  criteria: function(edge){
    return edge.getData("VISUALIZE") === "graphs";
  }
});

The logic inside the filtering criteria can be more complex than then just look at the node's or edge's data as follow:

// Keep only the edges where at least one end has the property "country" set to "France"
var edgeFilter = ogma.transformations.addEdgeFilter(function(edge){
  return edge.getExtremities().getData("country").indexOf("France") > -1;
});

There's also a shorthand for addNodeFilter and addEdgeFilter on the ogma instance object as well:

var nodeFilter = ogma.addNodeFilter(function(node){
  return node.getData("country") === "France";
});

var edgeFilter = ogma.addEdgeFilter(function(edge){
  return edge.getData("country") === "France";
});

Group nodes transformation

Nodes can be grouped together to simplify the visualization of the network: this transformation can group nodes according to a criteria, calculate compound properties at the group level and create a hierarchy of nested groups reducing the clutter in the visualization.

To group nodes, Ogma offers the addNodeGrouping function:

// group all the nodes that have a "country" property set to "France" in a single node
var nodeGrouping = ogma.transformations.addNodeGrouping({
  selector: function(node){
    return node.getData("country") === "France";
  },
  nodeGenerator: function(nodeList){
    return {
      id: "grouped-french-node",
      attributes: {
        text: "France Group",
        radius: nodeList.size * 2,
        image: "url/for/french/flag.jpg"
      }
    };
  }
});

In the code above all nodes with country set to "France" are grouped together and the resulting node is styled with a custom label and size, with the image of the French flag using the nodeGenerator option property.

It is possible to ungroup the nodes disabling the transformation object or destroying it:


// ..later to undo/ungroup just use the transformation object
// Note: it can be restored again calling .enable()
nodeGrouping.disable();

// or remove it completely
nodeGrouping.destroy();

When ungrouping the nested nodes will get their original position, with an offset based on the grouped node current position.

It is possible to perform multiple grouping at the time using the groupIdFunction option to create different groups:

// Group all nodes by country. Nodes with no country information will be grouped into the "No Country" group
var multipleGroups = ogma.transformations.addNodeGrouping({
  groupIdFunction: function(node){
    return node.getData("country") || "No Country";
  },
  nodeGenerator: function(nodeList, groupId){
    return {
      id: "custom-id-"+groupId,
      attributes: {
        text: groupId + " Group",
        radius: nodeList.size * 2,
        image: getFlagIcon(groupId)  // a simple function that returns the flag URL for each groupId value
      }
    };
  }
});

Note that in the last example selector is not specified, so all nodes in the graph are selected by default.

Sometimes it is useful to control how edges are grouped during the node grouping process: for example it can be of interest to group edges by direction for each connection. Ogma makes easy this task providing a specific separateEdgesByDirection flag:

// group all the nodes that have a "country" property set to "France" in a single node
var nodeGrouping = ogma.transformations.addNodeGrouping({
  selector: function(node){
    return node.getData("country") === "France";
  },
  nodeGenerator: function(nodeList){
    return {id: "grouped-node-1", attributes: {image: "url/for/french/flag.jpg"}}
  },
  separateEdgesByDirection: true,
  edgeGenerator: function(edgeList){
    var combinedId = edgeList.getId().join('-');
    return {id: "grouped-edge-"+combinedId, attributes: {shape: "arrow"}};
  }
});

The edgeGenerator function works in a similar way to the nodeGenerator function explained before: edges that will be grouped are passed to the function as edgeList parameter.

To see and example of node grouping in action taken a look at:

For more control on the way to group edges read the next section.

Visual grouping

Additionally, Ogma offers a way to group nodes without hiding them. That can be very useful to work with the 'meta-graph' and arrange it in clusters, while still having access to the contents of the clusters. This mode is embedded into the usual node grouping by adding more fields to the grouping options.

In order to leave some of the groups open, you should specify it in the showContents callback, based on the group node and its contents:

var grouping = ogma.transformations.addNodeGrouping({
  groupIdFunction: function(node){
    return node.getData("country") || "No Country";
  },

  showContents: function (metaNode) {
    return metaNode.getData('open');
  },

  nodeGenerator: function(nodeList){
    return {
      id: "grouped-node-1",
      attributes: { opacity: 0.32 },
      data: { open: nodeList.size > 5 }
    }
  }
})

Alternatively, if you can supply a boolean value to showContents field, if all of your compound nodes would be of one kind or another.

It is also possible to add more levels of grouping:

Another callback that can be useful in visual grouping is onCreated, which allows you to apply positioning to the contents of the group, i.e. run a layout or set pre-calculated positions. This callback should return a Promise if you want that the grouping transformation would wait until these procedures are finished for all the groups, so that you could interact with the compound nodes. For instance, run a layout on them.

var grouping = ogma.transformations.addNodeGrouping({
  groupIdFunction: function(node) {
    return node.getData("country") || "No Country";
  },

  showContents: function (metaNode) {
    return metaNode.getData('open');
  },

  nodeGenerator: function(nodeList) {
    ...
  },

  onCreated: function (metaNode, showContents, subNodes, subEdges) {
    if (showContents) {
      // return a Promise if you want transformation to wait until it's done
      return ogma.layouts.force({ nodes: subNodes }); // returns a Promise
    }
  }
});

// here we run the layout on the meta-nodes only
grouping.whenApplied()
  .then(function() { return ogma.layouts.force(); });

By default, all the layouts will ignore the nodes that are grouped. But, if you pass them to the layout explicitly, the layout would be applied on them. This can come in handy if you want to layout the contents of the particular group as if it was completely independent as a subgraph and see its shape. In the same way, you can apply different layouts to the contents of the different groups:

Examples of visual grouping in action:

Group edges transformation

Like nodes there are cases where it makes sense to merge edges to simplify the visualization, in order to analyze high level flows in the network, and drill down where needed.

To group edges, Ogma offers the addEdgeGrouping function:

var edgeGenerator = ogma.transformations.addEdgeGrouping();

The code above group all parallel edges in the graph in a single edge. That is the basic usage of the API. Note edge grouping applies only for each set of parallel edges: it is not possible to group together edges with different ends.

Of course it is possible to apply the transformation to a specific subset of the graph and better control the transformation behaviour:

// group all the edges that have a "country" property set to "France"
var edgeGenerator = ogma.transformations.addEdgeGrouping({
  selector: function(edge){
    return edge.getData("country") === "France";
  },
  generator: function(edgeList){
    // the final width is the sum of all the widths of the edges grouped together
    var finalWidth = edgeList.getAttribute("width").reduce(function(memo, width){
      return memo + width;
    }, 0);
    return {
      id: "grouped-french-edge",
      attributes: {
        text: "France Edges Group",
        width: finalWidth,
        color: 'rgb(178,18,18)'
      }
    };
  }
});

In the code above the edges between the two ends are filtered by some data property and the result is grouped together: a custom styling has been applied to the grouped edge using the generator option property.

As for nodes, also edge grouping can be undone disabling or destroying the transformation object:

// ..later to undo/ungroup just use the transformation object
edgeGenerator.disable();

// will be removed completely
edgeGenerator.destroy();

It is possible to perform multiple distinct grouping at the same time using the groupIdFunction option to create different groups:

// group parallel edges in multiple edges, each group by country
var edgeGenerator = ogma.transformations.addEdgeGrouping({
  groupIdFunction: function(edge){
    return edge.getData("country");
  },
  generator: function(edgeList, groupId){
    // the final width is the sum of all the widths of the edges grouped together
    var finalWidth = edgeList.getAttribute("width").reduce(function(memo, width){
      return memo + width;
    }, 0);

     var COLORS = {
      France: 'rgb(178,18,18)',
      Russia: 'rgb(9, 113, 178)'
    };

    return {
      id: "grouped-edge" + groupId,
      attributes: {
        text: groupId + " Edges Group",
        width: finalWidth,
        color: COLORS[groupId.substring(0, 6)]
      }
    };
  }
});

To see and example of edge grouping in action take a look at:

Manipulating grouped nodes and eges

As mentioned before, Ogma will throw an error when attempting to remove a virtual node or edge, because the intent for this operation is not clear.

Here's what we suggest when you need to remove a group node or edge: remove its underlying elements first and then it would disappear itself together with all its contents:

Removing a grouped node

// remove grouped node
const node = ogma.getNode('group_id');
ogma.removeNodes(node.getSubNodes())
  .then(() => ogma.transformations.afterNextUpdate())
  .then(() => {
    // it's removed by re-running the transformation
    console.log(ogma.getNode('group_id')); // undefined
  })

Removing a grouped edge

// remove grouped edge
const edge = ogma.getEdge('generated_group_id');
ogma.removeEdges(edge.getSubEdges())
  .then(() => ogma.transformations.afterNextUpdate())
  .then(() => {
    // it's removed by re-running the transformation
    console.log(ogma.getEdge('generated_group_id')); // undefined
  });

Removing a node whith grouped edges attached to it

// you don't know if there are some grouped edges connected to it
const adjacentVirtualEdges = node.getAdjacentEdges()
  .filter((edge) => edge.isVirtual());

// there are no compound edges, normal ones will be removed automatically
if (adjacentVirtualEdges.size === 0) {
  ogma.removeNode(node);
}

else {
  // get a list of all underlying grouped edges
  const edgesToRemove = adjacentVirtualEdges
    .reduce(
      (acc, virtualEdge) => virtualEdge.getSubEdges().concat(acc),
      ogma.createEdgeList()
    );

  // remove them
  ogma.removeEdges(edgesToRemove)
    .then(() => ogma.transformations.afterNextUpdate())
    // here you can also handle the grouped node case
    .then(() => ogma.removeNode(node));
}

Node collapsing

Sometimes grouping nodes or edges is not enough, while a simplified data model is required to be computed live. This can be achieved by making "technical" nodes in a complex data model become new edges.

This type of transformation is called node collapsing in Ogma, and it can be used with the addNodeCollapsing API:

var nodeCollapsed = ogma.transformations.addNodeCollapsing({
  selector: function(node){
    return node.getData("type") === "country";
  }
});

The function changes the data model of the network into a simpler one: the whole connectivity remains intact as nodes connected with the collapsed one will be connected each other with a many to many approach.

It is possible to control the styling of the new edge generated via the edgeGenerator parameter:

var nodeCollapsed = ogma.transformations.addNodeCollapsing({
  selector: function(node){
    return node.getData("type") === "country";
  },
  edgeGenerator: function(hiddenNode, sourceNode, targetNode, sourceEdges, targetEdges){

    return {
      attributes: {
        color: "rgb(9, 113, 178)",
        text: "Same country: "+hiddenNode.getData("name")
      }
    };
  }
});

Custom attributes and data can be generated using information from the collapsing node neighborhood, using the parameters of the edgeGenerator function.

It is also possible to prevent the creation of one or more edges during the process:

var nodeCollapsed = ogma.transformations.addNodeCollapsing({
  selector: function(node){
    return node.getData("type") === "country";
  },
  edgeGenerator: function(hiddenNode, sourceNode, targetNode, sourceEdges, targetEdges){
    // check if one of the two ends is "Joe Black"
    var isJoeBlack = sourceNode.getData("name") === "Joe Black" ||
                     targetNode.getData("name") === "Joe Black";

    // do not create the links with Joe Black
    if(isJoeBlack){
      return null;
    }

    return {
      attributes: {
        color: "rgb(9, 113, 178)",
        text: "Same country: "+hiddenNode.getData("name")
      }
    };
  }
});

As other transformation API also nodeCollapsing can be animated:

var nodeCollapsed = ogma.transformations.addNodeCollapsing({
  selector: function(node){
    return node.getData("type") === "country";
  },
  duration: 1500
});

To see and example of node collapsing in action taken a look at:

Neighbor merging

Often the data model for the presentation doesn't match the one from the database or storage. This can translate in a simplified data model with the same connectivity as in node collapsing, or in a different model where particular entities are transformed in attributes of their neighbors.

In Ogma this kind of transformation is implemented via the addNeighborMerging API:

var neighborMerge = ogma.transformations.addNeighborMerging({
  selector: function(node){
    return node.getData("type") === "country";
  },
  dataFunction: function(node){
    // this is the data from the node about to hide
    // that will enrich the neighborhood data
    return ({ country: node.getData('name') });
  }
});

The dataFunction parameter can be used to enrich the data of the neighborhood around the merged node. In some scenarios there's no need to enrich the neighborhood with data, so undefined can be returned from the function.

Note the 3 person node now acquired a new property country set to USA after the merging. A single node information has been merged into its neighborhood.

Note if the dataFunction parameter is not passed the default behaviour is to not enrich the neighborhood - same as returning undefined in your function. It is therefore recommended to always pass a dataFunction parameter.

To see and example of neighbor merging in action taken a look at:

Neighbor generation

On the other side of the spectrum, generating new entities from the data model can be a way to promote inner properties and make new patterns emerge in the analysis.

In Ogma this kind of transformation is implemented via the addNeighborGeneration API:

var neighborMerge = ogma.transformations.addNeighborGeneration({
  selector: function(node){
    return node.getData("type") === "person";
  },
  neighborIdFunction: function(node){
    return node.getData("country");
  }
});

The neighbourIdFunction tells Ogma what id to assign to the newer node generated. When a single string is returned then a single node is generated. It is possible to avoid the generation of a node returning null. At the same time it is possible to generate multiple neighbours returning an array of strings:

var neighborMerge = ogma.transformations.addNeighborGeneration({
  selector: function(node){
    return node.getData("type") === "person";
  },
  neighborIdFunction: function(node){
    return [node.getData("country"), node.getData("type")];
  },
});

The addNeighborGeneration function provides an API to control the styling of both newly generated nodes and their relative edges:

var neighborMerge = ogma.transformations.addNeighborGeneration({
  selector: function(node){
    return node.getData("type") === "person";
  },
  neighborIdFunction: function(node){
    return node.getData("country");
  },
  nodeGenerator: function(id, sourceNodeList){
    // Style the new node with a blue background and a flag icon
    return {
      attributes: {
        color: "rgb(9, 113, 178)",
        icon: {
          content: '\uf024',
          font: 'FontAwesome'
        }
      }
    };
  },
  edgeGenerator: function(sourceNode, newNode){
    // make the new edges blue
    return {
      attributes: {
        color: "rgb(9, 113, 178)"
      }
    };
  }
});

The edgeGeneration function is called for each edge generated and it provides contextual information useful for the creation of the edge. The nodeGenerator will be called for each new node generated, providing contextual information of the neighborhood that created the new node.

To see and example of neighbor generation in action taken a look at: