canvas 2d 環形統計圖手寫實現示例

正文

其實小程序上面也可以使用 echart 等開源圖表庫得,而且支持代碼包得裁切功能,但是可能我不會用吧,效果不太好,而且我這就一個圖,也沒什麼交互要寫,就手撕瞭一個環形統計圖,也算練習一下 canvas 2d 吧。

說到 canvas 2d 可真是頭疼,微信官方不知道幹嘛吃的,原接口不再維護瞭,但是 canvas 2d 得文檔幾乎沒有更新,寫起來摸不著頭腦。如果你也對 canvas 2d 有疑惑,希望這個環形統計圖能給你點幫助。

下面是 canvas 的官方文檔,api使用也挺重要,可以先瞭解瞭解。

developers.weixin.qq.com/miniprogram…

developers.weixin.qq.com/miniprogram…

先看看效果

中間得環形圖以及裡面的文字就是通過 canvas 2d 繪制出來的,下面看代碼。

看看代碼

  • WXML
<view class="row chart-container">
    <canvas type="2d" class="chart" id="myChart2d" />
    <view class="col center">
        <view class="row-center" wx:for="{{chartData}}" wx:key="chartData" style="margin-top:{{index==0?'0':'16'}}rpx">
            <view class="circle" style="background: {{item.color}}"></view>
            <view class="project-item font-size-12 flex1 row">
                <view>{{item.title}}</view>
                <view class="flex1 margin-left-16"> {{item.numb}}</view>
                <view class="margin-left-16"> {{item.percent}}%</view>
            </view>
        </view>
    </view>
</view>

這裡並不需要多少代碼,但是 type 和 id 一定要,而且記得 class 指定寬高。

  • WXSS
.chart {
    width: 112px;
    height: 112px;
}
.row{
  display:flex; 
  flex-direction:row;
}
.col{
  display:flex; 
  flex-direction:column;
}
.row-center{
  display:flex; 
  flex-direction:row;
  align-items: center;
}
.flex1{
  flex: 1;
}
.center{
  margin: auto;
  width: fit-content;
}
.circle {
    width: 18rpx;
    height: 18rpx;
    border-radius: 9rpx;
    box-sizing: border-box;
}
.project-item {
    font-family: PingFangSC-Regular, PingFang SC;
    font-weight: 400;
    color: #616161;
    line-height: 34rpx;
    margin-left: 8rpx;
}
.margin-left-16{
  margin-left: 16rpx;
}
.font-size-12{
  font-size: 24rpx;
}

這裡就是上面說的指定寬高瞭,暫時先用 px 作為單位,其他不知道會不會有問題。

  • JS
Component({
	properties: {
        show: {
            type: Boolean,
            value: false,
            observer: function (newVal, oldVal) {
                // 首次進來頁面圖標無法加載,監聽頁面切換來顯示
                let isFirstComeIn = this.data.isFirstComeIn
                if (isFirstComeIn) {
                    this.getCanvas()
                    this.data.isFirstComeIn = false
                }
            }
        }
    },
    lifetimes: {
        attached: function () {
        	// 初始化加載數據
            this.getData()
        },
    },
    data: {
        // 畫佈相關
        isFirstComeIn: true,
        context: null,
        height: 0,
        width: 0,
        // 圖表數據
        chartData: [{
            title: '待檢查項目',
            color: '#FF9000',
            numb: 0,
            percent: 0
        }, {
            title: '進行中項目',
            color: '#1FD55C',
            numb: 0,
            percent: 0
        }, {
            title: '已完成項目',
            color: '#0B7BFB',
            numb: 0,
            percent: 0
        }, {
            title: '已終止項目',
            color: '#616161',
            numb: 0,
            percent: 0
        }],
    }
    methods: {
    	getCanvas() {
            // 有的手機下拉刷新會造成畫兩個不同大小的餅圖
            let that = this;
            let query = wx.createSelectorQuery().in(this)
            query.select('#myChart2d')
                .fields({
                    node: true,
                    size: true
                })
                .exec((res) => {
                    const canvas = res[0].node
                    const ctx = canvas.getContext('2d')
                    const dpr = wx.getSystemInfoSync().pixelRatio
                    canvas.width = res[0].width * dpr
                    canvas.height = res[0].height * dpr
                    ctx.scale(dpr, dpr)
                    that.setData({
                        width: res[0].width * dpr,
                        height: res[0].height * dpr,
                        context: ctx
                    })
                    // 首次進來畫圖
                    that.drawPieChart2d()
            })
        },
        // 下拉刷新
        onPullDownRefresh() {
            this.getData()
        },
        // 獲取數據
        getData() {
        	app.request({
                url: 'you/url',
                data: {},
                finish: function () {
                    wx.stopPullDownRefresh();
                },
                success: function (res) {
                	let count = res.undoCount + res.doingCount + res.finishCount + res.stopCount
                    let chartData = that.data.chartData
                    if (count != 0) {
                        chartData[0].numb = res.undoCount
                        chartData[0].percent = (res.undoCount * 100 / count).toFixed(2)
                        chartData[1].numb = res.doingCount
                        chartData[1].percent = (res.doingCount * 100 / count).toFixed(2)
                        chartData[2].numb = res.finishCount
                        chartData[2].percent = (res.finishCount * 100 / count).toFixed(2)
                        chartData[3].numb = res.stopCount
                        chartData[3].percent = (res.stopCount * 100 / count).toFixed(2)
                    } else {
                        chartData[0].numb = 0
                        chartData[0].percent = 0
                        chartData[1].numb = 0
                        chartData[1].percent = 0
                        chartData[2].numb = 0
                        chartData[2].percent = 0
                        chartData[3].numb = 0
                        chartData[3].percent = 0
                    }
                    that.setData({
                        chartData: chartData,
                    })
                    // 因為本頁作為組件隱藏瞭,首次進來無法獲取canvas高度,首次進來另外處理
                    if (!that.data.isFirstComeIn) {
                        that.drawPieChart2d()
                    }
                }
            })    
        }
		// 一次性使用,前面是舊 canvas,註釋的是一次性調用 canvas 2d 代碼
		drawPieChart() {
            // 組件中使用需要增加 this
            const ctx = wx.createCanvasContext('myChart', this);
            //設置半徑 
            let radius = 56;
            let center = {
                x: 56,
                y: 56
            };
            // 設置數據、總數
            let data = this.data.chartData
            let count = 0;
            data.forEach(element => {
                count += element.numb
            });
            for (let i = 0; i < data.length; i++) {
                //計算占比,總長為 2PI
                let start = 0;
                for (let j = 0; j < i; j++) {
                    start += data[j].numb / count * 2 * Math.PI
                }
                var end = start + data[i].numb / count * 2 * Math.PI
                ctx.beginPath()
                ctx.arc(center.x, center.y, radius, start, end)
                ctx.setLineWidth(1)
                ctx.lineTo(center.x, center.y)
                ctx.setStrokeStyle('#fff')
                ctx.setFillStyle(data[i].color)
                ctx.fill();
                ctx.closePath();
                ctx.stroke();
            }
            ctx.beginPath()
            radius = 40;
            ctx.arc(center.x, center.y, radius, 0, 2 * Math.PI)
            ctx.setFillStyle('#fafafa')
            ctx.fill()
            ctx.closePath();
            ctx.stroke();
            ctx.fillStyle = "#2E2E2E";
            ctx.setFontSize(20)
            ctx.setTextAlign('center')
            ctx.fillText('' + count, 56, 50);
            ctx.setFontSize(14)
            ctx.setTextAlign('center')
            ctx.fillText('評估項目數', 56, 70);;
            ctx.draw()
            // let query = wx.createSelectorQuery().in(this)
            // query.select('#myChart2d')
            //     .fields({
            //         node: true,
            //         size: true
            //     })
            //     .exec((res) => {
            //         const canvas = res[0].node
            //         const ctx = canvas.getContext('2d')
            //         const dpr = wx.getSystemInfoSync().pixelRatio
            //         canvas.width = res[0].width * dpr
            //         canvas.height = res[0].height * dpr
            //         ctx.scale(dpr, dpr)
            //         //設置半徑 
            //         let radius = 56;
            //         let center = {
            //             x: 56,
            //             y: 56
            //         };
            //         // 設置數據、總數
            //         let data = this.data.chartData
            //         let count = 0;
            //         data.forEach(element => {
            //             count += element.numb
            //         });
            //         // 開始畫圖
            //         ctx.clearRect(0, 0, res[0].width * dpr, res[0].height * dpr)
            //         for (let i = 0; i < data.length; i++) {
            //             //計算占比,總長為 2PI
            //             let start = 0;
            //             for (let j = 0; j < i; j++) {
            //                 start += data[j].numb / count * 2 * Math.PI
            //             }
            //             var end = start + data[i].numb / count * 2 * Math.PI
            //             ctx.beginPath()
            //             ctx.arc(center.x, center.y, radius, start, end)
            //             ctx.lineWidth = 1
            //             ctx.lineTo(center.x, center.y)
            //             ctx.strokeStyle = '#fff'
            //             ctx.fillStyle = data[i].color
            //             ctx.closePath();
            //             ctx.fill();
            //         }
            //         radius = 40;
            //         ctx.beginPath()
            //         ctx.arc(center.x, center.y, radius, 0, 2 * Math.PI)
            //         ctx.fillStyle = '#fafafa'
            //         ctx.closePath()
            //         ctx.fill()
            //         ctx.fillStyle = "#2E2E2E";
            //         ctx.font = "20px Arial";
            //         ctx.textAlign = 'center'
            //         ctx.fillText('' + count, 56, 50)
            //         ctx.font = "14px Arial";
            //         ctx.fillText('評估項目數', 56, 70)
            //     })
        },
        drawPieChart2d() {
            let ctx = this.data.context
            //設置半徑 
            let radius = 56;
            let center = {
                x: 56,
                y: 56
            };
            // 設置數據、總數
            let data = this.data.chartData
            let count = 0;
            data.forEach(element => {
                count += element.numb
            });
            // 開始畫圖
            ctx.beginPath()
            ctx.clearRect(0, 0, this.data.width, this.data.height);
            for (let i = 0; i < data.length; i++) {
                //計算占比,總長為 2PI
                let start = 0;
                for (let j = 0; j < i; j++) {
                    start += data[j].numb / count * 2 * Math.PI
                }
                var end = start + data[i].numb / count * 2 * Math.PI
                ctx.beginPath()
                ctx.lineWidth = 1
                ctx.strokeStyle = '#fff'
                ctx.fillStyle = data[i].color
                ctx.arc(center.x, center.y, radius, start, end)
                ctx.lineTo(center.x, center.y)
                ctx.closePath();
                ctx.fill();
            }
            radius = 40;
            ctx.beginPath()
            ctx.fillStyle = '#fafafa'
            ctx.arc(center.x, center.y, radius, 0, 2 * Math.PI)
            ctx.closePath()
            ctx.fill()
            ctx.fillStyle = "#2E2E2E";
            ctx.font = "20px Arial";
            ctx.textAlign = 'center'
            ctx.fillText('' + count, 56, 50)
            ctx.font = "14px Arial";
            ctx.fillText('評估項目數', 56, 70)
        },
    } 
})

這裡寫的有些復雜瞭,但是復雜的東西能學到的也多吧,在組件中使用都掌握瞭,在 Page 中使用那就得心應手瞭,下面詳細講講。

繪制圖表

實際上繪制圖表並不需要這麼多的代碼,在Page也好,在組件頁面也好,其實隻需要在需要繪制的時候調用上面 js 中 drawPieChart 代碼即可,前面是舊版本的canvas,後面註釋的是 canvas 2d的寫法,可以對比看看,還是有些去別的,特別是字體大小坑瞭我一把。

但是為什麼要寫這麼多代碼呢?還是解決一些出現的問題,下面詳細介紹。

解決問題

  • 下拉刷新會造成畫兩個不同大小的餅圖

問題很奇怪,而且隻在某些機型出現。仔細研究一下發現這個問題是因為繪制圖表的時候,多次調用一次性生成圖表函數造成的,即每次獲取到的 canvas 對象可能不太一樣瞭,具體什麼不一樣瞭,我就沒有仔細研究瞭,可能是頁面發生瞭變化造成的。

這裡的解決辦法就是隻獲取一次 canvas,後面就用它不停的繪制圖表,當繪需要制新的圖表時,清空原來內容並繪制。首先提取出一個函數獲取 canvas,這個函數要在 page 的 onReady中監聽,這裡再組件中也可以在 lifetimes 的 ready 方法中監聽,都是一樣的。獲取到 canvas 對象後設置為全局變量,後面繪制的的時候取這個變量繪制就可以瞭。

這裡我們把 getCanvas 寫在瞭組件頁面第一次顯示時觸發,原因看下面問題。

  • 首次進來頁面圖表無法加載

這個問題是我們自定義的底部導航欄引入的,因為組件頁面的出現後就被設置成瞭隱藏狀態,所以 canvas 並沒有獲得到寬高,導致圖表不顯示。

解決辦法就是在組件頁面第一次顯示的時候觸發 getCanvas 函數,這裡監聽 show 屬性的寫法可以參考我前面自定義底部導航欄的博客,就不詳述瞭。第一次顯示的問題,用到瞭一個全局變量,一旦觸發瞭,這個變量就永久設置為 false,使 getCanvas 函數不會再次執行。

同時,在第一次獲取數據時因為 canvas 未獲取到,應該暫時不繪制圖表,當第一次進入頁面後,拿到 canvas 對象瞭,再進行繪制。後面在拉取數據,例如下拉刷新,因為 canvas 已經獲取到瞭,就不用特殊處理瞭。

getCanvas() {
	...
	// 首次進來畫圖
	that.drawPieChart2d()
}
getData() {
    ...
    // 因為本頁作為組件隱藏瞭,首次進來無法獲取canvas高度,首次進來另外處理
	if (!that.data.isFirstComeIn) {
    	that.drawPieChart2d()
	}
}
  • 圖表數據處理

這裡還碰到一個很奇怪的問題,就是我一開始把數據的百分比算成四位小數,在頁面綁定的時候乘上100加上百分號再顯示,按理來說應該顯示小數點後兩位的百分比值,可實際卻是取小數點後兩位並不生效,小數點後面取瞭十幾位,可能時在頁面計算的時候出瞭問題。

所以這裡最好在 JS 中算好值,保留小數點後幾位,再進行數據綁定。計算的時候,分母不為零千萬別忘瞭。

結語

都說代碼是最好的老師,canvas 2d的使用都在代碼中蘊含瞭,這個圖表用起來還是挺不錯的。

以上就是canvas 2d 環形統計圖手寫實現示例的詳細內容,更多關於canvas 2d 環形統計圖的資料請關註WalkonNet其它相關文章!

推薦閱讀: