Canvas是HTML5提供的一个新标签,默认是一块矩形的画布,Canvas本身是没有绘图能力的,但是通过JavaScript语言,可以在Canvas画布上绘制各种各样的效果。
<canvas
id="myCanvas" width="200" height="100"style="border:1px solid #000000;">
</canvas>
常用的ECharts图表库就是基于Canvas绘图技术实现的,例如:常用的饼图样式如下,通过查看浏览器控制台可发现,这些图表都是被绘制在一个叫做canvas的HTML标签内,于是,接下来就尝试利用canvas+JavaScript进行绘图,来模仿实现饼图和玫瑰饼图。
Canvas:绘制饼图
饼图绘制结果
绘制结果如下,
canvas局部坐标系
先看一下Canvas坐标系是如何规定的?其实和浏览器的窗口坐标系的定义规则是一致的,即:canvas标签的左上角作为坐标原点(0,0)出现,定义了一个局部坐标系,如下图所示,
canvas:饼图文字标签绘制算法
饼图绘制时,想要绘制的文字标签的位置并不是固定的,而是动态变化的,因此,就需要针对”文字标签位置计算”设计一种可行的计算方法。
如下图,canvas在调用context绘图上下文的arc()方法绘制圆形时(其参数介绍如上图),其起始角度位置在:标记为0°的橙黄色的水平线位置。
假设标记为黄色的扇形区域为对应占比为24%,那么它必然要对应一个起始角度beginAngle,一个终止角度endAngle,以及一个被标记的文字标签。
那么,如何计算文字标签的位置呢?首先对文字标签的位置进行定义:将每次新添加的文字标签内容起始位置定义在黄色扇形的角平分线延长线位置(对应长度定义为R+offset,其中:R表示饼图的半径长度,offset表示向外偏移部分的长度),那么,文字标签位置(x,y)的计算公式即为:
对应的代码部分如截图所示(splitAnlge角平分线的弧度值计算过程可自行推理),
饼图:示例代码
示例代码为:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>06-pieChart</title>
</head>
<body>
<canvas id="canvas" width="300" height="300"></canvas>
</body>
<script>
//数据源
let dataSet = [
{
value: 50,
name: "轻工业产值",
},
{
value: 65,
name: "农业产值",
},
{
value: 25,
name: "畜牧业产值",
},
{
value: 36,
name: "重工业产值",
},
{
value: 89,
name: "零售业产值",
},
];
initPieChart("canvas", dataSet);//绘制饼图
function initPieChart(_targetId, _dataSet) {
let canvas = document.querySelector(_targetId);
let context = canvas.getContext("2d");
canvas.style.border = "1px solid #ccc"; //设置canvas边框
//获取canvas属性
let canvasHeight = canvas.height,
canvasWidth = canvas.width;
//解析数据集
let sum = _dataSet.reduce(function (pre, cur) {
return pre + cur.value;
}, 0);
let trans_dataSet = _dataSet.map(item => {
return {
value: item.value / sum,
name: item.name,
}
});
console.log(trans_dataSet)
drawPieChart(trans_dataSet,sum,canvasWidth,canvasHeight); //绘制饼图
/**
* 饼图绘制方法
* @param _dataSet
* @param _canvasWidth
* @param _canvasHeight
*/
function drawPieChart(_dataSet,_sum,_canvasWidth,_canvasHeight){
//计算饼图中心坐标{不考虑padding值}
let pieCenter = {
x:_canvasWidth/2,
y:_canvasHeight/2
};
let pieRadius = 0.5*Math.min(_canvasHeight,_canvasWidth)*0.5;
let beginAngle = -Math.PI/2 ;//开始起始角度
let curAngle = -Math.PI/2 ;//设置当前起始角度
//计算随机颜色表
let randomColorTable = (function (_length,_alpha=1){
let colorTable = new Array(_length).fill("#000");
return colorTable.map(item=>`rgba(${Math.random()*255},${Math.random()*255},${Math.random()*255},${_alpha})`);
})(_dataSet.length,0.8);
//解析数据集
for (let i = 0; i < _dataSet.length; i++) {
let endAngle = curAngle + _dataSet[i]['value']*360*Math.PI/180; //计算当前终止角度-{角度转弧度}
//1-绘制圆弧
context.beginPath();//开始绘制圆弧
context.moveTo(pieCenter.x,pieCenter.y);
context.arc(pieCenter.x,pieCenter.y,pieRadius,curAngle,endAngle,false);//顺时针绘制饼图
context.closePath();
context.fillStyle = randomColorTable[i%randomColorTable.length];
context.fill();
//2-绘制文字标签
let splitAngle = curAngle + _dataSet[i]['value']*360*Math.PI/180*0.5; //角平分线所在射线的角度
let textParams = {
splitAngle : splitAngle,//角度参数
offset : 30,//文字位置偏移量
label: `${_dataSet[i]['name']}:${(_dataSet[i]['value']*100).toFixed(2)}%`,//文本内容
};
let textArray = textParams.label.split(":");
//计算文字位置
let textPosition = {
x: pieCenter.x + Math.cos(textParams.splitAngle) * (pieRadius+textParams.offset),
y: pieCenter.y + Math.sin(textParams.splitAngle) * (pieRadius+textParams.offset),
};
context.beginPath();
console.log()
context.stroke();
//文字换行处理
textArray.some((item,index)=>{
context.textAlign="center";
// context.fillStyle = randomColorTable[i%randomColorTable.length];
context.font = "12px Arial";//设置字体大小+字体样式
context.fillStyle = "rgba(0,0,0,0.8)";//设置字体颜色
context.fillText(item,textPosition.x,textPosition.y+index*20);
});
// context.fillText(textParams.label,textPosition.x,textPosition.y);//文字不换行可以直接调用此句代码
context.closePath();
curAngle = endAngle; //更新下一次起始角度
}
}
}
</script>
</html>
Canvas:绘制玫瑰饼图
玫瑰饼图实现思路
在上面饼图的基础上,实现玫瑰图。基本思路为:
①扇形半径的不确定性:注意到,每次绘制扇形区域时,都需要给定扇形半径r参数,而玫瑰图的特点就是——扇形半径长度的不确定性,这一点可以借助Math.random()产生随机数实现;
②文字标签位置自适应性:当扇形半径变化时,我们期待的预期效果——文字标签也跟着自动调整位置,回顾一下上面的饼图位置标签绘制算法,只需要将(R+offset)参数中的R更新为当前扇形的半径参数r即可。
玫瑰饼图绘制结果
玫瑰饼图:示例代码
示例代码如下,
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>06-pieChart</title>
</head>
<body>
<canvas id="canvas" width="300" height="300"></canvas>
</body>
<script>
//数据源
let dataSet = [
{
value: 50,
name: "轻工业产值",
},
{
value: 65,
name: "农业产值",
},
{
value: 25,
name: "畜牧业产值",
},
{
value: 36,
name: "重工业产值",
},
{
value: 89,
name: "零售业产值",
},
];
initPieChart("canvas", dataSet);//绘制饼图
function initPieChart(_targetId, _dataSet) {
let canvas = document.querySelector(_targetId);
let context = canvas.getContext("2d");
canvas.style.border = "1px solid #ccc"; //设置canvas边框
//获取canvas属性
let canvasHeight = canvas.height,
canvasWidth = canvas.width;
//解析数据集
let sum = _dataSet.reduce(function (pre, cur) {
return pre + cur.value;
}, 0);
let trans_dataSet = _dataSet.map(item => {
return {
value: item.value / sum,
name: item.name,
}
});
console.log(trans_dataSet)
drawPieChart(trans_dataSet,sum,canvasWidth,canvasHeight); //绘制饼图
/**
* 饼图绘制方法
* @param _dataSet
* @param _canvasWidth
* @param _canvasHeight
*/
function drawPieChart(_dataSet,_sum,_canvasWidth,_canvasHeight){
//计算饼图中心坐标{不考虑padding值}
let pieCenter = {
x:_canvasWidth/2,
y:_canvasHeight/2
};
let pieRadius = 0.5*Math.min(_canvasHeight,_canvasWidth)*0.45;
let beginAngle = -Math.PI/2 ;//开始起始角度
let curAngle = -Math.PI/2 ;//设置当前起始角度
//计算随机颜色表
let randomColorTable = (function (_length,_alpha=1){
let colorTable = new Array(_length).fill("#000");
return colorTable.map(item=>`rgba(${Math.random()*255},${Math.random()*255},${Math.random()*255},${_alpha})`);
})(_dataSet.length,0.8);
//解析数据集
for (let i = 0; i < _dataSet.length; i++) {
let roseRadians = pieRadius + Math.random()*50;//计算玫瑰图随机半径值
let endAngle = curAngle + _dataSet[i]['value']*360*Math.PI/180; //计算当前终止角度-{角度转弧度}
//1-绘制圆弧
context.beginPath();//开始绘制圆弧
context.moveTo(pieCenter.x,pieCenter.y);
context.arc(pieCenter.x,pieCenter.y,roseRadians,curAngle,endAngle,false);//顺时针绘制饼图
context.closePath();
context.fillStyle = randomColorTable[i%randomColorTable.length];
context.fill();
//2-绘制文字标签
let splitAngle = curAngle + _dataSet[i]['value']*360*Math.PI/180*0.5; //角平分线所在射线的角度
let textParams = {
splitAngle : splitAngle,//角度参数
offset : 30,//文字位置偏移量
label: `${_dataSet[i]['name']}:${(_dataSet[i]['value']*100).toFixed(2)}%`,//文本内容
};
let textArray = textParams.label.split(":");
//计算文字位置
let textPosition = {
x: pieCenter.x + Math.cos(textParams.splitAngle) * (roseRadians+textParams.offset),
y: pieCenter.y + Math.sin(textParams.splitAngle) * (roseRadians+textParams.offset),
};
context.beginPath();
console.log()
context.stroke();
//文字换行处理
textArray.some((item,index)=>{
context.textAlign="center";
// context.fillStyle = randomColorTable[i%randomColorTable.length];
context.font = "12px Arial";//设置字体大小+字体样式
context.fillStyle = "rgba(0,0,0,0.8)";//设置字体颜色
context.fillText(item,textPosition.x,textPosition.y+index*20);
});
// context.fillText(textParams.label,textPosition.x,textPosition.y);//文字不换行可以直接调用此句代码
context.closePath();
curAngle = endAngle; //更新下一次起始角度
}
}
}
</script>
</html>