D3库实践笔记之图表交互 |可视化系列36

对于前端可视化库来说,交互效果是其基本功能,需要有优雅的效果和简洁的API才能出彩,而如果一个前端可视化工具只能生成静态图表,绝对会显得格格不入,因为在前端拥有交互功能并不复杂。与图表的交互,是指图表元素能根据用户的键盘鼠标操作做出相应的反应,例如悬停高亮、缩放、漫游、拖动节点、点击涟漪效果等等。

对于HTML元素来说,要响应用户的行为,可以在图形元素上添加一个或多个事件监听器,当监测到对应行为时,执行某些响应代码。

事件监听器

JavaScript 有一个事件模型,在这个模型中,“事件”由发生的事情来触发,比如用户通过键鼠或触摸屏输入信息。大多数情况下,没人监听事件,事件就自生自灭,我们就无感知。而如果我们添加事件监听器后,触发对应的事件就能调用这个监听器的设置,具体来说就是执行某些代码。

D3的选择集有一个方法on(),用来设定事件的监听器。在可视化绘制时我们普遍用了var svg=d3.select("body").append("svg")或类似的代码,就可以使用以下代码给元素绑定事件监听器:

1
2
3
4
5
6
7
8
var rect=svg.selectAll("rect").data(ds).enter().append("rect")
.on("mouseover", function() {
//触发事件后执行一些操作
d3.select(this).style("fill","#BA5C25");
})
.on("mouseout", function(d) {
d3.select(this).style("fill","#1EAFAE");
});

以上代码可以给柱状图添加悬停高亮的交互效果,mouseover是事件名称,function()是监听器函数。当鼠标移动到某个柱子上时,触发一个mouseover事件,调用function()将所d3所选中的柱的填充色修改为设置的颜色。演示如下:

【gif】

为图表赋予交互能力只要两步:

  • 给选择集绑定事件监听器;
  • 定义响应行为。

键鼠事件

在交互中最常见的行为当然要属鼠标触发的,经典的鼠标行为有单机、双击、选中拖动等。常用的事件如下:

  • click:单击事件,鼠标单击某个元素触发,相当于mousedown和mouseup组合在一起;
  • dblclick:鼠标双击事件;
  • mouseover:鼠标的光标放在某元素上(悬停在元素上);
  • mouseout:光标从某元素上移出来时;
  • mousedown:鼠标按钮被按下;
  • mouseup:鼠标按钮被松开;

以下代码为图表标题添加了一个单击事件的监听器,当点击标题元素,会将标题加粗并在控制台输出当前标题文本;而如果当前是加粗的效果,点击后是变成非加粗文本,也就是点击会切换加粗和正常文本两种效果;

1
2
3
4
5
6
7
8
9
10
// var svg=d3.select("body").append("svg")  等等
svg.append("text").attr("x",200).attr("y",20)
.text("D3绘制柱状图").on("click", function() {
if (d3.select(this).style("font-weight")=="bold"){
d3.select(this).text("D3绘制柱状图").style("font-weight","normal");
}else{
d3.select(this).text("D3绘制柱状图-click").style("font-weight","bold");
}
console.log(d3.select(this).text());//输出标题文本
});

d3-click-title

键盘事件也很实用,特别是可以结合一些控制鼠标按键的自动化程序。键盘事件有三种:

  • keydown:当用户按下任意键时触发,按住不放会重复触发此事件,这一事件不会区分字母的大小写,例如“A”和“a”被视为一致;
  • keypress:当用户按下字符键(大小写字母、数字、加号、等号、回车等)时触发,按住不放会重复触发此事件,该事件就会区分字母的大小写;
  • keyup:当用户松开按键时触发,该事件不区分字母的大小写;

keydown和keypress事件的区别在于keydown用于任意键的事件,而keypress用于字符键,如果只需要处理字母数字类的响应,或是要对大小写字母分别处理的时候,使用keypress;如果要处理上下左右(↑→)、Shift、Ctrl等特殊键的输入,使用keydown。

随着各种移动设备的普及,触屏有着广泛的使用场景,无论是我们的手机还是触屏的显示器,触屏离我们很近。常用的触屏事件有以下三种:

  • touchstart:当触摸点被放在触摸屏上时,也就是触摸到某个元素;
  • touchmove:当触摸点在触摸屏上移动时;
  • touchend:当触摸点从触摸屏上拿开时;

我们可以为触摸事件配置点击事件以及拖动事件,也就是触摸有选中并拖动的效果。

缩放

通过d3.zoom().on("zoom", zoomed)配置缩放的交互,具体用法如下。需要说明的是在v3.x版本中是使用d3.behavior.zoom()创建缩放行为,而v5.x及之后的版本是d3.zoom(),不再有behavior这一层抽象;

给矩形和坐标轴添加缩放交互响应:

1
2
3
4
5
6
7
8
9
10
11
var zoom = d3.zoom()
.scaleExtent([0.1, 90])
//.translateExtent([[-100, -100], [60+ 90, 60+ 100]])
.on("zoom", zoomed);
svg.call(zoom);
function zoomed() {
var rects=svg.selectAll("rect");
rects.attr("transform", d3.event.transform);
tt.attr("transform", d3.event.transform);
gX.call(xAxis.scale(d3.event.transform.rescaleX(x)));
}

d3-zoom

绑定d3.zoom()的行为后,就具备了漫游的交互,zoom不仅仅可以放大缩小,还可以拖动元素进行漫游。

漫游是一种拖拽效果,但在力导向图等的交互中,我们希望有更纯粹的拖拽元素效果,因此d3也有d3.drag()用于创建拖拽行为。和zoom一样的,在v5.x版本中是使用d3.drag()而v3.x版本是使用d3.behavior.drag()。drag没有缩放功能。

drag和zoom一般通过call调用,写在svg.append("rect")语句中变成svg.append("rect").call(zoom),或者写svg.call(zoom)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
drag = simulation => {
function dragstarted(event) {
if (!event.active) simulation.alphaTarget(0.3).restart();
event.subject.fx = event.subject.x;
event.subject.fy = event.subject.y;
}

function dragged(event) {
event.subject.fx = event.x;
event.subject.fy = event.y;
}

function dragended(event) {
if (!event.active) simulation.alphaTarget(0);
event.subject.fx = null;
event.subject.fy = null;
}

return d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended);
};
var node = svg.append("g").attr("stroke", "#fff").attr("stroke-width", 1.5)
.selectAll("circle").data(nodes).join("circle")
.attr("r", 5).attr("fill", "#CAE9E0")
.call(drag(simulation));

[gif]

悬停文本标签

要实现鼠标悬停在图形元素上时显示其标签的tooltip效果,仍然使用选择集的on监听mouseover和mouseout事件,只是把响应的代码从修改选定的rect元素变成了增加文本标签元素,具体实现是可以选择加svg的<text>标签或者加HTML的<div>标签,按需使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
svg.selectAll("rect").data(dataset).enter().append("rect")
.attr("class","bar").style("fill","#1EAFAE")
.attr("x",function(d,i){return i*75+75 +"px";})
.attr("y",function(d){return (375-d*3.5) +"px";})
.attr("width",50).attr("height",d=>d*3.5 +"px")
.on("mouseover", function() {
d3.select(this).append("title").attr("id", "tooltip")
.text(function(d) {
return "Value of this bar: " + d;
});
})
.on("mouseout", function(d) {
d3.select("#tooltip").remove();
});

[gif]

过渡动画

过渡动画同样通过事件监听和缓动实现过渡效果和数据更新,实现友好的交互;还有便是用好.transition(),在方法链上,要把transition的调用插到选择元素之后,改变任何属性之前。transition()默认情况延迟(delay)为0ms,持续时长(duration)为250ms,可以自行设置这两个参数。

例如对一个矩形的变换应用过渡效果:

1
2
3
4
5
6
7
8
svg.append("rect")
.attr("fill","steelblue")
.attr("x",30)
.attr("y",30)
.attr("width",100)
.attr("height",30)
.transition() //在更新width之前调用
.attr("width",300);

和HTML元素交互

D3作为一个JavaScript库,自然可以和原生的HTML元素进行交互,例如响应按钮的点击事件,在html中配置了按钮和点击监测,<button type="button" onclick="update()"> 更新 </button>,点击按钮触发事件,在函数update里面调用d3的绘制代码,实现交互。

状态条是很实用的元素,通过状态条调节d3图表的参数,例如下面通过状态条调节绘制矩形的填充颜色,给状态条添加了onchange的事件监听器,有变化时更新矩形的颜色。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
//html中需要额外引入 <script src="https://unpkg.com/d3-simple-slider"></script>
var num2hex = rgb => {
return rgb.map(color => {
var s = color.toString(16);
if (s.length === 1) {
s= '0' + s;
}
return s;
}).join('');
};

var rgb = [0,175,174];
var colors = ['red', 'green', 'blue'];

var svg = d3.select('div#d3-slider').append('svg')
.attr('width', 600).attr('height', 400).append('g')
.attr('transform', 'translate(30,30)');
var box = svg
.append('rect')
.attr('width', 160)
.attr('height', 100)
.attr('transform', 'translate(400,30)')
.attr('fill', `#${num2hex(rgb)}`);

rgb.forEach((color, i) => {
var slider = d3
.sliderBottom()
.min(0)
.max(255)
.step(1)
.width(300)
.default(rgb[i])
.displayValue(false)
.fill(colors[i])
.on('onchange', num => {
rgb[i] = num;
box.attr('fill', `#${num2hex(rgb)}`);
d3.select('p#value-color').text(`#${num2hex(rgb)}`);
});
svg.append('g').attr('transform', `translate(30,${60 * i})`).call(slider);
});
d3.select('p#value-color').text(`#${num2hex(rgb)}`);

可视化结果输出

d3绘制的图像是svg或canvas对象,要将生成的可视化结果导出可以选择直接复制svg节点数据,从DOM里直接复制 SVG 代码,然后粘贴到文本文件里,命名为chart.svg,如果觉得麻烦可以用其他工具,导出的需求挺普遍,当然有大佬造了轮子,d3-downloadable是一个JavaScript库,用于下载绘制的svg图形,在html里引入后,在JavaScript代码里加入svg.call(d3.downloadable({width:w,height:h,filename: "filename"}));就可以下载svg文件了。而如果只需要图片,就可以直接用截图工具截图保存,例如在写这些笔记时,自己大部分图片都是直接截图的,部分svg图形在DOM里直接复制出来粘到文本文件里。

总结

交互是JavaScript可视化库的基本功能,一些封装的基于前端的Python库也都实现了缩放漫游、悬停文本标签等交互功能。d3实现交互效果并不复杂,只需要对选择集使用on(),设定事件的监听器,在监听器里写交互的代码,定义响应的行为。基础可视化实现挺简单,而深度交互的内容很多,如更优雅的过渡和渐变效果、更深入的适应触摸设备交互、迷你图加入悬停框等等,在之后的具体实践中深入学习。