Building a bubble chart

Although many items in our chart will have correlations with earlier charts that we created in Chapter 3, Creating Cartesian-based Graphs, we will start from scratch. Our goal is to create a chart that has bubbles in it—the bubbles enable us to showcase data with three data points (x, y, and the size of the bubble). This type of chart is really ideal when animated as it can showcase changes over time (it could showcase many years in a few seconds).

A great demo of the powers of bubble charts can be seen in a TED presentation by Hans Rosling (http://blog.everythingfla.com/2012/05/hans-rosling-data-vis.html).

Building a bubble chart

Getting ready

We will start up our project with a canvas setup and skip the HTML end. If you have forgotten how to create it please refer to the Graphics with 2D Canvas recipe in Chapter 1, Drawing Shapes in Canvas.

There are three major steps:

  • Creating the data source
  • Creating the background
  • Adding the chart data info into the chart

How to do it...

Let's list the steps required to create a bubble chart:

  1. The next data object should look familiar in an array that has objects within it with student scores in English, Math, and programming. Build the data object:
    var students2001 = [{name:"Ben",
      math:30,
      english:60,
      programing:30},
      {name:"Joe",
      math:40,
      english:60,
      programing:40},
      {name:"Danny",
      math:50,
      english:90,
      programing:50},
      {name:"Mary",
      math:60,
      english:60,
      programing:60},
      {name:"Jim",
      math:80,
      english:20,
      programing:80}];
  2. Create our chart information; contrary to previous charts, this chart has a third parameter for our bubble information. Define our chart rules:
    var chartInfo= { y:{min:0, max:100,steps:5,label:"math"},
      x:{min:0, max:100,steps:5,label:"programing"},
      bubble:{min:0, max:100, minRaduis:3, maxRaduis:20,label:"english"}
    };
  3. The last data object will contain all the styling information that we might want to change in the future. Add a styling object:
    var styling = { outlinePadding:4,
      barSize:16,
      font:"12pt Verdana, sans-serif",
      background:"eeeeee",
      bar:"cccccc",
      text:"605050"
    };
  4. We create an event callback when the document is ready to trigger init, so let's create the init function:
    var wid;
    var hei;
    function init(){
      var can = document.getElementById("bar");
    
      wid = can.width;
      hei = can.height;
      var context = can.getContext("2d");
    
      createOutline(context,chartInfo);
      addDots(context,chartInfo,students2001,["math","programing","english"],"name");
    }
  5. We start creating our outline when we create our style object. Now it's time to draw everything into our canvas. So we start by setting up our base canvas style:
    function createOutline(context,chartInfo){
      var s = styling;
      var pad = s.outlinePadding;
      var barSize = s.barSize;
      context.fillStyle = s.background;
      context.fillRect(0,0,wid,hei);
      context.fillStyle = s.bar;
      context.fillRect(pad,pad,barSize,hei-pad*2);
      context.font = s.font;
      context.fillStyle = s.text;
  6. We need to save our current, canvas-based graphic layout information, change it to make it easier to position elements and then return it back to its original state:
    context.save();
    context.translate(17, hei/2 );
    context.rotate(-Math.PI/2);
    context.textAlign = "center";
    context.fillText(chartInfo.y.label, 0, 0);
    context.restore();
    
    context.fillStyle = s.bar;
    context.fillRect(pad+barSize,hei-pad-barSize,wid-pad*2-barSize,barSize);
    context.font = s.font;
    context.fillStyle = s.text;
    context.fillText(chartInfo.x.label,( wid-pad*2-barSize)/2, hei-pad*2);
    
    context.translate(pad+barSize,hei-pad-barSize);
    context.scale(1, -1);
    //SET UP CONSTANTS - NEVER CHANGE AFTER CREATED
    styling.CHART_HEIGHT = hei-pad*2-barSize;
    styling.CHART_WIDTH = wid-pad*2-barSize;
  7. Now it is time to draw the outlines with the help of our chartInfo object:
    var steps = chartInfo.y.steps;
    var ratio;
    chartInfo.y.range = chartInfo.y.max-chartInfo.y.min;
    var scope = chartInfo.y.range;
    context.strokeStyle = s.text;
    var fontStyle = s.font.split("pt");
    var pointSize = fontStyle[0]/2;
    fontStyle[0]=pointSize;
    fontStyle = fontStyle.join("pt");
    context.font = fontStyle; // making 1/2 original size of bars
      for(var i=1; i<=steps; i++){
        ratio = i/steps;
        context.moveTo(0,ratio*styling.CHART_HEIGHT-1);
        context.lineTo(pad*2,ratio*styling.CHART_HEIGHT-1);
        context.scale(1,-1);
    
        context.fillText(chartInfo.y.min + (scope/steps)*i,0,(ratio*styling.CHART_HEIGHT-3 -pointSize)*-1);
        context.scale(1,-1);
    
      }
    
      steps = chartInfo.x.steps;
      chartInfo.x.range = chartInfo.x.max-chartInfo.x.min;
      scope = chartInfo.x.max-chartInfo.x.min;
      context.textAlign = "right";
      for(var i=1; i<=steps; i++){
        ratio = i/steps;
        context.moveTo(ratio*styling.CHART_WIDTH-1,0);
        context.lineTo(ratio*styling.CHART_WIDTH-1,pad*2);
        context.scale(1,-1);
        context.fillText(chartInfo.x.min + (scope/steps)*i,ratio*styling.CHART_WIDTH-pad,-pad/2);
        context.scale(1,-1);
    
      }
    
    context.stroke();
    }
  8. Now it is time to add the data into our chart by creating the addDots method. The function addDots will take in the data with the definition of rules (keys) to be used, contrary to what we did in the earlier recipes.
    function addDots(context,chartInfo,data,keys,label){
      var rangeX = chartInfo.y.range;
      var _y;
      var _x; 
    
      var _xoffset=0;
      var _yoffset=0;
    
      if(chartInfo.bubble){
        var range = chartInfo.bubble.max-chartInfo.bubble.min;
        var radRange = chartInfo.bubble.maxRadius-chartInfo.bubble.minRadius; 
        context.textAlign = "left";
      }
    
    
      for(var i=0; i<data.length; i++){
        _x = ((data[i][keys[0]] - chartInfo.x.min )/ chartInfo.x.range) * styling.CHART_WIDTH;
        _y = ((data[i][keys[1]] - chartInfo.y.min )/ chartInfo.y.range) * styling.CHART_HEIGHT;
        context.fillStyle = "#44ff44";
    
    
        if(data[i][keys[2]]){
          _xoffset = chartInfo.bubble.minRadius + (data[i][keys[2]]-chartInfo.bubble.min)/range *radRange;
          _yoffset = -3;
          context.beginPath();
          context.arc(_x,_y, _xoffset , 0, Math.PI*2, true); 
          context.closePath();
          context.fill();
    
          _xoffset+=styling.outlinePadding;
        }else{
          context.fillRect(_x,_y,10,10);	
        }
    
        if(label){
          _x+=_xoffset;
          _y+=_yoffset;
          context.fillStyle = styling.text;
          context.save();
          context.translate(_x,_y );
          context.scale(1,-1);
          context.fillText("Bluping",0,0);
          context.restore();
    
        }
      }
    }

This block of code, although redone from scratch, bears a lot of resemblance to the Spreading data in a scatter chart recipe in Chapter 3, Creating Cartesian-based Graphs, with modifications to enable the third level of data and the new charting format.

That's it. You should have a running bubble chart. Now when you run the application, you will see that the x parameter is showcasing the math score, the y parameter is showcasing the programming score, while the size of our bubble showcases the student's score in English.

How it works...

Let's start with the createOutline function. In this method, apart from the regular canvas drawing methods that we grow to love, we introduce a new style of coding where we manipulate the actual canvas to help us define our code in an easier way. The two important key methods here are as follows:

context.save();
context.restore();

We will be leveraging both the methods a few times. The save method saves the current view of the canvas while the restore method returns users to the last saved canvas:

context.save();
context.translate(17, hei/2 );
context.rotate(-Math.PI/2);
context.textAlign = "center";
context.fillText(chartInfo.y.label, 0, 0);
context.restore();

In the first use of this style, we are using it to draw our text by rotating it to the right. The translate method moves the 0, 0 coordinates of the canvas while the rotate method rotates the text using radians.

After drawing the external bars, it's time for us to use this new capability to our advantage. Most charts rely on a y coordinate that grows upwards, but this canvas has the y values growing from the top to the bottom of the canvas area. We can flip this relationship by adding some code before we loop through to add the range values.

context.translate(pad+barSize,hei-pad-barSize);
context.scale(1, -1);

In the preceding lines, we are first moving the 0,0 coordinates of our canvas to be exactly at the bottom-right range of our chart, and then we are flipping our canvas by switching the scale value. Note that from now on if we try to add text to the canvas, it will be upside down. Keep that in mind as we are now drawing in a canvas that is flipped upside down.

One thing to note in our first loop when we try to type in new text is that when we want to add text, we first undo our scale and then return back our canvas for it to be flipped:

context.scale(1,-1);
context.fillText(chartInfo.y.min + (scope/steps)*i,0,(ratio*styling.CHART_HEIGHT-3 -pointSize)*-1);
context.scale(1,-1);

Note that we are multiplying our y coordinate by *-1. We are doing this because we actually want the value of our y coordinate to be negative as we have just flipped the screen.

The work around the x bar text is very similar; notice the main differences related to finding the x and y value calculations.

It's time to dig into the addDots function. The function will again look familiar if you've been following Chapter 3, Creating Cartesian-based Graphs, but this time we are working with a modified canvas.

We start with a few helper variables:

var rangeX = chartInfo.y.range;
var _y;
var _x; 
var _xoffset=0;
var _yoffset=0;

We are adding the bubble effect dynamically, which means that this method can work even if there are only two points of information and not three. We continue by testing to see if our data object contains the bubble information:

if(chartInfo.bubble){
  var range = chartInfo.bubble.max-chartInfo.bubble.min;
  var radRange = chartInfo.bubble.maxRaduis-chartInfo.bubble.minRaduis; 
  context.textAlign = "left";
}

If so, we add a few more variables and align our text to the left as we are going to use it in this example.

It's time for us to look through our data object and propagate the data on the chart.

for(var i=0; i<data.length; i++){
  _x = ((data[i][keys[0]] - chartInfo.x.min )/ chartInfo.x.range) * styling.CHART_WIDTH;
  _y = ((data[i][keys[1]] - chartInfo.y.min )/ chartInfo.y.range) * styling.CHART_HEIGHT;
  context.fillStyle = "#44ff44";

For each loop, we recalculate the _x and _y coordinates based on the current values.

If we have a third element, we are ready to develop a bubble. If we do not have it, we need to create a simple dot.

if(data[i][keys[2]]){
  _xoffset = chartInfo.bubble.minRaduis + (data[i][keys[2]]-chartInfo.bubble.min)/range *radRange;
  _yoffset = -3;
  context.beginPath();
  context.arc(_x,_y, _xoffset , 0, Math.PI*2, true); 
  context.closePath();
  context.fill();	
  _xoffset+=styling.outlinePadding;
}else{
   context.fillRect(_x,_y,10,10);	
}

At this stage, we should have an active bubble/dot method. All that is left is for us to integrate our overlay copy.

Before we add a label, let's take a peek at the function signature:

function addDots(context,chartInfo,data,keys,label){}

The context and chartInfo parameters are already a standard in our samples. The idea of keys was to enable us to switch what data will be tested dynamically. The keys' values are the array positions 0 and 1 that are correlated to the x and y coordinates, and position 2 is used for bubbles, as we've seen earlier. The label parameter enables us to send in a key value for the label. In this way, if the label is there we will add a label and if it is not there we will not.

if(label){
  _x+=_xoffset;
  _y+=_yoffset;
  context.fillStyle = styling.text;
  context.save();
  context.translate(_x,_y );
  context.scale(1,-1);
  context.fillText(data[i][label],0,0);
  context.restore();						
 }

Then we add the preceding if statement. If our label is set, we position the style and create the text of the label.

..................Content has been hidden....................

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