# D3: Data-Driven Documents
D3 (或 D3.js) 是一个 JavaScript 库,用于使用 Web 标准可视化数据。 D3 帮助您使用 SVG,Canvas 和 HTML 使数据栩栩如生。 D3 将强大的可视化和交互技术与数据驱动的 DOM 操作方法相结合,为您提供现代浏览器的全部功能,并为您的数据设计正确的可视界面提供了自由。
# 官方资源
- API Reference
- Release Notes
- Gallery
- Examples
- Wiki
# 安装 D3
- npm 安装
安装命令 npm install d3
;
导入 D3 到 ES2015 应用中,或者导入 D3 中一个模块使用,范例如下:
import {scaleLinear} from "d3-scale"; |
导入 D3 所有模块:
import * as d3 from "d3"; |
在 Node 环境中使用:
var d3 = require("d3"); |
自定义导入 D3 模块组合:
var d3 = Object.assign({}, require("d3-format"), require("d3-geo"), require("d3-geo-projection")); |
- 直接下载 D3 包
请下载最新版本,发行的 release 包支持 anonymous AMD, CommonJS, and vanilla 环境。你也可以直接从 d3js.org, CDNJS, or unpkg 上下载,例如:
<script src="https://d3js.org/d3.v5.js"></script> |
引用压缩版本:
<script src="https://d3js.org/d3.v5.min.js"></script> |
也可以直接引用其中的一个微库,例如: d3-selection:
<script src="https://d3js.org/d3-selection.v1.js"></script> |
# D3 的优点
前端可视化库中,Hightcharts(商业付费)、Echarts、Charts 可以看作一类,能够非常简单的制作图表,但是给予开发者控制和设计的空间少,封装层次太高,会失去部分自由,太低又会使程序太长,D3 取得了相对完美的平衡:
1)D3 相对比较底层,代码却足够简洁;
2)D3 更像数学库,为绘图提供了支持;
3)封装了很多操作,又给予了足够的自由;
# 如何使用 D3 来绘图
# 0、D3 的学习方法
D3 是一个 JS 库,api 非常多,也很完善,尤其是,出版物都非常完善,示例很多,只需要看相关示例,会查 API 文档即可。下面拿几个 D3 基本的使用来介绍下 D3 的运用。
# 1、选择元素和绑定数据
# 如何选择元素
在 D3 中,用于选择元素的函数有两个:
- d3.select():是选择所有指定元素的第一个
- d3.selectAll():是选择指定元素的全部
这两个函数返回的结果称为选择集。
例如,选择集的常见用法如下:
var body = d3.select("body"); // 选择文档中的 body 元素 | |
var svg = body.select("svg"); // 选择 body 中的 svg 元素 | |
var p = body.selectAll("p"); // 选择 body 中所有的 p 元素 | |
var p1 = body.select("p"); // 选择 body 中第一个 p 元素 | |
var rects = svg.selectAll("rect"); // 选择 svg 中所有的 rect 元素 |
假设在 body 中有三个段落元素:
<p>Apple</p> | |
<p>Pear</p> | |
<p>Banana</p> |
现在,要分别完成以下四种选择元素的任务。
# 选择第一个 p 元素
使用 select ,参数传入 p 即可,如此返回的是第一个 p 元素。
var body = d3.select("body"); | |
var p1 = body.select("p"); | |
p1.style("color","red"); // 将第一 p 元素内容置为红色 |
# 选择所有 p 元素
使用 selectAll 选择 body 中所有的 p 元素。
var body = d3.select("body"); | |
var p = body.selectAll("p"); | |
p.style("color", "red"); |
# 根据 id 选择元素
使用 select 选择元素,注意参数中 id 名称前要加 # 号。该方法可用于选择任何位置的元素,示例如下:
<p>Apple</p> | |
<p id="pear-id">Pear</p> | |
<p>Banana</p> | |
<script> | |
var body = d3.select("body"); | |
var p2 = body.select("#pear-id"); | |
p2.style("color", "red"); | |
</script> |
# 选择后两个 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 是代表索引号的,于是便可以用条件判定语句来指定执行的元素。
# 如何绑定数据
D3 中是通过以下两个函数来绑定数据的:
- datum():绑定一个数据到选择集上
- data():绑定一个数组到选择集上,数组的各项值分别与选择集的各元素绑定
相对而言,data () 比较常用。
假设现在有三个段落元素如下:
<p>Apple</p> | |
<p>Pear</p> | |
<p>Banana</p> |
# datum()
作用是绑定一个数据到选择集上。
假设有一字符串 World,要将此字符串分别与三个段落元素绑定,代码如下:
var str = "World"; | |
var body = d3.select("body"); | |
var p = body.selectAll("p"); | |
p.datum(str); | |
p.text(function(d, i){ | |
return "第 "+ i + " 个元素绑定的数据是 " + d; | |
}); |
绑定数据后,使用此数据来修改三个段落元素的内容,其结果如下:
第 0 个元素绑定的数据是 World | |
第 1 个元素绑定的数据是 World | |
第 2 个元素绑定的数据是 World |
在上面的代码中,用到了一个无名函数 function(d, i)。当选择集需要使用被绑定的数据时,常需要这么使用。其包含两个参数,其中:
- d 代表数据,也就是与某元素绑定的数据。
- i 代表索引,代表数据的索引号,从 0 开始。
例如,上述例子中:第 0 个元素 apple 绑定的数据是 World。
# data()
有一个数组,接下来要分别将数组的各元素绑定到三个段落元素上。
var dataset = ["I like dog","I like cat","I like snake"]; |
绑定之后,其对应关系的要求为:
- Apple 与 I like dog 绑定
- Pear 与 I like cat 绑定
- Banana 与 I like snake 绑定
调用 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 dog。
- 当 i == 1 时, d 为 I like cat。
- 当 i == 2 时, d 为 I like snake。
此时,三个段落元素与数组 dataset 的三个字符串是一一对应的,因此,在函数 function (d, i) 直接 return d 即可。
结果自然是三个段落的文字分别变成了数组的三个字符串。
I like dog | |
I like cat | |
I like snake |
# 2、插入、删除元素
# 插入元素
插入元素涉及的函数有两个:
- append ():在选择集末尾插入元素
- insert ():在选择集前面插入元素
假设有三个段落元素,与上文相同。
# append()
body.append("p") | |
.text("append p element"); |
在 body 的末尾添加一个 p 元素,结果为:
Apple | |
Pear | |
Banana | |
append p element |
# insert()
在 body 中 id 为 myid 的元素前添加一个段落元素。
body.insert("p","#pear-id") | |
.text("insert p element"); |
已经指定了 Pear 段落的 id 为 myid,因此结果如下。
Apple | |
insert p element | |
Pear | |
Banana |
# 删除元素
删除一个元素时,对于选择的元素,使用 remove 即可,例如:
var p = body.select("#pear-id"); | |
p.remove(); |
如此即可删除指定 id 的段落元素。
# 3、做一个简单的图表
柱形图是一种最简单的可视化图标,主要有矩形、文字标签、坐标轴组成。本文为简单起见,只绘制矩形的部分,用以讲解如何使用 D3 在 SVG 画布中绘图。
# 画布是什么
前几章的处理对象都是 HTML 的文字,没有涉及图形的制作。
要绘图,首要需要的是一块绘图的 “画布”。
HTML 5 提供两种强有力的 “画布”:SVG 和 Canvas。
# SVG 是什么
SVG,指可缩放矢量图形(Scalable Vector Graphics),是用于描述二维矢量图形的一种图形格式,是由万维网联盟制定的开放标准。 SVG 使用 XML 格式来定义图形,除了 IE8 之前的版本外,绝大部分浏览器都支持 SVG,可将 SVG 文本直接嵌入 HTML 中显示。
SVG 有如下特点:
- SVG 绘制的是矢量图,因此对图像进行放大不会失真。
- 基于 XML,可以为每个元素添加 JavaScript 事件处理器。
- 每个图形均视为对象,更改对象的属性,图形也会改变。
- 不适合游戏应用。
# Canvas 是什么
Canvas 是通过 JavaScript 来绘制 2D 图形,是 HTML 5 中新增的元素。
Canvas 有如下特点:
- 绘制的是位图,图像放大后会失真。
- 不支持事件处理器。
- 能够以 .png 或 .jpg 格式保存图像
- 适合游戏应用 (是逐像素进行渲染的)
# 添加画布
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); // 设定高度 |
有了画布,接下来就可以在画布上作图了。
# 绘制矩形
本文绘制一个横向的柱形图。只绘制矩形,不绘制文字和坐标轴。
在 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 的形式,方便归类和修改。这里为了便于初学者理解,将样式直接写到元素里。
结果图如下所示:
# 4、比例尺的使用
比例尺是 D3 中很重要的一个概念,上一章里曾经提到过直接用数值的大小来代表像素不是一种好方法,本章正是要解决此问题。
# 为什么需要比例尺
上一章制作了一个柱形图,当时有一个数组:
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)。
# 有哪些比例尺
比例尺,很像数学中的函数。例如,对于一个一元二次函数,有 x 和 y 两个未知数,当 x 的值确定时,y 的值也就确定了。
在数学中,x 的范围被称为定义域,y 的范围被称为值域。
D3 中的比例尺,也有定义域和值域,分别被称为 domain 和 range。开发者需要指定 domain 和 range 的范围,如此即可得到一个计算关系。
D3 提供了多种比例尺,下面介绍最常用的两种。
# 线性比例尺
线性比例尺,能将一个连续的区间,映射到另一区间。要解决柱形图宽度的问题,就需要线性比例尺。
假设有以下数组:
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 scaleLinear = d3.scaleLinear() | |
.domain([min, max]) | |
.range([0, 300]); | |
scaleLinear(0.9); // 返回 0 | |
scaleLinear(2.3); // 返回 175 | |
scaleLinear(3.3); // 返回 300 |
其中,d3.scaleLinear() 返回一个线性比例尺。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.scaleLinear () 的返回值,是可以当做函数来使用的。因此,才有这样的用法:scaleLinear (0.9)。
# 序数比例尺
有时候,定义域和值域不一定是连续的。例如,有两个数组:
var index = [0, 1, 2, 3, 4]; | |
var color = ["red", "blue", "green", "yellow", "black"]; |
我们希望 0 对应颜色 red,1 对应 blue,依次类推。
但是,这些值都是离散的,线性比例尺不适合,需要用到序数比例尺。
var scaleOrdinal = d3.scaleOrdinal() | |
.domain(index) | |
.range(color); | |
scaleOrdinal (0); // 返回 red | |
scaleOrdinal (2); // 返回 green | |
scaleOrdinal (4); // 返回 black |
用法与线性比例尺是类似的。
# 给柱形图添加比例尺
在上一章的基础上,修改一下数组,再定义一个线性比例尺。
var dataset = [ 2.5 , 2.1 , 1.7 , 1.3 , 0.9 ]; | |
var scaleLinear = d3.scaleLinear() | |
.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 scaleLinear(d); // 在这里用比例尺 | |
}) | |
.attr("height",rectHeight-2) | |
.attr("fill","steelblue"); |
如此一来,所有的数值,都按照同一个线性比例尺的关系来计算宽度,因此数值之间的大小关系不变。
# 5、坐标轴
坐标轴,是可视化图表中经常出现的一种图形,由一些列线段和刻度组成。坐标轴在 SVG 中是没有现成的图形元素的,需要用其他的元素组合构成。D3 提供了坐标轴的组件,如此在 SVG 画布中绘制坐标轴变得像添加一个普通元素一样简单。
# 坐标轴由什么构成
在 SVG 画布的预定义元素里,有六种基本图形:
- 矩形
- 圆形
- 椭圆
- 线段
- 折线
- 多边形
另外,还有一种比较特殊,也是功能最强的元素:
- 路径
画布中的所有图形,都是由以上七种元素组成。
显然,这里面没有坐标轴 这种元素。如果有的话,我们可以采用类似以下的方式定义:
<axis x1="" x2="" ...></axis> |
很可惜,没有这种元素。但是,这种设计是合理的:不可能为每一种图形都配备一个单独的元素,那样 SVG 就会过于庞大。
因此,我们需要用其他元素来组合成坐标轴,最终使其变为类似以下的形式:
<g> | |
<!-- 第一个刻度 --> | |
<g> | |
<line></line> <!-- 第一个刻度的直线 --> | |
<text></text> <!-- 第一个刻度的文字 --> | |
</g> | |
<!-- 第二个刻度 --> | |
<g> | |
<line></line> <!-- 第二个刻度的直线 --> | |
<text></text> <!-- 第二个刻度的文字 --> | |
</g> | |
... | |
<!-- 坐标轴的轴线 --> | |
<path></path> | |
</g> |
<g>
分组元素 ,是 SVG 画布中的元素,意思是 group。此元素是将其他元素进行组合的容器,在这里是用于将坐标轴的其他元素分组存放。
如果需要手动添加这些元素就太麻烦了,为此,D3 提供了轴生成器:d3.axisTop ()、d3.axisRight ()、
d3.axisBottom ()、d3.axisLeft ()。它为我们完成了以上工作。
# 定义坐标轴
上一章提到了比例尺的概念,要生成坐标轴,需要用到比例尺,它们二者经常是一起使用的。下面,在上一章的数据和比例尺的基础上,添加一个坐标轴的组件。
坐标轴是有朝向的,在这里我们以向下朝向、水平方向的坐标轴为例,其他朝向的(比如向左朝向的、垂直的坐标轴)类似。
var dataset = [2.5, 2.1, 1.7, 1.3, 0.9]; // 数据 | |
// 定义线性比例尺 | |
var xScale = d3 | |
.scaleLinear() | |
.domain([0, d3.max(dataset)]) | |
.range([0, 250]); | |
// 定义坐标轴 | |
var axis = d3 | |
.axisBottom(xScale) // 定义一个 axis 并指定刻度的方向为 bottom(朝下)且指定比例尺为 xScale | |
.ticks(7); // 指定刻度的数量 |
# 在 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)); |
意思一致。
# 设定坐标轴的样式和位置
默认的坐标轴样式不太美观,下面提供一个常见的样式:
<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); |
# 6、完整的柱形图
一个完整的柱形图包含三部分:矩形、文字、坐标轴。本章将对前几章的内容进行综合的运用,制作一个实用的柱形图,内容包括:选择集、数据绑定、比例尺、坐标轴等内容。
# 添加 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 的周边留一个空白,最好不要将图形绘制到边界上。
# 定义数据和比例尺
// 定义一个数组 | |
var dataset = [10, 20, 30, 40, 33, 24, 12, 5]; | |
//x 轴的比例尺 (创建一个序列化比例尺) | |
var xScale = d3.scaleBand() | |
.domain(d3.range(dataset.length)) | |
.rangeRound([0, width - padding.left - padding.right]); | |
//y 轴的比例尺 | |
var yScale = d3.scaleLinear() | |
.domain([0,d3.max(dataset)]) | |
.range([height - padding.top - padding.bottom, 0]); |
- 这里出现了
d3.scaleBand()
,这是一个坐标轴 d3.range(dataset.length)
,前面说过,这里返回的是一个等差数列,dataset.length=9
,所以返回的是 0 到 8 的等差数列
x 轴使用序数化比例尺,y 轴使用线性比例尺。要注意两个比例尺值域的范围。
# 定义坐标轴
var xAxis = d3.axisBottom(xScale); // 定义 x 轴 | |
var yAxis = d3.axisLeft(yScale); // 定义 y 轴 |
x 轴刻度的方向向下,y 轴的向左。
# 添加矩形和文字元素
// 矩形之间的空白 | |
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.step() - rectPadding) | |
.attr("height", function(d) { | |
return height - padding.top - padding.bottom - yScale(d); | |
}) | |
.attr("fill","blue"); | |
// 添加文字元素 | |
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.step() - rectPadding) / 2; | |
}) | |
.attr("dy", function(d) { | |
return 20; | |
}) | |
.text(function(d) { | |
return d; | |
}); |
矩形元素和文字元素的 x 和 y 坐标要特别注意,要结合比例尺给予适当的值。
# 添加坐标轴的元素
// 添加 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 的值来设定。
# 7、让图表动起来
D3 支持制作动态的图表。有时候,图表的变化需要缓慢的发生,以便于让用户看清楚变化的过程,也能给用户不小的友好感。
# 什么是动态效果
前面几章制作的图表是一蹴而就地出现,然后绘制完成后不再发生变化的,这是静态的图表。
动态的图表,是指图表在某一时间段会发生某种变化,可能是形状、颜色、位置等,而且用户是可以看到变化的过程的。
例如,有一个圆,圆心为 (100, 100)。现在我们希望圆的 x 坐标从 100 移到 300,并且移动过程在 2 秒的时间内发生。
这种时候就需要用到动态效果,在 D3 里我们称之为过渡(transition)。
# 实现动态的方法
D3 提供了 4 个方法用于实现图形的过渡:从状态 A 变为状态 B。
# transition()
启动过渡效果。
其前后是图形变化前后的状态(形状、位置、颜色等等),例如:
.attr("fill","red") // 初始颜色为红色 | |
.transition() // 启动过渡 | |
.attr("fill","steelblue") // 终止颜色为铁蓝色 |
D3 会自动对两种颜色(红色和铁蓝色)之间的颜色值(RGB 值)进行插值计算,得到过渡用的颜色值。我们无需知道中间是怎么计算的,只需要享受结果即可。
# duration()
指定过渡的持续时间,单位为毫秒。
如 duration (2000) ,指持续 2000 毫秒,即 2 秒。
# ease()
指定过渡的方式,常用的有:
- linear:普通的线性变化
- circle:慢慢地到达变换的最终状态
- elastic:带有弹跳的到达最终状态
- bounce:在最终状态处弹跳几次
调用时,格式形如: ease (“bounce”)。
# delay()
指定延迟的时间,表示一定时间后才开始转变,单位同样为毫秒。此函数可以对整体指定延迟,也可以对个别指定延迟。
例如,对整体指定时:
.transition() | |
.duration(1000) | |
.delay(500) |
如此,图形整体在延迟 500 毫秒后发生变化,变化的时长为 1000 毫秒。因此,过渡的总时长为 1500 毫秒。
又如,对一个一个的图形(图形上绑定了数据)进行指定时:
.transition() | |
.duration(1000) | |
.delay(funtion(d,i){ | |
return 200*i; | |
}) |
如此,假设有 10 个元素,那么第 1 个元素延迟 0 毫秒(因为 i = 0),第 2 个元素延迟 200 毫秒,第 3 个延迟 400 毫秒,依次类推…. 整个过渡的长度为 200 * 9 + 1000 = 2800 毫秒。
# 实现简单的动态效果
下面将在 SVG 画布里添加三个圆,圆出现之后,立即启动过渡效果。
第一个圆,要求移动 x 坐标。
var circle1 = svg.append("circle") | |
.attr("cx", 100) | |
.attr("cy", 100) | |
.attr("r", 45) | |
.style("fill", "green"); | |
// 在 1 秒(1000 毫秒)内将圆心坐标由 100 变为 300 | |
circle1.transition() | |
.duration(1000) | |
.attr("cx", 300); |
第二个圆,要求既移动 x 坐标,又改变颜色。
var circle2 = svg.append("circle") | |
.attr("cx", 100) | |
.attr("cy", 200) | |
.attr("r", 45) | |
.style("fill", "green"); // 与第一个圆一样 | |
// 在 1.5 秒(1500 毫秒)内将圆心坐标由 100 变为 300, | |
// 将颜色从绿色变为红色 | |
circle2.transition() | |
.duration(1500) | |
.attr("cx", 300) | |
.style("fill", "red"); |
第三个圆,要求既移动 x 坐标,又改变颜色,还改变半径。
var circle3 = svg.append("circle") | |
.attr("cx", 100) | |
.attr("cy", 300) | |
.attr("r", 45) | |
.style("fill", "green"); // 与第一个圆一样 | |
// 在 2 秒(2000 毫秒)内将圆心坐标由 100 变为 300 | |
// 将颜色从绿色变为红色 | |
// 将半径从 45 变成 25 | |
// 过渡方式采用 bounce(在终点处弹跳几次) | |
circle3 | |
.transition() | |
.duration(2000) | |
.ease(d3.easeBounce) | |
.attr("cx", 300) | |
.style("fill", "red") | |
.attr("r", 25); |
# 给柱形图加上动态效果
在上一章完整柱形图的基础上稍作修改,即可做成一个带动态效果的、有意思的柱形图。
在添加文字元素和矩形元素的时候,启动过渡效果,让各柱形和文字缓慢升至目标高度,并且在目标处跳动几次。
对于文字元素,代码如下:
.attr("y",function(d){ | |
var min = yScale.domain()[0]; | |
return yScale(min); | |
}) | |
.transition() | |
.delay(function(d,i){ | |
return i * 200; | |
}) | |
.duration(2000) | |
.ease(d3.easeBounce) | |
.attr("y",function(d){ | |
return yScale(d); | |
}); |
文字元素的过渡前后,发生变化的是 y 坐标。其起始状态是在 y 轴等于 0 的位置(但要注意,不能在起始状态直接返回 0,要应用比例尺计算画布中的位置)。终止状态是目标值。
# 8、理解 Update、Enter、Exit
Update、Enter、Exit 是 D3 中三个非常重要的概念,它处理的是当选择集和数据的数量关系不确定的情况。
# 什么是 Update、Enter、Exit
前几章里,反复出现了形如以下的代码。
svg.selectAll("rect") // 选择 svg 内所有的矩形 | |
.data(dataset) // 绑定数组 | |
.enter() // 指定选择集的 enter 部分 | |
.append("rect") // 添加足够数量的矩形元素 |
前面提到,这段代码使用的情况是当以下情况出现的时候:
有数据,而没有足够图形元素的时候,使用此方法可以添加足够的元素。
当时并没有深究这段代码是什么意思,本章将对此进行讲解。但是,由于此问题相对复杂,本章只进行最初步的介绍。
假设,在 body 中有三个 p 元素,有一数组 [3, 6, 9],则可以将数组中的每一项分别与一个 p 元素绑定在一起。但是,有一个问题:** 当数组的长度与元素数量不一致(数组长度 > 元素数量 or 数组长度 < 元素数量)时呢?** 这时候就需要理解 Update、Enter、Exit 的概念。
如果数组为 [3, 6, 9, 12, 15],将此数组绑定到三个 p 元素的选择集上。可以想象,会有两个数据没有元素与之对应,这时候 D3 会建立两个空的元素与数据对应,这一部分就称为 Enter。而有元素与数据对应的部分称为 Update。如果数组为 [3],则会有两个元素没有数据绑定,那么没有数据绑定的部分被称为 Exit。示意图如下所示。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 (img-jbFyQNr1-1629179770606)(/download/attachments/41324300/1581165812504.png?version=1&modificationDate=1581176415000&api=v2)]
看到这,我想大家能体会到为什么本节最开始处的代码能够给 SVG 内添加足够数量的元素了吧。它的意思其实是:
此时 SVG 里没有 rect 元素,即元素数量为 0。有一数组 dataset,将数组与元素数量为 0 的选择集绑定后,选择其 Enter 部分(请仔细看上图),然后添加(append)元素,也就是添加足够的元素,使得每一个数据都有元素与之对应。
# Update 和 Enter 的使用
当对应的元素不足时 ( 绑定数据数量 > 对应元素 ),需要添加元素(append)。
现在 body 中有三个 p 元素,要绑定一个长度大于 3 的数组到 p 的选择集上,然后分别处理 update 和 enter 两部分。
var dataset = [ 3 , 6 , 9 , 12 , 15 ]; | |
// 选择 body 中的 p 元素 | |
var p = d3.select("body").selectAll("p"); | |
// 获取 update 部分 | |
var update = p.data(dataset); | |
// 获取 enter 部分 | |
var enter = update.enter(); | |
//update 部分的处理:更新属性值 | |
update.text(function(d){ | |
return "update " + d; | |
}); | |
//enter 部分的处理:添加元素后赋予属性值 | |
enter.append("p") | |
.text(function(d){ | |
return "enter " + d; | |
}); |
结果如下图,update 部分和 enter 部分被绑定的数据很清晰地表示了出来:
update 3 | |
update 6 | |
update 9 | |
enter 12 | |
enter 15 |
请大家记住:
- update 部分的处理办法一般是:更新属性值
- enter 部分的处理办法一般是:添加元素后,赋予属性值
# Update 和 Exit 的使用
当对应的元素过多时 ( 绑定数据数量 < 对应元素 ),需要删掉多余的元素。
现在 body 中有三个 p 元素,要绑定一个长度小于 3 的数组到 p 的选择集上,然后分别处理 update 和 exit 两部分。
var dataset = [ 3 ]; | |
// 选择 body 中的 p 元素 | |
var p = d3.select("body").selectAll("p"); | |
// 获取 update 部分 | |
var update = p.data(dataset); | |
// 获取 exit 部分 | |
var exit = update.exit(); | |
//update 部分的处理:更新属性值 | |
update.text(function(d){ | |
return "update " + d; | |
}); | |
//exit 部分的处理:修改 p 元素的属性 | |
exit.text(function(d){ | |
return "exit"; | |
}); | |
//exit 部分的处理通常是删除元素 | |
// exit.remove(); |
结果如下,请大家区分好 update 部分和 exit 部分。这里为了表明哪一部分是 exit,并没有删除掉多余的元素,但实际上 exit 部分的绝大部分操作是删除。
update 3 | |
exit | |
exit |
请大家记住:
- exit 部分的处理办法一般是:删除元素(remove)
# 9、交互式操作
与图表的交互,指在图形元素上设置一个或多个监听器,当事件发生时,做出相应的反应。
# 什么是交互
交互,指的是用户输入了某种指令,程序接受到指令之后必须做出某种响应。对可视化图表来说,交互能使图表更加生动,能表现更多内容。例如,拖动图表中某些图形、鼠标滑到图形上出现提示框、用触屏放大或缩小图形等等。
用户用于交互的工具一般有三种:鼠标、键盘、触屏。
# 如何添加交互
对某一元素添加交互操作十分简单,代码如下:
var circle = svg.append("circle"); | |
circle.on("click", function(){ | |
// 在这里添加交互内容 | |
}); |
这段代码在 SVG 中添加了一个圆,然后添加了一个监听器,是通过 on () 添加的。在 D3 中,每一个选择集都有 on () 函数,用于添加事件监听器。
on () 的第一个参数是监听的事件,第二个参数是监听到事件后响应的内容,第二个参数是一个函数。
鼠标常用的事件有:
- click:鼠标单击某元素时,相当于 mousedown 和 mouseup 组合在一起。
- mouseover:光标放在某元素上。
- mouseout:光标从某元素上移出来时。
- mousemove:鼠标被移动的时候。
- mousedown:鼠标按钮被按下。
- mouseup:鼠标按钮被松开。
- dblclick:鼠标双击。
键盘常用的事件有三个:
- keydown:当用户按下任意键时触发,按住不放会重复触发此事件。该事件不会区分字母的大小写,例如 “A” 和 “a” 被视为一致。
- keypress:当用户按下字符键(大小写字母、数字、加号、等号、回车等)时触发,按住不放会重复触发此事件。该事件区分字母的大小写。
- keyup:当用户释放键时触发,不区分字母的大小写。 触屏常用的事件有三个:
- touchstart:当触摸点被放在触摸屏上时。
- touchmove:当触摸点在触摸屏上移动时。
- touchend:当触摸点从触摸屏上拿开时。 当某个事件被监听到时,D3 会把当前的事件存到 d3.event 对象,里面保存了当前事件的各种参数,请大家好好参详。如果需要监听到事件后立刻输出该事件,可以添加一行代码:
circle.on("click", function(){ | |
console.log(d3.event); | |
}); |
# 带有交互的柱形图
将前面柱状图的部分代码修改成如下代码。
var rects = svg.selectAll(".MyRect") | |
.data(dataset) | |
.enter() | |
.append("rect") | |
.attr("class","MyRect") // 把类里的 fill 属性清空 | |
.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.step() - rectPadding ) | |
.attr("height", function(d){ | |
return height - padding.top - padding.bottom - yScale(d); | |
}) | |
.attr("fill","steelblue") // 填充颜色不要写在 CSS 里 | |
.on("mouseover",function(d,i){ | |
d3.select(this) | |
.attr("fill","yellow"); | |
}) | |
.on("mouseout",function(d,i){ | |
d3.select(this) | |
.transition() | |
.duration(500) | |
.attr("fill","steelblue"); | |
}); |
这段代码添加了鼠标移入(mouseover),鼠标移出(mouseout)两个事件的监听器。监听器函数中都使用了 d3.select(this)
,表示选择当前的元素,this 是当前的元素,要改变响应事件的元素时这么写就好。
mouseover 监听器函数的内容为:将当前元素变为黄色
mouseout 监听器函数的内容为:缓慢地将元素变为原来的颜色(蓝色)
# 10、布局
布局,可以理解成 “制作常见图形的函数”,有了它制作各种相对复杂的图表就方便多了。
# 布局是什么
布局,英文是 Layout。从字面看,可以想到有 “决定什么元素绘制在哪里” 的意思。布局是 D3 中一个十分重要的概念。D3 与其它很多可视化工具不同,相对来说较底层,对初学者来说不太方便,但是一旦掌握了,就比其他工具更加得心应手。下图展示了 D3 与其它可视化工具的区别:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 (img-4zHdMtx9-1629179770609)(/download/attachments/41324300/1581167394482.png?version=1&modificationDate=1581176415000&api=v2)]
可以看到,D3 的步骤相对来说较多。坏处是对初学者不方便、也不好理解。好处是能够制作出更加精密的图形。因此,我们可以据此定义什么时候选择 D3 比较好:
- 选择 D3:如果希望开发脑海中任意想象到的图表。
- 选择 Highcharts、Echarts 等:如果希望开发几种固定种类的、十分大众化的图表。
看起来,D3 似乎是为艺术家或发烧友准备的。有那么点意思,但请初学者也不要放弃。
# 如何理解布局
从上面的图可以看到,布局的作用是:将不适合用于绘图的数据转换成了适合用于绘图的数据。
因此,为了便于初学者理解,布局的作用解释成:数据转换。
# 布局有哪些
D3 总共提供了 12 个布局:饼状图(Pie)、力导向图(Force)、弦图(Chord)、树状图(Tree)、集群图(Cluster)、捆图(Bundle)、打包图(Pack)、直方图(Histogram)、分区图(Partition)、堆栈图(Stack)、矩阵树图(Treemap)、层级图(Hierarchy)。
12 个布局中,层级图(Hierarchy)不能直接使用。集群图、打包图、分区图、树状图、矩阵树图是由层级图扩展来的。如此一来,能够使用的布局是 11 个(有 5 个是由层级图扩展而来)。这些布局的作用都是将某种数据转换成另一种数据,而转换后的数据是利于可视化的。
Bundle —- 捆图
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 (img-giecBFN8-1629179770610)(/download/attachments/41324300/1581167469030.png?version=1&modificationDate=1581176415000&api=v2)]
Chord —- 弦图
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 (img-d9Wiuw9u-1629179770612)(/download/attachments/41324300/1581167483157.png?version=1&modificationDate=1581176415000&api=v2)]
Cluster —- 集群图
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 (img-nMtPMpIm-1629179770613)(/download/attachments/41324300/1581167499717.png?version=1&modificationDate=1581176415000&api=v2)]
Force —- 力学图、力导向图
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 (img-2UNpgRaY-1629179770614)(/download/attachments/41324300/1581167512490.png?version=1&modificationDate=1581176415000&api=v2)]
Histogram —- 直方图(数据分布图)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 (img-3NWkfusx-1629179770615)(/download/attachments/41324300/1581167526523.png?version=1&modificationDate=1581176415000&api=v2)]
Pack —- 打包图
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 (img-37UZ6ncS-1629179770616)(/download/attachments/41324300/1581167538005.png?version=1&modificationDate=1581176415000&api=v2)]
Partition —- 分区图
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 (img-P5knvexg-1629179770617)(/download/attachments/41324300/1581167549590.png?version=1&modificationDate=1581176415000&api=v2)]
Pie —- 饼状图
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 (img-hMZvE37L-1629179770617)(/download/attachments/41324300/1581167561567.png?version=1&modificationDate=1581176415000&api=v2)]
Stack —- 堆栈图
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 (img-r7DO8BjF-1629179770619)(/download/attachments/41324300/1581167572871.png?version=1&modificationDate=1581176415000&api=v2)]
Tree —- 树状图
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 (img-pG9OUCWC-1629179770620)(/download/attachments/41324300/1581167585333.png?version=1&modificationDate=1581176415000&api=v2)]
Treemap —- 矩阵树图
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 (img-rsFGuklC-1629179770621)(/download/attachments/41324300/1581167595875.png?version=1&modificationDate=1581176415000&api=v2)]
# 11、饼状图
本章制作一个饼状图。在布局的应用中,最简单的就是饼状图,通过本文你将对布局有一个初步了解。
1、数据准备
var marge = {top:60,bottom:60,left:60,right:60} | |
var svg = d3.select("svg") | |
var width = svg.attr("width") | |
var height = svg.attr("height") | |
var g = svg.append("g") | |
.attr("transform","translate("+marge.top+","+marge.left+")"); | |
var dataset = [ 30 , 10 , 43 , 55 , 13 ];// 需要将这些数据变成饼状图的数据 |
2、设置一个颜色比例尺
// 设置一个 color 的颜色比例尺,为了让不同的扇形呈现不同的颜色 | |
var colorScale = d3.scaleOrdinal() | |
.domain(d3.range(dataset.length)) | |
.range(d3.schemeCategory10); |
3、新建一个饼状图
// 新建一个饼状图 | |
var pie = d3.pie(); |
4、新建一个弧形生成器
// 新建一个弧形生成器 | |
var innerRadius = 0;// 内半径 | |
var outerRadius = 100;// 外半径 | |
var arc_generator = d3.arc() | |
.innerRadius(0) | |
.outerRadius(100); |
5、利用饼状图生成器转换数据
// 将原始数据变成可以绘制饼状图的数据, | |
var pieData = pie(dataset); | |
// 在浏览器的控制台打印 pieData | |
console.log(pieData); |
6、开始绘制,老规矩,先为每一个扇形及其对应的文字建立一个分组
var gs = g.selectAll(".g") | |
.data(pieData) | |
.enter() | |
.append("g") | |
.attr("transform","translate("+width/2+","+height/2+")")// 位置信息 |
7、绘制扇形
// 绘制饼状图的各个扇形 | |
gs.append("path") | |
.attr("d",function(d){ | |
return arc_generator(d);// 往弧形生成器中出入数据 | |
}) | |
.attr("fill",function(d,i){ | |
return colorScale(i);// 设置颜色 | |
}); |
arc_generator(d);
// 往弧形生成器中出入数据,由官方 API 的示例可知(我已经在本章开头截了图),弧形生成器所需要传入的数据就是饼状图生成器返回的数据
8、文字
// 绘制饼状图上面的文字信息 | |
gs.append("text") | |
.attr("transform",function(d){// 位置设在中心处 | |
return "translate("+arc_generator.centroid(d)+")"; | |
}) | |
.attr("text-anchor","middle") | |
.text(function(d){ | |
return d.data; | |
}) |
注意:这里的文字获取需要 用到 d.data,我们可以根据在控制台输出的数据格式可以知道,因为在转换数据后,新数据的 data 域才是原始数据
最后,整个代码的结果:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 (img-FWtubXEi-1629179770622)(/download/attachments/41324300/1581172884792.png?version=1&modificationDate=1581176415000&api=v2)]
# 12、力导向图
力导向图(Force-Directed Graph),是绘图的一种算法。在二维或三维空间里配置节点,节点之间用线连接,称为连线。各连线的长度几乎相等,且尽可能不相交。节点和连线都被施加了力的作用,力是根据节点和连线的相对位置计算的。根据力的作用,来计算节点和连线的运动轨迹,并不断降低它们的能量,最终达到一种能量很低的安定状态。
力导向图能表示节点之间的多对多的关系。
为了绘制一个力导向图,我们还是需要以下新的知识点
- d3.forceSimulation () ,新建一个力导向图,
- d3.forceSimulation ().force (), 添加或者移除一个力;对于 d3.forceSimulation ().force (name),也就是当 force 中只有一个参数,这个参数是某个力的名称,那么这段代码返回的是某个具体的力,(根据上面图片官方对 force 的说明也可以知道),例如 d3.forceSimulation ().force (“link”),则返回的是 d3.forceLink () 这个力,注意对照上面的图片,这个用法在下面会经常被用到
- d3.forceSimulation ().nodes (),输入是一个数组,然后将这个输入的数组进行一定的数据转换,例如添加坐标什么的
- d3.forceLink.links (),这里输入的也是一个数组(边集),然后对输入的边集进行转换,等一下我们会看到,它到底被转换成什么样子的
- tick 函数,这个函数对于力导向图来说非常重要,因为力导向图是不断运动的,每一时刻都在发生更新,所以需要不断更新节点和连线的位置
- d3.drag (), 是力导向图可以被拖动
1、数据准备
var marge = {top:60,bottom:60,left:60,right:60} | |
var svg = d3.select("svg") | |
var width = svg.attr("width") | |
var height = svg.attr("height") | |
var g = svg.append("g") | |
.attr("transform","translate("+marge.top+","+marge.left+")"); | |
// 准备数据 | |
var nodes = [// 节点集 | |
{name:"湖南邵阳"}, | |
{name:"山东莱州"}, | |
{name:"广东阳江"}, | |
{name:"山东枣庄"}, | |
{name:"泽"}, | |
{name:"恒"}, | |
{name:"鑫"}, | |
{name:"明山"}, | |
{name:"班长"} | |
]; | |
var edges = [// 边集 | |
{source:0,target:4,relation:"籍贯",value:1.3}, | |
{source:4,target:5,relation:"舍友",value:1}, | |
{source:4,target:6,relation:"舍友",value:1}, | |
{source:4,target:7,relation:"舍友",value:1}, | |
{source:1,target:6,relation:"籍贯",value:2}, | |
{source:2,target:5,relation:"籍贯",value:0.9}, | |
{source:3,target:7,relation:"籍贯",value:1}, | |
{source:5,target:6,relation:"同学",value:1.6}, | |
{source:6,target:7,relation:"朋友",value:0.7}, | |
{source:6,target:8,relation:"职责",value:2} | |
]; |
2、设置一个颜色比例尺
// 设置一个 color 的颜色比例尺,为了让不同的扇形呈现不同的颜色 | |
var colorScale = d3.scaleOrdinal() | |
.domain(d3.range(nodes.length)) | |
.range(d3.schemeCategory10); |
3、新建一个力导向图
var forceSimulation = d3.forceSimulation() | |
.force("link",d3.forceLink()) | |
.force("charge",d3.forceManyBody()) | |
.force("center",d3.forceCenter()); |
4、生成节点数据
// 生成节点数据 | |
forceSimulation.nodes(nodes) | |
.on("tick",ticked);// 这个函数很重要,后面给出具体实现和说明 |
注意,这里出现了 tick 函数,我把它的实现写到了一个有名函数 ticked(以前我们大多写的都是无名函数)
5、生成边集数据
// 生成边数据 | |
forceSimulation.force("link") | |
.links(edges) | |
.distance(function(d){// 每一边的长度 | |
return d.value*100; | |
}) |
注意,这里出现了 forceSimulation.force (name) 的用法,前面已经详细解释了它的返回值
6、设置图形中心位置
// 设置图形的中心位置 | |
forceSimulation.force("center") | |
.x(width/2) | |
.y(height/2); |
这里也是 forceSimulation.force (name) 的用法
7、输出顶点集和边集
// 在浏览器的控制台输出 | |
console.log(nodes); | |
console.log(edges); |
8、绘制边
// 绘制边 | |
var links = g.append("g") | |
.selectAll("line") | |
.data(edges) | |
.enter() | |
.append("line") | |
.attr("stroke",function(d,i){ | |
return colorScale(i); | |
}) | |
.attr("stroke-width",1); |
这里注意一下,应该先绘制边,在绘制顶点,因为在 d3 中,各元素是有层级关系的,先绘制的在下面
9、边上的文字
var linksText = g.append("g") | |
.selectAll("text") | |
.data(edges) | |
.enter() | |
.append("text") | |
.text(function(d){ | |
return d.relation; | |
}) |
10、老规矩,先建立用来放在每个节点和对应文字的分组 <g>
var gs = g.selectAll(".circleText") | |
.data(nodes) | |
.enter() | |
.append("g") | |
.attr("transform",function(d,i){ | |
var cirX = d.x; | |
var cirY = d.y; | |
return "translate("+cirX+","+cirY+")"; | |
}) | |
.call(d3.drag() | |
.on("start",started) | |
.on("drag",dragged) | |
.on("end",ended) | |
); |
注意,这里出现了 drag 函数,对于 call 函数大家应该比较熟悉了!我们也可以发现,这里使用了三个有名函数,具体实现见后面 13、drag
11、节点和文字
// 绘制节点 | |
gs.append("circle") | |
.attr("r",10) | |
.attr("fill",function(d,i){ | |
return colorScale(i); | |
}) | |
// 文字 | |
gs.append("text") | |
.attr("x",-10) | |
.attr("y",-20) | |
.attr("dy",10) | |
.text(function(d){ | |
return d.name; | |
}) |
注意,这里的文字的到需要根据转换后数据的特点得到。
12、ticked 函数的实现
function ticked(){ | |
links.attr("x1",function(d){return d.source.x;}) | |
.attr("y1",function(d){return d.source.y;}) | |
.attr("x2",function(d){return d.target.x;}) | |
.attr("y2",function(d){return d.target.y;}); | |
linksText.attr("x",function(d){ | |
return (d.source.x+d.target.x)/2; | |
}).attr("y",function(d){ | |
return (d.source.y+d.target.y)/2; | |
}); | |
gs.attr("transform",function(d) { return "translate(" + d.x + "," + d.y + ")"; }); | |
} |
注意,可以发现,这里写的都是位置信息,所以你在绘制相应的图形元素的时候,位置信息就不那么重要的,而且,建议先写完这个函数后,在进行测试
13、drag
function started(d){ | |
if(!d3.event.active){ | |
forceSimulation.alphaTarget(0.8).restart();设置衰减系数,对节点位置移动过程的模拟,数值越高移动越快,数值范围[0,1] | |
} | |
d.fx = d.x; | |
d.fy = d.y; | |
} | |
function dragged(d){ | |
d.fx = d3.event.x; | |
d.fy = d3.event.y; | |
} | |
function ended(d){ | |
if(!d3.event.active){ | |
forceSimulation.alphaTarget(0); | |
} | |
d.fx = null; | |
d.fy = null; | |
} |
drag 中有三个函数,在这里进行了实现,其中 d.fx 和 d.fy 表示固定坐标,例如,现在我们看到 dragged 函数,我们可以发现这样的代码:d.fx = d3.event.x; d.fy = d3.event.y;,也就是在拖动节点的时候,鼠标位置在哪里(d3.event),节点的固定位置就在哪里,再看到 ended 函数,也就是结束拖动的时候触发,可以发现,固定坐标都为空,也就是不固定,这样模拟的效果才好。
代码运行结果如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 (img-rIqq1tFr-1629179770623)(/download/attachments/41324300/1581173696042.png?version=1&modificationDate=1581176415000&api=v2)]
# 13、树状图
树状图,可表示节点之间的包含与被包含关系。
树状图知识点:
- d3.hierarchy (),层级布局,需要和 tree 生成器一起使用,来得到绘制树所需要的节点数据和边数据;
- d3.hierarchy ().sum () , 后序遍历;
- d3.tree (),创建一个树状图生成器;
- d3.tree ().size (),定义树的大小;
- d3.tree ().separation (), 定义邻居节点的距离;
- node.descendants () 得到所有的节点,已经经过转换的数据;
- node.links (),得到所有的边,已经经过转换的数据;
- d3.linkHorizontal (),创建一个贝塞尔生成曲线生成器,当然不止只有水平的,还有垂直的(d3.linkVertical ())
1、数据准备
// 定义边界 | |
var marge = {top:50, bottom:0, left:10, right:0}; | |
var svg = d3.select("svg"); | |
var width = svg.attr("width"); | |
var height = svg.attr("height"); | |
var g = svg.append("g") | |
.attr("transform","translate("+marge.top+","+marge.left+")"); | |
var scale = svg.append("g") | |
.attr("transform","translate("+marge.top+","+marge.left+")"); | |
// 数据 | |
var dataset = { | |
name:"中国", | |
children:[ | |
{ | |
name:"浙江", | |
children:[ | |
{name:"杭州" ,value:100}, | |
{name:"宁波",value:100}, | |
{name:"温州",value:100}, | |
{name:"绍兴",value:100} | |
] | |
}, | |
{ | |
name:"广西", | |
children:[ | |
{ | |
name:"桂林", | |
children:[ | |
{name:"秀峰区",value:100}, | |
{name:"叠彩区",value:100}, | |
{name:"象山区",value:100}, | |
{name:"七星区",value:100} | |
] | |
}, | |
{name:"南宁",value:100}, | |
{name:"柳州",value:100}, | |
{name:"防城港",value:100} | |
] | |
}, | |
{ | |
name:"黑龙江", | |
children:[ | |
{name:"哈尔滨",value:100}, | |
{name:"齐齐哈尔",value:100}, | |
{name:"牡丹江",value:100}, | |
{name:"大庆",value:100} | |
] | |
}, | |
{ | |
name:"新疆" , | |
children: | |
[ | |
{name:"乌鲁木齐"}, | |
{name:"克拉玛依"}, | |
{name:"吐鲁番"}, | |
{name:"哈密"} | |
] | |
} | |
] | |
}; |
可以发现,数据本来就以树的形式存储
2、创建一个层级布局
var hierarchyData = d3.hierarchy(dataset) | |
.sum(function(d){ | |
return d.value; | |
}); |
这时候如果你打印 hierarchyData 的话,会得到一个以树形式组织的树
3、创建一个树状图
// 创建一个树状图 | |
var tree = d3.tree() | |
.size([width-400,height-200]) | |
.separation(function(a,b){ | |
return (a.parent==b.parent?1:2)/a.depth; | |
}) |
4、初始化树状图,也就是传入数据,并得到绘制树基本数据
var treeData = tree(hierarchyData); |
5、得到边和节点(已经完成转换的)
var nodes = treeData.descendants(); | |
var links = treeData.links(); |
6、输出边和节点
// 输出节点和边 | |
console.log(nodes); | |
console.log(links); |
7、创建一个贝塞尔生成曲线生成器
var Bézier_curve_generator = d3.linkHorizontal() | |
.x(function(d) { return d.y; }) | |
.y(function(d) { return d.x; }); |
8、绘制边
// 绘制边 | |
g.append("g") | |
.selectAll("path") | |
.data(links) | |
.enter() | |
.append("path") | |
.attr("d",function(d){ | |
var start = {x:d.source.x,y:d.source.y}; | |
var end = {x:d.target.x,y:d.target.y}; | |
return Bézier_curve_generator({source:start,target:end}); | |
}) | |
.attr("fill","none") | |
.attr("stroke","yellow") | |
.attr("stroke-width",1); |
9、老规矩,先建立用来放在每个节点和对应文字的分组 <g>
var gs = g.append("g") | |
.selectAll("g") | |
.data(nodes) | |
.enter() | |
.append("g") | |
.attr("transform",function(d){ | |
var cx = d.x; | |
var cy= d.y; | |
return "translate("+cy+","+cx+")"; | |
}); |
10、绘制节点和文字
// 绘制节点 | |
gs.append("circle") | |
.attr("r",6) | |
.attr("fill","white") | |
.attr("stroke","blue") | |
.attr("stroke-width",1); | |
// 文字 | |
gs.append("text") | |
.attr("x",function(d){ | |
return d.children?-40:8; | |
}) // 如果某节点有子节点,则对应的文字前移 | |
.attr("y",-5) | |
.attr("dy",10) | |
.text(function(d){ | |
return d.data.name; | |
}) |
结果图:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传 (img-eJ16j6Bu-1629179770624)(/download/attachments/41324300/1581175195790.png?version=1&modificationDate=1581176415000&api=v2)]
# 参考资料:
- D3-Demo
- D3.js 入门教程
- D3.js API
- API