Diagram is a powerful visualization that is used to represent different sets of data by displaying nodes and their relationships. It provides a pluggable framework for application developers to define custom layouts in JavaScript. In this post, we'll take a closer look at Diagram by creating a simple horizontal tree layout.

 

DiagramTreeLayout.png

Let's start by inserting a diagram into the page. Starting with the JET QuickStart template, insert the following code into home.tmpl.html file in the /templates folder:

 

<div>
    <div id="diagram" data-bind="ojComponent: {
            component: 'ojDiagram',
            layout: layoutFunc,
            styleDefaults : styleDefaults,
            nodes : nodes,
            links : links
        }"
       style="max-width:800px;width:100%; height:600px;">
  </div>
</div>




























 

Open the corresponding JavaScript module (home.js) from the /js/modules folder. This is where we will add the JavaScript code to create our Diagram layout, and generate data for nodes and links. First we have to refer to the Diagram module by including it in the define call:

 

define(['ojs/ojcore', 'knockout', 'jquery', 'ojs/ojdiagram'],
    function (oj, ko, $) {













 

The following section of the home.js file defines Diagram layout function, and declares the static arrays for the demo data for nodes and links:

 

  /**
     * The view model for the main content view template
     */
    function mainContentViewModel() {
        var self = this;

         // Assign the treeLayout function to Diagram
        self.layoutFunc = treeLayout;

        // Generate 3 colors that we will use for the Diagram nodes
        var colorHandler = new oj.ColorAttributeGroupHandler();
        var color1 = colorHandler.getValue("Group 1");
        var color2 = colorHandler.getValue("Group 2");
        var color3 = colorHandler.getValue("Group 3");

        // For this demo we will create a Diagram with 10 nodes and 3 color categories
        var diagramNodes = [
                {id: "N0", label: "N0", shortDesc: "Node N0, Group 1", icon: {color: color1}},
                {id: "N1", label: "N1", shortDesc: "Node N1, Group 1", icon: {color: color1}},
                {id: "N2", label: "N2", shortDesc: "Node N2, Group 2", icon: {color: color2}},
                {id: "N3", label: "N3", shortDesc: "Node N3, Group 3", icon: {color: color3}},
                {id: "N4", label: "N4", shortDesc: "Node N4, Group 1", icon: {color: color1}},
                {id: "N5", label: "N5", shortDesc: "Node N5, Group 2", icon: {color: color2}},
                {id: "N6", label: "N6", shortDesc: "Node N6, Group 3", icon: {color: color3}},
                {id: "N7", label: "N7", shortDesc: "Node N7, Group 1", icon: {color: color1}},
                {id: "N8", label: "N8", shortDesc: "Node N8, Group 2", icon: {color: color2}},
                {id: "N9", label: "N9", shortDesc: "Node N9, Group 3", icon: {color: color3}}
        ];

        // Create Diagram links
        var diagramLinks = [
                {id: "L0", shortDesc: "Link L0, connects N0 to N1", startNode: "N0", endNode: "N1"},
                {id: "L1", shortDesc: "Link L1, connects N0 to N2", startNode: "N0", endNode: "N2"},
                {id: "L2", shortDesc: "Link L2, connects N0 to N3", startNode: "N0", endNode: "N3"},
                {id: "L3", shortDesc: "Link L3, connects N1 to N4", startNode: "N1", endNode: "N4"},
                {id: "L4", shortDesc: "Link L4, connects N1 to N5", startNode: "N1", endNode: "N5"},
                {id: "L5", shortDesc: "Link L5, connects N3 to N6", startNode: "N3", endNode: "N6"},
                {id: "L6", shortDesc: "Link L6, connects N3 to N7", startNode: "N3", endNode: "N7"},
                {id: "L7", shortDesc: "Link L7, connects N3 to N8", startNode: "N3", endNode: "N8"},
                {id: "L8", shortDesc: "Link L8, connects N3 to N9", startNode: "N3", endNode: "N9"},

        ];

        self.nodes = ko.observableArray(diagramNodes);
        self.links = ko.observableArray(diagramLinks);

        // Style the nodes and links
        this.styleDefaults = {
            nodeDefaults: {
                icon: {width: 50, height: 50, shape: "square"}},
            linkDefaults: {
                startConnectorType: "none",
                endConnectorType: "arrow"
            }
        };
    }












 

Note the line self.layoutFunc = treeLayout.  This configures the Diagram to perform layout by calling the treeLayout function which we will now define.

Diagram layouts need to conform to a layout contract, which assumes that there is a function that will accept a DvtDiagramLayoutContext, which is a class containing information about Diagram nodes, links and their positions. In the first part of our treeLayout function below, we get the count of nodes and links from the layout context, and loop though the links to create a map of child node IDs to parent node IDs, which will be useful later on in our layout code.

 

/**
     * Performs a simple horizontal tree layout.  Assumes that links are specified
     * to create a strict hierarchy (i.e. every node can have 0 or 1 incoming links
     * and arbitrarily many outgoing links)
     *
     * @param {DvtDiagramLayoutContext} layoutContext the Diagram layout context
     */
    function treeLayout(layoutContext) {

        // Get the node and link counts from the layout context
        var nodeCount = layoutContext.getNodeCount();
        var linkCount = layoutContext.getLinkCount();

        // Create a child-parent map based on the links
        var childParentMap = {};
        for (var i = 0; i < linkCount; i++) {
            var link = layoutContext.getLinkByIndex(i);
            var parentId = link.getStartId();
            var childId = link.getEndId();

            childParentMap[childId] = parentId;
        }

























 

Next we loop though the diagram nodes to create a parent-child map and to find the largest node width and height. The root nodes (nodes without a parent) will appear under the key "undefined" in the parent-child map.

 

        var parentChildMap = {};
        var maxNodeWidth = 0;
        var maxNodeHeight = 0;

        // Loop though the nodes to create a parent-child map
        // and to find the largest node width and height
        for (var i = 0; i < nodeCount; i++) {
            var node = layoutContext.getNodeByIndex(i);

            var nodeId = node.getId();
            var parentId = childParentMap[nodeId];

            // Keep track of the largest node width and height, for layout purposes
            var nodeBounds = node.getContentBounds();
            maxNodeWidth = Math.max(nodeBounds.w, maxNodeWidth);
            maxNodeHeight = Math.max(nodeBounds.h, maxNodeHeight);

            // Add this node id to the parent-child map for that parent
            // The root nodes (i.e. nodes with no parent) will appear under the key undefined in the parent-child map
            var children = parentChildMap[parentId];
            if (!children) {
                children = [];
                parentChildMap[parentId] = children;
            }
            children.push(nodeId);
        }

























 

In the next section of the code, we call the function to lay out the tree of nodes (which will be explained later in this post), and then we lay out the links. For each link, we specify the start point to be the center right side of the parent node, and the end point to be the center left side of the child node. Then we specify 2 points in between that make the links draw orthogonal lines.

 

        // For horizontal layout, calculate the level width based on the widest node in this level
        // and calculate space for each node based on the tallest node
        var levelSize = maxNodeWidth * 1.5;
        var siblingSize = maxNodeHeight * 1.1;

        // Layout the nodes
        layoutSubTree(layoutContext, undefined, parentChildMap, childParentMap, levelSize, siblingSize, -1, [0]);

        // Layout the links
        for (var i = 0; i < linkCount; i++) {
            var link = layoutContext.getLinkByIndex(i);
            var parentNode = layoutContext.getNodeById(link.getStartId());
            var childNode = layoutContext.getNodeById(link.getEndId());
            var parentNodePos = parentNode.getPosition();
            var parentNodeBounds = parentNode.getContentBounds();
            var childNodePos = childNode.getPosition();
            var childNodeBounds = childNode.getContentBounds();

            // Draw horizontal link between center of parent right edge and center of child left edge
            var startX = parentNodePos.x + parentNodeBounds.x + parentNodeBounds.w + link.getStartConnectorOffset();
            var startY = parentNodePos.y + parentNodeBounds.y + parentNodeBounds.h * .5;
            var endX = childNodePos.x + childNodeBounds.x - link.getEndConnectorOffset();
            var endY = childNodePos.y + childNodeBounds.y + childNodeBounds.h * .5;

            // Set the start, end and the middle points on the link
            link.setPoints([startX, startY, (startX + endX) * 0.5, startY, (startX + endX) * 0.5, endY, endX, endY]);
        }
    };

























 

The core of this simple layout is the function which recursively lays out the nodes for the Diagram. If the current node is a parent node, then we call the function recursively to lay out the subtree first and the center the parent node vertically next to its children, and we center the labels inside the nodes.

 

  /**
     * Lays out the subtree with the specified root id
     *
     * @param {DvtDiagramLayoutContext} layoutContext the Diagram layout context
     * @param {string} rootId the id of the subtree root, may be null if this is the top-level entry call
     * @param {object} parentChildMap A map from parent id to an array of child ids
     * @param {object} childParentMap A map from child id to parent id
     * @param {number} levelSize The width (including spacing) allocated to each level of the tree
     * @param {number} siblingSize The height (including spacing) allocated to siblings in the same level
     * @param {number} currentDepth the depth of rootId within the tree
     * @param {array} leafPos A singleton array containing the current y position for leaf nodes that will be updated during layout
     *
     * @return {object} the position of the subtree root
     */
     function layoutSubTree (layoutContext, rootId, parentChildMap, childParentMap, levelSize, siblingSize, currentDepth, leafPos) {

        var currentPos = leafPos[0];
        var childNodes = parentChildMap[rootId];

        // If this is a root node for other child nodes, then layout the child nodes
        if (childNodes) {

            currentPos = 0;
            for (var i = 0; i < childNodes.length; i++) {
                // Layout the child subtrees recursively
                var childPosition = layoutSubTree(layoutContext, childNodes[i], parentChildMap, childParentMap, levelSize, siblingSize, currentDepth + 1, leafPos);

                // Center parent node vertically next to the children
                currentPos += childPosition.y / childNodes.length;
            }
        } else {
            // Leaf node, advance the current leaf position
            leafPos[0] += siblingSize;
        }

        var position = {x: currentDepth * levelSize, y: currentPos};

        if (rootId) {
            var root = layoutContext.getNodeById(rootId);
            if (root) {
                var bounds = root.getContentBounds();
                var rootPos = {x: position.x - bounds.x - bounds.w * .5, y: position.y};
                root.setPosition(rootPos);

                // Center the label inside the node
                var nodeLabelBounds = root.getLabelBounds();
                if (nodeLabelBounds) {
                    var labelX = bounds.x + rootPos.x + 0.5 * (bounds.w - nodeLabelBounds.w);
                    var labelY = bounds.y + rootPos.y + 0.5 * (bounds.h - nodeLabelBounds.h);
                    root.setLabelPosition({'x': labelX, 'y': labelY});
                }
            }
        }
        return position;
    };

    return mainContentViewModel;
});

























 

You can play with different sets of nodes and links to create different layouts, like shown in these images:

DiagramTreeLayout1.pngdiagramTreeLyout3.png

 

A more detailed introduction to diagram layouts can be found on the Data Visualization Blog. While these posts were written with ADF in mind, the content remains largely applicable in JET. The JET cookbook also contains numerous Diagram examples with varying layouts.

We hope that this post clarifies the steps to create a new Diagram layout in JET and we are looking forward to your questions and comments.