使用Canvas创建极地区域图表

时间:2014-10-06 05:46:08

标签: javascript canvas graph charts html5-canvas

我正在尝试使用画布创建极地区域图表:

http://jsfiddle.net/wm7pwL2w/2/

代码:

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

更新 为了清除这些,我想要实现:

&LT; http://s15.postimg.org/fnyjfnffv/Coxcomb_chart.jpg&GT;

这种风格:

enter image description here

(这是一个简单的饼图) 我无法使用当前的实现来实现第二部分(爆炸切片)。

2 个答案:

答案 0 :(得分:1)

你不应该改变Radius'myRadius'的值,它必须是常数(简单数学)。

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

检查http://jsfiddle.net/sameersemna/xhpot31v/

答案 1 :(得分:1)

为此,我将使用对象模型以及在图表和切片之间保持父子关系。这样我就可以使用图表模型让它渲染所有的孩子,我可以扩展切片对象来做更强大的东西。我在这个例子中不支持文本,但这应该很容易从这里添加。

好的,首先让我们构建一个父对象 - 图表本身:

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 = [];
}

它非常基本,它还没有包含我们需要的所有功能。我们可以直接在这个对象中构建函数,但是如果我们使用Chart对象的几个实例,那么在每个实例之间共享内存空间会更聪明,所以我们将使用原型模型。

让我们首先创建一个添加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
};

在这里,我们看到它创建了一个Slice对象(见下文),然后将其添加到切片数组,更新总和并返回自身,以便我们可以链接它。

Slice对象(子)在这里相当简单,但是通过将它保持为对象而不是文字对象或数组,我们可以稍后使用强大的功能扩展它,如果我们想要几乎不修改parent(您可以让父对象在每个切片上调用一个渲染向量来渲染自身而不是在父对象中进行渲染)。除此之外,对象在现代浏览器中编译良好:

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
}

就是这样。我们支持偏移值(从中心),如果没有给出,默认为0。

现在我们需要做的就是有一个迭代每个切片的函数,并以偏移,角度,颜色等方式将它们渲染到画布。

魔术发生在这里:

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
  }
};

就是这样。变换的顺序很重要:

  • 首先转换为旋转中心
  • 围绕该中心旋转
  • 基于该旋转+半角(在这种情况下)
  • 的偏移平移

现在我们可以这样创建图表和切片:

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

对于每个添加,数据值累计为总值。然后,该总值变为用于找出每个切片的角度应该有多大的值:

angle = s.data / this.total * pi2; // calc. angle for this slice

这里我们首先得到一个百分比:

s.data / this.total

此百分比用于整圆(2 x PI):

pst * (2 * PI);

因此,无论我们添加多少个切片,我们都会相对于彼此和总数动态调整它们的角度。

现在,只需致电:

myChart.render();

渲染一切。

要调整,甚至为偏移设置动画,我们可以创建实用程序功能,例如在下面的实时代码中,或者直接为数组中的每个切片设置偏移量:

myChart.slices[sliceIndex].offset = value;

将它放在一个带有requestAnimationFrame的循环中,你可以用各种偏移动画它,你需要担心的是一维值(任何人都在关注正弦波爆炸吗?)。

如何定义对象的参数和方法取决于您,但是您应该能够根据需要进行扩展和优化。

希望这有帮助!

// 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>