继续改进“圳品”信息系统,我们上次绘制的饼图还需要做两点改进:
一是要加上标题。
一是饼图的文字说明的颜色与对应的饼图区域颜色一致,这样就更加直观了。
第一点改进比较简单,可以分两步实现。
第一步,在创建Pie对象时再增加一个名为title的属性,如下:
var typeCountPie = new Pie({
ctx: ctx,
x: tcCanvas.width / 2,
y: tcCanvas.height / 2,
r: 150,
data:aTypeCount,
title:'“圳品”产品类别分析' //增加饼图的标题
});
第二步,修改Pie的
drawText:function(){
var fontSize = 20;//标题文字大小
//如果标题属性已赋值且长度大于0,则输出标题文件
if (typeof(this.title) != "undefined" && 0 < this.title.length)
{
this.ctx.font = "bolder "+ fontSize + "px 微软雅黑";
this.ctx.fillStyle = 'black';
this.ctx.fillText(this.title, (tcCanvas.width - this.title.length*fontSize)/2, 25);
}
第二点似乎更简单,因为要让饼图的文字说明的颜色与对应的饼图区域颜色一致,只需要修改一行代码,即将
this.slice.forEach(function(obj){
this.ctx.fillStyle = 'black';
改为
this.slice.forEach(function(obj){
this.ctx.fillStyle = obj.color;// 'black';
但是如果饼图的文字说明比较长的话,文字说明就会输出到饼图里,其中输出到饼图里的文字就看不出来了。比如下图:
为了解决这个问题,我们需要换行输出。
正规的方法利用canvas的measureText() 方法。
我们先定义一个变量textWidth来指定饼图的文字说明的显示宽度:
var textWidth= 100;
然后再利用canvas的measureText() 方法来测算饼图的文字说明的输出长度,如果输出长度大于textWidth指定的宽度,那么我们就从文字说明的截取字符串输出,然后换行,再继续测算文字说明余下的字符的长度并输出。
对应的代码如下:
var i, lastSubStrIndex = lineWidth=0;
for (i=0;i< obj.text.length; i++)
{
taDbg.value += '\nobj.text['+i +']=' + obj.text[i];
lineWidth += ctx.measureText(obj.text[i]).width;
if (lineWidth > textWidth)
{
this.ctx.fillText(obj.text.substring(lastSubStrIndex,i), x2,y2);//绘制截取部分
y2 += fontSize;
lineWidth = 0;
lastSubStrIndex = i;
}
if(i==obj.text.length-1)
{ //绘制剩余部分
this.ctx.fillText(obj.text.substring(lastSubStrIndex,i+1),x2,y2);
break;
}
}//for
输出的效果如下:
当然还有一个取巧的方法,因为我们可以自己指定饼图的文字说明文字大小,那么我们就可以利用文字大小来测算,在textWidth来指定饼图的文字说明的显示宽度中可以显示的字符数charNumPerLine,然后截取charNumPerLine个字符输出,换行,再截取,输出……实现的代码如下:
var charNumPerLine = Math.ceil(textWidth / fontSize);
var t = obj.text, i = 0;
while (t.length > charNumPerLine)
{
this.ctx.fillText(t.substr(i,charNumPerLine), x2,y2);//绘制截取部分
i += charNumPerLine;
t = t.substring(i);
y2 += fontSize;
}
if (i <= obj.text.length)
{
this.ctx.fillText(t, x2,y2);//绘制剩余部分
}
效果如下:
哪种方法更好,就是见仁见智了。
最后放上完整的代码:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="Author" content="PurpleEndurer">
<meta name="Keywords" content="">
<meta name="Description" content="">
<title>“圳品”信息系统</title>
</head>
<body>
<script>
const aType = [
"水果",//0
"粮食",//1,谷类、豆类、薯类
"食用油",//2
"饮用水",//3
"畜",//4
"禽",//5
"渔",//6
"其它"//7
];
//结构:
//类别,数量,颜色
var aTypeCount = [
[0, 3, "purple"],
[1, 5, "gray"],
[2,3, 'yellow'],
[3,4,"green"],
[4,8,"red"]
];
</script>
<canvas id="typeCountCanvas" ></canvas>
<div>
<textarea border="1" id="taDebug" cols="80" rows="15">debug:</textarea>
</div>
<script>
var tcCanvas = document.getElementById("typeCountCanvas");
//var w = window.innerWidth;
//var h = window.innerHeight;
tcCanvas.height = 480;
tcCanvas.width = 640;
var ctx = tcCanvas.getContext('2d');
var taDbg = document.getElementById("taDebug");
function Pie(obj)
{
for(var key in obj)
{
this[key] = obj[key];
}
this.init();
this.render(this.slice);
this.drawText();
}
Pie.prototype = {
init: function () {
this.start = 0;
//1、累计数据求合
var sum = 0;
this.data.forEach(function (v) {
sum += v[1];
});
//2、计算每一个数据所占的比重
this.slice = this.data.map(function (v) {
var obj = {};
obj.number = v[1];
obj.ratio = v[1] / sum;//每个数据占据的比重
obj.radian = 2 * Math.PI * v[1] / sum;//该扇形所占据的弧度
obj.start = this.start;
obj.end = this.start + obj.radian;
this.start = obj.end;
obj.color = v[2];
obj.text = aType[v[0]] + "类产品"+ v[1] + "个,占比" + Math.ceil(obj.ratio*100) + "%";
return obj;
},this);
},
//渲染页面(画饼图)
render: function (obj) {
//3、计算每一个扇形的起始弧度和结束弧度
this.slice.forEach(function (v, i) {
var obj = {};
//第一个扇形的起始弧度:start 结束:start+第一个扇形占据的弧度差
obj.start = this.start;
obj.end = this.start + v.radian;
this.start += v.radian;
//绘制扇形
this.ctx.beginPath();
this.ctx.moveTo(this.x, this.y);
this.ctx.arc(this.x, this.y, this.r, obj.start, obj.end);
this.ctx.fillStyle = v.color;
this.ctx.fill();
}, this);
},
//输出文字说明
drawText:function(){
var fontSize = 20;//标题文字大小
//如果标题属性已赋值且长度大于0,则输出标题文件
if (typeof(this.title) != "undefined" && 0 < this.title.length)
{
this.ctx.font = "bolder "+ fontSize + "px 微软雅黑";
this.ctx.fillStyle = 'black';
this.ctx.fillText(this.title, (tcCanvas.width - this.title.length*fontSize)/2, 25);
}
fontSize= 16;
this.ctx.font = fontSize+"px 微软雅黑";
var textWidth= 100;
this.slice.forEach(function(obj){
this.ctx.fillStyle = obj.color;// 'black';
//计算文字所在的弧度
r2 = obj.start + obj.radian/2;
//计算相对于圆心文字偏移的位置
b = this.r * Math.cos(r2) ;
h = this.r * Math.sin(r2);
//文字的x坐标位置
var x2 = this.x + b;
if (x2 <= this.x)
{
//在圆心的左侧
x2 -= textWidth+10;
}
//文字的y坐标位置
var y2 = this.y + h;
if (y2 >= this.y)
{
y2 += fontSize;
}
else
{
//在圆心的上方
y2 -= fontSize;
}
//this.ctx.fillText(obj.text, x2, y2);//在一行输出
taDbg.value += "\nthis.r=" + this.r + " obj.text= " + obj.text + " x2=" + x2 + " y2=" + y2;
//换行输出方法1
var charNumPerLine = Math.ceil(textWidth / fontSize);
var t = obj.text, i = 0;
while (t.length > charNumPerLine)
{
this.ctx.fillText(t.substr(i,charNumPerLine), x2,y2);//绘制截取部分
i += charNumPerLine;
t = t.substring(i);
y2 += fontSize;
}
if (i <= obj.text.length)
{
this.ctx.fillText(t, x2,y2);//绘制剩余部分
}
/*
//换行输出方法2
var i, lastSubStrIndex = lineWidth=0;
for (i=0;i< obj.text.length; i++)
{
taDbg.value += '\nobj.text['+i +']=' + obj.text[i];
lineWidth += ctx.measureText(obj.text[i]).width;
if (lineWidth > textWidth)
{
this.ctx.fillText(obj.text.substring(lastSubStrIndex,i), x2,y2);//绘制截取部分
y2 += fontSize;
lineWidth = 0;
lastSubStrIndex = i;
}
if(i==obj.text.length-1)
{ //绘制剩余部分
this.ctx.fillText(obj.text.substring(lastSubStrIndex,i+1),x2,y2);
break;
}
}//for*/
},this);
}
};
var typeCountPie = new Pie({
ctx: ctx,
x: tcCanvas.width / 2,
y: tcCanvas.height / 2,
r: 150,
data:aTypeCount,
title:'“圳品”产品类别分析'
});
</script>
</body>
</html>