在可视化应用中,水球图也是一种常见的数据展示形式,关于使用CSS实现个性化水球,在相当长的一段时间并没有找到比较简洁的实现方式,因此在以往的可视化作品中,大多采用echarts插件-Liquid Fill Chart来实现,本章节结合CSS相关属性及SVG知识点,将实现水球图的思路简单讲解一下,以便在实际的项目中能够拿来即用、提高开发效率,同时能够对一些不常见的CSS属性有一个回顾。在了解本章节之前,需要对Vue框架、CSS变量、SVG相关知识点有一定程度的了解,具体展示效果如下
实现水球图的难点之处在于如何模拟水面的波纹?从实现效果上看,水面波纹其实就是一个平滑的曲线,这个使用CSS属性很难绘制出来,因此需要采用其他的方式实现,这就涉及到贝塞尔曲线的相关概念,这里不做过多阐述,具体使用的时候可以不用关注这一点。 本案例采用了SVG中的路径属性,通过绘制贝塞尔曲线来模拟水面波纹效果
实现原理讲解:我们将水球图进行简单拆分,可以看成是由外边框+内部水球+中间文字组成,往更细的地方分,内部水球=两个水面波纹+下方颜色填充区域构成。如果需要实现不同形状的水球图,只需要结合css属性clip-path进行裁剪即可。基于以上思路,我们封装一个水球图组件,代码如下
<!-- demo1.vue -->
<template>
<div class="box-wrap" :style="styObj">
<div class="box">
<div class="fill-area">
<svg
xmlns="http://www.w3.org/2000/svg"
version="1.0"
class="waves back-wave"
viewBox="0 0 600 140"
>
<path :d="path" />
</svg>
<svg
xmlns="http://www.w3.org/2000/svg"
version="1.0"
class="waves front-wave"
viewBox="0 0 600 140"
>
<path :d="path" />
</svg>
</div>
<!-- 插槽内容 -->
<div class="slot-content">
<slot></slot>
</div>
</div>
</div>
</template>
<script>
export default {
props: {
rate: {
type: String,
default: 0,
},
config: {
type: Object,
default: () => {
return {};
},
},
},
data() {
return {
defaultConfig: {
frontColor: "#0bc8e8", // 前面波纹颜色
backColor: "#0b6d98", // 后面波纹颜色
outerBorderColor: "#0bc8e8", // 外边框颜色
outerBorderWidth: "4px", // 外变宽宽度
outerPadding: "4px", // 外边框内边距
innerBackground: "transparent", // 水球内部背景颜色
doubleWaves: true, // 是否显示双波浪线
borderRadius: "50%", // 外围边框圆角程度
crests: 40, // 波峰-波谷,值0-70,值越大,水面突出越明显
},
};
},
computed: {
path() {
let obj = Object.assign({}, this.defaultConfig, this.config);
let crests = obj.crests;
if (crests >= 70) {
crests = 70;
} else if (crests <= 0) {
crests = 0;
}
return `M 0 70 Q 75 ${
70 - crests
},150 70 T 300 70 T 450 70 T 600 70 L 600 140 L 0 140 L 0 70Z`;
},
styObj() {
let obj = Object.assign({}, this.defaultConfig, this.config);
let rate = this.rate.replace("%", "");
let waveDisplay = obj.doubleWaves ? "block" : "none";
if (rate <= 0) {
rate = 0;
} else if (rate >= 100) {
rate = 100;
}
return {
"--front-color": obj.frontColor,
"--back-color": obj.backColor,
"--outer-border-color": obj.outerBorderColor,
"--outer-border-width": obj.outerBorderWidth,
"--outer-padding": obj.outerPadding,
"--inner-background": obj.innerBackground,
"--water-height": `${rate}%`,
"--wave-display": waveDisplay,
"--border-radius": obj.borderRadius,
};
},
},
};
</script>
<style scoped>
.box-wrap {
width: 100%;
height: 100%;
border: var(--outer-border-width) solid var(--outer-border-color);
padding: var(--outer-padding);
box-sizing: border-box;
border-radius: var(--border-radius);
}
.box {
position: relative;
width: 100%;
height: 100%;
box-sizing: border-box;
border-radius: var(--border-radius);
/** 解决增加圆角后超出部分不隐藏bug */
z-index: 1;
overflow: hidden;
background-color: var(--inner-background);
}
/* 波纹填充区域 */
.fill-area {
position: absolute;
left: 0;
bottom: -123.33%;
width: 100%;
height: 100%;
transform: translateY(calc(0% - var(--water-height)));
background-color: var(--front-color);
}
.waves {
position: absolute;
left: 0;
bottom: 100%;
width: 200%;
stroke: none;
/* 解决水球图中间有一条线问题 */
box-shadow: 0 10px 4px 4px var(--front-color);
}
.front-wave {
fill: var(--front-color);
transform: translate(-50%, 0);
animation: front-wave-move 3s linear infinite;
}
.back-wave {
display: var(--wave-display);
fill: var(--back-color);
transform: translate(0, 0);
animation: back-wave-move 1.5s linear infinite;
}
@keyframes front-wave-move {
100% {
transform: translate(0, 0);
}
}
@keyframes back-wave-move {
100% {
transform: translate(-50%, 0);
}
}
/* 插槽内容样式 */
.slot-content {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
}
</style>
组件中提供默认色值及属性配置,defaultConfig中的属性均可通过父子组件传值的方式在config属性中进行覆盖、从而实现个性化的配置方案,以下贴一下父组件中的实现方案,以供大家参考
<template>
<div class="page-box">
<div class="main-box">
<!-- 水球图1-正方形带圆角效果 -->
<div class="module">
<water-circle rate="25%" :config="config1">
<span class="slot-font2">25%</span>
</water-circle>
</div>
<!-- 水球图2-模拟echarts -->
<div class="module">
<water-circle rate="75%" :config="config2">
<span class="slot-font1">75%</span>
</water-circle>
</div>
<!-- 水球图3-单波浪纹 -->
<div class="module">
<water-circle rate="75%" :config="{ doubleWaves: false }">
<span class="slot-font1">75%</span>
</water-circle>
</div>
</div>
<hr />
<div class="main-box">
<!-- 水球图4-三角形裁剪 -->
<div class="module">
<water-circle
rate="10%"
:config="config3"
style="clip-path: polygon(50% 0%, 0% 100%, 100% 100%)"
>
<span class="slot-font2">10%</span>
</water-circle>
</div>
<!-- 水球图5-菱形裁剪 -->
<div class="module">
<water-circle
rate="10%"
:config="config3"
style="clip-path: polygon(50% 0%, 100% 50%, 50% 100%, 0% 50%)"
>
<span class="slot-font2">10%</span>
</water-circle>
</div>
<!-- 水球图6-五角星裁剪 -->
<div class="module">
<water-circle
rate="50%"
:config="config3"
style="clip-path: polygon(50% 0%,61% 35%,98% 35%,68% 57%,79% 91%,50% 70%,21% 91%,32% 57%,2% 35%,39% 35%)"
>
<span class="slot-font2">50%</span>
</water-circle>
</div>
</div>
</div>
</template>
<script>
import WaterCircle from "./demo1";
export default {
components: {
WaterCircle,
},
data() {
return {
config1: {
frontColor: "#148cdb",
backColor: "#2e5199",
outerBorderColor: "#294d99",
outerBorderWidth: "6px",
outerPadding: "6px",
innerBackground: "#ddf0f8",
borderRadius: "20%",
crests: 30,
},
config2: {
frontColor: "#148cdb",
backColor: "#2e5199",
outerBorderColor: "#294d99",
outerBorderWidth: "6px",
outerPadding: "6px",
innerBackground: "#ddf0f8",
crests: 50,
},
config3: {
outerBorderWidth: "0px",
outerPadding: "0px",
innerBackground: "#ddf0f8",
borderRadius: 0,
},
};
},
};
</script>
<style scoped>
.page-box {
width: 100%;
overflow: auto;
}
.main-box {
display: flex;
flex-wrap: wrap;
width: 100%;
}
.module {
/* 务必保证容器宽高一致、否则会导致水面高度计算有误 */
width: 200px;
height: 200px;
box-sizing: border-box;
padding: 10px;
}
.slot-font1 {
color: #fff;
font-size: 20px;
font-weight: bold;
}
.slot-font2 {
color: #274380;
font-size: 20px;
font-weight: bold;
}
</style>
为了省事儿,最后三个图的裁剪样式,直接写在组件的style中了。关于裁剪的形状可以根据需要进行设置,目前也有不少的网站提供在线的路径裁剪,非常简单方便。大家有什么疑问,可以在评论区留言,随时回复