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

以下是所有的代码,感兴趣的可以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)
})