SVG绘制等份环图
需求如下图
使用技术:svg.js和无敌的jQuery
我们需要使用svg的path绘制每项数据的环图份额
path元素的属性d用于定义路径,属性d实际上是一个字符串,包含了一系列路径描述。这些路径由下面这些指令组成:Moveto,Lineto,Curveto,Arcto,ClosePath。
我们会用到的指令有:
Moveto(移动画笔到起始点),语法:‘M x,y’ 在这里x和y是绝对坐标,分别代表水平坐标和垂直坐标;
Lineto(绘制直线),语法:‘L x, y’ 在这里x和y是绝对坐标,表示直线的结束点坐标;
Arcto(绘制弧曲线路径),语法:‘A rx,ry xAxisRotate LargeArcFlag,SweepFlag x,y’,rx和ry分别是x和y方向的半径(绘制圆弧时,rx和ry相等);LargeArcFlag的值确定是要画小弧或大弧,0表示画小弧(即画两点之间弧长最小的弧),1表示画大弧(当弧度大于Math.PI时需要画大弧);SweepFlag用来确定画弧的方向,0逆时针方向,1顺时针方向;x和y是目的地的坐标;
ClosePath(闭合路径),语法是’Z’或’z’;
我们需要用path绘制如下的路径:
如图:份额的绘制是先使用M命令移动到P0,L命令绘制一条直线到P1,A命令从P1画弧到P2,L命令从P2绘制一条直线到P3,A命令从P3绘制一条弧线到P0,最后Z命令关闭路径。然后我们只要填充这个路径就可以完成每项份额绘制了。这里我们需要求出4个点的绝对坐标,如何计算这四个坐标?
如图,通过三角函数,我们就可以计算出每个点的位置。我们已知原点O坐标(画布中点)、外环半径R和内环半径r(我们自己给定);再通过计算出每项数据的弧度占比,我们就可以得到每项数据的起始弧度(即上一项的结束弧度,第一项为0)和结束弧度(起点+项数据的弧度占比)。x值为原点x+sin(angel)*半径,y值为原点y-cos(angel)*半径
所以我们可以将计算点坐标的运算封装成一个函数
/* ==== 计算Xy坐标 ==== */
/**
* @param {[type]} r [半径]
* @param {[type]} angel [角度]
* @param {[type]} origin [原点坐标]
* @return {[Array]} 坐标
*/
function evaluateXY(r, angel, origin) {
return [
origin[0] + Math.sin(angel) * r,
origin[0] - Math.cos(angel) * r,
];
}
所有代码如下:
注意:因为我比较菜,所以在文字布局排版这一块是用的比较笨的方法,如果有更加优雅的方法可以评论或者私信一起探讨一下。
// 使用svg.js和jq
SVG.on(document, "DOMContentLoaded", function() {
let draw = SVG().addTo("body").size(560, 560);
// 调用函数 这里数据放到别的文件了 放在文章最后面参考
drawTorus(arr, 560, 560, 280, 180, "outTorus"); // 外圆环
drawTorus(arr2, 560, 560, 180, 120, "inTorus"); // 内圆环
drawTorus(arr3, 560, 560, 120, 120, "circle"); // 圆
/* ===== 绘制圆环函数 ===== */
/**
* @param {Array} data - [数据]
* @param {Number} svgW - [svg宽度]
* @param {Number} svgH - [svg高度]
* @param {Number} R - [外弧起终点计算半径]
* @param {Number} r - [内弧起终点计算半径]
* @param {String} str - [g标签的类名]
*/
function drawTorus(data, svgW, svgH, R, r, str) {
if (data.length == 1) {
// 当传进来的数据长度为1时 调用画圆函数绘制一个圆
drawCircle();
return;
}
let arr = data; // 数据
let origin = [svgW / 2, svgH / 2]; // svg中心点
let out_R = R; // 外弧起终点计算半径
let in_r = r; // 内弧起终点计算半径
let sAngel = 0; // 起始点角度
let eAngel = sAngel; // 结束点角度
let drawData = []; // 保存遍历后可直接绘制的数据
let group = draw.group().attr("class", str); // 创建一个g标签
let textSize = str == "inTorus" ? 20 : 12; // 绘制文字 文字默认像素12px 内圆环我们设置20px
for (let k of arr) {
// 处理数据
let itemData = Object.assign({}, k); // 复制一遍 不修改原数据
eAngel = sAngel + (1 / data.length) * 2 * Math.PI; // 分成num份 一份的弧度
itemData.arcsineStarts = [
evaluateXY(r, sAngel, origin), // p0
evaluateXY(R, sAngel, origin), // p1
evaluateXY(R, eAngel, origin), // p2
evaluateXY(r, eAngel, origin), // p3
];
//大于Math.PI需要画大弧,否则画小弧
itemData.LargeArcFlag = eAngel - sAngel > Math.PI ? "1" : "0";
drawData.push(itemData); // 保存到数组中 绘制的时候遍历
sAngel = eAngel; // 将下一项的起始弧度设置为当前项的结束弧度
}
// 绘制圆环
for (let v of drawData) {
// 创建圆环的item 将路径添加到item 再将item添加到外圆环
let group_item = draw.group().attr("class", "Torus-item");
// 取出坐标数据
let P = v.arcsineStarts;
// 绘制路径
let path =
`M ${P[0][0]} ${P[0][1]} L ${P[1][0]} ${P[1][1]} A ${R} ${R} 0 ${v.LargeArcFlag} 1 ${P[2][0]} ${P[2][1]} L ${P[3][0]} ${P[3][1]}z`;
// 绘制圆环
let torusPath = draw.defs().path(path).attr({
fill: v.color
});
let use = draw.use(torusPath);
// 绘制文字
let text = draw.text((add) => add.text(v.name).fill("#ffffff"));
let textPath = text.path(v.path).attr({
startOffset: "50%",
"text-anchor": "middle",
"font-size": `${textSize}px`,
});
// 1.将 use和text添加到group_item 2.将 group_item 添加到group
group_item.add(use);
group_item.add(text);
group.add(group_item);
}
/* ==== 计算Xy坐标 ==== */
/**
* @param {[type]} r [半径]
* @param {[type]} angel [角度]
* @param {[type]} origin [原点坐标]
* @return {[Array]} 坐标
*/
function evaluateXY(r, angel, origin) {
return [
origin[0] + Math.sin(angel) * r,
origin[0] - Math.cos(angel) * r,
];
}
/* ==== 绘制圆函数 ==== */
function drawCircle() {
let circleData = data[0]; // 获取单独的一条数据
let group = draw.group().attr("class", str); // 创建一个g标签
// 绘制一个圆
let circle = draw
.circle(r * 2)
.attr({
fill: circleData.color,
cx: svgW / 2,
cy: svgH / 2
});
// 绘制文字 并且再圆里面居中
let text = draw.text((add) =>
add
.tspan(circleData.name)
.fill("#ffffff")
.attr(
"style",
"font-size: 30px; font-weight: bold;dominant-baseline:middle;text-anchor:middle;"
)
.dx(svgW / 2)
.dy(svgH / 2)
);
// 必须先绘制圆 再绘制文字 否则文字将被覆盖
group.add(circle);
group.add(text);
}
}
/* ==== 对文字进行旋转操作 ==== */
$("text").each(function(index, item) {
let rotate = (360 / 7) * index;
$(this).css({
transform: `rotate(${rotate}deg)`,
});
});
/* ==== 外环对文字的布局 ==== */
SVG.find("text").slice(0, 7).forEach((text, index) => {
switch (index) {
case 0:
text.find("tspan")[0].dy(5).x(60);
text.find("tspan")[1].dy(30).x(70);
break;
case 1:
text.find("tspan")[0].dy(5).x(88).attr({
"letter-spacing": "0px"
});
text.find("tspan")[1].dy(30).x(95).attr({
"letter-spacing": "2px"
});
break;
case 2:
text.find("tspan")[0].dy(5).x(40);
text.find("tspan")[1].dy(30).x(40);
break;
case 3:
text.find("tspan")[0].dy(-5).x(135);
text.find("tspan")[1].dy(20).x(135);
text.find("tspan")[2].dy(20).x(88);
break;
case 4:
text.find("tspan")[0].dy(5).x(40);
text.find("tspan")[1].dy(30).x(40);
break;
case 5:
text.find("tspan")[0].dy(-5).x(120);
text.find("tspan")[1].dy(20).x(90);
text.find("tspan")[2].dy(20).x(130);
break;
case 6:
text.find("tspan")[0].dy(-5).x(170).attr({
"letter-spacing": "0px"
});
text.find("tspan")[1].dy(20).x(180).attr({
"letter-spacing": "0px"
});
text.find("tspan")[2].dy(20).x(180).attr({
"letter-spacing": "2px"
});
break;
}
})
/* ==== 内环对文字的布局 ==== */
SVG.find("text").slice(7, 14).forEach((text, index) => {
if (index == 6) {
text.find("tspan")[0].dy(45).x(70);
text.find("tspan")[1].dy(20).x(20).font({
size: 12
})
} else if (index != 6 && index != 7) {
text.find("tspan").dy(0)
}
});
});
动画效果,这个简单 css完成
.outTorus,.inTorus {
transform-origin: center;
transition: transform 1s ease-in;
}
/* 外圆环 */
.outTorus {
animation: 30s rotate linear infinite;
}
/* 内圆环 */
.inTorus {
animation: 30s rever_rotate linear infinite;
}
/* 旋转动画 */
@keyframes rotate {
0% {}
100% {
transform: rotate(360deg);
}
}
/* 旋转动画 */
@keyframes rever_rotate {
0% {}
100% {
transform: rotate(-360deg);
}
}
所使用的数据
/* ==== 外圆环数据 ==== */
let arr = [{
name: "贝贝贝贝贝贝贝贝系统\n贝贝贝贝贝贝系统",
color: "#60963e",
path: "M280,40 A240,240 0 0,1 467.63955579232714,130.36244755390396",
},
{
name: "贝贝贝贝/贝贝/贝贝贝贝贝贝贝贝贝贝\n贝贝贝贝 贝贝贝贝 贝贝贝贝",
color: "#6c5c98",
path: "M280,44 A236,236 0 0,1 464.512229862455,132.8564067613389",
},
{
name: "贝贝贝贝贝贝\n贝贝贝贝贝贝",
color: "#67958b",
path: "M280,40 A240,240 0 0,1 467.63955579232714,130.36244755390396"
},
{
name: "贝贝贝贝贝贝\n贝贝贝贝贝贝\n贝贝贝贝贝贝贝(风采展示)",
color: "#9b5c61",
path: "M280,40 A240,240 0 0,1 467.63955579232714,130.36244755390396",
},
{
name: "贝贝贝贝贝贝\n贝贝贝贝贝贝",
color: "#967d47",
path: "M280,40 A240,240 0 0,1 467.63955579232714,130.36244755390396"
},
{
name: "贝贝贝贝贝贝贝贝\n贝贝贝贝(贝贝)贝贝贝贝\n贝贝贝贝贝贝",
color: "#398095",
path: "M280,40 A240,240 0 0,1 467.63955579232714,130.36244755390396",
},
{
name: "贝贝贝贝贝贝 贝贝贝贝贝 贝贝贝贝\n贝贝贝贝(贝贝贝)贝贝贝贝贝\n贝贝贝贝贝贝贝贝(贝贝贝)",
color: "#9b5568",
path: "M280,40 A240,240 0 0,1 467.63955579232714,130.36244755390396",
},
];
/* ==== 内圆环数据 ==== */
let arr2 = [{
name: "贝贝",
color: "#db5672",
path: "M280,140 A140,140 0 0,1 389.45640754552414,192.71142773977732",
},
{
name: "贝贝",
color: "#19a6d1",
path: "M280,140 A140,140 0 0,1 389.45640754552414,192.71142773977732",
},
{
name: "贝贝",
color: "#d89e1d",
path: "M280,140 A140,140 0 0,1 389.45640754552414,192.71142773977732",
},
{
name: "贝贝",
color: "#d63440",
path: "M280,140 A140,140 0 0,1 389.45640754552414,192.71142773977732",
},
{
name: "贝贝",
color: "#37bcb8",
path: "M280,140 A140,140 0 0,1 389.45640754552414,192.71142773977732",
},
{
name: "贝贝",
color: "#735cc6",
path: "M280,140 A140,140 0 0,1 389.45640754552414,192.71142773977732",
},
{
name: "贝贝\n(贝贝、贝贝、贝贝)",
color: "#39b275",
path: "M280,80 A200,200 0 0,1 436.366296493606,155.30203962825328",
},
];
/* ==== 圆数据 ==== */
let arr3 = [{
name: "贝贝贝贝贝贝",
color: "#6d8dc6"
}, ];
参考文章:SVG绘制环图
svg.js官网:https://svgjs.com/docs/3.1/