在生活中我們各種曲線圖、折線圖和扇形圖等等都是在企業(yè)中不可以或缺的一部分對(duì)于做數(shù)據(jù)分析的小伙伴來說。那么大家知道“在前端中怎么繪制樹形結(jié)構(gòu)的圖形?”這個(gè)問題該怎么實(shí)現(xiàn)嗎?下面小編就帶大家來分享一下!
如下圖,最近項(xiàng)目中需要這么個(gè)樹形結(jié)構(gòu)可視化數(shù)據(jù)圖形,找了好多可視化插件,沒有找到可用的,所以就自己畫了一個(gè),代碼如下。
- 樹形分支是后端接口返回?cái)?shù)據(jù)渲染,可展示多條;
- 代碼可拓展,可封裝;
- 點(diǎn)擊節(jié)點(diǎn)可查看備注;
<canvas id="canvas" width="750" height="800"></canvas>
const canvas_options={
canvasWidth: 750,
canvasHeight: 800,
chartZone: [70,70,750,570], //坐標(biāo)繪制區(qū)域
yAxisLabel: ['0%','10%','20%','30%','40%','50%','60%','70%','80%','90%','100%'],//y軸坐標(biāo)text
yAxisLabelWidth: 70,//y軸最大寬度
yAxisLabelMax: 100,//y軸最大值
middleLine: 410, //中間線
pillarWidth: 10,//柱子寬度
distanceBetween: 50,//柱狀圖繪制區(qū)域距離兩邊間隙
pillar: [120,70,700,750],//柱狀圖繪制區(qū)域
mainTrunkHeight: 90,//底部開始主干高度
dialogWidth: 300,//彈窗寬度
dialogLineHeight: 30,//彈窗高度
dialogDrawLineMax: 4,
}
const nodeClick = [];
var chooseNode = null;
const datalist={
showDataInfo: {
city:[
{
name: '項(xiàng)目1',
status: 1, //狀態(tài):0已完成 1進(jìn)行中
node: [
{ value: 10, date: '2020-03-12 15:50:02', content: '用于組織信息和操作,通常也作為詳細(xì)信息的入口。' },
{ value: 20, date: '2020-03-12 15:50:02', content: '用于組織信息和操作,通常也作為詳細(xì)信息的入口。' },
]
},
{
name: '項(xiàng)目2',
status: 0, //狀態(tài):0已完成 1進(jìn)行中
node: [
{ value: 10, date: '2020-03-12 15:50:02', content: '用于組織信息和操作,通常也作為詳細(xì)信息的入口。' },
{ value: 50, date: '2020-03-12 15:50:02', content: '用于組織信息和操作,通常也作為詳細(xì)信息的入口。' },
{ value: 100, date: '2020-03-12 15:50:02', content: '用于組織信息和操作,通常也作為詳細(xì)信息的入口。' },
]
},
{
name: '項(xiàng)目3',
status: 1, //狀態(tài):0已完成 1進(jìn)行中
node: [
{ value: 20, date: '2020-03-12 15:50:02', content: '用于組織信息和操作,通常也作為詳細(xì)信息的入口。' },
{ value: 30, date: '2020-03-12 15:50:02', content: '用于組織信息和操作,通常也作為詳細(xì)信息的入口。' },
{ value: 40, date: '2020-03-12 15:50:02', content: '用于組織信息和操作,通常也作為詳細(xì)信息的入口。' },
]
},
{
name: '項(xiàng)目4',
status: 1, //狀態(tài):0已完成 1進(jìn)行中
node: [
{ value: 20, date: '2020-03-12 15:50:02', content: '用于組織信息和操作,通常也作為詳細(xì)信息的入口。' },
{ value: 30, date: '2020-03-12 15:50:02', content: '用于組織信息和操作,通常也作為詳細(xì)信息的入口。' },
]
},
]
}
}
const canvas = document.getElementById("canvas");
const ctx = canvas.getContext('2d');
ctx.save();
drawYLabel(canvas_options,ctx); //繪制y軸坐標(biāo)
drawStartButton(ctx,canvas_options);
drawData(ctx,datalist.showDataInfo,canvas_options);
canvas.addEventListener("click",event=>{
//清除之前的彈窗
if(chooseNode!=null){
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.save();
drawYLabel(canvas_options,ctx); //繪制y軸坐標(biāo)
drawStartButton(ctx,canvas_options);
drawData(ctx,datalist.showDataInfo,canvas_options);
chooseNode = null
}
//判斷點(diǎn)擊節(jié)點(diǎn)
let rect = canvas.getBoundingClientRect();
let zoom = rect.width/canvas_options.canvasWidth;
let x = (event.clientX/zoom - rect.left/zoom).toFixed(2);
let y = (event.clientY/zoom - rect.top/zoom).toFixed(2);
for(var t=0;t<nodeClick.length;t++){
ctx.beginPath();
ctx.arc(nodeClick[t].x,nodeClick[t].y,15,0,Math.PI*2,true);
if(ctx.isPointInPath(x, y)){
textPrewrap(ctx,`備注描述:${nodeClick[t].date}`,nodeClick[t].x+20,nodeClick[t].y+20,canvas_options.dialogWidth-40);
ctx.restore();
chooseNode=t
break;
}else{
chooseNode=null
}
}
});
//content:需要繪制的文本內(nèi)容; drawX:繪制文本的x坐標(biāo); drawY:繪制文本的y坐標(biāo);
//lineMaxWidth:每行文本的最大寬度
function textPrewrap(ctx,content,drawX, drawY, lineMaxWidth){
var drawTxt=''; //當(dāng)前繪制的內(nèi)容
var drawLine = 1;//第幾行開始繪制
var drawIndex=0;//當(dāng)前繪制內(nèi)容的索引
//判斷內(nèi)容是夠可以一行繪制完畢
if(ctx.measureText(content).width<=lineMaxWidth){
drawDialog(ctx,canvas_options.dialogWidth,canvas_options.dialogLineHeight,drawX,drawY);
ctx.fillText(content.substring(drawIndex,i),drawX.drawY);
}else{
for(var i=0;i<content.length;i++){
drawTxt += content[i];
if(ctx.measureText(drawTxt).width>=lineMaxWidth){
drawDialog(ctx,canvas_options.dialogWidth,canvas_options.dialogLineHeight,drawX,drawY);
ctx.fillText(content.substring(drawIndex,i+1),drawX,drawY);
drawIndex = i+1;
drawLine+=1;
//drawY+=lineHeight;
drawTxt='';
}else{
//內(nèi)容繪制完畢,但是剩下的內(nèi)容寬度不到lineMaxWidth
if(i===content.length-1){
drawDialog(ctx,canvas_options.dialogWidth,canvas_options.dialogLineHeight,drawX,drawY);
ctx.fillText(content.substring(drawIndex,i+1),drawX,drawY)
}
}
}
}
}
function drawDialog(ctx,width,height,x,y){
ctx.beginPath();
ctx.fillStyle="rgba(0,0,0,0.8)";
ctx.fillRect(x,y,width,height);
ctx.font="22px ''";
ctx.fillStyle="#fff";
ctx.textAlign = 'left';
ctx.textBaseline="top";
}
//繪制y軸坐標(biāo)
function drawYLabel(options,ctx){
let labels = options.yAxisLabel;
let yLength = (options.chartZone[3]-options.chartZone[1])*0.98;
let gap = yLength/(labels.length-1);
labels.forEach((item,index)=>{
//繪制圓角背景
//this.radiusButton(ctx,0,options.chartZone[3]-index*gap-13,50,24,8,"#313947");
//繪制坐標(biāo)文字
ctx.beginPath();
ctx.fillStyle="#878787";
ctx.font="18px ''";
ctx.textAlign="center";
ctx.fillText(item,25,options.chartZone[3]-index*gap+5);
//繪制輔助線
ctx.beginPath();
ctx.strokeStyle="#eaeaea";
ctx.strokeWidth=2;
ctx.moveTo(options.chartZone[0],options.chartZone[3]-index*gap);
ctx.lineTo(options.chartZone[2],options.chartZone[3]-index*gap);
ctx.stroke();
})
}
//繪制開始按鈕
function drawStartButton(ctx,options){
//繪制按鈕圖形
this.radiusButton(ctx,options.middleLine-(160/2),options.canvasHeight-50,160,50,8,'#F4C63D');
ctx.fillStyle="#fff";
ctx.font="24px ''";
ctx.textAlign="center";
ctx.fillText('開始',options.middleLine,options.canvasHeight-15);
//繪制狀態(tài)
ctx.beginPath();
ctx.fillStyle="#333";
ctx.font="24px ''";
ctx.textAlign = "left";
ctx.fillText("已完成",0,options.canvasHeight-100);
ctx.fillText("進(jìn)行中",0,options.canvasHeight-50);
//繪制紅色按鈕
ctx.beginPath();
ctx.fillStyle="#d35453";
ctx.arc(options.chartZone[0]+30,options.canvasHeight-100-7,8,0,2 * Math.PI,true);
ctx.fill();
ctx.beginPath();
ctx.strokeStyle="#d35453";
ctx.arc(options.chartZone[0]+30,options.canvasHeight-100-7,14,0,2 * Math.PI,true);
ctx.stroke();
//繪制藍(lán)色按鈕
ctx.beginPath();
ctx.fillStyle="#24b99a";
ctx.arc(options.chartZone[0]+30,options.canvasHeight-50-8,8,0,2 * Math.PI,true);
ctx.fill();
ctx.beginPath();
ctx.strokeStyle="#24b99a";
ctx.arc(options.chartZone[0]+30,options.canvasHeight-50-8,14,0,2 * Math.PI,true);
ctx.stroke();
}
//封裝繪制圓角矩形函數(shù)
function radiusButton(ctx,x,y,width,height,radius,color_back){
ctx.beginPath();
ctx.fillStyle= color_back
ctx.moveTo(x,y+radius);
ctx.lineTo(x,y+height-radius);
ctx.quadraticCurveTo(x,y+height,x+radius,y+height);
ctx.lineTo(x+width-radius,y+height);
ctx.quadraticCurveTo(x+width,y+height,x+width,y+height-radius);
ctx.lineTo(x+width,y+radius);
ctx.quadraticCurveTo(x+width,y,x+width-radius,y);
ctx.lineTo(x+radius,y);
ctx.quadraticCurveTo(x,y,x,y+radius);
ctx.fill()
}
//繪制數(shù)據(jù)
function drawData(ctx,data,options){
//const paths=[];
let number = data.city.length;
//繪制矩形
data.city.forEach((item,index)=>{
let indexVal = number==1?1:index;
let numberVal = number==1?2:number-1
let x0 = options.chartZone[0]+options.distanceBetween+(options.chartZone[2]-options.chartZone[0]-options.distanceBetween*2)/numberVal*indexVal;
let value = item.node[item.node.length-1].value;
let height = (value/options.yAxisLabelMax*(options.chartZone[3]-options.chartZone[0])*0.98).toFixed(2);
let y0=options.chartZone[3] - height;
//柱狀圖底部
ctx.beginPath();
ctx.fillStyle= '#eee';
ctx.fillRect(x0-5,80,options.pillarWidth,options.chartZone[3]-80);
//貝塞爾曲線
ctx.beginPath();
ctx.strokeStyle = item.status==0?"#d35453":'#24b99a';
ctx.lineWidth=options.pillarWidth;
ctx.moveTo(options.middleLine,options.pillar[3]); //貝塞爾曲線起始點(diǎn)
ctx.lineTo(options.middleLine,options.canvasHeight-50-options.mainTrunkHeight); //貝塞爾曲線中間豎線
ctx.quadraticCurveTo(x0,options.canvasHeight-50-options.mainTrunkHeight,x0,options.chartZone[3]);
//繪制柱狀圖進(jìn)度
ctx.lineTo(x0,y0);
ctx.stroke();
//繪制文字
ctx.font="28px ''";
ctx.textAlign='center';
ctx.fillStyle="#333";
ctx.fillText(item.name,x0,options.chartZone[1]-20);
//繪制節(jié)點(diǎn)
item.node.forEach((node_item,node_index)=>{
let y1= options.chartZone[3] - (node_item.value/options.yAxisLabelMax*(options.chartZone[3]-options.chartZone[0])*0.98).toFixed(2);
ctx.beginPath();
ctx.arc(x0,y1,15,0,Math.PI*2,true);
ctx.fillStyle="rgba(108,212,148,1)";
ctx.fill();
ctx.beginPath();
ctx.arc(x0,y1,9,0,Math.PI*2,true);
ctx.fillStyle="rgba(255,255,255,0.8)";
ctx.fill();
const pointInfo={
x:x0,
y:y1,
date: node_item.data,
content: node_item.content,
value: node_item.value
};
nodeClick.push(pointInfo);
})
})
}
那么通過這篇文章的說明和講解相信大家對(duì)于:“在前端中怎么繪制樹形結(jié)構(gòu)的圖形?”這方面的相關(guān)內(nèi)容有了不少的了解!如果大家對(duì)于html5這方面有感興趣的話,都是可以在W3Cschool進(jìn)行一個(gè)全面的學(xué)習(xí),也希望這篇文章對(duì)大家的問題有所幫助!