Making a sunburst chart with InfoVis

Making a sunburst chart with InfoVis


Another really cool library is InfoVis. If I had to categorize the library, I would say it's about connections. When you review the rich samples provided by Nicolas Garcia Belmonte, you will find a lot of relational datatypes that are very unique.

This library is distributed freely through Sencha legal owners. (The copyright is easy to follow, but please review the notes for this and any open source project that you encounter.)

We will start with one of his base samples—the sunburst example from the source files. I've made a few changes to give it a new personality. The basic idea of a sunburst chart is to showcase relationships between nodes. While a tree is an ordered parent-child relationship, the relationships in a sunbust chart are bidirectional. A node can have a relationship with any other node, and it can be a two-way or one-way relationship. A dataset that is perfect for this is the example of the total exports of a country—lines from one country to all the other countries that get exports from it.

We will keep it relatively simple by having only four elements (Ben, Packt Publishing, 02geek, and Nicolas the creator of InfoVis). I have a one-way relationship with each of them: as the owner of 02geek.com, as a writer for Packt Publishing, and a user of InfoVis. While that is true about me, not all the others have a real in-depth relationship with me. Some of them have a relationship back with me, such as 02geek and Packt Publishing, while Nicolas for this example is a stranger that I've never interacted with. This can be depicted in a sunburst chart in the following way:

Getting ready

As always you will need the source files, you can either download our sample files or get the latest release by visiting our aggregated list at http://blog.everythingfla.com/?p=339.

How to do it...

Let's create some HTML and JavaScript magic:

  1. Create an HTML file as follows:

    <!DOCTYPE html>
    <html>
      <head>
        <title>Sunberst - InfoVis</title>
        <meta charset="utf-8" />
    
        <style>
          #infovis {
            position:relative;
            width:600px;
            height:600px;
            margin:auto;
            overflow:hidden;
          }
        </style>
    
        <script  src="./external/jit/jit-yc.js"></script>
        <script src="./07.05.jit.js"></script>
      </head>
    
      <body onload="init();">
        <div id="infovis"></div>    
      </body>
    </html>
  2. The rest of the code will be in 07.05.jit.js. Create a base data source as follows:

    var dataSource = [ {"id": "node0", "name": "","data": {"$type": "none" },"adjacencies": []}]; //starting with invisible root
  3. Let's create a function that will create the nodes needed for our chart system:

    function createNode(id,name,wid,hei,clr){
      var obj = {id:id,name:name,data:{"$angularWidth":wid,"$height":hei,"$color":clr},adjacencies:[]};
      dataSource[0].adjacencies.push({"nodeTo": id,"data": {'$type': 'none'}});
      dataSource.push(obj);
    
      return obj;
    }
  4. To connect the dots, we will need to create a function that will create the relationships between the elements:

    function relate(obj){
      for(var i=1; i<arguments.length; i++){
        obj.adjacencies.push({'nodeTo':arguments[i]});
      }
    }
  5. We want to be able to highlight the relationships. To do that we will need to have a way to rearrange the data and highlight the elements that we want highlighted:

    function highlight(nodeid){
      var selectedIndex = 0;
      for(var i=1; i<dataSource.length; i++){
        if(nodeid!=	dataSource[i].id){
          for(var item in dataSource[i].adjacencies)
          delete dataSource[i].adjacencies[item].data;
        }else{
          selectedIndex = i;
          for(var item in dataSource[i].adjacencies)
          dataSource[i].adjacencies[item].data =  {"$color": "#ddaacc","$lineWidth": 4 };
          }
    
        }
    
        if(selectedIndex){ //move selected node to be first (so it will highlight everything)
        var node = dataSource.splice(selectedIndex,1)[0];
        dataSource.splice(1,0,node); 
      }
    
    }
  6. Create an init function:

    function init(){
    /* or the remainder of the steps 
    all code showcased will be inside the init function  */
    }
  7. Let's start building up data sources and relationships:

    function init(){
      var node = createNode('geek','02geek',100,40,"#B1DDF3");
      relate(node,'ben');
      node = createNode('packt','PacktBub',100,40,"#FFDE89");
      relate(node,'ben');
      node = createNode('ben','Ben',100,40,"#E3675C");
      relate(node,'geek','packt','nic');
       
      node = createNode('nic','Nicolas',100,40,"#C2D985");
      //no known relationships so far ;)
    ...
  8. Create the actual sunburst and interact with the API (I've stripped it down to its bare bones; in the original samples it's much more detailed):

    var sb = new $jit.Sunburst({
      injectInto: 'infovis', //id container
      Node: {
        overridable: true,
        type: 'multipie'
      },
      Edge: {
        overridable: true,
        type: 'hyperline',
        lineWidth: 1,
        color: '#777'
      },
      //Add animations when hovering and clicking nodes
      NodeStyles: {
        enable: true,
        type: 'Native',
        stylesClick: {
        'color': '#444444'
      },
      stylesHover: {
        'color': '#777777'
      },
        duration: 700
      },
      Events: {
        enable: true,
        type: 'Native',
        //List node connections onClick
        onClick: function(node, eventInfo, e){
          if (!node) return;
          
          highlight(node.id);
          sb.loadJSON(dataSource);
          sb.refresh()
        }
      },
      levelDistance: 120
    });
  9. Last but not least, we want to render our chart by providing its dataSource and refresh the render for the first time:

    sb.loadJSON(dataSource);
    sb.refresh();

That's it. If you run the application, you will find a chart that is clickable and fun, and just scratches the capabilities of this really cool data networking library.

How it works...

I'll avoid getting into the details of the actual API as that is fairly intuitive and has a really nice library of information and samples. So instead I will focus on the changes and enhancements that I've created in this application.

Before we do that we need to understand how the data structure of this chart works. Let's take a deeper look into how the data source object will look when filled with information:

{
        "id": "node0",
        "name": "",
        "data": {
          "$type": "none"
        },
        "adjacencies": [
            {"nodeTo": "node1","data": {'$type': 'none'}}, 
            {"nodeTo": "node2","data": {'$type': 'none'}}, 
            {"nodeTo": "node3","data": {'$type': 'none'}}, 
            {"nodeTo": "node4","data": {'$type': 'none'}}
                       ]
}, 
      
{
        "id": "node1",
        "name": "node 1",
        "data": {
          "$angularWidth": 300,
          "$color": "#B1DDF3",
          "$height": 40
        },
        "adjacencies": [
            {
              "nodeTo": "node3",
              "data": {
                "$color": "#ddaacc",
                "$lineWidth": 4
              }
            }
                    ]
},

There are a few important factors to note. The first is that there is a base parent that is the parent of all the parentless nodes. In our case it's a flat chart. The relationships that are really thrilling are between nodes that are at an equal level. As such the main parent has a relationship with all the nodes that are to follow. The children elements, such as node1 in this case, could have relationships. They are listed out in an array called adjacencies that holds objects. The only mandatory parameter is the nodeTo property. It lets the application know the one-way relationship list. There are optional layout parameters that we will add later only when we want to highlight a line. So let's see how we can create this type of data dynamically with the help of a few functions.

The createNode function helps us keep our code clean by wrapping up the dirty steps together. Every new element that we add needs to be added into our array and is needed to update our main parent (that is always going to be in position 0 of our array of new elements):

function createNode(id,name,wid,hei,clr){
  var obj = {id:id,name:name,data:{"$angularWidth":wid,"$height":hei,"$color":clr},adjacencies:[]};
  dataSource[0].adjacencies.push({"nodeTo": id,"data": {'$type': 'none'}});
  dataSource.push(obj);

  return obj; 	
}

We return the object as we want to continue and build up the relationship with this object. As soon as we create a new object (in our init function), we call the relate function and send to it all the relationships that our element will have to it. The logic of the relate function looks more complicated that it actually is. The function uses a hidden or often ignored feature in JavaScript that enables developers to send an open-ended number of parameters into a function with the use of the arguments array that is created automatically within every function. We can get these parameters as an array named arguments:

function relate(obj){
  for(var i=1; i<arguments.length; i++){
    obj.adjacencies.push({'nodeTo':arguments[i]});
  }
}

The arguments array is built into every function and stores all the actual information that has been sent into the function. As the first parameter is our object, we need to skip the first parameter and then add the new relationships into the adjacencies array.

Our last data-related function is our highlight function. The highlight function expects one parameter nodeID (that we created in createNode). The goal of the highlight function is to travel through all the data elements and de-highlight all the relationships limited to the one selected element and its relationships.

function highlight(nodeid){
  var selectedIndex = 0;
  for(var i=1; i<dataSource.length; i++){
    if(nodeid!=	dataSource[i].id){
      for(var item in dataSource[i].adjacencies)
      delete dataSource[i].adjacencies[item].data;
    }else{
      selectedIndex = i;
      for(var item in dataSource[i].adjacencies)
      dataSource[i].adjacencies[item].data =  {"$color": "#ddaacc","$lineWidth": 4 };
    }

  }
}

If we don't have highlight, we want to confirm and remove all the instances of the data object within the adjacencies of the node, while if it is selected, we need to add that same object by setting it with its own color and a thicker line.

We are almost done with the data. But when running the application, you will find an issue if we stop here. The issue is within the way the chart system works. If a line was drawn it will not redraw it again. In practical terms, if we select "Ben" while ben isn't the first element in the list, then not all the relationships that "Ben" has with the others will be visible. To fix this issue, we would want to push the selected node to be the first element right after position 0 (main parent), so it will render the selected relationships first:

if(selectedIndex){ 
  var node = dataSource.splice(selectedIndex,1)[0];
  dataSource.splice(1,0,node); 
}

There's more...

One more thing left is that we need to be able to refresh our content when the user clicks on an element. To accomplish this task, we will need to add an event parameter into the initializing parameter object of jit.Sunburst:

var sb = new $jit.Sunburst({
  injectInto: 'infovis', //id container
     ...
  Events: {
    enable: true,
    type: 'Native',
    //List node connections onClick
    onClick: function(node, eventInfo, e){
      if (!node) return;
          
      highlight(node.id);
      sb.loadJSON(dataSource);
        sb.refresh();
    }
  },
  levelDistance: 120
});

One more thing to note in this sample is the levelDistance property that controls how close/far you are to/from the rendered element (making it bigger or smaller).

Where is the copy?

There is still one more issue. We don't have any copy in our chart enabling us to know what is actually being clicked on. I've removed it from the original sample as I just didn't like the positioning of the text and couldn't figure out how to get it up right, so instead I came up with a workaround. You can directly draw into the canvas by directly interacting with it. The canvas element will always be called by the same ID as our project (in our case infovis followed by -canvas):

var can = document.getElementById("infovis-canvas");
  var context = can.getContext("2d"); 
...

I'll leave the rest for you to explore. The rest of the logic is easy to follow as I've stripped it down. So if you enjoy this project as well, please visit the InfoVis Toolkit site and play more with their interface options.