原文地址:canvas图表(3) - 饼图

这几天把canvas图表都优化了下,动画效果更加出色了,可以说很逼近Echart了。刚刚写完的饼图,非常好的实现了既定的功能,交互的动画效果也是很棒的。

效果请看:饼图https://edwardzhong.github.io/sites/demo/dist/chartpie.html



功能点包括:

  1. 组织数据;
  2. 画面绘制;
  3. 数据动画的实现;
  4. 鼠标事件的处理。

使用方式

饼图的数据方面要简单很多,因为不用多个分组的数据。把所有的数据相加得出总数,然后每个数据分别求出百分比,有了百分比再相乘360度的弧度得出每个数据在圆盘中对应的要显示的角度。

	var con=document.getElementById('container');
var pie=new Pie(con);
pie.init({
title:'网站用户访问来源',
toolTip:'访问来源',
data:[
{value:435, name:'直接访问'},
{value:310, name:'邮件营销'},
{value:234, name:'联盟广告'},
{value:135, name:'视频广告'},
{value:1548, name:'搜索引擎'}
]
});

代码结构

因为为了同时实现新增动画和更新动画,这次的代码结构经过了重构和优化,跟之前的有比较大的区别。

	class Line extends Chart{
constructor(container){
super(container);
}
// 初始化
init(opt){ }
// 绑定事件
bindEvent(){ }
// 显示信息
showInfo(pos,arr){ }
// 清除内容再绘制
clearGrid(index){ }
// 执行数据动画
animate(){ }
// 执行
create(){ }
// 组织数据
initData(){ }
// 绘制
draw(){ }
}

组织数据

这次把组织数据的功能单独拎了出来,这样方便重用和修改。然后还要给动画对象增加是否创建的属性create和上次最后更新的度数last,为什么呢?因为我们要同时实现创建和更新图形的动画效果。

	initData(){
var that=this,
item,
total=0;
if(!this.data||!this.data.length){return;}
this.legend.length=0;
for(var i=0;i<this.data.length;i++){
item=this.data[i];
// 赋予没有颜色的项
if(!item.color){
var hsl=i%2?180+20*i/2:20*(i-1);
item.color='hsla('+hsl+',70%,60%,1)';
}
item.name=item.name||'unnamed'; this.legend.push({
hide:!!item.hide,
name:item.name,
color:item.color,
x:50,
y:that.paddingTop+40+i*50,
w:80,
h:30,
r:5
}); if(item.hide)continue;
total+=item.value;
} for(var i=0;i<this.data.length;i++){
item=this.data[i];
if(!this.animateArr[i]){//创建
this.animateArr.push({
i:i,
create:true,
hide:!!item.hide,
name:item.name,
color:item.color,
num:item.value,
percent:Math.round(item.value/total*10000)/100,
ang:Math.round(item.value/total*Math.PI*2*100)/100,
last:0,
cur:0
});
} else {//更新
if(that.animateArr[i].hide&&!item.hide){
that.animateArr[i].create=true;
that.animateArr[i].cur=0;
} else {
that.animateArr[i].create=false;
}
that.animateArr[i].hide=item.hide;
that.animateArr[i].percent=Math.round(item.value/total*10000)/100;
that.animateArr[i].ang=Math.round(item.value/total*Math.PI*2*100)/100;
}
}
}

绘制

饼图的绘制功能很简单,因为不用坐标系,只需要绘制标题和标签列表。

	draw(){
var item,ctx=this.ctx;
ctx.fillStyle='hsla(0,0%,30%,1)';
ctx.strokeStyle='hsla(0,0%,20%,1)';
ctx.textBaseLine='middle';
ctx.font='24px arial'; ctx.clearRect(0,0,this.W,this.H);
if(this.title){
ctx.save();
ctx.textAlign='center';
ctx.font='bold 40px arial';
ctx.fillText(this.title,this.W/2,70);
ctx.restore();
}
ctx.save();
for(var i=0;i<this.legend.length;i++){
item=this.legend[i];
// 画分类标签
ctx.textAlign='left';
ctx.fillStyle=item.color;
ctx.strokeStyle=item.color;
roundRect(ctx,item.x,item.y,item.w,item.h,item.r);
ctx.globalAlpha=item.hide?0.3:1;
ctx.fill();
ctx.fillText(item.name,item.x+item.w+20,item.y+item.h-5);
}
ctx.restore();
}

执行绘制饼图动画

动画区分了创建和更新,这样用户很容易就能看出数据的比例关系变化,也就更加的直观。创建就是从0弧度到指定的弧度,只有数值的增加;而更新动画就要区分增加和减少的情况,因为当用户点击某个标签的时候,会隐藏显示某个分类的数据,于是需要重新计算每个分类的比例,那么相应的分类百分比就会增加或减少。我们根据当前最新要达到的比例ang和已经执行完的当前比例last的进行对比,相应执行增加和减少比例,动画原理就是这样。

canvas绘制圆形context.arc(x,y,r,sAngle,eAngle,counterclockwise);只要我们指定开始角度和结束角度就会画出披萨饼一样的效果,所有的披萨饼加起来就是一个圆。

	animate(){
var that=this,
ctx=that.ctx,
canvas=that.canvas,
item,startAng,ang,
isStop=true; (function run(){
isStop=true;
ctx.save();
ctx.translate(that.W/2,that.H/2);
ctx.fillStyle='#fff';
ctx.beginPath();
ctx.arc(0,0,that.H/3+30,0,Math.PI*2,false);
ctx.fill();
for(var i=0,l=that.animateArr.length;i<l;i++){
item=that.animateArr[i];
if(item.hide)continue;
startAng=-Math.PI/2;
that.animateArr.forEach((obj,j)=>{
if(j<i&&!obj.hide){startAng+=obj.cur;}
}); ctx.fillStyle=item.color;
if(item.create){//创建动画
if(item.cur>=item.ang){
item.cur=item.last=item.ang;
} else {
item.cur+=0.05;
isStop=false;
}
} else {//更新动画
if(item.last>item.ang){
ang=item.cur-0.05;
if(ang<item.ang){
item.cur=item.last=item.ang;
}
} else {
ang=item.cur+0.05;
if(ang>item.ang){
item.cur=item.last=item.ang;
}
}
if(item.cur!=item.ang){
item.cur=ang;
isStop=false;
}
} ctx.beginPath();
ctx.moveTo(0,0);
ctx.arc(0,0,that.H/3,startAng,startAng+item.cur,false);
ctx.closePath();
ctx.fill();
}
ctx.restore();
if(isStop) {
that.clearGrid();
return;
}
requestAnimationFrame(run);
}());
}

交互处理

执行完动画后,我这里再执行了一遍清除绘制,这个也是鼠标触摸标签和饼图时的对应动画方法,会绘制每个分类的名称描述,更方便用户查看。

	clearGrid(index){
var that=this,
ctx=that.ctx,
canvas=that.canvas,
item,startAng=-Math.PI/2,
len=that.animateArr.filter(item=>!item.hide).length,
j=0,angle=0,
r=that.H/3;
ctx.clearRect(0,0,that.W,that.H);
that.draw();
ctx.save();
ctx.translate(that.W/2,that.H/2); for(var i=0,l=that.animateArr.length;i<l;i++){
item=that.animateArr[i];
if(item.hide)continue;
ctx.strokeStyle=item.color;
ctx.fillStyle=item.color;
angle=j>=len-1?Math.PI*2-Math.PI/2:startAng+item.ang;
ctx.beginPath();
ctx.moveTo(0,0);
if(index===i){
ctx.save();
// ctx.shadowColor='hsla(0,0%,50%,1)';
ctx.shadowColor=item.color;
ctx.shadowBlur=5;
ctx.arc(0,0,r+20,startAng,angle,false);
ctx.closePath();
ctx.fill();
ctx.stroke();
ctx.restore();
} else {
ctx.arc(0,0,r,startAng,angle,false);
ctx.closePath();
ctx.fill();
}
//画分类描述
var tr=r+40,tw=0,
tAng=startAng+item.ang/2,
x=tr*Math.cos(tAng),
y=tr*Math.sin(tAng); ctx.lineWidth=2;
ctx.lineCap='round';
ctx.beginPath();
ctx.moveTo(0,0);
ctx.lineTo(x,y);
if(tAng>=-Math.PI/2&&tAng<=Math.PI/2){
ctx.lineTo(x+30,y);
ctx.fillText(item.name,x+40,y+10);
} else {
tw=ctx.measureText(item.name).width;//计算字符长度
ctx.lineTo(x-30,y);
ctx.fillText(item.name,x-40-tw,y+10);
} ctx.stroke();
startAng+=item.ang;
j++;
}
ctx.restore();
}

事件处理

mousemove的时候,触摸标签和触摸饼图都是基本相同的效果,选中的分类扩大半径,同时增加阴影,以达到凸出来的动画效果,具体实现请看上面的clearGrid方法。判断是否点中都是使用isPointInPath这个api,之前已经介绍过,不再细讲。

mousedown某个击标签就会显示隐藏对应分类,每次触发就会看到饼图的比例变化的动画效果,这个和之前的柱状图和折线图的功能一致。

	bindEvent(){
var that=this,
canvas=that.canvas,
ctx=that.ctx;
if(!this.data.length) return;
this.canvas.addEventListener('mousemove',function(e){
var isLegend=false;
var box=canvas.getBoundingClientRect(),
pos = {
x:e.clientX-box.left,
y:e.clientY-box.top
};
// 标签
for(var i=0,item,len=that.legend.length;i<len;i++){
item=that.legend[i];
roundRect(ctx,item.x,item.y,item.w,item.h,item.r);
if(ctx.isPointInPath(pos.x*2,pos.y*2)){
canvas.style.cursor='pointer';
if(!item.hide){
that.clearGrid(i);
}
isLegend=true;
break;
}
canvas.style.cursor='default';
that.tip.style.display='none';
} if(isLegend) return;
// 图表
var startAng=-Math.PI/2;
for(var i=0,l=that.animateArr.length;i<l;i++){
item=that.animateArr[i];
if(item.hide)continue;
ctx.beginPath();
ctx.moveTo(that.W/2,that.H/2);
ctx.arc(that.W/2,that.H/2,that.H/3,startAng,startAng+item.ang,false);
ctx.closePath();
startAng+=item.ang;
if(ctx.isPointInPath(pos.x*2,pos.y*2)){
canvas.style.cursor='pointer';
that.clearGrid(i);
that.showInfo(pos,that.toolTip,[{name:item.name,num:item.num+' ('+item.percent+'%)'}]);
break;
}
canvas.style.cursor='default';
that.clearGrid();
} },false);
this.canvas.addEventListener('mousedown',function(e){
e.preventDefault();
var box=that.canvas.getBoundingClientRect();
var pos = {
x:e.clientX-box.left,
y:e.clientY-box.top
};
for(var i=0,item,len=that.legend.length;i<len;i++){
item=that.legend[i];
roundRect(ctx,item.x,item.y,item.w,item.h,item.r);
if(ctx.isPointInPath(pos.x*2,pos.y*2)){
that.data[i].hide=!that.data[i].hide;
that.create();
break;
}
}
},false); }

最后

所有图表代码请看chart.js

canvas图表(3) - 饼图的更多相关文章

  1. canvas图表(1) - 柱状图

    原文地址:canvas图表(1) - 柱状图 前几天用到了图表库,其中百度的ECharts,感觉做得最好,看它默认用的是canva,canvas图表在处理大数据方面比svg要好.那我也用canvas来 ...

  2. canvas图表(4) - 散点图

    原文地址:canvas图表(4) - 散点图 今天开始完成散点图,做完这一节,我的canvas图表系列就算是完成了,毕竟平时最频繁用到的就是这几类图表了:柱状,折线,饼图,散点.经过编写canvas图 ...

  3. 超酷HTML5 Canvas图表应用Chart.js自定义提示折线图

    超酷HTML5 Canvas图表应用Chart.js自定义提示折线图 效果预览 实例代码 <div class="htmleaf-container"> <div ...

  4. canvas图表(2) - 折线图

    原文地址:canvas图表(2) - 折线图 话说这天气一冷啊, 就患懒癌, 就不想码代码, 就想着在床上舒舒服服看视频. 那顺便就看blender视频, 学习下3D建模, 如果学会了建3D模型, 那 ...

  5. 基于html5 Canvas图表库 : ECharts

    ECharts开源来自百度商业前端数据可视化团队,基于html5 Canvas,是一个纯Javascript图表库,提供直观,生动,可交互,可个性化定制的数据可视化图表.创新的拖拽重计算.数据视图.值 ...

  6. Chart.js – 效果精美的 HTML5 Canvas 图表库

    Chart.js 是一个令人印象深刻的 JavaScript 图表库,建立在 HTML5 Canvas 基础上.目前,它支持6种图表类型(折线图,条形图,雷达图,饼图,柱状图和极地区域区).而且,这是 ...

  7. canvas图表详解系列(3):动态饼状图(原生Js仿echarts饼状图)

    本章建议学习时间4小时 学习方式:详细阅读,并手动实现相关代码(如果没有canvas基础,需要先学习前面的canvas基础笔记) 学习目标:此教程将教会大家如何使用canvas绘制各种图表,详细分解步 ...

  8. canvas图表详解系列(1):柱状图

    本章建议学习时间4小时 学习方式:详细阅读,并手动实现相关代码(如果没有canvas基础,需要先学习前面的canvas基础笔记) 学习目标:此教程将教会大家如何使用canvas绘制各种图表,详细分解步 ...

  9. canvas图表详解系列(2):折线图

    本章建议学习时间4小时 学习方式:详细阅读,并手动实现相关代码(如果没有canvas基础,需要先学习前面的canvas基础笔记) 学习目标:此教程将教会大家如何使用canvas绘制各种图表,详细分解步 ...

随机推荐

  1. OpenCV中的绘图函数-OpenCV步步精深

    OpenCV 中的绘图函数 画线 首先要为画的线创造出环境,就要生成一个空的黑底图像 img=np.zeros((512,512,3), np.uint8) 这是黑色的底,我们的画布,我把窗口名叫做i ...

  2. 【完美解决】2017打开MVC 4项目,cshtml页面提示‘当前上下文不存在名称model’

    时间:2017/10/19 背景:领导让再之前的MVC 4老项目上新增功能,从GIT上拉取下来,使用VS2017打开之后,cshtml界面所有和Razor相关的代码均被提示‘当前上下文不存在名称XXX ...

  3. flume自定义Source(taildirSource),自定义Sink(数据库),开发完整步骤

    一.flume简单了解推荐网站(简介包括简单案例部署): http://www.aboutyun.com/thread-8917-1-1.html 二.我的需求是实现从ftp目录下采集数据,目录下文件 ...

  4. 【转】 bio 与块设备驱动

    原文地址: bio 与块设备驱动      系统中能够随机访问固定大小数据片(chunk)的设备被称作块设备,这些数据片就称作块.块设备文件都是以安装文件系统的方式使用,此也是块设备通常的访问方式.块 ...

  5. 【转】S3C2440存储系统-SDRAM驱动

    SDRAM(Synchronous Dynamic Random Access Memory,同步动态随机存储器)也就是通常所说的内存.内存的工作原理.控制时序.及相关控制器的配置方法一直是嵌入式系统 ...

  6. iOS开发从申请账号到上线APP Store步骤

    1.developer.apple.com 申请开发者账号 2.根据API Cloud创建证书: http://docs.apicloud.com/Dev-Guide/iOS-License-Appl ...

  7. .NET程序集引用COM组件MSScriptControl所遇到的问题

    问题描述:为了在C#中执行js脚本,在一个目标平台编译为Any Cpu的.NET程序集中引用了MSScriptControl组件,在winform程序中,调用这个程序集中的执行js的方法,没有任何问题 ...

  8. Jenkins项目部署使用教程-----02视图及项目添加

    注意:此以我公司为例,以svn上传代码 一.添加视图 1.点击右上角”+”号,新建新视图 勾选在该视图下显示的项目或者在该视图新建项目,点保存即可. 二.新建项目 1.点击左上角的新建,创建新项目. ...

  9. LeetCode 153. Find Minimum in Rotated Sorted Array (在旋转有序数组中找到最小值)

    Suppose an array sorted in ascending order is rotated at some pivot unknown to you beforehand. (i.e. ...

  10. Vue源码后记-vFor列表渲染(1)

    钩子函数比较简单,没有什么意思,这一节搞点大事情 => 源码中v-for的渲染过程. vue的内置指令包含了v-html.v-if.v-once.v-bind.v-on.v-show等,先从一个 ...