Jamie's Blog

Ruby developer. CTO. Swimmer. Always trying to write more

Generating a hierarchical graph with Rails & Javascript

2014 10 20 at 21.50

At WorkCompass, we can import a .csv file of employees from a customer but, as anyone who’s done anything with CSV files knows, you need a lot of validation to make sure the data is clean. Among the automated checks are some manual things like “these are the email domains used, are you sure they’re correct?” and, lastly, I build a graph showing the hierarchical corporate structure. Currently this all runs offline as a Rake task but today I built a proper admin page for it. I didn’t really need the graph but what the hell…

My Bonus Challenge: display the graph in a browser

Let’s be clear: I’m talking about a graph in the nodes-and-edges CompSci definition not line charts, pie charts, etc. Here’s how I decided to structure it.

First, generate the nodes and edges

Graphviz is a popular graph visualisation application and a standard format called DOT. Because of the popularity of GraphViz, it makes sense to use the DOT format and take advantage of that rich ecosystem. I use the graph gem to construct the DOT file:

require 'graph'

def graph
  nodes = []
  digraph do
    node_attribs << filled

    importer.each_row do |row, name, email, manager, auth|
      case auth.downcase
      when 'admin' then red << node(name)
      when 'manager' then orange << node(name)
      when 'employee' then white << node(name)
        node name

      edge name, manager if manager.present?

Essentially, we iterate through the CSV file (importer.each_row is an internal helper method for doing this), reading in the employee’s name, email, authorisation level and the name of their manager. For each employee, we construct a node node(name) and optionally add them to a colour. Then, if the employee has a manager, we construct an edge between the employee and the manager.

The DOT output looks like this:

node [ style = filled ](#);
"Mary Gilmore"       [ color = orange       ](#);
"Dave Carey"          [ color = white        ](#);
"Senan Glynn"        [ color = white        ](#);
"James Davin"         [ color = white        ](#);
"James Feely"         [ color = white        ](#);
"Fred Kyne"       [ color = white        ](#);
"Mary Monaghan"   [ color = white        ](#);
"Anna O'Donnell"     [ color = white        ](#);
"Cathal Reynolds"    [ color = white        ](#);
"Fred Geraghty"   [ color = white        ](#);
"Annette Murray"     [ color = orange       ](#);
"Dave McGrath"      [ color = white        ](#);
"Seamus Roche"       [ color = white        ](#);
"Mary Conneally"     [ color = white        ](#);
"Frances Higgins"    [ color = white        ](#);
"Alan Shaw"          [ color = red          ](#);
"Anne Marie Creaven" [ color = red          ](#);
"Eimear Kelly"       [ color = red          ](#);
"Dave Carey" -> "Mary Gilmore";
"Senan Glynn" -> "Mary Gilmore";
"James Davin" -> "Mary Gilmore";
"James Feely" -> "Mary Gilmore";
"Fred Kyne" -> "Mary Gilmore";
"Mary Monaghan" -> "Mary Gilmore";
"Anna O'Donnell" -> "Mary Gilmore";
"Cathal Reynolds" -> "Mary Gilmore";
"Fred Geraghty" -> "Mary Gilmore";
"Dave McGrath" -> "Annette Murray";
"Seamus Roche" -> "Annette Murray";
"Mary Conneally" -> "Annette Murray";
"Frances Higgins" -> "Annette Murray";

` As you can see, it’s just a list of nodes and their properties, followed by a simple syntax for specifing node-to-node relationships (edges).

Get this to the browser

I could render this on the server as a .png and just serve it up to the browser but that’s a little more complicated (storing, managing, sweeping old images etc) and wasteful than I’d like. Instead, let’s just stick it into the Rails template

<script type="text/vnd.graphviz" id="dot">
  <%= raw @importer.graph %>

This simply inserts the raw DOT file into a script tag (with an appropriate MIME type so the browser will ignore it).

Sprinkle in some Javascript

We use dagre-d3 library to render this DOT format into SVG. In our HTML we add an empty SVG element to render to:

<svg id="graphviz" width="800" height="600">
  <g transform="translate(20,20)">

Then, add the Dagre, D3 and Graphlib-dot javascript libraries. Specifically, I’m using the .parse() method in v0.6.1-pre of graphlib-dot which is not yet released.

<script src="http://d3js.org/d3.v3.min.js"></script>
<script src="http://cpettitt.github.io/project/dagre-d3/latest/dagre-d3.min.js"></script>
<script src="http://cpettitt.github.io/project/graphlib-dot/latest/graphlib-dot.min.js"></script>
  // Input related code goes here
  var inputGraph = $("#dot");

  dagreD3.zoom(d3.select("#graphviz"), dagreD3.zoom.panAndZoom(d3.select("svg g g")));

  var result;
    result = graphlibDot.parse(inputGraph.html());
  } catch (e)
    throw e;

  if (result)
var svg = d3.select("#graphviz");
var renderer = new dagreD3.Renderer();
var layout = renderer.run(result, svg.select("g g"));

This code is blatantly adapted from the Dagre interactive demo. First, we locate our DOT script element, we locate our SVG element that we’ll render to, and we set a couple of options to allow the user to zoom in and pan around. Then we parse the DOT format, and render it.

And style

By default, the output is a bit ugly but we can style it with CSS:

  border: 1px solid #999;
  overflow: hidden;

  font-weight: 300;
  font-family: "Helvetica Neue", Helvetica, Arial, sans-serf;
  font-size: 14px;

.node rect
  stroke: #333;
  fill: #fff;

.edgeLabel rect
  fill: #fff;

.edgePath path
  stroke: #333;
  fill: none;

The Result

Now, that’s better. Not bad for an hour’s work. Obviously if this was a customer-facing I’d probably spend a bit longer tidying it up etc, but it’s a good start.