声明:本教程针对D3.js v3版本进行讲解。
D3 是什么
D3 的全称是(Data-Driven Documents),顾名思义可以知道是一个被数据驱动的文档。听名字有点抽象,说简单一点,其实就是一个 JavaScript 的函数库,使用它主要是用来做数据可视化的。
D3 提供了各种简单易用的函数,大大简化了 JavaScript 操作数据的难度。由于它本质上是 JavaScript ,所以用 JavaScript 也是可以实现所有功能的,但它能大大减小你的工作量,尤其是在数据可视化方面,D3 已经将生成可视化的复杂步骤精简到了几个简单的函数,你只需要输入几个简单的数据,就能够转换为各种绚丽的图形。有过 JavaScript 基础的朋友一定很容易理解它。
为什么使用D3.js
我们知道现在有很多开源的图表库Echarts、HighCharts、G2.js等等。那么D3跟这些图表库相比有什么优势和劣势?
- D3 基于svg,因此对图像进行放大不会失真。(D3.js v4版本已经支持canvas了,本系列教程只讲v3版本)
- D3 相对来说较底层,对初学者来说不太方便,但是一旦掌握了,就比其他工具更加得心应手。下图展示了 D3 与其它可视化工具的区别:
可以看到,D3 的步骤相对来说较多。坏处是对初学者不方便、也不好理解。好处是能够制作出更加精密的图形。因此,我们可以据此定义什么时候选择 D3 比较好:
选择 D3:如果希望开发脑海中任意想象到的图表。
选择 Highcharts、Echarts 等:如果希望开发几种固定种类的、十分大众化的图表。
看起来,D3 似乎是为艺术家或发烧友准备的。有那么点意思,但请初学者也不要放弃。
SVG 是什么
SVG,指可缩放矢量图形(Scalable Vector Graphics),是用于描述二维矢量图形的一种图形格式,是由万维网联盟制定的开放标准。SVG 使用 XML 格式来定义图形,除了 IE8 之前的版本外,绝大部分浏览器都支持 SVG,可将 SVG 文本直接嵌入 HTML 中显示。
SVG 有如下特点:
- SVG 绘制的是矢量图,因此对图像进行放大不会失真。
- 基于 XML,可以为每个元素添加 JavaScript 事件处理器。
- 每个图形均视为对象,更改对象的属性,图形也会改变。
- 不适合游戏应用。
学习 D3 需要什么预备知识
想要通过 D3 来开启数据可视化之旅的朋友,需要什么预备知识呢?
- HTML:超文本标记语言,用于设定网页的内容
- CSS:层叠样式表,用于设定网页的样式
- JavaScript:一种直译式脚本语言,用于设定网页的行为
- DOM:文档对象模型,用于修改文档的内容和结构
- SVG:可缩放矢量图形,用于绘制可视化的图形
有人会问:多久能学会D3.js?
我可以告诉各位:只要跟着我这个D3入门系列教程学习、练习下来,你就学会了用D3.js编写常用的图表。学得快的人,两三天就掌握了,学的慢的人,可能需要一周时间吧。
下面开始学习D3.js具体技术细节。。。
【Lesson1】选择元素和绑定数据
1. 如何选择元素
在 D3 中,用于选择元素的函数有两个:
- d3.select():是选择所有指定元素的第一个
- d3.selectAll():是选择指定元素的全部
这两个函数返回的结果称为选择集。
这里涉及一个概念:选择集
使用 d3.select() 或 d3.selectAll() 选择元素后返回的对象,就是选择集。
例如,选择集的常见用法如下:
var body = d3.select("body"); //选择文档中的body元素
var p1 = body.select("p"); //选择body中的第一个p元素
var p = body.selectAll("p"); //选择body中的所有p元素
var svg = body.select("svg"); //选择body中的svg元素
var rects = svg.selectAll("rect"); //选择svg中所有的svg元素
复制代码
选择集和绑定数据通常是一起使用的。
2. 如何绑定数据
D3 有一个很独特的功能:能将数据绑定到 DOM 上,也就是绑定到文档上。这么说可能不好理解,例如网页中有段落元素 和一个整数 5,于是可以将整数 5 与 绑定到一起。绑定之后,当需要依靠这个数据才操作元素的时候,会很方便。
D3 中是通过以下两个函数来绑定数据的:
- datum():绑定一个数据到选择集上
- data():绑定一个数组到选择集上,数组的各项值分别与选择集的各元素绑定
相对而言,data() 比较常用。
假设现在有三个段落元素如下:
<p>Apple</p>
<p>Pear</p>
<p>Banana</p>
复制代码
接下来分别使用 datum() 和 data(),将数据绑定到上面三个段落元素上。
2.1 datum()
假设有一字符串 China,要将此字符串分别与三个段落元素绑定,代码如下:
var str = "China";
var body = d3.select("body");
var p = body.selectAll("p");
p.datum(str);
p.text(function(d, i){
return "第 "+ i + " 个元素绑定的数据是 " + d;
});
复制代码
绑定数据后,使用此数据来修改三个段落元素的内容,其结果如下:
第 0 个元素绑定的数据是 China
第 1 个元素绑定的数据是 China
第 2 个元素绑定的数据是 China
复制代码
在上面的代码中,用到了一个无名函数 function(d, i)。 当选择集需要使用被绑定的数据时,常需要这么使用。其包含两个参数,其中:
- d 代表数据,也就是与某元素绑定的数据。
- i 代表索引,代表数据的索引号,从 0 开始。
例如,上述例子中:第 0 个元素 apple 绑定的数据是 China。
2.2 data()
有一个数组,接下来要分别将数组的各元素绑定到三个段落元素上。
var dataset = ["I like dogs","I like cats","I like snakes"];
复制代码
绑定之后,其对应关系的要求为:
- Apple 与 I like dogs 绑定
- Pear 与 I like cats 绑定
- Banana 与 I like snakes 绑定
调用 data() 绑定数据,并替换三个段落元素的字符串为被绑定的字符串,代码如下:
var body = d3.select("body");
var p = body.selectAll("p");
p.data(dataset)
.text(function(d, i){
return d;
});
复制代码
这段代码也用到了一个匿名函数 function(d, i),其对应的情况如下:
- 当 i == 0 时, d 为 I like dogs。
- 当 i == 1 时, d 为 I like cats。
- 当 i == 2 时, d 为 I like snakes。
此时,三个段落元素与数组 dataset 的三个字符串是一一对应的,因此,在函数 function(d, i) 直接 return d 即可。
结果自然是三个段落的文字分别变成了数组的三个字符串。
I like dogs
I like cats
I like snakes
复制代码
有人会发现,D3 能够连续不断地调用函数,形如:
d3.select().selectAll().text()
这称为链式语法,和 JQuery 的语法很像,常用 JQuery 的朋友一定会感到很亲切。
【Lesson2】选择、插入、删除元素
1. 选择元素
前面已经讲解了 select 和 selectAll,以及选择集的概念。本节具体讲解这两个函数的用法。
假设在 body 中有三个段落元素:
<p>Apple</p>
<p>Pear</p>
<p>Banana</p>
复制代码
现在,要分别完成以下四种选择元素的任务。
1.1 选择第一个 p 元素
使用 select ,参数传入 p 即可,如此返回的是第一个 p 元素。
var p1 = body.select("p");
p1.style("color","red");
复制代码
结果如下,被选择的元素标记为红色。
1.2 选择三个 p 元素
使用 selectAll 选择 body 中所有的 p 元素。
var p = body.selectAll("p");
p.style("color","red");
复制代码
结果如下:
1.3 选择第二个 p 元素
有不少方法,一种比较简单的是给第二个元素添加一个 id 号。
<p id="myid">Pear</p>
复制代码
然后,使用 select 选择元素,注意参数中 id 名称前要加 # 号。
var p2 = body.select("#myid");
p2.style("color","red");
复制代码
结果如下:
1.4 选择后两个 p 元素
给后两个元素添加 class,
<p class="myclass">Pear</p>
<p class="myclass">Banana</p>
复制代码
由于需要选择多个元素,要用 selectAll。注意参数,class 名称前要加一个点。
var p = body.selectAll(".myclass");
p.style("color","red");
复制代码
结果如下:
关于 select 和 selectAll 的参数,其实是符合 CSS 选择器的条件的,即用“井号(#)”表示 id,用“点(.)”表示 class。
此外,对于已经绑定了数据的选择集,还有一种选择元素的方法,那就是灵活运用 function(d, i)。我们已经知道参数 i 是代表索引号的,于是便可以用条件判定语句来指定执行的元素。
2. 插入元素
插入元素涉及的函数有两个:
- append():在选择集末尾插入元素
- insert():在选择集前面插入元素
假设有三个段落元素,与上文相同。
2.1 append()
body.append("p")
.text("append p element");
复制代码
在 body 的末尾添加一个 p 元素,结果为:
Apple
Pear
Banana
append p element
复制代码
2.2 insert()
在 body 中 id 为 myid 的元素前添加一个段落元素。
body.insert("p","#myid")
.text("insert p element");
复制代码
已经指定了 Pear 段落的 id 为 myid,因此结果如下。
Apple
insert p element
Pear
Banana
复制代码
3. 删除元素
删除一个元素时,对于选择的元素,使用 remove 即可,例如:
var p = body.select("#myid");
p.remove();
复制代码
如此即可删除指定 id 的段落元素。
【Lesson3】做一个简单的图表
柱形图是一种最简单的可视化图表,主要有矩形、文字标签、坐标轴组成。本节为简单起见,只绘制矩形的部分,用以讲解如何使用 D3 在 SVG 画布中绘图。
1. 添加画布
D3 虽然没有明文规定一定要在 SVG 中绘图,但是 D3 提供了众多的 SVG 图形的生成器,它们都是只支持 SVG 的。因此,建议使用 SVG 画布。 使用 D3 在 body 元素中添加 svg 的代码如下:
var width = 300; //画布的宽度
var height = 300; //画布的高度
var svg = d3.select("body") //选择文档中的body元素
.append("svg") //添加一个svg元素
.attr("width", width) //设定宽度
.attr("height", height); //设定高度
复制代码
有了画布,接下来就可以在画布上作图了。
2. 绘制矩形
本文绘制一个横向的柱形图。只绘制矩形,不绘制文字和坐标轴。
在 SVG 中,矩形的元素标签是 rect。例如:
<svg>
<rect></rect>
<rect></rect>
</svg>
复制代码
上面的 rect 里没有矩形的属性。矩形的属性,常用的有四个:
- x:矩形左上角的 x 坐标
- y:矩形左上角的 y 坐标
- width:矩形的宽度
- height:矩形的高度
要注意,在 SVG 中,x 轴的正方向是水平向右,y 轴的正方向是垂直向下的。
现在给出一组数据,要对此进行可视化。数据如下:
var dataset = [ 250 , 210 , 170 , 130 , 90 ]; //数据(表示矩形的宽度)
复制代码
为简单起见,我们直接用数值的大小来表示矩形的像素宽度(后面会说到这不是一种好方法)。然后,添加以下代码。
var rectHeight = 25; //每个矩形所占的像素高度(包括空白)
svg.selectAll("rect")
.data(dataset)
.enter()
.append("rect")
.attr("x",20)
.attr("y",function(d,i){
return i * rectHeight;
})
.attr("width",function(d){
return d;
})
.attr("height",rectHeight-2)
.attr("fill","steelblue");
复制代码
这段代码添加了与 dataset 数组的长度相同数量的矩形,所使用的语句是:
svg.selectAll("rect") //选择svg内所有的矩形
.data(dataset) //绑定数组
.enter() //指定选择集的enter部分
.append("rect") //添加足够数量的矩形元素
复制代码
这段代码以后会常常出现在 D3 的代码中,请务必牢记。目前不深入讨论它的作用机制是怎样的,只需要读者牢记,当:
有数据,而没有足够图形元素的时候,使用此方法可以添加足够的元素。
添加了元素之后,就需要分别给各元素的属性赋值。在这里用到了 function(d, i),前面已经讲过,d 代表与当前元素绑定的数据,i 代表索引号。给属性赋值的时候,是需要用到被绑定的数据,以及索引号的。
最后一行的:
.attr("fill","steelblue");
复制代码
是给矩形元素设置颜色。一般来说,最好写成外置 CSS 的形式,方便归类和修改。这里为了便于初学者理解,将样式直接写到元素里。
结果图如本文开头的图片所示。
代码示例地址:github.com/legend-li/D…
【Lesson4】比例尺、坐标轴的使用
比例尺
比例尺是 D3 中很重要的一个概念,上一章里曾经提到过直接用数值的大小来代表像素不是一种好方法,本章正是要解决此问题。
1. 为什么需要比例尺
上一章制作了一个柱形图,当时有一个数组:
var dataset = [ 250 , 210 , 170 , 130 , 90 ];
复制代码
绘图时,直接使用 250 给矩形的宽度赋值,即矩形的宽度就是 250 个像素。
此方式非常具有局限性,如果数值过大或过小,例如:
var dataset_1 = [ 2.5 , 2.1 , 1.7 , 1.3 , 0.9 ];
var dataset_2 = [ 2500, 2100, 1700, 1300, 900 ];
复制代码
对以上两个数组,绝不可能用 2.5 个像素来代表矩形的宽度,那样根本看不见;也不可能用 2500 个像素来代表矩形的宽度,因为画布没有那么长。
于是,我们需要一种计算关系,能够:将某一区域的值映射到另一区域,其大小关系不变。
这就是比例尺(Scale)。
2. 有哪些比例尺
比例尺,很像数学中的函数。例如,对于一个一元二次函数,有 x 和 y 两个未知数,当 x 的值确定时,y 的值也就确定了。
在数学中,x 的范围被称为定义域,y 的范围被称为值域。
D3 中的比例尺,也有定义域和值域,分别被称为 domain 和 range。开发者需要指定 domain 和 range 的范围,如此即可得到一个计算关系。
D3 提供了多种比例尺,下面介绍最常用的两种。
2.1. 线性比例尺
线性比例尺,能将一个连续的区间,映射到另一区间。要解决柱形图宽度的问题,就需要线性比例尺。
假设有以下数组:
var dataset = [1.2, 2.3, 0.9, 1.5, 3.3];
复制代码
现有要求如下:
将 dataset 中最小的值,映射成 0;将最大的值,映射成 300。
代码如下:
var min = d3.min(dataset);
var max = d3.max(dataset);
var linear = d3.scale.linear()
.domain([min, max])
.range([0, 300]);
linear(0.9); //返回 0
linear(2.3); //返回 175
linear(3.3); //返回 300
复制代码
其中,d3.scale.linear() 返回一个线性比例尺。domain() 和 range() 分别设定比例尺的定义域和值域。在这里还用到了两个函数,它们经常与比例尺一起出现:
- d3.max()
- d3.min()
这两个函数能够求数组的最大值和最小值,是 D3 提供的。按照以上代码,
比例尺的定义域 domain 为:[0.9, 3.3]
比例尺的值域 range 为:[0, 300]
因此,当输入 0.9 时,返回 0;当输入 3.3 时,返回 300。当输入 2.3 时呢?返回 175,这是按照线性函数的规则计算的。
有一点请大家记住:
d3.scale.linear() 的返回值,是可以当做函数来使用的。因此,才有这样的用法:linear(0.9)。
2.2. 序数比例尺
有时候,定义域和值域不一定是连续的。例如,有两个数组:
var index = [0, 1, 2, 3, 4];
var color = ["red", "blue", "green", "yellow", "black"];
复制代码
我们希望 0 对应颜色 red,1 对应 blue,依次类推。
但是,这些值都是离散的,线性比例尺不适合,需要用到序数比例尺。
var ordinal = d3.scale.ordinal()
.domain(index)
.range(color);
ordinal(0); //返回 red
ordinal(2); //返回 green
ordinal(4); //返回 black
复制代码
用法与线性比例尺是类似的。
3. 给柱形图添加比例尺
在上一节的基础上,修改一下数组,再定义一个线性比例尺。
var dataset = [ 2.5 , 2.1 , 1.7 , 1.3 , 0.9 ];
var linear = d3.scale.linear()
.domain([0, d3.max(dataset)])
.range([0, 250]);
复制代码
其后,按照上一章的方法添加矩形,在给矩形设置宽度的时候,应用比例尺。
var rectHeight = 25; //每个矩形所占的像素高度(包括空白)
svg.selectAll("rect")
.data(dataset)
.enter()
.append("rect")
.attr("x",20)
.attr("y",function(d,i){
return i * rectHeight;
})
.attr("width",function(d){
return linear(d); //在这里用比例尺
})
.attr("height",rectHeight-2)
.attr("fill","steelblue");
复制代码
如此一来,所有的数值,都按照同一个线性比例尺的关系来计算宽度,因此数值之间的大小关系不变。
坐标轴
坐标轴,是可视化图表中经常出现的一种图形,由一些列线段和刻度组成。坐标轴在 SVG 中是没有现成的图形元素的,需要用其他的元素组合构成。D3 提供了坐标轴的组件,如此在 SVG 画布中绘制坐标轴变得像添加一个普通元素一样简单。
1. 坐标轴由什么构成
在 SVG 画布的预定义元素里,有六种基本图形:
- 矩形
- 圆形
- 椭圆
- 线段
- 折线
- 多边形
另外,还有一种比较特殊,也是功能最强的元素:
- 路径
画布中的所有图形,都是由以上七种元素组成。
显然,这里面没有坐标轴 这种元素。如果有的话,我们可以采用类似以下的方式定义:
<axis x1="" x2="" ...></axis>
复制代码
很可惜,没有这种元素。但是,这种设计是合理的:不可能为每一种图形都配备一个单独的元素,那样 SVG 就会过于庞大。
因此,我们需要用其他元素来组合成坐标轴,最终使其变为类似以下的形式:
<g>
<!-- 第一个刻度 -->
<g>
<line></line> <!-- 第一个刻度的直线 -->
<text></text> <!-- 第一个刻度的文字 -->
</g>
<!-- 第二个刻度 -->
<g>
<line></line> <!-- 第二个刻度的直线 -->
<text></text> <!-- 第二个刻度的文字 -->
</g>
...
<!-- 坐标轴的轴线 -->
<path></path>
</g>
复制代码
分组元素 ,是 SVG 画布中的元素,意思是 group。此元素是将其他元素进行组合的容器,在这里是用于将坐标轴的其他元素分组存放。
如果需要手动添加这些元素就太麻烦了,为此,D3 提供了一个组件:d3.svg.axis()。它为我们完成了以上工作。
2. 定义坐标轴
上面提到了比例尺的概念,要生成坐标轴,需要用到比例尺,它们二者经常是一起使用的。下面,在上一章的数据和比例尺的基础上,添加一个坐标轴的组件。
//数据
var dataset = [ 2.5 , 2.1 , 1.7 , 1.3 , 0.9 ];
//定义比例尺
var linear = d3.scale.linear()
.domain([0, d3.max(dataset)])
.range([0, 250]);
var axis = d3.svg.axis()
.scale(linear) //指定比例尺
.orient("bottom") //指定刻度的方向
.ticks(7); //指定刻度的数量
复制代码
第 1 – 2 行:定义数组。
第 4 – 7 行:定义比例尺,其中使用了数组 dataset。
第 9 – 12 行:定义坐标轴,其中使用了线性比例尺 linear。其中:
- d3.svg.axis():D3 中坐标轴的组件,能够在 SVG 中生成组成坐标轴的元素。
- scale():指定比例尺。
- orient():指定刻度的朝向,bottom 表示在坐标轴的下方显示。
- ticks():指定刻度的数量。
3. 在 SVG 中添加坐标轴
定义了坐标轴之后,只需要在 SVG 中添加一个分组元素 ,再将坐标轴的其他元素添加到这个 里即可。代码如下:
svg.append("g")
.call(axis);
复制代码
上面有一个 call() 函数,其参数是前面定义的坐标轴 axis。
在 D3 中,call() 的参数是一个函数。调用之后,将当前的选择集作为参数传递给此函数。也就是说,以下两段代码是相等的。
function foo(selection) {
selection
.attr("name1", "value1")
.attr("name2", "value2");
}
foo(d3.selectAll("div"))
复制代码
和
d3.selectAll("div").call(foo);
复制代码
因此,
svg.append("g").call(axis);
复制代码
与
axis(svg.append(g));
复制代码
是相等的。
4. 设定坐标轴的样式和位置
默认的坐标轴样式不太美观,下面提供一个常见的样式:
<style>
.axis path,
.axis line{
fill: none;
stroke: black;
shape-rendering: crispEdges;
}
.axis text {
font-family: sans-serif;
font-size: 11px;
}
</style>
复制代码
分别定义了类 axis 下的 path、line、text 元素的样式。接下来,只需要将坐标轴的类设定为 axis 即可。
坐标轴的位置,可以通过 transform 属性来设定。
通常在添加元素的时候就一并设定,写成如下形式:
svg.append("g")
.attr("class","axis")
.attr("transform","translate(20,130)")
.call(axis);
复制代码
代码示例地址:github.com/legend-li/D…
【Lesson5】完整的柱形图
一个完整的柱形图包含三部分:矩形、文字、坐标轴。本章将对前几章的内容进行综合的运用,制作一个实用的柱形图,内容包括:选择集、数据绑定、比例尺、坐标轴等内容。
1. 添加 SVG 画布
//画布大小
var width = 400;
var height = 400;
//在 body 里添加一个 SVG 画布
var svg = d3.select("body")
.append("svg")
.attr("width", width)
.attr("height", height);
//画布周边的空白
var padding = {left:30, right:30, top:20, bottom:20};
复制代码
上面定义了一个 padding,是为了给 SVG 的周边留一个空白,最好不要将图形绘制到边界上。
2. 定义数据和比例尺
//定义一个数组
var dataset = [10, 20, 30, 40, 33, 24, 12, 5];
//x轴的比例尺
var xScale = d3.scale.ordinal()
.domain(d3.range(dataset.length))
.rangeRoundBands([0, width - padding.left - padding.right]);
//y轴的比例尺
var yScale = d3.scale.linear()
.domain([0,d3.max(dataset)])
.range([height - padding.top - padding.bottom, 0]);
复制代码
x 轴使用序数比例尺,y 轴使用线性比例尺。要注意两个比例尺值域的范围。
d3.range()用法讲解:
d3.range([start,]stop[,step]); //返回等差数列
var a = d3.range( 10);
console.log(a) //[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
var b = d3.range( 2,10 );
console.log(b);//[2, 3, 4, 5, 6, 7, 8, 9]
var c = d3.range( 2,10 ,2);
console.log(c);//[2, 4, 6, 8]
复制代码
ordinal.rangeRoundBands()用法讲解: github.com/d3/d3/wiki/…
3. 定义坐标轴
//定义x轴
var xAxis = d3.svg.axis()
.scale(xScale)
.orient("bottom");
//定义y轴
var yAxis = d3.svg.axis()
.scale(yScale)
.orient("left");
复制代码
x 轴刻度的方向向下,y 轴的向左。
4. 添加矩形和文字元素
//矩形之间的空白
var rectPadding = 4;
//添加矩形元素
var rects = svg.selectAll(".MyRect")
.data(dataset)
.enter()
.append("rect")
.attr("class","MyRect")
.attr("transform","translate(" + padding.left + "," + padding.top + ")")
.attr("x", function(d,i){
return xScale(i) + rectPadding/2;
} )
.attr("y",function(d){
return yScale(d);
})
.attr("width", xScale.rangeBand() - rectPadding )
.attr("height", function(d){
return height - padding.top - padding.bottom - yScale(d);
});
//添加文字元素
var texts = svg.selectAll(".MyText")
.data(dataset)
.enter()
.append("text")
.attr("class","MyText")
.attr("transform","translate(" + padding.left + "," + padding.top + ")")
.attr("x", function(d,i){
return xScale(i) + rectPadding/2;
} )
.attr("y",function(d){
return yScale(d);
})
.attr("dx",function(){
return (xScale.rangeBand() - rectPadding)/2;
})
.attr("dy",function(d){
return 20;
})
.text(function(d){
return d;
});
复制代码
矩形元素和文字元素的 x 和 y 坐标要特别注意,要结合比例尺给予适当的值。
5. 添加坐标轴的元素
//添加x轴
svg.append("g")
.attr("class","axis")
.attr("transform","translate(" + padding.left + "," + (height - padding.bottom) + ")")
.call(xAxis);
//添加y轴
svg.append("g")
.attr("class","axis")
.attr("transform","translate(" + padding.left + "," + padding.top + ")")
.call(yAxis);
复制代码
坐标轴的位置要结合空白 padding 的值来设定。
代码示例地址:github.com/legend-li/D…
下一章:《【D3.js 入门系列二】理解 Update && Enter && Exit、制作交互式动态图表》
参考资料:www.ourd3js.com/ D3.js(v3)中文api:github.com/d3/d3/wiki/…