A Guide to Diagram – Part 5 – Starting with Custom Layouts, continued

At the end of the last article we had achieved the following layout:
Progress so far
Which was created by the following layout function:
var LearningDiagramLayouts = {
  simpleVertical : function(layoutContext){
    var nodeGap = 50;
    var nodeCount = layoutContext.getNodeCount();
    var linkCount = layoutContext.getLinkCount();
    var xPos = 0;
    var yPos = 0;
    for (var i = 0;i<nodeCount;i++) {
      var node = layoutContext.getNodeByIndex(i);
      node.setPosition(new DvtDiagramPoint(xPos, yPos));
      node.setLabelPosition(new DvtDiagramPoint((xPos + nodeGap) , yPos));
      yPos += nodeGap;
    var nodeSize = 20;
    var startY = nodeSize;
    var startX = nodeSize/2;
    var endY = nodeGap;
    var endX = startX;
    for (var j = 0;j<linkCount;j++) {
      var link = layoutContext.getLinkByIndex(j);
      link.setPoints([startX, startY, endX, endY]);
      link.setLabelPosition(new DvtDiagramPoint((startX - nodeGap) , (startY + 20)));
      startY += nodeGap;
      endY +=nodeGap;

To critique this layout as it stands, the obvious issue is that the Gamma link does not terminate in the correct place, but there is also a lot of hardcoding and calculations based on assumptions of fixed node sizes.  So the question is, how can we address these issues?

Getting Sizing Information

First of all we want to remove the hardcoding and make the layout more re-usable by making it sensitive to the sizing of the nodes that are passed to it.  There are three APIs that provide sizing information:
  • getBounds() – returns a DvtRectangle object which defines the x,y position and the width and height of the node.  This rectangle will include the space allocated to overlay markers (recall that you can position markers outside of the logical node boundaries)
  • getContentBounds() – returns sizing information for the node itself without accounting for the overlay markers
  • getLabelBounds() – returns the sizing information for the node (or link) label.
So you can rework the JavaScript to use some of these APIs:
var LearningDiagramLayouts = {
  simpleVertical : function (layoutContext) {
    var largestNodeBounds = LearningDiagramLayouts.getMaxNodeBounds(layoutContext);
    var nodeGap = (2 * largestNodeBounds.h);
    var nodeCount = layoutContext.getNodeCount();
    var linkCount = layoutContext.getLinkCount();
    var xPos = 0;
    var yPos = 0;
    var labelXPos = largestNodeBounds.w + 20;
    for (var i = 0;i < nodeCount;i++) {
      var node = layoutContext.getNodeByIndex(i);
      var nodeHeight = node.getContentBounds().h;
      node.setPosition(new DvtDiagramPoint(xPos, yPos));
      var labelHeight = node.getLabelBounds().h;
      var labelYPos = yPos + ((nodeHeight - labelHeight)/2);
      node.setLabelPosition(new DvtDiagramPoint(labelXPos, labelYPos));
      yPos += (nodeHeight + nodeGap);
You'll see here that first of all we work out the nodeGap variable by calling a utility function called getMaxNodeBounds() (shown below) which finds the largest node in the set. Using that information, the nodeGap variable is set to twice the height of this largest node, rather than it's old hardcoded size of 50. Then for each node we use the height of that node added to the gap to calculate the vertical position of the next node in the series. This will ensure that the layout will continue to work, even if nodes have differing sizes.
In the updated version of the code we also take into account the height  and width of the node and the height of the label so that the label position is vertically centered on the center of the node and left aligned at a constant distance from the right hand side of the nodes.  This does not show very clearly with the current diagram, however, with larger and mixed sized nodes it becomes more obvious.  So your calculations should always take the actual metrics for the label itself into account.

The getMaxNodeBounds Utility Function

The  LearningDiagramLayouts.getMaxNodeBounds() utility function runs through the array of nodes and pulls out the largest dimensions for you.
  getMaxNodeBounds : function (layoutContext) {
    var nodeCount = layoutContext.getNodeCount();
    var maxW = 0;
    var maxH = 0;
    for (var i = 0;i < nodeCount;i++) {
      var node = layoutContext.getNodeByIndex(i);
      var bounds = node.getContentBounds();
      maxW = Math.max(bounds.w, maxW);
      maxH = Math.max(bounds.h, maxH);
    return new DvtDiagramRectangle(0, 0, maxW, maxH);
So with this change to the node and node layout code here's how far we've got:
Slight progress with hardcoding removed
So not really a huge amount of obvious progress over our starting point, but at least the code is now more generic than it was before. Let's sort out those links - they actually look worse!

Correcting the Links

So far, when accessing nodes and links you've done it using index based lookup, basically looping through the arrays provided by the LayoutContext.  However, link and node objects each have individual IDs and the LayoutContext provides associated getNodeById() and getLinkById() APIs to access a specific instance.  This is really important when dealing with links, as each link itself has APIs getStartId() and getEndId() to return the IDs of the nodes that the link joins together.
So to correct all the links, rather than trying to keep a running total of the y position, as you did for the nodes, instead you can use the nodes associated with the link itself to work out the start and end positions. 
In terms of logic, the idea is that the link will leave the start node in the middle of its bottom edge and join the end node in the middle of its top edge.
Here's the amended version of the code for the links loop:
   var linkGap = 4;
   for (var j = 0;j < linkCount;j++) {
      var link = layoutContext.getLinkByIndex(j);
      var startNode = layoutContext.getNodeById(link.getStartId());
      var endNode = layoutContext.getNodeById(link.getEndId());
      var linkStartPos = LearningDiagramLayouts.getNodeEdgeMidPoint(startNode,"s");
      var linkEndPos = LearningDiagramLayouts.getNodeEdgeMidPoint(endNode,"n");    
      link.setPoints([linkStartPos.x, (linkStartPos.y + linkGap), 
                      linkEndPos.x, (linkEndPos.y - linkGap)]);
      var labelXPos = linkStartPos.x - (link.getLabelBounds().w + 20);
      var labelYPos = linkStartPos.y + 
                      ((linkEndPos.y - linkStartPos.y - link.getLabelBounds().h)/2);
      link.setLabelPosition(new DvtDiagramPoint(labelXPos, labelYPos));
You can find the complete JavaScript file, including the LearningDiagramLayouts.getNodeEdgeMidPoint() utility function, linked at the end of this article
The result of that change shows the links now anchored in the correct place and each of the labels now centered and right aligned to the relevant link.
Link Labels corrected
You'll notice in the calculations above that we make increasing use of the available metrics to work out the position of objects.  For example, when positioning a link label halfway along the length of the link, we again take the actual height of the label into account in the calculation, not just the length of the link.  This ensures that the result will look right, no matter what the label font size. 
There's just one problem left to sort out in this basic layout.  The algorithm is a little too literal, and the "Gamma" link, although correctly joining "Third Node" and "First Node", is taking the most direct route to do so and is overlapping the other links. So you need to add a little logic to re-route it.

Introducing Link Paths

So far we've been using links in a very simple mode, just a straight line joining two points in space.  However, links can follow complex paths, and indeed, part of your challenge when creating your own diagrams will be in calculating these paths though a busy nodescape to avoid overlaps.  There is no magic formula here to fit all occasions; it all comes down to math. 
In this case we want to route the "Gamma" link out to the left and back around to the "First Node" so we will have to define a path with 5 segments (assuming that the start and end points stick to our standard approach for the other links in the diagram).
It's worth mentioning at this point that links can, of course, be made up of straight lines with right angle junctions. But you can also get creative and use features such as curving lines and rounded junctions.  This is a topic for another day, for now we'll stick to orthogonal routing.
So what you have to do here is to calculate a series of points and call the same setPoints() API on the link to draw it: 
  if (linkEndPos.y > linkStartPos.y) {
    //A normal link
    link.setPoints([linkStartPos.x, (linkStartPos.y + linkGap), 
                    linkEndPos.x, (linkEndPos.y - linkGap)]);
  } else {
    // The case where we need to re-route 
    // Segment 1 - keep heading down for a short distance 
    var turn1 = new DvtDiagramPoint();
    turn1.x = linkStartPos.x;
    turn1.y = linkStartPos.y + (largestNodeBounds.h/2);
    // Segment 2 - head left far enough to avoid overlap with the other link labels
    var turn2 = new DvtDiagramPoint();
    turn2.x = turn1.x - (largestLinkLabelBounds.w + 40);
    turn2.y = turn1.y;
    // Segment 3 - Back up the diagram to a point equally above the end node
    var turn3 = new DvtDiagramPoint();
    turn3.x = turn2.x;
    turn3.y = linkEndPos.y - (largestNodeBounds.h/2);
    // Segment 4 - Back to the center line
    var turn4 = new DvtDiagramPoint();
    turn4.x = linkEndPos.x;
    turn4.y = turn3.y;
    // Segment 5 - And down to the end node for which 
    // we already have the coordinates
    //Now pass in the path:
    link.setPoints([linkStartPos.x, (linkStartPos.y + linkGap), 
                    turn1.x, turn1.y,
                    turn2.x, turn2.y,
                    turn3.x, turn3.y,
                    turn4.x, turn4.y,
                    linkEndPos.x, (linkEndPos.y - linkGap)]);
And here's the final result, to create this I did a little work to ensure that the label for the re-routed link was also in the corrected position:
Our basic layout is complete

The Full JavaScript Source

There is quite a lot of JavaScript now so I've linked the end result of this layout's evolution here.

In the Next Article

In the last two articles I've covered the basics of layouts, the positioning of nodes and the routing of links. You should be aware that there are multiple approaches that you can take here and not all layouts will consist of one loop through the nodes and one loop through the links.  Sophisticated or busy layouts may need multiple iterations through the population of nodes and links to calculate all of the relative positioning. 
Next time, I'll briefly explore some of the more interesting ways of defining links, introducing label rotation and curves.

Post a Comment:
  • HTML Syntax: NOT allowed

Oracle Data Visualizations provide a broad range of beautiful, interactive components for viewing and understanding data. This blog covers topics on the new features in Oracle Data Visualization components and how-to articles on advanced functionality.


« March 2015