X

An Oracle blog about Data Visualizations

  • November 27, 2014

A Guide to Diagram – Part 10 - Inter-Group Links

Duncan Mills
Architect
If you create a diagram that contains groups (containers) there are some special considerations to take into account when writing your layouts. These relate to the use of links that traverse the containers, specifically those which have an origin or destination inside of a container.
For example you have something like this:
Figure 1 - Basic Inter Group links in diagram
Figure 1 - Basic inter-group links
In this situation your layouts have to be somewhat aware of this cross container linking and how they will be presented with the information that will be needed to correctly route your links. 
When you have links such as those shown in Figure 1 and you collapse the containers, the diagram will create a special link which represents a roll-up or "summary" of the links that run between the two containers.  This is represented as a dashed line:
Figure 2 - Collapsed groups showing rollup links
Figure 2 - Collapsed Groups showing Links
If you hover the mouse over this link it will then provide you with information about the number of  "real" links that it represents.
When you build your layouts for diagram you are responsible for routing this summary link in just the same way as you would any other link, and on top of this special rules apply when one or more of the groups are expanded. This process can be a little confusing at first, so I thought that it would be a good example for an article in the diagram series:

Setting up 

If you want to follow along here, create yourself a new Workspace for an ADF Fusion Project and create a new Java class from this code.  This just creates a very basic set of nodes and links that I'll use to drive this example. Once you have created the class, expose it as a view scope managed bean called diagramModel that we can then use.
In summary this model defines two sets of nodes.  Two grouping nodes each with two children.  There are then links between the corresponding children in each group, as per Figure 1. 
The actual diagram definition on the page is really quite simple in this case:

<dvt:diagram id="d1" layout="alignedLayoutHorizontal"
                     summary="Demonstration diagram to show cross container links"
                     emptyText="No data to show" 
                     controlPanelBehavior="initCollapsed">
<dvt:clientLayout name="alignedLayoutHorizontal"
method="DemoDiagramLayouts.coreAlignedLayout"
featureName="DemoDiagramLayouts">
<f:attribute name="alignment" value="horizontal"/>
</dvt:clientLayout>
<dvt:clientLayout name="alignedLayoutVertical"
method="DemoDiagramLayouts.coreAlignedLayout"
featureName="DemoDiagramLayouts">
<f:attribute name="alignment" value="vertical"/>
</dvt:clientLayout>
<dvt:diagramNodes value="#{viewScope.diagramModel.nodes}" 
var="node" id="dn1">
<dvt:diagramNode id="dn_tg" nodeId="#{node.nodeId}" 
containerId="#{node.containerId}"
layout="alignedLayoutVertical" 
showNodeActions="false" >
<f:facet name="zoom100">
<dvt:marker id="nodeMarker" height="80" width="80" borderColor="#000000"
borderStyle="solid" borderWidth="1.0"
labelDisplay="on" value="#{node.label}">
<dvt:attributeGroups id="typeGroup" type="color shape"
value="#{node.nodeType}">
<dvt:attributeMatchRule id="typeG" group="G">
<f:attribute name="color" value="#EAEAEA"/>
<f:attribute name="shape" value="rectangle"/>
</dvt:attributeMatchRule>
<dvt:attributeMatchRule id="typeN" group="N">
<f:attribute name="color" value="red"/>
<f:attribute name="shape" value="circle"/>
</dvt:attributeMatchRule>
</dvt:attributeGroups>
</dvt:marker>
  </f:facet>
<f:facet name="containerBottom">
<af:outputText value="#{node.label}" id="ot1"/>
</f:facet>
</dvt:diagramNode>
</dvt:diagramNodes>
<dvt:diagramLinks value="#{viewScope.diagramModel.links}" var="link" id="dl1">
<dvt:diagramLink id="ln1" startNode="#{link.startId}" 
endNode="#{link.endId}" 
endConnectorType="arrow" 
linkStyle="solid" 
linkWidth="3"/>                  
</dvt:diagramLinks>                                              
</dvt:diagram>

Layouts

There are two layouts defined by the code: alignedLayoutHorizontal which is the top level layout used by the diagram itself and alignedLayoutVertical which is used by the only node definition.  The effect of these layouts are that the top level group nodes in the diagram will display side by side and then any nodes inside of the top level nodes will use a vertical arrangement. If you look a little more closely though, you will see that both clientLayout definitions actually point to the same JavaScript routine called coreAlignedLayout passing an attribute called alignment to control how that operates.
If we just concentrate on the part of the layout that handles the links it looks like this in the first version, the version where we don't take the containers into account (You will be able to read the full source code for the layout by following a link at the end of this article):
...
//Layout the links - loop in the normal way
var linkCount = layoutContext.getLinkCount();
for (var j = 0;j < linkCount;j++) {
var layoutLink = layoutContext.getLinkByIndex(j);
DemoDiagramLayouts._routeLink(layoutLink,layoutContext, alignment);
}
...  
So all this code does is to loop through each of the links for this layout instance and call the _routeLink routine to do the work. 
Here's the initial version of the _routeLink code that produced Figure 2. As you can see, in that case, it positioned the links nicely;vertically centered on the correct edge of the start node and connecting to the corresponding point on the destination:
_routeLink: function (routeLink, layoutContext, alignment) {
  var sourceNode = layoutContext.getNodeById(routeLink.getStartId());
  var destNode = layoutContext.getNodeById(routeLink.getEndId());
  var sourceNodePos = sourceNode.getPosition();
  var destNodePos = destNode.getPosition();
  var sourceNodeDims = sourceNode.getContentBounds();
  var destNodeDims = destNode.getContentBounds();
  var points = [];
  if (alignment == "horizontal"){
    if (sourceNodePos.x < destNodePos.x){
      points.push(sourceNodePos.x + sourceNodeDims.w + routeLink.getStartConnectorOffset());
      points.push(sourceNodePos.y + (sourceNodeDims.w/2));
      points.push(destNodePos.x - routeLink.getEndConnectorOffset());
      points.push(destNodePos.y + (destNodeDims.w/2));
    } else {
      points.push(sourceNodePos.x - routeLink.getStartConnectorOffset());
      points.push(sourceNodePos.y + (sourceNodeDims.w/2));
      points.push(destNodePos.x + destNodeDims.w + routeLink.getEndConnectorOffset());
      points.push(destNodePos.y + (destNodeDims.w/2));
    }
  } else { //vertical
    if (sourceNodePos.y < destNodePos.y){
      points.push(sourceNodePos.x + (sourceNodeDims.w /2 ));
      points.push(sourceNodePos.y + sourceNodeDims.h + routeLink.getStartConnectorOffset());
      points.push(destNodePos.x + (destNodeDims.w/2));
      points.push(destNodePos.y - routeLink.getEndConnectorOffset());
      } else {
        points.push(sourceNodePos.x + (sourceNodeDims.w/2));
        points.push(sourceNodePos.y  - routeLink.getStartConnectorOffset());
        points.push(destNodePos.x + (destNodeDims.w/2));
      points.push(destNodePos.y + destNodeDims.h + routeLink.getEndConnectorOffset());
    }    
  }
  routeLink.setPoints(points);
}
So that all seems to work. However, if we expand the first group we get this:
First group expanded
Figure 3 - First Group Expanded
It's sort of OK, the edges connect to the second group correctly but there is some overlap between the endpoints in the first group and the nodes. Notice as well that at this point that the inter group links are still show as dashed because the actual source and destination nodes within Group 2 are not yet visible. 
So we then expand Group 2:
Figure 4 - Both groups expanded
Figure 4 - Both Groups Expanded
Notice how the links are now rendered as the correct solid lines, and the vertical links that happen within the groups are just fine.  However, the inter-group links that should be joining 1 to 3 and 4 to 2 have gone very, very wrong. Why is that? 

Understanding the Problem

In order to really understand what's going on here you have to appreciate how diagram handles multiple layouts and how different layouts are responsible for different links. 
In this simple case, when we have two open containers on the page, the layout engine will be called three times. The diagram works from the inside out, so the innermost layouts (those for Group 1 and Group 2 respectively) will be processed before the final top level layout.  If you step through the layout code in this case you would see the alignedLayoutVertical being processed for each of those. Interestingly, as you do so you will also see that each of those layout routines only has to handle one link. When Group 1 is processed the only link to handle will be the one that starts at Child 2 and ends at Child 1 and in the case of Group 2 its the link between Child 3 and Child 4.
Because the links between Child 1 and 3  and Child 4 and 2 are not wholly contained within the same container they have to be processed by their mutual parent, which in this case is the top level layout.
Once the two inner layouts are done, then the overall diagram layout (alignedLayoutHorizontal) will be called to layout the two container nodes. It is this layout that will be handed the two inter-group layouts as nodes and the links between the,. 

So that's the first rule to remember:

Rule 1 - Cross container links are routed by the closest layout run that contains both the source and the destination nodes.

The second factor is the relative coordinate systems of each layout.
When you write layout code, you work to a standardized layout system relative to the top left hand origin of a virtual canvas at 0, 0.  Now within your layout, you are quite at liberty to place objects at any coordinate, positive or negative relative to this origin, by convention we start at 0,0 for convenience.  The way to think about this is that once a layout is complete, the diagram will logically draw the smallest rectangle that it can around the nodes and links that you have drawn and and crop out  any whitespace beyond this boundary. This rectangle now represents a single node (remember that a container layout runs before it's parent) and now when the parent layout is called, it will be passed this container node which is sized according to this cropped rectangle (assuming it is expanded of course).  The parent layout can now tell how to layout these nodes as the size of each will now be known.

So now we're laying out the parent layout and, according to Rule 1 above, we know that it will be the parent layout will be handled the two inter-container links to route.

Now when we route a link, we generally find out the locations and dimensions of the start and end nodes, so that we know where to anchor the link.  So we get the ID of the start or end node and call layoutContext.findNodeById() using that ID. Just as I've done in the code above, and as shown here:

var sourceNode = layoutContext.getNodeById(routeLink.getStartId());

We can then ask that node for it's location using the getPosition() API.  And here's the problem which leads to:

Rule 2 - A node always reports its position relative to its own layout container.  

A node knows nothing of where it actually is, outside of it's own layout context. So for example a node may have been positioned at 10,10 in it's own layout, but that layout itself may have been positioned at 20,20 by the parent layout.  The result is that the actual location of the node is 30,30 on the final canvas.

Dealing with Node Layout Relative Coordinates

So this is the mistake that my first attempt at the layout exhibits.  It uses the position of the node as reported by that node, rather than the position in terms of the parent layout that is running. 
To solve this, we first need to identify if the node is inside of a container, and if so, convert its position from this local frame of reference to the global system being used by the current layout. 
So the first problem is to check if the source or destination nodes for the links are within a container. We can do this by calling the getContainerId() function on the node.  If it returns a result, we know that we need to rebase the node position to the correct coordinate origins.
The way to do this rebasing task, is to call a separate API on the layoutContext called localToGlobal(). This API takes two arguments, a point location and a node. The node argument identifies the node that we want the coordinates of in global terms, the point argument is an offset from that.  We usually just pass DvtPoint(0,0) to this argument to indicate that we're interested in the location of the top left hand corner of the node in question.
So here's the amended code for the link routine with this simple change in place and highlighed for you:
_routeLink: function (routeLink, layoutContext, alignment) {
var sourceNode = layoutContext.getNodeById(routeLink.getStartId());
  var destNode = layoutContext.getNodeById(routeLink.getEndId());
  var sourceNodePos = sourceNode.getPosition();
  var destNodePos = destNode.getPosition();
 
  //See if we need to handle a cross container link
  var nodeTopLeft = new DvtPoint(0,0);
  if (sourceNode.getContainerId()){
    sourceNodePos = layoutContext.localToGlobal(nodeTopLeft,sourceNode); 
  }
  if (destNode.getContainerId()){
    destNodePos = layoutContext.localToGlobal(nodeTopLeft,destNode);   
  }

  var sourceNodeDims = sourceNode.getContentBounds();
  var destNodeDims = destNode.getContentBounds();
  var points = [];
  if (alignment == "horizontal"){
    if (sourceNodePos.x < destNodePos.x){
      points.push(sourceNodePos.x + sourceNodeDims.w + routeLink.getStartConnectorOffset());
      points.push(sourceNodePos.y + (sourceNodeDims.w/2));
      points.push(destNodePos.x - routeLink.getEndConnectorOffset());
      points.push(destNodePos.y + (destNodeDims.w/2));
    } else {
      points.push(sourceNodePos.x - routeLink.getStartConnectorOffset());
    points.push(sourceNodePos.y + (sourceNodeDims.w/2));
      points.push(destNodePos.x + destNodeDims.w + routeLink.getEndConnectorOffset());
      points.push(destNodePos.y + (destNodeDims.w/2));
    }
  } else { //vertical
    if (sourceNodePos.y < destNodePos.y){
      points.push(sourceNodePos.x + (sourceNodeDims.w /2 ));
      points.push(sourceNodePos.y + sourceNodeDims.h + routeLink.getStartConnectorOffset());
      points.push(destNodePos.x + (destNodeDims.w/2));
      points.push(destNodePos.y - routeLink.getEndConnectorOffset());
    } else {
      points.push(sourceNodePos.x + (sourceNodeDims.w/2));
      points.push(sourceNodePos.y  - routeLink.getStartConnectorOffset());
    points.push(destNodePos.x + (destNodeDims.w/2));
      points.push(destNodePos.y + destNodeDims.h + routeLink.getEndConnectorOffset());
    }    
  }
  routeLink.setPoints(points);
}

That then will be enough to correct the coordinates and fix up the links to the version that we see in Figure 1.

More Complex Cases

In the case that I've discussed in this example we have a simple two level diagram, where a node is either at the top level, or ir is inside a single container.  Of course diagrams might be more complicated than this and, for example, multiple levels of containership could be used.  In that case, just checking for a containerId on the node will not be enough to see if you have a cross container link.  In that case you will need to cross reference the nodeIds presented by the link, with the list of nodes being handled by this particular layout.

A second interesting use case is if you want to route the roll-up links in a different way from conventional links.  These links are handed to the layout in the normal way but you can identify them by testing the isPromoted() method on the link object itself. If either the source, destination or both terminating nodes for the link are hidden within a collapsed container then this flag will be set to true. 

The full source code for the fixed layout JavaScript can be found here.  


Be the first to comment

Comments ( 0 )
Please enter your name.Please provide a valid email address.Please enter a comment.CAPTCHA challenge response provided was incorrect. Please try again.