canvas[学习笔记四]

170 阅读1分钟

图表

1、饼图

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>饼图</title>
    <style>
        body{margin: 0;overflow: hidden}
        #canvas{background: antiquewhite;}
    </style>
</head>
<body>
<canvas id="canvas"></canvas>
<script>
    const canvas=document.getElementById('canvas');
    const [width,height]=[window.innerWidth,window.innerHeight];
    canvas.width=width;
    canvas.height=height;
    const  ctx=canvas.getContext('2d');


    /*======== 一,声明必备数据 ==========*/
    //系列标签
    const seriesLabel=['T恤','夹克','护甲背心','迷彩服','防弹衣'];
    //系列数据
    const seriesData=[11121,7355,6277,4253,2425];
    //饼图的圆心位置
    const pos={x:width/2,y:height/2};
    //饼图绘图区半径
    const radius=200;
    //色差
    const colorSpace=Math.floor(150/seriesLabel.length);


    /*======== 二,构建数据 ==========*/
    /*------ 1.计算基本数据 -------*/
    //系列数据总和
    const labelSum=eval(seriesData.join('+'));
    //系列扇形的起始角度和结束角度
    const seriesAngle=[];
    //系列扇形颜色
    const seriesColor=[];
    //系列扇形半径
    const seriesRadius=[];

    //遍历系列数据
    seriesData.forEach((ele,ind)=>{
        //数据比例
        const dataScalar=ele/labelSum;
        //扇形弧长
        const angleLen=dataScalar*Math.PI*2;
        //扇形起始角度
        let startAngle=ind===0?0:seriesAngle[ind-1][1];
        //扇形结束角度
        const endAngle=startAngle+angleLen;
        //系列扇形的起始角度和结束角度
        seriesAngle.push([startAngle,endAngle]);

        //色域
        const gamut=80+ind*colorSpace;
        const color=`rgba(255,${gamut},${gamut},1)`;
        //扇形颜色
        seriesColor.push(color);
        //系列扇形半径
        const itemRadius=radius-ind*10;
        seriesRadius.push(itemRadius);
    });


    /*======== 三,基于数据绘图 ==========*/
    /*------ 扇形  -------*/
    class Sector{
        constructor(radius,start,end,color='chocolate'){
            this.radius=0;
            this.realRad=radius;
            this.expandRad=radius+20;
            this.startAngle=start;
            this.endAngle=end;
            this.color=color;
            this.x=0;
            this.x=0;
            this.text='';
            this.data=0;
            this.textAlign='left';

            this.p1={x:0,y:0};
            this.p2={x:0,y:0};
            this.p3={x:0,y:0};
            this.p4={x:0,y:0};

            this.vr=0;
            this.ar=0.03;
            this.bounce=0.6;

            this.state=0;
        }
        /*更新引导线点位,基于圆形位置、半径、起始弧度,结束弧度
        *   引线方向dir = 起始弧度+扇形弧度的一半
        *   p1到圆心的距离p1Len = 半径+偏移距离22
        *   p1点位,基于方向和长度计算x,y 值
        *   p2点位同理,自半径偏移70
        *   根据p2的x点位和圆心x点位的关系,获取之后点位的绘制方向和文本对齐方式
        *   p3点位,x自p2偏移20,y和p2一致
        *   p4点位,x自p3偏移10,y和p2一致
        * */
        updatePoints(){
            //解构半径,起始弧度,结束弧度,位置
            const {realRad,startAngle,endAngle,x,y,p1,p2,p3,p4}=this;
            const dir=startAngle+(endAngle-startAngle)/2;
            const p1Len=realRad+22;
            p1.x=Math.cos(dir)*p1Len+x;
            p1.y=Math.sin(dir)*p1Len+y;
            const p2Len=realRad+70;
            p2.x=Math.cos(dir)*p2Len+x;
            p2.y=Math.sin(dir)*p2Len+y;
            let d=1;
            if(Math.cos(dir)<0){
                d=-1;
                this.textAlign='right';
            }
            p3.x=p2.x+20*d;
            p3.y=p2.y;
            p4.x=p3.x+10*d;
            p4.y=p2.y;
        }

        /*基于state 状态更新半径
        *   0 初始状态,绘图半径变大到实际半径 expand(时间差,目标半径)
        *   1 鼠标划上,绘图半径变大到扩展半径 expand(时间差,目标半径)
        *   2 鼠标划出,绘图半径缩小到实际半径 shrink(时间差,目标半径)
        * */
        updateRad(diff){
            const {state,realRad,expandRad}=this;
            switch (state) {
                case 0:
                    this.expand(diff,realRad);
                    break;
                case 1:
                    this.expand(diff,expandRad);
                    break;
                case 2:
                    this.shrink(diff,realRad);
                    break;
            }
        }
        /*半径扩展动画expand(时间差,结束状态)
        *   速度+=加速度
        *   半径+=速度*时间差
        *   若半径大于终点
        *       半径=终点
        *       速度反弹
        * */
        expand(diff,endR){
            const {ar,bounce}=this;
            this.vr+=ar;
            this.radius+=this.vr*diff;
            if(this.radius>endR){
                this.radius=endR;
                this.vr*=-bounce;
            }

        }
        /*半径收缩动画shrink(时间差,结束状态)
        *   速度-=加速度
        *   半径+=速度*时间差
        *   若半径小于终点
        *       半径=终点
        *       速度反弹
        * */
        shrink(diff,endR){
            const {ar,bounce}=this;
            this.vr-=ar;
            this.radius+=this.vr*diff;
            if(this.radius<endR){
                this.radius=endR;
                this.vr*=-bounce;
            }
        }

        /*绘图
        *   绘制扇形
        *   绘制引导线
        *   绘制标签文字
        * */
        draw(ctx){
            const {x,y,radius,startAngle,endAngle,color,p1,p2,p3,p4,text,textAlign}=this;
            //保存状态
            ctx.save();
            /*绘制扇形 arc() */
            ctx.beginPath();
            ctx.moveTo(x,y);
            ctx.arc(x,y,radius,startAngle,endAngle);
            ctx.fillStyle=color;
            ctx.fill();
            /*绘制引导线 lineTo() */
            ctx.beginPath();
            ctx.moveTo(p1.x,p1.y);
            ctx.lineTo(p2.x,p2.y);
            ctx.lineTo(p3.x,p3.y);
            ctx.stroke();
            /*绘制标签文字 fillText()*/
            ctx.fillStyle='#000';
            ctx.textBaseline='middle';
            ctx.textAlign=textAlign;
            ctx.font='14px Arial';
            ctx.fillText(text,p4.x,p4.y);
            //还原上一次保存的状态
            ctx.restore();
        }
    }

    /*将扇形添加到系列集合中*/
    const series=[];
    seriesAngle.forEach((ele,ind)=>{
        //实例化扇形
        const item=new Sector(seriesRadius[ind],ele[0],ele[1],seriesColor[ind]);
        item.x=pos.x;
        item.y=pos.y;
        item.text=seriesLabel[ind];
        item.data=seriesData[ind];
        item.updatePoints();
        series.push(item);
    });



    /*======== 四,鼠标交互 ==========*/

    /*------ 建立提示框 Tip -------*/
    class Tip{
        constructor() {
            this.text='';
            this.fontSize=14;
            this.x=0;
            this.y=0;
            this.visible=false;

            this.xn=0;
            this.yn=0;
            this.rate=0.1;
            this.state=0;
        }
        /*更新提示位置
        *   若不可见或者不移动,不执行此方法
        *   获取当前点位相对应下一个点位的距离len
        *   若距离小于0,则不再移动
        *   否则,提示的位置按比例接近下一个点位
        * */
        updatePos(){
            const {x,y,xn,yn,rate,state,visible}=this;
            //若不可见,返回
            if(!this.visible||!state){return}
            /*
            * 如果状态为1
            * 获取起点到终点的插值 subX,subY
            * 计算起点到终点的距离=subX,subY 的平方之和的平方根
            * 若长度小于1,设置state 为0,不再运动
            * 否则设置起点位置,做缓动运动:轴向距离*0.1+起点位
            * */
            const subX=xn-x;
            const subY=yn-y;
            const len=Math.sqrt(subX*subX+subY*subY);
            if(len<1){
                this.state=0;
            }else{
                this.x+=subX*rate;
                this.y+=subY*rate;
            }
        }
        draw(ctx){
            const {x,y,fontSize,visible,text}=this;
            const [padW,padH]=[15,8];
            if(!visible){return}
            ctx.save();
            ctx.font=`${fontSize}px arial`;
            //获取文字宽度
            const {width}=ctx.measureText(text);
            //绘制填充矩形
            ctx.fillStyle='rgba(0,0,0,0.6)';
            ctx.fillRect(x,y,width+padW*2,fontSize+padH*2);
            //绘制文字
            ctx.textBaseline='top';
            ctx.fillStyle='#fff';
            ctx.fillText(text,x+padW,y+padH);
            ctx.restore();
        }
    }
    const tip=new Tip();
    tip.x=pos.x;
    tip.y=pos.y;

    //监听鼠标移动事件
    canvas.addEventListener('mousemove',mousemoveFn);
    function mousemoveFn(event){
        //鼠标位置
        const mousePos=getMousePos(event);
        //当前被鼠标划上的扇形
        let hoveredItem=null;
        //遍历系列
        for (let i=0;i<series.length;i++){
            if(containPoint(series[i],mousePos)){
                series[i].state=1;
                hoveredItem=series[i];
            }else if(series[i].state===1){
                series[i].state=2;
            }
        }
        if(hoveredItem){
            tip.visible=true;
            tip.xn=mousePos.x+10;
            tip.yn=mousePos.y+20;
            tip.state=1;
            tip.text=hoveredItem.data;
        }else{
            tip.visible=false;
        }
    }

    //计时器
    let time=new Date();
    //渲染
    render();
    //渲染方法
    function render(){
        //获取时间差
        const now=new Date();
        const diff=now-time;
        time=now;

        //清理画布
        ctx.clearRect(0,0,width,height);
        //渲染系列图形
        series.forEach((item)=>{
            //更新半径
            item.updateRad(diff);
            //绘制系列图像
            item.draw(ctx);
        });
        //提示
        tip.updatePos();
        tip.draw(ctx);
        //连续渲染
        requestAnimationFrame(render);
    }

    //判断点是否在图形中
    function containPoint(obj,mousePos){
        const {x,y,radius,startAngle,endAngle}=obj;
        //鼠标位减图形位
        const [subX,subY]=[mousePos.x-x,mousePos.y-y];
        //获取鼠标到圆心的距离
        const len=Math.sqrt(subX*subX+subY*subY);
        //判断鼠标到圆心的距离是否小于半径
        const b1=len<radius;
        //获取鼠标位减图形位的方向 dir
        let dir=Math.atan2(subY,subX);
        if(dir<0){dir+=Math.PI*2}
        //判断鼠标向量的方向是否在扇形的夹角之间
        const b2=dir>startAngle&&dir<endAngle;
        return b1&&b2;
    }
    //获取鼠标位置
    function getMousePos(event){
        //获取鼠标位置
        const {clientX,clientY}=event;
        //获取canvas 边界位置
        const {top,left}=canvas.getBoundingClientRect();
        //计算鼠标在canvas 中的位置
        const x=clientX-left;
        const y=clientY-top;
        return {x,y};
    }
</script>
</body>
</html>

饼图完整版.gif