如何用 SVG 制作图表

Avatar of Robin Rendle
Robin Rendle

DigitalOcean 提供适用于您旅程各个阶段的云产品。 立即开始使用 $200 免费积分!

我关于制作图表的第一个帖子 中,我探讨了仅依赖 CSS 的方法。 我认为这在大多数情况下不是最佳选择; 有太多棘手的设计和开发障碍需要克服。 一般来说,最好使用 SVG、JavaScript 和 CSS 的组合来制作图表。

为什么不使用 canvas

您可以在网页上制作图表的方式有很多,最值得注意的是使用 canvas 元素。 然而,Sara Soueidan 建议 也要避免这种方法

HTML5 Canvas 也可以用来创建这样的可视化效果,但画布的内容不是 DOM 的一部分,因此屏幕阅读器无法访问。 您需要在 标签的开头和结尾之间创建一个辅助内容作为备用内容和可访问内容。 您还需要采取额外的措施来映射画布内容和备用内容之间的内容和交互性,以便屏幕阅读器知道正在与哪个元素进行交互。 因此,HTML5 Canvas 需要维护两倍的内容。 […] 使用 SVG,您可以获得语义和可访问性以及开箱即用的 JavaScript 交互性。

但是,这种独立的 canvas 方法还有其他替代方案。 例如,Filament Group 创建了一个名为 Visualize 的 jQuery 插件,该插件从 table 元素中获取数据,然后创建一个 canvas 图表。 这种做法很有意义,即使该元素本身最不适合制作图表的任务。

为什么选择 SVG?

SVG 图像格式不仅用于 图标 或简单图像。 它在制作图表方面也有优势。 在 我们的 SVG 概览 中,我们这样描述了该格式的一般优势

  • 文件大小小,压缩效果好
  • 可以按任何大小缩放,不会失去清晰度(除了非常小的尺寸)
  • 在视网膜显示屏上看起来很棒
  • 设计控制,如交互性和滤镜

我们可以再补充两点对于图表很有用的要点

  • SVG 对屏幕阅读器可访问(只需要做一些工作)
  • 有很多基于 SVG 的图表框架可供使用

让我们开始吧。 用 SVG 制作图表的最简单方法是什么?

使用 制作图表

用 SVG 制作图表就像在 Illustrator 或您选择的基于矢量的设计应用程序中设计一个,将其导出为 SVG,然后使用 标签直接将其插入标记一样简单

<img src="chart.svg" alt="Hopefully you can impart equally useful alternate content here.">

这很棒,因为它看起来很好,而且缩放效果很好。 然而,我们将会失去内联 SVG 的大多数优势,例如可访问性和交互性。 除了 alt 文本之外,我们的数据不会被屏幕阅读器朗读出来,图表本身的数据点也无法通过鼠标、触摸或键盘输入进行交互。

这些问题表明,如果我们想尽可能地控制它们,我们应该使用另一种 SVG 嵌入技术。 例如,如果我们正在处理一个类似于 Death from Above 的项目,其中图形的交互性极大地帮助我们理解数据,会发生什么?

A gif showing the interface of the Death from Above website

为了最大程度地利用 SVG,我们需要获取所有这些代码并将其直接放入我们的标记中。 这样我们就可以使用 CSS 对图形进行样式设置,使用 JavaScript 控制交互性,并获得内联 SVG 的所有可访问性优势。

(我们可以使用 <object><iframe> SVG 嵌入获得类似的优势,但概念非常相似,让我们继续使用内联 SVG。)

条形图

我们 图表 的每一列都包含在一个 <g> 元素中(在 SVG 中,这只是一组相关的元素),在每个元素内部,我们将放置一个 rect 元素,它定义了列的形状,以及一个 text 元素,它允许我们在屏幕上打印数字。 这是一个完成的示例

我们可以使用标准的 x/y 坐标来定位这些 <rect><text> 元素,如下所示

<svg class="chart" width="420" height="150" aria-labelledby="title desc" role="img">
  <title id="title">A bar chart showing information</title>
  <desc id="desc">4 apples; 8 bananas; 15 kiwis; 16 oranges; 23 lemons</desc>
  <g class="bar">
    <rect width="40" height="19"></rect>
    <text x="45" y="9.5" dy=".35em">4 apples</text>
  </g>
  <g class="bar">
    <rect width="80" height="19" y="20"></rect>
    <text x="85" y="28" dy=".35em">8 bananas</text>
  </g>
  <g class="bar">
    <rect width="150" height="19" y="40"></rect>
    <text x="150" y="48" dy=".35em">15 kiwis</text>
  </g>
  <g class="bar">
    <rect width="160" height="19" y="60"></rect>
    <text x="161" y="68" dy=".35em">16 oranges</text>
  </g>
  <g class="bar">
    <rect width="230" height="19" y="80"></rect>
    <text x="235" y="88" dy=".35em">23 lemons</text>
  </g>
</svg>

请注意,您可以将鼠标悬停在元素上以更改条形颜色和文本颜色? 这可以通过 fill CSS 属性实现

.bar {
  fill: red; /* changes the background */
  height: 21px;
  transition: fill .3s ease;
  cursor: pointer;
  font-family: Helvetica, sans-serif;
}
.bar text {
  color: black;
}
.bar:hover,
.bar:focus {
  fill: black;
}
.bar:hover text,
.bar:focus text {
  fill: red;
}

简单的交互式 SVG 万岁!

但是,这里存在一个问题:在图表中进行制表键操作将无法工作(因为 浏览器尚不支持 focusable 属性)。 Léonie Watson 提供了有关 SVG 的可访问性提示,建议您可以使用 <a xlink:href="#"> 链接来实现可聚焦性 - 但是,如果可聚焦区域实际上不是链接,该怎么办?

我们将在即将发布的文章中深入探讨这个问题。

火花线

由于火花线实际上是微型条形图,因此我们可以使用与之前相同的代码来制作此示例

这次要将每个 g 元素并排对齐,我们可以使用内联 CSS transform(最好坚持使用标准的 x 和 y 坐标,但这只是证明它是可能的)。 然后我们将更改 rect 元素上的 height 属性以正确显示数据,并使用 y 坐标将每个元素从顶部向下推。 这应该将每个条形图与火花线的底部对齐。 这是一个简化的代码示例

<g class="bar" transform="translate(0,0)">
  <rect height="10" y="10" width="3"></rect>
</g>
<g class="bar" transform="translate(3,0)">
  <rect height="6" y="14" width="3"></rect>
</g>

折线图

要标记折线图的数据点,我们可以使用 polyline 元素和 points 属性

<svg viewBox="0 0 500 100" class="chart">
  <polyline
     fill="none"
     stroke="#0074d9"
     stroke-width="3"
     points="
       0,120
       20,60
       40,80
       60,20"/>
</svg>

在此示例中,0,120 表示 SVG 画布的左侧 0 和顶部 120。 准备好数据点列表后,我们可以使用 stroke-width 定义线的宽度,使用 stroke 定义线的颜色

但这只是对数据进行样式设置 - 那些帮助每个人破译数据点本身的重要线呢? 轴呢?

制作轴和标签

Roemer Vlasveld 制作了 一篇很棒的教程,介绍如何使用 SVG 开发图形,并且他记录了一些有趣的属性,这些属性将帮助我们标记和设置图形每个轴的样式。 我们将在下面的示例中使用他工作的一个简化版本

<svg class="graph">
  <g class="grid x-grid">
    <line x1="90" x2="90" y1="5" y2="371"></line>
  </g>
  <g class="grid y-grid">
    <line x1="90" x2="705" y1="370" y2="370"></line>
  </g>
</svg>

每个 g 将用于创建水平线和垂直线,并且与正确定位的文本标签结合使用时,这将生成我们散点图的基本样式和标记,只是没有数据

在将数据的每个点作为一系列 circle 添加到另一个 g 元素内部之后,我们可以看到它作为散点图的外观

饼图

我们已经多次提到 Lea Verou 关于饼图的文章,因为它是一个很好的入门指南。 我在这里不会详细重复她的技术,尽管我认为看看我们如何制作这些交互式 SVG 饼图会很有趣。 这是一个完成的示例,让您了解我们的方向

当单击饼图上方的某个按钮时,图表将使用存储在 JavaScript 对象中的值进行更新。 此方法可能存在可访问性问题,但我想要关注的是 JavaScript 和 SVG 的交互式组合。

首先是标记

<figure>
  <figcaption>
    Percentage of world population by continent
  </figcaption>
  
  <div class="buttons"></div>

  <svg width="100" height="100" class="chart">
    <circle r="25" cx="50" cy="50" class="pie"/>
  </svg>
</figure>

然后我们可以设置人口数据,如下所示

var continents = {
  asia: 60,
  northAmerica : 5,
  southAmerica: 9,
  oceania: 1,
  africa: 15,
  europe: 12
};

我们要做的是用一系列按钮填充空的 .buttons div,当点击这些按钮时,将更改 circle SVG 元素的 stroke-dasharray 属性。我们可以像这样创建这些按钮

var buttons = document.querySelector('.buttons');

for(property in continents) {
  var newEl = document.createElement('button');
  newEl.innerText = property;
  newEl.setAttribute('data-name', property);
  buttons.appendChild(newEl);
}

接下来,我们需要修正这些百分比,因为我们 continents 对象中的 asia: 60 意味着 100 中的 60 ,而不是 圆周上的 60。我们可以创建一个函数来解决这个问题

var total = 158; 

var numberFixer = function(num){
  var result = ((num * total) / 100);
  return result;
}

从那里我们可以为每个按钮添加一个事件监听器,并创建一个名为 setPieChart() 的新函数,该函数通过双重检查每个按钮的 data-name 属性并在我们对象中找到相应的洲来更改 stroke-dasharray 的值

buttons.addEventListener('click', function(e){
  if(e.target != e.currentTarget) {
    var el = e.target,
        name = el.getAttribute('data-name');
    setPieChart(name);
    setActiveClass(el);
  }
  e.stopPropagation();
});

var setPieChart = function(name) {
  var number = continents[name],
      fixedNumber = numberFixer(number),
      result = fixedNumber + ' ' + total;
  pie.style.strokeDasharray = result;
}

一旦我们添加了一些辅助函数来为按钮添加活动样式,我们就拥有了一个功能齐全的交互式饼图

使用 CSS 和 JavaScript 操作 SVG

对前一个示例中更改进行动画处理相对简单,我们所要做的就是使用 CSS 中的 transition 属性,如下所示

circle {
  transition: stroke-dasharray .3s ease;
}

然后,一旦我们用脚本更改了属性,CSS 就会为我们完成所有动画处理。但是,还有哪些其他 SVG 属性可以用 CSS 操作呢?

好吧,使用 CSS 样式化 SVG 的一个特点是,我们只能控制某些属性。例如,如果我们想更改 g 的 x 或 y 坐标(不使用 CSS transform 属性),那么我们需要使用 JavaScript。如果您不熟悉 SVG 语法,那么这将更加奇怪,因为 CSS 属性会影响某些元素,而不会影响其他元素。

有一个方便的 W3C 提供的属性列表 ,其中显示了哪些属性影响哪些 SVG 元素,因此如果您在样式化它们时没有看到预期结果,请务必仔细检查该列表。

手动编辑 SVG 不是一个完美的解决方案

使用其他基本形状,包括 rect、 line 和 polygon ,我们可以创建任何我们想要的 SVG 图表。真正的问题是:我们真的想要吗?例如,当您手动编辑 SVG 时,线状图当然可以制作,但我不会推荐这样做,因为语法有点复杂,尤其是如果您想做一些像曲线线这样的事情。

手动编写 SVG 可能很慢且令人沮丧。即使对于简单的图表,编写代码和可视化定位每个部分都需要很长时间。与制作图形的纯 CSS 解决方案非常相似,除非您制作的是非常小的东西,否则您手动使用 SVG 制作图形的体验可能很痛苦。

一定有更好的方法,对吧?

框架来救援!

在下一篇文章中,我们将讨论使用图表框架来帮助我们更容易地生成图表的所有优点(和问题)。我们将比较所有流行的图表框架,并看看以更有效率、更健康的方式可视化数据是什么感觉。

更多信息