Maison > Article > interface Web > Exemple d'implémentation d'un histogramme dans un graphique en toile en HTML5
Cet article présente principalement l'exemple d'utilisation d'un graphique canevas pour implémenter un histogramme en HTML5. Je pense que c'est plutôt bien. Je vais maintenant le partager avec vous et le donner comme référence.
J'ai utilisé une bibliothèque de graphiques il y a quelques jours, parmi laquelle les ECharts de Baidu semblent être les meilleurs. Ils utilisent Canvas par défaut. Les graphiques Canvas sont meilleurs que les SVG pour traiter le Big Data. Ensuite, j'utiliserai également Canvas pour implémenter une bibliothèque de graphiques. Cela ne semble pas trop difficile. Implémentons d'abord un simple graphique à barres.
L'effet est le suivant :
Les principaux points de fonction incluent :
Dessin du texte
Dessin de l'axe XY
Dessin du regroupement de données ;
Mise en place de l'animation des données ;
Gestion des événements souris.
Méthode d'utilisation
Nous examinons d'abord la méthode d'utilisation. Nous nous référons à certaines méthodes d'utilisation d'ECharts et passons d'abord la balise html. pour afficher le graphique, puis appelez init et transmettez les données lors de l'initialisation.
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] } ] });
Classe de base de graphique. Nous écrirons également des diagrammes circulaires et des graphiques linéaires plus tard, alors extrayez les parties communes. Notez que canvas.style.width et canvas.width sont différents. Le premier étirera les graphiques, tandis que le second est ce que nous utilisons normalement et n'étirera pas les graphiques. Le but de l'écriture ici d'abord d'une expansion puis d'une réduction est de résoudre le problème du flou lors du dessin de texte sur la toile.
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'; } }
Pour initialiser l'histogramme, appelez Object.assign(this,opt) dans es6. Cela équivaut à la méthode extend de JQ, qui copie les propriétés dans l'instance actuelle. Dans le même temps, un attribut tip est également créé, qui est une balise HTML et est utilisé pour afficher les informations sur les données ultérieurement. Dessinez ensuite les graphiques et liez les événements de la souris.
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(){//显示数据 }
Dessinez l'axe XY
Dessinez d'abord le titre, puis l'axe XY, puis parcourez la série de données groupées, qui comporte des calculs complexes, puis dessinez l'échelle de l'axe XY, dessinez l'étiquette du groupe et enfin les données tracées. La série d'éléments de données est constituée de données groupées, qui correspondent à xAxis.data de l'axe X un à un. Chaque élément peut avoir un nom et une couleur personnalisés. S'il n'est pas spécifié, le nom est donné à nunamed et la couleur est automatiquement générée. L'attribut de légende est également utilisé ici pour enregistrer les informations de la liste de balises, car il est utile pour les clics de souris ultérieurs afin de déterminer si le clic est correct.
Principaux points de connaissance de Canvas :
La balise de regroupement utilise la méthode arcTo, afin que l'effet des coins arrondis puisse être dessiné.
Le dessin de texte utilise la méthode MeasureText, qui peut être utilisée pour mesurer la largeur du texte, afin que la position du dessin suivant puisse être ajustée pour éviter les conflits de position.
La méthode de déplacement par translation peut être placée dans le contexte du dessin (entre la sauvegarde et la restauration), ce qui peut éviter des calculs de position complexes.
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); } }
Dessiner des données
Étant donné que l'élément de données doit effectuer des animations ultérieures et afficher du contenu lorsque la souris glisse dessus, placez-le dans File d'attente d'animation animateArr. Ici, nous devons développer les données groupées, convertir les deux tableaux imbriqués précédents en un seul calque et calculer les attributs de chaque élément de données, tels que le nom, la coordonnée x, la coordonnée y, la largeur, la vitesse et la couleur. Une fois les données organisées, l'animation est exécutée.
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(); }
Exécuter l'animation
Il n'y a rien à dire sur l'exécution de l'animation, c'est juste une fonction de fermeture auto-exécutable. Le principe de l'animation est d'accumuler séquentiellement la valeur de vitesse vy sur l'axe y. Mais rappelez-vous que lorsque la file d'attente termine l'exécution de l'animation, elle doit être arrêtée, il y a donc un indicateur isStop, qui est jugé à chaque fois que l'exécution de la file d'attente est terminée.
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); }()) }
Événement de liaison
Événement 1 : lors du déplacement de la souris, vérifiez si la position de la souris est sur l'étiquette du groupe ou l'élément de données, et appelez isPointInPath après avoir tracé le chemin (x,y), si vrai, canvas.style.cursor='pointer'; s'il s'agit d'un élément de données, la colonne doit être redessinée, définir la transparence et différenciée. Le contenu doit également être affiché. Voici un p qui est positionné de manière absolue par rapport au conteneur parent. Il a été établi comme attribut tip lors de l'initialisation. Nous encapsulons la partie affichage dans la méthode showInfo.
Événement 2 : pendant le passage de la souris, déterminez sur quelle étiquette de groupe la souris clique, puis définissez l'attribut hide dans la série de données de groupe correspondante. Si c'est vrai, cela signifie que l'élément ne sera pas affiché, et. puis appelez la méthode draw pour remplacer le rendu et le dessin, exécutez l'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 }; // 分组标签 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'; }
Résumé
Ce qui est fait ici n'est qu'un effet de base. En fait, il reste encore de nombreux domaines qui doivent être encore optimisés, comme le responsive. prise en charge et prise en charge mobile, prise en charge multi-axes Y, effets de contenu d'affichage et prise en charge des fonctions polylignes, etc.
Recommandations associées :
Html réalise l'effet de rapport en affichant dynamiquement des blocs de couleur (exemple de code)
html5 génère un histogramme Exemple de code pour l'effet (graphique à barres)
Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!