I have some data that occasionally has data removed, and added. This doesn't often, but can, happen in quick succession.
I've found that the dataGroups.exit().transition()....remove() 'overrides' the enter dataGroups.enter(...).append(...).transition().
It seems when an item is being removed, then 'enters' again, it fails to stop the removal process, so the item gets left either removed, or in the removed state (e.g. transparent or size 0).
Here's a working example, click the button slowly and it works as expected, but too fast and you'll get the lower bar missing when it should be there.
I've tried adding interupt() to try interrupt the exit animation but it doesn't seem to work. Can't find any reference to this, but I'm surprised it's not an issue someone has seen before and come up with a solution:
const dataset1 = [
{ name: "item 1", value: 200 },
{ name: "item 2", value: 100 }
];
const dataset2 = [{ name: "item 1", value: 100 }];
let currentDataset = undefined;
function refresh() {
const newDataset = currentDataset === dataset1 ? dataset2 : dataset1;
currentDataset = newDataset;
// Join new data with old elements, if any.
const dataGroups = d3
.select(".vis")
.selectAll(".box")
.data(newDataset, (d) => d.name);
// Remove old elements as needed.
dataGroups
.exit()
.transition("remove")
.duration(400)
.attr("opacity", 0.2)
.remove();
// Create new elements as needed.
const newGroups = dataGroups
.enter()
.append("rect")
.attr("class", "box")
.attr("height", 10);
newGroups.transition("add").duration(400).attr("opacity", 1);
// Merge and update
newGroups
.merge(dataGroups)
.attr("width", (d) => d.value)
.attr("y", (d, i) => i * 12);
}
document.getElementById("button").onclick = () => {
refresh();
};
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<!DOCTYPE html>
<html>
<head>
<title>Parcel Sandbox</title>
<meta charset="UTF-8" />
</head>
<body>
<button id="button">Update data</button>
<svg width="200" height="30"></svg>
<p>
Click the button slowly, and see it add and remove elements with a
transition
</p>
<p>
Click the button twice in quick succession, and see it doesn't correctly
represent the data
</p>
<p>It should look like:</p>
<pre>
|====
|==
</pre
>
<p>Or:</p>
<pre>
|==
|
</pre
>
<p>Never:</p>
<pre>
|====
|
</pre
>
<script src="src/index.js"></script>
</body>
</html>
https://codesandbox.io/s/focused-almeida-dp58f?file=/src/index.js
CodePudding user response:
First of all, it should be noticed that D3 selections are immutable, so using merge on your enter selection the way you're doing has no effect. Also, you should set the opacity of the enter selection to 0 if you want to properly see the transition.
Back to the question, the problem here is that the exiting bar is counted when you select all elements with box class, and because of that your enter selection is empty. The simplest solution is using selection.interrupt(). Contrary to what you said interrupt does work, but the issue is just that because you named your exit transition, you need to use the same name:
dataGroups.interrupt("remove");
Here is your code with that change:
const dataset1 = [{
name: "item 1",
value: 200
},
{
name: "item 2",
value: 100
}
];
const dataset2 = [{
name: "item 1",
value: 100
}];
let currentDataset = undefined;
function refresh() {
const newDataset = currentDataset === dataset1 ? dataset2 : dataset1;
currentDataset = newDataset;
// Join new data with old elements, if any.
let dataGroups = d3
.select(".vis")
.selectAll(".box")
.data(newDataset, (d) => d.name);
dataGroups.interrupt("remove");
dataGroups.attr("opacity", 1);
// Remove old elements as needed.
dataGroups
.exit()
.transition("remove")
.duration(400)
.attr("opacity", 0.2)
.remove();
// Create new elements as needed.
const newGroups = dataGroups
.enter()
.append("rect")
.attr("class", "box")
.attr("height", 10)
.attr("opacity", 0)
newGroups.transition("add").duration(400).attr("opacity", 1);
// Merge and update
dataGroups = newGroups.merge(dataGroups);
dataGroups.attr("width", (d) => d.value)
.attr("y", (d, i) => i * 12);
}
document.getElementById("button").onclick = () => {
refresh();
};
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<!DOCTYPE html>
<html>
<head>
<title>Parcel Sandbox</title>
<meta charset="UTF-8" />
</head>
<body>
<button id="button">Update data</button>
<svg width="200" height="30"></svg>
<p>
Click the button slowly, and see it add and remove elements with a transition
</p>
<p>
Click the button twice in quick succession, and see it doesn't correctly represent the data
</p>
<p>It should look like:</p>
<pre>
|====
|==
</pre
>
<p>Or:</p>
<pre>
|==
|
</pre
>
<p>Never:</p>
<pre>
|====
|
</pre
>
<script src="src/index.js"></script>
</body>
</html>
Also, I put a dataGroups.attr("opacity", 1); just after interrupting the transition, so we return the opacity of the fading bar to 1.
