The line charts are based on scatter charts. Contrary to scatter charts that show isolated correlation between two variables, the line chart tells a story in many ways; we can go back to our previous recipe, Spreading data in a scatter chart, and draw a line between the dots to create the connection. This type of chart is usually used in website statistics, tracking things over time, speed, age, and so on. Let's jump right into it and see it in action.
As usual get your HTML wrapper ready. In this recipe we actually are going to base our changes on the previous recipe, Spreading data in a scatter chart.
In our case study for this example, we will create a chart that shows how many new members joined my site, 02Geek.com, in 2011 and 2010. I've gathered the information month by month and gathered it into two arrays:
var a2011 = [38,65,85,111,131,160,187,180,205,146,64,212]; var a2010 = [212,146,205,180,187,131,291,42,98,61,74,69];
Both arrays have a length of 12 (for 12 months of the year). I've deliberately created a new data source that is totally different than the one we used earlier. I did that to render our old map useless in this example. I've done it to add some extra value into this recipe (a good lesson in manipulating data to fit even when it doesn't, instead of rebuilding things).
var chartInfo= { y:{min:0, max:300, steps:5,label:"users"}, x:{min:1, max:12, steps:11,label:"months"} };
For our chart information we are using the same object type and for the y
position we will assume a range from 0 to 300 (as I haven't had the privilege of having more than 300 members in one month, yet I'm hopeful). For our x
position we are setting it to output values from 1 through 12 (for the 12 months of the year).
OK, it's time to build it!
As always, our init
function is going to look very similar to the one we used in the previous recipe. Let's take a look at the modifications that have taken place in this recipe:
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;
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"); }
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(); }
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!
I've added a new method, rect
, to our tool set for drawing; until now we worked with the drawRect
method. I've used the rect
method as it just adds the outlines without drawing anything, so I can perform the stroke or fill function separately and create an outline instead of a fill.
The fillChart
function did not change at all, cool right? And I've renamed the function createDots
to addLine
as it seemed more appropriate for our sample. A few additions have been made into that function and a new function, formatData
, is being used to format the data to fit what the addLine
function is expecting.
As you probably noticed we made a few small changes to our code to accommodate the needs of this chart style. Let's dive in and see them in action:
addLine(context,formatData(a2011,"/2011","#B1DDF3"),"#B1DDF3")
The biggest change we can visibly see in the way we are calling the addLine
function is that we are calling the formatData
function to render a data source for us that will be acceptable by the addLine
function. You might be thinking right now, why didn't I just create the data the way it needs to work for the addLine
function. When we move to the real, live data sources many times we will find data sources that just don't match our original work. That doesn't mean we need to change our work, often a better solution is to create a converter method that will modify the data and rebuild it to match our application structure so it is in the format we expect.
A reminder from our previous recipe: this is what our data source looked like:
var data = [{label:"David", math:50, english:80, art:92 style:"rgba(241, 178, 225, 0.5)"}, ... ];
While currently our array is flat, we need to change that to work with our current system; it expects two properties that will define the x
and y
values:
var chartInfo= { y:{min:0, max:300, steps:5,label:"users"}, x:{min:1, max:12, steps:11,label:"months"}
In other words the object we need to create needs to look something like the following:
var data = [{label: "01/2011", users:200, months:1, style:"#ff0000"} ... ];
So let's make the function that will create this data format:
function formatData(data , labelCopy , style){ newData = []; for(var i=0; i<data.length;i++){ newData.push({ label:(i+1)+labelCopy, users:data[i], months:i+1, style:style }); } return newData; }
Notice how we loop through our old array and restructure it to fit our expected data format using both the array data and external data that was sent to our formatData
functions. Even though we aren't using all of the information in this recipe, I wanted to keep it up-to-date with all the basics in case you want to expand this sample. We will do so in the future.
This is one of the most powerful tricks in programing in the tool set. I've met many developers, who change their code instead of changing their data to fit their required application structure. There is always a way to modify data to make it more easily consumed by your application and it's much easier to dynamically modify data than it is to change your architecture.
I didn't change anything in the core logic of this addLine
function, but instead just added drawing lines from one dot to the next one.
In case you're not familiar with the ternary operation, it is a shorthanded if
statement:
condition ? ifStatement: elseStatement;
By the way, if you are worried about efficiency, you might want to change the for
loop by extracting the first instance out of the loop as that's the only occurrence where our ternary operator would trigger the else value.
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.
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.
To create the fill shapes and for the sake of keeping our code simple, we will create an if...else
statement within our code, and if the user sends a new fourth parameter, we will render it in the fill mode (the changes are highlighted in the following code snippet):
function addLine(context,data,style,isFill){ var yDataLabel = chartInfo.y.label; var xDataLabel = chartInfo.x.label; var yDataRange = chartInfo.y.max-chartInfo.y.min; var xDataRange = chartInfo.x.max-chartInfo.x.min; var chartHeight = hei- CHART_PADDING*2; var chartWidth = wid- CHART_PADDING*2; var yPos; var xPos; context.strokeStyle = style; context.beginPath(); context.lineWidth = 3; if(!isFill){ for(var i=0; i<data.length;i++){ xPos = CHART_PADDING + (data[i][xDataLabel]-chartInfo.x.min)/xDataRange * chartWidth; yPos = (hei - CHART_PADDING) -(data[i][yDataLabel]-chartInfo.y.min)/yDataRange * chartHeight; context.fillStyle = data[i].style; context.fillRect(xPos-4 ,yPos-4,8,8); i==0? context.moveTo(xPos,yPos):context.lineTo(xPos,yPos); } if(style)context.stroke(); }else{ context.fillStyle = style; context.globalAlpha = .6; context.moveTo(CHART_PADDING,hei - CHART_PADDING) for(var i=0; i<data.length;i++){ xPos = CHART_PADDING + (data[i][xDataLabel]-chartInfo.x.min)/xDataRange * chartWidth; yPos = (hei - CHART_PADDING) -(data[i][yDataLabel]-chartInfo.y.min)/yDataRange * chartHeight; context.lineTo(xPos,yPos); } context.lineTo( CHART_PADDING + chartWidth, CHART_PADDING+chartHeight); context.closePath(); context.fill(); context.globalAlpha = 1; } }
The differences are not large in the new code. We just removed some of the code and added a few new lines to create a complete shape. I superimposed the Alpha value as well. A smarter way would be to revisit the values sent and put into them an Alpha value as needed; but that is left for you to improve. Now our addLine
function can add three types of visualization and we can add multiple types at the same time to our chart (check out the source code to see this in action).
52.14.8.34