IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> JavaScript知识库 -> 原生Canvas实现四分之三圆环图 -> 正文阅读

[JavaScript知识库]原生Canvas实现四分之三圆环图

在可视化应用中,圆环图的应用场景也比较多,从展示效果上看,它类似于echarts中的圆环饼图,但是又有诸多不同之处,使用echarts图表库实现以下展示效果相对来说还是有点困难的,本文将圆环图的实现方式简单记录一下,以备不时之需。当然实现思路和之前写的一篇文章一样,这里不做过多描述,具体可以参考以下连接:

Canvas弧形进度条https://blog.csdn.net/qq_40289557/article/details/125236441?spm=1001.2014.3001.5501

四分之三圆环图

?需求描述:

1.图表根据接口返回数据(数组形式),动态展示;

2.图表label可自定义,也可默认展示;

3.加入鼠标交互操作,鼠标悬浮至图表项时,展示提示信息;

4.图表渲染时可展示动画、从初始状态到目标值有一个过度效果;

5.圆环描边样式可自定义;

基于以上需求,这里采用面向对象的开发方式,封装一个可复用的圆环类?

class RingCharts {
	// 构造函数,初始化时调用
	constructor(arg) {
		this.options = this.deepCopy(this.defaultConfig(), arg)
		this.parentContainer = typeof this.options.container === 'string' ? document.querySelector(this.options.container) :
			this.options.container
		this.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.offsetWidth
		this.height = this.parentContainer.offsetHeight
		this.canvas.width = this.width * this.pixelRatio
		this.canvas.height = this.height * this.pixelRatio
		// 设置最外层环和最内层环的半径
		this.radius = (this.canvas.width > this.canvas.height ? this.canvas.height : this.canvas.width) / 2
		this.maxRadius = this.handleNum(this.options.polar.maxRadius, this.radius)
		this.minRadius = this.handleNum(this.options.polar.minRadius, this.radius)
		// 中心点坐标
		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.parentContainer.appendChild(this.container)
		this.container.appendChild(this.tips)
		// 渲染图表的数据集
		this.resultData = []
		if (this.options.tooltip.show) {
			this.canvas.onmousemove = this.debounce(this.mousemove, 20)
		}
		this.resizeTimer = null
		this.animateStartTime = null
		this.animateTimer = null
	}
	// 处理百分比小数及数值
	handleNum(num, value) {
		let returnNum = 0
		if (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 timer = null
		return 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
			})
		}
	}
	// 窗口resize
	resize() {
		// 防抖处理
		if (this.resizeTimer) {
			clearTimeout(this.resizeTimer)
			this.resizeTimer = null
		}
		this.resizeTimer = setTimeout(() => {
			this.width = this.parentContainer.offsetWidth
			this.height = this.parentContainer.offsetHeight
			this.canvas.width = this.width * this.pixelRatio
			this.canvas.height = this.height * this.pixelRatio
			this.radius = (this.canvas.width > this.canvas.height ? this.canvas.height : this.canvas.width) / 2
			this.maxRadius = this.handleNum(this.options.polar.maxRadius, this.radius)
			this.minRadius = this.handleNum(this.options.polar.minRadius, this.radius)
			this.center = {
				x: this.canvas.width / 2,
				y: this.canvas.height / 2
			}
			this.draw(this.resultData)
		}, 20)
	}
	// 批量设置样式
	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;
	}
	// 默认图表配置项
	defaultConfig() {
		return {
			color: ['#18b78e', '#6c77fd', '#ffbe75', '#ff647c', '#01b3ee', '#164bcd'],
			polar: {
				maxRadius: '90%',
				minRadius: '10%',
				center: ['center', 'center'],
				strokeBackgroundColor: '#031f3e',
				strokeBackgroundWidth: 14,
				strokeWidth: 14,
				strokeColor: '#6f78cc',
				lineCap: 'round'
			},
			xAxis: {
				marker: {
					show: true,
					symbolSize: 10,
					offsetX: 40,
				},
				axisLabel: {
					show: true,
					offsetX: 70,
					font: '24px Microsoft YaHei',
					color: '#00b2f6',
					align: 'left',
					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',
					pointerEvents: 'none'
				},
				markerTemplate: '<span style="display: inline-block;width:14px;height: 14px;border-radius: 50%;margin-right: 4px;background-color: #"></span>',
				show: true,
				formatter: function (param) {
					return `${param.marker}${param.data.name}:${param.data.value}`
				}
			}
		}
	}
	// 获取屏幕的像素比
	getPixelRatio(context) {
		var backingStroe = context.backingStorePixelRatio ||
			context.webkitBackingStorePixelRatio ||
			context.mozBackingStorePixelRatio ||
			context.msBackingStorePixelRatio ||
			context.oBackingStorePixelRatio ||
			1
		return (window.devicePixelRatio || 1) / backingStroe
	}
	// 绘制图表
	draw(resultData, arg) {
		this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height)
		if (!resultData) return;
		this.resultData = resultData
		let flagArr = []
		let gap = (this.maxRadius - this.minRadius) / resultData.length
		let data = resultData.map(item => item.value)
		let maxValue = Math.max(...data) * 1.2
		let percent = 1
		if (!arg && this.options.animation.show) {
			if (!this.animateStartTime) {
				percent = 0
				this.animateStartTime = new Date()
			} else {
				percent = (new Date() - this.animateStartTime) / this.options.animation.duration
			}

			if (percent >= 1) {
				percent = 1
				this.animateStartTime = null
				window.cancelAnimationFrame(this.animateTimer)
				this.animateTimer = null
			} else {
				this.animateTimer = window.requestAnimationFrame(() => {
					this.draw(this.resultData)
				})
			}
		}

		for (var i = 0; i < data.length; i++) {
			let radius = this.maxRadius - i * gap - this.options.polar.strokeWidth / 2
			this.drawArc({
				deg: 0,
				stroke: this.options.polar.strokeBackgroundColor,
				strokeWidth: this.options.polar.strokeBackgroundWidth,
				radius: radius,
				lineCap: this.options.polar.lineCap
			})
			flagArr.push(this.drawArc({
				deg: (1 - (data[i] / maxValue) * percent) * Math.PI * 3 / 2,
				stroke: this.options.color[i % this.options.color.length],
				strokeWidth: this.options.polar.strokeWidth,
				radius: radius,
				lineCap: this.options.polar.lineCap
			}, arg))
			if (this.options.xAxis.marker.show) {
				this.drawCircle({
					x: this.handleNum(this.options.xAxis.marker.offsetX, this.radius),
					y: radius,
					fillColor: this.options.color[i % this.options.color.length],
					symbolSize: this.options.xAxis.marker.symbolSize,
					centerX: this.center.x,
					centerY: this.center.y,
				})
			}
			if (this.options.xAxis.axisLabel.show) {
				this.drawText({
					x: this.handleNum(this.options.xAxis.axisLabel.offsetX, this.radius),
					y: radius,
					fillColor: this.options.xAxis.axisLabel.color == 'auto' ? this.options.color[i % this.options.color.length] : this
						.options.xAxis.axisLabel.color,
					font: this.options.xAxis.axisLabel.font,
					text: this.options.xAxis.axisLabel.formatter(resultData[i]),
					align: this.options.xAxis.axisLabel.align,
					verticalAlign: this.options.xAxis.axisLabel.verticalAlign,
					centerX: this.center.x,
					centerY: this.center.y,
				})
			}
		}
		if (arg) {
			if (flagArr.some(item => item == true)) {
				let index = flagArr.lastIndexOf(true)
				let color = this.options.color[index % this.options.color.length]
				this.tips.innerHTML = this.options.tooltip.formatter({
					marker: this.options.tooltip.markerTemplate.replace('#', color),
					color: color,
					data: resultData[index]
				})
				let tipsPosX = arg.x + 20
				let tipsPosY = arg.y + 20
				if (arg.x + this.tips.offsetWidth + 20 > this.width) {
					tipsPosX = arg.x - 20 - this.tips.offsetWidth
				}
				if (arg.y + this.tips.offsetHeight + 20 > this.height) {
					tipsPosY = arg.y - 20 - this.tips.offsetHeight
				}
				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[index])
				}
			} else {
				this.container.style.cursor = 'default'
				this.tips.style.display = 'none'
			}
		}
	}
	// 绘制圆弧
	drawArc(arg, arg2) {
		let isInStroke = false
		this.ctx.beginPath()
		this.ctx.arc(this.center.x, this.center.y, arg.radius, Math.PI * 3 / 2, arg.deg, true)
		this.ctx.lineCap = arg.lineCap
		this.ctx.strokeStyle = arg.stroke
		this.ctx.lineWidth = arg.strokeWidth
		this.ctx.stroke()
		if (arg2 && this.ctx.isPointInStroke(arg2.x * this.pixelRatio, arg2.y * this.pixelRatio)) {
			isInStroke = true
		}
		return isInStroke
	}
	// 绘制文字
	drawText(arg) {
		this.ctx.save()
		this.ctx.beginPath()
		this.ctx.translate(arg.centerX, arg.centerY);
		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()
	}
	// 绘制圆点
	drawCircle(arg) {
		this.ctx.save()
		this.ctx.beginPath()
		this.ctx.translate(arg.centerX, arg.centerY);
		this.ctx.arc(arg.x, -arg.y, arg.symbolSize, 0, Math.PI * 2, false)
		this.ctx.fillStyle = arg.fillColor
		this.ctx.fill()
		this.ctx.restore()
	}
}

export default RingCharts

?在页面中使用,这里以vue为例,此处使用的是静态数据,如果需要根据接口数据动态展示,就在请求到数据后,调用图表的draw方法,将数据列表传入即可,数据列表字段务必保持一致;

<template>
  <div class="chart-box">
    <div class="chart-wrapper">
      <div class="module-title">四分之三圆环类型一</div>
      <div class="container" id="container1"></div>
    </div>
    <div class="chart-wrapper">
      <div class="module-title">四分之三圆环类型二</div>
      <div class="container" id="container2"></div>
    </div>
  </div>
</template>
<script>
import RingCharts from "./RingChartJS";
export default {
  name: "RingChart",
  data() {
    return {
      ringCharts: null,
      ringCharts2: null,
    };
  },
  mounted() {
    this.ringCharts = new RingCharts({
      container: "#container1",
      color: ["#ff8700", "#ffc300", "#07e373", "#019dff", "#0033ff"],
      polar: {
        strokeBackgroundColor: "#031f3e",
        strokeBackgroundWidth: 14,
        strokeWidth: 14,
        strokeColor: "#6f78cc",
        lineCap: "butt",
      },
      animation: {
        show: true,
      },
      xAxis: {
        marker: {
          show: false,
          symbolSize: 10,
        },
        axisLabel: {
          show: true,
          offsetX: 20,
          font: "24px Microsoft YaHei",
          color: "auto",
          align: "left",
          verticalAlign: "middle",
          formatter: function (param) {
            return `${param.name}:${param.value}${param.unit}`;
          },
        },
      },
      tooltip: {
        show: true,
        formatter: function (param) {
          return `${param.marker}${param.data.name}:${param.data.value}${param.data.unit}`;
        },
      },
    });
    this.ringCharts2 = new RingCharts({
      container: "#container2",
      animation: {
        show: false,
      },
    });
    this.ringCharts.click(function (item) {
      console.log(item);
    });
    this.ringCharts2.click(function (item) {
      console.log(item);
    });
    this.initChart();
    window.addEventListener("resize", this.resize);
  },
  beforeDestroy() {
    window.removeEventListener("resize", this.resize);
  },
  methods: {
    resize() {
      this.ringCharts.resize();
      this.ringCharts2.resize();
    },
    initChart() {
      this.ringCharts.draw([
        {
          name: "使用中资源量",
          value: 100,
          unit: "项",
        },
        {
          name: "维修中资源量",
          value: 80,
          unit: "项",
        },
        {
          name: "保养中资源量",
          value: 60,
          unit: "项",
        },
        {
          name: "已损坏资源量",
          value: 45,
          unit: "项",
        },
      ]);

      this.ringCharts2.draw([
        {
          name: "类目名称一",
          value: 69,
          unit: "个",
        },
        {
          name: "类目名称二",
          value: 32,
          unit: "个",
        },
        {
          name: "类目名称三",
          value: 11,
          unit: "个",
        },
        {
          name: "类目名称四",
          value: 100,
          unit: "个",
        },
        {
          name: "类目名称五",
          value: 88,
          unit: "个",
        },
      ]);
    },
  },
};
</script>
<style scoped>
.chart-box {
  width: 100%;
  height: 100%;
  display: flex;
  flex-wrap: wrap;
  justify-content: space-around;
  align-items: center;
  background-color: #071727;
}
.chart-wrapper {
  width: 46%;
  height: 96%;
}

.module-title {
  position: relative;
  z-index: 1;
  width: 100%;
  height: 30px;
  line-height: 30px;
  font-size: 16px;
  text-align: center;
  color: #fff;
}
.module-title::before,
.module-title::after {
  content: "";
  position: absolute;
  z-index: -1;
  top: 0;
  width: 50%;
  height: 100%;
  background-image: linear-gradient(to bottom, #061223, #042c4c);
}
.module-title::before {
  left: 0;
  transform: skew(-45deg);
  transform-origin: left bottom;
}
.module-title::after {
  right: 0;
  transform: skew(45deg);
  transform-origin: right bottom;
}
.container {
  width: 100%;
  height: calc(100% - 30px);
  padding: 10px;
  border: 1px solid #064774;
  color: #fff;
  box-sizing: border-box;
}
</style>

  JavaScript知识库 最新文章
ES6的相关知识点
react 函数式组件 & react其他一些总结
Vue基础超详细
前端JS也可以连点成线(Vue中运用 AntVG6)
Vue事件处理的基本使用
Vue后台项目的记录 (一)
前后端分离vue跨域,devServer配置proxy代理
TypeScript
初识vuex
vue项目安装包指令收集
上一篇文章      下一篇文章      查看所有文章
加:2022-08-06 10:34:49  更:2022-08-06 10:38:06 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2025年1日历 -2025/1/11 12:46:02-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码