canvas?2d?環(huán)形統(tǒng)計圖手寫實現(xiàn)示例
正文
其實小程序上面也可以使用 echart 等開源圖表庫得,而且支持代碼包得裁切功能,但是可能我不會用吧,效果不太好,而且我這就一個圖,也沒什么交互要寫,就手撕了一個環(huán)形統(tǒng)計圖,也算練習(xí)一下 canvas 2d 吧。
說到 canvas 2d 可真是頭疼,微信官方不知道干嘛吃的,原接口不再維護了,但是 canvas 2d 得文檔幾乎沒有更新,寫起來摸不著頭腦。如果你也對 canvas 2d 有疑惑,希望這個環(huán)形統(tǒng)計圖能給你點幫助。
下面是 canvas 的官方文檔,api使用也挺重要,可以先了解了解。
developers.weixin.qq.com/miniprogram…
developers.weixin.qq.com/miniprogram…
先看看效果
中間得環(huán)形圖以及里面的文字就是通過 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) { // 首次進來頁面圖標(biāo)無法加載,監(jiān)聽頁面切換來顯示 let isFirstComeIn = this.data.isFirstComeIn if (isFirstComeIn) { this.getCanvas() this.data.isFirstComeIn = false } } } }, lifetimes: { attached: function () { // 初始化加載數(shù)據(jù) this.getData() }, }, data: { // 畫布相關(guān) isFirstComeIn: true, context: null, height: 0, width: 0, // 圖表數(shù)據(jù) 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() }, // 獲取數(shù)據(jù) 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,注釋的是一次性調(diào)用 canvas 2d 代碼 drawPieChart() { // 組件中使用需要增加 this const ctx = wx.createCanvasContext('myChart', this); //設(shè)置半徑 let radius = 56; let center = { x: 56, y: 56 }; // 設(shè)置數(shù)據(jù)、總數(shù) 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('評估項目數(shù)', 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) // //設(shè)置半徑 // let radius = 56; // let center = { // x: 56, // y: 56 // }; // // 設(shè)置數(shù)據(jù)、總數(shù) // 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('評估項目數(shù)', 56, 70) // }) }, drawPieChart2d() { let ctx = this.data.context //設(shè)置半徑 let radius = 56; let center = { x: 56, y: 56 }; // 設(shè)置數(shù)據(jù)、總數(shù) 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('評估項目數(shù)', 56, 70) }, } })
這里寫的有些復(fù)雜了,但是復(fù)雜的東西能學(xué)到的也多吧,在組件中使用都掌握了,在 Page 中使用那就得心應(yīng)手了,下面詳細(xì)講講。
繪制圖表
實際上繪制圖表并不需要這么多的代碼,在Page也好,在組件頁面也好,其實只需要在需要繪制的時候調(diào)用上面 js 中 drawPieChart 代碼即可,前面是舊版本的canvas,后面注釋的是 canvas 2d的寫法,可以對比看看,還是有些去別的,特別是字體大小坑了我一把。
但是為什么要寫這么多代碼呢?還是解決一些出現(xiàn)的問題,下面詳細(xì)介紹。
解決問題
- 下拉刷新會造成畫兩個不同大小的餅圖
問題很奇怪,而且只在某些機型出現(xiàn)。仔細(xì)研究一下發(fā)現(xiàn)這個問題是因為繪制圖表的時候,多次調(diào)用一次性生成圖表函數(shù)造成的,即每次獲取到的 canvas 對象可能不太一樣了,具體什么不一樣了,我就沒有仔細(xì)研究了,可能是頁面發(fā)生了變化造成的。
這里的解決辦法就是只獲取一次 canvas,后面就用它不停的繪制圖表,當(dāng)繪需要制新的圖表時,清空原來內(nèi)容并繪制。首先提取出一個函數(shù)獲取 canvas,這個函數(shù)要在 page 的 onReady中監(jiān)聽,這里再組件中也可以在 lifetimes 的 ready 方法中監(jiān)聽,都是一樣的。獲取到 canvas 對象后設(shè)置為全局變量,后面繪制的的時候取這個變量繪制就可以了。
這里我們把 getCanvas 寫在了組件頁面第一次顯示時觸發(fā),原因看下面問題。
- 首次進來頁面圖表無法加載
這個問題是我們自定義的底部導(dǎo)航欄引入的,因為組件頁面的出現(xiàn)后就被設(shè)置成了隱藏狀態(tài),所以 canvas 并沒有獲得到寬高,導(dǎo)致圖表不顯示。
解決辦法就是在組件頁面第一次顯示的時候觸發(fā) getCanvas 函數(shù),這里監(jiān)聽 show 屬性的寫法可以參考我前面自定義底部導(dǎo)航欄的博客,就不詳述了。第一次顯示的問題,用到了一個全局變量,一旦觸發(fā)了,這個變量就永久設(shè)置為 false,使 getCanvas 函數(shù)不會再次執(zhí)行。
同時,在第一次獲取數(shù)據(jù)時因為 canvas 未獲取到,應(yīng)該暫時不繪制圖表,當(dāng)?shù)谝淮芜M入頁面后,拿到 canvas 對象了,再進行繪制。后面在拉取數(shù)據(jù),例如下拉刷新,因為 canvas 已經(jīng)獲取到了,就不用特殊處理了。
getCanvas() { ... // 首次進來畫圖 that.drawPieChart2d() } getData() { ... // 因為本頁作為組件隱藏了,首次進來無法獲取canvas高度,首次進來另外處理 if (!that.data.isFirstComeIn) { that.drawPieChart2d() } }
- 圖表數(shù)據(jù)處理
這里還碰到一個很奇怪的問題,就是我一開始把數(shù)據(jù)的百分比算成四位小數(shù),在頁面綁定的時候乘上100加上百分號再顯示,按理來說應(yīng)該顯示小數(shù)點后兩位的百分比值,可實際卻是取小數(shù)點后兩位并不生效,小數(shù)點后面取了十幾位,可能時在頁面計算的時候出了問題。
所以這里最好在 JS 中算好值,保留小數(shù)點后幾位,再進行數(shù)據(jù)綁定。計算的時候,分母不為零千萬別忘了。
結(jié)語
都說代碼是最好的老師,canvas 2d的使用都在代碼中蘊含了,這個圖表用起來還是挺不錯的。
以上就是canvas 2d 環(huán)形統(tǒng)計圖手寫實現(xiàn)示例的詳細(xì)內(nèi)容,更多關(guān)于canvas 2d 環(huán)形統(tǒng)計圖的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
attachEvent的使用方法與傳遞參數(shù)[IE|firefox]
attachEvent的使用方法與傳遞參數(shù)[IE|firefox]...2007-05-05javascript從右邊截取指定字符串的三種實現(xiàn)方法
這篇文章主要介紹了javascript從右邊截取指定字符串的三種實現(xiàn)方法。需要的朋友可以過來參考下,希望對大家有所幫助2013-11-11基于javascript、ajax、memcache和PHP實現(xiàn)的簡易在線聊天室
這篇文章主要介紹了基于javascript、ajax、memcache和PHP實現(xiàn)的簡易在線聊天室,需要的朋友可以參考下2015-02-02?javascript數(shù)組中的findIndex方法?
這篇文章主要介紹了javascript數(shù)組中的findIndex方法,findIndex()?方法返回傳入一個測試條件函數(shù)符合條件的數(shù)組第一個元素位置,下面更多相關(guān)資料,需要的小伙伴可以參考一下2022-03-03