【译】Learn D3 入门文档:Joins

461 阅读7分钟

引子

Learn D3: Animation 第七篇,只是英文翻译,可修改代码的部分用静态图片替代了,想要实时交互请阅读原文。

正文

如果你熟悉此教程中的 D3 ,你可能会惊讶于 D3 选择器被提及的如此之少。

那是因为你可能不需要它们!

D3 选择器适用一个特殊的定位:快速,动态图表的增量更新。如果你关注的是静态图表,或者每帧都可以从头开始重新绘制的图表,那么你可能更喜欢不同的抽象。另一方面,如果你想要动画过渡或从现代浏览器中榨取最佳性能,选择器适合你。

(即使你决定不使用选择器,请记住 D3 还有大量其它有用的可视化工具。比例尺、形状、插值器、颜色、地图投影和许多其它功能可用于 Canvas、WebGL 或其它 DOM 抽象,如 Observable 的 HTML 标记模板文字、React 或 Svelte 。D3 还可以使用统计、分组和聚合、时间序列和解析的方法,协助数据清理和分析。)

本质上,D3 选择器指定的是转换而不是表示。你可以指定将当前状态转换为所需状态所做的更改(插入、更新和删除),而不是表示图表(DOM)的所需状态。这有时很乏味,但允许你设置过渡动画,并最小化对 DOM 的更改,从而提高性能。

让我们看看如何做。

假设我们想显示字母表中的字母。虽然不是很直观,但我们将保持简单,以专注于技术(有关实际的示例,请参见 D3 图库 的动画和交互部分。)

92-1

这个图表是静态的,因为它是在每次单元运行时从头开始创建的,这使得 D3 代码在本质上等同于 HTML 文本。

92-2

那么,何必要用选择器呢?对,对于静态图表,没有什么理由这样做。

但假设你想更新此图表以响应不断变化的数据。并且你不想从头开始重新绘制,希望应用最小的更新集来反映新数据。你希望重用现有元素,添加需要的元素,删除不需要的元素。只需将上述代码移动到一个在数据更改时调用的方法中,你就可以获得高性能的增量更新!😲

92-3

让我们把代码分析一下。

text 是一组文本元素,最初为空,其父元素是 SVG 元素。这个父项决定了输入的文本元素稍后将附加到何处。

通过调用 selection.data ,text 被绑定到一个新的数据数组 letters 。这将计算 text 选择集的三个子集:enter 选择集表示在新数据中,去除已存在元素与新数据的交集之后,剩下的数据;update 选择集表示新数据与已存在元素的交集;exit 选择集表示在已存在元素中,去除已存在元素与新数据的交集之后,剩下的元素。

作为图示如下:

92-4

(这些选择集隐藏在代码中:selection.data 返回 update 选择集,从中可以调用 selection.enter 或 selection.exit 访问其它选择集。)

我们可以手动处理这三种情况,但 selection.join 本就能方便的处理。enter 选择集添加进来;exit 选择集被移除;最后, updateenter 选择集被合并、排序并返回。然后,我们可以为这些加入或更新元素分配属性和文本。

我们观察到只要字母和文本元素之间的关联保持不变,就不需要在更新元素时重新分配某些属性和文本内容,这样就可以更高效。要保留此关联,selection.data 需要一个键函数;为了对 enterupdateexit 的精确操作,selection.join 需要相应的函数。如果 update 比 enter 和 exit 更常见,这将大大提高性能!

92-5

与前面一样,selection.join 返回合并的 enter 和 update 选择集,因此我们可以共享应用于两者的代码,例如设置 x 属性。

传递给 selection.data 的键函数用于计算每个新数据和已选元素数据的(字符串)键,确定哪个数据绑定到哪个元素:如果元素和数据具有相同的键,则该数据绑定到元素,并且元素被放入 update 选择集。字母是很好的键,因此标识函数(d => d)在这里是合适的。

(如果未指定键函数,则数据由索引绑定:第一个数据绑定到第一个元素,依此类推。作为练习,尝试重写上面的代码以通过索引联接。你将会也想把设置 x 属性与设置文本内容交换!)

然而,选择器真正闪耀的地方是过渡!✨

在下面,字母从顶部进入,更新时水平滑动,然后从底部退出。这比上面的瞬时转变更容易理解。

92-6

好的过渡不仅仅是为了吸引注意力而让图表“舞动”;它们帮助观看者通过变动了解数据是如何变化的

良好的过渡保持对象的恒定性:在过渡之前表示特定事物(如字母 C )的图表元素应在整个过渡过程中和过渡之后表示相同的事物,从而允许观看者连续跟踪。相反,如果给定元素的含义在过渡期间发生变化,那么变动将毫无意义。

举个更实际的例子怎么样?

下图显示了按特定年龄组人口百分比排列的前十个州(以及华盛顿特区,外地居住者)。这表明犹他州的年轻人口比例过高,反映了 LDS 教会对养育家庭的重视。相比之下,佛罗里达州退休人口众多,许多人都在 70 岁或以上。

当你更改选定的年龄组时,请观察图表如何重新排序以反映排名的变化。x 轴同时重新缩放以适应新数据。

92-7

chart = {
  const svg = d3.create("svg")
      .attr("viewBox", [0, 0, width, height]);
  console.info(data)

  // For the initial render, reference the current age non-reactively.
  const agedata = viewof agedata.value;

  const x = d3.scaleLinear()
      .domain([0, d3.max(agedata, d => d.value)])
      .rangeRound([margin.left, width - margin.right]);

  const y = d3.scaleBand()
      .domain(agedata.map(d => d.name))
      .rangeRound([margin.top, margin.top + 20 * data.names.length]);

  let bar = svg.append("g")
      .attr("fill", "steelblue")
    .selectAll("rect")
    .data(agedata, d => d.name)
    .join("rect")
      .style("mix-blend-mode", "multiply")
      .attr("x", x(0))
      .attr("y", d => y(d.name))
      .attr("width", d => x(d.value) - x(0))
      .attr("height", y.bandwidth() - 1);

  const gx = svg.append("g")
      .call(xAxis, x);

  const gy = svg.append("g")
      .call(yAxis, y);

  return Object.assign(svg.node(), {
    update(agedata) {
      const t = svg.transition().duration(750);

      gx.transition(t)
          .call(xAxis, x.domain([0, d3.max(agedata, d => d.value)]));

      gy.transition(t)
          .call(yAxis, y.domain(agedata.map(d => d.name)));

      bar = bar
        .data(agedata, d => d.name)
        .call(bar => bar.transition(t)
          .attr("width", d => x(d.value) - x(0))
          .attr("y", d => y(d.name)));
    }
  });
}

92-9

只有前 10 条可见,因为其余的隐藏在图表下方。因此,selection.join 是不需要的,因为没有条形加入或移除,它们只会更新。这不仅简化了代码,而且使过渡更有意义,因为进入或退出条形图的速度现在暗示它们在展示外的位置。

动画过渡通常由读者点击或在寻找答案时点击触发。接下来,让我们看看如何让图表响应此类查询。

Next

附录

92-10

根据源码,去除了平台依赖,提取了主要代码,有以下示例:

参考资料

wastebasket

《巫师 3》主线剧情一般难度完成,最后触发的是个悲剧结局。

再去看了别人的攻略,才发现前面很多任务的选择都影响到了这个结局。

自己之前自认为正当的选择,到最后却导致悲剧收场。

心情还是有些郁闷的,在做选择的时候明显的代入了自己的想法。

巫师3 游戏本地文件艺术设定集。

92-poster