渐进增强和数据可视化

Avatar of Chris Coyier
Chris Coyier 发表

DigitalOcean 为您旅程的每个阶段提供云产品。立即开始使用 200 美元的免费额度!

以下是 Chris Scott 的客座文章。Chris 之前曾为我们 撰写过文章,例如 这篇文章,他一直走在技术前沿。这次,Chris 向我们展示了一种新的图表技术,这是他公司提供的。但它基于旧事物:网络本身的基本原理。

这个月,我的公司 Factmint 发布了其图表套件。由于多种原因(尤其是其设计),它们非常酷,但其中一个对 Web 开发人员特别感兴趣:它们都将简单的 HTML 表格转换为复杂、交互式的数据可视化。这种方法是渐进增强的完美示例——为了浏览器兼容性、可访问性和 SEO,您拥有一个简单的表格,对于现代浏览器则拥有一个漂亮的图形。

我想更详细地探讨我们使用的技术。所以,开始了…

回顾渐进增强

PE 有几个核心概念。以下是 维基百科 上的列表

  • 基本内容应可供所有 Web 浏览器访问
  • 基本功能应可供所有 Web 浏览器访问
  • 稀疏的语义标记包含所有内容
  • 增强的布局由外部链接的 CSS 提供
  • 增强的行为由非侵入式、外部链接的 JavaScript 提供
  • 尊重最终用户 Web 浏览器的首选项

基本上,实现一个简单的、跨浏览器的、纯 HTML 解决方案。完成此操作后,您将拥有一个安全的最低功能页面。现在,使用 CSS 和 JavaScript 在此基础上进行构建。

本文将探讨如何使用这些概念来生成数据可视化。

数据可视化以数据为后盾

这很明显,但值得一提:数据可视化基于一些底层数据。在构建图形时,这些数据不需要丢失(例如,在光栅图像中会丢失)。数据格式也不必是 JSON,因为大多数图表库都使用 JSON。

回到 PE 的“核心概念”的前三个,基本功能应该是某种对数据进行编码的标记。例如 HTML 表格或列表。

工作示例

为了说明这个想法,我们将渐进增强一个时间线,使其成为 SVG 可视化。数据可能类似于以下内容

  • 1969:UNICS
  • 1971:UNIX 分时系统
  • 1978:BSD
  • 1980:XENIX 操作系统
  • 1981:UNIX 系统 III
  • 1982:SunOS
  • 1983:UNIX 系统 V
  • 1986:GNU(Trix)
  • 1986:HP-UX
  • 1987:Minix
  • 1989:NeXTSTEP
  • 1989:SCO UNIX
  • 1990:Solaris
  • 1991:Linux
  • 1993:FreeBSD
  • 1995:OpenBSD
  • 1999:Mac OS X

基本内容

有多种方法可以对这些数据进行编码。我将使用定义列表——我认为这在语义上是准确的,并且在没有太多样式的情况下也能很好地显示。让我们从基础(无增强)情况开始

<dl class="timeline">
  <dt>1969</dt><dd>UNICS</dd>
  <dt>1971</dt><dd>UNIX Time-Sharing System</dd>
  <dt>1978</dt><dd>BSD</dd>
  <dt>1980</dt><dd>XENIX OS</dd>
  <dt>1981</dt><dd>UNIX System III</dd>
  <dt>1982</dt><dd>SunOS</dd>
  <dt>1983</dt><dd>UNIX System V</dd>
  <dt>1986</dt><dd>GNU (Trix)</dd>
               <dd>HP-UX</dd>
  <dt>1987</dt><dd>Minix</dd>
  <dt>1989</dt><dd>NeXTSTEP</dd>
               <dd>SCO UNIX</dd>
  <dt>1990</dt><dd>Solaris</dd>
  <dt>1991</dt><dd>Linux</dd>
  <dt>1993</dt><dd>FreeBSD</dd>
  <dt>1995</dt><dd>OpenBSD</dd>
  <dt>1999</dt><dd>Mac OS X</dd>
</dl>

它看起来像这样

Chrome 中未设置样式的 DL

好的,它不漂亮,但它在所有浏览器上都清晰且可访问,并且对搜索引擎也有帮助。

增强布局

现在,让我们使用样式表来改进布局。这可以进一步完善,但出于本文的目的,让我们只进行一些简单的改进

html {
  font-family: sans-serif;
}

dl {
  padding-left: 2em;
  margin-left: 1em;
  border-left: 1px solid;
  
  dt:before {
    content: '-';
    position: absolute;
    margin-left: -2.05em;
  }
}

现在它呈现为

这看起来更像是一条时间线,但它存在一些明显的问题。最重要的是,时间线点应根据其相对日期进行分布(因此 1983 年和 1986 年不应彼此相邻)。此外,我希望时间线水平运行,以避免需要滚动(在生产案例中,我将检查最佳方向)。

增强行为

现在是乐趣的部分。我们将使用外部链接的、非侵入式的 JavaScript 来渲染 SVG 时间线,并用它替换定义列表。最终的可视化效果将如下所示

目标:由 JavaScript 增强的 SVG 时间线

非侵入式 JavaScript

我们将使用此脚本生成 SVG 图形,因此为了保持脚本的非侵入性,我们可以做的最重要的事情是检查浏览器是否支持 SVG

function supportsSvg() {
  return document.implementation &&
    (
      document.implementation.hasFeature('http://www.w3.org/TR/SVG11/feature#Shape', '1.0') ||
      document.implementation.hasFeature('SVG', '1.0')
    );
}

这应该提供相当准确的功能检测。或者,您可以选择 Modernizr。

现在,在执行任何操作之前,我们将检查 SVG 支持:如果浏览器支持 SVG,我们将绘制一个漂亮的可视化效果并隐藏定义列表,否则我们将保留列表。

if (supportsSvg()) {
  var timeline = document.querySelector('.timeline');
  timeline.style.display = 'none'; // We don't need to show the list
  // draw the graphic...
}

提取数据

PE 数据可视化这种方法的关键原则是数据格式是语义标记,因此让我们解析我们的数据…

function getDataFromDefinitionList(definitionList) {
  var children = definitionList.children;
  
  var yearIndex = {};
  var data = [];
  var currentYear = null;
  
  for (var childIndex = 0; childIndex < children.length; childIndex++) {
    var child = children[childIndex];
    
    if (child.nodeName == 'DT') {
      currentYear = child.innerText;
    } else if (child.nodeName == 'DD' && currentYear !== null) {
      if (! yearIndex[currentYear]) {
        yearIndex[currentYear] = data.length;
        data.push({
          year: currentYear,
          values: []
        });
      }
      
      data[yearIndex[currentYear]].values.push(child.innerText);
    }
  }
  
  return data;
}

那里有很多事情发生,但本质很简单:遍历子元素,使用 DT 作为年份,使用 DD 作为版本。事实上,定义列表允许每个 DT 多个 DD,这使得它稍微复杂了一些,因此需要查找将同一年的其他版本添加到数据数组中的同一条目。

其输出将类似于

[
  {
    "year": "1969",
    "values": ["UNICS"]
  }
  ...
]

拥有这样的数组而不是对象映射非常有用。稍后遍历条目会容易得多。

准备画布

要绘制此可视化效果,我们将使用 SnapSVG。它是 Dmitry Baranovskiy 编写的 SVG 绘图库,他也是 Raphael.js 的作者。首先,我们需要创建一个 SVG 元素

var SVG_NS = 'http://www.w3.org/2000/svg';

function createSvgElement() {
  var element = document.createElementNS(SVG_NS, 'svg');
  element.setAttribute('width', '100%');
  element.setAttribute('height', '250px');
  
  element.classList.add('timeline-visualization');
  
  return element;
}

然后 Snap 可以包装该元素。如下所示

var element = createSvgElement();
var paper = Snap(element);

绘制时间线

现在是乐趣的部分!

我们将编写一个方法,该方法遍历我们的数据对象并在 SVG 元素上绘制。这两件事(数据和 SVG 元素)将成为参数

function drawTimeline(svgElement, data) {
  var paper = Snap(svgElement);
  data.forEach(function(datum) {
    // draw the entry
  });
}

最简单的时间线将为每个元素绘制一个点

function drawTimeline(svgElement, data) {
  var paper = Snap(svgElement);
  var distanceBetweenPoints = 50;
  var x = 0;
  data.forEach(function(datum) {
    paper.circle(x, 200, 4);
    var x += distanceBetweenPoints;
  });
}

这样应该会得到 17 个均匀分布的点。但我们的主要目标是正确地间隔这些点,所以让我们做一些更有趣的事情。

function drawTimeline(svgElement, data) {
  var paper = Snap(svgElement);
  
  var canvasSize = paper.node.offsetWidth;
  
  var start = data[0].year;
  var end = data[data.length - 1].year;
  
  // add some padding
  start--;
  end++;
  
  var range = end - start;
  
  paper.line(0, 200, canvasSize, 200).attr({
    'stroke': 'black',
    'stroke-width': 2
  });
  
  data.forEach(function(datum) {
    var x = canvasSize * (datum.year - start) / range;
    
    paper.circle(x, 200, 6);
  });
}

酷:现在我们的点已经分布好了,下面还有一条线。不过还没有信息,所以让我们添加一些标签。

function drawTimeline(svgElement, data) {
  var paper = Snap(svgElement);
  
  var canvasSize = paper.node.offsetWidth;
  
  var start = data[0].year;
  var end = data[data.length - 1].year;
  
  // add some padding
  start--;
  end++;
  
  var range = end - start;
  
  paper.line(0, 200, canvasSize, 200).attr({
    'stroke': 'black',
    'stroke-width': 2
  });
  
  data.forEach(function(datum) {
    var x = canvasSize * (datum.year - start) / range;
    
    paper.circle(x, 200, 6);
    
    paper.text(x, 230, datum.year).attr({
      'text-anchor': 'middle'
    });
    
    var averageIndex = (datum.values.length - 1) / 2;
    var xOffsetSize = 24;
    datum.values.forEach(function(value, index) {
      var offset = (index - averageIndex) * xOffsetSize;
      
      paper.text(x + offset, 180, value)
        .attr({
          'text-anchor': 'start'
        })
        .transform('r -45 ' + (x + offset) + ' 180');
    });
  });
}

处理有多个条目的年份稍微复杂一些,但我们还是可以处理的:datum.values.forEach 循环用于将重复项水平展开,以点为中心。还应用了旋转以防止标签重叠(即使这可能被认为是不好的做法,因为它增加了认知负荷——更好的解决方案是始终显示关键版本,并在悬停时显示其他版本,但这不是本文的重点)。

最后,让我们为 SVG 元素添加一些样式。

svg.timeline-visualization {
  circle {
    fill: white;
    stroke: black;
    stroke-width: 2;
  }
}

整合在一起

现在我们只需要在 if 语句中将我们的组件拼接在一起。

if (supportsSvg()) {
  var timeline = document.querySelector('.timeline');
  
  timeline.style.display = 'none';
  
  var data = getDataFromDefinitionList(timeline);
  
  var svgElement = createSvgElement();
  timeline.parentNode.insertBefore(svgElement, timeline);
  
  drawTimeline(svgElement, data);
}

以下是 Pen 中的完整代码。

查看 chrismichaelscott 在 CodePen 上创建的 Pen gbYqRW (@chrismichaelscott)。

总结

这种数据可视化方法有很多好处;标记是语义化的,对 SEO 友好,可供屏幕阅读器访问,并且从大多数浏览器都支持的简单元素开始逐步增强。

不过也有一些需要注意的事项。如果你只有一组静态数据,可能不值得付出这种努力——只需手动构建一个 SVG 即可。如果你有大量的数据,这可能也不是正确的方法,因为遍历 DOM 树可能效率不够高。

如果你想制作标准图表,例如饼图、环图、气泡图、折线图等,绝对值得查看 Factmint Charts。它们非常漂亮,我们在设计上投入了大量思考。