d3 / d3-selection

Transform the DOM by selecting elements and joining to data.

Home Page:https://d3js.org/d3-selection

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

selection.join?

mbostock opened this issue · comments

How about formalizing the general update pattern?

Before:

const text = g.selectAll("text")
  .data(data);

text.attr("class", "update");

text.enter().append("text")
    .attr("class", "enter")
    .attr("x", (d, i) => i * 32)
    .attr("dy", ".35em")
  .merge(text)
    .text(d => d);

text.exit().remove();

After:

const text = g.selectAll("text")
  .data(data)
  .join(
    enter => enter.append("text")
        .attr("class", "enter")
        .attr("x", (d, i) => i * 32)
        .attr("dy", ".35em"),
    update => update
        .attr("class", "update"),
    exit => exit.remove()
  )
  .text(d => d);

Where selection.join returns the the appended-enter merged with update.

Challenges:

  1. Should selection.join be separate from selection.data? That seems fine, and is nice because selection.data takes an optional key function.

  2. Should selection.join take the element name as the first argument, so that it can materialize the enter selection, rather than having the enter callback materialize it? That means that selection.join would bake-in the behavior of selection.append, so you couldn’t use selection.insert… but it has a huge advantage in that the return value of the enter callback can now be ignored. If, on the other hand, the enter callback is responsible for materializing the enter selection, then it must return this materialized enter so that it can be merged with update. That makes it more error-prone, especially when you want to create nested elements on enter.

  3. Should selection.join call exit.remove for you automatically? This feels related to the previous question: if we append automatically, shouldn’t we remove automatically? And that’s tricky because when you have exit transitions, you want to call transition.remove rather than selection.remove, which would mean the exit callback would need to return whatever you wanted to be removed, and that would be a little error-prone again. So it feels like we can’t have symmetry and avoid pitfalls.

How would the following nested variant translate to the proposed API?

const groups = selection.selectAll('g')
  .data(colorScale.domain());
const groupsEnter = groups
  .enter().append('g')
    .attr('class', 'tick');
groupsEnter
  .merge(groups)
    .attr('transform', (d, i) => `translate(0, ${i * spacing})`);
groups.exit().remove();

groupsEnter.append('circle')
  .merge(groups.select('circle'))
    .attr('r', circleRadius)
    .attr('fill', colorScale);

groupsEnter.append('text')
  .merge(groups.select('text'))
    .text(d => d)
    .attr('dy', '0.32em')
    .attr('x', textOffset);

It's not immediately clear how one could tap into the return value for the enter selection, as is useful in the above example.

You wouldn’t be able to merge the entering child elements with the updating child elements as your example, but you could simply reselect them from the post-join parent elements.

const groups = selection.selectAll('g')
  .data(colorScale.domain())
  .join(
    enter => {
      enter = enter.append('g').attr('class', 'tick');
      enter.append('circle');
      enter.append('text');
      return enter;
    },
    () => {},
    exit => exit.remove()
  )
    .attr('transform', (d, i) => `translate(0, ${i * spacing})`);

groups.select('circle')
    .attr('r', circleRadius)
    .attr('fill', colorScale);

groups.select('text')
    .text(d => d)
    .attr('dy', '0.32em')
    .attr('x', textOffset);

Aha! Nice formulation.

I am pretty excited about how this is turning out and plan on releasing this in 1.4. Here’s a preview if you want to take a peek and give feedback:

https://beta.observablehq.com/d/6c522dda5dd9daa3

@mbostock I saw that you where using () => {} in the join statement. But the documentation states: selection.join(enter[, update][, exit])
[https://github.com/d3/d3-selection/blob/master/README.md#selection_join]
change it with update => update , this also resolved my error.

Besides that error this was quite a useful post

You wouldn’t be able to merge the entering child elements with the updating child elements as your example, but you could simply reselect them from the post-join parent elements.

const groups = selection.selectAll('g')
  .data(colorScale.domain())
  .join(
    enter => {
      enter = enter.append('g').attr('class', 'tick');
      enter.append('circle');
      enter.append('text');
      return enter;
    },
    () => {},
    exit => exit.remove()
  )
    .attr('transform', (d, i) => `translate(0, ${i * spacing})`);

groups.select('circle')
    .attr('r', circleRadius)
    .attr('fill', colorScale);

groups.select('text')
    .text(d => d)
    .attr('dy', '0.32em')
    .attr('x', textOffset);

Hi Mike, can you elaborate on how update and exit transitions can be chained on the child elements without interruption, in the above example? For example, in the case of circle radius would it just be a simple matter of making use a transition call before .attr('r', circleRadius)?

@orrery See the Observable notebook linked above.