25

I'm struggling with understanding the merge function in D3, despite reading through the D3 API countless times.

The API says: "This method is commonly used to merge the enter and update selections after a data-join. After modifying the entering and updating elements separately, you can merge the two selections and perform operations on both without duplicate code."

Here's an example of the supposedly straightforward use of it, in a force directed chart, where the ticked function is called with every tick:

var simulation = d3.forceSimulation(nodes)
    .force("charge", chargeForce)
    .force("center", centerForce)
    .on("tick", ticked);

    function ticked() {

    var u = d3.select("svg").selectAll("circle").data(nodes)

    u.enter().append("circle").attr("r",5)
        .merge(u) // What is the merge function doing here?
        .attr("cx", d => d.x)
        .attr("cy", d => d.y)

    u.exit().remove() // Why is it necessary to remove excess objects w/ the exit selection?

    }

I understand how data-binding works, and how enter() and exit() selections work. However, I've never had to use a "merge" before, and I don't understand it is doing here. If someone could briefly walk through what is going on in this function step-by-step, that would be extremely useful. I'm sure others have similar questions.

Gerardo Furtado
  • 100,839
  • 9
  • 121
  • 171
Harrison Cramer
  • 3,792
  • 9
  • 33
  • 61
  • 1
    Where did you get that example? You should **not** append elements or rebind data in the tick function. – Gerardo Furtado Nov 02 '17 at 03:54
  • What's hard to understand, you have 2 selections and combine them into 1 selection. Just like if you have 2 arrays, A and B. You can concat them to make array C. – Eric Guan Nov 02 '17 at 05:29
  • 1
    @EricGuan what you said is not correct: `merge()` does not concatenate selections. The API states that explicitly: *"This method is **not intended for concatenating** arbitrary selections, however: if both this selection and the specified other selection have (non-null) elements at the same index, this selection’s element is returned in the merge and the other selection’s element is ignored."*. – Gerardo Furtado Nov 02 '17 at 05:48
  • @GerardoFurtado I got it here: http://d3indepth.com/force-layout/, why does it not make sense to do that? – Harrison Cramer Nov 02 '17 at 22:26
  • @HarryCramer this is quite easy to understand: why should you re-bind the data and compute the enter and exit selections dozens of times per second, **if the data doesn't change**? It makes no sense! I just wrote an answer explaining it, with the alternative code. – Gerardo Furtado Nov 03 '17 at 00:06

3 Answers3

21

The documentation explains very well what that function does, so what it does is instead of you having to do this

u.attr("cx", d => d.x)
 .attr("cy", d => d.y);

u.enter().append("circle").attr("r",5)
        .attr("cx", d => d.x)
        .attr("cy", d => d.y);

You can just call attr once like

u.enter().append("circle").attr("r",5)
        .merge(u) // after this point, any updates will apply to both u and u.enter() selections
        .attr("cx", d => d.x)
        .attr("cy", d => d.y)

It will set attributes cx and cy on both u-the update selection and u.enter()-the enter selection

Why is it necessary to remove excess objects w/ the exit selection?

Because the exit selection contains any extra DOM elements that were not bound to the elements in the array you passed to data(), you can do whatever you need on the exit colllection, for example setting the styles by calling u.exit().style(...), etc. instead of calling remove to delete them from the DOM

Trash Can
  • 6,608
  • 5
  • 24
  • 38
6

You actually have two issues here:

  1. Understanding the merge() method;
  2. Understanding that piece of code you shared;

Regarding #1 you already received an answer. Regarding #2, these are my two cents: that code does not make sense.

This is simple to understand: the ticked function runs dozens of times per second. Why would you re-bind the data and reassign the update, enter and exit selections dozens of times per second, if the data doesn't change? (it's worth mentioning that the author of that code is a good programmer, something strange has happened here... after all, we all make mistakes)

The ticked function just need to compute the positions of the elements, that's all.

Here is that same code you linked with the ticked function simplified to just this:

function ticked() {
    u.attr('cx', function(d) {
            return d.x;
        })
        .attr('cy', function(d) {
            return d.y;
        })
}

And here the running code:

var width = 600,
  height = 400;

var colorScale = ['orange', 'lightblue', '#B19CD9'];
var xCenter = [100, 300, 500]

var numNodes = 100;
var nodes = d3.range(numNodes).map(function(d, i) {
  return {
    radius: Math.random() * 25,
    category: i % 3
  }
});

var u = d3.select('svg g')
  .selectAll('circle')
  .data(nodes);

var enter = u.enter()
  .append('circle')
  .attr('r', function(d) {
    return d.radius;
  })
  .style('fill', function(d) {
    return colorScale[d.category];
  });

u = enter.merge(u);

u.exit().remove();

var simulation = d3.forceSimulation(nodes)
  .force('charge', d3.forceManyBody().strength(5))
  .force('x', d3.forceX().x(function(d) {
    return xCenter[d.category];
  }))
  .force('collision', d3.forceCollide().radius(function(d) {
    return d.radius;
  }))
  .on('tick', ticked);

function ticked() {
  u.attr('cx', function(d) {
      return d.x;
    })
    .attr('cy', function(d) {
      return d.y;
    })
}
<script src="https://d3js.org/d3.v4.min.js"></script>
<div id="content">
  <svg width="700" height="400">
    <g transform="translate(50, 200)"></g>
  </svg>
</div>
Gerardo Furtado
  • 100,839
  • 9
  • 121
  • 171
  • Thank you Gerardo, that was very helpful. I was confused why the re-selection was necessary, so your clarification was great. You never know with self-teaching when you're going to run across something like this. – Harrison Cramer Nov 04 '17 at 23:36
  • No worries. You can always email the author of that code asking why did he do that. As I said, it doesn't make much sense to me. – Gerardo Furtado Nov 04 '17 at 23:51
  • "The ticked function just needs to set the positions" - Thank you for that! – Nikhil Das Nomula Jan 24 '19 at 19:24
3

In view of the comments I should alert readers that this answer may be wrong. So please read the comments first.

(2023 edit:) this fiddle demonstrates that merge will combine elements and not union two collections: https://jsfiddle.net/gkutq7co/ so please don't upvote :)

TL;DR - merge makes two node collections into one

var x = d3.selectAll(".node");
var y = d3.selectAll(".link");
var z = x.merge(y);

z now contains all the elements in x AND all the elements in y.

user2728841
  • 1,333
  • 17
  • 32
  • 1
    thats not correct. x and y will share indexes, so you'll end up just with one of the two selections. To be precise z will be just the x selection – 40detectives Jun 23 '20 at 16:51
  • @40detectives I did test this code before publishing and it works great – user2728841 Jun 26 '20 at 12:25
  • 1
    I'm seeing the behavior described by @40detectives. – Michael Hoffmann Oct 02 '20 at 05:20
  • 1
    Sorry but this answer is definitely wrong, `merge()` is meant to combine the results of a `selectAll()` and `enter()`, filling in `null` entries from the first. To get a combination selection you would do: `selectAll('.node,.link')`. – chris Oct 13 '20 at 09:28