CSS实现个性化水球图

在可视化应用中,水球图也是一种常见的数据展示形式,关于使用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中了。关于裁剪的形状可以根据需要进行设置,目前也有不少的网站提供在线的路径裁剪,非常简单方便。大家有什么疑问,可以在评论区留言,随时回复

猜你喜欢

转载自blog.csdn.net/qq_40289557/article/details/127593240