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