熟悉的陌生人,揭开 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)

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步,也就是移除。仅仅是让有限的元素变换位置,这样的例子有很多,比较典型的就是散点图,或者聚类图,下面我们看一个单个节点的例子:


缓动.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的形式。看下面这个图:

缓动函数.gif

每个小方块代表着一种形式,从右移动到左。默认的形式就是Linear,也就是线性移动。它其实是利用了D3中的函数实现的,代码如下:

var data = [ 
    { 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的场景)

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 = 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的例子.
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 219,366评论 6 508
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,521评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 165,689评论 0 356
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,925评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,942评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,727评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,447评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,349评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,820评论 1 317
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,990评论 3 337
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,127评论 1 351
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,812评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,471评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,017评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,142评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,388评论 3 373
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,066评论 2 355