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:

< http://s15.postimg.org/fnyjfnffv/Coxcomb_chart.jpg>

This style with this:

enter image description here

(This is a simple pie chart) I can't seem to implement the second part (hacking slices) with my current implementation.

+2


source to share


2 answers


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>
      

Run codeHide result


+1


source


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();

      



Check http://jsfiddle.net/sameersemna/xhpot31v/

+1


source







All Articles