Wanderer
Wanderer

Reputation: 592

Nested selections not updating

I'm to display a grouping of groups of data using d3js. The nested selection does seem to see the update. How can I get the inner groupings to update?

I've created this (http://jsfiddle.net/f0m574wp/17/) fiddle.

I've got some nested data that my application retrieves asynchronously and constructs and displays as data is coming in.

dat = [{
  key: 'asdf',
  values:[{
      key:'1',
      value:'hello'
    },{
      key:'2',
      value:''
    }]
  },{
  key:'xyz',
  values:[{
    key:'1',
    value:'foo'
  },{
    key:'3',
    value:'bar'
  }]
}];

As the data object is updated I want to re-run the display function to update the visualization.

function display(mydata){
  //let x = mydata;
  let x = JSON.parse(JSON.stringify(mydata));
  console.log('x', x);
  let root = d3.select('div');

  // create the outer groupings
  let outer = root.selectAll('div.outer').data(x, d=>d.key);
  let outerEnter = outer.enter()
    .append('div')
    .classed('outer', true)
    .attr('id', d=>d.key);

  // add a heading and body to contain data
  outerEnter.append('div').classed('heading', true).append('h1');
  outerEnter.append('div').classed('body', true);
  outer.exit().remove();

  // get prevoius and existing outer groups
  let outerall = outerEnter.merge(outer);
  let headings = outerall.selectAll('div.heading');
  let bodies = outerEnter.selectAll('div.body')
  headings.selectAll('h1').text(d=>d.key); // update outer group heading

  //lets work with the inner arrays of data
  let innerGroup = bodies.selectAll('div.mid')
    .data(d=>{ console.log(d); return d.values}, k=>k.key);

  let innerGpEnter = innerGroup.enter()
    .append('div')
    .classed('mid', true);

  let innerGpAll = innerGpEnter.merge(innerGroup);

  // inner heading
  let inner = innerGpAll.selectAll('h4.inner')
    //.call(function(sel){console.log(sel.nodes());})
    .data(d=>{ console.log(d); return [d];}, k=>k.key);
  let innerEnter = inner.enter()
    .append('h4')
    .classed('inner', true);
  innerEnter.merge(inner).text(k=>{console.log(k); return k.key});

  // inner detail
  let p = innerGpAll.selectAll('p.inner').data(d=>[d], k=>k.key);
  let pEnter = p.enter().append('p').classed('inner', true);
  pEnter.merge(p).text(v=>v.value);
}

The problem seems to be with the inner data binding :

//lets work with the inner arrays of data
let innerGroup = bodies.selectAll('div.mid')
  .data(d=>{ console.log(d); return d.values}, k=>k.key);

It does not seem to update the inner binding as data changes.

Upvotes: 1

Views: 64

Answers (1)

Gerardo Furtado
Gerardo Furtado

Reputation: 102174

The main problem here is the use of two chained selectAll. You have...

let bodies = outerEnter.selectAll('div.body');

... followed by...

let innerGroup = bodies.selectAll('div.mid').etc...

, which gives us this:

let innerGroup = outerEnter.selectAll('div.body').selectAll('div.mid').etc...

Firstly, you should choose outerall, not outerEnter, since outerall is the update selection. But the main issue here is the chained selectAlls: there is no data binding after the first one, just after the second one. Because of that the data is not carried to the inner selections.

It's important having in mind that, unlike select, selectAll doesn't propagate the data. Have a look at this table I made:

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

Pay attention to the propagates data versus doesn't propagate data.

Solution:

Let's make that a single selectAll, followed by the data method:

let innerGroup = outerall.selectAll('div.mid').etc...

Here is your code with that change:

dat = [{
  key: 'asdf',
  values: [{
    key: '1',
    value: 'hello'
  }, {
    key: '2',
    value: ''
  }]
}, {
  key: 'xyz',
  values: [{
    key: '1',
    value: 'foo'
  }, {
    key: '3',
    value: 'bar'
  }]
}];

function display(mydata) {
  //let x = mydata;
  let x = JSON.parse(JSON.stringify(mydata));
  console.log('x', x);
  let root = d3.select('div');

  // create the outer groupings
  let outer = root.selectAll('div.outer').data(x, d => d.key);
  let outerEnter = outer.enter()
    .append('div')
    .classed('outer', true)
    .attr('id', d => d.key);

  // add a heading and body to contain data
  outerEnter.append('div').classed('heading', true).append('h1');
  outerEnter.append('div').classed('body', true);
  outer.exit().remove();

  // get prevoius and existing outer groups
  let outerall = outerEnter.merge(outer);
  let headings = outerall.selectAll('div.heading');
  headings.selectAll('h1').text(d => d.key); // update outer group heading

  //lets work with the inner arrays of data
  let innerGroup = outerall.selectAll('div.mid')
    .data(d => {
      console.log(d);
      return d.values
    }, k => k.key);

  let innerGpEnter = innerGroup.enter()
    .append('div')
    .classed('mid', true);

  let innerGpAll = innerGpEnter.merge(innerGroup);

  // inner heading
  let inner = innerGpAll.selectAll('h4.inner')
    //.call(function(sel){console.log(sel.nodes());})
    .data(d => {
      console.log(d);
      return [d];
    }, k => k.key);
  inner.exit().remove();
  let innerEnter = inner.enter()
    .append('h4')
    .classed('inner', true);
  innerEnter.merge(inner).text(k => {
    console.log(k);
    return k.key
  });

  // inner detail
  let p = innerGpAll.selectAll('p.inner').data(d => [d], k => k.key);
  p.exit().remove();
  let pEnter = p.enter().append('p').classed('inner', true);
  pEnter.merge(p).text(v => v.value);
}

display(dat);
setTimeout(function() {
  console.log('Send New data');
  //dat[0].key = 'abc';
  dat[0].values[1].value = 'good bye';
  dat[1].values.push({
    key: 2,
    value: 'baz'
  });
  display(dat);
}, 3000)
* {
  margin: 2px;
  padding: 2px;
  border: 0;
}

body {
  background: #ffd;
}

.outer {
  border: 1px solid green;
  width:
}

.mid {
  border: 1px solid red;
}

.inner {
  border: 1px solid blue;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<div>
</div>

Solution 2:

According to your comment, you need to keep that HTML structure. In that case, use select:

let bodies = outerall.select('div.body');

You can safely use select here because you're appending just one <div> with a class body.

Here is the new snippet:

dat = [{
  key: 'asdf',
  values: [{
    key: '1',
    value: 'hello'
  }, {
    key: '2',
    value: ''
  }]
}, {
  key: 'xyz',
  values: [{
    key: '1',
    value: 'foo'
  }, {
    key: '3',
    value: 'bar'
  }]
}];

function display(mydata) {
  //let x = mydata;
  let x = JSON.parse(JSON.stringify(mydata));
  console.log('x', x);
  let root = d3.select('div');

  // create the outer groupings
  let outer = root.selectAll('div.outer').data(x, d => d.key);
  let outerEnter = outer.enter()
    .append('div')
    .classed('outer', true)
    .attr('id', d => d.key);

  // add a heading and body to contain data
  outerEnter.append('div').classed('heading', true).append('h1');
  outerEnter.append('div').classed('body', true);
  outer.exit().remove();

  // get prevoius and existing outer groups
  let outerall = outerEnter.merge(outer);
  let headings = outerall.selectAll('div.heading');
  let bodies = outerall.select('div.body');
  headings.selectAll('h1').text(d => d.key); // update outer group heading

  //lets work with the inner arrays of data
  let innerGroup = bodies.selectAll('div.mid')
    .data(d => {
      console.log(d);
      return d.values
    }, k => k.key);

  let innerGpEnter = innerGroup.enter()
    .append('div')
    .classed('mid', true);

  let innerGpAll = innerGpEnter.merge(innerGroup);

  // inner heading
  let inner = innerGpAll.selectAll('h4.inner')
    //.call(function(sel){console.log(sel.nodes());})
    .data(d => {
      console.log(d);
      return [d];
    }, k => k.key);
  inner.exit().remove();
  let innerEnter = inner.enter()
    .append('h4')
    .classed('inner', true);
  innerEnter.merge(inner).text(k => {
    console.log(k);
    return k.key
  });

  // inner detail
  let p = innerGpAll.selectAll('p.inner').data(d => [d], k => k.key);
  p.exit().remove();
  let pEnter = p.enter().append('p').classed('inner', true);
  pEnter.merge(p).text(v => v.value);
}

display(dat);
setTimeout(function() {
  console.log('Send New data');
  //dat[0].key = 'abc';
  dat[0].values[1].value = 'good bye';
  dat[1].values.push({
    key: 2,
    value: 'baz'
  });
  display(dat);
}, 3000)
* {
  margin: 2px;
  padding: 2px;
  border: 0;
}

body {
  background: #ffd;
}

.outer {
  border: 1px solid green;
  width:
}

.mid {
  border: 1px solid red;
}

.inner {
  border: 1px solid blue;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<div>
</div>

Upvotes: 3

Related Questions