使用 Canvas 创建极地面积图
我正在尝试在这里使用画布创建极地区域图:
I am trying to create Polar Area Chart using canvas here :
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();
更新:为了弄清楚这就是我想要实现的:
Update: To clear things out this is what I want to implement :
<>
这个风格搭配这个:
(这是一个简单的饼图)我无法用我当前的实现来实现第二部分(分解切片).
(This is a simple pie chart) I am not able to implement the second part(exploding the slices) with my current implementation.
推荐答案
为此,我将使用对象模型以及在图表和切片之间保持父子关系.这样我就可以只使用图表模型让它渲染所有的孩子,我可以扩展切片对象来做更强大的事情.在这个例子中我不支持文本,但是从这里添加应该很容易.
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 = [];
}
它非常基础,还没有包含我们需要的所有功能.我们可以将函数直接构建到这个对象中,但如果我们使用 Chart 对象的多个实例,在每个实例之间共享内存空间会更明智,因此我们将使用原型模型.
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.
让我们首先创建一个函数来添加一个 Slice
对象:
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
};
在这里,我们看到它创建了一个 Slice
对象(见下文),然后将其添加到切片数组中,更新总和并返回自身,以便我们可以将其链接起来.
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.
Slice
对象(子对象)在这里相当简单,但是通过将其保留为对象而不是文字对象或数组,我们可以稍后使用强大的功能扩展它,如果我们想要不修改父级(您可以让父级在每个切片本身上调用渲染向量来渲染自身,而不是在父级中进行).除此之外,对象在现代浏览器中也能很好地编译:
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
}
就是这样.我们支持偏移值(从中心),如果没有给出默认为 0.
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.
魔法发生在这里:
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
这个百分比用于整个圆(2 x PI):
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.
现在,只需调用:
myChart.render();
全部渲染.
要调整甚至动画偏移量,我们可以创建实用函数,例如在下面的实时代码中,或者直接为数组中的每个切片设置一个偏移量:
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;
用 requestAnimationFrame
将它放在一个循环中,您可以使用各种偏移量对其进行动画处理,您只需要担心一维值(有人关心窦波爆炸吗?).
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.
希望这会有所帮助!
// 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>
相关文章