首先,我们看一看要实现的效果:

echarts 柱状图 legend xAxis series 三个不同维度_echarts 堆叠柱状图3d效果

今天这篇文章所实现的动画效果起源于一个小小的想法,这个想法来自于另一个网站的一篇文章,它介绍了如何在网页中使用CSS、图片和JavaScript创建立体的柱状图。在阅读了那篇文章之后,我想挑战一下,尝试使用纯CSS来实现相同的效果。一开始的难点在于创建一个六面半透明的立方体,而后面的难点在于如何创建一个完整的带有动画效果的3D柱状图。

下面,我们就一起来看一下如何解决这些难点。

让我们先列举一些要实现的要求,我们所实现的柱状图应该是:

  • 背景独立(即柱状图与背景互不影响)
  • 自适应的(柱子数量的多少不会影响布局)
  • 可缩放(如矢量图一样)
  • 易于定制(颜色、尺寸和比例)

计划是任何项目中最重要的一个部分。所以我们要先制定一个计划。

在实际编码之前,我通常会列出项目中我会遇到的潜在挑战和解决这些挑战的方案,然后重复这个过程,直到我得到一个看起来可以执行的策略的东西。下面是我为这个项目提出的挑战列表和解决方案:

挑战1 - 带有可伸缩内核的柱子

我们知道:

  • 一个柱状图是由6个面组成的立体图形
  • 这个柱状图的内核是可以垂直伸缩的,并且有一个选项可以隐藏它

所以,我们需要:

  • 一个div,生成柱状图的三个面(背部、底部、左侧)
  • 一个div,生成柱状图的另三个面(正面、顶部、右侧)
  • 一个div,生成内核柱体的三个面,与上面的第二个div类似,但是它的z-index值要小
  • 一个div,作为容器,用于定位以上的三个组件,并且在右下角实现一个实色的背景
  • 一个div,带有overflow: hidden的容器,用于内核柱体的高度为0时,隐藏它。

总共有五个div。

你可能想知道为什么我们需要两个容器?嗯,这是一个不好解释的问题,但我会尝试着说明清楚。

每个柱体我们需要至少一个容器(以保证前三个div的位置),由于我们的柱体内核是可伸缩的,所以我们使用百分比来操纵内核的高度,这就要求容器的高度等于条形图Y轴的高度。

这看起来很好,但是,有另外一个问题,应该有一个选项可以隐藏移动中的内核,这意味着它应该“低于柱体”并且隐藏。你可能会说有一个解决方法 - overflow: hidden,是的,但是它不适用于这里的容器,因为它的高度要比实际的柱体高度短,这就是我们为什么要添加另一个容器的原因。

希望我说清楚了,下面我们继续。

挑战2 - 坐标轴

坐标轴应该:

  • 是一个三维立体坐标轴,它包含3个面(背景面,X轴面,Y轴面)
  • 背景独立
  • 自适应柱体的数量和属性(width,height等)
  • 外侧有X轴与Y轴的文字标签

所以,我们需要:

  • 一个无序列表
  • X轴标签的每个列表项内有一个span元素
  • 每个列表项中有一个柱体
  • Y轴标签内包含一个无序列表

实现

现在我们有了一个总体的计划,让我们把它转换成代码。

请注意,文章中的代码没有写浏览器前缀。在实际的项目中请不要省略。

挑战1 - 带有可伸缩内核的柱子



50







让我们再次回顾每个元素的用途:

  • bar-wrapper – 当.bar-inner滑动到柱体的下方时隐藏它
  • bar-container – 作为.bar-foreground, .bar-inner, .bar-foreground定位的参考元素,并设置底角的背景颜色
  • bar-background – 生成柱状图的三个面(背部、底部、左侧)
  • bar-inner – 最重要的部分 – 柱子内核
  • bar-foreground – 生成柱状图的另三个面(正面、顶部、右侧)


首先,让我们设置容器的样式。

/* Bar wrapper容器 - 当内核低于柱体高度时隐藏内核,必需的 */.bar-wrapper {  overflow: hidden;}/* Bar container容器 - 这家伙是柱形图里真正的家长——子元素都是相对于它定位的。*/.bar-container {  position: relative;  margin-top: 2.5em;  width: 12.5em;}/* 右下角的小块 - 确保内核向下滑动时右下角被“切割” */.bar-container:before {  content: "";  position: absolute;  z-index: 3;  bottom: 0;  right: 0;  width: 0;  height: 0;  border-style: solid;  border-width: 0 0 2.5em 2.5em;  border-color: transparent transparent rgba(183,183,183,1);}
/* Bar wrapper容器 - 当内核低于柱体高度时隐藏内核,必需的 */
.bar-wrapper {
  overflow: hidden;
}
/* Bar container容器 - 这家伙是柱形图里真正的家长——子元素都是相对于它定位的。*/
.bar-container {
  position: relative;
  margin-top: 2.5em;
  width: 12.5em;
}
/* 右下角的小块 - 确保内核向下滑动时右下角被“切割” */
.bar-container:before {
  content: "";
  position: absolute;
  z-index: 3;

  bottom: 0;
  right: 0;

  width: 0;
  height: 0;
  border-style: solid;
  border-width: 0 0 2.5em 2.5em;
  border-color: transparent transparent rgba(183,183,183,1);
}



请注意,我们将.bar-container的宽度设置为12.5em。这个数字是柱体的正面和右侧宽度的总和-在我们的示例中,它是10+2.5=12.5。 我们还使用border属性来创建三角形,并将其放置在 .bar-container的右下角,以确保内核的侧边在垂直移动时能被“切割”。我们使用:before伪类来生成这个元素。 下面我们来设置.bar-background:

/* 背面 */.bar-background {  width: 10em;  height: 100%;  position: absolute;  top: -2.5em;  left: 2.5em;  z-index: 1;}.bar-background:before,.bar-background:after {  content: "";  position: absolute;}/* 底面 */.bar-background:before {  bottom: -2.5em;  right: 1.25em;  width: 10em;  height: 2.5em;  transform: skew(-45deg);}/* 左后面 */.bar-background:after {  top: 1.25em;  right: 10em;  width: 2.5em;  height: 100%;  /* 仅倾斜Y轴 */  transform: skew(0deg, -45deg);}
/* 背面 */
.bar-background {
  width: 10em;
  height: 100%;
  position: absolute;
  top: -2.5em;
  left: 2.5em;
  z-index: 1;
}

.bar-background:before,
.bar-background:after {
  content: "";
  position: absolute;
}

/* 底面 */
.bar-background:before {
  bottom: -2.5em;
  right: 1.25em;
  width: 10em;
  height: 2.5em;
  transform: skew(-45deg);
}

/* 左后面 */
.bar-background:after {
  top: 1.25em;
  right: 10em;
  width: 2.5em;
  height: 100%;

  /* 仅倾斜Y轴 */
  transform: skew(0deg, -45deg);
}



如你所见,我们将.bar-background向上和向右移动2.5em。当然,我们把左后面和底面倾斜45度。请注意,:after伪元素中将第一个倾斜值设置为0deg,第二个设置为-45度,这样只倾斜元素的Y轴。 接着来设置.bar-foreground:

/* 前面 */.bar-foreground {    z-index: 3; /* 在 .bar-background 和.bar-inner 之上 */}.bar-foreground,.bar-inner {  position: absolute;  width: 10em;  height: 100%;  top: 0;  left: 0;}.bar-foreground:before,.bar-foreground:after,.bar-inner:before,.bar-inner:after {  content: "";  position: absolute;}/* 右前面 */.bar-foreground:before,.bar-inner:before {  top: -1.25em;  right: -2.5em;  width: 2.5em;  height: 100%;  background-color: rgba(160, 160, 160, .27);  transform: skew(0deg, -45deg);}/* 前面 */.bar-foreground:after,.bar-inner:after {  top: -2.5em;  right: -1.25em;  width: 100%;  height: 2.5em;  background-color: rgba(160, 160, 160, .2);  transform: skew(-45deg);}
/* 前面 */
.bar-foreground {
    z-index: 3; /* 在 .bar-background 和.bar-inner 之上 */
}
.bar-foreground,
.bar-inner {
  position: absolute;
  width: 10em;
  height: 100%;
  top: 0;
  left: 0;
}

.bar-foreground:before,
.bar-foreground:after,
.bar-inner:before,
.bar-inner:after {
  content: "";
  position: absolute;
}

/* 右前面 */
.bar-foreground:before,
.bar-inner:before {
  top: -1.25em;
  right: -2.5em;
  width: 2.5em;
  height: 100%;
  background-color: rgba(160, 160, 160, .27);

  transform: skew(0deg, -45deg);
}

/* 前面 */
.bar-foreground:after,
.bar-inner:after {
  top: -2.5em;
  right: -1.25em;
  width: 100%;
  height: 2.5em;
  background-color: rgba(160, 160, 160, .2);

  transform: skew(-45deg);
}



这里没什么新鲜的,一切都和.bar-background的样式一样,只是方向不同。 其中,部分的样式同时应用在了.bar-foreground和.bar-inner元素上,因为它们的样子是完全相同的。 好了,下面我们继续设置内核的样式。

.bar-inner {  z-index: 2; /* 在.bar-background的上面 */  top: auto; /* 重置 top属性 */  background-color: rgba(5, 62, 123, .6);  height: 0;  bottom: -2.5em;  color: transparent; /* 隐藏文字 */  transition: height 1s linear, bottom 1s linear;}/* 右面 */.bar-inner:before {  background-color: rgba(5, 62, 123, .6);}/* 上面 */.bar-inner:after {  background-color: rgba(47, 83, 122, .7);}
.bar-inner {
  z-index: 2; /* 在.bar-background的上面 */
  top: auto; /* 重置 top属性 */
  background-color: rgba(5, 62, 123, .6);
  height: 0;
  bottom: -2.5em;
  color: transparent; /* 隐藏文字 */

  transition: height 1s linear, bottom 1s linear;
}

/* 右面 */
.bar-inner:before {
  background-color: rgba(5, 62, 123, .6);
}

/* 上面 */
.bar-inner:after {
  background-color: rgba(47, 83, 122, .7);
}



好了,柱体的样式就设置好了,接下来我们来看坐标轴。 挑战2 - 坐标轴

<ul class="graph-container">  <li>    <span>2011span>    <-- 此处显示柱状图图的HTML标记 -->  li>  <li>    <span>2012span>    <-- 此处显示柱状图图的HTML标记 -->  li>  <li>    <ul class="graph-marker-container">      <li><span>25%span>li>      <li><span>50%span>li>      <li><span>75%span>li>      <li><span>100%span>li>    ul>  li>ul>
<ul class="graph-container">
  <li>
    <span>2011span>
    <-- 此处显示柱状图图的HTML标记 -->
  li>
  <li>
    <span>2012span>
    <-- 此处显示柱状图图的HTML标记 -->
  li>
  <li>
    <ul class="graph-marker-container">
      <li><span>25%span>li>
      <li><span>50%span>li>
      <li><span>75%span>li>
      <li><span>100%span>li>
    ul>
  li>
ul>



如您所见,我们在项目中使用无序列表和span元素来定位X轴和Y轴标签。

/** 坐标轴容器 **/.graph-container {  position: relative;   display: inline-block;   padding: 0;   list-style: none; /* 去除列表元素自带的小黑点 */  /* 背景 */  background-image: linear-gradient(left , rgba(255, 255, 255, .3) 100%, transparent 100%);  background-repeat: no-repeat;  background-position: 0 -2.5em;}
/** 坐标轴容器 **/
.graph-container {
  position: relative; 
  display: inline-block; 
  padding: 0; 
  list-style: none; /* 去除列表元素自带的小黑点 */

  /* 背景 */
  background-image: linear-gradient(left , rgba(255, 255, 255, .3) 100%, transparent 100%);
  background-repeat: no-repeat;
  background-position: 0 -2.5em;
}



这里有一个小点,我们使用线性渐变填充容器背景并将其提升2.5em,为什么?因为坐标轴的底端(我们将在下一个样式中设置)高度是2.5em。而且坐标轴倾斜了45度,所以右下角有一个空白区域。 坐标轴的X轴样式:

/* X轴 */.graph-container:before {  position: absolute;  content: "";  bottom: 0;  left: -1.25em; /* 倾斜会将它向左推,所以我们将它向相反的方向移动一点。*/  width: 100%; /* 确保它和整个组件一样宽 */  height: 2.5em;  background-color: rgba(183, 183, 183, 1);  transform: skew(-45deg);}
/* X轴 */
.graph-container:before {
  position: absolute;
  content: "";

  bottom: 0;
  left: -1.25em; /* 倾斜会将它向左推,所以我们将它向相反的方向移动一点。*/

  width: 100%; /* 确保它和整个组件一样宽 */

  height: 2.5em;
  background-color: rgba(183, 183, 183, 1);

  transform: skew(-45deg);
}



我们把它倾斜45度,然后向左移动一点,以确保它的位置正确。 坐标轴的Y轴样式:

/* Y轴 */.graph-container:after {  position: absolute;  content: "";  top: 1.25em; /* 倾斜会将其向上推,因此我们将其向下移动一点。*/  left: 0em;  width: 2.5em;  background-color: rgba(28, 29, 30, .4);  transform: skew(0deg, -45deg);}
/* Y轴 */
.graph-container:after {
  position: absolute;
  content: "";

  top: 1.25em; /* 倾斜会将其向上推,因此我们将其向下移动一点。*/
  left: 0em;

  width: 2.5em;
  background-color: rgba(28, 29, 30, .4);

  transform: skew(0deg, -45deg);
}



这里没什么特别的。一样将元素倾斜45度,然后向下推一点,以便正确定位。 坐标轴的基本设置就是这些,接下来我们继续设置列表项里面的样式:

.graph-container > li {  float: left; /* 水平排列 */  position: relative; }.graph-container > li:nth-last-child(2) {  margin-right: 2.5em;}/* X轴的文字标签 */.graph-container > li > span {  position: absolute;  left: 0;  bottom: -2em;  width: 80%;   text-align: center;  font-size: 1.5em;  color: rgba(200, 200, 200, .4);}
.graph-container > li {
  float: left; /* 水平排列 */
  position: relative; 
}
.graph-container > li:nth-last-child(2) {
  margin-right: 2.5em;
}
/* X轴的文字标签 */
.graph-container > li > span {
  position: absolute;
  left: 0;
  bottom: -2em;
  width: 80%; 
  text-align: center;

  font-size: 1.5em;
  color: rgba(200, 200, 200, .4);
}



这里有两个要注意的点。首先,我们使用浮动将柱体水平排列。通常情况下都应该非常小心地使用浮动,它会带来高度塌陷等布局问题。所以,在这里你可以尝试变为设置display:inline-block来实现。 第二,我们在最后一个柱体上添加了一些右边距。这样我们就可以确保给坐标轴底部留出足够的空间,试着去掉它,你就会明白我的意思。 OK,我们就快完成了。最后要做的是添加Y轴的文字标记。

/* 文字标记的容器 */.graph-container > li:last-child {  width: 100%;  position: absolute;  left: 0;  bottom: 0;}/* Y轴文字标记列表 */.graph-marker-container > li {  position: absolute;  left: -2.5em;  bottom: 0;  width: 100%;  margin-bottom: 2.5em;  list-style: none;}/* Y轴线条常规样式 */.graph-marker-container > li:before,.graph-marker-container > li:after {  content: "";  position: absolute;  border-style: none none dotted;  border-color: rgba(100, 100, 100, .15);  border-width: 0 0 .15em;  background: rgba(133, 133, 133, .15);}/* Y轴侧线 */.graph-marker-container > li:before {  width: 3.55em;  height: 0;  bottom: -1.22em;  left: -.55em;  z-index: 2;   transform: rotate(-45deg);}/* Y轴背景线 */.graph-marker-container li:after {  width: 100%;  bottom: 0;  left: 2.5em;}/* Y轴文本标签 */.graph-marker-container span {  color: rgba(200, 200, 200, .4);  position: absolute;  top: 1em;  left: -3.5em;   width: 3.5em;   font-size: 1.5em;}
/* 文字标记的容器 */
.graph-container > li:last-child {
  width: 100%;
  position: absolute;
  left: 0;
  bottom: 0;
}

/* Y轴文字标记列表 */
.graph-marker-container > li {
  position: absolute;
  left: -2.5em;
  bottom: 0;
  width: 100%;
  margin-bottom: 2.5em;
  list-style: none;
}

/* Y轴线条常规样式 */
.graph-marker-container > li:before,
.graph-marker-container > li:after {
  content: "";
  position: absolute;
  border-style: none none dotted;
  border-color: rgba(100, 100, 100, .15);
  border-width: 0 0 .15em;
  background: rgba(133, 133, 133, .15);
}

/* Y轴侧线 */
.graph-marker-container > li:before {
  width: 3.55em;
  height: 0;
  bottom: -1.22em;
  left: -.55em;
  z-index: 2; 
  transform: rotate(-45deg);
}

/* Y轴背景线 */
.graph-marker-container li:after {
  width: 100%;
  bottom: 0;
  left: 2.5em;
}

/* Y轴文本标签 */
.graph-marker-container span {
  color: rgba(200, 200, 200, .4);
  position: absolute;

  top: 1em;
  left: -3.5em; 
  width: 3.5em; 

  font-size: 1.5em;
}



如你所见,我们将文字标记容器的宽度设置为100%,使得背景线能够覆盖整个坐标轴,使用虚线边框设置Y轴线条的样式并定位span元素,使文字标签位于坐标轴的外侧。使用:before和:after伪元素,可以减少HTML的代码量,让页面保持干净。 到这里,我们已经完成了柱状图的所有样式设置,但是我们缺少一些重要的变量——大小、颜色和条形填充值!上面说过我们的图表是可定制的,所以,我决定不把变量和其他代码混合在一起,这样你就可以更方便的自定义它们了。

/**************** * 尺寸        * ****************/ /* 图表的整体大小 */.graph-container,.bar-container {  font-size: 8px;}/* 柱体的高度 */.bar-container,.graph-container:after,.graph-container > li:last-child {  height: 40em;}/**************** * 间距      * ****************//* 柱体的间距 */.graph-container > li .bar-container {  margin-right: 1.5em;}/* 第一个柱体的左边距 */.graph-container > li:first-child {  margin-left: 1.5em;}/* 最后一个柱体的右边距 */.graph-container > li:nth-last-child(2) .bar-container {  margin-right: 1.5em;}/**************** *    颜色    * ****************//* 柱体的背面颜色 */.bar-background {  background-color: rgba(160, 160, 160, .1);}/* 柱体的底面颜色 */.bar-background:before {  background-color: rgba(160, 160, 160, .2);}/* 柱体的左后面颜色 */.bar-background:after {  background-color: rgba(160, 160, 160, .05);}/* 柱体的正面颜色 */.bar-foreground {  background-color: rgba(160, 160, 160, .1);}/* 内核的颜色 */.bar-inner,.bar-inner:before { background-color: rgba(5, 62, 123, .6); }.bar-inner:after { background-color: rgba(47, 83, 122, .7); }/************************************* *   内核的高度                      * *************************************/.graph-container > li:nth-child(1) .bar-inner { height: 25%; bottom: 0; }.graph-container > li:nth-child(2) .bar-inner { height: 50%; bottom: 0; }.graph-container > li:nth-child(3) .bar-inner { height: 75%; bottom: 0; }
/****************
 * 尺寸        *
 ****************/
 /* 图表的整体大小 */
.graph-container,
.bar-container {
  font-size: 8px;
}
/* 柱体的高度 */
.bar-container,
.graph-container:after,
.graph-container > li:last-child {
  height: 40em;
}

/****************
 * 间距      *
 ****************/
/* 柱体的间距 */
.graph-container > li .bar-container {
  margin-right: 1.5em;
}
/* 第一个柱体的左边距 */
.graph-container > li:first-child {
  margin-left: 1.5em;
}
/* 最后一个柱体的右边距 */
.graph-container > li:nth-last-child(2) .bar-container {
  margin-right: 1.5em;
}

/****************
 *    颜色    *
 ****************/
/* 柱体的背面颜色 */
.bar-background {
  background-color: rgba(160, 160, 160, .1);
}
/* 柱体的底面颜色 */
.bar-background:before {
  background-color: rgba(160, 160, 160, .2);
}
/* 柱体的左后面颜色 */
.bar-background:after {
  background-color: rgba(160, 160, 160, .05);
}
/* 柱体的正面颜色 */
.bar-foreground {
  background-color: rgba(160, 160, 160, .1);
}
/* 内核的颜色 */
.bar-inner,
.bar-inner:before { background-color: rgba(5, 62, 123, .6); }
.bar-inner:after { background-color: rgba(47, 83, 122, .7); }

/*************************************
 *   内核的高度                      *
 *************************************/
.graph-container > li:nth-child(1) .bar-inner { height: 25%; bottom: 0; }
.graph-container > li:nth-child(2) .bar-inner { height: 50%; bottom: 0; }
.graph-container > li:nth-child(3) .bar-inner { height: 75%; bottom: 0; }



在下载的源码中,您将无法找到这一部分代码,因为我在那里做了一些更有趣的事情——我使用了单选按钮让您在不修改代码的情况下使用变量。但是,如果您只需要定制一个静态图形,那么就从上面获取代码片段,并根据您的喜好对其进行定制。 总结


让我们回顾一下文章中介绍的一些CSS规范/技术。


  • transform:skew()和transform:rotate()用于倾斜和旋转元素,它们组合起来使元素模拟产生三维立体的效果。
  • :before和:after伪元素可以保持HTML标记相对干净
  • :nth-last-child()和:not是针对特定列表项的伪类,可以避免向HTML中添加额外的类/id。
  • linear-gradient和background-position一起使用可以实现背景的部分填充
  • rgba()可以实现具有透明度的颜色
  • border属性可以创建三角形形状



END