Creating Cartesian-based Graphs

Exclusive offer: get 50% off this eBook here
HTML5 Graphing and Data Visualization Cookbook

HTML5 Graphing and Data Visualization Cookbook — Save 50%

Learn how to create interactive HTML5 charts and graphs with canvas, JavaScript, and open source tools with this book and ebook.

$29.99    $15.00
by Ben Fhala | January 2013 | Cookbooks

In this article by Ben Fhala, author of HTML5 Graphing and Data Visualization Cookbook, we will cover the following topics:

  • Building a bar chart from scratch

  • Spreading data in a scatter chart

  • Building line charts

  • Creating the flying brick chart (waterfall chart)

  • Building a candlestick chart (stock chart)

(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='//dgdsbygo8mp3h.cloudfront.net/sites/default/files/blank.gif' data-original="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.

HTML5 Graphing and Data Visualization Cookbook Learn how to create interactive HTML5 charts and graphs with canvas, JavaScript, and open source tools with this book and ebook.
Published: November 2012
eBook Price: $29.99
Book Price: $49.99
See more
Select your format and quantity:

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 section, 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.

Getting ready

As usual get your HTML wrapper ready. In this section we actually are going to base our changes on the previous section, 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 section (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 section. Let's take a look at the modifications that have taken place in this section:

  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 section: 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 section, I wanted to keep it up-to-date with all the basics in case you want to expand this sample.

This is one of the most powerful tricks in programming 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):

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.

Creating the flying brick chart (waterfall chart)

In each section in this article we've been advancing the complexity of our code and so we are going to revisit the bar chart and modernize it to fit our evolving charting platform. After we complete this mini task, we will be ready to create our first waterfall chart breaking away from the standard charts into more creative avenues.

The waterfall chart is a very useful chart to outline trends, such as monthly total changes (positive and negative) while outlining the total value of the big picture. This type of chart helps to outline total assets of a company while showing if they made profits or losses throughout the month. This type of chart is ideal for data that shifts between positive/negative values.

Getting ready

We will be taking advantage of the interface we created in the earlier sections and as such we will be integrating the creation of bars into our library of updated functions.

The following is the code before implementing changes:

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

And the following is our newly updated function to fit into the new skills.

function createBars(context,data){
var range = chartInfo.x;
var elementWidth =(wid-CHART_PADDING*2)/ data.length;
var startY = CHART_PADDING;
var endY = hei-CHART_PADDING;
var chartHeight = endY-startY;
var stepSize = chartHeight/(chartInfo.y.max-chartInfo.y.min);
context.textAlign = "center";
for(i=0; i<data.length; i++){
context.fillStyle = data[i].style;
context.fillRect(CHART_PADDING +elementWidth*i ,endY
- data[i][chartInfo.y.label]*stepSize,elementWidth,data[i]
[chartInfo.y.label]*stepSize);

context.fillStyle = "rgba(255, 255, 225, 0.8)";
context.fillText(data[i].label, CHART_PADDING
+elementWidth*(i+.5), hei-CHART_PADDING*1.5);
}
}

These changes are more than cosmetic. Now that our function is up-to-date and working in our latest logic developed in the previous two sections, it's time to start building our waterfall chart.

How to do it...

The first step to create a waterfall chart is to copy, paste, and rename the function createBars and then manipulate it and change the way the data is rendered (mainly where and how we position elements). Notice what we change in this method before I dig deeper into why and how it was done:

  1. Let's start with an updated data source:

    var a2011 = [60,60,60,111,-31,-80,0,-43,-29,14,64,12];
    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. In the init function we will update the following highlighted code snippets:

    function init(){
    ...
    context.strokeStyle = "#cccccc";
    fillChart(context,chartInfo);
    createWaterfall(context,formatData(a2011));
    }

  3. Add a few helper variables:

    function createWaterfall(context,data){
    var range = chartInfo.x;
    var elementWidth =(wid-CHART_PADDING*2)/ data.length;
    var startY = CHART_PADDING;
    var endY = hei-CHART_PADDING;
    var chartHeight = endY-startY;
    var stepSize = chartHeight/(chartInfo.y.max-chartInfo.y.min);
    var currentY= endY;
    var elementValue ;
    var total=0;
    context.textAlign = "center";

  4. In the for loop logic, draw a rectangle if the value is not 0:

    for(i=0; i<data.length; i++){
    elementValue = data[i][chartInfo.y.label];
    total +=elementValue;
    if(elementValue!=0){
    context.fillStyle = elementValue>0? "#C2D985" :"#E3675C" ;
    currentY -=(elementValue*stepSize);
    context.fillRect(CHART_PADDING +elementWidth*i ,currentY,ele
    mentWidth,elementValue*stepSize);
    }

  5. If the current data value is 0 then make it a total column:

    else{
    context.fillStyle = "#B1DDF3" ;
    context.fillRect(CHART_PADDING +elementWidth*i
    ,currentY,elementWidth,endY-currentY);
    elementValue = total; //hack so we see the right value
    }
    context.fillStyle = "rgba(255, 255, 255, .8)"
    ;

  6. Add the changed value inside the element:

    context.fillText(elementValue, CHART_PADDING
    +elementWidth*(i+.5), endY - (stepSize*total) +
    (stepSize*elementValue/2) + 6);
    }
    }

There are a lot of changes here, but in essence these two functions do almost the same thing; only our waterfall chart is smarter and more detailed.

How it works...

The first step and the first problem when we start to think about how to create a waterfall chart is that there is a relationship between each element. To simplify the logic we would want to create a counter that would store the changes (the current summary value).

var elementValue ; var total=0;

The first variable is just a helper variable to try to make our code more readable while the total is all about keeping up with what is the current total.

It's time to jump into the for loop and see the big changes. Let's focus first on the types of tasks we might need to do within a waterfall chart. There are three types (value goes up, value goes down, and value remains unchanged). Before we start figuring out how to deal with these cases, let's update our variables:

for(i=0; i<data.length; i++){ elementValue = data[i][chartInfo.y.label]; total +=elementValue;

The element value will give us the current numeric value (don't forget this does not correspond to the size on screen; we would still need to duplicate this by the stepSize number when we want to draw the element as this is the raw real number). Same is true for our total variable; we are just keeping track of the current summary.

So as we stated earlier, we have three possible tasks and there is nothing wrong with creating an if...else case as follows:

if(elementValue>0){ //do the positive values }else if(elementValue<0){ //do the negative values }else{ //do 0 }

This would capture all three options but would add some extra code that wouldn't be needed. So, we will use the same if statement for both negative and positive values as their logic is very close to each other. That way we can reuse our code and type less.

if(elementValue!=0){ //do positive/negative values }else{ // do 0 }

Perfecto! Now let's dig into the positive/negative task:

context.fillStyle = elementValue>0? "#C2D985" :"#E3675C" ; currentY -=(elementValue*stepSize); context.fillRect(CHART_PADDING +elementWidth*i ,currentY,elementWidth, elementValue*stepSize);

Notice that our first line of code in this block is the only difference between positive/negative values. We are literally just changing a color based on whether we are in the positive or negative range using a ternary operation. After figuring out our currentY position we create a rectangle in the current position (this current position is after the value was added, so this is our end point). The most important element to notice is the fourth parameter, elementValue*stepSize. The fourth parameter captures the size of the rectangle. With it, it captures if it's a negative or positive value. The elementValue variable can be positive or negative. This is the trick we are using here as we would draw the bar upwards (if the value is negative) or draw the bar downwards (if the value is positive). If we first created the drawing before updating our currentY position, it would be much harder and probably require us to create the three separate if cases. Cases like these are really what makes programming so much fun to me; finding these hidden ways to take advantage of the same code to do opposite things.

It's time for us to visit the else case:

}else{
context.fillStyle = "#B1DDF3" ;
context.fillRect(CHART_PADDING +elementWidth*i
,currentY,elementWidth,endY-currentY);
elementValue = total; //hack so we see the right value
}

In the else case we want to draw the full length of the bar followed by a small hack. We are assigning the value of our current total into the elementValue variable (this will not change our original data as we are doing this as the last thing after we have no more use for the elementValue variable). We are doing this to avoid another if...else statement when adding the text into the bars. Only if the value is 0, we want the total to be displayed instead of the current change and that's what this hack captures.

Leaving us the last part of creating a bar for our waterfall chart, which is to get the value of the bar in the center of the element we just created:

context.fillStyle = "rgba(255, 255, 255, .8)";
context.fillText(elementValue,
CHART_PADDING +elementWidth*(i+.5),
endY - (stepSize*total) + (stepSize*elementValue/2) + 6);

Take a deep look at the positioning of the text element; it took a bit of tinkering until I figured it out. What I'm doing here mainly in the last parameter (the y position of our new text) is taking the bottom area of our chart, subtracting the current total that would give us exactly the tip of the bar. But that wouldn't work well as for positive values as it would be above the bar while for negative elements it would place it within the bottom area of the bar. Here comes the creative thinking; instead of creating a few cases, how about we position our text exactly in the middle of our element. To do that, we can take advantage of our elementValue variable again (as it's positive or negative) and if we take half its size and add it to our total, we will be in the center of the bar leaving only one last tweak adding 6 to our value (as our text is 12 pixels in height).

There you go! You have just created your first waterfall chart. Let's test it; remove any data visualization function calls from our init function (such as createBars or addLine) and instead replace them with our new function:

createWaterfall(context,formatData(a2011));

Note that I am using the formatData object as I'm just reusing our array from an earlier sample. I just updated the values so they don't step out of a total of 300:

var a2011 = [60,60,60,111,-31,-80,0,-43,-29,14,64,12];

 

There's more...

Where we ended, leads us to the issue that we can't control the data and the more we ask of the end user/developer to adjust, the longer the learning curve is. It's great that we have our chartInfo object that stores most of our helper information, but what if someone doesn't fill out the attributes? Should our application fail or should we do our best to figure out new default values for the user? So let's say in this example the user does not fill out the max and min attributes of the y object:

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

The user indicates how many steps they want, but they do not provide any information on the smallest and largest values the chart should output. To solve this issue we need to revisit the way we are creating our charts. So far we created the chart in two totally separate steps (the last two lines in our init function):

fillChart(context,chartInfo); createWaterfall(context,formatData(a2011));

First we usually create the background and then draw the items, but in this case we have a much clearer relationship between the fillchart function and the createWaterfall function. As we are trying to reduce the user's code footprint we wouldn't want to add a bunch of logic for each sample that would be unique for each bar type. So instead we will revisit all the graphic functions we created ( addLine, createBars, and createWaterfall ) and move the fillChart function call to be the first thing in the functions. This will enable us to create custom tweaks before calling the fillChart function that would be invisible to the end user of our functions (such as you in a few months, so you don't need to remember how everything works). Everything should work the same for now but only our createWaterfall function will know how to deal with missing information (I'll leave it up to you to update the other functions).

function createWaterfall(context,data){ fillChart(context,chartInfo); //all the rest the same //do to all 3 functions

Now that we have our fillChart function and everything is working, let's add some extra logic just before we call the fillChart function to help add the min /max values dynamically:

function createWaterfall(context,data){
if(!chartInfo.y.min || !chartInfo.y.max)
updateCumulativeChartInfo(chartInfo,data);
fillChart(context,chartInfo);

Notice that we are checking to see if the min or max values are missing and if so, we are calling the updateCumulativeChartInfo function to update or add the values.

Let's create the updateCumulativeChartInfo function:

function updateCumulativeChartInfo(chartInfo,data){ var aTotal=[]; var total = 0; aTotal.push(total); for(i=0; i<data.length; i++){ total +=data[i][chartInfo.y.label] aTotal.push(total); } chartInfo.y.min = Math.min.apply(this,aTotal); chartInfo.y.max = Math.max.apply(this,aTotal); }

We are using two variables: aTotal and total. The aTotal variable stores the total in each loop. After we have the value of our total variable throughout all phases of our chart within the aTotal array, it's time to figure out what the minimum and maximum value will be. We have a problem here. The Math.min method can take in an unlimited number of parameters but we have an array that isn't compatible with the requirements of the Math.min method. To determine the values we can use a fun hack by using the apply method. Every function has an apply method. What the apply method does is enable you to change the scope of a function and send the parameters as an array.

For more info on the apply method check the video at the following website: http://02geek.com/catagory/favorites/apply.html

Now that our data has been dynamically created everything should work. When we run the application we will see we are getting some numbers that are too detailed (such as 3.33333). The next step is to do some format tweaking.

Cleaning the format of numbers

To solve the problem of our numeric values being very ugly, we can create a formatting function and just call it each time we output dynamically created values. So let's first create the function:

function formatNumber(num,lead){ for(var i=0;i<lead;i++) num*=10; num = parseInt(num); for(var i=0;i<lead;i++) num/=10; return num; }

The function parameters are the values to be formatted ( num ) and the number of places we want after the decimal point. In the function, we are multiplying the value by ten; the number of times is based on the value of the lead variable. We then convert the number into an integer and divide the number again.

Last but not least, let's track down where we are adding the text; we will find it in the fillChart function. All that is left for us is to find the right text that is affected and update it to use our new formatting function:

context.fillText(formatNumber,
(yData.min+stepSize*(steps-i),2), 0,
currentY+4);

Our format will look much better. And yes, you probably should leave these details to be configurable in the external chartInfo object, but we will leave that to you to make our library even smarter.

Other tasks I've left open

Our new waterfall has one assumption that we always start from zero. In our sample we won't change that, but we will revisit this idea in the next section when working with the candlestick chart. If you're bold, try to figure out a solution.

HTML5 Graphing and Data Visualization Cookbook Learn how to create interactive HTML5 charts and graphs with canvas, JavaScript, and open source tools with this book and ebook.
Published: November 2012
eBook Price: $29.99
Book Price: $49.99
See more
Select your format and quantity:

Building a candlestick chart (stock chart)

We are just about to make a super leap. Until now we worked with charts that had one data point, two data points, and a few variations on them, and now we are moving into a new world of four data points in every bar. The stock chart is a way to showcase changes in the market in a given time frame (in our example this is one day). Each day stock prices change many times, but the most important factors are the low and high values of the day and the opening and closing prices. A stock analyst needs to be able to see the information quickly and understand overall trends.

The worst thing you can do is to assume that the only usage of four dimensions of data is in the stock market. This is where you can come up with the next big thing. Visualizing data in a clean and quick way and converting data into logic is one of the most fun things about charts. With that said let's start creating our stock chart.

Getting ready

Our first step is going to be a bit different in this section. I've created a sample CSV file called DJI.txt; you can find it in our source files. The format is the standard CSV format and the first line names all the data columns:

DATE,CLOSE,HIGH,LOW,OPEN,VOLUME

And all future lines contain the data (daily data in our case):

1309752000000,12479.88,12506.22,12446.05,12505.99,128662688

So the steps we will need to go through are loading the file, converting the data to fit our standard data set, and then build the new chart type (and then fixing things as we discover issues; agile development).

How to do it...

We are going to base our work starting from where we left in the previous section. We will start the modifications right in the JavaScript file:

  1. Let's update our global variables:

    var chartInfo= { y:{min:11500, max:12900,steps:5,label:"close"}, x:{min:1, max:12, steps:11,label:"date"} }; var stockData; var CHART_PADDING = 20; var wid; var hei

  2. Before we start our internal logic, we need to load our new external CSV file. We will rename the init function and call it startUp and then create a new init function:

    function init(){ var client = new XMLHttpRequest(); client.open('GET', 'data/DJI.txt'); client.onreadystatechange = function(e) { if(e.target.readyState==4){ var aStockInfo = e.target.responseText.split("\n"); stockData = translateCSV(aStockInfo,7); startUp() } } client.send(); } function startUp(){ //old init function }

  3. The data we get back from the CSV file needs to be formatted to a structure we can work with. For that we create the translateCSV function that takes in the raw CSV data and converts it into an object that matches our architecture needs:

    function translateCSV(data,startIndex){ startIndex|=1; //if nothing set set to 1 var newData = []; var aCurrent; var dataDate; for(var i=startIndex; i<data.length;i++){ aCurrent = data[i].split(","); dataDate = aCurrent[0].charAt(0)=="a"?parseInt(aCurrent[0].sli ce(1)):parseInt(aCurrent[0]); newData.push({ date:dataDate, close:parseFloat(aCurrent[1]), high:parseFloat(aCurrent[2]), low:parseFloat(aCurrent[3]), open:parseFloat(aCurrent[4]), volume:parseFloat(aCurrent[5]) }); } return newData; }

  4. Our startUp function, formally known as init, will remain the same besides changing the createWaterfall method to call addStock:

    function startUp(){
    ...
    addStock(context,stockData);
    }

  5. It is time to create the addStock function:

    function addStock(context,data){ fillChart(context,chartInfo); var elementWidth =(wid-CHART_PADDING*2)/ data.length; var startY = CHART_PADDING; var endY = hei-CHART_PADDING; var chartHeight = endY-startY; var stepSize = chartHeight/(chartInfo.y.max-chartInfo.y.min); var openY; var closeYOffset; var highY; var lowY; var currentX; context.strokeStyle = "#000000"; for(i=0; i<data.length; i++){ openY = (data[i].open-chartInfo.y.min)*stepSize; closeYOffset = (data[i].open-data[i].close)*stepSize; highY = (data[i].high-chartInfo.y.min)*stepSize; lowY =(data[i].low-chartInfo.y.min)*stepSize; context.beginPath(); currentX = CHART_PADDING +elementWidth*(i+.5); context.moveTo(currentX,endY-highY); context.lineTo(currentX,endY-lowY); context.rect(CHART_PADDING +elementWidth*i ,endY-openY,element Width,closeYOffset); context.stroke(); context.fillStyle = closeYOffset<0? "#C2D985" :"#E3675C" ; context.fillRect(CHART_PADDING +elementWidth*i ,endY-openY,ele mentWidth,closeYOffset); } }

All these steps are required to create a new candlestick chart.

How it works...

Let's review the steps to load our external file. If you are working with open source tools such as jQuery you will be better off using them to load external files but, to avoid using other libraries, we will work with the XMLHttpRequest object as it's supported in all modern browsers that support HTML5.

var client = new XMLHttpRequest(); client.open('GET', 'data/DJI.txt');

We start with creating a new XMLHttpRequest object:

client.onreadystatechange = function(e) { if(e.target.readyState==4){ var aStockInfo = e.target.responseText.split("\n"); stockData = translateCSV(aStockInfo,1); startUp() } } client.send();

The next step is to set what we want to do ( GET/POST ) and the name of the file, followed by creating a handler function for the onreadystatechange callback and sending our request.

The event handler onreadystatechange gets called a few times throughout the loading process of a file. We only want to listen in and act once the file is loaded and ready to be played with; to do that we will check whether the readyState variable is equal to four (ready and loaded). When the file is loaded, we want to split our file into an array based on line breaks.

Note that the file was created on a Mac. The \n does the trick, but when you create your own files or download files, you might need to use \r or a combination \n\r or \n\r. Always confirm that you made the right selection by outputting the length of your array and validating its right size (then test to see if its content is what you expect it to be).

After our array is ready we want to format it to the user-friendly format followed by starting up the old init function that is now known as startUp.

Let's quickly review the translateCSV formatting function. We are literally looping through our data array that was created earlier and replacing each line with a formatted object that will work for our needs. Notice that we have an optional parameter startIndex. If nothing or zero is set then on the first line we are assigning it the value of 1:

startIndex||=1; //if nothing set set to 1

The former is a shorthand way of writing:

startIndex = startIndex || 1;

If the startIndex parameter has a value that is equivalent to true then it would remain as it was; if not, it would be converted to 1.

By the way, if you don't know how to work with these shortcuts, I really recommend getting familiar with them; they are really fun and save time and typing. If you want to learn more on this check the following links:

Great! Now we have a data source that is formatted in the style we've been using so far.

We will hardcode our chartInfo object. It will work out well for our y values but not that well for our date requirements (in the x values). We will revisit that issue later after we get our chart running. We created a dynamic range generator in an earlier exercise, so if you want to keep up with that then review it and add that type of logic into this chart as well, but for our needs we will keep it hardcoded for now.

Ok, so let's dig deeper into the addStock function. By the way, notice that as we are working with the same format and overall tools, we can mix charts together with ease. But before we do that, let's understand what the addStock function actually does. Let's start with our base variables:

fillChart(context,chartInfo); var elementWidth =(wid-CHART_PADDING*2)/ data.length; var startY = CHART_PADDING; var endY = hei-CHART_PADDING; var chartHeight = endY-startY; var stepSize = chartHeight/(chartInfo.y.max-chartInfo.y.min);

We are gathering information that will make it easier to work in our loop when creating the bars from the width of elements ( elementWidth to the ratio between our values and the height of our chart). All these variables have been covered in earlier sections in this article.

var openY;
var closeYOffset;
var highY;
var lowY;
var currentX;
context.strokeStyle = "#000000";

These variables are going to be our helper variables (updated after every round of our loop) to establish the position of the high, low, open, and close offsets (as we are drawing a rectangle, it expects the height and not a second y value).

The first thing we do in each round of our loop is to find out the values for these variables:

for(i=0; i<data.length; i++){ openY = (data[i].open-chartInfo.y.min)*stepSize; closeYOffset = (data[i].open-data[i].close)*stepSize; highY = (data[i].high-chartInfo.y.min)*stepSize; lowY =(data[i].low-chartInfo.y.min)*stepSize;

You will notice that the logic is almost the same in all of the variables. We are just subtracting the minimum from the value (as our chart doesn't cover values under our minimum value), and then multiplying it by our stepSize ratio to have the value fit within our chart dimensions (this way even if we change our chart size everything should continue working). Note that only the closeYOffset variable doesn't subtract the min property but instead it subtracts the close property.

The next step is to draw our candlestick chart starting with a line from the low to the high of the day:

context.beginPath();
currentX = CHART_PADDING +elementWidth*(i+.5);
context.moveTo(currentX,endY-highY);
context.lineTo(currentX,endY-lowY);

This will be followed by the rectangle that represents the full open and close values:

context.rect(CHART_PADDING +elementWidth*i ,endY-openY, elementWidth,closeYOffset); context.stroke(); context.fillStyle = closeYOffset<0? "#C2D985" :"#E3675C" ; context.fillRect(CHART_PADDING +elementWidth*i ,endY-openY,element Width,closeYOffset); }

After this, we will create a fill to this rectangle and set the style color based on the value of the closeYOffset variable. At this stage we have a running application, although it can use a few more tweaks to make it work better.

There's more...

It's time to fix our x coordinate values:

var chartInfo= { y:{min:11500, max:12900,steps:5,label:"close"},
x:{min:1, max:12, steps:11,label:"date"}
};

We didn't change this variable before as until now there was a clear separation between the outline and our content (the chart itself); but at this stage as our x outline content isn't a linear number anymore but a date; we need to somehow introduce into the fillChart method external data that is related to the content of the chart. The biggest challenge here is that we don't want to introduce into this method something that is only relevant to our chart as this is a globally used function. Instead we want to put our unique data in an external function and send that function in as a formatter. So let's get started:

var chartInfo= { y:{min:11500, max:12900,steps:5,label:"close"},
x:{label:"date",formatter:weeklyCapture}
};

Our x space in a stock chart represents time and as such our previous usage based on linear data does not apply (the properties such as min, max, and steps have no meaning in this case). We will remove them in favor of a new property formatter that will take a function as its value. We will use this formatter function instead of the default function. If this function is set we will let an external function define the rules. We will see more on this when we describe the weeklyCapture function. This method of coding is called plugin coding. Its name is derived out of the idea of enabling us to create replaceable functions without reworking our core logic in the future. Before we create the weeklyCapture function, let's tweak the chartInfo object so we have the right range and number of steps:

function addStock(context,data){
if(!chartInfo.x.max){
chartInfo.x.min = 0;
chartInfo.x.max = data.length;
chartInfo.x.steps = data.length;
}
fillChart(context,chartInfo);
...

What we are doing here is, before we call the fillChart function in our addStock function, we are checking to see if the max value is set; if it isn't set, we are going to reset all the values, setting the min to 0 and the max and steps to the length of our data array. We are doing this as we want to travel through all of our data elements to test and see if there is a new weekday.

Now we integrate our weeklyCapture function into the fillChart function.

function fillChart(context, chartInfo){
// ....
var output;
for(var i=0; i<steps; i++){
output = chartInfo.x.formatter && chartInfo.x.formatter(i);
if(output || !chartInfo.x.formatter){

currentX = startX + (i/steps) * chartWidth;
context.moveTo(currentX, startY );
context.lineTo(currentX,endY);
context.fillText(output?output:xData.min+stepSize*(i),
currentX-6, endY+CHART_PADDING/2);
}

}
if(!chartInfo.x.formatter){
currentX = startX + chartWidth;
context.moveTo(currentX, startY );
context.lineTo(currentX,endY);
context.fillText(xData.max, currentX-3, endY+CHART_PADDING/2);
}
context.stroke();
}

In our first step, we are going to fetch the value that comes back from our formatter function.

output = chartInfo.x.formatter && chartInfo.x.formatter(i);

The logic is simple, we are checking to see if the formatter function exists and if it does we are calling it and sending the current value of i (as we are in the loop).

The next step is if our output isn't empty (negative or has a value equivalent to false) or if our output is empty but our formatter isn't active then render the data:

if(output || !chartInfo.x.formatter){
currentX = startX + (i/steps) * chartWidth;
context.moveTo(currentX, startY );
context.lineTo(currentX,endY);
context.fillText(output?output:xData.min+stepSize*(i), currentX-6,
endY+CHART_PADDING/2);
}

Only if we have an output from the formatter function and/or if the formatter function does not exist we don't need to do anything. As such we need the if statement to capture both the scenarios, if we do not have the if statement, then our output will not conform to our earlier sections. The only content we are changing within this block of code is the fillText method. If we are working with our output, we want to use that for the text. If not, we want to keep the logic that was there before the same:

if(output || !chartInfo.x.formatter){
currentX = startX + (i/steps) * chartWidth;
context.moveTo(currentX, startY );
context.lineTo(currentX,endY);
context.fillText(output?output:xData.min+stepSize*(i),
currentX-6,
endY+CHART_PADDING/2);

}

We have one last thing we need to cover before we can run our application and see it in action and that is to create our weeklyCapture function. So let's create it now:

var DAY = 1000*60*60*24; function weeklyCapture(i){ var d; if(i==0){ d = new Date(stockData[i].date); }else if ( i>1 && stockData[i].date != stockData[i-1].date+1 ){ d = new Date(stockData[i].date + DAY*stockData[i].date ); } return d? d.getMonth()+1+"/"+d.getDate():false; }

We start by creating a helper variable called DAY that will store how many milliseconds there are in a day:

var DAY = 1000*60*60*24;

If you take a peek at our external data, you will see that only on day 0 we have an actual date (formatted in milliseconds since 1970). All we need to do is send that to the date object to create a date:

var d; if(i==0){ d = new Date(stockData[i].date); }

While all other data lines contain only a number that represents how many days passed since that original date, we want to test and see if the current date is only one day after the last day. Only if the date change is greater than one day, we will create a new date format for it:

}else if ( i>1 && stockData[i].date != stockData[i-1].date+1 ){ d = new Date(stockData[0].date + DAY*stockData[i].date ); }

Note that to create the date object we need to take our current original date from row 0 and then add to it the total days in milliseconds (multiplying our DAY variable with the current day value).

With this method all that is left to check is if we have a valid date. Let's format it and send it back, and if not, we will send back false:

return d? d.getMonth()+1+"/"+d.getDate():false;

Congratulations! Now our sample is a fully fledged integrated candlestick chart with live dynamic dates.

Adding other render options to our stock chart

Although the candlestick chart is a very popular option, there is another popular technical chart view. One that is used when there are no colors to use. Instead of the usage of colors, on the left-hand side we draw a line to define the opening price, and on the right-hand side we capture the closing price. Let's integrate that logic into our chart as an optional render mode. We will add a new parameter to the addStock function:

function addStock(context,data,isCandle){

We are now going to adjust our internal for loop to change the render depending on the value of this variable:

for(i=0; i<data.length; i++){
openY = (data[i].open-chartInfo.y.min)*stepSize;
closeYOffset = (data[i].open-data[i].close)*stepSize;
highY = (data[i].high-chartInfo.y.min)*stepSize;
lowY =(data[i].low-chartInfo.y.min)*stepSize;
context.beginPath();
currentX = CHART_PADDING +elementWidth*(i+.5);
context.moveTo(currentX,endY-highY);
context.lineTo(currentX,endY-lowY);
if(!isCandle){
context.moveTo(currentX,endY-openY);
context.lineTo(CHART_PADDING +elementWidth*(i+.25),endY-openY);
context.moveTo(currentX,endY-openY+closeYOffset);
context.lineTo(CHART_PADDING +elementWidth*(i+.75),endYopenY+
closeYOffset);
context.stroke();
}else{
context.rect(CHART_PADDING +elementWidth*i ,endY-openY,
elementWi
dth,closeYOffset);
context.stroke();
context.fillStyle = closeYOffset<0? "#C2D985" :"#E3675C" ;
context.fillRect(CHART_PADDING +elementWidth*i ,endY-openY,
elementWidth,closeYOffset);
}
}

There we go. We set the default to be false for our isCandle Boolean variable. If we run our application again, we will find it rendering in the new format. To change that, all we need to do is provide that third parameter as true when calling the addStock function:

This article has been self-contained and really the hub of all the charts if you need to strengthen your chart building skills. I recommend you to revisit some of the earlier sections in this article.

Summary

In this article, we have laid down the foundations to building charts. It has presented a cluster of charts under the microscope, the Cartesian-based graphs. Altogether this graph style is relatively simple; it opens the door to amazingly creative ways of exploring data.

Resources for Article :


Further resources on this subject:


About the Author :


Ben Fhala

Ben Fhala discovered his passion for data visualization six years ago while he was working at Parsons in New York, in their data visualization department, PIIM. He is the owner of the online video training school, 02geek.com, and an Adobe ACP. He enjoys spending most of his time learning and teaching and has a love for visual programming and visualization in general. Ben has had the honor of developing applications for members of the US Congress, Prime Ministers, and Presidents around the world.

He has built many interactive experiences for companies such as Target, AT&T, Crayola, Marriott, Neutrogena, and Nokia. He has technically directed many award-winning projects and has been part of teams that have won three Agency of the Year awards. Among Ben's publications are Packt’s HTML5 Graphing and Data Visualization Cookbook.

Books From Packt


 gnuplot Cookbook
gnuplot Cookbook

 HTML5 Canvas Cookbook
HTML5 Canvas Cookbook

ActionScript Graphing Cookbook
ActionScript Graphing Cookbook

 Scratch 1.4: Beginner’s Guide
Scratch 1.4: Beginner’s Guide

MATLAB Graphics and Data Visualization Cookbook
MATLAB Graphics and Data Visualization Cookbook

 Circos Data Visualization How-to
Circos Data Visualization How-to

 R Graph Cookbook
R Graph Cookbook

 Data Visualization: a successful design process
Data Visualization: a successful design process


No votes yet

Post new comment

CAPTCHA
This question is for testing whether you are a human visitor and to prevent automated spam submissions.
K
M
N
n
h
g
Enter the code without spaces and pay attention to upper/lower case.
Code Download and Errata
Packt Anytime, Anywhere
Register Books
Print Upgrades
eBook Downloads
Video Support
Contact Us
Awards Voting Nominations Previous Winners
Judges Open Source CMS Hall Of Fame CMS Most Promising Open Source Project Open Source E-Commerce Applications Open Source JavaScript Library Open Source Graphics Software
Resources
Open Source CMS Hall Of Fame CMS Most Promising Open Source Project Open Source E-Commerce Applications Open Source JavaScript Library Open Source Graphics Software