js+svg实现的一个环图

428 阅读5分钟

这是在实际项目中碰到的一个功能,首先按每一项的权重分配环占比,在换占比中有自身的一个热度百分比,开始试用过echart插件感觉太复杂,只能自己用svg实现一个,过程中感谢身边同事的指导和万能(恶)的度娘!

先看效果

图片的标注

效果链接 git

以下是所有的代码,感兴趣的可以git直接下载,注释已经尽量清晰,不理解的可以留言,有错误的欢迎指正,有更好的优化方法也请告诉我。如果有谁弄过svg转png下载,请一定联系我

html

<div id="ring-box" class="ring-box">
	<div id='labels' class="labels"></div>
</div>

css

* {
	margin: 0;
	padding: 0;
	border: none;
}

.ring-box {
	width: 500px;
	height: 500px;
	margin: 50px auto;
	border: 1px solid aqua;
	position: relative;
}

path {
	/*stroke-width: 30px;*/
	stroke-linecap: round;
}
.labels{
	position: absolute;
	left: 0;top: 0;right: 0;bottom: 0;margin: 0 auto;
}
.labels .label-item{
	position: absolute;
	left: 50%;
	top: 30px;
	transform-origin: 0 220px;
	text-align: center;
	cursor: pointer;
	transition: 0.3s;
}
.label-item span{
	font-size: 30px;
	color: #f40;
	font-weight: 600;
}
.label-item p{
	font-size: 12px;
	color: #666;
}

js代码

function DrawRing(el) {
	this.el = el // 画布挂载容器
	this.weight = 0 // 总权重
	this.endCoordinate = [] // 实环坐标
	this.endCoordinateBG = [] // 背景环坐标
	this.lenghtys = [] // 大小环
	this.degs = [] // 每个环所占角度
	this.lenghtysBG = [] // 背景环--大小环
	this.degrees = [] // 实环 结束角度
	this.degreesBgT = [] // 背景环 结束角度 真值
	this.degreesBgF = [] // 背景环 结束角度 实际值 = 真值 - 间隔
	this.middleDegs = [] // 记录每个环的中间角度--用来确定箭头位置
}

DrawRing.prototype.getDegrees = function(num) {
	// 计算当前的进度对应的角度值
	return num / 100 * 360
}
DrawRing.prototype.getRad = function(degrees) {
	// 计算当前进度对应的弧度值
	return degrees * (Math.PI / 180)
}
DrawRing.prototype.getEndCoordinate = function(rad, r) {
	// 极坐标转换成直角坐标
	return {
		x: (Math.sin(rad) * r),
		y: -(Math.cos(rad) * r)
	}
}
DrawRing.prototype.getLenghty = function(degrees) {
	// 大于180度时候画大角度弧,小于180度的画小角度弧,(deg > 180) ? 1 : 0
	return Number(degrees > 180)
}
DrawRing.prototype.getEndCoordinate = function(rad) {
	let r = this.r
	// 极坐标转换成直角坐标
	return {
		x: (Math.sin(rad) * r),
		y: -(Math.cos(rad) * r)
	}
}

DrawRing.prototype.init = function(option, r, ringWidth, margin) {
	this.option = option // 数据源
	this.r = r // 半径
	this.margin = margin // 间距
	this.ringWidth = ringWidth // 圆环大小
	this.getWeight() // 计算总权重
	this.setOption() // 根据权重计算各种数值
	this.startDrawRing() // 开始画环
}

// 获取总权重
DrawRing.prototype.getWeight = function() {
	this.option.data.forEach((ele, index) => {
		this.weight += ele.pro
	})
}

// 遍历option 计算画环需要的参数
DrawRing.prototype.setOption = function() {
	let weight = this.weight
	this.option.data.forEach((ele, index) => {
		this.getDegs(ele.pro, weight) // 计算每个环所占角度
		this.getDegreesBg(index) // 算背景环应到角度值degreesBgT 和 实到角度值degreesBgF 和 大小环
		this.getDegreesT(index, ele.value) // 计算实环结束角度值--大小环
		this.getCoordinate(index) // 计算实环结束坐标
		this.getCoordinateBG(index) // 计算背景环起始点坐标
	})
}

// 计算每个环所占角度
DrawRing.prototype.getDegs = function(pro, weight) {
	let deg = this.getDegrees(pro / weight * 100)
	this.degs.push(deg)
}
// 计算背景环应到角度值degreesBgT 和 实到角度值degreesBgF 和 大小环
DrawRing.prototype.getDegreesBg = function(index) {
	// 背景环(不考环间距前)结束角度 = 环占角度 + 上一个环结束角度 
	let deg = this.degs[index] + (index ? this.degreesBgT[index - 1] : 0)
	this.degreesBgT.push(deg)
	// 背景环实际画到角度 = degreesBgT - 间距角度
	this.degreesBgF.push(deg - this.margin)

	// 大于180度时候画大角度弧,小于180度的画小角度弧,(deg > 180) ? 1 : 0
	this.lenghtysBG.push(Number(this.degs[index] - this.margin > 180))
}

// 计算实环结束角度值--大小环
DrawRing.prototype.getDegreesT = function(index, val) {
	let degT = index ? (this.degs[index]) * val / 100 : (this.degs[index] - this.margin) * val / 100 // 实环所占角度
	let deg = degT + (index ? this.degreesBgF[index - 1] : 0) // 实环真实需要画的角度
	//	let degT = (index === 0 ? 0 : this.degs[index-1]) + this.degs[index]-this.margin 
	this.degrees.push(deg)

	// 每个环中间点角度
	let midDeg = (this.degs[index] - this.margin) * 50 / 100
	let degMid = midDeg + (index ? this.degreesBgT[index - 1] : 0)
	this.middleDegs.push(degMid)

	// 大于180度时候画大角度弧,小于180度的画小角度弧,(deg > 180) ? 1 : 0
	this.lenghtys.push(Number(degT > 180))
}

// 计算实环结束点坐标 (起点坐标和背景环一样)
DrawRing.prototype.getCoordinate = function(index) {
	let endCoordinate = this.getEndCoordinate(this.getRad(this.degrees[index]))
	this.endCoordinate.push(endCoordinate)
}

// 计算背景环起始点坐标
DrawRing.prototype.getCoordinateBG = function(index) {
	let endCoordinateBGT = this.getEndCoordinate(this.getRad(this.degreesBgT[index]))
	let endCoordinateBGF = this.getEndCoordinate(this.getRad(this.degreesBgF[index]))
	this.endCoordinateBG.push({
		x0: endCoordinateBGF.x,
		y0: endCoordinateBGF.y,
		x1: endCoordinateBGT.x,
		y1: endCoordinateBGT.y
	})
}

// 开始画
DrawRing.prototype.startDrawRing = function() {
	var svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg')
	svg.id = 'ring'
	svg.setAttribute('viewBox', '0,0,600,600') // 保证画布空间足够,自适应将按照这个比例缩放
	svg.style.width = '100%'
	svg.style.height = '100%'

	this.endCoordinate.forEach((ele, index) => {
		let path = this.createPathBG(index)
		svg.appendChild(path[2])
		svg.appendChild(path[0])
		svg.appendChild(path[1])
	})
	this.el.appendChild(svg)
}
DrawRing.prototype.createPathBG = function(index) {
	let pathBG = document.createElementNS('http://www.w3.org/2000/svg', 'path')
	let path = document.createElementNS('http://www.w3.org/2000/svg', 'path')
	// 环上三角形指示器
	let pathTriangle = document.createElementNS('http://www.w3.org/2000/svg', 'path')

	pathTriangle.id = `pathTriangle${index}`
	pathBG.id = `pathBG${index}`
	path.id = `path${index}`
	// 设置背景环样式
	setStyle(pathBG, {
		'transform': 'translate(300,300)',
		'stroke': this.option.colorsBG[index],
		'stroke-width': this.ringWidth,
		'fill': 'none'
	})
	// 设置实体环样式
	setStyle(path, {
		'transform': 'translate(300,300)',
		'stroke': this.option.colors[index],
		'stroke-width': this.ringWidth,
		'fill': 'none'
	})
	// 设置三角形指示器样式
	setStyle(pathTriangle, {
		'd': `M 280 ${(300 - this.r) + 10} l 40 0 l -20 -20`,
		'transform-origin': '300px 300px',
		'transform': `rotate(${this.middleDegs[index]})`,
		'opacity': 0
	})
	if(this.option.data[index].value < 50) {
		pathTriangle.setAttribute('fill', this.option.colorsBG[index])
	} else {
		pathTriangle.setAttribute('fill', this.option.colors[index])
	}
	setTimeout(() => {
		pathTriangle.style.transition = '0.5s'
		setStyle(pathTriangle, {
			'd': `M 280 ${(300 - this.r) - 10} l 40 0 l -20 -20`,
			'opacity': 1
		})
	}, 810)

	let r = this.r
	// 公用起点
	let startXBG = index ? this.endCoordinateBG[index - 1].x1 : 0
	let startYBG = index ? this.endCoordinateBG[index - 1].y1 : -r

	// 背景环
	let endXBG = this.endCoordinateBG[index].x0
	let endYBG = this.endCoordinateBG[index].y0
	let lenghtysBG = this.lenghtysBG[index]
	let descriptionsBG = ['M', startXBG, startYBG, 'A', r, r, 0, lenghtysBG, 1, endXBG, endYBG]
	pathBG.setAttribute('d', descriptionsBG.join(' '))

	// 实环
	let endX = this.endCoordinate[index].x
	let endY = this.endCoordinate[index].y
	let lenghtys = this.lenghtys[index]
	let descriptions = ['M', startXBG, startYBG, 'A', r, r, 0, lenghtys, 1, endX, endY]
	path.setAttribute('d', descriptions.join(' '))

	// 实环动画
	let pathLen = path.getTotalLength()
	path.style.strokeDasharray = `${pathLen}, ${pathLen * 2}`
	path.style.strokeDashoffset = pathLen + 'px'
	setTimeout(() => {
		path.style.transition = '.8s'
		path.style.strokeDashoffset = 0 + 'px'
	}, 10)

	let pathLenBG = pathBG.getTotalLength()
	pathBG.style.strokeDasharray = `${pathLenBG}, ${pathLenBG * 2}`
	pathBG.style.strokeDashoffset = pathLenBG + 'px'
	setTimeout(() => {
		pathBG.style.transition = '.4s'
		pathBG.style.strokeDashoffset = 0 + 'px'
	}, 10)

	return [pathBG, path, pathTriangle]
}

// 查询三角指示器旋转角度

DrawRing.prototype.getMiddleDegs = function() {
	return this.middleDegs
}

function setStyle(el, obj) {
	for(var item in obj) {
		el.setAttribute(item, obj[item])
	}
}

初始化

var option = {
	data: [{
			name: 'Java',
			pro: 5,
			value: 40
		},
		{
			name: 'JavaScript',
			pro: 4.5,
			value: 78
		},
		{
			name: 'Python',
			pro: 4,
			value: 78
		},
		{
			name: 'C++',
			pro: 3.5,
			value: 88
		},
		{
			name: 'Android',
			pro: 3,
			value: 77
		},
		{
			name: 'IOS',
			pro: 2.5,
			value: 88
		}
	],
	colors: ['red', 'green', '#f40', '#ff00ff', '#f60', '#f80'],
	colorsBG : ['#ffb3b3', '#b3d9b3', '#ffc7b3', '#ffb3ff', '#ffd1b3', '#ffdbb3']
}

var Ring = new DrawRing(document.getElementById("ring-box"))
Ring.init(option, 150, 30, 15)

// 以下代码是图上占比和占比样式和动画代码
// 方便修改样式写在外部,不用svg写
let degs = Ring.getMiddleDegs()
option.data.forEach((ele, index) => {
	labels.innerHTML += `
		<div class="label-item" style='opacity: 0;transform: rotate(${degs[index]}deg) translateX(-50%)'>
			<div style='transform: rotate(${-degs[index]}deg)'>
				<span>${ele.value}</span>
				<p>${ele.name}</p>
			</div>
		</div>
	`
	
	setTimeout(() => {
		let item = document.getElementsByClassName("label-item")[index]
		item.style.opacity = 1
	}, 1000)
})