饼图
上次年中总结的时候说过,试用svg画了个饼图,这篇文章就详细说一下过程.本文是在vue-cli脚手架搭建的项目下,写的一个组件
环境
版本
-
vue2.*
-
snapsvg --> 0.5.1
-
imports-loader --> 0.8.0
这里需要注意的是imports-loader的版本,版本不同会导致下面的引入方式出错
安装
npm install --save snapsvg || yarn add snapsvg
npm install --save-dev imports-loader@0.8.0 || yarn add imports-loader@0.8.0 --save-dev
引入
// 在vue组件中引入,
const snap = require(`imports-loader?this=>window,fix=>module.exports=0!snapsvg/dist/snap.svg.js`)
动手
思路
- 根据数据算出占比
- 根据占比算角度
- 根据角度和初始坐标计算结束的坐标
代码
pie.vue
<template>
<svg :id="id" :width="width" :height="height"></svg>
</template>
<script>
const snap = require(`imports-loader?this=>window,fix=>module.exports=0!snapsvg/dist/snap.svg.js`);
export default {
name: "svg-pie",
props: {
width: {
// eslint-disable-next-line vue/require-prop-type-constructor
type: Number | String,
default: "100%",
},
height: {
// eslint-disable-next-line vue/require-prop-type-constructor
type: Number | String,
default: "100%",
},
r: {
type: Number,
default: 70,
},
id: {
type: String,
required: true,
},
title: {
type: String,
required: true,
},
num: {
type: Number,
required: true,
},
cx: {
type: Number,
default: 85,
},
cy: {
type: Number,
default: 85,
},
data: {
type: Array,
required: true,
},
},
mounted() {
this.init();
},
watch: {
data() {
this.init();
},
},
methods: {
init() {
// 如果data没有数据 return
if (this.data.length < 0) return;
// snap svg实例
let s = snap(`#${this.id}`);
// 半径
let r = this.r;
let cx = this.cx;
let cy = this.cy;
let sx = cx;
let sy = cy - r;
let srartX = sx;
let srartY = sy;
let endX;
let endY;
let angles = 0;
let largeArcFlag;
let dataArry = this.data;
let lh = 16;
let domArr = [];
dataArry.forEach((v, i) => {
// 计算角度
let angle = (360 * v.ratio) / 100;
angles += angle;
let radian;
// 这里计算下一个坐标 很繁琐 需要优化
if (angles < 90) {
radian = ((2 * Math.PI) / 360) * angles;
endX = sx + r * Math.sin(radian);
endY = sy + r - r * Math.cos(radian);
} else if (angles === 90) {
endX = sx + r;
endY = sy + r;
} else if (angles < 180) {
radian = ((2 * Math.PI) / 360) * (180 - angles);
endX = sx + Math.abs(r * Math.cos(radian));
endY = sy + r + Math.abs(r * Math.sin(radian));
} else if (angles === 180) {
endX = sx;
endY = sy + 2 * r;
} else if (angles < 270) {
radian = ((2 * Math.PI) / 360) * (270 - angles);
endX = sx - Math.abs(r * Math.sin(radian));
endY = sy + r + Math.abs(r * Math.cos(radian));
} else if (angles === 270) {
endX = sx - r;
endY = sy + r;
} else if (angles === 360) {
endX = sx;
endY = sy;
} else {
radian = ((2 * Math.PI) / 360) * (360 - angles);
endX = sx - Math.abs(r * Math.sin(radian));
endY = sy + r - Math.abs(r * Math.cos(radian));
}
largeArcFlag = v.ratio > 50 ? 1 : 0;
let dom = s
.path(
`M${srartX},${srartY}A${r},${r},${angles},${largeArcFlag},1,${endX},${endY}`
)
.attr({
stroke: v.color,
strokeWidth: 20,
fillOpacity: 0,
cursor: "pointer",
})
.hover(
function () {
this.animate({
strokeWidth: 35,
},
100
);
},
function () {
this.animate({
strokeWidth: 20,
},
100
);
}
);
domArr.push(dom);
s.circle(cx - r, cy + r + 20 + lh, 3)
.attr({
fill: v.color,
strokeWidth: 0,
stroke: "#000",
})
.hover(
function () {
domArr[i].animate({
strokeWidth: 35,
},
100
);
},
function () {
domArr[i].animate({
strokeWidth: 20,
},
100
);
}
);
let titleDom = s
.text(cx - r + 10, cy + r + lh + 25, v.title)
.hover(
function () {
domArr[i].animate({
strokeWidth: 35,
},
100
);
},
function () {
domArr[i].animate({
strokeWidth: 20,
},
100
);
}
)
.attr({
cursor: "pointer",
});
if (v.blodArr) {
v.blodArr.forEach((val) => {
titleDom.node.childNodes[val].style.fontWeight = "bold";
});
}
lh += 20;
srartX = endX;
srartY = endY;
});
s.circle(cx, cy, r - 7.5).attr({
fill: "#e3eaef",
strokeWidth: 0,
});
s.circle(cx, cy, r - 15).attr({
fill: "#fff",
strokeWidth: 0,
});
s.text(cx, cy - 8, this.title).attr({
textAnchor: "middle",
});
s.text(cx, cy + 20, this.num).attr({
fontSize: "16px",
fill: "#6462fe",
fontWeight: "bold",
textAnchor: "middle",
});
},
},
};
</script>
使用
App.vue
<template>
<div id="app">
<Pie id="course" title="总数" :num="22" :r="60" :width="600" :height="600" :cy="100" :data="data" />
</div>
</template>
<script>
import Pie from "./components/pie.vue";
export default {
name: "App",
components: {
Pie,
},
data() {
return {
data: [{
blodArr: [2, 5],
color: "#635ffd",
num: 11,
percent: 50,
ratio: 50,
title: ["类型一", " ", 11, "个", " ", 50, "%"]
},
{
blodArr: [2, 5],
color: "#666",
num: 11,
percent: 50,
ratio: 50,
title: ["类型二", " ", 11, "个", " ", 50, "%"]
}
],
};
},
};
</script>
各属性的意义
属性名 | 说明 |
---|---|
title | 中间圆盘的内容 |
num | 中间圆盘的内容 |
r | 半径 |
width | svg宽 |
height | svg高 |
cy | 圆心y坐标 |
cx | 圆心x坐标 |
data | 数据 |
data各字段详解
key | 说明 |
---|---|
color | 该饼图的颜色 |
num | 数量 |
ratio | 占比 |
title | 说明, 可以是字符串和数组 |
blodArr | title内容要加粗的下标(title为数组时生效) |
成果
总结
难点在于path画线时的坐标计算, 在这里对我数学老师道歉, 我感觉自己就是个废人了!!! 看完点个赞吧!!