熟悉的陌生人,揭开|熟悉的陌生人,揭开 d3 transition 的面纱

d3是一个Js库,它是基于Svg来绘图,提供丰富的Api接口,国产的EchartsHeightChart能提供的图,它都能实现,非常强大。d3也是可视化可视分析领域一个重要的工具。今天本文要讲的是D3中的一个功能:Transition
官方关于Transition的定义 官方是英文的,就一段话:
------D3’s focus on transformation extends naturally to animated transitions. Transitions gradually interpolate styles and attributes over time. Tweening can be controlled via easing functions such as “elastic”, “cubic-in-out” and “linear”. D3’s interpolators support both primitives, such as numbers and numbers embedded within strings (font sizes, path data, etc.), and compound values. You can even extend D3’s interpolator registry to support complex properties and data structures.
Google翻译过来是:
------D3对转换的关注自然会扩展到动画转换。 过渡会随着时间逐渐插入样式和属性。 可以通过诸如“弹性”,“三次进出”和“线性”之类的缓动功能来控制补间。 D3的插值器同时支持数字和字串等原语,例如数字和嵌入字符串中的数字(字体大小,路径数据等)。 您甚至可以扩展D3的插值器注册表以支持复杂的属性和数据结构。
简单来说,就是很强大。基于原生的Html的Animation,不会有什么兼容性的问题。
Nick Zhu 的渐变柱状图 加拿大的朱哥(Nick Zhu)写过一本关于D3的书(D3 4.X 数据可视化实战手册),强烈推荐想学的同学去看看。有一章专门讲了渐变(transition),下面我借花献佛,学习一下其中的一个例子。首先看一下最终效果:
type1.gif 可以看到右侧的柱子从0到1 逐渐变大,并且移动。这个效果就是运用了Transition,那到底是怎么实现的呢?
大概分3步:

  1. 数据输入(Enter)
  2. 数据更新,执行Transition(Update)
  3. 退出(Exit)
【熟悉的陌生人,揭开|熟悉的陌生人,揭开 d3 transition 的面纱】1. 数据输入(Enter)
根据数据Append元素,指定元素的位置和大小。与其他例子不同的是这里没有用到SVG元素而是单纯地对普通Dom节点进行Transition
var selection = d3.select("body") .selectAll("div.v-bar") .data(data, function(d, i) { return d.id; }); //Enter selection.enter() .append("div") .attr("class", "v-bar") .style("z-index", "0") .style("position", "fixed") .style("top", 200 + "px") .style("left", function(d, i) { return barLeft(i + 1) + "px"; }) .style("height", "0px") .append("span");

伪代码假定已经定义了Barleft方法 和数据 Data。这个时候不会有任何效果,只是添加了一排柱状图的位置。
2. 数据更新,执行Transition(Update)
这一步,就是指定新的位置,然后利用Transition特性,让元素从原来的位置缓动到新的位置。代码很简单:
selection .transition().duration(duration) .style("top", function(d) { return 200 - barHeight(d) + "px"; }) .style("left", function(d, i) { return barLeft(i) + "px"; }) .style("height", function(d) { return barHeight(d) + "px"; }) .select("span") .text(function(d) { return d.value; });

伪代码假定已经定义了Barleft方法、barHeight方法和duration。到这一步,图中的效果就有了,你可以看到柱子会向左移,但是,不是很完美,最左侧的柱子会叠在一起:

type2.gif
那么怎样解决左边的堆叠呢,其实我们需要的就是让他“消失”。
3. 退出(Exit)
让它消失其实就是remove,但是,为了优雅地”消失“,我们还需要用到Transition:
selection.exit() .transition().duration(duration) .style("left", function(d, i) { return barLeft(-1) + "px"; }) .remove();

原理就是让Dom元素先优雅地移动到左边隐藏,然后Remove掉。怎么样,是不是很简单,柯柯~
另一种形态 很多时候,并不会用到上面的第3步,也就是移除。仅仅是让有限的元素变换位置,这样的例子有很多,比较典型的就是散点图,或者聚类图,下面我们看一个单个节点的例子:

熟悉的陌生人,揭开|熟悉的陌生人,揭开 d3 transition 的面纱
文章图片
缓动.gif 图中点做来回运动,并没有Remove操作,只是让它缓动,重复两次即可实现这种效果:
function repeat() { dom.attr('cx', 40) .attr('cy', 250) .transition() .duration(2000) .attr('cx', 920) .transition() .duration(2000) .attr('cx', 40) .on("end", repeat); };

假定伪代码中Dom已定义,几行代码搞定的效果,是不是比Echarts强大多了呢,柯柯~
缓动函数 还有另一个跟Transition相关的概念,介绍给大家,它就是缓动函数。 用大白话说就是,元素A从位置B移动到位置C的形式。看下面这个图:

熟悉的陌生人,揭开|熟悉的陌生人,揭开 d3 transition 的面纱
文章图片
缓动函数.gif
每个小方块代表着一种形式,从右移动到左。默认的形式就是Linear,也就是线性移动。它其实是利用了D3中的函数实现的,代码如下:
var data = https://www.it610.com/article/[ { name:'Linear', fn: d3.easeLinear }, { name: 'Cubic', fn: d3.easeCubic }, { name: 'CubicIn', fn: d3.easeCubicIn }, { name: 'Sin', fn: d3.easeSin }, { name: 'SinIn', fn: d3.easeSinIn }, { name: 'Exp', fn: d3.easeExp }, { name: 'Circle', fn: d3.easeCircle }, { name: 'Back', fn: d3.easeBack }, { name: 'Bounce', fn: d3.easeBounce }, { name: 'Elastic', fn: d3.easeElastic }, { name: 'Custom', fn: function (t) { return t * t; } } ], ....... d3.selectAll("div").each(function (d) { d3.select(this) .transition().ease(d.fn) .duration(1500) .style("left", "10px"); });

所以,上面的例子都可以用这些缓动函数。除了这些之外,还有个概念是 中间帧(tween) 和级联过渡,感兴趣的同学可以自行搜索一下,这里就不再细说了。
Transition 源码浅析 所以,这个东西是怎么实现的?让我们打开Github来看看。
对于Transition,它定义了一个Function
selection.prototype.transition = selection_transition; function selection_transition(name) { var id,timing; if (name instanceof Transition) { id = name._id, name = name._name; } else { id = newId(), (timing = defaultTiming).time = now(), name = name == null ? null : name + ""; } for (var groups = this._groups, m = groups.length, j = 0; j < m; ++j) { for (var group = groups[j], n = group.length, node, i = 0; i < n; ++i) { if (node = group[i]) { schedule(node, name, id, i, group, timing || inherit(node, id)); } } } return new Transition(groups, this._parents, name, id); }Transition.prototype = transition.prototype = { constructor: Transition, select: transition_select, selectAll: transition_selectAll, ...... style: transition_style, styleTween: transition_styleTween, tween: transition_tween, delay: transition_delay, duration: transition_duration, ease: transition_ease };

挂载在原型上方法有很多属性,在Selection集合上为每个节点挂载Transition。通过schedule为它们分配Duration,控制调用频率,通过tween方法去设置每个Style属性,实现视觉上的缓动。
整个Transition的流程大概如下(这里只讲了它的一个场景,他还有attrTween的场景):

熟悉的陌生人,揭开|熟悉的陌生人,揭开 d3 transition 的面纱
文章图片
d3transition
所以, 它根Css3transition还是有很大区别的,实质上是去多次设置元素的Style,让它达到变化的效果。这几个步骤中,其他几步都很好理解的,这里就把 Tween这里贴出来看一下:
function styleTween(name, value, priority) { function tween() { var node = this, i = value.apply(node, arguments); return i && function(t) { node.style.setProperty(name, i(t), priority); }; } tween._value = https://www.it610.com/article/value; return tween; }

每个Tween实质上都是去做一件事,设置属性(setProperty). 通过 ScheduleTimer方法去控制它的调用频率,达到缓动效果。
最后 看完此文,是不是对这个属性不那么陌生了呢,相对Css3Transition的封装,我觉得这个理解起来更直接,至少让我们看到了它的实现过程。 喜欢我的文章就点个关注,一起进步,柯柯~
注:本文所有关于D3的实例均基于V4版本。
本文转自我的CSDN博客:(https://blog.csdn.net/kingbox000/article/details/107472851),转载请注明出处。
参考:
  1. Looping a transition in v4.
  2. d3 transition.
  3. Nick Zhu的例子.

    推荐阅读