由于业务需求,echart并不能满足圆角环状饼图,故自定义实现之。效果如下:
js代码:
function drawCircle(id,arr,options,colors = ["#007FDD", "#379CDD", "#63BAF2", "#C3DCED", "#E9F6FF"]) {
const {radius, lng, title, titleColor, horizon} = options;
let dom = document.getElementById(id);
dom.innerHTML="";
let c = document.createElement('canvas');
c.height = dom.clientHeight;
c.width = dom.clientWidth;
let x = dom.clientWidth / 2;
let y = dom.clientHeight / 2;
dom.appendChild(c);
let ctx = c.getContext("2d");
//求总和
let sum = 0;
for(let i=0;i<arr.length;i++){
let item = arr[i];
sum += item.value;
}
//绘制标题
ctx.beginPath();
ctx.fillStyle=titleColor;
//文字居中
ctx.textAlign = 'center';
//相对原点垂直居中
ctx.textBaseline="middle";
//文字样式:加粗 16像素 字体Arial
ctx.font = 'normal 18px 微软雅黑';
ctx.fillText(title,x,y);
//累计角度
let sumAngle = 0;
//设置开始的位置
let start = 1.5*Math.PI;
function drawLine(ctx, angle, color, item, rate) {
let label = item.name + ":"+rate +"%";
//累计角度 - 当前的一半角度
let bjAngle = sumAngle-angle;
ctx.beginPath();
ctx.globalCompositeOperation = 'destination-over';
let chgAngle = 0.017453293 * bjAngle;
//设置起点状态
let tmp1 = {
x: x-radius*Math.sin(chgAngle),
y: y-radius*Math.cos(chgAngle)
};
//设置末端状态
let tmp2 = {
x: x-lng*Math.sin(chgAngle),
y: y-lng*Math.cos(chgAngle)
};
ctx.moveTo (tmp1.x, tmp1.y);
ctx.lineTo (tmp2.x, tmp2.y);
//设置线宽状态
ctx.lineWidth = 1;
let tmp3 = {};
let tmp4 = {};
//向左绘制横线
if(bjAngle<180){
tmp3.x = tmp2.x-horizon;
tmp3.y = tmp2.y;
tmp4.x = tmp3.x-6*label.length;
tmp4.y = tmp3.y;
}else{
tmp3.x = tmp2.x+horizon;
tmp3.y = tmp2.y;
tmp4.x = tmp3.x+6*label.length;
tmp4.y = tmp3.y;
}
ctx.lineTo (tmp3.x, tmp3.y);
//进行绘制
ctx.stroke();
ctx.beginPath();
ctx.fillStyle = color;
ctx.arc(tmp3.x, tmp3.y, 2, 0, 2*Math.PI, true);
ctx.fill();
//绘制标题
ctx.beginPath();
ctx.fillStyle= '#fff';
//相对原点垂直居中
ctx.textBaseline="middle";
//文字样式:加粗 16像素 字体Arial
ctx.font = 'normal 12px 微软雅黑';
ctx.fillText(label,tmp4.x, tmp4.y);
if(item.name1){
//绘制标题
ctx.beginPath();
ctx.fillStyle= '#CDFFEF';
//相对原点垂直居中
ctx.textBaseline="middle";
//文字样式:加粗 16像素 字体Arial
ctx.font = 'normal 12px 微软雅黑';
ctx.fillText("("+item.name1+")",tmp4.x, tmp4.y+12);
}
}
//动画
function sleep(ms, callback) {
setTimeout(callback, ms)
}
let lastItem = arr[arr.length-1];
for(let key=0;key<arr.length;key++){
let item = arr[key];
let rate = item.value/sum;
let curColor = colors[key];
sleep(80*(key+1), function () {
ctx.beginPath();
ctx.lineWidth = 20;
ctx.strokeStyle = curColor;
if(item.value==lastItem.value&&item.name==lastItem.name){
//先渲染一半
let add = rate*Math.PI;
ctx.arc(x,y,radius, start, start-add, true);
ctx.lineCap = 'round';
ctx.globalCompositeOperation = 'source-over'
start -= add;
ctx.stroke();
ctx.beginPath();
ctx.globalCompositeOperation = 'destination-over';
ctx.arc(x,y,radius, start, start-add, true);
ctx.lineCap = 'butt';
ctx.stroke();
} else {
let add = rate*2*Math.PI;
ctx.arc(x,y,radius, start, start-add, true);
ctx.lineCap = 'round';
start -= add;
ctx.stroke();
}
sumAngle += rate*180*2;
let angle = rate*180;
drawLine(ctx, angle, colors[key], item, Math.round(rate*100));
ctx.globalCompositeOperation = 'source-over'
})
}
}
使用:
const options = {
radius: 50,
lng: 80,
title: 'CPU',
titleColor: '#73FBFD',
horizon: 10
};
const arr = [{name1: '全辖:30%',name: '工作', value: 8},{name1: '全辖:30%',name: '看书', value: 4},{name1: '全辖:30%',name: '娱乐', value: 2}, {name1: '全辖:30%',name: '睡觉', value: 8},{name1: '全辖:30%',name: '吃饭', value: 3}];
drawCircle('myCanvas', arr,options)
方法说明
- 主要使用的是lineWidth宽度来形成环形;
- ctx.lineCap = 'round'属性来实现圆角;
- 对于最后一个数据的渲染要分两半,一半不是圆角,另一半设置圆角,并ctx.globalCompositeOperation = 'destination-over'来实现最后一部分圆弧的圆角不被覆盖;
- 引导线绘制的时候要分左右两边;并且引导线的位置是从每段圆弧中间出发绘制的;
- 动画部分的实现比较简单粗暴,利用每个数据作为分割点,一个数据一个数据的绘制就出现了动画的效果;
- 兼容性:目前只兼容了IE8及以上。
优化
- 对于最后一个数据的渲染要分两半,一半不是圆角,另一半设置圆角,并ctx.globalCompositeOperation = 'destination-over'来实现最后一部分圆弧的圆角不被覆盖;(这种方法会出现圆弧很小的时候,覆盖不明显的情况)如下图:
解决方法是:记录最大的数据,从最大数据开始从新绘制一遍圆弧。
- 还有一个问题就是label会挤在一起。 解决方法是:先计算出label绘制的位置,动态调整,直到满足条件,再绘制。
- 对于这些label,首先确定它的外包矩形。 主要是宽度和高度的计算: 高度:字体大小 * 2 宽度:计算文字个数 * 字体大小 文字个数:中文是1,非中文为 0.5 计算方法:
//统计汉字https://www.cnblogs.com/jkr666666/p/11645070.html
function getByteLen(val) {
let len = 0;
for (let i = 0; i < val.length; i++) {
let a = val.charAt(i);
if (a.match(/[^\x00-\xff]/ig) != null) {
len += 1;
} else {
len += 0.5;
}
}
return len;
}
根据高度和宽度以及圆点的位置就可以求出矩形4个点的坐标。
-
计算2个矩形相交的算法
参考:www.jianshu.com/p/a2d881847…
满足下面3个条件:
max(Xa1,Xb1) <= min(Xa2,Xb2)
max(Ya1,Yb1) <= min(Ya2,Yb2)
相交矩形面积 > 0 -
如果两个或多个矩形相交了,动态调整矩形的位置。这个时候分了左边和右边两种情况。
对于左边的圆弧,记录相交的两个或多个矩形坐标;保持第一个矩形4个点位置不变,根据矩形的高度,依次向下(Y轴坐标变大)动态调整其余矩形的坐标位置。重新遍历左边圆弧矩形,直到不出现彼此相交的矩形为止。
对于右边的圆弧类似。
Git地址:github.com/YY88Xu/draw…
效果图: