Building line charts

The line charts are based on scatter charts. Contrary to scatter charts that show isolated correlation between two variables, the line chart tells a story in many ways; we can go back to our previous recipe, Spreading data in a scatter chart, and draw a line between the dots to create the connection. This type of chart is usually used in website statistics, tracking things over time, speed, age, and so on. Let's jump right into it and see it in action.

Building line charts

Getting ready

As usual get your HTML wrapper ready. In this recipe we actually are going to base our changes on the previous recipe, Spreading data in a scatter chart.

In our case study for this example, we will create a chart that shows how many new members joined my site, 02Geek.com, in 2011 and 2010. I've gathered the information month by month and gathered it into two arrays:

var a2011 = [38,65,85,111,131,160,187,180,205,146,64,212];
var a2010 = [212,146,205,180,187,131,291,42,98,61,74,69];

Both arrays have a length of 12 (for 12 months of the year). I've deliberately created a new data source that is totally different than the one we used earlier. I did that to render our old map useless in this example. I've done it to add some extra value into this recipe (a good lesson in manipulating data to fit even when it doesn't, instead of rebuilding things).

var chartInfo= { y:{min:0, max:300, steps:5,label:"users"},
        x:{min:1, max:12, steps:11,label:"months"}
      };

For our chart information we are using the same object type and for the y position we will assume a range from 0 to 300 (as I haven't had the privilege of having more than 300 members in one month, yet I'm hopeful). For our x position we are setting it to output values from 1 through 12 (for the 12 months of the year).

OK, it's time to build it!

How to do it...

As always, our init function is going to look very similar to the one we used in the previous recipe. Let's take a look at the modifications that have taken place in this recipe:

  1. Update/create the global variables:
    var a2011 = [38,65,85,111,131,160,187,180,205,146,64,212];
    var a2010 = [212,146,205,180,187,131,291,42,98,61,74,69];
    
    var chartInfo= { y:{min:0, max:300, steps:5,label:"users"},
            x:{min:1, max:12, steps:11,label:"months"}
          };
    
    var CHART_PADDING = 20;
    var wid;
    var hei;
  2. Update the init function:
    function init(){
      
      var can = document.getElementById("bar");
      
      wid = can.width;
      hei = can.height;
      var context = can.getContext("2d");
      context.fillStyle = "#eeeeee";
      context.strokeStyle = "#999999";
      context.fillRect(0,0,wid,hei);
      
      context.font = "10pt Verdana, sans-serif";
      context.fillStyle = "#999999";
      
      
      context.moveTo(CHART_PADDING,CHART_PADDING);
      context.rect(CHART_PADDING,CHART_PADDING,wid-CHART_PADDING*2,hei-CHART_PADDING*2);
      context.stroke();
      context.strokeStyle = "#cccccc";
      fillChart(context,chartInfo);
      addLine(context,formatData(a2011, "/2011","#B1DDF3"),"#B1DDF3");
      addLine(context,formatData(a2010, "/2010","#FFDE89"),"#FFDE89");	
    }
  3. Change the name of the function createDots to addLine and update the logic:
    function addLine(context,data,style){
      var yDataLabel = chartInfo.y.label;
      var xDataLabel = chartInfo.x.label;
      var yDataRange = chartInfo.y.max-chartInfo.y.min;
      var xDataRange = chartInfo.x.max-chartInfo.x.min;
      var chartHeight = hei- CHART_PADDING*2;
      var chartWidth = wid- CHART_PADDING*2;
      
      var yPos;
      var xPos;
      context.strokeStyle = style;
      context.beginPath();
      context.lineWidth = 3;
      for(var i=0; i<data.length;i++){
        xPos = CHART_PADDING + (data[i][xDataLabel]-chartInfo.x.min)/xDataRange * chartWidth;
        yPos = (hei - CHART_PADDING)  -(data[i][yDataLabel]-chartInfo.y.min)/yDataRange * chartHeight;
        
        context.fillStyle = data[i].style;
        context.fillRect(xPos-4 ,yPos-4,8,8);
        
        i ? context.lineTo(xPos,yPos):context.moveTo(xPos,yPos);
    
      }
      context.stroke();
    }
  4. Create the formatData function:
    function formatData(data , labelCopy , style){
      newData = [];
      for(var i=0; i<data.length;i++){
        newData.push({	label:(i+1)+labelCopy,
                users:data[i],
                months:i+1,
                style:style
                });	
      }
      
      return newData;	
    }

That's it! We are done!

How it works...

I've added a new method, rect, to our tool set for drawing; until now we worked with the drawRect method. I've used the rect method as it just adds the outlines without drawing anything, so I can perform the stroke or fill function separately and create an outline instead of a fill.

The fillChart function did not change at all, cool right? And I've renamed the function createDots to addLine as it seemed more appropriate for our sample. A few additions have been made into that function and a new function, formatData, is being used to format the data to fit what the addLine function is expecting.

As you probably noticed we made a few small changes to our code to accommodate the needs of this chart style. Let's dive in and see them in action:

addLine(context,formatData(a2011,"/2011","#B1DDF3"),"#B1DDF3")

The biggest change we can visibly see in the way we are calling the addLine function is that we are calling the formatData function to render a data source for us that will be acceptable by the addLine function. You might be thinking right now, why didn't I just create the data the way it needs to work for the addLine function. When we move to the real, live data sources many times we will find data sources that just don't match our original work. That doesn't mean we need to change our work, often a better solution is to create a converter method that will modify the data and rebuild it to match our application structure so it is in the format we expect.

A reminder from our previous recipe: this is what our data source looked like:

var data = [{label:"David",
       math:50,
       english:80,
       art:92
       style:"rgba(241, 178, 225, 0.5)"},
       ...
       ];

While currently our array is flat, we need to change that to work with our current system; it expects two properties that will define the x and y values:

var chartInfo= { y:{min:0, max:300, steps:5,label:"users"},
        x:{min:1, max:12, steps:11,label:"months"}

In other words the object we need to create needs to look something like the following:

var data = [{label: "01/2011",
       users:200,
      months:1,
      style:"#ff0000"} ... ];

So let's make the function that will create this data format:

function formatData(data , labelCopy , style){
  newData = [];
  for(var i=0; i<data.length;i++){
    newData.push({	label:(i+1)+labelCopy,
            users:data[i],
            months:i+1,
            style:style
            });	
  }
  
  return newData;
}

Notice how we loop through our old array and restructure it to fit our expected data format using both the array data and external data that was sent to our formatData functions. Even though we aren't using all of the information in this recipe, I wanted to keep it up-to-date with all the basics in case you want to expand this sample. We will do so in the future.

Tip

This is one of the most powerful tricks in programing in the tool set. I've met many developers, who change their code instead of changing their data to fit their required application structure. There is always a way to modify data to make it more easily consumed by your application and it's much easier to dynamically modify data than it is to change your architecture.

I didn't change anything in the core logic of this addLine function, but instead just added drawing lines from one dot to the next one.

In case you're not familiar with the ternary operation, it is a shorthanded if statement:

condition ? ifStatement: elseStatement;

By the way, if you are worried about efficiency, you might want to change the for loop by extracting the first instance out of the loop as that's the only occurrence where our ternary operator would trigger the else value.

There's more...

Let's revisit our code and optimize it to be more adaptable. Our goal is to add more flexibility to our chart to render in various modes.

My goal here is to enable our chart to render in three render modes: dot mode (as in the previous sample), line mode (in this sample), and fill mode (new addition):

There's more...

Although, in the preceding screenshot, we have three chart elements and they're all with a fill, with the new code you can pick, per line added, how you wish to treat it. So let's jump right in.

Enabling switching mode between dots and lines

All the work we added into the function doesn't need to go through a big overhaul as nothing is visible until it's actually rendered. That is controlled in one line, where we create the stroke in the addLine function. So let's add a new rule that if a style is not sent, it would mean we don't want to create a line:

if(style)context.stroke();

In other words, only if we have style information will the line we just created be drawn; if not, no line will be drawn.

Creating fill shapes

To create the fill shapes and for the sake of keeping our code simple, we will create an if...else statement within our code, and if the user sends a new fourth parameter, we will render it in the fill mode (the changes are highlighted in the following code snippet):

function addLine(context,data,style,isFill){
  var yDataLabel = chartInfo.y.label;
  var xDataLabel = chartInfo.x.label;
  var yDataRange = chartInfo.y.max-chartInfo.y.min;
  var xDataRange = chartInfo.x.max-chartInfo.x.min;
  var chartHeight = hei- CHART_PADDING*2;
  var chartWidth = wid- CHART_PADDING*2;
  
  var yPos;
  var xPos;
  context.strokeStyle = style;
  context.beginPath();
  context.lineWidth = 3;
  
  if(!isFill){
    for(var i=0; i<data.length;i++){
      xPos = CHART_PADDING + (data[i][xDataLabel]-chartInfo.x.min)/xDataRange * chartWidth;
      yPos = (hei - CHART_PADDING)  -(data[i][yDataLabel]-chartInfo.y.min)/yDataRange * chartHeight;
      
      context.fillStyle = data[i].style;
      context.fillRect(xPos-4 ,yPos-4,8,8);
      
      i==0? context.moveTo(xPos,yPos):context.lineTo(xPos,yPos);
  
    }
    if(style)context.stroke();
  }else{
    context.fillStyle = style;
    context.globalAlpha = .6;
    context.moveTo(CHART_PADDING,hei - CHART_PADDING)
    for(var i=0; i<data.length;i++){
      xPos = CHART_PADDING + (data[i][xDataLabel]-chartInfo.x.min)/xDataRange * chartWidth;
      yPos = (hei - CHART_PADDING)  -(data[i][yDataLabel]-chartInfo.y.min)/yDataRange * chartHeight;
      
      context.lineTo(xPos,yPos);
  
    }
    context.lineTo(	CHART_PADDING + chartWidth, CHART_PADDING+chartHeight);
    context.closePath();
    context.fill();	
    context.globalAlpha = 1;
  }
}

The differences are not large in the new code. We just removed some of the code and added a few new lines to create a complete shape. I superimposed the Alpha value as well. A smarter way would be to revisit the values sent and put into them an Alpha value as needed; but that is left for you to improve. Now our addLine function can add three types of visualization and we can add multiple types at the same time to our chart (check out the source code to see this in action).

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

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