I am trying to create Polar Area Chart using canvas here :
http://jsfiddle.net/wm7pwL2w/2/
Code:
var myColor = [\"#ff0\",
For this I would use an object model as well as keeping a parent-child relationship between the chart and the slices. This way I can work with just the chart model having it render all the children, and I could expand the slice object to do more powerful stuff. I did not support text in this example but this should be easy to add from here.
Ok, first lets built a parent object - the chart itself:
function Chart(x, y) {
this.x = x; // expose these values so we can alter them from outside
this.y = y; // as well as within the prototypes (see below)
this.total = 0;
this.slices = [];
}
Its pretty basic and it does not yet contain all the functionality we need. We could build the function directly into this object but in case we use several instances of the Chart object it would be smarter to share that memory-space between each instance, so we're gonna use the prototype model instead.
Lets first make a function to add a Slice
object:
Chart.prototype.addSlice = function(data, radius, color, offset) {
var slice = new Slice(data, radius, color, offset);
this.slices.push(slice); // add slice to internal array
this.total += data; // update total value
return this; // just to make call chain-able
};
Here we see it creates a Slice
object (see below), then adds it to the slice array, updates the total sum and returns itself so we can chain it.
The Slice
object (child) is fairly simple here, but by keeping it as an object rather than a literal object or an array we can expand it later with powerful functionality if we want with little to no modification of parent (you could have parent call a render vector on each slice itself to render itself instead of doing it in parent). Besides from that, objects compile well within modern browsers:
function Slice(data, radius, color, offset) {
this.data = data; // self-expl.
this.radius = radius
this.color = color;
this.offset = offset || 0; // default to 0 if non is given
}
That's about it. We support an offset value (from center) which defaults to 0 if not given.
All we need to do now is to have a function that iterates over each slice and render them to canvas at offset, angle, color and so forth.
The magic happens here:
Chart.prototype.render = function() {
var i = 0, s, // iterator, slice object
angle, angleHalf, // angle based on data and total
currentAngle = 0, // current angle for render purpose
pi2 = 2 * Math.PI; // cache PI*2
// iterate over each slice in the slice array (see addSlice())
for(; s = this.slices[i++];) {
angle = s.data / this.total * pi2; // calc. angle for this slice
angleHalf = angle * .5; // calc. half angle for center
ctx.translate(this.x, this.y); // move to pivot point
ctx.rotate(currentAngle); // rotate to accumulated angle
// The "explosion" happens here...
ctx.translate(s.offset * Math.cos(angleHalf), // translate so slice
s.offset * Math.sin(angleHalf)); // explodes using center
ctx.beginPath(); // draw slice (outer stroke not shown here)
ctx.moveTo(0, 0);
ctx.arc(0, 0, s.radius, 0, angle);
ctx.fillStyle = s.color;
ctx.fill();
ctx.setTransform(1, 0, 0, 1, 0, 0);// reset all transforms
currentAngle += angle; // accumulate angle of slice
}
};
That´s it. The order of the transforms is important:
Now we can create the charts and the slices this way:
var myChart = new Chart(canvas.width * .5, canvas.height * .5);
// add some slices to the chart
myChart.addSlice(10, 120, '#ff0')
.addSlice(30, 80, '#00f')
.addSlice(20, 40, '#002')
.addSlice(60, 70, '#003')
.addSlice(40, 40, '#004');
For each add the data value is accumulated to a total value. This total value then becomes the value used to find how large the angle should be for each slice:
angle = s.data / this.total * pi2; // calc. angle for this slice
Here we first get a percentage of total:
s.data / this.total
this percentage is used of the full circle (2 x PI):
pst * (2 * PI);
So no matter how many slices we add we will dynamically adjust their angles relative to each other and the total.
Now, simply call:
myChart.render();
to render it all.
To adjust, and even animate the offsets, we can create utility functions such as in the live code below, or simply set an offset directly for each slice in the array:
myChart.slices[sliceIndex].offset = value;
Put it in a loop with requestAnimationFrame
and you can animate it with various offsets and all you need to worry about is 1-dimensional values (anyone care for a sinus wave explosion?).
How you define the parameters and methods for the objects is up to you, but with this you should be able to expand and refine as needed.
Hope this helps!
// Main object (parent of slices)
function Chart(x, y) {
this.x = x;
this.y = y;
this.total = 0;
this.slices = [];
}
// shared function to all chart instances to add a slice to itself
Chart.prototype.addSlice = function(data, radius, color, offset) {
var slice = new Slice(data, radius, color, offset);
this.slices.push(slice);
this.total += data;
return this;
};
// shared function to all chart instances to render itself
Chart.prototype.render = function() {
var i = 0, s,
angle, angleHalf,
currentAngle = 0,
pi2 = 2 * Math.PI;
ctx.lineWidth = 7;
ctx.strokeStyle = '#79f';
for(; s = this.slices[i++];) {
angle = s.data / this.total * pi2;
angleHalf = angle * .5;
ctx.translate(this.x, this.y);
ctx.rotate(currentAngle);
ctx.translate(s.offset * Math.cos(angleHalf), s.offset * Math.sin(angleHalf));
ctx.beginPath();
ctx.moveTo(0, 0);
ctx.arc(0, 0, s.radius, 0, angle);
ctx.fillStyle = s.color;
ctx.fill();
ctx.beginPath();
ctx.arc(0, 0, s.radius, 0, angle);
ctx.stroke();
ctx.setTransform(1, 0, 0, 1, 0, 0);
currentAngle += angle;
}
return this;
};
// utility method to add offset to all child-slices.
// offset can be added to each individual slice as well
Chart.prototype.addOffsetToAll = function(offset) {
for(var i = 0, s; s = this.slices[i++];) s.offset += offset;
return this;
};
// Child object, slice to be added to parent internally
function Slice(data, radius, color, offset) {
this.data = data;
this.radius = radius
this.color = color;
this.offset = offset || 0;
}
// MAIN CODE HERE
var canvas = document.getElementById('canvas'),
ctx = canvas.getContext('2d'),
// create a chart instance with center at the center of canvas
myChart = new Chart(canvas.width * .5, canvas.height * .5),
offset = 0; // for adjusting offset later
// add some slices to the chart
myChart.addSlice(10, 120, '#ff0')
.addSlice(30, 80, '#00f')
.addSlice(20, 40, '#f72')
.addSlice(60, 70, '#003')
.addSlice(25, 80, '#555')
.addSlice(40, 40, '#052');
// render function to clear canvas, update offsets and render again
function render() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
myChart.addOffsetToAll(offset)
.render();
}
// initial render
render();
// handle buttons
document.getElementById('oPlus').addEventListener('click', function() {
offset = 2;
render();
}, false);
document.getElementById('oMinus').addEventListener('click', function() {
offset = -2;
render();
}, false);
// this is how you can adjust each individual slice
document.getElementById('oRnd').addEventListener('click', function() {
for(var i = 0, s; s = myChart.slices[i++];) s.offset = 15 * Math.random();
offset = 0;
render();
}, false);
#canvas {display:inline-block}
<canvas id="canvas" width=360 height=180></canvas>
<button id="oPlus">Offset+</button>
<button id="oMinus">Offset-</button>
<button id="oRnd">Random</button>
You are not supposed to change the values of the Radius 'myRadius', it must be constant (simple math).
var myColor = ["#ff0","#00f","#002","#003","#004"];
var myData = [10,30,20,60,40];
var myRadius = 120;//[120,80,40,70,40]; <=====Changed here
function getTotal(){
var myTotal = 0;
for (var j = 0; j < myData.length; j++) {
myTotal += (typeof myData[j] == 'number') ? myData[j] : 0;
}
return myTotal;
}
function plotData() {
var canvas;
var ctx;
var lastend = 0;
var myTotal = getTotal();
canvas = document.getElementById("canvas");
ctx = canvas.getContext("2d");
ctx.clearRect(0, 0, canvas.width, canvas.height);
for (var i = 0; i < myData.length; i++) {
ctx.fillStyle = myColor[i];
ctx.beginPath();
ctx.moveTo(200,150);
ctx.arc(200,150,myRadius,lastend,lastend+(Math.PI*2*(myData[i]/myTotal)),false);//<=====And Changed here
console.log(myRadius[i]);
ctx.lineTo(200,150);
ctx.fill();
lastend += Math.PI*2*(myData[i]/myTotal);
}
}
plotData();
Check http://jsfiddle.net/sameersemna/xhpot31v/