10月份因为疫情原因、又开启了居家办公模式,空闲之余,与其选择“躺平”,不如去做一些有意义的事情,内心的想法驱使着我去做些什么,但是又没有合适的素材,直到接手了最近的一个可视化项目,一个图表勾起了我无限的好奇心,本着对技术死磕到底的想法,于是开启了我的探索之旅。具体的原型效果如下:
关于此类进度条的实现方式,在我之前的章节(SVG绘制圆环进度条)中也有涉及,本章则另辟蹊跷,从另一个维度简单介绍一下CSS锥形渐变(conic-gradient)在可视化图表中的应用场景。本章依旧采用vue+原生css的形式进行案例展示、在了解本章节之前,需要对vue框架、css变量、css属性conic-gradient有一定程度的认识。案例实现效果如下:
实现思路:首先从原型图入手,我们可以将效果图进行拆分,背景圆环+进度圆环+进度条开始处小圆点(和边框一样大小、模拟圆角效果)+进度尾部圆点+进度尾部小眼睛+进度条中心内容。因此我们只需要将以上几个小功能点实现即可
1.背景圆环:div添加背景颜色+圆角
2.进度圆环:使用css属性conic-gradient进行进度控制
3.进度条开始处小圆点:使用伪元素(::before或::after)或div均可,定位解决
4.进度条尾部圆点:相当于在一个指针上添加一个小球,然后将指针根据数值旋转一定的角度
5.进度尾部小眼睛:使用指针头部小球元素的伪元素进行定位
6.进度条中心内容:可根据需要,使用插槽的形式解决
首先看一下前两个图表的具体实现细节:
<!-- demo1.vue -->
<template>
<div class="chart-box" :style="styObj">
<!-- 进度条部分 -->
<div class="outer-box">
<div class="inner-box">
<div class="pointer-box"></div>
</div>
</div>
<!-- 插槽内容 -->
<div class="slot-content">
<slot></slot>
</div>
</div>
</template>
<script>
export default {
props: {
rate: {
type: Number,
default: 0,
},
config: {
type: Object,
default: () => {
return {};
},
},
},
computed: {
styObj() {
let rate = 0;
if (this.rate <= 0) {
rate = 0;
} else if (this.rate >= 1) {
rate = 1;
} else {
rate = this.rate;
}
let endPos = `${rate * 100}%`;
let obj = Object.assign({}, this.defaultConfig, this.config);
let rotate = `rotate(${360 * rate}deg)`;
let chartRotate = obj.clockwise ? "rotateY(0deg)" : "rotateY(180deg)";
let showEyes = obj.showEyes ? 1 : 0;
return {
"--background-image": `conic-gradient(${obj.startColor} 0%, ${obj.endColor} ${endPos}, transparent ${endPos})`,
"--border-width": obj.borderWidth,
"--dot-width": obj.circleSize,
"--pointer-rotate": rotate,
"--background-color": obj.borderBackground,
"--center-gap-bg": obj.centerCircleBg,
"--circle-color": obj.circleColor,
"--clockwise-wise": chartRotate,
"--show-eyes": showEyes,
"--eyes-size": obj.eyesSize,
"--start-color": obj.startColor,
};
},
},
data() {
return {
/* 此配置下所有属性均可在config中进行覆盖,实现个性化配置 */
defaultConfig: {
borderWidth: "8px", // 描边宽度
borderBackground: "#eee", // 描边背景颜色
circleSize: "16px", // 结尾处圆点直径
circleColor: "#2ec4a7", // 结尾处圆点颜色
startColor: "#d5f4ee", // 进度条起始颜色
endColor: "#2ec4a7", // 进度条结束颜色
centerCircleBg: "#fff", // 中间空心圆背景
clockwise: true, // 是否顺时针
showEyes: false, // 是否显示结尾处小眼睛
eyesSize: "8px", // 结尾处小眼睛大小
},
};
},
};
</script>
<style scoped>
.chart-box {
position: relative;
width: 100%;
height: 100%;
}
/* 核心代码、控制进度条样式及进度 */
.outer-box {
width: 100%;
height: 100%;
border-radius: 50%;
box-sizing: border-box;
background-color: var(--background-color);
background-image: var(--background-image);
padding: var(--border-width);
transform: var(--clockwise-wise);
}
/* 开始处增加一个圆形端点, 模拟圆角效果 */
.outer-box::after {
content: "";
width: var(--border-width);
height: var(--border-width);
border-radius: 50%;
position: absolute;
left: 50%;
top: 0;
transform: translateX(-50%);
background: var(--start-color);
}
/* 中间添加一个和背景色一样的圆圈 */
.inner-box {
position: relative;
width: 100%;
height: 100%;
border-radius: 50%;
background: var(--center-gap-bg);
}
/* 指示针 */
.pointer-box {
position: absolute;
left: 50%;
top: calc(0px - var(--border-width) / 2);
bottom: calc(0px - var(--border-width) / 2);
z-index: 1;
transform-origin: center center;
transform: var(--pointer-rotate);
}
/* 指示针的头部添加一个小圆点 */
.pointer-box::after {
content: "";
position: absolute;
left: 50%;
top: 0;
width: var(--dot-width);
height: var(--dot-width);
border-radius: 50%;
background: var(--circle-color);
transform: translate(-50%, -50%);
}
/* 进度条结尾处添加一个小眼睛,背景白色 */
.pointer-box::before {
content: "";
position: absolute;
left: 50%;
top: 0;
width: var(--eyes-size);
height: var(--eyes-size);
border-radius: 50%;
background: #fff;
transform: translate(-50%, -50%);
z-index: 1;
opacity: var(--show-eyes);
}
/* 插槽内容样式 */
.slot-content {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
}
</style>
分析:从代码中不难看出、进度圆环中间的空白地方(class类名为“inner-box”)使用了一个背景为白色的元素进行遮盖,这点需要根据具体场景进行微调,在纯色背景下并无大碍,但是在有背景图的场景下,显示效果就有点差强人意了。因此此处需要做一下优化,扒一扒css手册,刚好有一个属性可以解决这个问题,那就是mask属性了,优化后代码如下,实现效果见第三、四个图表
<!-- demo2 -->
<template>
<div class="chart-box" :style="styObj">
<!-- 进度条部分 -->
<div class="process-wrapper">
<div class="process-box"></div>
<div class="pointer-box"></div>
</div>
<!-- 插槽内容 -->
<div class="slot-content">
<slot></slot>
</div>
</div>
</template>
<script>
export default {
props: {
rate: {
type: Number,
default: 0,
},
config: {
type: Object,
default: () => {
return {};
},
},
},
computed: {
styObj() {
let rate = 0;
if (this.rate <= 0) {
rate = 0;
} else if (this.rate >= 1) {
rate = 1;
} else {
rate = this.rate;
}
let endPos = `${rate * 100}%`;
let obj = Object.assign({}, this.defaultConfig, this.config);
let rotate = `rotate(${360 * rate}deg)`;
let chartRotate = obj.clockwise ? "rotateY(0deg)" : "rotateY(180deg)";
let showEyes = obj.showEyes ? 1 : 0;
return {
"--background-image": `conic-gradient(${obj.startColor} 0%, ${obj.endColor} ${endPos}, transparent ${endPos})`,
"--border-width": obj.borderWidth,
"--dot-width": obj.circleSize,
"--pointer-rotate": rotate,
"--background-color": obj.borderBackground,
"--circle-color": obj.circleColor,
"--clockwise-wise": chartRotate,
"--show-eyes": showEyes,
"--eyes-size": obj.eyesSize,
"--start-color": obj.startColor,
};
},
},
data() {
return {
/* 此配置下所有属性均可在config中进行覆盖,实现个性化配置 */
defaultConfig: {
borderWidth: "8px", // 描边宽度
borderBackground: "#eee", // 描边背景颜色
circleSize: "16px", // 结尾处圆点直径
circleColor: "#2ec4a7", // 结尾处圆点颜色
startColor: "#d5f4ee", // 进度条起始颜色
endColor: "#2ec4a7", // 进度条结束颜色
clockwise: true, // 是否顺时针
showEyes: false, // 是否显示结尾处小眼睛
eyesSize: "8px", // 结尾处小眼睛大小
},
};
},
};
</script>
<style scoped>
.chart-box {
position: relative;
width: 100%;
height: 100%;
}
/* 将图表和插槽内容分开,便于控制进度条顺时针亦或是逆时针 */
.process-wrapper {
position: relative;
width: 100%;
height: 100%;
transform: var(--clockwise-wise);
}
/* 开始处增加一个圆形端点,模拟圆角效果 */
.process-wrapper::after {
content: "";
width: var(--border-width);
height: var(--border-width);
border-radius: 50%;
position: absolute;
left: 50%;
top: 0;
transform: translateX(-50%);
background: var(--start-color);
}
/* 核心代码、控制进度条样式及进度 */
.process-box {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
border-radius: 50%;
box-sizing: border-box;
background-color: var(--background-color);
background-image: var(--background-image);
-webkit-mask: radial-gradient(
closest-side at center center,
transparent calc(100% - var(--border-width)),
#fff calc(100% - var(--border-width))
);
mask: radial-gradient(
closest-side at center center,
transparent calc(100% - var(--border-width)),
#fff calc(100% - var(--border-width))
);
}
/* 指示针 */
.pointer-box {
position: absolute;
left: 50%;
top: calc(0px + var(--border-width) / 2);
bottom: calc(0px + var(--border-width) / 2);
z-index: 1;
transform: var(--pointer-rotate);
}
/* 指示针的头部(进度条结尾处)添加一个小圆点 */
.pointer-box::after {
content: "";
position: absolute;
left: 50%;
top: 0;
width: var(--dot-width);
height: var(--dot-width);
border-radius: 50%;
background: var(--circle-color);
transform: translate(-50%, -50%);
}
/* 进度条结尾处添加一个小眼睛,背景白色 */
.pointer-box::before {
content: "";
position: absolute;
left: 50%;
top: 0;
width: var(--eyes-size);
height: var(--eyes-size);
border-radius: 50%;
background: #fff;
transform: translate(-50%, -50%);
z-index: 1;
opacity: var(--show-eyes);
}
/* 插槽内容样式 */
.slot-content {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
}
</style>
至此、已完成了进度条的优化改造、不过还存在一个小小的瑕疵、使用mask后,进度条内部交合区域稍微也有一点锯齿感,这个暂时还没有找到优化措施,不过并无大碍。
为了实现更加丰富的展现形式、我们可以在进度条上添加分割线实现花纹效果、这个其实也不麻烦、只需要再添加一层锥形渐变即可解决,具体实现如下:
<!-- demo3 -->
<template>
<div class="chart-box" :style="styObj">
<div class="process-box">
<div class="center-mask"></div>
</div>
<!-- 插槽内容 -->
<div class="slot-content">
<slot></slot>
</div>
</div>
</template>
<script>
export default {
props: {
rate: {
type: Number,
default: 0,
},
config: {
type: Object,
default: () => {
return {};
},
},
},
computed: {
styObj() {
let rate = 0;
if (this.rate <= 0) {
rate = 0;
} else if (this.rate >= 1) {
rate = 1;
} else {
rate = this.rate;
}
let endPos = `${rate * 100}%`;
let obj = Object.assign({}, this.defaultConfig, this.config);
let rotate = `rotate(${360 * rate}deg)`;
let chartRotate = obj.clockwise ? "rotateY(0deg)" : "rotateY(180deg)";
let bgInfo = [];
let gap = 100 / obj.gapNum;
for (let i = 0; i < obj.gapNum; i++) {
bgInfo.push(`#fff ${i * gap}%`);
bgInfo.push(`#fff ${i * gap + obj.lineWidth}%`);
bgInfo.push(`transparent ${i * gap + obj.lineWidth}%`);
bgInfo.push(`transparent ${(i + 1) * gap}%`);
}
return {
"--background-image": `conic-gradient(${bgInfo.join(",")})`,
"--background-image1": `conic-gradient(${obj.startColor} 0%, ${obj.endColor} ${endPos}, transparent ${endPos})`,
"--border-width": obj.borderWidth,
"--background-color": obj.borderBackground,
"--center-gap-bg": obj.centerCircleBg,
"--clockwise-wise": chartRotate,
};
},
},
data() {
return {
/* 此配置下所有属性均可在config中进行覆盖,实现个性化配置 */
defaultConfig: {
borderWidth: "8px", // 描边宽度
borderBackground: "#eee", // 描边背景颜色
startColor: "#d5f4ee", // 进度条起始颜色
endColor: "#2ec4a7", // 进度条结束颜色
centerCircleBg: "#fff", // 中间空心圆背景
clockwise: true, // 是否顺时针
gapNum: 10, // 分割段数
lineWidth: 2, // 间隔线宽度,百分比
},
};
},
};
</script>
<style scoped>
.chart-box {
position: relative;
width: 100%;
height: 100%;
}
.process-box {
position: relative;
width: 100%;
height: 100%;
border-radius: 50%;
padding: var(--border-width);
box-sizing: border-box;
background-color: var(--background-color);
background-image: var(--background-image), var(--background-image1);
transform: var(--clockwise-wise);
}
.center-mask {
width: 100%;
height: 100%;
border-radius: 50%;
background: var(--center-gap-bg);
}
/* 插槽内容样式 */
.slot-content {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
}
</style>
最后,在汇总页面中依次将三个组件引入,增加不同的个性化参数,即可实现封面的展示效果,贴一下汇总页面代码
<template>
<div class="page-box">
<div class="main-box">
<!-- 第一种实现方式,中间的镂空部分采用背景色(和页面背景一致)的形式 -->
<div class="module">
<conic-gradient :rate="0.8888">
<span class="slot-font1">88.88%</span>
</conic-gradient>
</div>
<div class="module">
<conic-gradient :rate="0.8888" :config="config">
<div class="slot-bg">
<span class="slot-font2">88.88%</span>
</div>
</conic-gradient>
</div>
<!-- 第二种实现方式,中间镂空部分采用遮罩(mask)的方式实现 -->
<div class="module">
<conic-mask
:rate="0.6666"
:config="{ showEyes: true, eyesSize: '6px', circleSize: '8px' }"
>
<span class="slot-font1">66.66%</span>
</conic-mask>
</div>
<div class="module">
<conic-mask :rate="0.6666" :config="config">
<div class="slot-bg">
<span class="slot-font2">66.66%</span>
</div>
</conic-mask>
</div>
</div>
<hr />
<!-- 锥形渐变实现花纹进度条 -->
<div class="main-box">
<div class="module">
<conic-process :rate="0.6666">
<span class="slot-font1">66.66%</span>
</conic-process>
</div>
<div class="module">
<conic-process
:rate="0.8888"
:config="{
startColor: '#e45739',
endColor: '#e45739',
borderBackground: '#fbedea',
gapNum: 20,
lineWidth: 1,
clockwise: false
}"
>
<div class="slot-bg">
<span class="slot-font2">88.88%</span>
</div>
</conic-process>
</div>
<div class="module">
<conic-process :rate="0.8888" :config="{ gapNum: 1, lineWidth: 0 }">
<span class="slot-font1">88.88%</span>
</conic-process>
</div>
<div class="module">
<conic-process
:rate="0.8888"
:config="{
startColor: '#e45739',
endColor: '#e45739',
borderBackground: '#fbedea',
gapNum: 1,
lineWidth: 0,
clockwise: false,
}"
>
<div class="slot-bg">
<span class="slot-font2">88.88%</span>
</div>
</conic-process>
</div>
</div>
</div>
</template>
<script>
import ConicGradient from "./demo1";
import ConicMask from "./demo2";
import ConicProcess from "./demo3";
export default {
components: {
ConicGradient,
ConicMask,
ConicProcess,
},
data() {
return {
config: {
borderWidth: "8px",
circleSize: "16px",
circleColor: "#e45739",
borderColor: "#d5f4ee",
startColor: "#eead99",
endColor: "#e45739",
borderBackground: "#fbedea",
centerCircleBg: "#fff",
clockwise: false,
showEyes: true,
},
};
},
};
</script>
<style scoped>
.page-box {
width: 100%;
height: 100%;
overflow: auto;
}
.main-box {
display: flex;
flex-wrap: wrap;
width: 100%;
}
.module {
width: 200px;
height: 200px;
box-sizing: border-box;
padding: 20px;
}
.slot-bg {
display: flex;
align-items: center;
justify-content: center;
width: 75%;
height: 75%;
border-radius: 50%;
background: #fbedea;
}
.slot-font1 {
color: #009d84;
font-size: 20px;
font-weight: bold;
}
.slot-font2 {
color: #e45638;
font-size: 20px;
font-weight: bold;
}
</style>
好了,本章节的内容就到这里,如小伙伴有疑问,可评论区留言、随时交流。