如下图,最近项目中需要这么个树形结构可视化数据图形,找了好多可视化插件,没有找到可用的,所以就自己画了一个,代码如下。
- 树形分支是后端接口返回数据渲染,可展示多条;
- 代码可拓展,可封装;
- 点击节点可查看备注;
<canvas id="canvas" width="750" height="800"></canvas>
const canvas_options={ canvasWidth: 750, canvasHeight: 800, chartZone: [70,70,750,570], //坐标绘制区域 yAxisLabel: ['0%','10%','20%','30%','40%','50%','60%','70%','80%','90%','100%'],//y轴坐标text yAxisLabelWidth: 70,//y轴最大宽度 yAxisLabelMax: 100,//y轴最大值 middleLine: 410, //中间线 pillarWidth: 10,//柱子宽度 distanceBetween: 50,//柱状图绘制区域距离两边间隙 pillar: [120,70,700,750],//柱状图绘制区域 mainTrunkHeight: 90,//底部开始主干高度 dialogWidth: 300,//弹窗宽度 dialogLineHeight: 30,//弹窗高度 dialogDrawLineMax: 4, } const nodeClick = []; var chooseNode = null; const datalist={ showDataInfo: { city:[ { name: '项目1', status: 1, //状态:0已完成 1进行中 node: [ { value: 10, date: '2020-03-12 15:50:02', content: '用于组织信息和操作,通常也作为详细信息的入口。' }, { value: 20, date: '2020-03-12 15:50:02', content: '用于组织信息和操作,通常也作为详细信息的入口。' }, ] }, { name: '项目2', status: 0, //状态:0已完成 1进行中 node: [ { value: 10, date: '2020-03-12 15:50:02', content: '用于组织信息和操作,通常也作为详细信息的入口。' }, { value: 50, date: '2020-03-12 15:50:02', content: '用于组织信息和操作,通常也作为详细信息的入口。' }, { value: 100, date: '2020-03-12 15:50:02', content: '用于组织信息和操作,通常也作为详细信息的入口。' }, ] }, { name: '项目3', status: 1, //状态:0已完成 1进行中 node: [ { value: 20, date: '2020-03-12 15:50:02', content: '用于组织信息和操作,通常也作为详细信息的入口。' }, { value: 30, date: '2020-03-12 15:50:02', content: '用于组织信息和操作,通常也作为详细信息的入口。' }, { value: 40, date: '2020-03-12 15:50:02', content: '用于组织信息和操作,通常也作为详细信息的入口。' }, ] }, { name: '项目4', status: 1, //状态:0已完成 1进行中 node: [ { value: 20, date: '2020-03-12 15:50:02', content: '用于组织信息和操作,通常也作为详细信息的入口。' }, { value: 30, date: '2020-03-12 15:50:02', content: '用于组织信息和操作,通常也作为详细信息的入口。' }, ] }, ] } } const canvas = document.getElementById("canvas"); const ctx = canvas.getContext('2d'); ctx.save(); drawYLabel(canvas_options,ctx); //绘制y轴坐标 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轴坐标 drawStartButton(ctx,canvas_options); drawData(ctx,datalist.showDataInfo,canvas_options); chooseNode = null } //判断点击节点 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:需要绘制的文本内容; drawX:绘制文本的x坐标; drawY:绘制文本的y坐标; //lineMaxWidth:每行文本的最大宽度 function textPrewrap(ctx,content,drawX, drawY, lineMaxWidth){ var drawTxt=''; //当前绘制的内容 var drawLine = 1;//第几行开始绘制 var drawIndex=0;//当前绘制内容的索引 //判断内容是够可以一行绘制完毕 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{ //内容绘制完毕,但是剩下的内容宽度不到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轴坐标 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"); //绘制坐标文字 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); //绘制状态 ctx.beginPath(); ctx.fillStyle="#333"; ctx.font="24px ''"; ctx.textAlign = "left"; ctx.fillText("已完成",0,options.canvasHeight-100); ctx.fillText("进行中",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(); //绘制蓝色按钮 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(); } //封装绘制圆角矩形函数 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() } //绘制数据 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]); //贝塞尔曲线起始点 ctx.lineTo(options.middleLine,options.canvasHeight-50-options.mainTrunkHeight); //贝塞尔曲线中间竖线 ctx.quadraticCurveTo(x0,options.canvasHeight-50-options.mainTrunkHeight,x0,options.chartZone[3]); //绘制柱状图进度 ctx.lineTo(x0,y0); ctx.stroke(); //绘制文字 ctx.font="28px ''"; ctx.textAlign='center'; ctx.fillStyle="#333"; ctx.fillText(item.name,x0,options.chartZone[1]-20); //绘制节点 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); }) }) }
广告合作:本站广告合作请联系QQ:858582 申请时备注:广告合作(否则不回)
免责声明:本站资源来自互联网收集,仅供用于学习和交流,请遵循相关法律法规,本站一切资源不代表本站立场,如有侵权、后门、不妥请联系本站删除!
免责声明:本站资源来自互联网收集,仅供用于学习和交流,请遵循相关法律法规,本站一切资源不代表本站立场,如有侵权、后门、不妥请联系本站删除!
暂无评论...
《魔兽世界》大逃杀!60人新游玩模式《强袭风暴》3月21日上线
暴雪近日发布了《魔兽世界》10.2.6 更新内容,新游玩模式《强袭风暴》即将于3月21 日在亚服上线,届时玩家将前往阿拉希高地展开一场 60 人大逃杀对战。
艾泽拉斯的冒险者已经征服了艾泽拉斯的大地及遥远的彼岸。他们在对抗世界上最致命的敌人时展现出过人的手腕,并且成功阻止终结宇宙等级的威胁。当他们在为即将于《魔兽世界》资料片《地心之战》中来袭的萨拉塔斯势力做战斗准备时,他们还需要在熟悉的阿拉希高地面对一个全新的敌人──那就是彼此。在《巨龙崛起》10.2.6 更新的《强袭风暴》中,玩家将会进入一个全新的海盗主题大逃杀式限时活动,其中包含极高的风险和史诗级的奖励。
《强袭风暴》不是普通的战场,作为一个独立于主游戏之外的活动,玩家可以用大逃杀的风格来体验《魔兽世界》,不分职业、不分装备(除了你在赛局中捡到的),光是技巧和战略的强弱之分就能决定出谁才是能坚持到最后的赢家。本次活动将会开放单人和双人模式,玩家在加入海盗主题的预赛大厅区域前,可以从强袭风暴角色画面新增好友。游玩游戏将可以累计名望轨迹,《巨龙崛起》和《魔兽世界:巫妖王之怒 经典版》的玩家都可以获得奖励。
更新日志
2024年12月29日
2024年12月29日
- 小骆驼-《草原狼2(蓝光CD)》[原抓WAV+CUE]
- 群星《欢迎来到我身边 电影原声专辑》[320K/MP3][105.02MB]
- 群星《欢迎来到我身边 电影原声专辑》[FLAC/分轨][480.9MB]
- 雷婷《梦里蓝天HQⅡ》 2023头版限量编号低速原抓[WAV+CUE][463M]
- 群星《2024好听新歌42》AI调整音效【WAV分轨】
- 王思雨-《思念陪着鸿雁飞》WAV
- 王思雨《喜马拉雅HQ》头版限量编号[WAV+CUE]
- 李健《无时无刻》[WAV+CUE][590M]
- 陈奕迅《酝酿》[WAV分轨][502M]
- 卓依婷《化蝶》2CD[WAV+CUE][1.1G]
- 群星《吉他王(黑胶CD)》[WAV+CUE]
- 齐秦《穿乐(穿越)》[WAV+CUE]
- 发烧珍品《数位CD音响测试-动向效果(九)》【WAV+CUE】
- 邝美云《邝美云精装歌集》[DSF][1.6G]
- 吕方《爱一回伤一回》[WAV+CUE][454M]