This is the third 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
To not stray too far from D3.js I used the excellent tutorials on canvas and d3 from Irene Ros & Yannick Assogba which you can find here and here. I managed to get the circle packing working. However, when you zoom it is very jittery.
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 3. See the previous, non-zoomable version here and the next better working zoomable version here. In the next version the d3 components have been downsized and the jitter is almost gone
If you want to see the final result, with everything up and running in canvas look here
xxxxxxxxxx
<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="https://d3js.org/queue.v1.min.js"></script>
<!-- jQuery -->
<script src="https://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);
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 = dataset;
//Dataset to swtich between color of a circle (in the hidden canvas) and the node data
var colToCircle = {};
//Create the circle packing as if it was a normal D3 thing
var dataBinding = dataContainer.selectAll(".node")
.data(nodes)
.enter().append("circle")
.attr("id", function(d,i) { return "nodeCircle_"+i; })
.attr("class", function(d,i) { return d.parent ? d.children ? "node" : "node node--leaf" : "node node--root"; })
.attr("r", function(d) { return d.r; })
.attr("cx", 0)
.attr("cy", 0)
.attr("fill", function(d) { return d.children ? colorCircle(d.depth) : "white"; });
//.attr("transform", function(d) { return "translate(" + (d.x - v[0]) * k + "," + (d.y - v[1]) * k + ")"; });;
//First zoom to get the circles to the right location
zoomToCanvas(root);
//////////////////////////////////////////////////////////////
///////////////// Canvas draw function ///////////////////////
//////////////////////////////////////////////////////////////
//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,canvas.attr("width"),canvas.attr("height"));
chosenContext.fill();
//Select our dummy nodes and draw the data to canvas.
var elements = dataContainer.selectAll(".node");
elements.each(function(d) {
var node = d3.select(this);
//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.attr("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.attr("color", genColor());
colToCircle[node.attr("color")] = node;
}//if
// On the hidden canvas each rectangle gets a unique color.
chosenContext.fillStyle = node.attr("color");
} else {
chosenContext.fillStyle = node.attr("fill");
}//else
//Draw each circle
chosenContext.beginPath();
chosenContext.arc((centerX + +node.attr("cx")), (centerY + +node.attr("cy")), node.attr("r"), 0, 2 * Math.PI, true);
chosenContext.fill();
chosenContext.closePath();
})
}//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) {
chosen = dataContainer.selectAll("#"+node.attr("id"))[0][0].__data__;
//Zoom to the clicked on node
if (focus !== chosen) zoomToCanvas(chosen); else zoomToCanvas(root);
}//if
});
//////////////////////////////////////////////////////////////
///////////////////// Zoom Function //////////////////////////
//////////////////////////////////////////////////////////////
//Zoom into the clicked on circle
//Use the dataContainer to do the transition on
//The canvas will continuously redraw whatever happens to these circles
function zoomToCanvas(d) {
focus = d;
var v = [focus.x, focus.y, focus.r * 2.05],
k = diameter / v[2];
dataContainer.selectAll(".node")
.transition().duration(2000)
.attr("cx", function(d) { return (d.x - v[0]) * k; })
.attr("cy", function(d) { return (d.y - v[1]) * k; })
.attr("r", function(d) { return d.r * k; });
}//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 https://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
//////////////////////////////////////////////////////////////
/////////////////////// Initiate /////////////////////////////
//////////////////////////////////////////////////////////////
//d3.timer(function() { drawCanvas(context) });
function animate() {
drawCanvas(context);
window.requestAnimationFrame(animate);
}
window.requestAnimationFrame(animate);
}//drawAll
</script>
</body>
</html>
Modified http://d3js.org/queue.v1.min.js to a secure url
Modified http://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js to a secure url
https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.6/d3.min.js
https://d3js.org/queue.v1.min.js
https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js