【问题标题】:How to get out of nested selectAll in D3JS?如何摆脱 D3JS 中的嵌套 selectAll?
【发布时间】:2024-04-13 12:15:02
【问题描述】:

重新认识 hos D3JS 工作,尤其是 .selectAll/.data/.enter;学习如何嵌套数据。

我有这个工作演示:

svg = d3.select('body')
  .append('svg')
  .attr('width',  640)
  .attr('height', 480);
svg.selectAll('text')
  .data( [ 'hello', 'world' ] )
  .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( '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()
    .attr('class', 'strong') // this will set strong on <tspan>, but I wanted it on <text>
    ;

(在 Codepen 上查看:D3JS selectAll SVG nested。)

注意我们有两个data(),第二个(用于&lt;tspan&gt;s)嵌套在第一个(用于&lt;text&gt;s)中。

我正在尝试做的事情:

  • 我想&lt;text&gt; 上设置strong 类属性
  • 我认为exit() 会让我脱离使用 tspan 的“范围”/巢穴……但我错了。
  • 如果我注释掉exit(),那么在&lt;tspan&gt;s 上设置强类属性...而我希望在父&lt;text&gt; 上设置它!

除此之外,我怎样才能做到这一点:

  • 将队列向上移动(在第二个 data() 之前)。
  • 使用单独的语句 (svg.selectAll('text').attr('class', 'strong');)

我可以使用这两个选项之一,在这个例子中它是微不足道的......
但是我想知道是否有可能摆脱嵌套选择,如果可以,如何?

我希望这已经足够清楚了;如果不是,请发表评论,我会澄清:)

【问题讨论】:

  • 我尝试了很多东西,但如果不使用变通方法、黑客、反模式或您提到的两种解决方案,这似乎是不可能的。
  • 无论好坏,我意识到两年前我问过同样的问题:D3JS: appending simple nested HTML in a single command。简短的回答是:“不,你不能摆脱嵌套。”但是,您可以使用变量来分解代码并保持其可读性;见this answer,特别是最后一个代码sn-p。

标签: javascript d3.js svg


【解决方案1】:

您可以考虑修改原始数据,这样您就可以只使用一种数据绑定和一种更新模式。在该模式中,您可以检查/使用单词,也可以检查单词的每个字母(您也可以稍后即时执行此操作):

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);

然后使用此数据(演示 - 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();

注意.html() 函数的使用,然后在其中使用.map() 在正常更新模式中使用我们当前的data.letters 添加'&lt;tspan class="vowel"&gt;''&lt;tspan&gt;'(无类元音)。

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

但我不确定您是否想要这样一个特定的用例或更普遍的嵌套更新模式答案?如果是这样,那么也许这个块有帮助? - https://bl.ocks.org/mpbastos/bd1d6763d34ac5d3ce533a581b291364

更新 - 我意识到我第一次看错了问题

所以我现在很清楚,我的一次性解决方案并不比@Fabien (OP) 建议的其他两个解决方案更有帮助(移动队列(在第二个 data() 之前)或使用单独的语句svg.selectAll('text').attr('class', 'strong');)

我尝试了以下模式:

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')

还可以在.exit() 之前使用.select(this.ParentNode),但都不起作用。

唯一可行但它也是一种解决方法,可能是一种反模式 - 劫持 attr 函数以在嵌套更新之外“获取”父级:

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();

这也不是一个漂亮的解决方案。

我认为因为一旦在选择上调用.data() 函数,它现在就不再是一个普通的节点列表对象,而.exit() 函数被设计为仅用于一层深度。

我认为 OP 概述的两种解决方案是最好的,但我很想被证明是错误的!

【讨论】:

  • 我应该提到我也想避免使用 .html() ,因为感觉库应该处理元素的创建。不确定我最初的问题是否有模式……玩了更多的嵌套……最后只是为*父级设置了一个变量并使用它;感觉最易读。