ShiraishiMai
ShiraishiMai

Reputation: 135

Update with DOM children and data propagation

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

Answers (1)

Gerardo Furtado
Gerardo Furtado

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

Related Questions