最近项目中遇到一些占比环形图的需求(如图 ),初始设计方案定为: 1. svg , 2.css3, 3. canvas 。 最终调研后决定使用svg
,业务逻辑更加清晰及技术成本更低一些。
javascript
使用 svg
画这种图,确定就须要用到 svg
的 <path>
元素的椭圆弧指令(A)css
SVG椭圆弧路径指令说明:
html
如此, 咱们的实现逻辑就出来了: 把每一个占比用 path
元素实现,每一段拼接(由于每一个数据占比%最后以后确定为100%)便可造成一个环形圆
vue
- 最后获得的是一个环形圆,因此椭圆的长短半轴rx, ry在这里就是最后圆的半径
- x-axis-rotation 是占比对应的度数,也就是 2 * PI * per(对应占比 %)
- large-arc-flag: 若是旋转度数(deg) 超过了180度 则为1 不然为 0 ,这里也就表示 若是占比超过50%则为1, 不然为0
- sweep-flag 表示是否为 顺时针 画图, 咱们在这里认定 顺时针便可
- 圆弧的终点(x, y) ,这里的逻辑须要经过数学公式须要计算出终点,这里要提醒一点,其实本次的终点就是下一段的起点,这一点颇有用
接下来请拿起小本本记下来,这是重点,必考,5分爱要不要~
已知圆心,半径,角度,求圆上的点坐标java
圆心坐标:(x0,y0) 半径:r 角度:deg 单位:°css3
圆周率: PInpm
则圆上任一点为:(x1,y1)canvas
x1 = x0 + r * cos(deg)app
y1 = y0 + r * sin(deg)svg
这里还有一点:
数学中,咱们的坐标系为这样(左),可是在业务中咱们须要这样(右)
这里调整很简单,只须要给svg画布 设置一个旋转就好
transform: rotate(-90deg);
复制代码
基本业务逻辑及注意点已经梳理完,接下来就是代码部分
为了简单方便,我经过cdn引入了vue.js
// doughnut.js
let vm = new Vue({
el: '#app',
data: {
list: [ // 占比列表
'30%',
'20%',
'10%',
'5%',
'8%',
'2%',
'15%',
'3.33%',
'3%',
'3.64%',
],
renderList: [],// 处理后用于渲染环形图的数据
svgData: { // svg 数据 即画布参数
width: 200,
height: 200
},
arcData: { // 环形图参数
r: 80, // 环形图的半径
x0: 100, // 圆心x,通常把环形图放在画布中心位置就好
y0: 100, // 同上
stockWidth: 20 // 环形图的粗度...
},
colorMap: [ // 环形图颜色映射表
'#3C76FF',
'#36E1E2',
'#92E27B',
'#FAD850',
'#F89E35',
'#EA5486',
'#EF4A4A',
'#BF6FE4',
'#6CBE6A',
'#E1E1E1'
]
},
created() {
this.renderList = this.handleChartData(this.list);
},
filters: {
getPath(cur, arcData) {
// 这里在经过 圆心(x0, y0) r ,拼接好路径数据
const {x0, y0, r} = arcData;
let str = 'M';
const isLargeArc = cur.relayPer > 50 ? 1 : 0;
const startX = cur.start.x * r + x0;
const startY = cur.start.y * r + y0;
const endX = cur.end.x * r + x0;
const endY = cur.end.y * r + y0;
str += ' ' + startX
+ ' ' + startY
+ ' ' + 'A'
+ ' ' + r
+ ' ' + r
+ ' ' + '0'
+ ' ' + isLargeArc
+ ' ' + '1'
+ ' ' + endX
+ ' ' + endY;
return str;
}
},
methods: {
handleChartData(list) {
// 这里按照 圆心点为(0,0), r 为 1 来处理
const newList = [];
list.forEach((item, index) => {
const obj = {};
let per = +item.split('%')[0];
// 保留真实占比,后面须要判断是不是大小弧
obj.relayPer = per;
const PI = Math.PI;
if (index !== 0) {
per += newList[index - 1].per;
}
// 由于是拼接,因此本次的终点要在以前的基础上,所要要累加占比
obj.per = per;
const deg = (per / 100) * PI * 2;
obj.start = {};
obj.end = {};
if (index === 0) {
obj.start.x = Math.cos(0);
obj.start.y = Math.sin(0);
}
else {
obj.start = newList[index - 1].end;
}
obj.end.x = Math.cos(deg);
obj.end.y = Math.sin(deg);
newList.push(obj);
});
return newList;
}
}
});
复制代码
// doughnut.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>doughnut</title>
<style> .doughnut-svg { display: block; margin: 0 auto; transform: rotate(-90deg); } </style>
</head>
<body>
<div id="app">
<h3 style="text-align: center;">svg--环形图</h3>
<svg :width="svgData.width" :height="svgData.height" :viewBox="`0 0 ${svgData.width} ${svgData.height}`" class="doughnut-svg" xmlns="http://www.w3.org/2000/svg" >
<path v-if="renderList && renderList.length > 0" v-for="(cur, index) in renderList" :key="index" :d="cur | getPath(arcData)" :stroke="colorMap[index]" :stroke-width="arcData.stockWidth" fill="none" />
</svg>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue@2.6.10/dist/vue.js"></script>
<script src="./doughnut.js"></script>
</body>
</html>
复制代码