Although many items in our chart will have correlations with earlier charts that we created in Chapter 3, Creating Cartesian-based Graphs, we will start from scratch. Our goal is to create a chart that has bubbles in it—the bubbles enable us to showcase data with three data points (x, y, and the size of the bubble). This type of chart is really ideal when animated as it can showcase changes over time (it could showcase many years in a few seconds).
A great demo of the powers of bubble charts can be seen in a TED presentation by Hans Rosling (http://blog.everythingfla.com/2012/05/hans-rosling-data-vis.html).
We will start up our project with a canvas setup and skip the HTML end. If you have forgotten how to create it please refer to the Graphics with 2D Canvas recipe in Chapter 1, Drawing Shapes in Canvas.
There are three major steps:
Let's list the steps required to create a bubble chart:
var students2001 = [{name:"Ben", math:30, english:60, programing:30}, {name:"Joe", math:40, english:60, programing:40}, {name:"Danny", math:50, english:90, programing:50}, {name:"Mary", math:60, english:60, programing:60}, {name:"Jim", math:80, english:20, programing:80}];
var chartInfo= { y:{min:0, max:100,steps:5,label:"math"}, x:{min:0, max:100,steps:5,label:"programing"}, bubble:{min:0, max:100, minRaduis:3, maxRaduis:20,label:"english"} };
var styling = { outlinePadding:4, barSize:16, font:"12pt Verdana, sans-serif", background:"eeeeee", bar:"cccccc", text:"605050" };
init
, so let's create the init
function:var wid; var hei; function init(){ var can = document.getElementById("bar"); wid = can.width; hei = can.height; var context = can.getContext("2d"); createOutline(context,chartInfo); addDots(context,chartInfo,students2001,["math","programing","english"],"name"); }
function createOutline(context,chartInfo){ var s = styling; var pad = s.outlinePadding; var barSize = s.barSize; context.fillStyle = s.background; context.fillRect(0,0,wid,hei); context.fillStyle = s.bar; context.fillRect(pad,pad,barSize,hei-pad*2); context.font = s.font; context.fillStyle = s.text;
context.save(); context.translate(17, hei/2 ); context.rotate(-Math.PI/2); context.textAlign = "center"; context.fillText(chartInfo.y.label, 0, 0); context.restore(); context.fillStyle = s.bar; context.fillRect(pad+barSize,hei-pad-barSize,wid-pad*2-barSize,barSize); context.font = s.font; context.fillStyle = s.text; context.fillText(chartInfo.x.label,( wid-pad*2-barSize)/2, hei-pad*2); context.translate(pad+barSize,hei-pad-barSize); context.scale(1, -1); //SET UP CONSTANTS - NEVER CHANGE AFTER CREATED styling.CHART_HEIGHT = hei-pad*2-barSize; styling.CHART_WIDTH = wid-pad*2-barSize;
chartInfo
object:var steps = chartInfo.y.steps; var ratio; chartInfo.y.range = chartInfo.y.max-chartInfo.y.min; var scope = chartInfo.y.range; context.strokeStyle = s.text; var fontStyle = s.font.split("pt"); var pointSize = fontStyle[0]/2; fontStyle[0]=pointSize; fontStyle = fontStyle.join("pt"); context.font = fontStyle; // making 1/2 original size of bars for(var i=1; i<=steps; i++){ ratio = i/steps; context.moveTo(0,ratio*styling.CHART_HEIGHT-1); context.lineTo(pad*2,ratio*styling.CHART_HEIGHT-1); context.scale(1,-1); context.fillText(chartInfo.y.min + (scope/steps)*i,0,(ratio*styling.CHART_HEIGHT-3 -pointSize)*-1); context.scale(1,-1); } steps = chartInfo.x.steps; chartInfo.x.range = chartInfo.x.max-chartInfo.x.min; scope = chartInfo.x.max-chartInfo.x.min; context.textAlign = "right"; for(var i=1; i<=steps; i++){ ratio = i/steps; context.moveTo(ratio*styling.CHART_WIDTH-1,0); context.lineTo(ratio*styling.CHART_WIDTH-1,pad*2); context.scale(1,-1); context.fillText(chartInfo.x.min + (scope/steps)*i,ratio*styling.CHART_WIDTH-pad,-pad/2); context.scale(1,-1); } context.stroke(); }
addDots
method. The function addDots
will take in the data with the definition of rules (keys) to be used, contrary to what we did in the earlier recipes.function addDots(context,chartInfo,data,keys,label){ var rangeX = chartInfo.y.range; var _y; var _x; var _xoffset=0; var _yoffset=0; if(chartInfo.bubble){ var range = chartInfo.bubble.max-chartInfo.bubble.min; var radRange = chartInfo.bubble.maxRadius-chartInfo.bubble.minRadius; context.textAlign = "left"; } for(var i=0; i<data.length; i++){ _x = ((data[i][keys[0]] - chartInfo.x.min )/ chartInfo.x.range) * styling.CHART_WIDTH; _y = ((data[i][keys[1]] - chartInfo.y.min )/ chartInfo.y.range) * styling.CHART_HEIGHT; context.fillStyle = "#44ff44"; if(data[i][keys[2]]){ _xoffset = chartInfo.bubble.minRadius + (data[i][keys[2]]-chartInfo.bubble.min)/range *radRange; _yoffset = -3; context.beginPath(); context.arc(_x,_y, _xoffset , 0, Math.PI*2, true); context.closePath(); context.fill(); _xoffset+=styling.outlinePadding; }else{ context.fillRect(_x,_y,10,10); } if(label){ _x+=_xoffset; _y+=_yoffset; context.fillStyle = styling.text; context.save(); context.translate(_x,_y ); context.scale(1,-1); context.fillText("Bluping",0,0); context.restore(); } } }
This block of code, although redone from scratch, bears a lot of resemblance to the Spreading data in a scatter chart recipe in Chapter 3, Creating Cartesian-based Graphs, with modifications to enable the third level of data and the new charting format.
That's it. You should have a running bubble chart. Now when you run the application, you will see that the x
parameter is showcasing the math score, the y
parameter is showcasing the programming score, while the size of our bubble showcases the student's score in English.
Let's start with the createOutline
function. In this method, apart from the regular canvas drawing methods that we grow to love, we introduce a new style of coding where we manipulate the actual canvas to help us define our code in an easier way. The two important key methods here are as follows:
context.save(); context.restore();
We will be leveraging both the methods a few times. The save
method saves the current view of the canvas while the restore
method returns users to the last saved canvas:
context.save(); context.translate(17, hei/2 ); context.rotate(-Math.PI/2); context.textAlign = "center"; context.fillText(chartInfo.y.label, 0, 0); context.restore();
In the first use of this style, we are using it to draw our text by rotating it to the right. The translate
method moves the 0, 0
coordinates of the canvas while the rotate
method rotates the text using radians.
After drawing the external bars, it's time for us to use this new capability to our advantage. Most charts rely on a y coordinate that grows upwards, but this canvas has the y values growing from the top to the bottom of the canvas area. We can flip this relationship by adding some code before we loop through to add the range values.
context.translate(pad+barSize,hei-pad-barSize); context.scale(1, -1);
In the preceding lines, we are first moving the 0,0
coordinates of our canvas to be exactly at the bottom-right range of our chart, and then we are flipping our canvas by switching the scale value. Note that from now on if we try to add text to the canvas, it will be upside down. Keep that in mind as we are now drawing in a canvas that is flipped upside down.
One thing to note in our first loop when we try to type in new text is that when we want to add text, we first undo our scale and then return back our canvas for it to be flipped:
context.scale(1,-1); context.fillText(chartInfo.y.min + (scope/steps)*i,0,(ratio*styling.CHART_HEIGHT-3 -pointSize)*-1); context.scale(1,-1);
Note that we are multiplying our y coordinate by *-1
. We are doing this because we actually want the value of our y coordinate to be negative as we have just flipped the screen.
The work around the x bar text is very similar; notice the main differences related to finding the x and y value calculations.
It's time to dig into the addDots
function. The function will again look familiar if you've been following Chapter 3, Creating Cartesian-based Graphs, but this time we are working with a modified canvas.
We start with a few helper variables:
var rangeX = chartInfo.y.range; var _y; var _x; var _xoffset=0; var _yoffset=0;
We are adding the bubble effect dynamically, which means that this method can work even if there are only two points of information and not three. We continue by testing to see if our data object contains the bubble information:
if(chartInfo.bubble){ var range = chartInfo.bubble.max-chartInfo.bubble.min; var radRange = chartInfo.bubble.maxRaduis-chartInfo.bubble.minRaduis; context.textAlign = "left"; }
If so, we add a few more variables and align our text to the left as we are going to use it in this example.
It's time for us to look through our data object and propagate the data on the chart.
for(var i=0; i<data.length; i++){ _x = ((data[i][keys[0]] - chartInfo.x.min )/ chartInfo.x.range) * styling.CHART_WIDTH; _y = ((data[i][keys[1]] - chartInfo.y.min )/ chartInfo.y.range) * styling.CHART_HEIGHT; context.fillStyle = "#44ff44";
For each loop, we recalculate the _x
and _y
coordinates based on the current values.
If we have a third element, we are ready to develop a bubble. If we do not have it, we need to create a simple dot.
if(data[i][keys[2]]){ _xoffset = chartInfo.bubble.minRaduis + (data[i][keys[2]]-chartInfo.bubble.min)/range *radRange; _yoffset = -3; context.beginPath(); context.arc(_x,_y, _xoffset , 0, Math.PI*2, true); context.closePath(); context.fill(); _xoffset+=styling.outlinePadding; }else{ context.fillRect(_x,_y,10,10); }
At this stage, we should have an active bubble/dot method. All that is left is for us to integrate our overlay copy.
Before we add a label, let's take a peek at the function signature:
function addDots(context,chartInfo,data,keys,label){}
The context
and chartInfo
parameters are already a standard in our samples. The idea of keys was to enable us to switch what data will be tested dynamically. The keys' values are the array positions 0
and 1
that are correlated to the x and y coordinates, and position 2
is used for bubbles, as we've seen earlier. The label
parameter enables us to send in a key value for the label. In this way, if the label is there we will add a label and if it is not there we will not.
if(label){ _x+=_xoffset; _y+=_yoffset; context.fillStyle = styling.text; context.save(); context.translate(_x,_y ); context.scale(1,-1); context.fillText(data[i][label],0,0); context.restore(); }
Then we add the preceding if
statement. If our label is set, we position the style and create the text of the label.
3.147.73.35