您好,欢迎访问一九零五行业门户网

html5中canvas图表实现柱状图的示例

本篇文章主要介绍了html5中canvas图表实现柱状图的示例,本文使用canvas来实现一个图表,小编觉得挺不错的,现在分享给大家,也给大家做个参考。
前几天用到了图表库,其中百度的echarts,感觉做得最好,看它默认用的是canvas,canvas图表在处理大数据方面比svg要好。那我也用canvas来实现一个图表库吧,感觉不会太难,先实现个简单的柱状图。
效果如下:
主要功能点包括:
文本的绘制
xy轴的绘制;
数据分组绘制;
数据动画的实现;
鼠标事件的处理。
使用方式
首先我们看一下使用方式,参考了部分echarts的使用方式,先传入要显示图表的html标签,接着调用init,初始化的同时传入数据。
var con=document.getelementbyid('container'); var chart=new bar(con); chart.init({ title:'全年降雨量柱状图', xaxis:{// x轴 data:['1月','2月','3月','4月','5月','6月','7月','8月','9月','10月','11月','12月'] }, yaxis:{//y轴 name:'水量', formatter:'{value} ml' }, series:[//分组数据 { name:'东部降水量', data:[62,20,17,45,100,56,19,38,50,120,56,130] }, { name:'西部降水量', data:[52,10,17,25,60,39,19,48,70,30,56,8] }, { name:'南部降水量', data:[12,10,17,25,27,39,50,38,100,30,56,90] }, { color:'hsla(270,80%,60%,1)', name:'北部降水量', data:[12,30,17,25,7,39,49,38,60,30,56,10] } ] });
图表基类,我们后面还要写饼图,折线图,所以把公共的部分抽出来。注意canvas.style.width与canvas.width是不一样的,前者会拉伸图形,后者才是我们正常用的,不会拉伸图形。在这里这样写先扩大再缩小是为了解决canvas绘制文字时模糊的问题。
class chart{ constructor(container){ this.container=container; this.canvas=document.createelement('canvas'); this.ctx=this.canvas.getcontext('2d'); this.w=1000*2; this.h=600*2; this.padding=120; this.paddingtop=50; this.title=''; this.legend=[]; this.series=[]; //通过缩小一倍,解决字体模糊问题 this.canvas.width=this.w; this.canvas.height=this.h; this.canvas.style.width = this.w/2 + 'px'; this.canvas.style.height = this.h/2 + 'px'; } }
柱状图初始化,调用es6中的object.assign(this,opt),这个相当于jq中的extend方法,把属性复制到当前实例。同时还建了个tip属性,这是个html标签,后面显示数据信息用。接着绘制图形,然后绑定鼠标事件。
class bar extends chart{ constructor(container){ super(container); this.xaxis={}; this.yaxis=[]; this.animatearr=[]; } init(opt){ object.assign(this,opt); if(!this.container)return; this.container.style.position='relative'; this.tip=document.createelement('p'); this.tip.style.csstext='display: none; position: absolute; opacity: 0.5; background: #000; color: #fff; border-radius: 5px; padding: 5px; font-size: 8px; z-index: 99;'; this.container.appendchild(this.canvas); this.container.appendchild(this.tip); this.draw(); this.bindevent(); } draw(){//绘制 } showinfo(){//显示信息 } animate(){//执行动画 } showdata(){//显示数据 }
绘制xy轴
首先绘制标题,接着xy轴,然后遍历分组数据series,里面有复杂的计算,然后绘制xy轴的刻度,绘制分组标签,最后是绘制数据。数据项series中是分组数据,它跟x轴的xaxis.data一一对应。每个项可以自定义名称和颜色,没有指定的话,名称赋予nunamed和自动生成颜色。这里还用legend属性记录下了标签列表信息,因为后续鼠标点击判断是否点中用的上。
canvas主要知识点:
分组标签使用了arcto方法,这样就能绘制出圆角的效果。
绘制文本使用了measuretext方法,可以用来测量文字所占宽度,这样就可以调整下一次绘制的位置,避免位置冲突。
translate位移方法,可以放在绘制上下文(save和restore的中间)中,这样可以避免复杂的位置运算。
draw(){ var that=this, ctx=this.ctx, canvas=this.canvas, w=this.w, h=this.h, padding=this.padding, paddingtop=this.paddingtop, xl=0,xs=0,xdis=w-padding*2,//x轴单位数,每个单位长度,x轴总长度 yl=0,ys=0,ydis=h-padding*2-paddingtop;//y轴单位数,每个单位长度,y轴总长度 ctx.fillstyle='hsla(0,0%,20%,1)'; ctx.strokestyle='hsla(0,0%,10%,1)'; ctx.linewidth=1; ctx.textalign='center'; ctx.textbaseline='middle'; ctx.font='24px arial'; ctx.clearrect(0,0,w,h); if(this.title){ ctx.save(); ctx.textalign='left'; ctx.font='bold 40px arial'; ctx.filltext(this.title,padding-50,70); ctx.restore(); } if(this.yaxis&&this.yaxis.name){ ctx.filltext(this.yaxis.name,padding,padding+paddingtop-30); } // x轴 ctx.save(); ctx.beginpath(); ctx.translate(padding,h-padding); ctx.moveto(0,0); ctx.lineto(w-2*padding,0); ctx.stroke(); // x轴刻度 if(this.xaxis&&(xl=this.xaxis.data.length)){ xs=(w-2*padding)/xl; this.xaxis.data.foreach((obj,i)=>{ var x=xs*(i+1); ctx.moveto(x,0); ctx.lineto(x,10); ctx.stroke(); ctx.filltext(obj,x-xs/2,40); }); } ctx.restore(); // y轴 ctx.save(); ctx.beginpath(); ctx.strokestyle='hsl(220,100%,50%)'; ctx.translate(padding,h-padding); ctx.moveto(0,0); ctx.lineto(0,2*padding+paddingtop-h); ctx.stroke(); ctx.restore(); if(this.series.length){ var curr,txt,dim,info,item,tw=0; for(var i=0;i<this.series.length;i++){ item=this.series[i]; if(!item.data||!item.data.length){ this.series.splice(i--,1);continue; } // 赋予没有颜色的项 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'; // 画分组标签 ctx.save(); ctx.translate(padding+w/4,paddingtop+40); that.legend.push({ hide:item.hide||false, name:item.name, color:item.color, x:padding+that.w/4+i*90+tw, y:paddingtop+40, w:60, h:30, r:5 }); ctx.textalign='left'; ctx.fillstyle=item.color; ctx.strokestyle=item.color; roundrect(ctx,i*90+tw,0,60,30,5); ctx.globalalpha=item.hide?0.3:1; ctx.fill(); ctx.filltext(item.name,i*90+tw+70,26); tw+=ctx.measuretext(item.name).width;//计算字符长度 ctx.restore(); if(item.hide)continue; //计算数据在y轴刻度 if(!info){ info=calculatey(item.data.slice(0,xl)); } curr=calculatey(item.data.slice(0,xl)); if(curr.max>info.max){ info=curr; } } if(!info) return; yl=info.num; ys=ydis/yl; //画y轴刻度 ctx.save(); ctx.fillstyle='hsl(200,100%,60%)'; ctx.translate(padding,h-padding); for(var i=0;i<=yl;i++){ ctx.beginpath(); ctx.strokestyle='hsl(220,100%,50%)'; ctx.moveto(-10,-math.floor(ys*i)); ctx.lineto(0,-math.floor(ys*i)); ctx.stroke(); ctx.beginpath(); ctx.strokestyle='hsla(0,0%,80%,1)'; ctx.moveto(0,-math.floor(ys*i)); ctx.lineto(xdis,-math.floor(ys*i)); ctx.stroke(); ctx.textalign='right'; dim=math.min(math.floor(info.step*i),info.max); txt=this.yaxis.formatter?this.yaxis.formatter.replace('{value}',dim):dim; ctx.filltext(txt,-20,-ys*i+10); } ctx.restore(); //画数据 this.showdata(xl,xs,info.max); }}
绘制数据
因为数据项需要后续执行动画和鼠标滑过的时候显示内容,所以把它放进动画队列animatearr中。这里要把分组数据展开,把之前的两次嵌套的数组转为一层,并计算好每个数据项的属性,比如名称,x坐标,y坐标,宽度,速度,颜色。数据组织完毕后,接着执行动画。
showdata(xl,xs,max){ //画数据 var that=this, ctx=this.ctx, ydis=this.h-this.padding*2-this.paddingtop, sl=this.series.filter(s=>!s.hide).length, sp=math.max(math.pow(10-sl,2)/3-4,5), w=(xs-sp*(sl+1))/sl, h,x,index=0; that.animatearr.length=0; // 展开数据项,填入动画队列 for(var i=0,item,len=this.series.length;i<len;i++){ item=this.series[i]; if(item.hide)continue; item.data.slice(0,xl).foreach((d,j)=>{ h=d/max*ydis; x=xs*j+w*index+sp*(index+1); that.animatearr.push({ index:i, name:item.name, num:d, x:math.round(x), y:1, w:math.round(w), h:math.floor(h+2), vy:math.max(300,math.floor(h*2))/100, color:item.color }); }); index++; } this.animate();}
执行动画
执行动画也没啥好说的,里面就是个自执行闭包函数。动画原理就是给y轴依次累加速度值vy。但记得当队列执行完动画后,要停止它,所以有个isstop的标志,每次执行完队列的时候就判断。
animate(){ var that=this, ctx=this.ctx, isstop=true; (function run(){ isstop=true; for(var i=0,item;i<that.animatearr.length;i++){ item=that.animatearr[i]; if(item.y-item.h>=0.1){ item.y=item.h; } else { item.y+=item.vy; } if(item.y<item.h){ ctx.save(); // ctx.translate(that.padding+item.x,that.h-that.padding); ctx.fillstyle=item.color; ctx.fillrect(that.padding+item.x,that.h-that.padding-item.y,item.w,item.y); ctx.restore(); isstop=false; } } if(isstop)return; requestanimationframe(run); }())}
绑定事件
事件一:mousemove的时候,看看鼠标位置是不是处于分组标签还是数据项上,绘制路径后调用ispointinpath(x,y),true则canvas.style.cursor='pointer';如果是数据项的话,还要给把该柱形重新绘制,设置透明度,区分出来。还需要把内容显示出来,这里是一个相对父容器container为绝对定位的p,初始化的时候已经建立为tip属性了。我们把显示部分封装成showinfo方法。
事件二:mousedown的时候,判断鼠标点击哪个分组标签,然后设置对应分组数据series中的hide属性,如果是true,表示不显示该项,然后调用draw方法,重写渲染绘制,执行动画。
bindevent(){ var that=this, canvas=this.canvas, ctx=this.ctx; this.canvas.addeventlistener('mousemove',function(e){ var islegend=false; // pos=windowtocanvas(canvas,e.clientx,e.clienty); var box=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]; ctx.save(); roundrect(ctx,item.x,item.y,item.w,item.h,item.r); // 因为缩小了一倍,所以坐标要*2 if(ctx.ispointinpath(pos.x*2,pos.y*2)){ canvas.style.cursor='pointer'; ctx.restore(); islegend=true; break; } canvas.style.cursor='default'; ctx.restore(); } if(islegend) return; //选择数据项 for(var i=0,item,len=that.animatearr.length;i<len;i++){ item=that.animatearr[i]; ctx.save(); ctx.fillstyle=item.color; ctx.beginpath(); ctx.rect(that.padding+item.x,that.h-that.padding-item.h,item.w,item.h); if(ctx.ispointinpath(pos.x*2,pos.y*2)){ //清空后再重新绘制透明度为0.5的图形 ctx.clearrect(that.padding+item.x,that.h-that.padding-item.h,item.w,item.h); ctx.globalalpha=0.5; ctx.fill(); canvas.style.cursor='pointer'; that.showinfo(pos,item); ctx.restore(); break; } canvas.style.cursor='default'; that.tip.style.display='none'; ctx.globalalpha=1; ctx.fill(); ctx.restore(); } },false); this.canvas.addeventlistener('mousedown',function(e){ e.preventdefault(); var box=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); // 因为缩小了一倍,所以坐标要*2 if(ctx.ispointinpath(pos.x*2,pos.y*2)){ that.series[i].hide=!that.series[i].hide; that.animatearr.length=0; that.draw(); break; } } },false); } //显示数据 showinfo(pos,obj){ var txt=this.yaxis.formatter?this.yaxis.formatter.replace('{value}',obj.num):obj.num; var box=this.canvas.getboundingclientrect(); var con=this.container.getboundingclientrect(); this.tip.innerhtml = '<p>'+obj.name+':'+txt+'</p>'; this.tip.style.left=(pos.x+(box.left-con.left)+10)+'px'; this.tip.style.top=(pos.y+(box.top-con.top)+10)+'px'; this.tip.style.display='block'; }
总结
这里完成的只是个基本的效果,其实还有很多地方要进一步优化,比如响应式的支持,移动端的支持,动画的效果,多y轴的支持,显示内容的效果,同时支持折线功能等。
相关推荐:
html 实现动态显示颜色块的报表效果(实例代码)
html5生成柱状图(条形图)效果的实例代码
以上就是html5中canvas图表实现柱状图的示例的详细内容。
其它类似信息

推荐信息