首页 > 解决方案 > D3.js 中的 .append 和 .join 有什么区别

问题描述

这可能是一个愚蠢的问题,但我只是不知道何时在 D3 中使用 .join 以及何时使用 .append。
就像在这个块中一样,我不知道为什么要在这里使用 join 而不是 append 。

elements.selectAll('circle')
  .data(d=>d.values)
  //.append('circle')
  .join('circle')
  .attr("class","dots")
  .attr('r',2)
  .attr('fill',d=>colorScale(d['track']))
  .attr("cx", d=>dateScale(d['edate']))
  .attr("cy", d=>valueScale(d['record_time']));

有人可以帮我理解吗?

标签: d3.js

解决方案


TL;博士

selection.append 本身仅将单个子元素附加到调用它的选择中的每个元素(继承其父元素的数据)。selection.join() 依赖于数据:它执行进入/更新/退出循环,以便 DOM 中匹配元素的数量与数据数组项的数量相匹配。

您的代码建议您要使用 .enter().append("circle") 而不是仅使用 .append("circle"):这完成了 enter/update/exit 循环的 enter() 部分,即也通过使用 .join() 完成。

您可以使用加入或单独的进入/退出/更新选择来获得相同的结果,加入只是一种方便,如文档中所述:

此方法是显式通用更新模式的便捷替代方案,替代了 selection.enter、selection.exit、selection.append、selection.remove 和 selection.order。(文档

进入/更新/退出

当您看到selectAll()后面是.data()我们正在选择所有匹配的元素时,对于每个存在的元素,我们将数据数组中的一个项目绑定到它。.data() 的使用返回所谓的更新选择:它包含现有元素(如果有的话)以及绑定到这些现有项目的新提供的数据。

但是,如果所选元素的数量与项目的数量不匹配¹,则 .data() 创建进入选择或退出选择。如果我们有多余的数据项,那么我们有一个输入选择,为我们需要添加的每个项添加一个元素,以便拥有相同数量的 DOM 元素和数据数组项。相反,如果我们有多余的 DOM 元素,那么我们就有一个退出选择。

在更新选择上调用 .enter() 会返回输入选择。此选择包含占位符(“从概念上讲,输入选择的占位符是指向父元素文档的指针”),我们可以使用 .append("tagname") 添加我们需要的元素。

相反,在更新选择上调用 .exit() 会返回退出选择,通常使用 .exit().remove() 简单地删除它;

这种模式通常看起来像这样:

 let circle = svg.selectAll("circle")
    .data([1,2,3,4])

 circle.exit().remove();    

 circle.enter()
    .append("circle") 
    .attr...

  circle.attr(...

首先我们选择所有的圆圈,假设有 2 个圆圈可供选择。

其次,我们使用 selection.exit() 删除多余的元素:但是,由于我们有四个数据项并且只有两个匹配的 DOM 元素,所以没有什么要删除的,所以选择是空的,没有任何内容被删除。

第三,我们根据需要添加元素,以确保匹配的 DOM 元素的数量与数据数组项的数量相同。由于我们有四个数据项和只有两个匹配的 DOM 元素,因此输入选择包含两个占位符(指向父级的指针)。我们将圆圈附加到它们并根据需要设置它们的样式。

最后,我们使用包含两个预先存在的圆圈的更新选择,并根据新数据为它们设置样式。

通常我们希望新元素和现有元素的样式相同,因此我们可以使用 merge 方法来组合输入和更新选择:

 let circle = svg.selectAll("circle")
    .data([1,2,3,4])

 circle.exit().remove();    

 circle.enter()
    .append("circle") 
    .merge(circle)
    .attr(...

这稍微简化了我们的代码,因为我们不需要分别为 enter 和 update 复制样式。

加入

那么 .join() 是从哪里来的呢?它是为了方便。最简单的形式: .join("elementTagName") .join 将上面的代码替换为:

 let circle = svg.selectAll("circle")
    .data([1,2,3,4])
    .join("circle")
    .attr(...

在这里,join 方法删除了退出选择并返回合并的更新选择和输入选择(包含新圆圈),我们现在可以根据需要对其进行样式设置。它本质上是一种速记方法,可让您修改更简洁的代码,但在功能上与第二个代码块相同²。

你的代码

在您的代码中,您可以选择一个或多个元素(一个/一些父元素)。绑定的数据包含一个子数组,您希望使用它来创建子元素。为此,您向 .data() 提供该子数据数组:

parentElements.selectAll("circle")
  .data(d=>d.values)

您可以跟进.join():这将为每个父元素执行进入/更新/退出循环,以便它们每个都有适当数量的圆圈并返回所有这些圆圈的选择。

您不能只使用 .append() ,因为这会将一个圆圈附加到每个父元素,并返回这些圆圈的选择。这不太可能是预期的结果。

相反,如本答案顶部所述,您可以使用 .enter().append("circle") 以便正确使用该模式。

如果您只创建一次元素并且从不更新数据,则只需要 enter 选择,否则,您将需要使用 enter/update/exit 方法来处理多余的元素、多余的数据项和更新的元素。

归根结底,加入和进入/更新/退出之间的区别在于代码偏好、样式、简洁性,但除此之外,没有什么是你不能用另一个做的。


¹ 假设只为 .data() 提供一个参数 - 第二个可选参数是一个键控函数,它基于键匹配 DOM 元素和数据项。没有匹配数据的 DOM 元素被放置在退出选择中,没有匹配 DOM 元素的数据数组项被放置在输入选择中,其余的被放置在更新选择中。

² 假设 .join() 没有提供它的第二个或第三个参数,这样可以更精细地控制进入/退出/更新周期。


推荐阅读