Automatic graph layout with JointJS and Dagre

July 29th, 2013

JointJS is a diagramming library that focuses on rendering and interacting with diagrams. This post shows how to integrate JointJS with Dagre, the directed graph layout engine for JavaScript, in order to automatically render and layout directed graphs.

JointJS library's main focus is on creating diagrams and interacting with them. Its MVC architecture strictly separates models (graph, element, link) from views. Models hold geometrical and presentation attributes of diagram elements and links, while views are responsible for rendering models onto the paper and handling interaction. JointJS uses SVG to render all the graphics onto the screen.

Dagre is a directed graph layout engine for JavaScript. Dagre is a neat library with three key priorities. First, it operates completely client side. Second, it's very fast. And third, it's rendering agnostic. All these characteristics make Dagre a great fit for integration with JointJS.

Demo

The demo below shows a combination of both libraries. It is a simple application that automatically builds a diagram from a directed graph represented as an adjacency list.

The textarea contains the graph definition in the form of an adjacency list. An adjacency list is a compact representation of a graph associating vertices with an unordered list of their neighbors.


Implementation

The implementation is quite straightforward, although there are some things you might not be familiar with if you're new to JointJS.

First, we create a graph and paper objects. A JointJS graph is a model that holds all the cells (elements and links). A paper is a view that knows how to render cells added to the graph.


var graph = new joint.dia.Graph;

var paper = new joint.dia.Paper({

    el: $('#paper'),
    width: 2000,
    height: 2000,
    gridSize: 1,
    model: graph
});

The paper takes two important attributes. The holder for the paper element el and the graph as its model.

Next, we create a function that walks through the adjacency list and returns an array of cells that will be added to the graph later on. Note that JointJS comes prebuilt with the Underscore library so you'll see some of the _. methods used in the code below.


function buildGraphFromAdjacencyList(adjacencyList) {

    var elements = [];
    var links = [];
    
    _.each(adjacencyList, function(edges, parentElementLabel) {
        elements.push(makeElement(parentElementLabel));

        _.each(edges, function(childElementLabel) {
            links.push(makeLink(parentElementLabel, childElementLabel));
        });
    });

    // Links must be added after all the elements. This is because when the links
    // are added to the graph, link source/target
    // elements must be in the graph already.
    return elements.concat(links);
}

In this function, we're using two other helpers, makeLink and makeElement. These helper functions create JointJS link and element models, respectively. The first one takes two element IDs and creates a link between those elements. The second one takes an element ID and creates a rectangle element with this ID (which also becomes the label). Let's have a look at these functions:


function makeLink(parentElementLabel, childElementLabel) {

    return new joint.dia.Link({
        source: { id: parentElementLabel },
        target: { id: childElementLabel },
        attrs: { '.marker-target': { d: 'M 4 0 L 0 2 L 4 4 z' } },
        smooth: true
    });
}

function makeElement(label) {

    var maxLineLength = _.max(label.split('\n'), function(l) { return l.length; }).length;

    // Compute width/height of the rectangle based on the number 
    // of lines in the label and the letter size. 0.6 * letterSize is
    // an approximation of the monospace font letter width.
    var letterSize = 8;
    var width = 2 * (letterSize * (0.6 * maxLineLength + 1));
    var height = 2 * ((label.split('\n').length + 1) * letterSize);

    return new joint.shapes.basic.Rect({
        id: label,
        size: { width: width, height: height },
        attrs: {
            text: { text: label, 'font-size': letterSize, 'font-family': 'monospace' },
            rect: {
                width: width, height: height,
                rx: 5, ry: 5,
                stroke: '#555'
            }
        }
    });
}

Now, we're able to collect JointJS cells and populate the graph. Our paper (a view for the graph) automatically reacts to new cells added to the graph, and renders views for our links and elements.


var cells = buildGraphFromAdjacencyList(adjacencyList);
graph.resetCells(cells);

Finally, we can use the joint.layout.DirectedGraph to auto-layout our graph. This plugin is a tiny wrapper around Dagre layout function. Simply, it transforms JointJS graph model into a data structure Dagre understands. After Dagre is finished with layouting, the plugin applies Dagre generated positions to JointJS elements.


 joint.layout.DirectedGraph.layout(graph, { setLinkVertices: false });

That's it! We've just built a simple application for rendering and layouting directed graphs represented by an adjacency list. The full source code of this demo is available here. You'll also need JointJS core files (joint.js and joint.css) and the joint.layout.DirectedGraph plugin that already contains Dagre. All is available from the JointJS Download page.

Discussion



comments powered by Disqus