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其它相關文章!
推薦閱讀:
- JavaScript+Canvas實現帶跳動效果的粒子動畫
- JavaScript canvas復刻蘋果發佈會環形進度條
- 微信小程序使用canvas繪制鐘表
- 小程序canvas實現畫佈半圓環
- JavaScript 繪制餅圖的示例