Canvas弧形进度条

编程入门 行业动态 更新时间:2024-10-15 14:15:36

Canvas<a href=https://www.elefans.com/category/jswz/34/1759498.html style=弧形进度条"/>

Canvas弧形进度条

在可视化应用中,弧形进度条应用也比较广泛,在“SVG绘制圆环进度条”和“线形进度条的几种实现方式”这两章之后,今天针对平时开发中关于弧形进度条的实现方案进行一个总结,结合Javascript面向对象的开发方式,参考echarts的实现方式,封装一个可复用、个性化的弧形进度条组件,也算是对原生Canvas相关api进行一下回顾。展示效果如下:

1.我们都知道Canvas绘图是基于像素点进行绘制,而设备的分辨率又是千差万别,为了使我们绘制的图表能适应各种屏幕,通常的做法是绘制2倍或多倍图,1倍展示,也就是将绘制好的Canvas图表(通俗的来说就是一张图片)进行压缩展示,这在高清屏上能明显优化展示效果。本篇文章采用的是另一种方法,采用获取屏幕像素比的方式,解决高清屏下图表模糊问题,这里引入一个辅助性函数,用于获取用户屏幕的像素比。

// 获取屏幕的像素比
getPixelRatio(context) {var backingStroe = context.backingStorePixelRatio ||context.webkitBackingStorePixelRatio ||context.mozBackingStorePixelRatio ||context.msBackingStorePixelRatio ||context.oBackingStorePixelRatio ||1return (window.devicePixelRatio || 1) / backingStroe
}

 2.使用过echarts的同学都知道,echarts个性化图表的实现方式是通过传入配置对象来实现的,因此我们也可以通过此方式实现,提供默认的配置方案,基于用户自己的喜好传入个性化参数,在实际绘制时将二者参数进行合并及覆盖即可实现。

// 默认图表配置项
defaultConfig() {return {polar: {radius: '90%',arcDeg: 240,center: ['center', 'center'],strokeBackgroundColor: '#e1e8ee',strokeBackgroundWidth: 14,strokeWidth: 14,strokeColor: '#6f78cc',startAngle: 0,lineCap: 'round'},xAxis: {axisLabel: {show: false,offsetCenterY: '50%',font: '24px Microsoft YaHei',color: '#6f78cc',align: 'center',verticalAlign: 'middle',formatter: function(param) {return param.name},}},animation: {show: false,duration: 800,},desc: {show: true,offsetCenterY: 0,font: '24px Microsoft YaHei',color: '#000',align: 'center',verticalAlign: 'middle',formatter: function(param) {return param.value + param.unit}},tooltip: {style: {position: 'absolute',display: 'none',whiteSpace: 'nowrap',zIndex: '9999999',transition: 'left 0.4s cubic-bezier(0.23, 1, 0.32, 1) 0s, top 0.4s cubic-bezier(0.23, 1, 0.32, 1) 0s',backgroundColor: 'rgba(50, 50, 50, 0.7)',borderRadius: '4px',border: '0px solid rgb(51, 51, 51)',color: 'rgb(255, 255, 255)',font: '20px / 30px Microsoft YaHei',padding: '5px',left: '0px',top: '0px',},markerTemplate: '<span style="display: inline-block;width:14px;height: 14px;border-radius: 50%;margin-right: 4px;background-color: #"></span>',show: false,formatter: function(param) {return `${param.marker}${param.data.name}:${param.data.value}`}}}
}

 3.既然是将对象进行合并,这里就又涉及到一个新的知识点,深拷贝的问题,关于深拷贝和浅拷贝的实现方式,网上不胜枚举,作为一个必考的面试题,这里不做过多描述,仅实现一个简易版的深拷贝功能。

// 深拷贝
deepCopy(result, obj) {for (var key in obj) {if (obj.hasOwnProperty(key)) {if (typeof obj[key] === 'object' && result[key]) {this.deepCopy(result[key], obj[key]);} else {result[key] = obj[key];}}}return result;
}

 4.为了增加配置参数的通用性,用户既可以传入百分比数据,也可以传入具体的数值,因此在类中提供一个简易的处理函数,以提高程序的通用性和复用性

// 处理百分比小数及数值
handleNum(num, value) {let returnNum = 0if (num.toString().indexOf('%') > -1) {returnNum = num.replace("%", "") / 100 * value;} else if (num > 0 && num <= 1) {returnNum = num * value} else {returnNum = parseInt(num)}return returnNum
}

5.在实际的使用场景中,用户可能进行浏览器窗口的拉伸或还原操作,如果不做图表重新渲染会引起图表的拉伸变形等问题,因此需要使用window.onresize监听窗口变化,当窗口宽高发生变化时,进行图表重绘即可解决此问题,另外由于窗口变化重绘会引起不必要的渲染,从节省系统资源方面考虑,我们可以提供一个防抖函数进行性能上的调优

// 防抖函数
debounce(fn, delay) {let _this = thislet timer = nullreturn e => {if (timer != null) {clearTimeout(timer)}timer = setTimeout(() => {fn.call(_this, e)}, delay)}
}

 6.加入用户的交互操作,用户鼠标悬浮至图表时,进行提示信息展示。针对Canvas来说,鼠标交互并不像其它dom那样方便,因为整个Canvas画布是一个整体。实现画布内图表组件的鼠标交互操作需要重绘图表,在绘制过程中,通过isPointInStroke或isPointInPath这两个api判断是否在指定上下文对象上即可

// 鼠标移动事件
mousemove(e) {this.draw(this.resultData, {callBack: null,type: 'mousemove',x: e.offsetX,y: e.offsetY})
}
// 点击事件
click(callBack) {this.canvas.onclick = e => {this.draw(this.resultData, {callBack: callBack,type: 'click',x: e.offsetX,y: e.offsetY})}
}
// 绘制圆弧
drawArc(arg, arg2) {let isInStroke = falsethis.ctx.beginPath()this.ctx.arc(this.center.x, this.center.y, arg.radius, arg.startDeg, arg.endDeg, false)this.ctx.lineCap = arg.lineCapthis.ctx.strokeStyle = arg.stroke// 判断鼠标是否悬浮在指定的图表组件上if (arg2 && this.ctx.isPointInStroke(arg2.x * this.pixelRatio, arg2.y * this.pixelRatio)) {isInStroke = true}this.ctx.lineWidth = arg.strokeWidththis.ctx.stroke()return isInStroke
}

 针对以上分析汇总,以下提供完整的实现方式

class PercentCharts {// 构造函数,初始化时调用constructor(arg) {this.options = this.deepCopy(this.defaultConfig(), arg)this.parentContainer = typeof this.options.container === 'string' ? document.querySelector(this.options.container) :this.options.containerthis.container = document.createElement('div')this.tips = document.createElement('div')// 提示信息样式this.setStyle(this.tips, this.options.tooltip.style)this.canvas = document.createElement('canvas')this.ctx = this.canvas.getContext('2d')// 获取屏幕像素比,解决高清屏下图表模糊问题this.pixelRatio = this.getPixelRatio(this.ctx)this.width = this.parentContainer.offsetWidththis.height = this.parentContainer.offsetHeightthis.canvas.width = this.width * this.pixelRatiothis.canvas.height = this.height * this.pixelRatiothis.maxRadius = this.canvas.width > this.canvas.height ? this.canvas.height : this.canvas.width// 中心点坐标this.center = {x: this.canvas.width / 2,y: this.canvas.height / 2}// 设置容器及canvas标签样式this.container.style.cssText = this.canvas.style.cssText ='position:relative;width:100%;height:100%;overflow:hidden'this.container.appendChild(this.canvas)this.container.appendChild(this.tips)this.parentContainer.appendChild(this.container)this.radius = this.handleNum(this.options.polar.radius, this.maxRadius / 2)// 渲染图表的数据集this.resultData = []if (this.options.tooltip.show) {this.canvas.onmousemove = this.debounce(this.mousemove, 20)}this.resizeTimer = nullthis.animateStartTime = nullthis.animateTimer = null}// 窗口resizeresize() {// 防抖处理if (this.resizeTimer) {clearTimeout(this.resizeTimer)this.resizeTimer = null}this.resizeTimer = setTimeout(() => {this.width = this.parentContainer.offsetWidththis.height = this.parentContainer.offsetHeightthis.canvas.width = this.width * this.pixelRatiothis.canvas.height = this.height * this.pixelRatiothis.maxRadius = this.canvas.width > this.canvas.height ? this.canvas.height : this.canvas.widththis.radius = this.handleNum(this.options.polar.radius, this.maxRadius / 2)this.center = {x: this.canvas.width / 2,y: this.canvas.height / 2}this.draw(this.resultData)}, 20)}// 默认图表配置项defaultConfig() {return {polar: {radius: '90%',arcDeg: 240,center: ['center', 'center'],strokeBackgroundColor: '#e1e8ee',strokeBackgroundWidth: 14,strokeWidth: 14,strokeColor: '#6f78cc',startAngle: 0,lineCap: 'round'},xAxis: {axisLabel: {show: false,offsetCenterY: '50%',font: '24px Microsoft YaHei',color: '#6f78cc',align: 'center',verticalAlign: 'middle',formatter: function (param) {return param.name},}},animation: {show: false,duration: 800,},desc: {show: true,offsetCenterY: 0,font: '24px Microsoft YaHei',color: '#000',align: 'center',verticalAlign: 'middle',formatter: function (param) {return param.value + param.unit}},tooltip: {style: {position: 'absolute',display: 'none',whiteSpace: 'nowrap',zIndex: '9999999',transition: 'left 0.4s cubic-bezier(0.23, 1, 0.32, 1) 0s, top 0.4s cubic-bezier(0.23, 1, 0.32, 1) 0s',backgroundColor: 'rgba(50, 50, 50, 0.7)',borderRadius: '4px',border: '0px solid rgb(51, 51, 51)',color: 'rgb(255, 255, 255)',font: '20px / 30px Microsoft YaHei',padding: '5px',left: '0px',top: '0px',},markerTemplate: '<span style="display: inline-block;width:14px;height: 14px;border-radius: 50%;margin-right: 4px;background-color: #"></span>',show: false,formatter: function (param) {return `${param.marker}${param.data.name}:${param.data.value}`}}}}// 批量设置样式setStyle(obj, sty) {for (let key in sty) {obj.style[key] = sty[key]}}// 深拷贝deepCopy(result, obj) {for (var key in obj) {if (obj.hasOwnProperty(key)) {if (typeof obj[key] === 'object' && result[key]) {this.deepCopy(result[key], obj[key]);} else {result[key] = obj[key];}}}return result;}// 处理百分比小数及数值handleNum(num, value) {let returnNum = 0if (num.toString().indexOf('%') > -1) {returnNum = num.replace("%", "") / 100 * value;} else if (num > 0 && num <= 1) {returnNum = num * value} else {returnNum = parseInt(num)}return returnNum}// 防抖函数debounce(fn, delay) {let _this = thislet timer = nullreturn e => {if (timer != null) {clearTimeout(timer)}timer = setTimeout(() => {fn.call(_this, e)}, delay)}}// 鼠标移动事件mousemove(e) {this.draw(this.resultData, {callBack: null,type: 'mousemove',x: e.offsetX,y: e.offsetY})}// 点击事件click(callBack) {this.canvas.onclick = e => {this.draw(this.resultData, {callBack: callBack,type: 'click',x: e.offsetX,y: e.offsetY})}}// 获取屏幕的像素比getPixelRatio(context) {var backingStroe = context.backingStorePixelRatio ||context.webkitBackingStorePixelRatio ||context.mozBackingStorePixelRatio ||context.msBackingStorePixelRatio ||context.oBackingStorePixelRatio ||1return (window.devicePixelRatio || 1) / backingStroe}// 绘制图表draw(resultData, arg) {if (this.animateTimer) {window.cancelAnimationFrame(this.animateTimer)}this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height)if (!resultData) return;this.resultData = resultDatalet flagArr = []let halfDeg = (this.options.polar.arcDeg / 2 / 360) * Math.PI * 2let startDeg = Math.PI * 3 / 2 - halfDeg - (this.options.polar.startAngle / 360) * (Math.PI * 2)let percent = 1if (!arg && this.options.animation.show) {if (!this.animateStartTime) {percent = 0this.animateStartTime = new Date()} else {percent = (new Date() - this.animateStartTime) / this.options.animation.duration}if (percent >= 1) {percent = 1this.animateStartTime = nullwindow.cancelAnimationFrame(this.animateTimer)this.animateTimer = null} else {this.animateTimer = window.requestAnimationFrame(() => {this.draw(this.resultData)})}}this.drawArc({startDeg: startDeg,endDeg: Math.PI * 3 / 2 + halfDeg,stroke: this.options.polar.strokeBackgroundColor,strokeWidth: this.options.polar.strokeBackgroundWidth,radius: this.radius,lineCap: this.options.polar.lineCap})flagArr.push(this.drawArc({startDeg: startDeg,endDeg: startDeg + (halfDeg * 2) * (this.resultData.value / 100) * percent,stroke: this.options.polar.strokeColor,strokeWidth: this.options.polar.strokeWidth,radius: this.radius,lineCap: this.options.polar.lineCap}, arg))// 绘制底部文字if (this.options.xAxis.axisLabel.show) {this.drawText({x: 0,y: this.handleNum(this.options.xAxis.axisLabel.offsetCenterY, this.maxRadius / 2),font: this.options.xAxis.axisLabel.font,fillColor: this.options.xAxis.axisLabel.color == 'auto' ? this.options.polar.strokeColor : this.options.xAxis.axisLabel.color,text: this.options.xAxis.axisLabel.formatter(resultData),align: this.options.xAxis.axisLabel.align,verticalAlign: this.options.xAxis.axisLabel.verticalAlign,})}// 绘制中心文字if (this.options.desc.show) {this.drawText({x: 0,y: this.handleNum(this.options.desc.offsetCenterY, this.maxRadius / 2),font: this.options.desc.font,fillColor: this.options.desc.color == 'auto' ? this.options.polar.strokeColor : this.options.desc.color,text: this.options.desc.formatter(resultData),align: this.options.desc.align,verticalAlign: this.options.desc.verticalAlign,})}if (arg) {if (flagArr.some(item => item == true)) {this.tips.innerHTML = this.options.tooltip.formatter({marker: this.options.tooltip.markerTemplate.replace('#', this.options.polar.strokeColor),color: this.options.polar.strokeColor,data: resultData})let tipsPosX = 0let tipsPosY = 0if (arg.x + this.tips.offsetWidth + 20 > this.width) {tipsPosX = arg.x - 20 - this.tips.offsetWidth} else {tipsPosX = arg.x + 20}if (arg.y + this.tips.offsetHeight + 20 > this.height) {tipsPosY = arg.y - 20 - this.tips.offsetHeight} else {tipsPosY = arg.y + 20}this.tips.style.left = `${tipsPosX}px`this.tips.style.top = `${tipsPosY}px`this.tips.style.display = 'block'this.container.style.cursor = 'pointer'if (arg.callBack) {arg.callBack(resultData)}} else {this.container.style.cursor = 'default'this.tips.style.display = 'none'}}}// 绘制圆弧drawArc(arg, arg2) {let isInStroke = falsethis.ctx.beginPath()this.ctx.arc(this.center.x, this.center.y, arg.radius, arg.startDeg, arg.endDeg, false)this.ctx.lineCap = arg.lineCapthis.ctx.strokeStyle = arg.strokeif (arg2 && this.ctx.isPointInStroke(arg2.x * this.pixelRatio, arg2.y * this.pixelRatio)) {isInStroke = true}this.ctx.lineWidth = arg.strokeWidththis.ctx.stroke()return isInStroke}// 绘制文字drawText(arg) {this.ctx.save()this.ctx.beginPath()this.ctx.translate(this.center.x, this.center.y);this.ctx.font = arg.font;this.ctx.fillStyle = arg.fillColor;this.ctx.textAlign = arg.align;this.ctx.textBaseline = arg.verticalAlign;this.ctx.fillText(arg.text, arg.x, arg.y);this.ctx.restore()}
}
export default PercentCharts

 在组件中使用图表(以vue为例)


<template><div class="chart-box"><div class="container" id="container10"></div><div class="container" id="container20"></div><div class="container" id="container30"></div><div class="container" id="container40"></div><div class="container" id="container50"></div><div class="container" id="container60"></div></div>
</template>
<script>
import PercentCharts from "./PercentChartJS";
export default {name: "RingChart",data() {return {ringCharts1: null,ringCharts2: null,ringCharts3: null,ringCharts4: null,ringCharts5: null,ringCharts6: null,};},mounted() {this.ringCharts1 = new PercentCharts({container: "#container10",tooltip: {show: true,formatter: function (param) {return param.marker + "这是自定义信息";},},desc: {show: true,font: "40px Microsoft YaHei",formatter: function (param) {return param.value + param.unit;},},animation: {show: true,},xAxis: {axisLabel: {show: true,font: "30px Microsoft YaHei",formatter: function (param) {return param.name + "😃";},},},});this.ringCharts2 = new PercentCharts({container: "#container20",tooltip: {show: true,},polar: {strokeColor: "#ffc300",strokeWidth: 20,},xAxis: {axisLabel: {show: true,color: "auto",font: "30px Microsoft YaHei",formatter: function (param) {return param.name + "😒";},},},desc: {show: true,font: "40px Microsoft YaHei",},});this.ringCharts3 = new PercentCharts({container: "#container30",polar: {strokeColor: "#07e373",lineCap: "butt",},tooltip: {show: true,formatter: function (param) {return param.marker + "这是自定义信息";},},desc: {show: true,font: "40px Microsoft YaHei",formatter: function (param) {return param.value + param.unit;},},xAxis: {axisLabel: {show: true,color: "#000",font: "30px Microsoft YaHei",formatter: function (param) {return param.name + "😊";},},},});this.ringCharts4 = new PercentCharts({container: "#container40",polar: {arcDeg: 360,startAngle: 180,},tooltip: {show: true,},xAxis: {axisLabel: {offsetCenterY: "15%",show: true,font: "30px Microsoft YaHei",formatter: function (param) {return param.name + "😃";},},},animation: {show: true,},desc: {show: true,offsetCenterY: "-15%",font: "40px Microsoft YaHei",},});this.ringCharts5 = new PercentCharts({container: "#container50",polar: {arcDeg: 360,startAngle: 180,strokeColor: "#ffc300",strokeWidth: 20,},tooltip: {show: true,formatter: function (param) {return param.marker + "这是自定义信息";},},desc: {show: true,offsetCenterY: "-15%",font: "40px Microsoft YaHei",formatter: function (param) {return param.value + param.unit;},},xAxis: {axisLabel: {show: true,offsetCenterY: "15%",color: "auto",font: "30px Microsoft YaHei",formatter: function (param) {return param.name + "😒";},},},});this.ringCharts6 = new PercentCharts({container: "#container60",polar: {arcDeg: 360,startAngle: 180,strokeColor: "#07e373",lineCap: "butt",},tooltip: {show: true,},xAxis: {axisLabel: {show: true,offsetCenterY: "15%",font: "30px Microsoft YaHei",color: "#000",formatter: function (param) {return param.name + "😊";},},},desc: {show: true,offsetCenterY: "-15%",font: "40px Microsoft YaHei",},});this.initChart();window.addEventListener("resize", this.resize);},beforeDestroy() {window.removeEventListener("resize", this.resize);},methods: {resize() {this.ringCharts1.resize();this.ringCharts2.resize();this.ringCharts3.resize();this.ringCharts4.resize();this.ringCharts5.resize();this.ringCharts6.resize();},initChart() {this.ringCharts1.draw({name: "正面评论",value: 50,unit: "%",});this.ringCharts2.draw({name: "负面评论",value: 30,unit: "%",});this.ringCharts3.draw({name: "中立评论",value: 20,unit: "%",});this.ringCharts4.draw({name: "正面评论",value: 60,unit: "%",});this.ringCharts5.draw({name: "负面评论",value: 25,unit: "%",});this.ringCharts6.draw({name: "中立评论",value: 15,unit: "%",});},},
};
</script>
<style scoped>
.chart-box {width: 100%;height: 100%;display: flex;flex-wrap: wrap;
}
.container {width: 33.3%;height: 50%;
}
</style>

更多推荐

Canvas弧形进度条

本文发布于:2024-03-05 01:04:38,感谢您对本站的认可!
本文链接:https://www.elefans.com/category/jswz/34/1710900.html
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。
本文标签:弧形   进度条   Canvas

发布评论

评论列表 (有 0 条评论)
草根站长

>www.elefans.com

编程频道|电子爱好者 - 技术资讯及电子产品介绍!