声明: 本教程针对 D3.js v3 版本进行讲解
D3 是什么
D3 的全称是(Data-Driven Documents), 顾名思义可以知道是一个被数据驱动的文档听名字有点抽象, 说简单一点, 其实就是一个 JavaScript 的函数库, 使用它主要是用来做数据可视化的
D3 提供了各种简单易用的函数, 大大简化了 JavaScript 操作数据的难度由于它本质上是 JavaScript , 所以用 JavaScript 也是可以实现所有功能的, 但它能大大减小你的工作量, 尤其是在数据可视化方面, D3 已经将生成可视化的复杂步骤精简到了几个简单的函数, 你只需要输入几个简单的数据, 就能够转换为各种绚丽的图形有过 JavaScript 基础的朋友一定很容易理解它
为什么使用 D3.js
我们知道现在有很多开源的图表库 EchartsHighChartsG2.js 等等那么 D3 跟这些图表库相比有什么优势和劣势?
D3 基于 svg, 因此对图像进行放大不会失真(D3.js v4 版本已经支持 canvas 了, 本系列教程只讲 v3 版本)
D3 相对来说较底层, 对初学者来说不太方便, 但是一旦掌握了, 就比其他工具更加得心应手下图展示了 D3 与其它可视化工具的区别:
可以看到, D3 的步骤相对来说较多坏处是对初学者不方便也不好理解好处是能够制作出更加精密的图形因此, 我们可以据此定义什么时候选择 D3 比较好:
选择 D3: 如果希望开发脑海中任意想象到的图表
选择 HighchartsEcharts 等: 如果希望开发几种固定种类的十分大众化的图表
看起来, 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 下的 pathlinetext 元素的样式接下来, 只需要将坐标轴的类设定为 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 轴使用线性比例尺要注意两个比例尺值域的范围
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
来源: https://juejin.im/post/5aa0d5716fb9a028da7c1a4b