css小技巧:面积图效益

984 阅读9分钟

前言

作为一个前端工程师需要对css的各种属性有充分的了解,今天先用纯css带大家实现面积图再扩展知识,首先看下单层效果👇

HTML 的部分

为简化起见,我们将使用<ul><li>标签:

<ul class="area-chart">
  <li> 40% </li>
  <li> 80% </li>
  <li> 60% </li>
  <li> 100% </li>
  <li> 30% </li>
</ul>

css无法获取到html文本内容,这里我们将自定义CSS属性将数据传递到CSS。这需要给每个<li>添加--start--end属性。

<ul class="area-chart">
  <li style="--start: 0.1; --end: 0.4;"> 40% </li>
  <li style="--start: 0.4; --end: 0.8;"> 80% </li>
  <li style="--start: 0.8; --end: 0.6;"> 60% </li>
  <li style="--start: 0.6; --end: 1.0;"> 100% </li>
  <li style="--start: 1.0; --end: 0.3;"> 30% </li>
</ul>

在设计样式之前,我们应考虑以下设计原则👇:

  • 数据单位:我们将在我们的HTML中使用无单位的数据(即无pxemrem%或任何其他单位)。--start--end自定义属性将是0和1之间的数字。
  • 列宽:我们不会为每个<li>元素设置固定值的width或者%,因为我们不知道有多少列。每一列的宽度将基于父容器的宽度除以总列数。在我们的例子中,这就是<ul>元素的宽度除以<li>元素的数量。

CSS 的部分

让我们先从一般布局样式开始。面积图父元素是一个flex容器,可连续显示并拉伸子元素,以便填充整个区域。

.area-chart {
  /* 重置样式 */
  margin: 0;
  padding: 0;
  border: 0;
  /* 尺寸 */
  width: 100%;
  max-width: var(--chart-width, 600px);
  height: var(--chart-height, 300px);
  /* 布局 */
  display: flex;
  justify-content: stretch;
  align-items: stretch;
  flex-direction: row;
}

如果面积图容器是<ul>列表,则应删除列表样式,以使样式更具灵活性。

ul.area-chart {
  list-style: none;
}

下面代码为整个图表中的所有列设置样式。首先我们为每一列设置background-colorheight来实现一个简单的条形图。然后我们将使用clip-path属性设置面积图应显示的区域。

首先,我们设置每一列的基本条形样式:

.area-chart > * {
  color: transparent;
 /* 均分 */
  flex-grow: 1;
  flex-shrink: 1;
  flex-basis: 0;

  /* 颜色 */
  background: var(--color, rgb(55, 83, 125,.75));
}

到这一步我们创建一个覆盖整个区域的矩形

现在是最重要的步骤:

为了仅显示列的一部分,我们将其裁剪只显示我们想要的区域以达到面积图的效果。这里用到clip-path属性,并使用polygon()函数设置所有区域坐标(分别是四边形的左上角右上角右下角左下角

clip-path: polygon(
  0% calc(100% * (1 - var(--start))), /* top left */
  100% calc(100% * (1 - var(--end))), /* top right  */
  100% 100%, /* bottom right */ 
  0% 100% /* bottom left */
);

到这里靠CSS基本完成所有工作。这是完整代码👇:

<ul class="area-chart">
  <li style="--start: 0.1; --end: 0.4">40%</li>
  <li style="--start: 0.4; --end: 0.8">80%</li>
  <li style="--start: 0.8; --end: 0.6">60%</li>
  <li style="--start: 0.6; --end: 1">100%</li>
  <li style="--start: 1; --end: 0.3">30%</li>
</ul>
<style>
  :root {
    --chart-width: 600px;
    --chart-height: 300px;
  }
  body {
    display: grid;
    place-items: center;
    height: 100vh;
    margin: 0;
    background-color: rgb(132, 176, 224);
  }
  .area-chart {
    /* 重置样式 */
    margin: 0;
    padding: 0;
    border: 0;
    /* 尺寸 */
    width: 100%;
    max-width: var(--chart-width, 600px);
    height: var(--chart-height, 300px);
    /* 布局 */
    display: flex;
    justify-content: stretch;
    align-items: stretch;
    flex-direction: row;
  }
  ul.area-chart {
    list-style: none;
  }
  .area-chart > * {
    color: transparent;
    /* 均分 */
    flex-grow: 1;
    flex-shrink: 1;
    flex-basis: 0;
    /* 布局 */
    background: var(--color, rgb(55, 83, 125, 0.75));
    clip-path: polygon(
      0% calc(100% * (1 - var(--start))), /* top left */
      100% calc(100% * (1 - var(--end))), /* top right  */
      100% 100%, /* bottom right */ 
      0% 100% /* bottom left */
    );
  }
</style>

处理多个数据集

现在我们已经了解了基础知识,让我们创建一个包含多个数据集的面积图。面积图通常可以测量一组以上的数据,其效果是对数据进行分层比较。

这种图表需要几个子元素,因此我们将用<table>代替<ul>

<table class="area-chart">
  <tbody>
    <tr>
      <td> 40% </td>
      <td> 80% </td>
    </tr>
    <tr>
      <td> 60% </td>
      <td> 100% </td>
    </tr>
  </tbody>
</table>

同样,我们将使用--start--end自定义属性,其数字介于0和1之间。

<table class="area-chart">
  <tbody>
    <tr>
      <td style="--start: 0.1; --end: 0.5">50</td>
      <td style="--start: 0; --end: 0.2">20</td>
      <td style="--start: 0.2; --end: 0.4">40</td>
    </tr>
    <tr>
      <td style="--start: 0.5; --end: 0.8">80</td>
      <td style="--start: 0.2; --end: 0.5">50</td>
      <td style="--start: 0.4; --end: 0.1">10</td>
    </tr>
    <tr>
      <td style="--start: 0.8; --end: 0.4">40</td>
      <td style="--start: 0.5; --end: 0.3">30</td>
      <td style="--start: 0.1; --end: 0.2">20</td>
    </tr>
  </tbody>
</table>

首先为我们的总体布局(table)设置样式,并为其提供了一个.area-chart类:

.area-chart {
  /* 重置样式 */
  margin: 0;
  padding: 0;
  border: 0;

  /* 尺寸 */
  width: 100%;
  max-width: var(--chart-width, 600px);
  height: var(--chart-height, 300px);
}

接下来,我们将使用<tbody>元素成为flex容器,将<tr>显示为大小均匀的列:

.area-chart tbody {
  width: 100%;
  height: var(--chart-height, 300px);

  /* 布局 */
  display: flex;
  justify-content: stretch;
  align-items: stretch;
  flex-direction: row;
}
.area-chart tr {
  /* 均分 */
  flex-grow: 1;
  flex-shrink: 1;
  flex-basis: 0;
}

现在我们需要使<td>元素相互重叠,以便获得分层效果。每个<td>都覆盖包含它的<tr>元素的整个区域。

.area-chart tr {
  position: relative;
}
.area-chart td {
  position: absolute;
  top: 0;
  right: 0;
  bottom: 0;
  left: 0;
}

让我们使用clip-path: polygon()的神奇力量!我们仅显示介于--start--end自定义属性之间的区域,该区域也是介于0和1之间的值:

.area-chart td {
  clip-path: polygon(
    0% calc(100% * (1 - var(--start))),
    100% calc(100% * (1 - var(--end))),
    100% 100%,
    0% 100%
  );
}

现在让我们为每个<td>添加颜色:

.area-chart td {
  background: var(--color);
}
.area-chart td:nth-of-type(1) {
  --color: rgba(240, 50, 50, 0.75);
}
.area-chart td:nth-of-type(2) {
  --color: rgba(255, 180, 50, 0.75);
}
.area-chart td:nth-of-type(3) {
  --color: rgba(255, 220, 90, 0.75);
}

这里使用有透明度的rgba()以获得更好的效果

完整代码👇:

<table class="area-chart">
  <tbody>
    <tr>
      <td style="--start: 0.1; --end: 0.5">50</td>
      <td style="--start: 0; --end: 0.2">20</td>
      <td style="--start: 0.2; --end: 0.4">40</td>
    </tr>
    <tr>
      <td style="--start: 0.5; --end: 0.8">80</td>
      <td style="--start: 0.2; --end: 0.5">50</td>
      <td style="--start: 0.4; --end: 0.1">10</td>
    </tr>
    <tr>
      <td style="--start: 0.8; --end: 0.4">40</td>
      <td style="--start: 0.5; --end: 0.3">30</td>
      <td style="--start: 0.1; --end: 0.2">20</td>
    </tr>
  </tbody>
</table>
<style>
  body {
    display: grid;
    place-items: center;
    height: 100vh;
    margin: 0;
    background-color: rgb(132, 176, 224);
  }
  .area-chart {
    /* 重置样式 */
    margin: 0;
    padding: 0;
    border: 0;
    /* 尺寸 */
    width: 100%;
    max-width: var(--chart-width, 600px);
    height: var(--chart-height, 300px);
  }
  .area-chart tbody {
    width: 100%;
    height: var(--chart-height, 300px);
    /* 布局 */
    display: flex;
    justify-content: stretch;
    align-items: stretch;
    flex-direction: row;
  }
  .area-chart tr {
    position: relative;
    /* 均分 */
    flex-grow: 1;
    flex-shrink: 1;
    flex-basis: 0;
  }
  .area-chart td {
    position: absolute;
    top: 0;
    right: 0;
    bottom: 0;
    left: 0;
    /* 颜色 */
    background: var(--color, rgba(55, 83, 125, 0.75));
    clip-path: polygon(0% calc(100% * (1 - var(--start))), 100% calc(100% * (1 - var(--end))), 100% 100%, 0% 100%);
  }
  .area-chart td:nth-of-type(1) {
    --color: rgb(55, 83, 125, 0.75);
  }
  .area-chart td:nth-of-type(2) {
    --color: rgba(255, 180, 50, 0.75);
  }
  .area-chart td:nth-of-type(3) {
    --color: rgba(255, 220, 90, 0.75);
  }
</style>

现在无论我们向图表中添加多少HTML元素都没有关系,基于flex的布局可确保所有项目的大小均相等。这样,我们只需要设置容器的宽度,项目就会根据响应布局进行相应调整。

关于CSS属性clip-path

clip-path CSS 属性使用裁剪方式创建元素的可显示区域。区域内的部分显示,区域外的隐藏。

使用如下👇:

clip-path: <clip-source> | [ <basic-shape> || <geometry-box> ] | none /* 默认值为none*/
  • <clip-source>:用表示剪切元素的svg路径,语法clip-path: url(resources.svg#c1);
  • <basic-shape>:使用一些基本的形状函数创建的一个形状
  • <geometry-box>:如果同<basic-shape>一起声明,它将为基本形状提供相应的参考盒子。通过自定义,它将利用确定的盒子边缘包括任何形状边角(比如说,被border-radius定义的剪切路径)。其中几何体盒子有fill-boxstroke-boxview-boxmargin-boxborder-boxpadding-boxcontent-box作为引用盒子,语法clip-path: margin-box;
  • none:这里没有创建的剪切路径。

以下为当前支持的图形。所有<basic-shape>值都由函数表达式定义,并且遵循属性值定义语法👇

  • Circle:circle(radius at x-axis y-axis)
    • radius: 半径
    • x-axis: 圆心的x坐标
    • y-axis: 圆心的y坐标
  • Ellipse:ellipse(x-rad y-rad at x-axis y-axis)
    • x-rad: X轴半径
    • y-rad: Y轴半径
    • x-axis: 圆心的x坐标
    • y-axis: 圆心的y坐标
  • Polygon:polygon(x-axis y-axis, x-axis y-axis, … )
    • x-axis: 各个顶点的x坐标
    • y-axis: 各个顶点的y坐标
  • Inset:inset(top right bottom left round top-radius right-radius bottom-radius left-radius)
    • top right bottom left: 分别代表了插进的长方形与相关盒模型的上,右,下与左边界和顶点的偏移量
    • round 固定写死
    • top-radius right-radius bottom-radius left-radius: 分别定义上右下左插进长方形顶点的圆弧角度

一个简单的三角形剪裁

<body>
  <div class="box clipPolygon"></div>
  <style>
    .box {
      width: 200px;
      height: 200px;
      background: rgb(243, 155, 195);
      background-size: cover;
      margin: 28px auto;
    }
    .clipPolygon {
      clip-path: polygon(0 100%, 50% 0, 100% 100%);
    }
  </style>
</body>

polygon中的每一项分别是当前顶点的x,y坐标:

X:0Y:0表示从元素的左上角开始,并从那里移动。X:100%表示元素右边,Y:100%表示元素的底部。因此按照我们的坐标点可以得出剪切三角形时如下剪切坐标顺序分别是👇

x: 0, y:100% // 左下
x: 50%, y: 0  // 中上
x: 100%, y: 100% // 右下

创建梯形

只需把上面👆的polygon类样式修改下即可:

.clipPolygon {
  clip-path: polygon(100% 0, 75% 100%, 25% 100%, 0 0);
}

剪切梯形:坐标顺序为,右上、右下,左下,左上

创建五角星

.clipPolygon {
  clip-path: polygon(50% 0%, 63% 38%, 100% 38%, 69% 59%, 82% 100%, 50% 75%, 18% 100%, 31% 59%, 0 38%, 37% 38%);
}

漫画文本框

.clipPolygon {
  clip-path: polygon(0% 0%, 100% 0%, 100% 75%, 75% 75%, 75% 100%, 50% 75%, 0% 75%);
}

圆形

只需把上面👆的clipPolygon类样式修改下即可:

.clipPolygon {
  clip-path: circle(50% at 50% 50%);
}

要创建圆形,我们需要在圆形中输入三个值。圆心的x轴和y轴坐标,以及圆的半径。先定义圆的半径,再使用关键词at来定义圆心的x和y坐标。

椭圆

只需使用ellipse修改即可:

.clipPolygon {
  clip-path: ellipse(30% 20% at 50% 50%);
}

使用ellipse,只需要提供四个值。椭圆的x轴半径和y轴半径(或形状),后面跟一个at关键字,用于分开另外一组用于定义椭圆位置的x和y坐标。

Inset

.clipPolygon {
  clip-path: inset(25% 0 25% 0 round 0 25% 0 25%);
}

创建圆角矩形可以使用Inset的值。Inset使用四个值(对应“上 右 下 左”的顺序)来设置圆角半径。

动画

使用clip-path可以实现很多动画效果,这里展示一个简单的动画完成正方形和圆形的过渡

<style>
  body {
    align-items: center;
    display: flex;
    height: 100vh;
    justify-content: center;
  }
  .box {
    background: url('./timg.jpg');
    clip-path: circle(75%);
    height: 200px;
    transition: clip-path 1s;
    width: 200px;
  }
  .box:hover {
    clip-path: circle(25%);
  }
</style>
<div class="box"></div>

效果如下👇:

var变量

:root

:root是一个伪类,表示文档根元素,非IE及ie8及以上浏览器都支持,在:root中声明相当于全局属性,只要当前页面引用了:root segment所在文件,都可以使用var()来引用

var()

var()函数用于插入自定义的属性值,如果一个属性值在多处被使用,该方法就很有用。var()函数可以代替元素中任何属性中的值的任何部分。var()函数不能作为属性名、选择器或者其他除了属性值之外的值。(这样做通常会产生无效的语法或者一个没有关联到变量的值。)

语法

var(custom-property-name, value)

属性

  • custom-property-name:必需。自定义属性的名称,必需以 -- 开头。
  • value:可选。备用值,在属性不存在的时候使用。 使用 var() 函数调用多个自定义的值:
<head>
<style>
:root {
  --main-bg-color: coral;
  --main-padding: 15px;  
}

#div1 {
  background-color: var(--main-bg-color); 
  padding: var(--main-padding);
}

#div2 {
  background-color: var(--main-bg-color);
  padding: var(--main-padding);
}

</style>
</head>
<body>
<div id="div1"></div>
<br>
<div id="div2"></div>
</body>

在项目中自定义变量也可以放在根元素body或者任何要修改的元素的父元素底下声明,

body {
  --main-bg-color: coral;
  --main-padding: 15px; 
}

Vue2中使用:

<template>
  <!-- 需要显式声明styleVar,这样设置根元素嵌套范围内都可以使用 -->
  <div :style="styleVar">
    <div class="text">johnYu</div>
  </div>
</template>

<script>
export default {
  data() {
    return {
      styleVar: {
        '--color': 'rgb(243, 155, 195)',
        '--fontSize': '2rem',
      },
    };
  },
};
</script>

<style>
.text {
  font-size: var(--fontSize);
  color: var(--color);
}
</style>
实测在pc端微信浏览器修改背景时图片无效(webkit内核过低无法兼容),字体和颜色仍可使用

在vue3中的使用:

在vue3中采用了v-bind来进行css变量注入,这样就无需显式声明将哪些属性作为CSS变量注入,而且不会使样式泄漏到子组件中,有效解耦

<template>
  <div class="text">johnYu</div>
</template>

<script>
  export default {
    data() {
      return {
        color: 'rgb(243, 155, 195)',
        font: {
          size: '2em',
        },
      };
    },
  };
</script>

<style>
  .text {
    font-size: v-bind('font.size');
    color: v-bind(color);
  }
</style>

显示结果:

参考文章 📜

❤️ Making Sense of Clip Path

❤️ 为什么 Vue3 选择了 CSS 变量

扩展 🏆

如果你觉得本文对你有帮助,可以查看我的其他文章❤️:

👍 「多图预警」那些年,被blob虐过的程序猿觉醒了!

👍 vue3实战笔记 | 快速入门🚀

👍 10个简单的技巧让你的 vue.js 代码更优雅🍊

👍 零距离接触websocket🚀

👍 Web开发应了解的5种设计模式