instant values for d3 enter selection and transitioned for update
I'm trying to avoid repeating code for the enter and update selections in d3. I have successfully used merge
to get the right selection of all items that are present with the new data (after I have used append
to add the new ones). Now I want to set some attributes such as r
, cy
and cx
. I can do that on the merged selection but if I put them behind a transition
the items that are just appearing animate in from 0,0 and I want them to just appear in the right place.
What I'm hoping for is some way to write all the attribute setting lines into one function and one time pass in the enter() selection and the next time pass in the transitioned merge so that I only have to write the lines for setting cx
once. Here's what I have already followed by my guess at pseudo code
const valueCircles = this.chartGroup.selectAll('.valueCircle')
.data(values);
valueCircles.enter()
.append('circle')
.attr('stroke-width', '1px')
.attr('stroke', '#ffffff')
.attr('opacity', '1')
.attr('class', 'dots valueCircle')
.merge(valueCircles)
.transition()
.duration(100)
.attr('r', (d) => {
if (d.y < 0.5 ) {
return 5;
} else {
return 15;
}
})
.attr('cx', (d) => {
return timescale(new Date(d.x));
})
.attr('cy', (d) => {
return valueScale(d.y)
})
This is what I think I want to achieve
function setTheDynamicValues(aSelection) {
aSelection
.attr('cx', (d) => {
return timescale(new Date(d.x));
})
.attr('cy', (d) => {
return valueScale(d.y)
})
}
function updateGraph(newData) {
const valueCircles = this.chartGroup.selectAll('.valueCircle')
.data(newData);
setTheDynamicValues(valueCircles.enter());
setTheDynamicValues(valueCircles.transition().duration(100));
}
Another way of describing this would be to make the duration of the transition 0 for the entering elements and 100 for existing elements so that new ones appear correct and existing ones have a visible transition.
First of all, D3 selections are immutable: that merge
of yours is not doing anything. This would be the proper pattern:
//here, pay attention to "let" instead of const
let valueCircles = this.chartGroup.selectAll('.valueCircle')
.data(values);
//our enter selection is called "valuesCirclesEnter"
const valueCirclesEnter = valueCircles.enter()
.append('circle')
etc...
//now, you merge the selections
valueCircles = valueCirclesEnter.merge(valueCircles);
From that point on, valueCircles
contains both your updating and entering elements.
Now, back to your question:
An idiomatic D3 indeed has a lot of repetition, and that's a point lots of people complain about D3. But we can avoid some repetition with methods like selection.call
. Also, since you want a transition for the update selection but none for the enter selection, you can simply drop the merge
.
Here is a basic exemple, using a bit of your code:
const data1 = [{
x: 100,
y: 100
}, {
x: 230,
y: 20
}];
const data2 = [{
x: 10,
y: 10
}, {
x: 50,
y: 120
}, {
x: 190,
y: 100
}, {
x: 140,
y: 30
}, {
x: 270,
y: 140
}];
const svg = d3.select("svg");
draw(data1);
setTimeout(() => draw(data2), 1000);
function draw(values) {
const valueCircles = svg.selectAll('.valueCircle')
.data(values);
valueCircles.enter()
.append('circle')
.attr("class", "valueCircle")
.attr("r", 10)
.call(positionCircles);
valueCircles.transition()
.duration(1000)
.call(positionCircles)
};
function positionCircles(selection) {
selection.attr("cx", d => d.x)
.attr("cy", d => d.y)
}
<script src="https://d3js.org/d3.v7.min.js"></script>
<svg></svg>
In the snippet above, I'm positioning both the enter and update selections using a function called positionCircles
; however, while for the enter selection I'm passing a simple selection, for the update selection I'm passing a transitioning selection. But the function is the same:
function positionCircles(selection) {
selection.attr("cx", d => d.x)
.attr("cy", d => d.y)
}