你的位置:首页 > 网页设计

[网页设计]用CSS和SVG制作饼图

原文出处: lulux的博客   欢迎分享原创到伯乐头条

在涉及到CSS技术时,没有人会比Lea Verou更执着、但是又足够聪明,努力去找寻问题的各种解决方案。最近,Lea自己撰写、设计和出版了一本书——CSS Secrets,这本书非常有趣,包括一些CSS小技巧以及解决常见问题的技术。如果你觉得自己的CSS技术还不错,看看这本书,你会吃惊的。在这篇文章中,我们发布了书里的一些片段,这也被发表在Lea最近在SmashingConf New York的演讲内容中——用CSS设计简单的饼图。注意,因为浏览器的支持有限,有些demo可能不能正常运行。——编辑

饼图,即使是最简单的只有两种颜色的形式,用Web技术创建也并不简单,尽管都是一些常见的信息内容,从简单的统计到进度条指标还有计时器。通常是使用外部图像编辑器来分别为多个值创建多个图像来实现,或是使用大型的JavaScript框架来设计更复杂的图表。

尽管这个东西并不像它曾经看起来那么难以实现,但是也没有什么直接并且简单的方法。但是,现在已经有很多更好、更易于维护的方式来实现它。

基于变换的解决方案


这个方案从HTML的角度来说是最好的:它只需要一个元素,其它的都可以用伪元素、变换和CSS渐变完成。我们从下面这个简单的元素开始:

 
 
1<div class="pie"></div>

现在,假设我们希望显示一个 20% 比例的饼图。灵活性的问题我们后面再解决。我们先给元素添加样式,让它变成一个圆,也就是我们的背景:

图1:第一步是先画一个圆(或者可以说是显示0%比例的饼图)

 
 
 
 
 
CSS 
1
2
3
4
5
.pie {
  width: 100px; height: 100px;
  border-radius: 50%;
  background: yellowgreen;
}

 

我们的饼图是绿色(特指 yellowgreen )和棕色( #655 )显示的百分比。可能会在比例部分尝试使用 transform 中的 skew ,但是经过几次试验之后表明,这是一个非常混乱的方案。因此,我们用这两种颜色为这个饼图的左右部分分别着色,然后对于我们想要的百分比,使用旋转的伪元素来实现。

我们使用一个简单的线性渐变,给右半部分着棕色:

 
 
 
 
 
CSS 
1background-image: linear-gradient(to right, transparent 50%, #655 0);

图2:用一个简单的线性渐变给右半圆着棕色

如图2所示,这样就完成了。现在,我们可以继续为伪元素添加样式,让它成为一个蒙版:

 
 
 
 
 
CSS 
1
2
3
4
5
6
.pie::before {
  content: '';
  display: block;
  margin-left: 50%;
  height: 100%;
}

图3:虚线内的内容表示伪元素将作为蒙版的区域

你可以在图3中看到我们的伪元素当前定位相对于我们的pie元素。目前,它还没有添加样式,也没有覆盖任何东西,只是一个透明的矩形。在开始添加样式之前,我们先来分析一下:

  • 因为我们希望它覆盖圆的棕色部分,我们需要给它应用一个绿色的背景,使用 background-color: inherit 来避免重复定义,因为我们本来就希望它和父元素的背景颜色保持一致。
  • 我们希望它绕着圆的中心点旋转,中心点在伪元素的左边,所以我们需要给它的 transform-origin ,应用一个 0 50% ,或者是直接一个 left 。
  • 我们不想要它是一个矩形,因为它会超过饼图的边缘,所以我们需要给 .pie 应用 overflow: hidden ,或者是一个恰当的 border-radius 让它成为一个半圆。

综上所述,伪元素的CSS样式如下:

 
 
 
 
 
CSS 
1
2
3
4
5
6
7
8
9
.pie::before {
  content: '';
  display: block;
  margin-left: 50%;
  height: 100%;
  border-radius: 0 100% 100% 0 / 50%;
  background-color: inherit;
  transform-origin: left;
}

图4:添加样式之后的伪元素(这里用虚线表示)

注意:不要使用 background: inherit; ,要用 background-color: inherit ;,否则父元素背景图像上的渐变也会被继承

我们的饼图目前如图4所示。现在开始有趣起来了!我们可以开始旋转伪元素,给它应用一个rotate() 变换。要显示 20% 的比例,我们可以给它一个 72deg ( 0.2 x 360 = 72 ),或 .2turn ,这个可读性更好。你可以在图5中看到不同旋转角度值的结果。

图5:分别展示不同百分比的饼图,从左到右: 10%  ( 36deg 或 .1turn ), 20%  ( 72deg 或  .2turn ), 40%  ( 144deg  或 .4turn )

你可能会想我们已经完成了,但是它可没这么简单。我们的饼图在展示050%的大小的内容时是没有任何问题的,但是如果我们要描绘一个 60% 的旋转(通过应用 .6turn ),就会发生如图6的情况。但是,别担心,我们可以解决这个事情!

图6:对于超过50%的比例,我们的饼图就跪了orz(这里的是60%)

如果我们把 50%-100% 比例的情况作为单独的一个问题,可能会注意到可以使用之前的解决方案的反相版本:从0.5turn旋转的棕色伪元素。所以,对于一个60%的饼图,伪元素的CSS代码如下:

 
 
 
 
 
CSS 
1
2
3
4
5
6
7
8
9
10
.pie::before {
  content: '';
  display: block;
  margin-left: 50%;
  height: 100%;
  border-radius: 0 100% 100% 0 / 50%;
  background: #655;
  transform-origin: left;
  transform: rotate(.1turn);
}

图7: 60% 饼图的正确打开方式~

你可以在图7中看到结果。因为我们已经制定了一个可以描绘出任何百分比的方法,我们甚至可以为饼图从0%100%添加动画效果,创建出一个有趣的进度条:

 
 
 
 
 
CSS 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@keyframes spin {
  to { transform: rotate(.5turn); }
}
 
@keyframes bg {
  50% { background: #655; }
}
 
.pie::before {
  content: '';
  display: block;
  margin-left: 50%;
  height: 100%;
  border-radius: 0 100% 100% 0 / 50%;
  background-color: inherit;
  transform-origin: left;
  animation: spin 3s linear infinite,
             bg 6s step-end infinite;
}

 

See the Pen zGbNLJ by Airen (@airen) on CodePen.

显示没有问题,但是我们如果给多个不同百分比的静态饼图添加样式呢,最常见的用例是?在理想情况下,我们希望可以简单地输入如下的内容:

 
 
1
2
<div class="pie">20%</div>
<div class="pie">60%</div>

然后就可以得到两个饼图,一个表示20%,一个表示60%。首先,我们先研究一下如何使用内联样式来完成,然后我们可以写一个简短的脚本来解析文本内容,对应地添加内联样式,而且要代码优雅、封装、可维护性,还有最重要的一点,可访问性。

使用内联样式控制饼图百分比的一个困难是:用于设置百分比CSS代码是用伪元素完成的。而且你也知道,我们不能给伪元素设置内联样式,所以我们需要创新。

注意:如果你想要使用的值是在某个不需要经过重复的复杂的计算的范围内的情况,你可以使用相同的技术,包括通过它们一步一步调试动画的情况。看该技术的一个简单的示例。

 

See the Pen YXgNOK by Airen (@airen) on CodePen.

解决方案来自最不可能的地方之一。我们将要使用我们已经介绍过的动画,但是它是暂停状态的。我们不会让它像一个正常的动画那样运行,我们将使用负延迟来让它可以静态地暂停在某个点。很奇怪?一个负的animation-delay的值不仅在规范中是允许的,在类似这样的案例中也是非常好用:

负延迟是有效的。和0s的延迟类似,它表示动画将立即执行,但是是根据延迟的绝对值来自动运行的,所以如果动画已经在指定的时间之前就开始运行了,那它就会直接从active的时间中途运行。 —CSS Animations Level 1

因为我们的动画是暂停的,它的第一帧就是我们唯一展示的那一帧(通过我们的animation-delay定义)。饼图上显示的百分比将会是我们的animation-delay的总时间。例如,当前的持续时间是6s,我们的 animation-delay 值为-1.2s则显示20%的百分比。为了简化计算,我们设置一个100s的持续时间。记住因为我们的动画是永远暂停的,我们给它指定的延迟大小并不会有什么影响。

还有最后一个问题:动画是赋给伪元素的,但是我们想要给.pie元素设置内联样式。因为<div>上没有动画,我们可以给它设置animation-delay作为内联样式,然后给伪元素应用 animation-delay: inherit; 。综上所述,20%60%的饼图的HTML代码如下:

 
 
1
2
<div class="pie" style="animation-delay: -20s"></div>
<div class="pie" style="animation-delay: -60s"></div>

刚刚提出的这个动画的CSS代码如下(省略 .pie 规则,因为没有改变):

 
 
 
 
 
CSS 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
@keyframes spin {
  to { transform: rotate(.5turn); }
}
 
@keyframes bg {
  50% { background: #655; }
}
 
.pie::before {
  /* [Rest of styling stays the same] */
  animation: spin 50s linear infinite, bg 100s step-end infinite;
  animation-play-state: paused;
  animation-delay: inherit;
}

这时候,可以把HTML标签改成使用百分比作为内容,和一开始希望的一样,然后通过一个简单的脚本为其添加 animation-delay 内联样式。

 
 
 
 
 
JavaScript 
1
2
3
4
$$('.pie').forEach(function(pie) {
  var p = parseFloat(pie.textContent);
  pie.style.animationDelay = '-' + p + 's';
});

图8:没有隐藏文本前的图

  • 把饼图的height转换成 line-height (或添加一个和height值相等的line-height,但是这值是毫无意义的重复代码,因为line-height会自动计算height的值)。
  • 通过绝对定位给伪元素设置大小和位置,这样它不会把文本挤下去。
  • 添加 text-align: center; 让文本水平居中。

最后的代码如下:

 
 
 
 
 
CSS 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
.pie {
  position: relative;
  width: 100px;
  line-height: 100px;
  border-radius: 50%;
  background: yellowgreen;
  background-image: linear-gradient(to right, transparent 50%, #655 0);
  color: transparent;
  text-align: center;
}
 
@keyframes spin {
  to { transform: rotate(.5turn); }
}
@keyframes bg {
  50% { background: #655; }
}
 
.pie::before {
  content: '';
  position: absolute;
  top: 0; left: 50%;
  width: 50%; height: 100%;
  border-radius: 0 100% 100% 0 / 50%;
  background-color: inherit;
  transform-origin: left;
  animation: spin 50s linear infinite, bg 100s step-end infinite;
  animation-play-state: paused;
  animation-delay: inherit;
}

 

See the Pen qdvRMv by Airen (@airen) on CodePen.

基于SVG的解决方案


SVG使得很多图形工作变得更加简单,饼图也不例外。但是,用path路径创建饼图,需要复杂的数学计算,我们可以使用一点小技巧来代替。

我们从一个圆开始:

 
 
1
2
3
<svg width="100" height="100">
<circle r="30" cx="50" cy="50" />
</svg>

现在,给它应用一些基础的样式:

 
 
 
 
 
CSS 
1
2
3
4
5
circle {
  fill: yellowgreen;
  stroke: #655;
  stroke-width: 30;
}

 

注意:你可能知道,这些CSS属性也可以作为SVG元素的属性使用,如果把可移植性考虑在内的话这可能挺方便的。

图9:从一个绿色的SVG圆形,带一个胖胖的#655描边开始

你可以在图9中看到我们绘制的加了描边的圆。SVG描边不止有strokestroke-width属性。还有很多不是特别流行的描边相关的属性可以用于对描边进行微调。其中一个是stroke-dasharray,用于创建虚线描边。例如,我们可以使用如下:

 
 
 
 
 
CSS 
1stroke-dasharray: 20 10;

图10:一个简单的虚线描边,通过stroke-dasharray属性创建

这行代码的意思是我们的虚线是20的长度加上10的边距,如图10所示。在这里,你可能会好奇这个SVG描边属性和饼图究竟有什么关系呢。如果我们给描边应用一个值为0的虚线宽度,和一个大于或等于我们当前圆的周长的边距,它可能就清晰一些了(计算周长: C = 2πr , 所以在这里  C = 2π × 30 ≈ 189 ):

 
 
 
 
 
CSS 
1stroke-dasharray: 0 189;

图11:不同stroke-dasharray值对应的效果;从左到右: 0 189; 40 189; 95 189; 150 189 

如图11中的第一个圆所示,它的描边的都被移除了,只剩下一个绿色的圆。但是,当我们开始增大第一个值的时候,有趣的事情发生了(图11):因为边距太长,我们就没有虚线描边了,只有一个描边覆盖了我们指定的圆的周长的百分比。

你可能已经开始弄清楚了这是怎么回事:如果我们把圆的半径减小到一定程度,它可能就会完全被它的描边覆盖,最后得到的是一个非常类似于饼图的东西。例如,你可以在图12中看到:当给圆应用一个25的半径和一个50stroke-width,像下面的效果:

图12:我们的SVG图像开始像一个饼图了O(∩_∩)O

记住:SVG描边总是相对于元素边缘一半在内一半在外的(居中的)。将来应该可以控制这一行为。

 
 
1
2
3
4
5
6
7
8
9
10
<svg width="100" height="100">
  <circle r="25" cx="50" cy="50" />
</svg>
 
circle {
  fill: yellowgreen;
  stroke: #655;
  stroke-width: 50;
  stroke-dasharray: 60 158; /* 2π × 25 ≈ 158 */
}

现在,把它变成我们在上一个解决方案中制作的饼图的样子是非常容易的:我们只需要在描边下面添加一个更大的绿色圆形,然后逆时针旋转90°,这样它的起点就在顶部中间。因为<svg>元素也是HTML元素,我们可以给它添加样式:

 
 
 
 
 
CSS 
1
2
3
4
5
svg {
  transform: rotate(-90deg);
  background: yellowgreen;
  border-radius: 50%;
}

图13:最后的SVG饼图

你可以在图13中看到最终结果。这种技术可以让饼图更容易实现从0%100%变化的动画。我们只需要创建一个CSS动画,让stroke-dasharray从 0 158 变成 158 158 :

 
 
 
 
 
CSS 
1
2
3
4
5
6
7
8
9
10
11
@keyframes fillup {
  to { stroke-dasharray: 158 158; }
}
 
circle {
  fill: yellowgreen;
  stroke: #655;
  stroke-width: 50;
  stroke-dasharray: 0 158;
  animation: fillup 5s linear infinite;
}

 

作为一个额外的改进,我们可以在圆上指定一个特定半径,使其周长无限接近100,这样我们可以用百分比指定stroke-dasharray的长度,而不需要做计算。因为周长是2πr,我们的半径则是100 ÷ 2π ≈ 15.915494309,约等于16。我们还可以用viewBox特性指定SVG的尺寸,可以让它自动调整为容器的大小,不要使用widthheight属性。

经过以上调整,图13的饼图的HTML标签如下:

 
 
1
2
3
<svg viewBox="0 0 32 32">
  <circle r="16" cx="16" cy="16" />
</svg>

CSS如下:

 
 
 
 
 
CSS 
1
2
3
4
5
6
7
8
9
10
11
12
13
svg {
  width: 100px; height: 100px;
  transform: rotate(-90deg);
  background: yellowgreen;
  border-radius: 50%;
}
 
circle {
  fill: yellowgreen;
  stroke: #655;
  stroke-width: 32;
  stroke-dasharray: 38 100; /* for 38% */
}

注意现在百分比已经可以很方便地改变了。当然,即使已经简化了标签,我们还是不想在绘制每个饼图的时候都重复一遍所有这些SVG标签。这是时候拿出JavaScript来帮我们一把了。我们写一个简单的脚本,让我们的HTML标签直接简单地这样写:

 
 
1
2
<div class="pie">20%</div>
<div class="pie">60%</div>

然后在每个.pie元素里边添加一个内联SVG,包括所有需要的元素和属性。它还会添加一个<title>元素,为了增加可访问性,这样屏幕阅读器用户还可以知道当前的饼图表示的百分比。最后的脚本如下:

 
 
 
 
 
JavaScript 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
$$('.pie').forEach(function(pie) {
  var p = parseFloat(pie.textContent);
  var NS = "http://www.w3.org/2000/svg";
  var svg = document.createElementNS(NS, "svg");
  var circle = document.createElementNS(NS, "circle");
  var title = document.createElementNS(NS, "title");
  circle.setAttribute("r", 16);
  circle.setAttribute("cx", 16);
  circle.setAttribute("cy", 16);
  circle.setAttribute("stroke-dasharray", p + " 100");
  svg.setAttribute("viewBox", "0 0 32 32");
  title.textContent = pie.textContent;
  pie.textContent = '';
  svg.appendChild(title);
  svg.appendChild(circle);
  pie.appendChild(svg);
});

 

就是它了!你可能会觉得CSS方法比较好,因为它的代码比较简单而且更靠谱。但是,SVG方法相比纯CSS方案还是有一定的优势的:

  • 可以非常容易地添加第三种颜色:只需要添加另一个描边圆,然后使用stroke-dashoffset设置它的描边属性。然后,将它的描边长度添加到下方的圆的描边长度上。如果是前面那个CSS的方案,你要如何给饼图添加第三种颜色呢?
  • 我们不需要考虑打印的问题,因为SVG元素就像<img>元素一样,被默认为是内容的一部分,打印完全没有问题。第一种方案取决于背景,所以不会被打印。
  • 我们可以使用内联样式改变颜色,也就是说我们可以通过脚本就直接改变颜色(如,根据用户输入改变颜色)。第一种方案依赖于伪元素,除了通过继承,它没有其它办法可以添加内联样式,这很不方便。

  都说程序员的工资高,却很少了解他们加班的痛苦,你是不是每次也在心里想,按时间折算下来这个工资都给少了,于是会想在心里呐喊,要么涨工资,要么涨工资,要么涨工资,为什么??因为不让我们加班,这是不可能的!!!

  想要颠覆自己的工作模式吗?想要减少自己的加班时间吗?加入我们,和我们一起探寻属于我们程序员的自由模式吧!

  一款针对程序员的原生APP,以共享知识技能为目的,以悬赏方式在线互动交互平台。

  我们拥有高达近20人顶尖的技术团队,以及优秀的产品及运营团队。团队领军人物均在行业内有10年以上的丰富经验。

  现在我们正在招募原始的参与英雄,您将同我们一起改变程序员的工作方式,改变程序员的世界!同时也会有丰厚的报酬。作为我们的原始的参与者,您将同我们一起体验这款程序员神器,您可以提出专业的建议,我们会虚心采纳。每一个人都会是英雄,而您就会是我们需要的英雄!同时您也可以邀请您的朋友一起参与这场英雄的招募互动。

  我们不会耽误你太多时间,我们只需要您的专业看法,只要您从一个月内抽出1个小时,以后您每天都可以节省两个小时,一切都是为了我们自己!

  来?还是不来?

  接头人暗号:1955246408 (QQ)