This is the fourth step of my first attempt to learn canvas. I want to improve a piece a made a few weeks ago about the division of occupations. The D3.js version has so many DOM elements due to all the small bar charts that it is very slow. Therefore, I hope that a canvas version might improve things
Thanks to a version of the circle packing that does away with the D3.js data binding made by Stephan Smola I was able to make a few adjustments to make the zooming more linear from point A to B. Next step, add the bar charts…
I wrote a more extensive tutorial around what I learned while doing this project in my blog Learnings from a D3.js addict on starting with Canvas in which this can be seen as step 4. See the previous, more jittery zoomable version here in which the code is still more base on d3.js
And if you want to see the final result, with everything up and running in canvas look here
<!DOCTYPE html>
<head>
<meta charset="utf-8">
<!-- D3.js -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.6/d3.min.js" charset="utf-8"></script>
<script src="//d3js.org/queue.v1.min.js"></script>
<!-- stats -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/stats.js/r14/Stats.js"></script>
<!-- jQuery -->
<script src="//ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>
<!-- Stylesheet -->
<style>
body { text-align: center; }
</style>
</head>
<body>
<div id="chart"></div>
<script>
queue()
.defer(d3.json, "occupation.json")
.await(drawAll);
function drawAll(error, dataset) {
//////////////////////////////////////////////////////////////
////////////////// Create Set-up variables //////////////////
//////////////////////////////////////////////////////////////
var width = Math.max($("#chart").width(),350) - 20,
height = (window.innerWidth < 768 ? width : window.innerHeight - 20);
var mobileSize = (window.innerWidth < 768 ? true : false);
var centerX = width/2,
centerY = height/2;
//////////////////////////////////////////////////////////////
/////////////////////// Create SVG ///////////////////////
//////////////////////////////////////////////////////////////
//Create the visible canvas and context
var canvas = d3.select("#chart").append("canvas")
.attr("id", "canvas")
.attr("width", width)
.attr("height", height);
var context = canvas.node().getContext("2d");
context.clearRect(0, 0, width, height);
//Create a hidden canvas in which each circle will have a different color
//We can use this to capture the clicked on circle
var hiddenCanvas = d3.select("#chart").append("canvas")
.attr("id", "hiddenCanvas")
.attr("width", width)
.attr("height", height)
.style("display","none");
var hiddenContext = hiddenCanvas.node().getContext("2d");
hiddenContext.clearRect(0, 0, width, height);
//Create a custom element, that will not be attached to the DOM, to which we can bind the data
var detachedContainer = document.createElement("custom");
var dataContainer = d3.select(detachedContainer);
//////////////////////////////////////////////////////////////
/////////////////////// Create Scales ///////////////////////
//////////////////////////////////////////////////////////////
var colorCircle = d3.scale.ordinal()
.domain([0,1,2,3])
.range(['#bfbfbf','#838383','#4c4c4c','#1c1c1c']);
var diameter = Math.min(width*0.9, height*0.9),
radius = diameter / 2;
var zoomInfo = {
centerX: width / 2,
centerY: height / 2,
scale: 1
};
//Dataset to swtich between color of a circle (in the hidden canvas) and the node data
var colToCircle = {};
var pack = d3.layout.pack()
.padding(1)
.size([diameter, diameter])
.value(function(d) { return d.size; })
.sort(function(d) { return d.ID; });
//////////////////////////////////////////////////////////////
////////////////// Create Circle Packing /////////////////////
//////////////////////////////////////////////////////////////
var nodes = pack.nodes(dataset),
root = dataset,
focus = root;
//////////////////////////////////////////////////////////////
///////////////// Canvas draw function ///////////////////////
//////////////////////////////////////////////////////////////
var cWidth = canvas.attr("width");
var cHeight = canvas.attr("height");
var nodeCount = nodes.length;
//The draw function of the canvas that gets called on each frame
function drawCanvas(chosenContext, hidden) {
//Clear canvas
chosenContext.fillStyle = "#fff";
chosenContext.rect(0,0,cWidth,cHeight);
chosenContext.fill();
//Select our dummy nodes and draw the data to canvas.
var node = null;
// It's slightly faster than nodes.forEach()
for (var i = 0; i < nodeCount; i++) {
node = nodes[i];
//If the hidden canvas was send into this function and it does not yet have a color, generate a unique one
if(hidden) {
if(node.color == null) {
// If we have never drawn the node to the hidden canvas get a new color for it and put it in the dictionary.
node.color = genColor();
colToCircle[node.color] = node;
}//if
// On the hidden canvas each rectangle gets a unique color.
chosenContext.fillStyle = node.color;
} else {
chosenContext.fillStyle = node.children ? colorCircle(node.depth) : "white";
}//else
//Draw each circle
chosenContext.beginPath();
chosenContext.arc(((node.x - zoomInfo.centerX) * zoomInfo.scale) + centerX,
((node.y - zoomInfo.centerY) * zoomInfo.scale) + centerY,
node.r * zoomInfo.scale, 0, 2 * Math.PI, true);
chosenContext.fill();
}//for i
}//function drawCanvas
//////////////////////////////////////////////////////////////
/////////////////// Click functionality //////////////////////
//////////////////////////////////////////////////////////////
// Listen for clicks on the main canvas
document.getElementById("canvas").addEventListener("click", function(e){
// We actually only need to draw the hidden canvas when there is an interaction.
// This sketch can draw it on each loop, but that is only for demonstration.
drawCanvas(hiddenContext, true);
//Figure out where the mouse click occurred.
var mouseX = e.layerX;
var mouseY = e.layerY;
// Get the corresponding pixel color on the hidden canvas and look up the node in our map.
// This will return that pixel's color
var col = hiddenContext.getImageData(mouseX, mouseY, 1, 1).data;
//Our map uses these rgb strings as keys to nodes.
var colString = "rgb(" + col[0] + "," + col[1] + ","+ col[2] + ")";
var node = colToCircle[colString];
if(node) {
if (focus !== node) zoomToCanvas(node); else zoomToCanvas(root);
}//if
});
//////////////////////////////////////////////////////////////
///////////////////// Zoom Function //////////////////////////
//////////////////////////////////////////////////////////////
//Based on the generous help by Stephan Smola
////bl.ocks.org/smoli/d7e4f9199c15d71258b5
var ease = d3.ease("cubic-in-out"),
duration = 2000,
timeElapsed = 0,
interpolator = null,
vOld = [focus.x, focus.y, focus.r * 2.05];
//Create the interpolation function between current view and the clicked on node
function zoomToCanvas(focusNode) {
focus = focusNode;
var v = [focus.x, focus.y, focus.r * 2.05]; //The center and width of the new "viewport"
interpolator = d3.interpolateZoom(vOld, v); //Create interpolation between current and new "viewport"
duration = interpolator.duration; //Interpolation gives back a suggested duration
timeElapsed = 0; //Set the time elapsed for the interpolateZoom function to 0
vOld = v; //Save the "viewport" of the next state as the next "old" state
}//function zoomToCanvas
//Perform the interpolation and continuously change the zoomInfo while the "transition" occurs
function interpolateZoom(dt) {
if (interpolator) {
timeElapsed += dt;
var t = ease(timeElapsed / duration);
zoomInfo.centerX = interpolator(t)[0];
zoomInfo.centerY = interpolator(t)[1];
zoomInfo.scale = diameter / interpolator(t)[2];
if (timeElapsed >= duration) interpolator = null;
}//if
}//function zoomToCanvas
//////////////////////////////////////////////////////////////
//////////////////// Other Functions /////////////////////////
//////////////////////////////////////////////////////////////
//Generates the next color in the sequence, going from 0,0,0 to 255,255,255.
//From: https://bocoup.com/weblog/2d-picking-in-canvas
var nextCol = 1;
function genColor(){
var ret = [];
// via //stackoverflow.com/a/15804183
if(nextCol < 16777215){
ret.push(nextCol & 0xff); // R
ret.push((nextCol & 0xff00) >> 8); // G
ret.push((nextCol & 0xff0000) >> 16); // B
nextCol += 100; // This is exagerated for this example and would ordinarily be 1.
}
var col = "rgb(" + ret.join(',') + ")";
return col;
}//function genColor
//////////////////////////////////////////////////////////////
/////////////////////// FPS Stats box ////////////////////////
//////////////////////////////////////////////////////////////
var stats = new Stats();
stats.setMode(0); // 0: fps, 1: ms, 2: mb
// align top-left
stats.domElement.style.position = 'absolute';
stats.domElement.style.left = '0px';
stats.domElement.style.top = '0px';
document.body.appendChild( stats.domElement );
//////////////////////////////////////////////////////////////
/////////////////////// Initiate /////////////////////////////
//////////////////////////////////////////////////////////////
//First zoom to get the circles to the right location
zoomToCanvas(root);
var dt = 0;
d3.timer(function(elapsed) {
stats.begin();
interpolateZoom(elapsed - dt);
dt = elapsed;
drawCanvas(context);
stats.end();
});
}//drawAll
</script>
</body>
</html>