DAGs visualization using dagre-3d

Directed Acyclic Graphs are directed graphs, that have a topological ordering, a sequence of the vertices such that every edge is directed from earlier to later in the sequence.

DAGs are often used in ETL processes to define pipelines for ingestion/transform of files and lately as a replacement of block-chain in cryptocurrencies such as IOTA or Byteball.

Even though there are some tools like Apache Airflow that allow the users to have an overview of the DAG, sometimes you want to show the DAG in a dashboard, or in a report, this can be achieved by rendering the DAG using Javascript and Dagre.

Dagre is a JavaScript library that makes it easy to lay out directed graphs on the client-side.
The dagre-d3 library acts as a front-end to dagre, providing actual rendering using D3js.

<!DOCTYPE html>  
<html>  
   <head>
      <meta charset="utf-8">
      <meta name="viewport" content="width=device-width">
      <title>JS Bin</title>
      <script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.16/d3.min.js"></script>
      <script src="https://cdnjs.cloudflare.com/ajax/libs/dagre-d3/0.4.17/dagre-d3.min.js"></script>
   </head>
   <body>
      <div class="container">
         <h2>DAGs</h2>
         <div class="well">
            <button id="zoom_in">+</button>
            <button id="zoom_out">-</button>
            <svg class="test" width=960 height=400>
              <g/>
            </svg>
         </div>
      </div>   
   </body>
</html>  
body {  
  width: 960px;
  margin: 0 auto;
  color: #333;
  font-weight: 300;
  font-family: "Helvetica Neue", Helvetica, Arial, sans-serf;
}


section {  
  margin-bottom: 3em;
}

section p {  
  text-align: justify;
}

svg {  
  border: 1px solid #ccc;
  overflow: hidden;
  margin: 0 auto;
    background:white;
}


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

.node rect {
  stroke: #333;
  fill: #fff;
  stroke-width: 1.5px;
}

.edgePath path {
  stroke: #222;
   background:#444;
  stroke-width: 2px;
}

table {  
  border-spacing: 0;
}

table td {  
  padding: 7px;
}
'use strict';  
//
// setup Zoom from example http://bl.ocks.org/mgold/f61420a6f02adb618a70
// 
var width = 960,  
    height = 400,
    center = [width / 2, height / 2];
//
var svg = d3.select('svg'),  
    inner = svg.select('g');
//

var zoom = d3.behavior.zoom()  
.translate([0, 0])
.scale(1)
.size([900, 400])
.scaleExtent([1, 8])
.on('zoom', zoomed);
//
svg  
   .call(zoom) // delete this line to disable free zooming
   .call(zoom.event);

function zoomed() {  
   inner.attr('transform', 'translate(' + zoom.translate() + ')scale(' + zoom.scale() + ')');
}

function interpolateZoom(translate, scale) {  
   var self = this;
   return d3.transition().duration(350).tween('zoom', function () {
      var iTranslate = d3.interpolate(zoom.translate(), translate),
          iScale = d3.interpolate(zoom.scale(), scale);
      return function (t) {
         zoom
            .scale(iScale(t))
            .translate(iTranslate(t));
         zoomed();
      };
   });
}

function zoomClick() {  
   var clicked = d3.event.target,
       direction = 1,
       factor = 0.2,
       target_zoom = 1,
       center = [width / 2, height / 2],
       extent = zoom.scaleExtent(),
       translate = zoom.translate(),
       translate0 = [],
       l = [],
       view = {
          x: translate[0],
          y: translate[1],
          k: zoom.scale()
       };

   d3.event.preventDefault();
   direction = (this.id === 'zoom_in') ? 1 : -1;
   target_zoom = zoom.scale() * (1 + factor * direction);

   if (target_zoom < extent[0] || target_zoom > extent[1]) {
      return false;
   }

   translate0 = [(center[0] - view.x) / view.k, (center[1] - view.y) / view.k];
   view.k = target_zoom;
   l = [translate0[0] * view.k + view.x, translate0[1] * view.k + view.y];

   view.x += center[0] - l[0];
   view.y += center[1] - l[1];

   interpolateZoom([view.x, view.y], view.k);
}

d3.selectAll('button').on('click', zoomClick);  
//
//
//
//  tcp-state-diagram EXAMPLE
//
// Create a new directed graph
var g = new dagreD3.graphlib.Graph().setGraph({});

// States and transitions from RFC 793
var states = ["TASK1","TASK2", "TASK3", "TASK4","TASK5"];

// Automatically label each of the nodes
states.forEach(function (state) {  
   g.setNode(state, {
      label: state
   });
});

// Set up the edges
g.setEdge('TASK1', 'TASK2', {  
   label: 'label'
});
g.setEdge('TASK1', 'TASK3', {  
   label: 'label'
});
g.setEdge('TASK1', 'TASK4', {  
   label: 'label'
});
g.setEdge('TASK3', 'TASK5', {  
   label: 'label'
});
g.setEdge('TASK4', 'TASK5', {  
   label: 'label'
});
g.setEdge('TASK2', 'TASK5', {  
   label: 'label'
});

// Set some general styles
g.nodes().forEach(function (v) {  
   var node = g.node(v);
   node.rx = node.ry = 5;
});

// Add some custom colors based on state
g.node('TASK1').style = 'fill: #7f7';  
g.node('TASK2').style = 'fill: #7f7';  
g.node('TASK3').style = 'fill: #ff7';  
g.node('TASK4').style = 'fill: #f77';

console.log(g)

// Create the renderer
var render = new dagreD3.render();

// Run the renderer. This is what draws the final graph.
render(inner, g);

// Center the graph
var initialScale = 0.75;  
var _height = svg.attr('height') - g.graph().height;  
var _width = svg.attr('width') - g.graph().width;  
console.log(height / _height);

zoom.translate([(svg.attr('width') - g.graph().width * initialScale) / 2, 10]).scale(1).event(svg);

//svg.transition().duration(750).call(zoom.translate([0, 0]).scale(1).event);
//svg.transition().duration(500).attr('transform', 'scale(0.75) translate(0,0)');

JS Bin on jsbin.com

The result is pretty cool and can be customised using CSS for color and javascript to add behaviour on the different nodes, like onclick, onhover, etc...

Add a click behaviour to a node of the graph

The nodes of the graph can be found in the g object under the property _nodes
Something like the code below will add a click listener on the node, in the example, it will pop up an alert box.

g._nodes  
   .TASK1
   .elem
   .addEventListener("click", () => alert("this is task1"))

Davide Andreazzini

Read more posts by this author.