《复仇者联盟4:终局之战》如今正在热映当中。最高高达300多元的票价也真的是“前无古人,后无来者”,出现了“今天你请我看《复联4》,明天我们就是兄弟”的江湖义气。
《复联4》的大火,很大程度和去年的《复仇者联盟3:无限战争》的悲惨的结局有关——宇宙里一半的生命消失了。而这个结果是由很多因素导致的,但是超级英雄的之一的星爵的最后一顿坑爹操作得负很大的责任。影迷们都异常愤怒,于是根据《复联3》中超级英雄的表现,下面这张图诞生了。
十七世纪的大科学家伽利略曾经说过:“我们要测量那些可以测量的东西,至于那些无法测量的东西,我们也要想办法加以测量”。的确,“坑”这种说法太模糊了,下面我们就来“测量”一下星爵“坑”的程度。假如星爵的战斗力为1,并且高一级别的每个英雄的战斗力是低一级别的2倍,那么就获得如下的这张表格。
级别 | 每个英雄的战斗力 | 人数 | 总战斗力 |
---|---|---|---|
超神 | 32 | 1 | 32 |
很厉害 | 16 | 2 | 32 |
有帮上忙 | 8 | 4 | 32 |
尽力了 | 4 | 11 | 44 |
废物 | 2 | 5 | 20 |
一坨屎 | 1 | 1 | 1 |
看了这个表,大家应该能隐隐约约地感受到了星爵的坑队友行为了,但是这还不够。接下来我们要一起可视化一下这些数据,用D3写一个可以在浏览器展示的可以交互的条形图,一起更真切地感受星爵的坑爹行为。效果如下:
现在我们来仔细看看这个条形图(如下图)。星爵的“坑”简直铺面而来,有没有?在队友的战斗力面前,他就像蚂蚁遇见了姚明一样微不足道。另一方面我们也可以很快地发现一个有趣的现象,那就是虽然“尽力了”级别每个英雄的战斗力不高,但是这个级别的战斗力总和却是最高的!难道三个臭皮匠真的可以顶一个诸葛亮?
下面我们就从0到1来实现上面到效果,当然这里必须强调说明上面的数据纯属娱乐,大家不要当真。
本篇文章一共有两个部分:第一部分介绍网页的组成和开发基础(HTML、CSS、JavaScript等),让你们可以开发和调试一些简单的网页;第二部分介绍D3的基本概念和使用,并且一起来完成上述条形图。
如果按耐不住内心激动的同学,可以先看看这个条形图的在线效果或者完整代码(代码量不多)。
同时本人水平有限,接下来内容若有讲解得不恰当的地方,希望大家指出,我感激不尽!
到目前为止,我们已经搭建好了网页的大概框架,下面我们要做的就是用D3完成可视化部分的代码。按照惯例我们先来了解一下D3。
D3(Data-Driven Documents)是一个基于JavaScript的库(就像Processing是基于Java一样),主要目的是通过HMTL,SVG,CSS来给数据生命。D3允许我们将任意数据和基于网页的文档(Documnts)绑定起来(就是任意可以被浏览器渲染的东西,比如HTML、SVG),在数据和文档之间建立联系,这个就是所谓的数据驱动(Data-Driven)。
在学习D3之前,我们先来谈谈我们选择D3进行可视化的理由。
首先我们得说说为什么需要计算机来参与可视化的过程,直接用笔在纸上画为什么不行?。一方面是数据太多,将每一个数据画出来需要花太多的时间。比如下面的a图还勉强可以让人来画,但是面对b图,人就显得无能为力了。另一方面,数据集随着时间一般在变化,用一个基于计算机的工具显然可以节省更多人力。最后还有一个原因就是纸上的图表都是静态的,但是计算机可以给图表添加交互,前面我们也提到了,交互是可视化工具的一个法宝。
其次计算机上可视化的软件可以说是很多,那为什么我们选则网页来探索和展现可视化的结果。那是因为给大家展现的可视化结果是很重要的,网页是一个获得全球性观众的最快途径。但是必须说一句,D3并不支持一些比较老的浏览器。
最后就是为什么在众多相当优秀可视化的开源库选择了D3。诸如百度的Echarts,阿里蚂蚁金服的AntV,可以用很短的代码(甚至不用写代码),在很快的时间生成很漂亮的图片。相比之下D3的代码更多和更难以理解,呈现的结果不确定性也很大。
那是因为D3更加底层,它没有给我们提供一些组件,而是给我们提供了一些数据驱动的函数来帮助我们创造自己的组件。也就说,其他的库就是卖汽车的,我们只要给商家说明我们想要汽车的型号、颜色等,就可以获得一辆崭新的汽车。但是D3是卖零件的,需要按照自己的需求组装这些零件,从而获得汽车。D3更像一个探索可视化原理的库,而不是一个可视化的库。
同时D3的作者是纽约时报的工程师,而D3 项目的代码托管于 GitHub(一个开发管理平台,目前已经是全世界最流行的代码托管平台,云集了来自世界各地的优秀工程师)。在众多开源库中是一个很流行的库,目前star的数量在所有github开源项目里排11位,并且是这11位中唯一一个数据可视化的开源库。
流行就意味着D3有很好的生态环境,有很多案例、教程和社区,我们能很容易找到学习资源、问题解答。
如果你想在短的时间作出优美的可视化,你可以使用Echarts和AntV等,但是如果你希望了解每种可视化方法的原理,那么D3将是一个不错的选择。了解原理有很多好处,比如出了问题我知道问题的根源在哪里,同时可以制造更个性化和新颖的可视化图表。
下面激动人心的可视化部分就要开始了。我们使用的是最新版本的D3(5.9.2)。
第一步我们来确定代码的大概执行流程,在之前代码的基础上我们加入一些代码,最后结果如图:
<html>
<head>
<meta charset="UTF-8">
<title>条形图title>
<style>
/*之前定义的一些样式信息*/
style>
<script src="https://d3js.org/d3.v5.min.js">script>
<script>
function changeColor(){
//之前些的代码....
}
//这个函数是将data对应的条形图现实在网页中
function drawBarChart(data){
//设置svg
//获得svg
//设置比例尺
//绘制条形
//绘制坐标轴
//绘制标题
//添加交互
}
script>
head>
<body>
<h1 class='text' onclick="changeColor()">D3与可视化的第一次邂逅h1>
<p class="text" id="introduction">星爵的“坑”简直铺面而来,有没有?在队友的战斗力面前,他就像蚂蚁遇见了姚明一样微不足道。p>
<svg>svg>
<script>
//这个是我们要可是化的数据
let data = [
{
key: '一坨屎', value: 1},
{
key: '废物', value: 10},
{
key: '尽力了', value: 44},
{
key: '有帮上忙', value: 32},
{
key: '很厉害', value: 32},
{
key: '超神', value: 32},
]
//在控制台以表格形式输出数据
console.table(data)
drawBarChart(data);
script>
body>
html>
浏览器渲染引擎渲染一个网页过程大概如下。
浏览器渲染引擎会从上到下地渲染HMTL文档。标签中会包含一些引用外部文件的代码,从开始运行就会下载这些被引用的外部文件。当遇见
标签的时候会暂停解析,将控制权交给JavaScript引擎。如果
标签引用了外部脚本,就下载该脚本,否则就直接执行,执行完毕后将控制权交给浏览器渲染引擎继续渲染。当
中的代码全部执行完毕、并且整个页面的CSS样式加载完毕后,CSS会重新渲染整个页面的html元素。
我们来看一下浏览器渲染BarChart.html的过程。浏览器会首先遇见第一个标签,下载D3.js。下载完成后,会让JavaScript引擎执行D3.js里面的代码。执行完毕后会遇见第二个
,执行里面的代码,在这里面我们定义了两个函数changColor和drawBarChart。最后当浏览器遇见到第三个
标签的时候,会调用在第二个
标签里定义的drawBarChart函数,来绘制我们的可视化图形。
保存为BarChart.html,打开浏览器效果如图:
按F12打开开发者工具,在Elements里我们可以看见我们目前的网页结构:
在Console里可以看见我们将要可视化的数据:
接下来我们的任务就是来完成drawBarChart里面代码的编写。
可以发现我们在标签后面新增了一个
标签,这就是我们条形图的画布,我们首先来了解一下它。
SVG,指可缩放矢量图形(Scalable Vector Graphics),是用于描述二维矢量图形的一种图形格式。除了 IE8 之前的版本外,绝大部分浏览器都支持 SVG。
在网页中我们可以用标签在网页中嵌入SVG。SVG里面有很多视觉元素可以放在
标签里,包括
、
、< ellipse>
、< line>
、
和
。而我们的条形图就是由这些基本元素构成的。
下面我们来看一个最基本也是我们马上会用的元素
。
<svg>
<rect x="100" y="100" height="100" width="100" fill="red"/>
svg>
svg
标签里面的svg元素必须要设定width和height属性,同时在
标签外面的元素是看不见的。g
元素,g是group的意思,可以把一些列元素变成一个组,这样移动g
元素的时候,所有的元素都会跟着一起移动。 第二步我们来确定我们画布的布局。
我们希望我我们的画布如下图,其中紫色区域是我们的svg容器,黄色区域是我们条形图所在的区域。
所以在函数drawBarChart的“设置svg”区域输入如下代码:
const margin = {
top: 80, right: 180, bottom: 80, left: 180}, //确定内边距
containerDimensions = {
height: 500, width: 960}, //确定整个svg的大小
chartDimensions = {
//根据svg的大小和内边距确定条形图的大小
height: containerDimensions.height - margin.top - margin.bottom,
width: containerDimensions.width - margin.left - margin.right
}
第三步我们就应该选择我们的画布SVG, 并且修改它的属性了。
在 D3 中,我们可以用select和selectAll来选择我们想要的DOM对象。这两个函数输入是CSS选择器,返回对应的DOM对象,返回的结果称为选择集。
d3.select() //选择所有指定元素的第一个
d3.selectAll() //是选择指定元素的全部
//基于之前提到过的css选择器
const p1 = d3.select('p') //根据标签名选择
const text = d3.select('#text') //根据类名选择
const title = d3.select('.title') //根据id选择
在 D3 中,我们可以添加通过append函数在当前选择集后面添加元素,并且返回这个元素。
const p2 = p1.append('p') //p1原属后面添加一个p标签,并返回这个元素
在 D3 中,我们可以在获得DOM对象后,通过attr和style函数来改变它的属性和样式。
p2.attr('class', 'text'); //将p的class设置为text
p2.style('color', 'red'); //设置字体颜色为红色
在D3中,链式调用是一个特色。因为JavaScript不关心空格符号和换行符,所以上面的代码我们可以写成如下的形式:
const p2 = d3.select('p')
.append('p')
.attr('class', 'text')
.style('color', 'red')
接下来我们就在drawBarChart函数的“获得svg”区域加入如下的代码:
const svg = d3.select('svg') //获得网页中svg元素
.attr('width', containerDimensions.width) //设置svg的高
.attr('height', containerDimensions.height) //设置svg的宽
.style('background', 'black') //设置背景颜色
const group = svg.append('g') //改变后面绘制的坐标原点
.attr('transform', `translate(${
margin.left}, ${
margin.top})`);
保存BarChart.html,用浏览器执执行,在开发者工具里我们可以看见当前HTML的网页结构如下所示:
一个级别的英雄的对应一个条,每一个条为一个rect元素。第四步,我们需要将每个级别英雄的数据和一个rect元素绑定起来,这样不仅能在网页中增加对应数量的rect元素,还可以根据数据来确定每一个rect的位置和大小。
D3 中是通过datum和data来将数据绑定到DOM上,然后用join函数来返回当前绑定好的DOM:
datum() //绑定一个数据到选择集上
data() //绑定一个数组到选择集上,数组的各项值分别与选择集的各元素绑定,相对而言,data() 比较常用。
join() //返回所有绑定绑定好的DOM
接下来在BarChart.html的drawBarChart中的“绘制条形图”加入以下的代码:
//绘制bar
const bars = group.append('g')
.selectAll('rect') //选择关联的svg元素
.data(data) //需要关联的数据
.join('rect') //获得绑定了数据的rect元素
就这样我们将每个级别英雄的战斗力总和和一个rect元素绑定起来了。那么如何确定已经绑定起来了呢?
首先保存BarChart.html,并且用浏览器打开,切换到Elements界面。可以发现已经增加了6个rect元素,现在的网页结构如下图:
接着我们切换到Console界面,在控制台里输入console.log(d3.selectAll('rect'))
,可以看见选择集的NodeList由6个rect对象组成,如下图:
而每一个rect对像都有一个__data__
属性,就是我们的绑定的数据,如下图:
虽然目前我们在开发者工具界面可以看见该网页已经多了6个rect元素,但是网页上还看不见任何的矩形。这是因为我们还没设置每一个rect元素的位置和大小,接下来第五步我们需要确定每一个rect元素的大小和位置。
我们可以把每个rect的宽度设为定值,比100px。我们一共有6个rect,那么就一共是600px。但是如果我们数据有100个呢,那岂不是6000px?所以我们把每个rect的宽度设置成定值是不合理的。高度也是同样的道理。我们希望不管的数据的有多少,条形图都能充满某个固定大小的区域,那么这个时候就需要比例尺了。
比例尺是D3中很重要的一个概念,D3的作者Mike Bostock对比例尺定义如下:
“Scales are functions that map from an input domain to an output range.”
比例尺是就是一个函数。在定义它的时候我们需要指定一些参数,比如定义域(domain)和值域(range)。当我们调用它的时候,我们传入一个值,该函数会根据我们定义的参数返回一个缩放后的值。
因为数据类型有多种(连续的,不连续的),所以D3 提供了多种比例尺。下面介绍最常用也是我们将要用到的两种。
第一种是线性比比例尺,类似于processing里面的map函数。能将一个连续的区间的数,映射到另一连续的区间。工作原理如图:
定义方法如下:
const y = d3.scaleLinear()
.domain([1, 5])
.range([0, 100]);
y(1) // 0
y(4) // 75
y(5) // 100
第二种是序数比比例尺,与线性比例不同的是,它的定义域是离散的,映射到另一连续的区间。工作原理如图:
定义方法如下:
const x = d3.scaleBand()
.domain([1, 2, 3, 4])
.range([0, 100])
x(1); // 0
x(2); // 25
x(3); // 50
x(4); // 75
因为数据的种类(超神,很厉害……)是离散的,而它们对应条的位置是连续的([0, 700],所以我们需要一个序数比例尺。同时数据的值([1, 44])是连续的,对应条的高度也是连续的([0, 340]),所以我们需要一个线性比例尺。在drawBarChart的“设置比例尺”区域加入如下的代码:
const y = d3.scaleLinear()
.domain([0, d3.max(data, d => d.value)])
.nice()//扩展定义域的两段使其都为整数,比如[1.0021, 2.999] => [1, 3]
.range([chartDimensions.height, 0])
const x = d3.scaleBand()
.domain(data.map(d => d.key))
.range([0, chartDimensions.width])
.padding(0.1) //设置条之间的间距,通过改变每一个条的宽度来实现
这里得加一点说明关于js中map和D3中的d3.max函数的使用:
/*d3.max函数返回data中的最大值,第二个参数是指定用什么来比较*/
const a = [{
key:'a', value: 1},
{
key:'b', value: 2},
{
key:'c', value: 3}]
const maxA = d3.max(a, d => d.value)
maxA // 3
/*js中数组的map函数会对原数组的每一个元素执行传入的函数,然后返回一个新的数组*/
const a = [1, 2, 3]
const b = a.map(d => d * 2)
b // [2, 4, 6]
接下来我们就可以用定义好的比例尺来设置每个rect的位置和大小了,在drawBarChart的“绘制条形”区域加入如下的代码:
bars.attr('x', d => x(d.key)) //设置rect的x坐标
.attr('y', d => y(d.value))//设置rect的y坐标
.attr('height', d => y(0) - y(d.value))//设置rect的width
.attr('width', x.bandwidth())//设置rect的width, 已经被比例尺计算好了,通过x.bandwidth()获得
.attr('fill', 'steelblue'); //设置颜色
在这里要说明一下,上面的attr函数传入的第一个参数是我们想给选择集设置的属性名称,第二个参数是一个箭头函数,该箭头函数的参数d就是绑定的数据,返回的值就是需要设定的值。同时,选择集上的每一DOM都会执行这个函数。所以执行完成后,网页显示和结构如下图:
第七步,我们需要绘制坐标轴。一个坐标轴是有一些列svg元素(line,text,path)构成的。至于如何构成的,我们之后会看见。不过,D3有现成的坐标轴生成器。坐标生成器是一个函数,该函数会在输入的svg元素后面添加一系列元素构成坐标轴:
const axisGenerator = d3.axisBottom(x) //x是一个比例尺,用于控制坐标轴的范围,刻度等属性
const g = d3.select('g')
axisGenerator(g) //在g元素后面添加一个坐标轴
D3的call函数传入一个函数,该函数会作用到调用call函数的选择集上。在call函数的帮助下,上面的代码可以写成如下更简洁的形式:
g.call(axisGenerator) //与上面等价
在drawBarChart的“绘制坐标轴”区域加入如下的代码:
//绘制x轴
group.append('g')
.attr('class', 'axis') //把容纳坐标轴的g元素设置class为axis
.attr('transform', `translate(0, ${
chartDimensions.height})`)//平移到指定位置
.call(d3.axisBottom(x))
//绘制y轴
group.append('g')
.attr('class', 'axis') //把容纳坐标轴的g元素设置class为axis
.call(d3.axisLeft(y))
保存barChart.html,用浏览器打开。我们发现还是看不见坐标轴,那是因为D3的坐标轴默认是黑色的 。我们进入开发者页面,选择Elements,可以发现一个坐标轴的构成如下图:
于是我们就用css对这些元素的颜色进行更改,在元素里面加入如下代码:
.axis line, path{
stroke: white;
}
.axis text{
fill: white;
}
第八步我们来添加标题,这一步很简单。在“绘制标题”区域添加如下代码:
//绘制title
group.append('text') //添加一个text元素
.attr('x', x(data[0].key)) //将和第一个条形对齐
.attr('font-size', 14) //设置文本字体的粗细
.attr('font-weight', 'bold') //设置文本字体的粗细
.attr('fill', 'white') //设置文本的颜色
.text('复联3战力分布') //设定文本的内容
到目前为止我们的完整的条形图已经实现了,但是目前还不能交互。我们希望当鼠标移动到任意一个条上的时候,这个条的颜色会从蓝色变成褐色。当我们的鼠标移出任意一个条的时候,这个条的颜色会变回蓝色。
这个时候就是需要给rect的元素们绑定事件监听器。
D3绑定的事件监听器的方法很简单,使用on函数。这个函数有两个参数,第一个参数我们想绑定的事件,第二个参数是响应该事件的函数。同样on函数也会对选择集中的每一个元素执行。
在drawBarChart的的“添加交互”区域输入如下的代码:
bars.on('mouseover', d => {
//mousevoer是鼠标移到的元素上触发的事件
d3.selectAll('rect')
.filter(data => data.key == d.key) //筛选出被选中的rect元素并且改变颜色
.attr('fill', 'brown')
})
.on('mouseleave', d => {
//mouseleav是鼠标离开的元素上触发的事件
d3.selectAll('rect') //离开时将所有rect元素的颜色设置为蓝色
.attr('fill', 'steelblue')
})
D3的filter函数用来对选择集进行筛选。它的参数是一个函数,这个函数会对选择集里的没一个元素执行。如果执行结果是true那么保存这个元素,否者删除这个元素。
保存BarChart.html,用浏览器打开。效果如下:
目前我们的写代码部分就完成了,接下来就可以导出可视化的结果了。这里介绍三种方式。
第一种是导出成位图png。直接用截图软件截图即可,可以使用QQ自带的截图工具。
第二种是导出成PDF。方法是使用Chrome浏览器,点击顶部菜单栏的文件,选择打印即可。
最后一种是导出成SVG。在Chrome的开发者工具Elements中,选中你想要导出的svg元素,选择复制。接着新建一个扩展名为svg的文件,比如BarChart.svg。用文本编辑器打开,将内容复制进去,保存。然后就可以用Illustrator等处理矢量图的软件打开了。
我们的复联3战力分布图已经制作完成了,你只要把data按照我们给定的形式替换成你自己的数据,这个条形图就是你自己的条形图了。
我们上面介绍的只是最基本的条形图,条形图有很多兄弟姐妹。大家请看下面的条形图大家庭,可以想想可以如何用D3来实现它们,它们又分别在什么时候使用。
关于D3想了解更多的同学可以去D3的官网。
相信大家看了这么多,已经很累啦,接下来给大家看一个有趣的视频。大家可以分析一下视频中的可视化为什么好?你获得了哪些有趣的事实。
我们大概了解了一下数据可视化,也亲手实现了一个简单的条形图,最重要的知道了星爵在《复仇者联盟3》中有多么的坑。
不过话又说回来,“三十年河东,三十年河西,莫欺少年穷”,《复联4》中的星爵也许就不坑了呢?所以,感兴趣的同学可以走进电影院看看这次超级英雄们是如何“超越无限,逆转未来的”,毕竟这可是漫威10年的收官之作!