canvas绘制树形结构可视图形的实现


Posted in HTML / CSS onApril 03, 2020

如下图,最近项目中需要这么个树形结构可视化数据图形,找了好多可视化插件,没有找到可用的,所以就自己画了一个,代码如下。

  • 树形分支是后端接口返回数据渲染,可展示多条;
  • 代码可拓展,可封装;
  • 点击节点可查看备注;

canvas绘制树形结构可视图形的实现

<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);
        })
    })
}

到此这篇关于canvas绘制树形结构可视图形的实现的文章就介绍到这了,更多相关canvas树形结构内容请搜索三水点靠木以前的文章或继续浏览下面的相关文章,希望大家以后多多支持三水点靠木!

HTML / CSS 相关文章推荐
css3制作动态进度条以及附加jQuery百分比数字显示
Dec 13 HTML / CSS
一款css实现的鼠标经过按钮的特效
Sep 11 HTML / CSS
HTML5+CSS3 实现灵动的动画 TAB 切换效果(DEMO)
Sep 15 HTML / CSS
详解css3中dispaly的Grid布局与Flex布局
Sep 11 HTML / CSS
CSS3实现彩色进度条动画的示例
Oct 29 HTML / CSS
HTML5的一个显示电池状态的API简介
Jun 18 HTML / CSS
canvas裁剪clip()函数的具体使用
Mar 01 HTML / CSS
详解h5页面在不同ios设备上的问题总结
Mar 01 HTML / CSS
Html5写一个简单的俄罗斯方块小游戏
Dec 03 HTML / CSS
HTML如何让IMG自动适应DIV容器大小的实现方法
Feb 25 HTML / CSS
html form表单基础入门案例讲解
Jul 15 HTML / CSS
HTML基础详解(上)
Oct 16 HTML / CSS
Html5踩坑记之mandMobile使用小记
Apr 02 #HTML / CSS
总结html5自定义属性有哪些
Apr 01 #HTML / CSS
详解canvas.toDataURL()报错的解决方案全都在这了
Mar 31 #HTML / CSS
浅谈Html5页面打开app的一些思考
Mar 30 #HTML / CSS
详解HTML5常用的语义化标签
Sep 27 #HTML / CSS
html5 移动端视频video的android兼容(去除播放控件、全屏)
Mar 26 #HTML / CSS
HTML5 Canvas实现放大镜效果示例
Mar 25 #HTML / CSS
You might like
其他功能
2006/10/09 PHP
ThinkPHP之R方法实例详解
2014/06/20 PHP
php实现图片上传并进行替换操作
2016/03/15 PHP
PHP邮箱验证示例教程
2016/06/01 PHP
magento后台无法登录解决办法的两种方法
2016/12/09 PHP
javascript 极速 隐藏/显示万行表格列只需 60毫秒
2009/03/28 Javascript
javascript权威指南 学习笔记之javascript数据类型
2011/09/24 Javascript
javascript (用setTimeout而非setInterval)
2011/12/28 Javascript
js中AppendChild与insertBefore的用法详细解析
2013/12/16 Javascript
JavaScript编程的10个实用小技巧
2014/04/18 Javascript
jquery列表拖动排列(由项目提取相当好用)
2014/06/17 Javascript
jQuery 动态云标签插件
2014/11/11 Javascript
Bootstrap Paginator分页插件与ajax相结合实现动态无刷新分页效果
2016/05/27 Javascript
jquery 判断selection range 是否在容器中的简单实例
2016/08/02 Javascript
JS图片压缩(pc端和移动端都适用)
2017/01/12 Javascript
JavaScript定时器制作弹窗小广告
2017/02/05 Javascript
Node.js使用gm拼装sprite图片
2017/07/04 Javascript
Node.js pipe实现源码解析
2017/08/12 Javascript
限时抢购-倒计时的完整实例(分享)
2017/09/17 Javascript
使用html+js+css 实现页面轮播图效果(实例讲解)
2017/09/21 Javascript
小程序实现发表评论功能
2018/07/06 Javascript
Vue官方推荐AJAX组件axios.js使用方法详解与API
2018/10/09 Javascript
mongodb初始化并使用node.js实现mongodb操作封装方法
2019/04/02 Javascript
Cordova(ionic)项目实现双击返回键退出应用
2019/09/17 Javascript
Python 抓取动态网页内容方案详解
2014/12/25 Python
python递归打印某个目录的内容(实例讲解)
2017/08/30 Python
Python实现接受任意个数参数的函数方法
2018/04/21 Python
python pcm音频添加头转成Wav格式文件的方法
2019/01/09 Python
python接口自动化之ConfigParser配置文件的使用详解
2020/08/03 Python
使用HTML5里的classList操作CSS类
2016/06/28 HTML / CSS
Shoes For Crews法国官网:美国领先的防滑鞋设计和制造商
2018/01/01 全球购物
Lookfantastic西班牙官网:英国知名美妆购物网站
2018/06/13 全球购物
澳大利亚优质葡萄酒专家:Vintage Cellars
2019/01/08 全球购物
2015年安全生产目标责任书
2015/01/29 职场文书
运动会开幕式通讯稿
2015/07/18 职场文书
公司要求试用期员工提交“述职报告”,该怎么写?
2019/07/17 职场文书