最近項目中遇到一些佔比環形圖的需求(如圖 ),初始設計方案定爲: 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>
複製代碼