canvas(六)绘制带说明的饼图

1.前言

  • 将以下数据渲染成饼图,数据格式:
var data = [
    {value:"10",title:"16-22的年龄人数"},
    {value:"15",title:"23-30的年龄人数"},
    {value:"25",title:"31-35的年龄人数"},
    {value:"10",title:"36及以上的年龄人数"}
]

2.代码

  • 实例化传入dom,内部变量初始化
  • 调用渲染方法,传入数据
  • 接收传入的数据,进行初始化(区间占比,起始角度,结束角度,颜色)
  • 渲染扇形模块
  • 渲染图例模块
  • 渲染延长线和文字模块
<script>
    //构造函数
    var PieChart = function(dom) {
        //初始化
        this.init(dom)
    }

    //初始化方法
    PieChart.prototype.init = function(dom){
        //初始化dom
        var dom = dom || document.querySelector('canvas')
        //初始化绘图工具
        this.ctx = dom && dom.getContext("2d")

        //以下配置暂时写死
        //获取画布宽高
        this.w = this.ctx.canvas.width
        this.h = this.ctx.canvas.height
        //饼图的圆心和半径
        this.x0 = this.w / 2
        this.y0 = this.h / 2
        this.r = 150 //半径
        //延长线的长度
        this.outLineWidth = this.r + 20
        //图例大小及间距
        this.legend_width = 30
        this.legend_height = 16
        this.legend_space = 10

        //初始化文本对其方式
        this.ctx.textAlign = 'left'

        //设置字体(设置字体要在计算字体宽度之前)
        this.ctx.font = '12px 微软雅黑'
    }


    //接收数据,执行渲染
    PieChart.prototype.render = function(data = []){
        //缓存数据
        this._data = JSON.parse(JSON.stringify(data))
        this.data = JSON.parse(JSON.stringify(data))
        //区间信息初始化
        this.dataInit()
        //绘制扇形
        this.drawSector()
        //绘制图例
        this.drawLegend()
        //绘制延长线及说明
        this.drawText()
    }


    //获取随机颜色
    PieChart.prototype.getRandomColor = function() {
        var r = Math.round(Math.random() * 255)
        var g = Math.round(Math.random() * 255)
        var b = Math.round(Math.random() * 255)
        return 'rgb(' + r + ',' + g + ',' + b + ')'
    }

    //为区间挂载角度信息
    PieChart.prototype.dataInit = function() {
        //计算总数
        var total = data.reduce((total,item) => {
            return total + Number(item.value)
        },0)

        //遍历前数据
        var startRadian = 0 //开始角度
        var endRadian = 0 //结束角度
        //遍历数据
        for(var i=0;i<this.data.length;i++){
            //占比
            var proportion = this.data[i].value/total
            //区间所占角度
            var radian = Math.PI * 2 * proportion
            //开始角度 = 上一次结束角度
            startRadian = endRadian
            //结束角度 = 开始角度 + 区间所占角度
            endRadian = startRadian + radian
            //颜色
            var color = this.getRandomColor()
            //挂载到数据中
            this.data[i].proportion = proportion
            this.data[i].radian = radian
            this.data[i].startRadian = startRadian
            this.data[i].endRadian = endRadian
            this.data[i].color = color
        }
    }

    //绘制扇形
    PieChart.prototype.drawSector = function(){
        //遍历数据
        for(var i=0;i<this.data.length;i++){
            //当前区间数据
            var section_data = this.data[i]
            //开启路径
            this.ctx.beginPath()
            //设置填充颜色
            this.ctx.fillStyle = this.data[i].color
            //绘制曲线(角度直接读取data)
            this.ctx.arc(this.x0, this.y0, this.r, section_data.startRadian, section_data.endRadian)
            //延时到原点,以便绘制扇形
            this.ctx.lineTo(this.x0, this.y0)
            //填充
            this.ctx.fill()
        }
    }

    //绘制图例
    PieChart.prototype.drawLegend = function(){
        //初始化垂直对其方式
        this.ctx.textBaseline = 'middle'
        //默认一行一个图例,一次往下移动
        var item_center_y = -this.legend_height //当前图例y轴坐标
        //提示
        //遍历数据
        for(var i=0;i<this.data.length;i++){
            //开启新路径
            this.ctx.beginPath()
            //设置颜色
            this.ctx.fillStyle = this.data[i].color
            //标题
            var title = this.data[i].title
            //当前y轴坐标(依次往下移动)
            item_center_y += (this.legend_space + this.legend_height)
            //绘制矩形
            this.ctx.fillRect(this.legend_space, item_center_y, this.legend_width, this.legend_height)
            //绘制文字,文字的基点y坐标为矩形的垂直方向的中心
            this.ctx.fillText(title, this.legend_space + this.legend_width + 10, item_center_y + this.legend_height/2)
        }
        //还原垂直对其方式
        this.ctx.textBaseline = 'baseline'
    }

    //绘制延长线及说明
    PieChart.prototype.drawText = function(){
        //初始化垂直对其方式
        this.ctx.textBaseline = 'bottom'
        //遍历数据
        for(var i=0;i<this.data.length;i++){
            //当前区间数据
            var section_data = this.data[i]
            //标题内容
            var title = section_data.title
            
            //计算中心角度
            var middleRadian = (section_data.startRadian + section_data.endRadian) / 2
            //延长线坐标(起点坐标为圆心,终点坐标根据正弦 余弦计算)
            var outX = this.x0 + this.outLineWidth * Math.cos(middleRadian)
            var outY = this.y0 + this.outLineWidth * Math.sin(middleRadian)

            //优化文字对齐方式
            if (outX >= this.x0) {
                //在右边
                var underlineX = outX + this.ctx.measureText(title).width
                this.ctx.textAlign = 'left'
            } else {
                //在左边
                var underlineX = outX - this.ctx.measureText(title).width
                this.ctx.textAlign = 'right'
            }

            //绘制延长线
            //开启路径
            this.ctx.beginPath()
            //设置颜色
            this.ctx.strokeStyle = this.data[i].color
            this.ctx.fillStyle = this.data[i].color
            //绘制直线
            this.ctx.moveTo(this.x0, this.y0)
            this.ctx.lineTo(outX, outY)
            //绘制下划线
            this.ctx.lineTo(underlineX, outY)
            this.ctx.stroke()

            //绘制文本
            this.ctx.beginPath()
            this.ctx.fillText(title, outX, outY)
        }

        //还原垂直对其方式
        this.ctx.textBaseline = 'baseline'
    }
</script>

3.调用

  • 数据随机生成
<script>
    var box = document.querySelector("canvas")

    var data = [
        {value:0,title:"16-22的年龄人数"},
        {value:0,title:"23-30的年龄人数"},
        {value:0,title:"31-35的年龄人数"},
        {value:0,title:"36及以上的年龄人数"}
    ]

    //模拟数据(产生50个数据)
    var num = 50
    for(var i=0;i<num;i++){
        //随机产生16-40的整数
        var age = Math.floor(16 + Math.random()*25)
        if(age < 23){
            data[0].value++
        }else if(age < 31){
            data[1].value++
        }else if(age < 36){
            data[2].value++
        }else{
            data[3].value++
        }
    }

    //创建饼图对象
    var pieChart = new PieChart(box)
    //传入数据开始绘制饼图
    pieChart.render(data)
</script>

4.效果图

5.ES6类语法

<script>
    class PieChart{
        //构造器
        constructor(dom){
            //初始化
            this.init(dom)
        }

        //初始化方法
        init(dom){
            //初始化dom
            var dom = dom || document.querySelector('canvas')
            //初始化绘图工具
            this.ctx = dom && dom.getContext("2d")

            //以下配置暂时写死
            //获取画布宽高
            this.w = this.ctx.canvas.width
            this.h = this.ctx.canvas.height
            //饼图的圆心和半径
            this.x0 = this.w / 2
            this.y0 = this.h / 2
            this.r = 150 //半径
            //延长线的长度
            this.outLineWidth = this.r + 20
            //图例大小及间距
            this.legend_width = 30
            this.legend_height = 16
            this.legend_space = 10

            //初始化文本对其方式
            this.ctx.textAlign = 'left'

            //设置字体(设置字体要在计算字体宽度之前)
            this.ctx.font = '12px 微软雅黑'
        }


        //接收数据,执行渲染
        render(data = []){
            //缓存数据
            this._data = JSON.parse(JSON.stringify(data))
            this.data = JSON.parse(JSON.stringify(data))
            //区间信息初始化
            this.dataInit()
            //绘制扇形
            this.drawSector()
            //绘制图例
            this.drawLegend()
            //绘制延长线及说明
            this.drawText()
        }


        //获取随机颜色
        getRandomColor() {
            var r = Math.round(Math.random() * 255)
            var g = Math.round(Math.random() * 255)
            var b = Math.round(Math.random() * 255)
            return 'rgb(' + r + ',' + g + ',' + b + ')'
        }

        //为区间挂载角度信息
        dataInit() {
            //计算总数
            var total = data.reduce((total,item) => {
                return total + Number(item.value)
            },0)

            //遍历前数据
            var startRadian = 0 //开始角度
            var endRadian = 0 //结束角度
            //遍历数据
            for(var i=0;i<this.data.length;i++){
                //占比
                var proportion = this.data[i].value/total
                //区间所占角度
                var radian = Math.PI * 2 * proportion
                //开始角度 = 上一次结束角度
                startRadian = endRadian
                //结束角度 = 开始角度 + 区间所占角度
                endRadian = startRadian + radian
                //颜色
                var color = this.getRandomColor()
                //挂载到数据中
                this.data[i].proportion = proportion
                this.data[i].radian = radian
                this.data[i].startRadian = startRadian
                this.data[i].endRadian = endRadian
                this.data[i].color = color
            }
        }

        //绘制扇形
        drawSector(){
            //遍历数据
            for(var i=0;i<this.data.length;i++){
                //当前区间数据
                var section_data = this.data[i]
                //开启路径
                this.ctx.beginPath()
                //设置填充颜色
                this.ctx.fillStyle = this.data[i].color
                //绘制曲线(角度直接读取data)
                this.ctx.arc(this.x0, this.y0, this.r, section_data.startRadian, section_data.endRadian)
                //延时到原点,以便绘制扇形
                this.ctx.lineTo(this.x0, this.y0)
                //填充
                this.ctx.fill()
            }
        }

        //绘制图例
        drawLegend(){
            //初始化垂直对其方式
            this.ctx.textBaseline = 'middle'
            //默认一行一个图例,一次往下移动
            var item_center_y = -this.legend_height //当前图例y轴坐标
            //提示
            //遍历数据
            for(var i=0;i<this.data.length;i++){
                //开启新路径
                this.ctx.beginPath()
                //设置颜色
                this.ctx.fillStyle = this.data[i].color
                //标题
                var title = this.data[i].title
                //当前y轴坐标(依次往下移动)
                item_center_y += (this.legend_space + this.legend_height)
                //绘制矩形
                this.ctx.fillRect(this.legend_space, item_center_y, this.legend_width, this.legend_height)
                //绘制文字,文字的基点y坐标为矩形的垂直方向的中心
                this.ctx.fillText(title, this.legend_space + this.legend_width + 10, item_center_y + this.legend_height/2)
            }
            //还原垂直对其方式
            this.ctx.textBaseline = 'baseline'
        }

        //绘制延长线及说明
        drawText(){
            //初始化垂直对其方式
            this.ctx.textBaseline = 'bottom'
            //遍历数据
            for(var i=0;i<this.data.length;i++){
                //当前区间数据
                var section_data = this.data[i]
                //标题内容
                var title = section_data.title
                
                //计算中心角度
                var middleRadian = (section_data.startRadian + section_data.endRadian) / 2
                //延长线坐标(起点坐标为圆心,终点坐标根据正弦 余弦计算)
                var outX = this.x0 + this.outLineWidth * Math.cos(middleRadian)
                var outY = this.y0 + this.outLineWidth * Math.sin(middleRadian)

                //优化文字对齐方式
                if (outX >= this.x0) {
                    //在右边
                    var underlineX = outX + this.ctx.measureText(title).width
                    this.ctx.textAlign = 'left'
                } else {
                    //在左边
                    var underlineX = outX - this.ctx.measureText(title).width
                    this.ctx.textAlign = 'right'
                }

                //绘制延长线
                //开启路径
                this.ctx.beginPath()
                //设置颜色
                this.ctx.strokeStyle = this.data[i].color
                this.ctx.fillStyle = this.data[i].color
                //绘制直线
                this.ctx.moveTo(this.x0, this.y0)
                this.ctx.lineTo(outX, outY)
                //绘制下划线
                this.ctx.lineTo(underlineX, outY)
                this.ctx.stroke()

                //绘制文本
                this.ctx.beginPath()
                this.ctx.fillText(title, outX, outY)
            }

            //还原垂直对其方式
            this.ctx.textBaseline = 'baseline'
        }
    }
</script>
posted @ 2019-10-10 11:27  ---空白---  阅读(971)  评论(0编辑  收藏  举报