Reputation: 135
I want to update the DOM containing children with data propagation.
The first time when DOM is populated, the data is properly propagated to their children. However when I update the parent DOM with new data, the children DOM doesn't update along with it.
Also surprisingly, the old data were retrievable from the children elements, which I am strongly against because the data is too huge and I don't want to have old/extra data persist, which holds the reference and retains the old data. (Memory leak in my sense)
const DATA = [{
name: "data1",
left: 70,
top: 70,
radius: 20
}, {
name: "data2",
left: 200,
top: 100,
radius: 50
}];
const svg = d3.select("#svg");
// Not working as expected
function loadData(data) {
console.log(data);
const container = svg.selectAll(".container")
.data(data);
container.exit().remove();
const enter = container.enter().append("g")
.attr("class", "container");
enter.append("circle");
enter.append("text");
container.merge(enter)
.attr("transform", entry => `translate(0,${entry.top})`)
svg.selectAll(".container circle")
.attr("r", entry => entry.radius)
.attr("cx", entry => entry.left);
svg.selectAll(".container text")
.text(entry => entry.name);
}
// Working, but not desired
function expected(data) {
console.log(data);
const container = svg.selectAll(".container")
.data(data);
container.exit().remove();
const enter = container.enter().append("g")
.attr("class", "container");
enter.append("circle");
enter.append("text");
container.merge(enter)
.attr("transform", entry => `translate(0,${entry.top})`)
svg.selectAll(".container circle")
.data(data)
.attr("r", entry => entry.radius)
.attr("cx", entry => entry.left);
svg.selectAll(".container text")
.data(data)
.text(entry => entry.name);
}
let i = 0;
loadData(DATA);
function updateData() {
i = (i+1) % 2;
loadData(DATA.slice(i,i+1));
}
function expectedUpdate() {
i = (i+1) % 2;
expected(DATA.slice(i,i+1));
}
button {
display: inline-block;
}
svg {
width: 300px;
height: 300px;
display: block;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<button onclick="updateData();">Update</button>
<button onclick="expectedUpdate();">Expected</button>
<svg id="svg"></svg>
This is just demonstration, where I have a parent <g>
element, and 2 children <circle>
& <text>
are appending to it.
The first time when it renders, I used the loadData()
method to populate the DOM, the corresponding children DOMs (circle & text) were able to retrieve the data inserted into their parent DOM (g), which showed a proper data propagation along the DOM tree.
However when I press "update" button multiple times, only the parent DOM is updating, and children DOMs are able access the non-existing data (old data).
The "expected" button shows what is the expected behaviour, but I can see the data is being binded multiple times, which I as a javascript engineer am so afraid of this kind of data binding.
Upvotes: 3
Views: 141
Reputation: 102174
The main problem in your non-working function (loadData
) is that you're using selectAll
. Unlike select
, selectAll
doesn't propagate the data.
Have a look at this table I made summarising the differences between them:
Method | select() | selectAll() |
---|---|---|
Selection | selects the first element that matches the selector string | selects all elements that match the selector string |
Grouping | Does not affect grouping | Affects grouping |
Data propagation | Propagates data | Doesn't propagate data |
As you can see, the main information for you is this:
select
: propagates the data.selectAll
: doesn't propagate the data.Given that your snippet has just one child (circle and text) per group, the simplest solution is just changing selectAll
to select
. However, if you have more than one child per parent, the best (and idiomatic) solution is creating a nested enter-update-exit selection, which is a bit more laborious.
Besides that, you're missing the key function:
const container = svg.selectAll(".container")
.data(data, function(d) {
return d.name;
});
Here is your code with those changes:
const DATA = [{
name: "data1",
left: 70,
top: 70,
radius: 20
}, {
name: "data2",
left: 200,
top: 100,
radius: 50
}];
const svg = d3.select("#svg");
// Not working as expected
function loadData(data) {
console.log(data);
const container = svg.selectAll(".container")
.data(data, function(d) {
return d.name;
});
container.exit().remove();
const enter = container.enter().append("g")
.attr("class", "container");
enter.append("circle");
enter.append("text");
container.merge(enter)
.attr("transform", entry => `translate(0,${entry.top})`)
svg.select(".container circle")
.attr("r", entry => entry.radius)
.attr("cx", entry => entry.left);
svg.select(".container text")
.text(entry => entry.name);
}
// Working, but not desired
function expected(data) {
console.log(data);
const container = svg.selectAll(".container")
.data(data);
container.exit().remove();
const enter = container.enter().append("g")
.attr("class", "container");
enter.append("circle");
enter.append("text");
container.merge(enter)
.attr("transform", entry => `translate(0,${entry.top})`)
svg.selectAll(".container circle")
.data(data)
.attr("r", entry => entry.radius)
.attr("cx", entry => entry.left);
svg.selectAll(".container text")
.data(data)
.text(entry => entry.name);
}
let i = 0;
loadData(DATA);
function updateData() {
i = (i + 1) % 2;
loadData(DATA.slice(i, i + 1));
}
function expectedUpdate() {
i = (i + 1) % 2;
expected(DATA.slice(i, i + 1));
}
button {
display: inline-block;
}
svg {
width: 300px;
height: 300px;
display: block;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<button onclick="updateData();">Update</button>
<button onclick="expectedUpdate();">Expected</button>
<svg id="svg"></svg>
Upvotes: 2