D3.js 是什么?
D3.js 是一个可以基于数据来操作文档的 JavaScript 库。
“D3 可以帮助你使用 HTML, CSS, SVG 以及 Canvas 来展示数据。D3 遵循现有的 Web 标准,可以不需要其他任何框架独立运行在现代浏览器中,它结合强大的可视化组件来驱动 DOM 操作。” - d3js.org
首先考虑为什么要用 D3.js 创建图表?为什么不只显示图片呢?
图表是基于第三方资源的信息,在渲染时需要动态可视化。此外,SVG 是一个非常强大的工具,非常适合这个应用场景。
让我们先看看 SVG 有什么好。
SVG 的优点
SVG 代表可缩放矢量图形,从技术上讲,这是一种基于 XML 的标记语言。
它通常用于绘制矢量图形,比如线条和形状或修改现有图像。
优点:
- 支持所有主流浏览器;
- 有 DOM 接口,不需要第三方库;
- 可伸缩,可保持高分辨率;
- 和其他图像格式相比,体积更小。
缺点:
- 只能显示二维图像;
- 学习曲线长;
- 对于计算密集型操作,渲染可能需要很长时间。
SVG 尽管有缺点,但它仍是显示图标,logo,插图或者此文提及的图表的优良工具。
开始使用 D3.js
我选择以柱状图作为开始,因为它代表了一个低复杂度的视觉元素,同时它还能教会 D3.js 本身的基本应用。没骗你,D3 提供了一套很棒的可视化数据的工具。看看它的 github page 页面,欣赏一些非常好的用例!
柱状图可以是水平或垂直的,取决于它的方向。我们从垂直的柱状图开始。
画起来!
SVG 的坐标系从左上角开始(0;0)。正 x 轴向右,正 y 轴向下。因此,在计算元素的 y 坐标时,必须考虑 SVG 的高度。
背景知识差不多了,让我们撸代码吧!
我想创建一个宽1000像素、高600像素的图表。
<body>
<svg />
</body>
<script>
const margin = 60;
const width = 1000 - 2 * margin;
const height = 600 - 2 * margin;
const svg = d3.select('svg');
</script>
以上代码片段中,我用 d3 select
选择了 HTML 创建的 <svg>
元素。此选择方法接收各种类型的选择器字符串
并返回第一个匹配元素。如果想获取所有匹配元素,使用 selectAll
。
我还定义了一个边距值,它给图表提供了一点间距。间距也可以应用到 <g>
元素上,通过 translate
移动期望的值。从现在起,我将在这个分组中绘制,确保与页面其它内容保持合理的间距。
const chart = svg.append('g')
.attr('transform', `translate(${margin}, ${margin})`);
往元素添加属性就像调用 attr
方法一样简单。方法的第一个参数接收用于所选 DOM 元素的属性。第二个参数是属性值或返回其值的回调函数。以上代码简单将图表的原点移到 SVG 的 (60;60) 位置。
D3.js 支持的数据源格式
要开始绘图,我需要定义使用的数据源。本教程中,我使用了一个简单的 JavaScript 数组,该数组保存了语言名称及其所占百分比率的对象,但是这里着重提到一点,D3.js 支持多种数据格式。
该库具备从 XMLHttpRequest,.csv 文件,文本文件等数据源加载数据的内置功能。每一种数据源都可能包含 D3.js 可用的数据,最重要的是把它们构建成数组。注意,从版本5.0
开始,D3 库使用 Promise 取代回调来加载数据,这是一次不向后兼容的更改。
缩放,坐标轴
让我们继续讨论图表的坐标轴。为了画 y 轴,我需要设定最小和最大值,分别设置为0和100。
我必须将图表的高度在这两个值之间均分。为此,我创建了一个缩放函数。
const yScale = d3.scaleLinear()
.range([height, 0])
.domain([0, 100]);
线性缩放是最常见的缩放类型。它将连续输入范围转换为连续输出范围。请注意 range
和 domain
方法。第一个 range
方法取的长度应该在 domain
的边界值之间。
记住,SVG 坐标系从左上角开始,这就是为什么 range
将高度作为第一个参数而不是零。
在左侧创建一个坐标轴跟添加另一个分组一样简单,调用 d3 的 axisLeft
方法,并把缩放函数作为参数。
chart.append('g')
.call(d3.axisLeft(yScale));
现在,继续添加 x 轴。
const xScale = d3.scaleBand()
.range([0, width])
.domain(sample.map((s) => s.language))
.padding(0.2)
chart.append('g')
.attr('transform', `translate(0, ${height})`)
.call(d3.axisBottom(xScale));
请注意,我使用 scaleBand 方法创建 x 轴,它将 x 轴 分成多段,并且使用余下的间隙计算柱状图的坐标和宽度。
D3.js 还能处理许多其他日期类型。scaleTime 与 scaleLinear 非常相似,只是这里的 domain 是一个日期数组。
使用 D3.js 绘制柱状图
想想我们需要什么样的输入来画柱条。它们各自代表一个用简单形状,特别是矩形来展示的值。下一段代码中,我把它们添加到已创建的分组元素中了。
chart.selectAll()
.data(goals)
.enter()
.append('rect')
.attr('x', (s) => xScale(s.language))
.attr('y', (s) => yScale(s.value))
.attr('height', (s) => height - yScale(s.value))
.attr('width', xScale.bandwidth())
首先,我 selectAll
图表上的所有元素,返回结果为空。然后,data
函数根据数组长度通知 DOM 应该更新多少元素。如果数据个数多于 DOM 个数时,则 enter
会标识出缺少的元素。enter
会返回需要添加的元素。
通常,后面紧跟 append
方法会把元素添加到 DOM 中。
基本上,我用 D3.js 给数组每一项都追加了一个矩形。
当前只在彼此顶部添加了没有宽高的矩形。这两个属性必须通过之前的缩放函数计算所得。
我调用 attr
方法添加了矩形坐标。第二个参数可以是回调,它返回3个参数:当前绑定的数据,索引和所有数据数组。
.attr(’x’, (actual, index, array) =>
xScale(actual.value))
缩放函数返回给定范围值的坐标。计算坐标就是小菜一碟,诀窍是利用柱子的高度。必须从图表的高度减去计算出的 y 坐标,才能得到正确的列值。
定义矩形的宽度也会用到缩放函数。scaleBand
有一个 bandwidth
函数,它基于设置的间距返回一个元素的计算宽度。
干得不错,但没那么花哨,对吧?
为了防止观众视觉疲劳,让我们添加一些信息改善下视觉效果!
制作柱状图的技巧
有一些基本规则值得一提。
- 避免使用 3D 效果;
- 直观地排序数据点 - 按字母顺序或按数字排序;
- 柱条之间保持一定距离;
- y 轴从 0 开始,而不是从最小值开始;
- 使用统一的颜色;
- 添加轴标签、标题、导引线。
D3.js 网格系统
我想在背景中添加栅格线突出那些值。
垂直和水平的线都可以添加,我的建议是只添加一种。过多的线会分散注意力。以下代码片段演示了如何添加水平和垂直的栅格。
chart.append('g')
.attr('class', 'grid')
.attr('transform', `translate(0, ${height})`)
.call(d3.axisBottom()
.scale(xScale)
.tickSize(-height, 0, 0)
.tickFormat(''))
chart.append('g')
.attr('class', 'grid')
.call(d3.axisLeft()
.scale(yScale)
.tickSize(-width, 0, 0)
.tickFormat(''))
此例中,我更喜欢垂直栅格线,因为它可以引导视线,保持整体画面简介明快。
D3.js 中的标签
我还想添加一些文字指导,从而使图表更加全面。让我们给图表命个名,并为坐标轴添加标签吧。
文本是 SVG 元素,同样可以添加到 SVG 或者分组中。它们可以使用 x 和 y 坐标定位,文本对齐是通过 text-anchor
属性实现的。
添加标签文字,只需调用文本元素上的 text
方法。
svg.append('text')
.attr('x', -(height / 2) - margin)
.attr('y', margin / 2.4)
.attr('transform', 'rotate(-90)')
.attr('text-anchor', 'middle')
.text('Love meter (%)')
svg.append('text')
.attr('x', width / 2 + margin)
.attr('y', 40)
.attr('text-anchor', 'middle')
.text('Most loved programming languages in 2018')
与 D3.js 交互
我们的图表内容已然丰富,但是仍然可以添加些互动效果。
以下的代码演示了如何给 SVG 元素添加事件监听。
svgElement
.on('mouseenter', function (actual, i) {
d3.select(this).attr(‘opacity’, 0.5)
})
.on('mouseleave’, function (actual, i) {
d3.select(this).attr(‘opacity’, 1)
})
注意,我用了函数表达式而不是箭头函数,因为我通过 this 关键字访问元素。
当鼠标滑过选中的 SVG 元素时,它的透明度变为原始值的一半,鼠标离开元素时透明度恢复原始值。
你也可以通过 d3.mouse
获取鼠标坐标。它返回一个具有 x 和 y 坐标的数组。在光标所在位置显示提示,就可以通过这个实现。
创建令人瞠目结舌的图表并没那么简单。
可能需要图形设计师,UX 研究员和其他牛人的智慧。以下例子展示了几个提升图表效果的可能性!
我们的图表显示了非常相似的值,所以为了突出条形值之间的差异,我添加了一个 mouseenter
事件。每当用户悬停在特定的列时,该栏的顶部就会画一条水平线。此外,我还计算了与其他柱条的差异,并显示在了相应的柱条上。
很整齐吧?我还在此例中增加了透明度,加大了柱条的宽度。
.on(‘mouseenter’, function (s, i) {
d3.select(this)
.transition()
.duration(300)
.attr('opacity', 0.6)
.attr('x', (a) => xScale(a.language) - 5)
.attr('width', xScale.bandwidth() + 10)
chart.append('line')
.attr('x1', 0)
.attr('y1', y)
.attr('x2', width)
.attr('y2', y)
.attr('stroke', 'red')
// 部分实现
})
transition
方法表明我想把 DOM 改变绘制成动画。它的时间间隔是用 duration
函数设置的,该函数以毫秒作为参数。上面的过渡会淡化带状颜色,并加宽条形的宽度。
要画一条 SVG 线,我需要起点和终点。这可以通过 x1
,y1
和 x2
,y2
坐标来设置。直到我用 stroke
属性设置线条的颜色,线条才可见。
这里只展示了 mouseenter
事件这部分,切记,必须在 mouseout
事件上恢复或删除更改。本文末尾提供了完整的源代码。
让我们给图表添加一些样式吧!
回顾下我们目前为止完成了那些功能,以及如何通过样式装扮图表。可以通过先前用过的 attr
方法给 SVG 元素添加 class 属性。
我们的图表功能丰富,而不是死板的静态图片,鼠标悬停时可以显示各个柱条的差值。标题交代表格的背景,标签帮助识别坐标轴的测量单位。我还在右下角添加了新的标签,注明数据来源。
剩下的事情就差颜色和字体了!
深色背景的图表使亮色柱条看起来很酷。我还使用了 open Sans
字体,并给不同的标签设置不同的大小和粗细。
注意到那条虚线了吗?它是通过 stroke-width
和 stroke-dasharray
属性实现的。使用 stroke-dasharray
,可以定义虚线的图案和间距,从而改变形状的轮廓。
line#limit {
stroke: #FED966;
stroke-width: 3;
stroke-dasharray: 3 6;
}
.grid path {
stroke-width: 3;
}
.grid .tick line {
stroke: #9FAAAE;
stroke-opacity: 0.2;
}
网格线比较讨巧,我给分组中的路径元素使用了 stroke-width: 0
,为了隐藏表格的框架,我还通过设置线条的透明度降低它们的可见性。
所有其它有关字体大小和颜色的 CSS 可以参照源码。
收尾我们的 D3.js 柱状图教程
D3.js 是一个令人惊叹的 DOM 操作库。它的内部埋藏了无数的宝藏等待你去探索(确切的说,不是埋藏,文档也很齐全)。此文仅仅使用了它的工具集的冰山一角,就创建了一个不同凡响的柱状图。