在交互式可视化中,有一个词叫reactive,指的是以可视化的方式来响应用户的行为,帮助用户进行可视化并理解其结果。这个很有用。那如何来实现这种交互呢?通过动画。
如果处理得当,动画可以展现出不错的可视化交互数据...
是怎样的呢?
- 交互可以有效地提供用户的反馈。我们可以知道自上次用户操作之后发生了什么变化?如果屏幕上的内容从一种状态变成另一种状态,动画可以使这一过程更加明显,突出而更具含义。另外,当需要显示任何形式的实时数据时,动画是必不可少的。
- 动画可以使人们更加关注图表的重要部分。我们的视觉对运动非常敏感,因此引入和使用这些transitions可以大大地帮助我们更加容易地从图表中获取到有用的信息。
比较下面两个图表:
哪一个更能让用户注意到柱状图中最后的那一个柱子呢?
[说明:以上两个图使用了相同的model。点击按钮可以开始一个动画。如果图中内容为空,点击按钮可以显示图表。]
- 动画可以和隐喻一起使用,比如增大,展开,移动,缩小等等。所以它可以真正提高可视化的表现力,并试图传达任何要表达的意思。
这就是说,动画也肯定会破坏你的可视化。这里有三个普遍问题。
- 动画非常突出。把注意力放在图表的一个特定的,明确的部分是很好的。但是当动画太多时会发生什么呢?没有其它的提示,用户很难将注意力集中到他们需要关注的地方。
- 动画跨越了多种状态(如动画数据视频),使得它们难以相互比较,这不同于我们将各种不同状态的静态图并排显示出来可以很容易地进行比对。
- 如果动画不连续,又或者图表忽然莫名其妙地消失,这就会导致一个变化盲视,从而大大地抵消了你希望从动画中能获得的效果。
看这个例子。
动画过程中线经过了一个空白状态,从而使我们很难去跟踪原始状态和最终状态之间的变化。找出变化的唯一方法是将注意力集中到一个点上,然后记住它的原始位置,但这样做效率太低。
现在我们怎么做呢?
我们已经看到动画在数据可视化中的作用了。现在我们来做吧!我们使用d3,它提供了多种数据动画的方式,使用它实现动画效果会相对容易一些。
原则
如果你知道如何在d3中绘图,你就知道如何实现动画。(如果你还不知道,Alignedleft有一个精彩的教程集,可以帮助你如何开始,d3站点也列出了很多,包括一些我提供的。)出于某种原因,动画在d3中被称之为transitions。动画在技术上被定义为,对象的一个或多个特征在经过一段时间之后,从一个值过渡(transitions)到了另一个值。
那么我们所说的特征指的是什么呢?它基本上代表了任何可以用数字表示的东西。
几个transitions的例子
不出所料,当你随着时间来平滑地更新item的位置时,它就移动了。在svg中,对大多数形状而言位置是确定的,例如我们这里的蓝色矩形,它的位置由属性x和y的值来确定,对应于形状的左上角。对于圆形,使用cx和cy或者中心点的坐标来确定位置。对于路径,例如我们的红色三角形,实际上通过"d"属性指定了所有点的位置。
同样,当你改变大小时,对象会增大(或收缩)。你可以使用width(宽)和height(高)来确定矩形的大小,或者使用r(半径)来确定圆形的大小。
颜色也是一个数字属性,从一种颜色过度到另一种颜色也是可能的(这个也很有用)。在svg中,颜色是由fill或stroke定义的样式属性。
与颜色一样,改变透明度也很有用。当opacity被设置为0时,对象是完全透明的。所以要实现对象的淡入淡出效果,需要对opacity属性进行transition操作。
如何实现
现在我们已经看到transitions可以用来些干什么了,让我们来看看如何用d3来编写代码。
我们回到第一个例子。我们尽量简单一点。
在d3中要创建一个像这样的方块,我们可以这样写:
var mySquare=svg.append("rect") .attr("x",60) .attr("y",60) .attr("width",60) .attr("height",60);
4个属性,很简单。
如果你想让它移动到右边,只需要更新属性x的值。像这样:
mySquare
.transition()
.attr("x",320);
就是这么简单:使用transition方法,然后指定你想要改变的值,就像创建一个新对象一样。通过这种方式,我们可以很容易地实现上面任何一个例子的效果。
mySquare .transition() .attr("width",120); // 将会变大 mySquare .style("fill","white") // 如果fill的值一开始是空白,然后再指定样式,那么动画将从黑色开始 .transition() .style("fill","blue"); mySquare .transition() .style("opacity",0);
我们的例子并非如此。Transitions发生在event之后,即当用户点击按钮时才开始动画。事实上,transitions通常会与事件和交互关联在一起。不过这并不复杂。我们可以这样写:
button.on("click", function() { mySquare.transition().attr("x",320); })
现在,我们的动画仅当按钮被点击时才开始。很明显,由于transition是在一个函数内部,所以我们可以通过编程来决定方块走向哪里。不过这个例子我们还是让它简单一点。
动画102
到目前为止,我们已经看到了如何在d3中实现一些简单的动画,甚至进行一些交互。正如我们所看到的,动画的实现方式和创建元素一样简单。好消息是,d3中的transitions非常灵活,同时也可以通过很多的技巧来进行自定义,而不用编写很复杂的代码。我们更多需要知道的是该如何来做。
在使用transition()方法之后,我们可以指定一个duration和delay的值。Duration是transition将要持续的毫秒数,而delay是动画在执行之前需要等待的毫秒数。写法:
mySquare.transition() .attr("x",320) .duration(1000) // 持续时间为1秒 .delay(100) // 延迟0.1秒执行
默认的duration是250ms,没有delay。
我发现250ms的duration时间有点太短了。大多数时候,我们都希望动画要明显,我自己经常将duration的值增加到500或1000.除非有特殊原因,动画的持续时间不应太长。如果你使用它们来可视化数据,你肯定不希望动画要花好几秒才将数据都显示出来。观察下面这两个例子(点击按钮开始)。
第二个是不是会让人抓狂?你很难相信它纯粹浪费了你25秒的时间。
缓动是一个技术名称,它实际上是一个将时间转换为属性值变化的函数。在前面的例子中,你可能已经注意到了,一开始的时候值变化得很慢,然后加快,然后又变慢。是的,这表明你可以使用不同的函数来获得不同的结果。我这里的例子只给出了3个缓动函数,而事实上还有很多的缓动函数。你可以自己编写缓动函数,这个不在本文的讨论范围。
写法类似于这样:
mysquare.transition() .attr("x",320) .ease("elastic")
(顺便说一下,修改属性和指定动画方式的顺序没有任何影响,你可以先使用.ease,后使用.attr)
对于路径对象,通过transition来更新每一个点的位置,你可以有效地改变路径的形状。这对于线图或任何一个路径图来说特别有意思。
像这样,如果你绘制的值发生了变化,你可以轻易地发现这些变化。相反,如果你只是清除并重绘,那么就很难发现数据的变化。在这几个例子中,路径的属性"d"的值被修改了(所以它们与最简单的例子本质上是不同的)。
有时(事实上经常会有),你希望在一个transition完成之后马上启动另一个transition。然而下面这种方式不起作用:
mysquare.transition() .attr("x",320); mysquare.transition() .attr("y",200);
你可能会认为方块会向右移动,然后再向下移动。但事实并非如此,它会开始一个向右的移动,然后紧接着启动第二个transition使其向下移动。由于这两个transition的duration相同并且都没有delay,因此第二个transition的效果更明显。
如果第二个transition有一个delay,并且比第一个transition的duration要小,那么第一个transition会持续一段时间直到第二个transition的delay时间到期。然后,第二个transition会接管第一个transition开始执行。然而这并不是你想要的,因为第一个transition完成到什么程度完全取决于用户的机器和浏览器等,而这些都是不可预知的(看下面一段的解释)。
那么给第二个transition一个精确的delay使其能够刚好对应上第一个transition的duration如何呢?这通常是可以的,但是delay和duration的值并非十分精准。启动一个transition需要一定的时间(在我的机器上大约需要15毫秒,但可能会有变化),因此我们很难通过这种方式将两个transitions无缝地连接起来。
在更加复杂一点的程序中,有时候好几个事件会尝试对同一个对象触发transition。在这种情况下,第一个过程会被启动然后运行,直到另一个transition开始。第二个transition会取代第一个transition。这意味着在第一个transition中被改变的属性值将会保留到第二个transition开始,这个值处于开始值和目标值之间。
如果你想要确保所有的transitions都能将其属性更新为要达到的值,那么你可能需要在第一个transition的后续transitions中重新指定属性值,就像这样:
mysquare.transition() .attr("x",320); mysquare.transition() .delay(250) .attr("y",200) .attr("x",320); // 即使第一个transition没有执行完,这里也会将x更新为320。
这里还有一个方法能确定将两个transitions连接起来。使用下面这种写法,一个事件可以精准地在一个transition结束时开始。这个事件可以是另一个transition(上面的例子就是这样)。
mysquare .transition() ... .each("end", function() { ... });
这里,由.each("end")引入的最后一行的回调函数中的内容会正好在transition结束时被触发。
那要怎么做呢?这里有3种常见的场景。
(顺便说一句,这里的例子和前面的例子没有什么不同,仅仅只是为了方便查看)。一种情况是在同一个item上启动另一个transition。这里,方块会向右移动,然后向下移动。
这里是实现的代码:
mysquare .transition() .attr("x",320) .each("end",function() { // 正如上面所说的,这是一个新的transition对象 d3.select(this). // 这里我们还可以有另一个.each("end") transition() .attr("y",180); });
另一种情况是在transition执行完后删除对象。这个很有用,特别是在创建大量临时对象时。一个有趣的组合动画是,当你将透明度一直减到0,使对象不可见,如果你不再需要它了,那么你就可以使用remove()方法来删除它。
mysquare .transition() .attr("x",320) .each("end",function() { d3.select(this). // 如上所述,这里我们将对象删除了 remove(); });
最后,我们可以创建一个新对象。我们可以通过这种方式添加一个特效。下面是一个例子:
这里,在transition结束时,创建了一个圆形,随后在圆形上开始了一个transition,并将透明度减少为0,然后删除该圆形。
这是最后一个带有多个效果合并的例子。
进一步
信不信由你,我们只是接触到了d3动画一些非常基础的东西。
还有两个transition的用处我们没有讲到,因为稍微有点复杂,所以这里我只是简单提一下。
到目前为止,我们看到的一直都是基于一个特定对象的属性的transition。例如我们将这个方块的x属性的值变到200。
但有时候我们需要根据一个变量值的变化来更新可视化图形中的许多部分。
我们可以通过.tween和.interpolate方法来实现。所有的方法在d3的文档中都有说明。
还有一个是d3 timer的使用,它允许重复调用一个函数,也可以用来创建动画。
我一直希望可以通过相对简单的代码和技术来实现你想要的东西,特别是对于连接多个transitions,以及在恰当的时刻添加和删除对象。要创建更强大的效果,还有很长的路要走!