Spreading data in a scatter chart

The scatter chart is a very powerful chart and is mainly used to get a bird's-eye view while comparing two data sets. For example, comparing the scores in an English class and the scores in a Math class to find a correlative relationship. This style of visual comparison can help find surprising relationships between unexpected data sets.

This is ideal when the goal is to show a lot of details in a very visual way.

Spreading data in a scatter chart

Getting ready

If you haven't had a chance yet to scan through the logic of our first recipe in this chapter, I recommend you take a peek at it as we are going to base a lot of our work on that while expanding and making it a bit more complex to accommodate two data sets.

The regular HTML start-up code can be found in the code bundle or go through Chapter 1, Drawing Shapes in Canvas, for more information on creating the HTML document.

I've revisited our data source from the previous recipe and modified it to store three variables of students' exam scores in Math, English, and Art.

var data = [{label:"David",
       math:50,
       english:80,
       art:92,
       style:"rgba(241, 178, 225, 0.5)"},
       {label:"Ben",
       math:80,
       english:60,
       art:43,
       style:"#B1DDF3"},
       {label:"Oren",
       math:70,
       english:20,
       art:92,
       style:"#FFDE89"},
       {label:"Barbera",
       math:90,
       english:55,
       art:81,
       style:"#E3675C"},
       {label:"Belann",
       math:50,
       english:50,
       art:50,
       style:"#C2D985"}];

Notice that this data is totally random so we can't learn anything from the data itself; but we can learn a lot about how to get our chart ready for real data. We removed the value attribute and instead replaced it with math, english, and art attributes.

How to do it...

Let's dive right into the JavaScript file and the changes we want to make:

  1. Define the y space and x space. To do that, we will create a helper object that will store the required information:
    var chartInfo= { y:{min:40, max:100, steps:5,label:"math"},
            x:{min:40, max:100, steps:4,label:"english"}
          };
  2. It's time for us to set up our other global variables and start up our init function:
    var CHART_PADDING = 30;
    var wid;
    var hei;
    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.lineTo(CHART_PADDING,hei-CHART_PADDING);
      context.lineTo(wid-CHART_PADDING,hei-CHART_PADDING);
      
      fillChart(context,chartInfo);
      createDots(context,data);
    }

    Not much is new here. The major changes are highlighted. Let's get on and start creating our fillChart and createDots functions.

  3. If you worked on our previous recipe, you might notice that there are a lot of similarities between the functions in the previous recipe and this function. I've deliberately changed the way we create things just to make them more interesting. We are now dealing with two data points as well, so many details have changed. Let's review them:
    function fillChart(context, chartInfo){
      var yData = chartInfo.y;
      var steps = yData.steps;
      var startY = CHART_PADDING;
      var endY = hei-CHART_PADDING;
      var chartHeight = endY-startY;
      var currentY;
      var rangeLength = yData.max-yData.min;
      var stepSize = rangeLength/steps;
      context.textAlign = "left";
      for(var i=0; i<steps; i++){
        currentY = startY + (i/steps) *	chartHeight;
        context.moveTo(wid-CHART_PADDING, currentY );
        context.lineTo(CHART_PADDING,currentY);
        context.fillText(yData.min+stepSize*(steps-i), 0, currentY+4);
      }
      
      currentY = startY +	chartHeight;
      context.moveTo(CHART_PADDING, currentY );
      context.lineTo(CHART_PADDING/2,currentY);
      context.fillText(yData.min, 0, currentY-3);
      
      
      var xData = chartInfo.x;
      steps = xData.steps;
      var startX = CHART_PADDING;
      var endX = wid-CHART_PADDING;
      var chartWidth = endX-startX;
      var currentX;
      rangeLength = xData.max-xData.min;
      stepSize = rangeLength/steps;
      context.textAlign = "left";
      for(var i=0; i<steps; i++){
        currentX = startX + (i/steps) *	chartWidth;
        context.moveTo(currentX, startY );
        context.lineTo(currentX,endY);
        context.fillText(xData.min+stepSize*(i), currentX-6, endY+CHART_PADDING/2);
      }
      
      currentX = startX +	chartWidth;
      context.moveTo(currentX, startY );
      context.lineTo(currentX,endY);
      context.fillText(xData.max, currentX-3, endY+CHART_PADDING/2);
      
      
      context.stroke();
      
    }

    When you review this code you will notice that our logic is almost duplicated twice. While in the first loop and first batch of variables we are figuring out the positions of each element in the y space, we move on in the second half of this function to calculate the layout for the x area. The y axis in canvas grows from top to bottom (top lower, bottom higher) and as such we need to calculate the height of the full graph and then subtract the value to find positions.

  4. Our last function is to render the data points and to do that we create the createDots function:
    function createDots(context,data){
      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;
      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);
    
    
      }  
    }

    Here we are figuring out the same details for each point—both the y position and the x position—and then we draw a rectangle. Let's test our application now!

How it works...

We start by creating a new chartInfo object:

var chartInfo= { y:{min:40, max:100, steps:5,label:"math"},
        x:{min:40, max:100, steps:4,label:"english"}
      };

This very simple object encapsulates the rules that will define what our chart will actually output. Looking closely you will see that we set an object named chartInfo that has information on the y and x axes. We have a minimum value (min property), maximum value (max property), and the number of steps we want to have in our chart (steps property), and we define a label.

Let's look deeper into the way the fillChart function works. In essence we have two numeric values; one is the actual space on the screen and the other is the value the space represents. To match these values we need to know what our data range is and also what our view range is, so we first start by finding our startY point and our endY point followed by calculating the number of pixels between these two points:

var startY = CHART_PADDING;
var endY = hei-CHART_PADDING;
var chartHeight = endY-startY;

These values will be used when we try to figure out where to place the data from the chartInfo object. As we are already speaking about that object, let's look at what we do with it:

  var yData = chartInfo.y;
  var steps = yData.steps;
  var rangeLength = yData.max-yData.min;
  var stepSize = rangeLength/steps;

As our focus right now is on the height, we are looking deeper into the y property and for the sake of comfort we will call it yData. Now that we are focused on this object, it's time to figure out what is the actual data range (rangeLength) of this value, which will be our converter number. In other words we want to take a visual space between the points startY and endY and based on the the range, position it in this space. When we do so we can convert any data into a range between 0-1 and then position them in a dynamic visible area.

Last but not least, as our new data object contains the number of steps we want to add into the chart, we use that data to define the step value. In this example it would be 12. The way we get to this value is by taking our rangeLength (100 - 40 = 60) value and then dividing it by the number of steps (in our case 5). Now that we have got the critical variables out of the way, it's time to loop through the data and draw our chart:

var currentY;
context.textAlign = "left";
  for(var i=0; i<steps; i++){
    currentY = startY + (i/steps) *	chartHeight;
    context.moveTo(wid-CHART_PADDING, currentY );
    context.lineTo(CHART_PADDING,currentY);
    context.fillText(yData.min+stepSize*(steps-i), 0, currentY+4);
  }

This is where the magic comes to life. We run through the number of steps and then calculate the new Y position again. If we break it down we will see:

currentY = startY + (i/steps) *	chartHeight;

We start from the start position of our chart (upper area) and then we add to it the steps by taking the current i position and dividing it by the total possible steps (0/5, 1/5, 2/5 and so on). In our demo it's 5, but it can be any value and should be inserted into the chartInfo steps attribute. We multiply the returned value by the height of our chart calculated earlier.

To compensate for the fact that we started from the top we need to reverse the actual text we put into the text field:

yData.min+stepSize*(steps-i)

This code takes our earlier variables and puts them to work. We start by taking the minimal value possible and then add into it stepSize times the total number of steps subtracted by the number of the current step.

Let's dig into the createDots function and see how it works. We start with our setup variables:

var yDataLabel = chartInfo.y.label;
var xDataLabel = chartInfo.x.label;

This is one of my favorite parts of this recipe. We are grabbing the label from our chartInfo object and using that as our ID; this ID will be used to grab information from our data object. If you wish to change the values, all you need to do is switch the labels in the chartInfo object.

Again it's time for us to figure out our ranges as we've done earlier in the fillChart function. This time around we want to get the actual ranges for both the x and y axes and the actual width and height of the area we have to work with:

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;

We also need to get a few variables to help us keep track of our current x and y positions within loops:

var yPos;
var xPos;

Let's go deeper into our loop, mainly into the highlighted code snippets:

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);

  }

The heart of everything here is discovering where our elements need to be. The logic is almost identical for both the xPos and yPos variables with a few variations. The first thing we need to do to calculate the xPos variable is:

(data[i][xDataLabel]-chartInfo.x.min)

In this part we are using the label, xDataLabel, we created earlier to get the current student score in that subject. We then subtract from it the lowest possible score. As our chart doesn't start from 0, we don't want the values between 0 and our minimum value to affect the position on the screen. For example, let's say we are focused on math and our student has a score of 80; we subtract 40 out of that (80 - 40 = 40) and then apply the following formula:

(data[i][xDataLabel] - chartInfo.x.min) / xDataRange

We divide that value by our data range; in our case that would be (100 - 40)/60. The returned result will always be between 0 and 1. We can use the returned number and multiply it by the actual space in pixels to know exactly where to position our element on the screen. We do so by multiplying the value we got, that is between 0 and 1, by the total available space (in this case, width). Once we know where it needs to be located we add the starting point on our chart (the padding):

xPos = CHART_PADDING + (data[i][xDataLabel]-chartInfo.x.min)/xDataRange * chartWidth;

The yPos variable has the same logic as that of the xPos variable, but here we focus only on the height.

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

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