注:本文主要是在分享中使用,如果只是看本文的话最好是拿出编辑器跟着我的代码copy也好,对着敲也好,这样才更加了解我在说什么,完整代码在这,文章里面的链接也可以点击去试一试,放心,试出了问题才会想办法去解决~
D3是什么
D3的全称是Data-Driven Document,可见D3是一个基于数据来操作文档的js库。他可以帮助你使用HTML,CSS,SVG甚至是Canvas来展示数据。D3操作SVG的写法像是JQuery,可以对元素添加删除和属性设置,不过D3可以将数据绑定到DOM上,然后根据数据来计算DOM的展示效果,这就是D3的数据驱动。D3不依赖任何库,而且D3不只是一个单独的库,你可以按需加载D3中任意一个单独的模块去使用。
D3能做什么
我们可以看看D3的官方示例库,里面不但可以实现动画,交互,有我们常用的柱图,线图,甚至是一些复杂,另类的图表也可以实现出来。


其他图表库
说到可视化图表我们最常用的就是Echarts,这个大家都很清楚,上手难度非常低,但是定制化程度也低,往往有想要实现产品的一些特殊的需求,就需要绞尽脑汁的想各种奇淫巧技。
还有就是G2,我没用过,只是大概的去了解了一下,它把图表分成了,图形组件(坐标轴、图例、提示信息等)和几何形状(点、线、面),你可以把数据映射到任意的元素上,然后拼凑成想要的图表。显然自由度就比Echarts高一些,同时上手的难度也要高一些,适合团队根据ui需求封装成自己的图表库。
D3的话就我们可以在接下来内容中来了解了解。
D3的使用步骤
下面张图完美的诠释了使用D3去绘制一个图表的所有步骤。
- 先得要有数据,获得json或者csv的数据。
- 定义图表的尺寸和边距。
- 定义你要绘制图表的画布。
- 创建图表的比例尺,设置物理像素到图表数据的比例。
- 使用对应的图表绘制你的数据。
- 给图表添加一些额外的元素,坐标轴、图例、提示框等。
- 最后给你的图表加上相应的交互。
有了上述的步骤我们应该就了解了D3绘制一个图表的过程,接下来我们就一步步的来实现出我的的第一个D3图表。
手把手来做个图
基础架子搭建
首先我们引入D3,在引入了D3后就可以在控制台上输入d3看看我们有没有引入成功,同时可以看见D3下面包含了很多方法。
然后我们创建我们的SVG的元素用来绘制我们的图表,同时给定元素的大小。在script里面定义图表的大小和边距。
1 | <!-- 初始化代码 --> |
加载数据
架子搭好后我们可以先引入数据看看数据是什么样子的。从这开始我们就正式的接触D3了,数据引入我们使用的是d3-fetch,他提供了一系列加载数据的方法,可以支持你加载txt、csv、json等各种常用的文件类型。你可以单独引入d3-fetch进行使用,不过我们引入了D3就不单独来引入了。
1 | // 单独引入的例子 |
你可以发现csv方法返回的是一个Promise对象,在D3的v5.x版本采用了Promise来替代之前的异步回调的方式,如果使用的是之前的版本需要注意使用回调函数来获得数据,最好还是使用最新的版本来使用D3。
通过打印我们可以看到数据如下。
1 | [{ |
创建比例尺
在正式画图之前我们先要确认画图的比例尺,比例尺(scale)是D3中很重要的一个概念,举个简单的例子我们为什么需要比例尺。y轴一般用来展示数量的数据,我们预计绘制的图形有200px高,然后y轴的数据在0到10这个范围内,所以需要把0-10映射到0-200像素上面,0对应的高度是0像素,5对应的高度是100像素,10对应200。我们通过下面方法创建一个比例尺。
1 | // 使用线性的比例尺 |

由上图可见我们的映射效果,然后神奇的是创建后的比例尺myScale是一个方法,他接受一个需要映射的值返回映射后的值效果如下。
1 | const data = [0,2,3,4,7.5,9,10] |
我们把数字传入创建的myScale就会返回当前对应的位置,既然是比例尺也不会有范围的限制我们传入大于定义时的数字20得到的是符合比例的400。所以使用比例尺我们就可以很好的控制数据的展示位置和展示大小。

上面讲的是非常常用的线性比例尺,D3提供了大约12种不同类型的比例尺,主要分为了四大类。
- 线性数据映射到线性数据,就是我们刚刚说的,在echarts中数值轴和时间轴就是这一类映射。
- 线性数据映射到离散数据,比如说scaleQuantize,量化比例尺能够吧0到10的连续数据映射到四个颜色上,10被平均分成四段,每一段内返回的是当前段的颜色。具体效果如下,如果超出两端小于0是返回最左端的颜色,大于10是返回最右端的颜色。


- 离散数据映射到线性数据,scaleBand就是接下来做x轴映射的比例尺,我们通常理解为类目轴。效果就从下图就可以看得出来。

- 离散数据映射到离散数据,其实第三种离散到线性的映射,D3帮助你把映射后的线性数据算成了成离散的相同份数,效果上就是离散到离散的映射。
最后回到我们要绘制的图表我们采用scaleLinear作为y轴,scaleBand作为x轴,就很容易的得到下面的代码。
1 | // 创建比例尺 |
创建坐标轴
比例尺创建了后就接下来就可以绘制出坐标轴了。非常简单,通过d3.axisBttom创建x轴,d3.axisLeft创建y轴,传入比例尺就好。
然后我们创建放置坐标轴的容器(这块的用法在下一步进行讲解)然后通过call方法在容器中添加坐标轴。定义坐标轴返回的xAxis是一个方法,参数是挂载的元素,call方法作用是调用xAxis方法同时传入自身作为参数,同等于调用xAxis传入创建的g。这样就可以渲染出坐标轴了。
1 | // 定义坐标轴 |
聪明的大家肯定会想axisBottom是创建下面的轴,axisLeft是在左边的轴,但是创建的bar元素没有高度怎么能知道下面在哪,其实bottom、left表示的是轴刻度的位置不是坐标轴的位置。是下面,左边。把四个轴放出来就很清楚了。所以x轴需要设置偏移才能展示正确。
然后又发现y轴反过来了,因为默认原点是左上角,所以需要把y轴的映射domain或者range选一个反着传数据就好了。同时可以加上nice让分割更加均匀。


绘制图表
接下来我们开始我们的重头戏,图表的绘制。首先我们了解一下D3中怎么添加元素。
Selected
可以通过select选择一个元素,selectAll选择多个元素,这两个方法的参数是一个css选择器。就跟jQuery的一样,D3的选择器是可以链式调用的,选择了元素后会返回自身,继续选择或者设置元素的属性。举个例子。
1 | d3.selectAll('rect') |

我们可以通过attr设置属性,style设置样式,第一个是设置的属性,第二个参数可以是值,也可以是一个方法,在selectAll时选择的多个元素每一个都会执行一次这个方法,传入当前元素的数据和当前选择元素中的index(元素的的数据在后面说明)这样可以设置每一个元素的样式。
我们一般把select后的值称为Selection,打印Selection后发现他有一个_groups,里面就是我们选择到的元素,调用attr方法时会遍历每一个元素去设置属性。

selection.append可以添加元素,remove可以删除元素。添加元素后当前的Selection会指向添加后的元素。(demo)
1 | console.log( |
在我学到这里的时候我觉得可以画出柱形图了!先创建放柱子的容器,然后循环数据每次循环为一根柱子,分别设置
- x,y 比例尺算出的位置
- width bandwidth是散点比例尺每一项的宽度
- height 因为y轴反过来了,所以需要用总高度减去比例尺算出的高度
1 | const barGroup = bar.append('g') |
这是你会发现x轴宽度直接撑满了,非常不美观,所以需要在x的比例尺添加padding,0-1的数。


看起来效果非常的满意,回顾一下难道D3只是帮我操作了SVG的Dom吗?接下来我们来讲讲就是D3的灵魂。
数据驱动
首先是绑定数据,D3可以帮助你把数据绑定到Dom上,通过selected.datum可以绑定一个数据,通过selected.data可以绑定一串数据。之前用循环实现的柱图可以改写一下。或者看例子。可以看见元素的data属性上绑定了对应的数据。
1 | // data绑定实现 |
看上面的代码虽然说是把数据绑定到了data上,attr通过function的方式就可以拿到数据,绑定data然后通过function的方式应用数据,这就是D3的数据驱动。但是现在的方式前面得循环append矩形,不太友好。接下来我们介绍一种方式自动管理元素的方法。
Data-join
根据数据渲染元素我们会进行三种操作,如果数据增多会添加(enter)元素,数据减少会删除(exit)元素。如果相等的话元素数量不会更新,只是数据会进行更新(update).
通过data-join可以自动帮我们根据现有元素进行增删改操作,data-join的使用方式如下:
1 | // 父容器 |
改写一下我们的线图,只需要在data下面添加join就跟之前的效果一模一样。
1 | // datajoin方式 |
在这个例子里面,你可以注释或者添加html里面的circle标签,发现使用了join后最后的circle元素始终就是数据个数。
数据更新
大多数时候数据不是定死的,通常是会跟着时间变化的,在你数据变化的时候data-join并不会监听去改变,所以一般我们会把data-join封装成一个方法,数据变化后去触发。
讲到数据变化就会存在Dom重新渲染的问题,在Vue中我们使用v-for需要加上key防止只修改了一个数据的时候重新修改了整个列表。data-join同样也考虑到了这点,data的第二个参数就接受一个function返回key,在首次渲染的时候肯定是新增元素,在第二次渲染的时候,会通过你的key 方法先检测Dom上之前的数据,然后检测新的数据,通过你的返回值进行匹配,匹配上的就用原来的元素没有就新加(你在key方法里面打log会发现打印出两变,一次是以前的一次是新的)这样有两个小例子可以进去试试 没加key 加了key,在我们的柱图上也可以改一下试试。
1 | function update() { |
数据渲染我封装成了function,定了三秒钟第二次渲染新的数据,我这里有两个return一个是根据序号一个是更新x值,(第二次的序号我打乱了)按道理key值是使用x值才合理,但是我这里用序号效果也是一样的,原因是x轴位置是通过比例尺就算好了的根x值已经绑定了,所以看不出变化,这是就需要添加数据变化的动画看看Dom到底是怎么去改变的。
添加动画
于是就引出了本小节,动画非常简单,在需要变化的属性设置前加上transition即可。可以移动添加的位置确定在transition之后的操作才有动画。
1 | // 在上文join后面加上 |
添加交互
最后我们的图表还缺少点灵魂就是交互。D3的Selection提供了一个on的方法,允许监听鼠标键盘的事件,这里就跟写Dom的监听事件差不多了。简单的两个事件就可以实现出hover高亮的效果。
1 | .on('mouseenter', function() { |
来给你看几个厉害的
到这里我们的D3基本功能就讲完了一大部分了,用这些功能可以做出很多基本的图形了,不过D3的能量远不仅仅于此。下面简单讲几个复杂点的图形。(主要也是我也在学习中只能简单的带大家过一下)
线图
线图和柱图不同点是,他是线条需要用path路径来绘制,就是SVG中path元素的d属性,他是由直线曲线等各种命令组成的,不过D3提供了一个path路径的生成工具,大家感兴趣可以去了解d3-shape的api,线图他提供了d3.line生成,饼图可以使用d3.arc生成。
1 | const line = d3 |

地图
地图看上去非常难完成,其实最难的就是找到想要的地图数据了,其实就是根据地图的数据绘制出path路径。可以给大家炫耀一下我学了D3后第一个作业。

力导向图
力导向图用了D3的力布局还有一些作用力,我也还没弄懂就给大家看看例子吧,还有树图也是用了布局相关的知识。

Canvas
D3最重要的部分就是data-join数据绑定了,使用SVG很容易把数据绑定到元素上对他进行属性的更改和更新都很容易。而Canvas画布只有像素点是无状态的。我们选择不了画布上的元素进行更新。对于Canvas的局限性,这里提出了一些解决方案。
数据绑定
我们习惯join数据后append对应的元素然后就逐个设置样式,如果是使用Canvas每次更新都需要直接根据全量的数据数据来绘制画布。
1 | data.forEach(function(d) { |
但是在一些元素量非常大的情况下我们只能用Canvas来渲染,一般的解决方法是使用虚拟Dom来模拟data-join,然后我们就可以进行各种D3的操作。
1 | const vDOm = d3.select(document.createElement("custom")); |
这个例子就是在Canvas中模拟data-join的过程。
交互事件
Canvas最主要的问题是无状态,鼠标的交互只能获取在Canvas上的位置,没有元素的概念。不过也有几种方式去模拟点击元素的操作。
- 用四叉树来记录所有元素的位置然后根据点击的点去遍历。
- 在d3-force的布局中提供了.find方法,可以找到最接近鼠标点击的节点。
- 非常巧妙的办法,创建一个一样的画布,每个元素渲染一种颜色,通过点击到的像素点的颜色确定点击到的是那个元素。
ECharts主要也是用Canvas渲染,ECharts 使用的是 ZRender 底层渲染器。ZRender 提供了三种渲染器,分别是 Canvas,SVG 和 VML。下面是原文,总结一下就是根据鼠标位置计算出当前指向的元素。
在判断事件将被哪个图形元素响应的时候,我们会反向循环渲染列表,也就是先判断鼠标是否在位于屏幕前面的图形内。判断的时候,会先将鼠标坐标变换到图形坐标系。这是因为图形可能是经过平移旋转缩放的,甚至图形与图形之间还可能有组合的变换。将鼠标变换到图形坐标系后,我们就可以知道两者的相对关系,然后先根据包围盒做粗略的判断,再根据路径做精确的判断。如果鼠标在该图形内,则让这个图形进行事件分发与冒泡,如果不在,则对位于屏幕后方的元素一一判断。
api支持
D3的大部分模块不是和Dom进行交互的比如说d3-hierarchy用来绘制层级结构的图就只是给出了每个点的位置和每条线的路径。
对于d3-selection、d3-transition模块我们可以通过操作虚拟Dom来转换成Canvas。
对于d3-axis就跟SVG强相关,他会自动帮你构建SVG的坐标轴,这就没办法了。
d3-path来绘制线图饼图地图非常有用,不过D3也允许提供一个context可以帮你绘制在Canvas画布上。demo