以下是 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>
它看起来像这样

好的,它不漂亮,但它在所有浏览器上都清晰且可访问,并且对搜索引擎也有帮助。
增强布局
现在,让我们使用样式表来改进布局。这可以进一步完善,但出于本文的目的,让我们只进行一些简单的改进
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 图形,因此为了保持脚本的非侵入性,我们可以做的最重要的事情是检查浏览器是否支持 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。它们非常漂亮,我们在设计上投入了大量思考。
您好,CodePen 不起作用!
哦,天哪,有人没有进行跨浏览器测试。以下是使示例在 Firefox 和 IE 中运行所需的更改列表。
将
.innerText(仅在 Webkit/Blink 中受支持)替换为.textContent(标准 DOM)。将 SVG 节点上的
.offsetWidth(仅在 HTML 元素上是标准的,而不是 SVG)替换为对 getComputed 的调用。显式地将 SVG 设置为显示模式
block或inline-block以避免在某些版本的 Firefox 上出现 getComputedStyle 错误。工作版本
http://codepen.io/AmeliaBR/pen/QwqgPa
很酷的可视化。
我在这里用纯 CSS 重做了它
http://dabblet.com/gist/d568ca6c5c3f962b90b8
还没有 100% 准确,但它可以达到那里。
我真的很喜欢这个,感谢分享!
一如既往地棒极了,信息量也很大。
但是,CodePen 没有显示所需的结果。
这太棒了,感谢分享!
很棒的例子。在数据可视化示例和教程中,通常会忽略渐进增强和其他最佳实践。
但是,您当前提供的示例对于屏幕阅读器来说并不像您想象的那么好。现代屏幕阅读器在经过 JavaScript 和 CSS 的任何修改后,都会读出网页浏览器的显示内容。当您在定义列表上设置
display:none时,它也会对屏幕阅读器用户隐藏。他们仍然可以在 SVG 中拥有文本元素,但没有时间轴的语义结构。可以使用 ARIA 修复一些小更改。
为定义列表提供一个
id属性(例如,id="timeline-1");为 SVG 提供
role="img"ARIA 属性,以便屏幕阅读器将其视为单个元素,忽略图形的文本内容和其他结构;为 SVG 提供一个
aria-describedby属性,指向定义列表的 id(没有任何哈希标记,只需aria-describedby=”timeline-1″),以便屏幕阅读器使用结构化的 HTML 内容来描述图形;为 SVG 提供一个
aria-label属性,并使用简短的名称,例如“Unix 版本时间轴”(您还可以使用 SVG title 元素来实现相同目的,但我怀疑aria-label目前具有更好的支持)。感谢您提供的提示和上面的修复。感谢您的帮助。我们在产品中不使用 innerText 或 offsetWidth,所以请对我进行一下手腕的轻拍;)
是的,我非常确定浏览器不兼容问题可能仅限于为博客快速创建的演示,并且生产就绪代码将经过充分测试!
图表库整体看起来很棒——我看到它已经使用了
aria-describedby!我很高兴在提出相同的批评之前阅读了评论,但我建议采取更简单的路线。我认为 ARIA 更像是一种“逆向适配”工具;换句话说,为什么不利用此解决方案依赖于正确的/语义化标记这一事实呢?因此,我建议提供一个按钮(可以视觉隐藏),让用户可以取消 DL 的样式——以便向键盘用户显示它,而不是实现 ARIA 角色或其他内容。
请注意,我不确定定义列表在屏幕阅读器中的支持情况如何,但这是另一回事。
@Thierry Koblentz
这是一个很好的观点。即使是完全有视力的鼠标用户有时也希望查看源数据表(或数据列表)。由于它已经在标记中,因此在单击按钮时显示它并不需要太多操作。
不错的东西。
如果你选择使用单独的功能检测……你绝对应该始终从 Modernizr 使用的内容中提取。
https://github.com/Modernizr/Modernizr/blob/master/feature-detects/svg.js
只需获取该代码并使其独立。小菜一碟。
在这种情况下,检测是由 SVG 规范编辑器编写的,并且多年来在所有浏览器中都经过了实战检验。许多替代的 SVG 检测都存在误报。在 Github 问题中搜索 Modernizr 以查看所有试验和磨难。
是的!这是我用于内联 SVG 的一个/最喜欢的。用于 SVG 作为
<img>的一个也非常简单https://github.com/Modernizr/Modernizr/blob/master/feature-detects/svg/asimg.js
正如我们所述 这里。
仅图像就可以很好地比较,向客户展示渐进增强的示例,这太棒了,整篇文章都非常适合发送给我的同事,谢谢。
我喜欢这种数据可视化方法。
我构建了整个地图,它由底部的 HTML 数据表提供支持。永久链接也有效,因此您可以在选择菜单中更改选项后将某人链接到特定地图。
http://www.pewhispanic.org/interactives/unauthorized-immigrants-2012/
实际上,我们在本页面上找到的所有图表都是使用 HTML 数据表中的数据构建的。
http://www.pewresearch.org/data/