I used a chart library a few days ago, among which Baidu's ECharts seems to be the best. It uses canvas by default. Canvas charts are better than SVG in processing big data. Then I will also use canvas to implement a chart library. It doesn’t feel too difficult. Let’s implement a simple bar chart first.
The effect is as follows: Main function points include:First, let's take a look at how to use it. We refer to some ECharts usage methods. First, we pass in the html tag to display the chart, then call init, and pass in the data during initialization.
var con=document.getElementById('container'); var chart=new Bar(con); chart.init({ title:'Year-round rainfall histogram', xAxis:{// x-axis data: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October','November','December'] }, yAxis:{//y-axis name:'water volume', formatter:'{value} ml' }, series:[//Group data{ name:'Precipitation in the East', data:[62,20,17,45,100,56,19,38,50,120,56,130] }, { name:'Precipitation in the West', data:[52,10,17,25 ,60,39,19,48,70,30,56,8] }, { name:'Southern precipitation', data:[12,10,17,25,27,39,50,38,100,30,56,90] }, { color:'hsla(270,80%,60%,1)', name:'Northern precipitation Quantity', data:[12,30,17,25,7,39,49,38,60,30,56,10] } ] });
For the chart base class, we will also write pie charts and line charts later, so we will extract the common parts. Note that canvas.style.width and canvas.width are different. The former will stretch the graphics, while the latter is what we use normally and will not stretch the graphics. The purpose of writing first expansion and then reduction here is to solve the problem of blurring when drawing text on 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=[]; //Solve the font blur problem by doubling the size 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'; } }
To initialize the histogram, call Object.assign(this,opt) in es6. This is equivalent to the extend method in JQ, which copies the properties to the current instance. At the same time, a tip attribute is also created, which is an html tag and is used to display data information later. Then draw the graphics and bind mouse events.
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('div'); 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(){//Drawing} showInfo(){//Display information} animate(){//Perform animation} showData(){//Display data}
Draw XY axis
First draw the title, then the XY axis, then traverse the grouped data series, which contains complex calculations, then draw the scale of the XY axis, draw the group labels, and finally draw the data. The data item series is grouped data, which corresponds to xAxis.data of the X-axis one-to-one. Each item can have a customized name and color. If not specified, the name is given to nunamed and the color is automatically generated. The legend attribute is also used here to record the tag list information, because it is useful for subsequent mouse clicks to determine whether the click is correct.
Main knowledge points of canvas:
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,//Number of x-axis units, length of each unit, total length of x-axis yl=0,ys=0,ydis=H-padding*2-paddingTop; //The number of y-axis units, the length of each unit, the total length of the y-axis 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-axis ctx.save(); ctx.beginPath(); ctx.translate(padding,H-padding); ctx.moveTo(0,0); ctx.lineTo(W-2*padding,0); ctx .stroke(); // x-axis scale 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-axis 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; } // Assign items without color 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'; // Draw grouping labels 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;//Calculate the character length ctx.restore(); if(item.hide)continue; //Calculate the data on the Y-axis scale 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; //Draw Y-axis scale 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() ; //Draw data this.showData(xl,xs,info.max); }}plot data
Because the data item needs to be subsequently animated and displayed when the mouse slides over it, it is put into the animation queue animateArr. Here we need to expand the grouped data, convert the previous two nested arrays into one layer, and calculate the attributes of each data item, such as name, x coordinate, y coordinate, width, speed, and color. After the data is organized, the animation is executed.
showData(xl,xs,max){ //Draw data 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; // Expand the data items and fill in the animation queue 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();}Execute animation
There's nothing much to say about executing the animation, it's just a self-executing closure function. The principle of animation is to sequentially accumulate the velocity value vy on the y-axis. But remember that when the queue finishes executing the animation, it needs to be stopped, so there is an isStop flag, which is judged every time the queue is finished executing.
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); }())}Binding event
Event 1: When mousemove, check whether the mouse position is on the group label or the data item. After drawing the path, call isPointInPath(x,y). If true, canvas.style.cursor='pointer'; if it is a data item, You also need to redraw the column, set the transparency, and distinguish it. The content also needs to be displayed. Here is a div that is absolutely positioned relative to the parent container container. The tip attribute has been established during initialization. We encapsulate the display part into the showInfo method.
Event 2: When mousedown occurs, determine which group label the mouse clicks on, and then set the hide attribute in the corresponding group data series. If it is true, it means that the item will not be displayed, and then call the draw method, override rendering and drawing, and perform animation.
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 }; // Grouping label 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); // Because it is doubled, the coordinates are *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; //Select data items 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)){ //Clear and then redraw the graphics with a transparency of 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); // Because it is doubled, the coordinates are *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); } //Display data 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'; }Summarize
What is completed here is only a basic effect. In fact, there are many areas that need to be further optimized, such as responsive support, mobile support, animation effects, multi-y-axis support, display content effects, and polyline function support.
The above is the entire content of this article. I hope it will be helpful to everyone’s study. I also hope everyone will support VeVb Wulin Network.