18 min read

(For more resources related to this topic, see here.)

Introduction

Our first graph/chart under the microscope is the most popular and simplest one to create. We can classify them all roughly under Cartesian-based graphs. Altogether this graph style is relatively simple; it opens the door to creating amazingly creative ways of exploring data. In this article we will lay down the foundations to building charts in general and hopefully motivate you to come up with your own ideas on how to create engaging data visualizations.

Building a bar chart from scratch

The simplest chart around is the one that holds only one dimensional data (only one value per type). There are many ways to showcase this type of data but the most popular, logical, and simple way is by creating a simple bar chart. The steps involved in creating this bar chart will be very similar even in very complex charts. The ideal usage of this type of chart is when the main goal is to showcase simple data, as follows:

Getting ready

Create a basic HTML file that contains a canvas and an onLoad event that will trigger the init function. Load the 03.01.bar.js script. We will create the content of the JavaScript file in our recipe as follows:

<!DOCTYPE html> <html> <head> <title>Bar Chart</title> <meta charset="utf-8" /> <script src="03.01.bar.js"></script> </head> <body onLoad="init();" style="background:#fafafa"> <h1>How many cats do they have?</h1> <canvas id="bar" width="550" height="400"> </canvas> </body> </html>

Creating a graph in general has three steps: defining the work area, defining the data sources, and then drawing in the data.

How to do it…

In our first case, we will compare a group of friends and how many cats they each own. We will be performing the following steps:

  1. Define your data set:

    var data = [{label:"David", value:3, style:"rgba(241, 178, 225, 0.5)"}, {label:"Ben", value:2, style:"#B1DDF3"}, {label:"Oren", value:9, style:"#FFDE89"}, {label:"Barbera", value:6, style:"#E3675C"}, {label:"Belann", value:10, style:"#C2D985"}];

    For this example I’ve created an array that can contain an unlimited number of elements. Each element contains three values: a label, a value, and a style for its fill color.

  2. Define your graph outlines.

    Now that we have a data source, it’s time to create our basic canvas information, which we create in each sample:

    var can = document.getElementById("bar"); var wid = can.width; var hei = can.height; var context = can.getContext("2d"); context.fillStyle = "#eeeeee"; context.strokeStyle = "#999999"; context.fillRect(0,0,wid,hei);

  3. The next step is to define our chart outlines:

    var CHART_PADDING = 20; context.font = "12pt 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); var stepSize = (hei - CHART_PADDING*2)/10; for(var i=0; i<10; i++){ context.moveTo(CHART_PADDING, CHART_PADDING + i* stepSize); context.lineTo(CHART_PADDING*1.3,CHART_PADDING + i* stepSize); context.fillText(10-i, CHART_PADDING*1.5, CHART_PADDING + i* stepSize + 6); } context.stroke();

  4. Our next and final step is to create the actual data bars:

    var elementWidth =(wid-CHART_PADDING*2)/ data.length; context.textAlign = "center"; for(i=0; i<data.length; i++){ context.fillStyle = data[i].style; context.fillRect(CHART_PADDING +elementWidth*i ,hei- CHART_PADDING - data[i].value*stepSize,elementWidth,data[i]. value*stepSize); context.fillStyle = "rgba(255, 255, 225, 0.8)"; context.fillText(data[i].label, CHART_PADDING +elementWidth*(i+.5), hei-CHART_PADDING*1.5); }

    That’s it. Now, if you run the application in your browser, you will find a bar chart rendered.

How it works…

I’ve created a variable called CHART_PADDING that is used throughout the code to help me position elements (the variable is in uppercase because I want it to be a constant; so it’s to remind myself that this is not a value that will change in the lifetime of the application).

Let’s delve deeper into the sample we created starting from our outline area:

context.moveTo(CHART_PADDING,CHART_PADDING); context.lineTo(CHART_PADDING,hei-CHART_PADDING); context.lineTo(wid-CHART_PADDING,hei-CHART_PADDING);

In these lines we are creating the L-shaped frame for our data; this is just to help and provide a visual aid.

The next step is to define the number of steps that we will use to represent the numeric data visually.

var stepSize = (hei - CHART_PADDING*2)/10;

In our sample we are hardcoding all of the data. So in the step size we are finding the total height of our chart (the height of our canvas minus our padding at the top and bottom), which we then divide by the number of the steps that will be used in the following for loop:

for(var i=0; i<10; i++){ context.moveTo(CHART_PADDING, CHART_PADDING + i* stepSize); context.lineTo(CHART_PADDING*1.3,CHART_PADDING + i* stepSize); context.fillText(10-i, CHART_PADDING*1.5, CHART_PADDING + i* stepSize + 6); }

We loop through 10 times going through each step to draw a short line. We then add numeric information using the fillText method.

Notice that we are sending in the value 10-i. This value works well for us as we want the top value to be 10. We are starting at the top value of the chart; we want the displayed value to be 10 and as the value of i increases, we want our value to get smaller as we move down the vertical line in each step of the loop.

Next we want to define the width of each bar. In our case, we want the bars to touch each other and to do that we will take the total space available, and divide it by the number of data elements.

var elementWidth =(wid-CHART_PADDING*2)/ data.length;

At this stage we are ready to draw the bar but before we do that, we should calculate the width of the bars.

We then loop through all the data we have and create the bars:

context.fillStyle = data[i].style; context.fillRect(CHART_PADDING +elementWidth*i ,hei-CHART_PADDING - data[i].value*stepSize,elementWidth,data[i].value*stepSize); context.fillStyle = "rgba(255, 255, 225, 0.8)";

Notice that we are resetting the style twice each time the loop runs. If we didn’t, we wouldn’t get the colors we are hoping to get. We then place our text in the middle of the bar that was created.

context.textAlign = "center";

There’s more…

In our example, we created a non-flexible bar chart, and if this is the way we create charts we will need to recreate them from scratch each time. Let’s revisit our code and tweak it to make it more reusable.

Revisiting the code

Although everything is working exactly as we want it to work, if we played around with the values, it would stop working. For example, what if I only wanted to have five steps; if we go back to our code, we will locate the following lines:

var stepSize = (hei - CHART_PADDING*2)/10; for(var i=0; i<10; i++){

We can tweak it to handle five steps:

var stepSize = (hei - CHART_PADDING*2)5; for(var i=0; i<5; i++){

We would very quickly find out that our application is not working as expected.

To solve this problem let’s create a new function that will deal with creating the outlines of the chart. Before we do that, let’s extract the data object and create a new object that will contain the steps. Let’s move the data and format it in an accessible format:

var data = [...];
var chartYData = [{label:"10 cats", value:1},
{label:"5 cats", value:.5},
{label:"3 cats", value:.3}];
var range = {min:0, max:10};
var CHART_PADDING = 20;
var wid;
var hei;
function init(){

Take a deep look into chartYData object as it enables us to put in as many steps as we want without a defined spacing rule and the range object that will store the minimum and maximum values of the overall graph. Before creating the new functions, let’s add them into our init function (changes marked in bold).

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 = "12pt 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,chartYData);
createBars(context,data);
}

All we did in this code is to extract the creation of the chart and its bars into two separate functions. Now that we have an external data source both for the chart data and the content, we can build up their logic.

Using the fillChart function

The fillChart function’s main goal is to create the foundation of the chart. We are integrating our new stepData object information and building up the chart based on its information.

function fillChart(context, stepsData){ var steps = stepsData.length; var startY = CHART_PADDING; var endY = hei-CHART_PADDING; var chartHeight = endY-startY; var currentY; var rangeLength = range.max-range.min; for(var i=0; i<steps; i++){ currentY = startY + (1-(stepsData[i].value/rangeLength)) * chartHeight; context.moveTo(CHART_PADDING, currentY ); context.lineTo(CHART_PADDING*1.3,currentY); context.fillText(stepsData[i].label, CHART_PADDING*1.5, currentY+6); } context.stroke(); }

Our changes were not many, but with them we turned our function to be much more dynamic than it was before. This time around we are basing the positions on the stepsData objects and the range length that is based on that.

Using the createBars function

Our next step is to revisit the createBars area and update the information so it can be created dynamically using external objects.

function createBars(context,data){ var elementWidth =(wid-CHART_PADDING*2)/ data.length; var startY = CHART_PADDING; var endY = hei-CHART_PADDING; var chartHeight = endY-startY; var rangeLength = range.max-range.min; var stepSize = chartHeight/rangeLength; context.textAlign = "center"; for(i=0; i<data.length; i++){ context.fillStyle = data[i].style; context.fillRect(CHART_PADDING +elementWidth*i ,hei- CHART_PADDING - data[i].value*stepSize,elementWidth,data[i].value*stepSize); context.fillStyle = "rgba(255, 255, 225, 0.8)"; context.fillText(data[i].label, CHART_PADDING +elementWidth*(i+.5), hei-CHART_PADDING*1.5); } }

Almost nothing changed here apart from a few changes in the way we positioned the data and extracted hardcoded values.

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.

Getting ready

If you haven’t had a chance yet to scan through the logic of our first section in this article, 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.

I’ve revisited our data source from the previous section 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 section, you might notice that there are a lot of similarities between the functions in the previous section 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 section. 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.

LEAVE A REPLY

Please enter your comment!
Please enter your name here