Tree mapping and recursiveness

Tree mapping enables us to see in-depth data from a bird's-eye view. Contrary to comparative charts—such as most of the charts that we have created until now—tree mapping displays tree structured data as a set of nested rectangles, enabling us to visualize their quantitative nature and relationship.

Tree mapping and recursiveness

Let's start with a tree mapping that showcases only one level of information.

Getting ready

We will start our application with the number of people in the world, in millions, divided by continent (based on public data from 2011).

var chartData = [
  {name: "Asia", value:4216},
  {name: "Africa",value:1051},
  {name: "The Americas and the Caribbean", value:942},
  {name: "Europe", value:740},
  {name: "Oceania", value:37}
];

We will update this data source later in our example, so keep in mind that this dataset is temporary.

How to do it...

We will start by creating a simple, working, flat tree chart. Let's jump right into it and figure out the steps involved in creating the tree map:

  1. Let's add a few helper variables on top of our dataset.
    var wid;
    var hei;
    var context;
    var total=0;
  2. Create the init function.
    function init(){
      var can = document.getElementById("bar");
    
      wid = can.width;
      hei = can.height;
      context = can.getContext("2d");
    
      for(var item in chartData) total += chartData[item].value;
    
      context.fillRect(0,0,wid,hei);
      context.fillStyle = "RGB(255,255,255)";
      context.fillRect(5,5,wid-10,hei-10);
      context.translate(5,5);
      wid-=10;
      hei-=10;
    
      drawTreeMap(chartData);
    
    }
  3. Create the function drawTreeMap.
    function drawTreeMap(infoArray){
      var percent=0;
      var cx=0;
      var rollingPercent = 0;
      for(var i=0; i<infoArray.length; i++){
        percent = infoArray[i].value/total;
        rollingPercent +=percent
        context.fillStyle = formatColorObject(getRandomColor(255));
        context.fillRect(cx,0 ,wid*percent,hei);
        cx+=wid*percent;
        if(rollingPercent > 0.7) break;
    
      }
    
      var leftOverPercent = 1-rollingPercent;
      var leftOverWidth = wid*leftOverPercent;
      var cy=0;
      for(i=i+1; i<infoArray.length; i++){
        percent = (infoArray[i].value/total)/leftOverPercent;
        context.fillStyle = formatColorObject(getRandomColor(255));
        context.fillRect(cx,cy ,leftOverWidth,hei*percent);
        cy+=hei*percent;
      }
    
    }
  4. Create a few formatting functions to help us create a random color for our tree map block.
    function formatColorObject(o){
      return "rgb("+o.r+","+o.g+","+o.b+")";
    }
    
    function getRandomColor(val){
      return {r:getRandomInt(255),g:getRandomInt(255),b:getRandomInt(255)};
    }
    
    function getRandomInt(val){
      return parseInt(Math.random()*val)+1
    }

There is a bit of overkill in the creation of so many formatting functions; their main goal is to help us when we are ready for the next step—to create more depth in our data (refer to the There's more... section in this recipe for more details).

How it works...

Let's start with the initial idea. Our goal is to create a map that will showcase the bigger volume areas inside our rectangular area and leave a strip on the side for the smaller areas. So, let's start with our init function. Our first task beyond our basic getting started work is to calculate the actual total. We do that by looping through our data source, thus:

for(var item in chartData) total += chartData[item].value;

We continued with some playing around with the design and making our work area 10 pixels smaller than our total canvas size.

CONTEXT.FILLRECT(0,0,WID,HEI);
CONTEXT.FILLSTYLE = "RGB(255,255,255)";
CONTEXT.FILLRECT(5,5,WID-10,HEI-10);
CONTEXT.TRANSLATE(5,5);
WID-=10;
HEI-=10;

drawTreeMap(chartData);

It's time to take a look into how our drawTreeMap function works. The first thing to notice is that we send in an array instead of working directly with our data source. We do that because we want to be open to the idea that this function will be re-used when we start building the inner depths of this visualization type.

function drawTreeMap(infoArray){...}

We start our function with a few helper variables (the percent variable will store the current percent value in a loop). The cx (the current x) position of our rectangle and rollingPercent will keep track of how much of our total chart has been completed.

var percent=0;
var cx=0;
var rollingPercent = 0;

Time to start looping through our data and drawing out the rectangles.

for(var i=0; i<infoArray.length; i++){
  percent = infoArray[i].value/total;
  rollingPercent +=percent
  context.fillStyle =
  formatColorObject(getRandomColor(255));
  context.fillRect(cx,0 ,wid*percent,hei);
  cx+=wid*percent;

Before we complete our first loop, we will test it to see when we cross our threshold (you are welcome to play with that value). When we reach it, we need to stop the loop, so that we can start drawing our rectangles by height instead of by width.

if(rollingPercent > 0.7) break;
}

Before we start working on our boxes, which take the full leftover width and expand to the height, we need a few helper variables.

var leftOverPercent = 1-rollingPercent;
var leftOverWidth = wid*leftOverPercent;
var cy=0;

As we need to calculate each element from now on based on the amount of space left, we will figure out the value (leftOverPercent), and then we will extract the remaining width of our shape and start up a new cy variable to store the current y position.

for(i=i+1; i<infoArray.length; i++){
  percent = (infoArray[i].value/total)/leftOverPercent;
  context.fillStyle = formatColorObject(getRandomColor(255));
  context.fillRect(cx,cy ,leftOverWidth,hei*percent);
  cy+=hei*percent;
}

We start our loop with one value higher than what we left off (as we broke out of our earlier loop before we had a chance to update its value and draw to the height of our remaining area.

Note that in both loops we are using formatColorObject and getRandomColor. The breakdown of these functions was created so that we can have an easier way to manipulate the colors returned in our next part.

There's more...

For our chart to really have that extra kick, we need to have a way to make it capable of showing data in at least a second lower-level details of data. To do that, we will revisit our data source and re-edit it:

var chartData = [
  {name: "Asia", data:[
    {name: "South Central",total:1800},
    {name: "East",total:1588},
    {name: "South East",total:602},
    {name: "Western",total:238},
    {name: "Northern",total:143}
  ]},
  {name: "Africa",total:1051},
  {name: "The Americas and the Caribbean", data:[
    {name: "South America",total:396},
    {name: "North America",total:346},
    {name: "Central America",total:158},
    {name: "Caribbean",total:42}
  ]},
  {name: "Europe", total:740},
  {name: "Oceania", total:37}
];

Now we have two regions of the world with a more in-depth view of their subregions. It's time for us to start modifying our code, so that it will work again with this new data.

Updating the init function – recalculating the total

The first step we need to carry out in the init function is to replace the current total loop with a new one that can dig deeper into elements to count the real total.

var val;
var i;
for(var item in chartData) {
  val = chartData[item];
  if(!val.total && val.data){
    val.total = 0;
    for( i=0; i<val.data.length; i++)
    val.total+=val.data[i].total;
  }

  total += val.total;
}

In essence, we are checking to see whether there is no total and whether there is a data source. If that is the case, we start a new loop to calculate the actual total for our elements—a good exercise for you now would be to try to make this logic into a recursive function (so that you can have more layers of data).

Next, we will change drawTreeMap and get it ready to become a recursive function. To make that happen, we need to extract the global variables from it and send them in as parameters of the function.

drawTreeMap(chartData,wid,hei,0,0,total);

Turning drawTreeMap into a recursive function

Let's update our function to enable recursive operations. We start by adding an extra new parameter to capture the latest color.

function drawTreeMap(infoArray,wid,hei,x,y,total,clr){
  var percent=0;
  var cx=x ;
  var cy=y;

  var pad = 0;
  var pad2 = 0;

  var rollingPercent = 0;
  var keepColor = false;
  if(clr){ //keep color and make darker
    keepColor = true;
    clr.r = parseInt(clr.r *.9);
    clr.g = parseInt(clr.g *.9);
    clr.b = parseInt(clr.b *.9);
    pad = PAD*2;	
    pad2 = PAD2*2;
  }

If we pass a clr parameter, we need to keep that color throughout all the new rectangles that will be created, and we need to add a padding around the shapes so that it becomes easier to see them. We make the color a bit darker as well by subtracting 10 percent of its color on all its RGA properties.

The next stage is to add the padding and recursive logic.

for(var i=0; i<infoArray.length; i++){
  percent = infoArray[i].total/total;
  rollingPercent +=percent
  if(!keepColor){
    clr = getRandomColor(255);
  }

  context.fillStyle = formatColorObject(clr);
  context.fillRect(cx+pad ,cy+pad ,wid*percent - pad2,hei-pad2);
  context.strokeRect(cx+pad ,cy+pad ,wid*percent - pad2,hei-pad2);
  if(infoArray[i].data){
    drawTreeMap(infoArray[i].data,parseInt(wid*percent - PAD2),hei - PAD2,cx+ PAD,cy + PAD,infoArray[i].total,clr);
  }
  cx+=wid*percent;
  if(rollingPercent > 0.7) break;

}

The same logic is then implemented on the second loop as well (to see it check the source files).

Turning the data and total to recursive data

Let's start by updating our tree data to be really recursive (for the full dataset please refer to the source code).

...
{name: "Asia", data:[
  {name: "South Central",total:1800},
  {name: "East",total:1588},
  {name: "South East",total:602},
  {name: "Western",total:238},
  {name: "Northern",data:[{name: "1",data:[
    {name: "2",total:30},
    {name: "2",total:30}
  ]},
  {name: "2",total:53},
  {name: "2",total:30}
]}  ...

Now, with a tree map that has over four levels of information, we can revisit our code and finalize our last outstanding issue validating that our total is always up-to-date at all levels. To fix that, we will extract the logic of calculating the total into a new function and update the total line in the init function.

function init(){
  var can = document.getElementById("bar");

  wid = can.width;
  hei = can.height;
  context = can.getContext("2d");

  total = calculateTotal(chartData); //recursive function
...

Time to create this magical (recursive) function.

function calculateTotal(chartData){
  var total =0;
  var val;
  var i;
  for(var item in chartData) {
    val = chartData[item];
    if(!val.total && val.data)
      val.total = calculateTotal(val.data);

    total += val.total;
  }

return total;

}

The logic is really similar to what it was, with the exception that all the data entries are internal to the function, and each time there is a need to deal with another level of data, it's re-sent to the same function (in a recursive way) until all data is resolved—until it returns the total.

See also

  • The Adding user interaction into tree mapping recipe
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.226.180.68