使用CSS 或 SVG 制作可扩展、易维护修改的饼状图 | 前端记录
前端记录 记录分享前端知识

前言

最近 CSS 女神 Lea Verou 出版了一本书《CSS Secrets》,书中有大量的css技巧来解决日常开发会碰到的问题。这是一本让人感到惊喜的书,有兴趣的开发者朋友可以购来一看。本文是翻译了这本书中一个片段。

饼状图,是一种常见的网页技术,可以用来显示统计数据、计时器等。以往我们使用多图片重合或者 javascript 框架来构建饼状图。今天我们使用一种更简单的方式来构建可扩展控制的饼状图。

基于 Transform 的解决方案

这个方法只用到一个元素,剩下只需要定义它的伪元素、变形、渐变就行。我们先开始一个简单的例子:

现在我们假设需要饼状图能显示 20% 的统计比例,而且比例可以随时变化。我们先创造一个圆:

图片1:开始绘图(显示统计比例 0%)

我们的饼状图用yellowgreen当背景色,用棕色#655显示百分比。把这两种颜色显示在圆圈的两边,并通过旋转伪元素覆盖出我们需要显示的百分比

要获得拥有两种颜色的圈,我们只需要使用一个简单的渐变:

图片2:用一个简单的渐变获得双色圆

然后,我们继续定义一个伪元素充当遮罩:

图片3: 伪元素将会显示在虚框位置

从图3可以看到伪元素相对于饼形图的位置,目前伪元素没有任何样式和内容,仅仅是个无形的矩形。在填充样式之前,我们考虑一下几点:

  • 我们希望它能覆盖圆的棕色部分,这需要让它也显示绿色,可以使用background-color: inherit来避免重复,表示它有与父元素相同的背景色。
  • 我们希望它绕着圆的中心旋转,这个中心现在是位于伪元素的左侧,所以需要使transform-origin的值为0 50%left,让伪元素的重心靠左。
  • 我们不希望伪元素是方形的,因此需要给.pie加上overflow: hidden,以切除伪元素多余的部分。或者给伪元素设置合适的border-radius让它像个扇形。

考虑完这些,伪元素的 CSS 可以这样设置:

图片4:样式设置完后伪元素显示在虚框中

提示:注意不要使用background: inherit;,应该使用background-color: inherit;以替代,否则渐变也会被继承!

现在我们的饼状图就是图片4的样子,接下来乐趣就开始了。我们可以使用transformrotate()旋转伪元素,如果是旋转20%,可以使用72deg(0.2 * 360 = 72),或者使用更容易阅读与理解的.2turn,当然也可以设置其他不同的值,如图片5中所示:

图片5: 我们简单的饼状图已经可以显示比例了; 从上到下依次: 10% (36deg 或者 .1turn), 20% (72deg 或者 .2turn), 40% (144deg 或者 .4turn)

看起来工作似乎完成了,事实上并没那么简单。饼状图现在能很好的显示0%-50%,但如果要旋转60%,图片6就出现了。不过可以使用一个简单的方法解决这个问题。

图片6: 在需要显示 60% 的时候图上的百分比显示小于 50%

如果把50%-100%的情况单独分离出来,就可以注意到做个对立版本:只需要把伪元素的颜色改成棕色。我们可以给伪元素在0%-50%、50%-100%时定义不同的颜色。对于60%的饼状图可以这样写:

图片7:正确的 60% 显示

结果如图片7显示,现在就能显示任意百分比了,我们使用css动画做个从0%-100%变化的指示图:

动画1

这样一切看起来都很好,但我们如何在HTML里用一种通用简单易读易修改的方式设置不同的百分比显示?如果 HTML 只需要这样写就好了:

上面代码中定义了两个饼状图,一个显示20%,一个显示60%。首先要考虑如何获取数值并加入到内联样式,这个使用一段简单的 js 就可以完成。不过我们是要在伪元素上设置样式以显示百分比,如你所知,伪元素是无法添加内联样式的,所以我们需要做点创新。

解决方案来自很难被想到的地方——动画的 paused(暂停)属性。我们将使用负的 animation delays 将动画提前暂停在我们需要的时间点。是不是有点迷惑?负的animation-delay不仅仅是规范允许的,而且非常有用:

提示:animation-delay可以指定在动画启动之前要等待的时间量,该属性的默认值为 0,并且该属性不会被继承。将 animation-delay 设置为负值会导致动画启动,就好像已经过指定的延迟。如果你在其他的项目中需要使用某些不需要重复和复杂计算的元素的值,或者需要调试动画,都可以使用这种方法。
这里有个简单的例子

因为动画是paused(暂停)的,所以第一帧就会暂停在我们使用负的animation-delay指定的位置,饼状图上的比例就是通过animation-delay定义的相对整个动画运行一周所需时间的比例。例如当一个完成的动画需要6s时,我们可以定义animation-delay-1.2s来显示20%。为了方便计算,我们把接下来整个动画时间设定为100s来完成饼形图。

现在还有一个问题:动画是要运行在伪元素上,但我们只能给.pie设置内联样式.pie本身没有动画,所以我们可以给它设置animation-delay,然后再给伪元素设置animation-delay: inherit;以继承该属性。考虑完这些,20% 跟 60% 的饼形图可以这样写:

我们最初是要把需要显示的百分比写在HTML里,而不是直接写成内联样式。这个可以通过一段简单的js把百分比提取出来作为animation-delay内联样式:

20%
60%
图片8: 文本没有隐藏

现在的饼状图如图片8所示,左下角存在个文本,我们可以给文本应用color: transparent以隐藏文本,同时是它仍然可选择可打印。另外可以把它放在饼状图的中间位置,以便用户点选。要做到这点,我们需要做这些:

  • 把 pie 的height转换成line-height,以便使内容能垂直居中,同时有了line-height 后元素会自动计算高度。
  • 把伪元素设置为绝对定位,这样它就不会把文本挤下来。
  • 添加text-align: center;使文本水平居中。

最后的代码如下所示:

SVG 解决方案

SVG使很多图形绘画处理变得方便。但是使用路径绘画出一个饼状图需要很复杂的数学,我们将使用一个简单的技巧来绘制。

我们先画一个圆:

然后给它定义点基本样式:

提示:你可能已经知道了,CSS属性也可以作为SVG元素的属性。

图片9: 使用SVG画出一个带描边的圆

你可以在图片9看到一个带描边的圆,不过 SVG 的描边并不只有strokestroke-with两种属性,还有很多不常用的描边属性来定义描边。比如stroke-dasharray,用它可以创造一个虚线的描边,下面有个例子:

图片10: 一个简单的虚线描边, 通过stroke-dasharray定义

如果定义了虚线长为20间隔为10就会得到图片10。你可能还是很奇怪饼状图跟这个 SVG 的描边属性有什么关系。如果我把描边的长度定为0,描边间隔定为大于等于它的周长(C = 2πr,描边的宽度为30,所以它的周长是2π * 30 ≈ 189),事件就清晰很多了:

图片11: 不同的stroke-dasharray的值和结果;从左到右分别是:0 189; 40 189; 95 189; 150 189

可以看到图片11中的第一个圆已经完全没有了描边,只剩一个绿色圆。当我们增加第一个属性值的大小,因为虚线间距太长以致无法显示出第二条虚线,只显示了第一条虚线,这条虚线的长度我们可以随意定义的。

你可能已经猜到我是要干嘛:如果我们减少圆的半径,一直到它能完全被描边覆盖,我们就能得到一个类似闭合的饼状图。如图片12我们把圆的半径定义为25,把描边的宽定义为50

图片12: 我们的 SVG 图形开始像一个饼状图了

注意:SVG 的描边总是一半在元素外部一半在元素内部。现在是无法定义它处于外部或内部的,可能在未来新的css协议下我们能控制这个行为。

现在我们只需要在描边下方添加一个大的绿色圆圈,并且把描边逆时针旋转90°让它的起点处于顶部的中间。由于 SVG 也是一个 HTML 元素,我们可以给它添加样式:

图片13: 最终的 SVG 饼状图

你可以在图片13看到最终结果,这个方法可以更简单的使用动画让比例从 0% 变化到 100%,我们只需要添加一个 CSS 动画让stroke-dasharray0 158变化到158 158

为了更简单指定不同比例饼形图,我们可以给圆指定一个半径,让它的周长等于(无限接近)100,这样就可以很简单明了的指定虚线的百分比,而不用计算。周长等于2πr,我们需要的半径就是 100 ÷ 2π ≈ 15.915494309,把它约等于16。然后再给 SVG 指定一个viewBox属性来代替widthheight以使他放大充满画布。

经过这些修改,代码变成:

如果要简单的定义出更多的饼状图,我们肯定不希望重复的绘制SVG。可以让JavaScript自动化的做这件事,只需写一段简单的 HTML,剩下的交给 JavaScript:

把 SVG 加入到每个.pie元素里。同时可以添加个<title>元素,以便屏幕阅读器的用户可以了解是什么比例呈现在显示器上。最后的 js 可以这样写:

好了,SVG 的方法到此为止。你可能觉得 CSS Transform 的解决方法更好,因为代码更简单易读,但 SVG 的方法相对 纯粹地CSS是有好处的:

  • 可以简单的添加第三种颜色:只需添加另外一种颜色的描边并使用stroke-dashoffset偏移上一个描边的长度量。或者也可以让这个新描边的长度大于上一个描边,并置于上一个描边底部。
  • 我们不需要额外的再考虑打印问题:SVG 元素就像img元素一样是可以直接打印的,第一种解决方案颜色是取于背景色,无法打印。
  • 我们可以通过内联样式改变颜色,这意味这我们可以通过 js (例如,通过用户输入框)直接修改样式。第一种元素依赖于伪元素,只能使用继承,无法使用内联样式,这是不方便的。

这里是最终的饼状图

未来的饼状图

一个新的 CSS 属性有望添加进 CSS4 标准中,锥形渐变(Conical gradients)。这个只需要个圆形元素,然后用锥形渐变定义两种颜色,就可以简单的做个饼状图。

此外,通过attr()控制 HTML 属性,你可以轻易地改变饼状图显示比例:

而且它也能很简单的添加第三种颜色,例如我们要在一个饼状图上添加两种颜色以显示比例,只需在渐变上多加两种颜色:

现在已经有一部分浏览器支持conic-gradient了(例如最新的谷歌浏览器),这里有锥形渐变的详细介绍: Lea’s Conic Gradient polyfill

终于翻译完了!!!囧rz,《CSS Secrets》是本不错的书,本文只是这本书中的一个奇技淫巧,有兴趣的可以买原版一看或者等译版出版后再买。这里是Lea Verou的博客,学习前端的可以收藏下。本文如果转载请注明版权跟原文地址。

1 条评论

欢迎留言