sabato 1 novembre 2014

Sankey Diagram with D3JS




I received a task recently which gave me a lot of fun.
I had to create a diagram like the one above with adapted numbers for my company core business.
The idea was to have such numbers in a Power point. One of the biggest challenge was to dig in the DB in order to find the correct informations (but this is beyond the scope of this post).
One of the funniest task was to build the power point presentation! How can I build a power point reporting the information such the one in the diagram above?

I googled it and I discovered that this diagram is called "Sankey" diagram.
I tried then to find any tool who was supporting the creation of those type of diagrams.
Of course, first choice was google chart:
https://developers.google.com/chart/interactive/docs/gallery/sankey

I found that this lybrary is quite static, it does not give too much freedom. Additionally the graphic is quite poor.

I searched deeper and I fell in love for the library D3js
http://d3js.org/

This library support too many of the non conventional diagrams. No words to mention, just have a look at the examples:
https://github.com/mbostock/d3/wiki/Gallery

I must confess that it is not really straightforward, not as easy as the google chart one.
As well the possibilities are enormous.

In this post I want to have my usual copy and paste snippets.
I found a very interesting tutorial here:
http://bl.ocks.org/d3noob/5028304

I have reported here the result of my task (of course number changed):
http://marcoandolfi.eu5.org/sankey_test/viewer.html

And now the copy and paste part.
You need three things:
- The javascript library
- The json file.
- The HTML file rendering it.

The javascript library Sankey.js, I stored a copy on my website here).
Here it follow an example of possible JSON file. Very easy: links and nodes.

 {  
 "links": [  
 {"source":"1A","target":"2A","value":"10"},  
 {"source":"1A","target":"2B","value":"1" },  
 {"source":"1B","target":"2B","value":"15"},  
 {"source":"1C","target":"2A","value":"20"},  
 {"source":"1C","target":"2B","value":"14"},  
 {"source":"1C","target":"2C","value":"45"},  
 {"source":"1D","target":"2A","value":"25"},  
 {"source":"1D","target":"2C","value":"25"},  
 {"source":"2A","target":"3A","value":"9" },  
 {"source":"2A","target":"3B","value":"15"},  
 {"source":"2A","target":"3C","value":"28"},  
 {"source":"2A","target":"3D","value":"17"},  
 {"source":"2B","target":"3B","value":"15"},  
 {"source":"2B","target":"3C","value":"15"},  
 {"source":"2C","target":"4B","value":"50"},  
 {"source":"3A","target":"4A","value":"10"},  
 {"source":"3B","target":"4B","value":"20"},  
 {"source":"3B","target":"4A","value":"5" },  
 {"source":"3C","target":"4B","value":"40"}  
 ] ,  
 "nodes": [  
 {"name":"1A"},  
 {"name":"1B"},  
 {"name":"1C"},  
 {"name":"1D"},  
 {"name":"2A"},  
 {"name":"2B"},  
 {"name":"2C"},  
 {"name":"3A"},  
 {"name":"3B"},  
 {"name":"3C"},  
 {"name":"3D"},  
 {"name":"4A"},  
 {"name":"4B"}  
 ] }  


Finally the HTML page (it is needed to adapt only the bold part: Json file name or ajax call and the location of your Sankey.js).
 <!DOCTYPE html>  
 <meta charset="utf-8">  
 <title>SANKEY Experiment</title>  
 <style>  
 .node rect {  
  cursor: move;  
  fill-opacity: .9;  
  shape-rendering: crispEdges;  
 }  
 .node text {  
  pointer-events: none;  
  text-shadow: 0 1px 0 #fff;  
 }  
 .link {  
  fill: none;  
  stroke: #000;  
  stroke-opacity: .2;  
 }  
 .link:hover {  
  stroke-opacity: .5;  
 }  
 </style>  
 <body>  
 <p id="chart">  
 <script src="http://d3js.org/d3.v3.js"></script>  
 <script src="sankey.js"></script>  
 <script>  
 var units = "Widgets";  
 var margin = {top: 10, right: 10, bottom: 10, left: 10},  
   width = 1200 - margin.left - margin.right,  
   height = 740 - margin.top - margin.bottom;  
 var formatNumber = d3.format(",.0f"),  // zero decimal places  
   format = function(d) { return formatNumber(d) + " " + units; },  
   color = d3.scale.category20();  
 // append the svg canvas to the page  
 var svg = d3.select("#chart").append("svg")  
   .attr("width", width + margin.left + margin.right)  
   .attr("height", height + margin.top + margin.bottom)  
  .append("g")  
   .attr("transform",   
      "translate(" + margin.left + "," + margin.top + ")");  
 // Set the sankey diagram properties  
 var sankey = d3.sankey()  
   .nodeWidth(36)  
   .nodePadding(10)  
   .size([width, height]);  
 var path = sankey.link();  
 // load the data  
 d3.json("data.json", function(error, graph) {  
   var nodeMap = {};  
   graph.nodes.forEach(function(x) { nodeMap[x.name] = x; });  
   graph.links = graph.links.map(function(x) {  
    return {  
     source: nodeMap[x.source],  
     target: nodeMap[x.target],  
     value: x.value  
    };  
   });  
  sankey  
    .nodes(graph.nodes)  
    .links(graph.links)  
    .layout(32);  
 // add in the links  
  var link = svg.append("g").selectAll(".link")  
    .data(graph.links)  
   .enter().append("path")  
    .attr("class", "link")  
    .attr("d", path)  
    .style("stroke-width", function(d) { return Math.max(1, d.dy); })  
    .sort(function(a, b) { return b.dy - a.dy; });  
 // add the link titles  
  link.append("title")  
     .text(function(d) {  
         return d.source.name + " → " +   
         d.target.name + "\n" + format(d.value); });  
 // add in the nodes  
  var node = svg.append("g").selectAll(".node")  
    .data(graph.nodes)  
   .enter().append("g")  
    .attr("class", "node")  
    .attr("transform", function(d) {   
            return "translate(" + d.x + "," + d.y + ")"; })  
   .call(d3.behavior.drag()  
    .origin(function(d) { return d; })  
    .on("dragstart", function() {   
            this.parentNode.appendChild(this); })  
    .on("drag", dragmove));  
 // add the rectangles for the nodes  
  node.append("rect")  
    .attr("height", function(d) { return d.dy; })  
    .attr("width", sankey.nodeWidth())  
    .style("fill", function(d) {   
            return d.color = color(d.name.replace(/ .*/, "")); })  
    .style("stroke", function(d) {   
            return d3.rgb(d.color).darker(2); })  
   .append("title")  
    .text(function(d) {   
            return d.name + "\n" + format(d.value); });  
 // add in the title for the nodes  
  node.append("text")  
    .attr("x", -6)  
    .attr("y", function(d) { return d.dy / 2; })  
    .attr("dy", ".35em")  
    .attr("text-anchor", "end")  
    .attr("transform", null)  
    .text(function(d) { return d.name; })  
   .filter(function(d) { return d.x < width / 2; })  
    .attr("x", 6 + sankey.nodeWidth())  
    .attr("text-anchor", "start");  
 // the function for moving the nodes  
  function dragmove(d) {  
   d3.select(this).attr("transform",   
     "translate(" + (  
            d.x = Math.max(0, Math.min(width - d.dx, d3.event.x))  
          ) + "," + (  
           d.y = Math.max(0, Math.min(height - d.dy, d3.event.y))  
       ) + ")");  
   sankey.relayout();  
   link.attr("d", path);  
  }  
 });  
 </script>  
 </body>  
 </html>  


Most probably you want to run the first test on your machine. Therefore you need to launch your browser in a "special mode" so that it will be able to read local file (for securityy reason browser deactivate this mode). I like Chrome and therefore what follows is valid only for Chrome, but extremely similar is for the other browser.

First be sure that you have killed every process of Chrome, which does not simply mean to close the browser, but to open the task manager and kill every process.
Then create a batch file like this (or invoke from command line):

 cd "C:\Program Files (x86)\Google\Chrome\Application"  
 chrome.exe --allow-file-access-from-files  



Nessun commento:

Posta un commento