Creating a polarity diagram using Canvas
I am trying to create a polar regions table using a canvas here:
http://jsfiddle.net/wm7pwL2w/2/
Code:
var myColor = ["#ff0", "#00f", "#002", "#003", "#004"];
var myData = [10, 30, 20, 60, 40];
var myRadius = [120, 80, 40, 70, 40];
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[i], lastend, lastend + (Math.PI * 2 * (myData[i] / myTotal)), false);
console.log(myRadius[i]);
ctx.lineTo(200, 150);
ctx.fill();
lastend += Math.PI * 2 * (myData[i] / myTotal);
}
}
plotData();
Update: To clarify this, I want to implement:
< >
This style with this:
(This is a simple pie chart) I can't seem to implement the second part (hacking slices) with my current implementation.
source to share
For this I would use the object model and also maintain parent-child relationships between the chart and slicers. This way I can only work with the chart model in which it displays all the children and I could extend the slice object to do more powerful stuff. I didn't support the text in this example, but it should be easy to add from here.
Ok, first let's create a parent object - the graph 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 simple and it doesn't yet contain all the features we need. We could build a function directly on this object, but if we are using multiple instances of the Chart object, it would make more sense to share this memory space between each instance, so we'll use the prototype model instead.
Lets first create a function to add an object Slice
:
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 can see that it creates an object Slice
(see below), then adds it to the slices array, updates the total and returns itself so that we can link it.
The object Slice
(child) is pretty simple here, but by storing it as an object, not a literal object or an array, we can extend it later with powerful functionality if we want with a minor modification to the parent (you can have a parent call to the render vector for each segment to do it myself, rather than do it in the parent). Also, objects compile well in 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
}
What about it. We support an offset value (from the center), which defaults to 0 if not specified.
All we need to do is have a function that iterates over each chunk and renders them on the canvas with offset, angle, color, etc.
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 all. The order of transformations is important:
- Move to center of rotation first
- Rotate around this center
- Offset translation based on this rotation + half-triangle (in this case)
We can now create charts and slicers as follows:
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 addition, the data value is accumulated to the total value. This overall value then becomes the value used to determine how large the angle should be for each slice:
angle = s.data / this.total * pi2; // calc. angle for this slice
Here we first get the percentage of the total:
s.data / this.total
this percentage is used for a full circle (2 x PI):
pst * (2 * PI);
This way, no matter how many slots we add, we will dynamically adjust their angles relative to each other and the total.
Now just call:
myChart.render();
to do it all.
To tweak and even animate the offsets, we can create utility functions, for example in the live code below, or simply set the offset directly for each piece of the array:
myChart.slices[sliceIndex].offset = value;
Put it in a loop with requestAnimationFrame
and you can animate it with different offsets, and all you have to worry about is the one-dimensional values (does anyone care about sine wave explosions?).
How you define parameters and methods for objects is up to you, but 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>
source to share
You shouldn't change the "myRadius" radius values, it should 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();
source to share