网络编程
位置:首页>> 网络编程>> JavaScript>> canvas 2d 环形统计图手写实现示例

canvas 2d 环形统计图手写实现示例

作者:方大可  发布时间:2023-07-13 16:35:23 

标签: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 环形统计图手写实现示例

中间得环形图以及里面的文字就是通过 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 中算好值,保留小数点后几位,再进行数据绑定。计算的时候,分母不为零千万别忘了。

来源:https://juejin.cn/post/7222872422998458427

0
投稿

猜你喜欢

手机版 网络编程 asp之家 www.aspxhome.com