Using D3.js’ Enter‑Update‑Exit Pattern to Keep Bar Charts in Sync
Learn how D3.js’ enter‑update‑exit pattern keeps bar charts in sync by adding, updating, and removing only the changed SVG elements.
29 Oct 2025, 00:45 UTC

Problem: Redrawing the whole SVG creates duplicates
When a bar chart is rebuilt from scratch each time new data arrives, every call to svg.selectAll('rect') creates a fresh set of <rect> elements. The old rectangles stay in the DOM unless you explicitly remove them, which leads to visual clutter, unnecessary layout work, and stale bars that never disappear.
Thesis: Let D3 handle the add‑modify‑remove cycle
D3’s data join (selection.data(data)) separates the concerns of entering new data, updating existing items, and exiting items that no longer have a match. By chaining enter, update, and exit you tell the library exactly what to do with each subset, keeping the SVG node count equal to the data length and animating only the changed parts.
How the join works
Three steps:
- Select the elements you want to bind:
svg.selectAll('rect'). - Data pass the array:
.data(dataset, key). The optionalkeyfunction tells D3 how to match old and new datum. - Modify the three returned selections:
enter– append new<rect>for datum without a DOM node.update– modify attributes of the matched nodes.exit– remove nodes whose datum disappeared.
Worked example: a button‑driven bar chart
<svg width="400" height="200"></svg>
<button id="updateBtn">New data</button>
<script>
const svg = d3.select('svg');
let data = [30, 80, 45, 60]; // initial dataset
function draw(dataset) {
const rects = svg.selectAll('rect')
.data(dataset, d => d); // key is the value itself (simple case)
// ENTER: new bars
rects.enter()
.append('rect')
.attr('x', (d, i) => i * 50)
.attr('y', d => 200 - d * 2)
.attr('width', 40)
.attr('height', 0)
.attr('fill', 'steelblue')
.transition()
.duration(500)
.attr('height', d => d * 2);
// UPDATE: existing bars
rects.transition()
.duration(500)
.attr('y', d => 200 - d * 2)
.attr('height', d => d * 2);
// EXIT: removing bars
rects.exit()
.transition()
.duration(500)
.attr('height', 0)
.remove();
}
// Initial render
draw(data);
// Button handler
document.getElementById('updateBtn').addEventListener('click', () => {
const newData = Array.from({length: 4}, () => Math.floor(Math.random() * 100));
data = newData;
draw(data);
});
</script>
Clicking the button replaces the dataset. Because we pass a key function (d => d) D3 can match rectangles that keep the same value, animate only the changed heights, and remove any values that disappeared. Open the page, open DevTools → Elements, and watch the number of <rect> tags stay equal to the current data length.
Trade‑off: choosing a stable key
The join relies on the key function to identify which DOM node belongs to which datum. If you omit the key or use the array index (function(d,i){return i;}), a reordering of the data causes D3 to reuse the wrong rectangles, leading to bars jumping to unexpected positions or leaving ghost bars. For objects with an identifier, use d => d.id. When the dataset is large (thousands of items), even the efficient join can trigger layout thrashing if you change properties that force a reflow (like width/height) inside the update loop. Mitigate this by batching attribute changes or using requestAnimationFrame for animations.
Actionable checklist
- Always supply a key function that uniquely identifies each datum.
- Verify the node count with the browser’s inspector after each update.
- Keep animations short and avoid layout‑forcing properties inside the update transition.
- Test with a dataset that changes size, order, and content to confirm enter, update, and exit behave as expected.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.