Stacking graphical layers

Before we can do any real animations on our canvas we really need to rethink the concept of building everything on one canvas layer. Once a canvas element is drawn, it's incredibly hard to create subtle small changes to it, such as fade-ins for specific elements. We will revisit one of our famous charts, the bar chart, which we played around with and enhanced many times throughout the earlier chapters. In this chapter, our goal will be to break the logic apart and make it more modular. In this recipe we will separate layers. Each layer will give us more control later when we are ready to animate.

Getting ready

Start by grabbing the latest files from the previous chapter: 05.02.line-revisit.html and 05.02.line-revisit.js.

How to do it...

The following changes are made to the HTML file:

  1. Update the HTML file to incorporate more canvas elements (one per drawn line):
    <body onLoad="init();" style="background:#fafafa">
        <h1>Users Changed between within a year</h1>
        <div class="graphicLayers" >
          <canvas id="base" class="canvasLayer" width="550" height="400"> </canvas>
      
          <canvas id="i2011" class="canvasLayer" width="550" height="400"> </canvas>
          <canvas id="i2010" class="canvasLayer" width="550" height="400"> </canvas>
          <canvas id="i2009" class="canvasLayer" width="550" height="400"> </canvas>
    
      </div>
      <div class="controllers">
      2009 : <input type="radio" name="i2009" value="-1" /> off
            <input type="radio" name="i2009" value="0" /> line
            <input type="radio" name="i2009" value="1" select="1" /> full ||
        2010 : <input type="radio" name="i2010" value="-1" /> off
            <input type="radio" name="i2010" value="0" /> line
            <input type="radio" name="i2010" value="1" select="1" /> full ||
        2011 : <input type="radio" name="i2011" value="-1" /> off
            <input type="radio" name="i2011" value="0" /> line
            <input type="radio" name="i2011" value="1" select="1" /> full
      </div>
    </body>
    </html>
  2. Add a CSS script so the layers will be stacked:
    <head>
        <title>Line Chart</title>
        <meta charset="utf-8" />
        <style>
        .graphicLayers {
        	position: relative;	
        	left:100px
        }
        
        .controllers {
          position: relative;	
          left:100px;
          top:400px;
    
        }
        
        .canvasLayer{
          position: absolute; 
          left: 0; 
          top: 0; 
        }
        </style>
      <script src="06.01.layers.js"></script>		
      </head>

    Let's move into the JavaScript file to update it.

  3. Add a window.onload callback function (changes highlighted in the code snippet):
    window.onload = init;
    
    function init(){
  4. Remove the variable context from global scope (delete the highlighted code snippet):
    var CHART_PADDING = 20;
    var wid;
    var hei;
    var context;
    
  5. Consolidate all bar line information into one object for easier control (delete all the highlighted code snippets):
    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 a2009 = [17,46,75,60,97,131,71,52,38,21,84,39];
    
    
    var chartInfo= { y:{min:0, max:300, steps:5,label:"users"},
            x:{min:1, max:12, steps:11,label:"months"}
          };
    
    
    var HIDE_ELEMENT = -1;
    var LINE_ELEMENT = 0;
    var FILL_ELEMENT = 1;
    
    
    var elementStatus={i2009:FILL_ELEMENT,i2010:FILL_ELEMENT,i2011:FILL_ELEMENT};
    
    var barData = {
            i2009:{
              status:	FILL_ELEMENT,
              style: "#E3675C",
              label: "/2009",
              data:[17,46,75,60,97,131,71,52,38,21,84,39]
            },
            i2010:{
              status:	FILL_ELEMENT,
              style: "#FFDE89",
              label: "/2010",
              data:[212,146,205,180,187,131,291,42,98,61,74,69]
            },
            i2011:{
              status:	FILL_ELEMENT,
              style: "#B1DDF3",
              label: "/2011",
              data:[38,65,85,111,131,160,187,180,205,146,64,212]
            }
    
          };
  6. Remove all canvas logic from the init function and add it to the drawChart function:
    function init(){
      var can = document.getElementById("bar");
    
      wid = can.width;
      hei = can.height;
      context = can.getContext("2d");
    
      drawChart();
    
      var radios ;
      for(var id in elementStatus){
        radios = document.getElementsByName(id);
        for (var rid in radios){
           radios[rid].onchange = onChangedRadio;
          if(radios[rid].value == elementStatus[id] ) radios[rid].checked = true;	 
        }
    
      }
    
    }
    
    function drawChart(){
      var can = document.getElementById("base");
    
      wid = can.width;
      hei = can.height;
      var context = can.getContext("2d");
    ...
  7. Update references to the new data object in the init function:
    function init(){
      drawChart();
    
      var radios ;
      for(var id in barData){
        radios = document.getElementsByName(id);
        for (var rid in radios){
           radios[rid].onchange = onChangedRadio;
          if(radios[rid].value == barData[id].status ) radios[rid].checked = true;	 
        }
    
      }
    
    }
  8. In the drawChart function, extract the logic of line creation to an external function (delete the highlighted code snippets):
      if(elementStatus.i2011>-1) addLine(context,formatData(a2011, "/2011","#B1DDF3"),"#B1DDF3",elementStatus.i2011==1);
      if(elementStatus.i2010>-1) addLine(context,formatData(a2010, "/2010","#FFDE89"),"#FFDE89",elementStatus.i2010==1);
      if(elementStatus.i2009>-1) addLine(context,formatData(a2009, "/2009","#E3675C"),"#E3675C",elementStatus.i2009==1);
      changeLineView("i2011",barData.i2011.status);
      changeLineView("i2010",barData.i2010.status);
      changeLineView("i2009",barData.i2009.status);
  9. Change the logic in the onChangedRadio callback function. Instead of what it was doing so far let's have it trigger a call to the changeLineView function (we will create that function next):
    function onChangedRadio(e){
      changeLineView(e.target.name,e.target.value);
    }
  10. Create the function changeLineView:
    function changeLineView(id,value){
      barData[id].status = value;
      var dataSource = barData[id];
    
      can = document.getElementById(id);
      context = can.getContext("2d");
      context.clearRect(0,0,wid,hei);
      if( dataSource.status!=HIDE_ELEMENT){
        context.beginPath();
        addLine(context,formatData(dataSource.data, dataSource.label,dataSource.style),dataSource.style,dataSource.status==1);
      }
    }

When you run your HTML file after all these changes, you should see exactly the same thing as you did before we started making all these changes. If that's true then you are in a great place. However, we can't visually see any change yet.

How it works...

The heart of this recipe is our HTML file that enables us to layer canvas elements on top of each other, and as our canvas is by default transparent, we can see through to the elements that are under it. After our canvas is layered with four layers, it's time for us to separate our background from our lines and as such we want to put all of our chart background information right into the base canvas:

var can = document.getElementById("base");

With each line layer, we are using a preconfigured canvas element that is already set:

changeLineView("i2011",barData.i2011.status);
changeLineView("i2010",barData.i2010.status);
changeLineView("i2009",barData.i2009.status); 

The first parameter is both the ID of our canvas and the key we are using in our new object that stores our line information (to keep our code simple):

var barData = {
        i2009:{...},
        i2010:{...},
        i2011:{...}	

      };

In this data object we have exactly the same number of elements as we do in our canvas with the exact same names. This way we can very easily fetch information without using extra variables or conditions. This ties in to the logic of creating/updating lines:

function changeLineView(id,value){
  barData[id].status = value;
  var dataSource = barData[id];

  can = document.getElementById(id);
  context = can.getContext("2d");
  context.clearRect(0,0,wid,hei);
  if( dataSource.status!=HIDE_ELEMENT){
    context.beginPath();
    addLine(context,formatData(dataSource.data, dataSource.label,dataSource.style),dataSource.style,dataSource.status==1);
  }
}

We didn't change the core logic of our line but redirected the logic into the context of the current line:

can = document.getElementById(id);

This way we can extract any direct mention of a year or element without referring to element names directly. This way we can add or remove elements and we would only need to add another canvas in our HTML file, add new properties, and finish off by adding the line in our creating function. That is still a lot, so how about we continue and optimize this code before we move on to more creative lands?

There's more...

Our final goal is this recipe is to help minimize the number of changes the user needs to do to create lines. Currently to add more lines the user would need to make changes in three places. The next few optimization tricks will help us reduce the number of steps it takes to add/remove lines.

Optimizing the drawChart function

Our drawChart function has been through a facelift, but right now, when we are creating our lines we are still referring directly to our current elements:

  changeLineView("i2011",barData.i2011.status);
  changeLineView("i2010",barData.i2010.status);
  changeLineView("i2009",barData.i2009.status);

Instead, let's take advantage of the barData object and use the data keys of this object. This way we can completely avoid the need to refer directly to our explicit elements and instead depend on our data source as the source of information:

  for(var id in barData){
    changeLineView(id,barData[id].status);
  }

Perfect! Now any change in our barData object will define the elements that will get rendered initially when the application starts. We just cut down the number of changes users will need to do to two.

Further streamlining our code

We are in much better shape now than when we started. Originally there where three places in our code that referred directly to hardcoded values for our chart information. With the last update we reduced it to two (once within the HTML file and once in our data source).

It's time for us to remove one more hardcoded instance. Let's remove our extra canvases and create them dynamically.

So let's start by removing our chart canvas elements from the HTML file and setting up an ID to our <div> tag (delete the highlighted code snippet):

<div id="chartContainer" class="graphicLayers" >
      <canvas id="base" class="canvasLayer" width="550" height="400"> </canvas>
  
      <canvas id="i2011" class="canvasLayer" width="550" height="400">       </canvas>
      <canvas id="i2010" class="canvasLayer" width="550" height="400">       </canvas>
      <canvas id="i2009" class="canvasLayer" width="550" height="400">       </canvas>

  </div>

By the way, we added an ID for our <div> containing the layers so we can easily access it and change things within JavaScript.

Now that there isn't any canvas for our layers, we want to dynamically create them only when we draw the chart for the first time (this happens in the drawChart function with the new for loop we just created in the Optimizing the drawChart function section in the previous recipe):

var chartContainer = document.getElementById("chartContainer");

  
  for(var id in barData){
    can = document.createElement("canvas");
    can.id=id;
        can.width=wid;
        can.height=hei; 
    can.setAttribute("class","canvasLayer");
    chartContainer.appendChild(can);

    changeLineView(id,barData[id].status);

  }

}

Refresh your HTML file and you will find our canvas elements looking exactly the way they did before. We have one last thing to sort out to truly make this application dynamic, and that is our controllers that right now are hardcoded in the HTML file.

Creating the radio buttons dynamically

Yet another section that could be dynamic is our creation of radio buttons. So let's start with removing our radio buttons from the HTML file and adding an ID to our wrapper (delete the highlighted code snippet):

<div id="chartContainer" class="controllers">
  2009 : <input type="radio" name="i2009" value="-1" /> off
        <input type="radio" name="i2009" value="0" /> line
        <input type="radio" name="i2009" value="1" select="1" /> full ||
    2010 : <input type="radio" name="i2010" value="-1" /> off
        <input type="radio" name="i2010" value="0" /> line
        <input type="radio" name="i2010" value="1" select="1" /> full ||
    2011 : <input type="radio" name="i2011" value="-1" /> off
        <input type="radio" name="i2011" value="0" /> line
        <input type="radio" name="i2011" value="1" select="1" /> full
  </div>

Back into our HTML file let's create a function that creates new radio buttons. We will call it the appendRadioButton function:

function appendRadioButton(container, id,value,text){
  var radioButton = document.createElement("input");
  radioButton.setAttribute("type", "radio");
  radioButton.setAttribute("value", value);
  radioButton.setAttribute("name", id);

  container.appendChild(radioButton);

  container.innerHTML += text;
}

Last but not the least let's draw our new button right before we start interacting with it:

function init(){
  drawChart();
  
  var radContainer = document.getElementById("controllers");

  var hasLooped= false;
  for(var id in barData){

    radContainer.innerHTML += (hasLooped ? " || ":"") + barData[id].label +": " ;

    appendRadioButton(radContainer,id,-1," off ");
    appendRadioButton(radContainer,id,0," line ");
    appendRadioButton(radContainer,id,1," full ");
    hasLooped = true;

  }

  var radios ;
  for(id in barData){
    radios = document.getElementsByName(id);
    for (var i=0; i<radios.length; i++){
       radios[i].onchange = onChangedRadio;
      if(radios[i].value == barData[id].status ){
         radios[i].checked = true;	 
      }
    }
  }

}

Notice that we are not integrating the two for loops together. Even though it might look like the same thing, the separation is needed. It takes JavaScript some time, a few nanoseconds, to actually render the elements to the screen, and as such by separating our loops we are giving the browser a chance to catch up. The separation between creating the elements and manipulating the elements is present mainly to give JavaScript a chance to render the HTML file before interacting with the created elements.

Great job! We just finished updating our content to make it totally dynamic. Now that everything is controlled through one location, that is the data source, we are ready to start exploring layered canvas logic in the following recipes.

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

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