Quentin Roy
Quentin Roy

Reputation: 7887

Merging transitions and normal selections with D3

I find myself repeating the following pattern over and over with D3:

const chart = d3.select('#chart');

const update = updateData => {
  const circle = chart.selectAll(".circle").data(updateData);

  // New elements.
  circle.enter().append("circle")
    .attr("class", "circle")
    .attr("cx", d => d.x)   // |
    .attr("cy", d => d.y)   // | <-
    .attr("r", d => d.r);   // |   |
                            //     |
  // Animated update.              | Same stuff :(
  circle.transition()       //     |
    .attr("cx", d => d.x)   // |   |
    .attr("cy", d => d.y)   // | <-
    .attr("r", d => d.r);   // |
};


/*** Code bellow is just for the demo ***/

const ALL_DATA = [
  [{ x: 10, y: 2, r: 1 }, { x: 30, y: 6, r: 4 }],
  [{ x: 5, y: 7, r: 3 }, { x: 10, y: 2, r: 2 }]
];
let currentData = 0;

d3.select('#switch').on('click', () => {
  currentData = (currentData + 1) % ALL_DATA.length;
  update(ALL_DATA[currentData]);
});

update(ALL_DATA[currentData]);
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>

<svg id="chart" viewBox="0 0 50 10" />

<button id="switch">Switch</button>

Notice how setting up the center and the radius of the circle is the same both when entering and updating? This is just a few lines for the example here, but it grows quickly.

When the update is not animated, one can just merge the two selections and everything is fine. But if you want to go fancy and want the update animated, you can't anymore because apparently, merging a transition with a selection will not work...

Is there any way to make this DRY?

Upvotes: 1

Views: 375

Answers (1)

Andrew Reid
Andrew Reid

Reputation: 38151

A merge between a transition and a selection wouldn't be possible without altering d3's innards, this probably would be more complex and less clear than other solutions which do not modify both transition and selection with the same single method chain.

Perhaps this is a bit better, create a function to position the circles and set their radii:

function position(selection) {
  selection.attr(...)...
}

And then just call it on newly entered elements and on the transitions.

const chart = d3.select('#chart');

const position = function(selection) {
  selection.attr("cx", d=> d.x)
    .attr("cy", d=>d.y)
    .attr("cx", d=>d.x)
    .attr("r", d=>d.r);
}

const update = (updateData) => {

  const circle = chart.selectAll(".circle").data(updateData);

  // New stuff.
  circle.enter().append("circle")
    .attr("class", "circle")
    .call(position);

  // Animated update.
  circle.transition()
    .call(position);
}

const ALL_DATA = [
  [{ x: 10, y: 2, r: 1 }, { x: 30, y: 6, r: 4 }],
  [{ x: 5, y: 7, r: 3 }, { x: 10, y: 2, r: 2 }]
];
let currentData = 0;

d3.select('#switch').on('click', () => {
  currentData = (currentData + 1) % ALL_DATA.length;
  update(ALL_DATA[currentData]);
});

update(ALL_DATA[currentData]);
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>

<svg id="chart" viewBox="0 0 50 10" />

<button id="switch">Switch</button>

Alternatively, and perhaps less clear, you could shoehorn the transition to accomplish the same effect as your snippet (where you are visibly transitioning some elements while not others). Merge the selections without positioning the circles on enter, set the duration of the transition to be zero if the circle has not yet had its cx,cy set to non-zero values (or some other check depending on circumstance).

Yes, everything is run through a transition, but entered elements will not visibly transition, acting the same as if modified through a selection:

const chart = d3.select('#chart');

const update = (updateData) => {

  const circle = chart.selectAll(".circle").data(updateData);

  circle.enter().append("circle")  
    .attr("class", "circle")
    .merge(circle).transition()   
    .attr("cy", d=>d.y)
    .attr("cx", d=>d.x)
    .attr("r", d=>d.r)
    .duration(function() {      
      return d3.select(this).attr("cx") ? 750 : 0
    })
}

const ALL_DATA = [
  [{ x: 10, y: 2, r: 1 }, { x: 30, y: 6, r: 4 }],
  [{ x: 5, y: 7, r: 3 }, { x: 10, y: 2, r: 2 }]
];
let currentData = 0;

d3.select('#switch').on('click', () => {
  currentData = (currentData + 1) % ALL_DATA.length;
  update(ALL_DATA[currentData]);
});

update(ALL_DATA[currentData]);
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>

<svg id="chart" viewBox="0 0 50 10" />

<button id="switch">Switch</button>

Upvotes: 1

Related Questions