Saturday, August 22, 2015

D3.js: How to draw a Node Relationship Graph like Neo4J?

First of all, I am deeply sorry for the lack of update for this blog! I have been really busy with my work, family and self-learning.

Anyway, this entry is about some cool technologies that I have been spending time lately - Neo4j and D3.js.

I have always been curious about graph database. Recently, I have finally conceded to my better curious half and started to get my hands on Neo4j (one of the more popular graph database engine).

I am greatly impressed by its Web GUI that is able to draw a proper Node -> Relationship graph that is both pretty and practical.

Courtesy from Neo4j website

At work, I have a sudden need for such a graph to analyze some really complex text-based source-target relationships. That was how D3.js came into play!

I have always known that I need to lay my hands on D3.js one day for data visualisation. This need at work just justified it.

After some googling and reading, I ended up with the codes below:
NOTE: Simplified to increase readability!

<!DOCTYPE html>
<html lang="en">
<head>
<%@page pageEncoding="UTF-8" contentType="text/html; charset=UTF-8"%>
<title>Node Relationship Graph</title>
<script type="text/javascript" src="d3/d3.min.js"></script>
<style>

path.link {
  fill: none;
  stroke: #666;
  stroke-width: 1.5px;
}

circle {
  fill: #ccc;
  stroke: #fff;
  stroke-width: 1.5px;
}

text {
  fill: #000;
  font: 10px sans-serif;
  pointer-events: none;
}

</style>
</head>

    <BODY>
<script type="text/javascript">
d3.csv("data/lsrel.csv", function(error, links) {

var nodes = {};
var rel = {};

// Compute the distinct nodes from the links.
links.forEach(function(link) {
    link.id = "rel" + link.relnum; 
    // link.relnum = link.relnum;
   var sLinkSrc = link.source;
   var sLinkTgt = link.target;
    link.source = nodes[link.source] || 
        (nodes[link.source] = {name: link.source, relcnt: 0, srccnt: 0, tgtcnt: 0});
    link.target = nodes[link.target] || 
        (nodes[link.target] = {name: link.target, relcnt: 0, srccnt: 0, tgtcnt: 0});
    link.relationship = link.relationship;
   
   if (nodes[sLinkSrc])
   {
         nodes[sLinkSrc]["relcnt"] = nodes[sLinkSrc]["relcnt"]+1;
         nodes[sLinkSrc]["srccnt"] = nodes[sLinkSrc]["srccnt"]+1;
   }

   if (nodes[sLinkTgt])
   {
         nodes[sLinkTgt]["relcnt"] = nodes[sLinkTgt]["relcnt"]+1;
         nodes[sLinkTgt]["tgtcnt"] = nodes[sLinkTgt]["tgtcnt"]+1;
   }

   // console.log(JSON.stringify(nodes));
   // console.log("NODEPROP: " + nodes[sLinkSrc].name);

});

var width = screen.width-80,
    height = screen.height-80;

console.log("Width: " + width);
console.log("Height: " + height);

var force = d3.layout.force()
    .nodes(d3.values(nodes))
    .links(links)
    .size([width, height])
    .linkDistance(350)
    .charge(-800)
    .on("tick", tick)
    .start();

var drag = force.drag()
            .on("dragstart", dragstart);

var svg = d3.select("body").append("svg")
    .attr("width", width)
    .attr("height", height);

// build the arrow.
svg.append("svg:defs").selectAll("marker")
    .data(["end"])
  .enter().append("svg:marker")
    .attr("id", String)
    .attr("viewBox", "0 -5 10 10")
    .attr("refX", 22)
    .attr("refY", -1)
    .attr("markerWidth", 8)
    .attr("markerHeight", 8)
    .attr("orient", "auto")
  .append("svg:path")
    .attr("d", "M0,-5L10,0L0,5");

// add the links and the arrows
var path = svg.append("svg:g").selectAll("path")
    .data(force.links())
  .enter()
.append("svg:path")
   .attr("id", function(d) { return d.id; } )
    .attr("class", "link")
    .attr("marker-end", "url(#end)");

var mytext = svg.append("svg:g").selectAll("text")
.data(force.links())
.enter()
.append("text")
.attr("dx", "150")
.attr("dy", "-8")
 .append("textPath")
 .attr("xlink:href", function(d) { return "#" + d.id; })
 .attr("style", "fill:magenta; font-weight:bold; font-size:12")
 .text(function(d) { return d.relationship; } );

// define the nodes
var node = svg.selectAll(".node")
    .data(force.nodes())
  .enter().append("g")
    .attr("class", "node")
    .call(force.drag);

// add the nodes
node.append("circle")
    .attr("r", 12)
    .attr("fill", "grey")
   .append("svg:title")
   .text(function(d) { return "Source: " + d.srccnt + " ~ Target: " + d.tgtcnt; });

// add the text
node.append("text")
    .attr("x", 12)
    .attr("dy", ".35em")
    .attr("style", "fill:blue; font-weight:bold; font-size:16")
    .text(function(d) { return d.name; });

node.append("text")
   .attr("text-anchor", "middle")
    // .attr("style", "font-weight:bold; font-size:12")
    .attr("style", function(d) {
      if (d.relcnt >= 3)
      {
         return "font-weight:bold; font-size:12; fill:red"
      }
      else
      {
         return "font-weight:bold; font-size:12"
      }
   })
   .text(function(d) { return d.relcnt; });

// add the curvy lines
function tick() {
    path.attr("d", function(d) {
        var dx = d.target.x - d.source.x,
            dy = d.target.y - d.source.y,
            dr = Math.sqrt(dx * dx + dy * dy);
        return "M" +
            d.source.x + "," +
            d.source.y + "A" +
            dr + "," + dr + " 0 0,1 " +
            d.target.x + "," +
            d.target.y;
    });

    node
         .attr("transform", function(d) {
             return "translate(" + d.x + "," + d.y + ")"; });
}

function dragstart(d)
{
   d3.select(this).classed("fixed", d.fixed = true);
}
if (error)
{
   console.log(error);
}
else
{
   console.log(nodes);
   console.log(links);
   console.log(path);
   console.log(rel);
}
});

</script>

    </BODY>
</HTML>      


A sample of the input file "lsrel.csv" is as follow:

relnum,source,target,relationship
6,c,a,DependsOn
5,c,d,Anti-Collocated
1,a,b,DependsOn
2,a,c,StartAfter
3,b,c,StopAfter
4,b,d,Collocated

The output of the D3.js script based on the data above:

Hope you guys like it!

6 comments:

  1. Sorry,do you try to connect neo4j , not use csv? can you teach me?

    ReplyDelete
    Replies
    1. What do you mean? Can you please elaborate on your needs?

      Delete
  2. Thank you Daniel!
    Simple and easy to understand.
    Do you know how to show the arrows direction (> or <)?
    Do you know how to change the arrow in order to be lines not curves?
    Do you know how to hide the relationship number inside the bubble?
    Thank you again!

    ReplyDelete
  3. Thanks a million Daniel!
    In order to adapt the graph to my needs, may I ask you if you know how to:
    - Show the arrow pointer of the relationship?
    - Change the arcs into a strike line?
    - Change the orientation of the text of the relationship when is downside?

    ReplyDelete
    Replies
    1. Hi,
      I tried that some time ago, but do not remember the details.

      But, I referred to the following technote while doing that =>
      https://www.w3.org/TR/SVG/paths.html

      The line of code you should focus on =>

      // add the curvy lines
      function tick() {
      path.attr("d", function(d) {
      var dx = d.target.x - d.source.x,
      dy = d.target.y - d.source.y,
      dr = Math.sqrt(dx * dx + dy * dy);
      return "M" +
      d.source.x + "," +
      d.source.y + "A" +
      dr + "," + dr + " 0 0,1 " +
      d.target.x + "," +
      d.target.y;
      });

      ***

      Currently, you can see that it's using the "A" attribute (ecliptical arc).

      Hope that helps!

      Delete
    2. As for the arrow direction, that's a bit tougher. I have tried with no success.

      But, I was referring to https://www.w3.org/TR/svg-markers/#VertexMarkerProperties

      The orientation of the text is also a tough one and I tested that too so time ago. I remember it follows the direction of the arrow.

      I have not spent a lot of time of this, sorry!

      Delete