score:0

You could think about modifying your original data and that way you could just use one data binding and one update pattern. Within that pattern, you could check/use the word but also check each letter of the word (you could also just do this on the fly later):

const test_data = [ 'hello', 'world', 'wthtvwls' ];

const modified_data = test_data.map((item,index) => {
  return {
    word: item,
    array: Array.from(item)
  }
});

console.log(modified_data);

Then using this data (demo - https://codepen.io/Alexander9111/pen/qBdZrLv):

svg = d3.select('body')
  .append('svg')
  .attr('width',  640)
  .attr('height', 480);
svg.selectAll('text')
  .data( modified_data )
  .enter()
    .append('text')
    .attr('x', 10)
    .attr('y', function( d, i ) {
      return 20 + i * 20;
    })
    .html(function(d) {      
      return d.letters.map((letter,index) => {
        if (['a', 'e', 'i', 'o', 'u', 'y'].includes(letter)){
          return '<tspan class="vowel">' + letter + '</tspan>';
        } else{
          return '<tspan>' + letter + '</tspan>';
        }        
      }).join("");
    })   
    .attr('class', function( d, i ) {
        for (letter of d.letters){
          console.log('letter', letter);
          if (['a', 'e', 'i', 'o', 'u', 'y'].includes(letter)) {
            return 'strong';
          }
        }
      })
    .exit();

Note the use of the .html() function and then .map() within that using our current data.letters in the normal update pattern to add either '<tspan class="vowel">' or '<tspan>' (no class vowel).

https://codepen.io/Alexander9111/pen/qBdZrLv

But I am not sure if you want such a specific use case or a more general answer to the nested update pattern? If so then maybe this block helps? - https://bl.ocks.org/mpbastos/bd1d6763d34ac5d3ce533a581b291364

UPDATE - I realize I misread the question the first time

So it is now clear to me that my one-time solution is no more helpful than the other two solutions suggested by @Fabien (OP) (Moving the line up (before the second data()) or Using a separate statement svg.selectAll('text').attr('class', 'strong');)

I tried patterns such as:

svg.selectAll('text')
    .data( test_data )
    .enter()
    .append('text')
    .attr('x', 10)
    ...
    .selectAll('tspan')
    .data( ...)
    .enter()
      .append('tspan')
      .attr('class', 'example')
    .exit()
    .select(this.ParentNode)
    .attr('class', 'example')

And also with the .select(this.ParentNode) before the .exit() but neither worked.

The only thing that works but it is also a workaround and probably an anti-pattern is this - hijack the attr function to "grab" parent outside nested update :

svg = d3.select('body')
  .append('svg')
  .attr('width',  640)
  .attr('height', 480);
text = svg.selectAll('text')
    .data( test_data )
    .enter()
    .append('text')
    .attr('x', 10)
    .attr('y', function( d, i ) {
      return 20 + i * 20;
    })
    .selectAll('tspan')
    .data( function( d, i ) { // d is from the first data
      return Array.from(d); // if needed, could return an array or object that includes the initial value, too.
    })
    .enter()
      .append('tspan')
      .attr('class', function( d, i ) {
        console.log(1, i, this.parentNode);
         //hijack the attr function to "grab" parent outside nested update pattern
        if (i == 0) {
          d3.select(this.parentNode).attr('class', 'strong');
        }
        //console.log( 'tspan class:', d, i );
        if ( ['a', 'e', 'i', 'o', 'u', 'y'].includes(d) ) {
          return 'vowel';
        }
      })
      .text( function( d, i, foo ) { // d is from the second data
        //console.log( 'tspan text:', d, i, /*foo*/ );
        return d;
      })    
    .exit();

This is also not a beautiful solution.

I think because once the .data() function is called on a selection, it is now longer a normal node list object, and the .exit() function is only designed to be used one layer deep.

I think the two solutions the OP outlined are the best, but I would love to be proven wrong!


Related Query