【问题标题】:d3.js insert alternated siblings with selection.joind3.js 使用 selection.join 插入交替的兄弟姐妹
【发布时间】:2021-09-02 08:52:32
【问题描述】:

我正在使用 d3.js v.7。

我需要建立一个带有硬停止的线性渐变(即,color 0 从 0 到 x,color 1 从 x 到 y, ... color n 从 z到1)。 stop 列表具有任意长度,由如下矩阵定义:

const matrix = [
  [{start: 0, stop:0.5, color: 'blue'}, {start: 0.5, stop:1, color: 'teal'}],
  [{start: 0, stop:0.2, color: 'blue'}, {start: 0.2, stop:1, color: 'teal'}]
];

应该呈现如下:

<defs>
    <lineargradient id="grad_0">
        <!--stop 0 -->
        <stop offset="0" stop-color="blue"></stop>
        <stop offset="0.5" stop-color="blue"></stop>
        <!--stop 1 -->
        <stop offset="0.5" stop-color="teal"></stop>
        <stop offset="1" stop-color="teal"></stop>
    </lineargradient>
    <lineargradient id="grad_1">
        <!--stop 0 -->
        <stop offset="0" stop-color="blue"></stop>
        <stop offset="0.2" stop-color="blue"></stop>
        <!--stop 1 -->
        <stop offset="0.2" stop-color="teal"></stop>
        <stop offset="1" stop-color="teal"></stop>
    </lineargradient>
</defs>

注意:查看stop-color 元素:stops 遵循矩阵嵌套

从 Mike Bostock 的 Nested Selections 开始,我尝试了这个解决方案:

const defs = d3.select('body').append('defs'); 

const linearGradients = defs
.selectAll('linearGradient')
.data(matrix)
.join('linearGradient')
.attr('id', (d, i) => `grad_${i}`);

linearGradients
  .selectAll('stop')
  .data(d => d) //matrix[j]
  .join((enter) => {
  enter
    .append('stop')
    .attr('offset', (d) => d.start)
    .attr('stop-color', (d) => d.color);

  enter
    .append('stop')
    .attr('offset', (d) => d.stop)
    .attr('stop-color', (d) => d.color);

});

但是,stops 不遵循矩阵嵌套,也没有插入。这就是我用上面的代码得到的:

<defs>
    <lineargradient id="grad_0">
        <stop offset="0" stop-color="blue"></stop>
        <stop offset="0.5" stop-color="teal"></stop>
        <stop offset="0.5" stop-color="blue"></stop>
        <stop offset="1" stop-color="teal"></stop>
    </lineargradient>
    <lineargradient id="grad_1">
        <stop offset="0" stop-color="blue"></stop>
        <stop offset="0.2" stop-color="teal"></stop>
        <stop offset="0.2" stop-color="blue"></stop>
        <stop offset="1" stop-color="teal"></stop>
    </lineargradient>
</defs>

...这是错误的!

我已经查看了this one 之类的答案,但它们不再适用于较新版本的 d3。

这是一个可以使用的工作示例:https://codepen.io/floatingpurr/pen/RwpvvPq

是否有任何惯用且有效的解决方案?

【问题讨论】:

  • span 的目的是什么?我不认为 span 是 tr 的有效子代,但知道它的目的是什么可能有助于回答。
  • 嗨@AndrewReid,这样的跨度没有真正的目的。这只是构建一个示例来解释我的问题的一种便捷方式。然而,你是对的:这听起来很尴尬。将以更“线性”的方式重构示例。感谢您指出这一点。
  • 看起来不错 - 最后一个问题,我假设您需要使用新数据更新每个渐变的停靠点,对吗?
  • 今天晚些时候有时间看看。最简单和最规范的解决方案是修改数据 - D3 确实是为 one data item to one DOM element 设计的,但这并不总是可行的,other options 存在。
  • 我意识到 - 今晚晚些时候我会有一些东西,第一个链接只是一个支持引用,主要来自 D3 的创建者,第二个链接是以交替方式附加兄弟姐妹的示例(他们没有'不过需要更新)。

标签: javascript dom d3.js


【解决方案1】:

如链接中所述:D3 通常期望数据数组中的一项对应 DOM 中的一个元素。这是其数据绑定方法基础的一部分。这种方法仍然允许:

  1. 将多个子级附加到父级数据包含数组(嵌套数据)的父级,或
  2. 与其父级具有一对一关系的多种不同类型的子级(例如:标签、节点)。

我们不能这样做,因为我们的嵌套数据不包含我们希望输入的所有元素的数组。

我们也不能做两个,因为我们在父子之间没有一对一的关系——毕竟我们有嵌套数据。

有时您可以作弊并重复使用输入选择来加倍元素或使用类多次输入元素,但这些方法的前提是不关心 DOM 中元素的顺序。

但是,当顺序很重要,并且我们需要在 DOM 中为数据数组中的每个项目提供多个元素时,我们需要一种不同的方法。正如comments 中所述,一个等效问题是更新/退出/输入&lt;dd&gt; 和&lt;dt&gt; 元素对,因为严格来说,描述的列表不允许任何父级可以将这些对分组以进行排序和组织。与&lt;linearGradient&gt; 中的&lt;stop&gt; 元素相同,您的情况也是如此。

我想到了四类解决方案:

  1. 修改数据,使一个元素对应数据数组中的一项
  2. 滥用 d3-selection
  3. 在排序中发挥创意
  4. 使用 selection.html() 手动添加子元素

修改数据,使一个元素对应数据数组中的一项。

第一个可能是最直接的,因为它是最规范的,几乎不需要考虑如何修改 D3 代码。下面我使用一个函数来获取您的输入数据并为每个站点创建一个数据数组项 - 实质上是将每个现有项分成两个。为此,我们可能应该使用公共属性,因此我们将只使用 offset 值而不是 start 和 end 属性:

const matrix = [
  [{start: 0, stop:0.5, color: 'blue'}, {start: 0.5, stop:1, color: 'teal'}],
  [{start: 0, stop:0.2, color: 'blue'}, {start: 0.2, stop:1, color: 'teal'}]
];

// produce new data array:
function processData(data) {
  let newData = data.map(function(gradient) {  
    let stops = [];
    gradient.forEach(function(stop) {
      stops.push({offset: stop.start, color: stop.color})
      stops.push({offset: stop.stop, color: stop.color})
    })
    return stops;
  })
  return newData;
}

console.log(processData(matrix));

下面是一些不断更新的随机数据:

// produce new data array:
function processData(data) {
  let newData = data.map(function(gradient) {  
    let stops = [];
    gradient.forEach(function(stop) {
      stops.push({offset: stop.start, color: stop.color})
      stops.push({offset: stop.stop, color: stop.color})
    })
    return stops;
  })
  return newData;
}


//Basic set up:
var defs = d3.select("defs");
var svg = d3.select("svg");

update();
setInterval(update, 1000);
// Update with new data:
function update() {
  var data = randomGradients();
  data = processData(data);

  // update gradients:
  var grads = defs.selectAll("linearGradient")
     .data(data)
     .join("linearGradient")
     .attr("id",(d,i)=>"grad_"+i);

  var pairs = grads.selectAll("stop")
      .data(d=>d)  
      .join("stop")
      .transition()
      .attr("stop-color",d=>d.color)
      .attr("offset",d=>d.offset);

   // update rectangles:
   svg.selectAll("rect")
     .data(grads.data())
     .join("rect")
     .attr("y", (d,i)=>i*60+5)
     .attr("x", 10)
     .attr("width", 360)
     .attr("height", 50)
     .attr("fill",(d,i)=>"url(#grad_"+i+")");

}

// Random Data Generator Functions:
function createStop(a,b,c) {
  return {start: a, stop: b, color: c};
}

function randomData(n) {
  let colorSchemes = [
    d3.schemeBlues,
    d3.schemeReds,
    d3.schemePurples,
    d3.schemeGreens,
    d3.schemeGreys,
    d3.schemeOranges
  ];
  let points = d3.range(n-1).map(function(i) {
    return Math.random()  
  }).sort()
  points.push(1);
  let stops = [];
  let start = 0;
  let end = 1;
  let r = Math.floor(Math.random()*(colorSchemes.length));
  let colors = colorSchemes[r][n];
  for (let i = 0; i < n; i++) {
    stops.push(createStop(start,points[i],colors[i]));
    start = points[i];
  }
  return stops;
}

function randomGradients() {
  let gradients = [];
  let n = Math.floor(Math.random()*5)+1
  let m = Math.floor(Math.random()*3)+3;
  for(var i = 0; i < n; i++) {
     gradients.push(randomData(m));
  }
  return gradients;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/7.0.0/d3.min.js"></script>

<svg width="500" height="400"> 
  <defs>

  </defs>
  <rect width="300" height="50" fill="url(#grad_0)">
  </rect>
</svg>

当我们为数据数组 (stop) 元素中的每个项目附加相同类型的元素时,这很有效,但如果我们有不同类型的项目,就像在 @987654334 中一样,我们需要更有创意@ 案子。 append() 确实允许附加 variable tags(链接答案中列出的第四种方法),但更新时事情会变得有点复杂。

滥用 d3-selection

我添加这个是为了完整性,因为这是一个有趣的案例,在大多数情况下,您应该按照上面的选项更改数据结构,尽管扩展 D3 有时可能是明显首选的选项

您可以通过多种方式做到这一点。我决定创建一个新的 d3-selection 方法,但这不是必需的。这种方法需要多加注意,并且可能会产生一些可能的意外后果(我认为我在避免大多数方面做得很好) - 但保留了您的数据结构。我创建了一个方法selection.joinPair(),它接受一个参数:我们希望数据数组中的每个项目都有两个元素的类型。它返回所有元素的选择,但也有两种方法:first() 和 second(),它们分别返回第一组或第二组元素。它假设父元素的数据是子元素的数据数组,就像我们这里的例子一样。 (我稍后可能会修改下面的 sn-p 以使其更灵活一些,但它更多地是作为演示而不是作为最终产品或潜在的 d3 模块提供的。 p>

// New d3 method to join pairs:
d3.selection.prototype.joinPairs = function(type) {
  let parents = this; // selection this is called on.
  // select every even, odd element in two selections, set data
  let a = parents.selectAll(type+":nth-child(2n+1)")
     .data((d,i)=>parents.data()[i]);
  let b = parents.selectAll(type+":nth-child(2n+2)")
      .data((d,i)=>parents.data()[i]);

  // remove unneeded children:
  a.exit().remove();
  b.exit().remove();

  // enter, as we enter in pairs, we can use selection.clone()
  // which enters a new element immediately after the cloned node in the DOM.
  enterA = a.enter().append(type);
  enterB = enterA.clone();
  
  // return the selection of all elements, but allow access to odds/evens separately:
  let sel = parents.selectAll(type);
  sel.first=()=>a.merge(enterA);
  sel.second=()=>b.merge(enterB);
  return sel;
}

我们在这里工作:

// New d3 method to join pairs:
d3.selection.prototype.joinPairs = function(type) {
  let parents = this;
  let a = parents.selectAll(type+":nth-child(2n+1)")
     .data((d,i)=>parents.data()[i]);
  let b = parents.selectAll(type+":nth-child(2n+2)")
      .data((d,i)=>parents.data()[i]);

  a.exit().remove();
  b.exit().remove();

  enterA = a.enter().append(type);
  enterB = enterA.clone();
  
  let sel = parents.selectAll(type);
  sel.first=()=>a.merge(enterA);
  sel.second=()=>b.merge(enterB);
  return sel;
}

//Basic set up:
var defs = d3.select("defs");
var svg = d3.select("svg");

update();
setInterval(update, 1000);
// Update with new data:
function update() {
  // update gradients:
  var grads = defs.selectAll("linearGradient")
     .data(randomGradients())
     .join("linearGradient")
     .attr("id",(d,i)=>"grad_"+i);

  var pairs = grads.joinPairs("stop")
         .attr("stop-color",d=>d.color)

  // set first element in pair's offset:
  pairs.first().transition()
        .attr("offset",d=>d.start)
       
  // set second element in pair's offset:
  pairs.second().transition()
       .attr("offset",d=>d.stop)
     
   // update rectangles:
   svg.selectAll("rect")
     .data(grads.data())
     .join("rect")
     .attr("y", (d,i)=>i*60+5)
     .attr("x", 10)
     .attr("width", 360)
     .attr("height", 50)
     .attr("fill",(d,i)=>"url(#grad_"+i+")");

}

// Random Data Generator Functions:
function createStop(a,b,c) {
  return {start: a, stop: b, color: c};
}

function randomData(n) {
  let colorSchemes = [
    d3.schemeBlues,
    d3.schemeReds,
    d3.schemePurples,
    d3.schemeGreens,
    d3.schemeGreys,
    d3.schemeOranges
  ];
  let points = d3.range(n-1).map(function(i) {
    return Math.random()  
  }).sort()
  points.push(1);
  let stops = [];
  let start = 0;
  let end = 1;
  let r = Math.floor(Math.random()*(colorSchemes.length));
  let colors = colorSchemes[r][n];
  for (let i = 0; i < n; i++) {
    stops.push(createStop(start,points[i],colors[i]));
    start = points[i];
  }
  return stops;
}

function randomGradients() {
  let gradients = [];
  let n = Math.floor(Math.random()*5)+1
  let m = Math.floor(Math.random()*3)+3;
  for(var i = 0; i < n; i++) {
     gradients.push(randomData(m));
  }
  return gradients;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/7.0.0/d3.min.js"></script>

<svg width="500" height="400"> 
  <defs>

  </defs>
  <rect width="300" height="50" fill="url(#grad_0)">
  </rect>
</svg>

这种方式不太便携,除非进一步开发。

在排序中发挥创意

这种方式适合标准 D3 方法,但不是最典型的方法。在某些情况下它可能会变得混乱,但在这个用例中是相当干净的 - 尽管有许多项目可能比其他项目慢。

我们将根据类输入/更新/退出数据,并根据元素的属性对数据进行排序。然而,它有点具有挑战性,因为我们不能使用数据来存储索引,因为数据是在元素之间共享的。幸运的是,只要我们在所有“停止”之前添加所有“开始”,我们首先添加“开始”停止,然后通过 d.start 排序“停止”停止第二次排序将起作用:

//Basic set up:
var defs = d3.select("defs");
var svg = d3.select("svg");

update();
setInterval(update, 1000);
// Update with new data:
function update() {
  // update gradients, same as before:
  var grads = defs.selectAll("linearGradient")
     .data(randomGradients())
     .join("linearGradient")
     .attr("id",(d,i)=>"grad_"+i);

  // starts:
  grads.selectAll(".start")
    .data(d=>d)
    .join("stop")
    .transition()
    .attr("stop-color",d=>d.color)
    .attr("offset",d=>d.start)
    .attr("class","start");
  // ends:
  grads.selectAll(".end")
    .data(d=>d)
    .join("stop")
    .transition()
    .attr("stop-color",d=>d.color)
    .attr("offset",d=>d.stop)
    .attr("class","end")
    
  // sort based on start value  (where they are the same, first appended item is first):
  grads.each(function() {
    d3.select(this).selectAll("stop")
      .sort(function(a,b) { return a.start-b.start })
  })
  
   // update rectangles:
   svg.selectAll("rect")
     .data(grads.data())
     .join("rect")
     .attr("y", (d,i)=>i*60+5)
     .attr("x", 10)
     .attr("width", 360)
     .attr("height", 50)
     .attr("fill",(d,i)=>"url(#grad_"+i+")");

}

// Random Data Generator Functions:
function createStop(a,b,c) {
  return {start: a, stop: b, color: c};
}

function randomData(n) {
  let colorSchemes = [
    d3.schemeBlues,
    d3.schemeReds,
    d3.schemePurples,
    d3.schemeGreens,
    d3.schemeGreys,
    d3.schemeOranges
  ];
  let points = d3.range(n-1).map(function(i) {
    return Math.random()  
  }).sort()
  points.push(1);
  let stops = [];
  let start = 0;
  let end = 1;
  let r = Math.floor(Math.random()*(colorSchemes.length));
  let colors = colorSchemes[r][n];
  for (let i = 0; i < n; i++) {
    stops.push(createStop(start,points[i],colors[i]));
    start = points[i];
  }
  return stops;
}

function randomGradients() {
  let gradients = [];
  let n = Math.floor(Math.random()*5)+1
  let m = Math.floor(Math.random()*3)+3;
  for(var i = 0; i < n; i++) {
     gradients.push(randomData(m));
  }
  return gradients;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/7.0.0/d3.min.js"></script>

<svg width="500" height="400"> 
  <defs>

  </defs>
  <rect width="300" height="50" fill="url(#grad_0)">
  </rect>
</svg>

我不打算深入探讨这个选项,它应该相对简单。

使用 selection.html() 手动添加子元素

这确实超出了 D3 习语,但有时这种方法可能是合理的。我们将正常加入linearGradients,然后使用 selection.html 创建子站点。我只是为了完整性才包含它,如下所示:

这甚至是可转换的! (勉强)

//Basic set up:
var defs = d3.select("defs");
var svg = d3.select("svg");

update();
setInterval(update,1000);
// Update with new data:
function update() {
  // update gradients:
  var grads = defs.selectAll("linearGradient")
     .data(randomGradients())
     .join("linearGradient")
     .attr("id",(d,i)=>"grad_"+i);

   grads.transition().attrTween("anything", function(d) {
      var el = this;
      var end = "";
      d.forEach(function(s) {
        end += '<stop offset="'+s.start+'" stop-color="'+d3.color(s.color).formatRgb()+'"></stop>';
        end += '<stop offset="'+s.stop+'" stop-color="'+d3.color(s.color).formatRgb()+'"></stop>';
      })
      var start = d3.select(this).html() || end;
      function interpolator(t) {
        var i = d3.interpolate(start,end)(t);
        d3.select(el).html(i);
        return 1;
      }
      return interpolator
    });
   
   
   // update rectangles:
   svg.selectAll("rect")
     .data(grads.data())
     .join("rect")
     .attr("y", (d,i)=>i*60+5)
     .attr("x", 10)
     .attr("width", 360)
     .attr("height", 50)
     .attr("fill",(d,i)=>"url(#grad_"+i+")");

}


// Random Data Generator Functions:
function createStop(a,b,c) {
  return {start: a, stop: b, color: c};
}

function randomData(n) {
  let colorSchemes = [
    d3.schemeBlues,
    d3.schemeReds,
    d3.schemePurples,
    d3.schemeGreens,
    d3.schemeGreys,
    d3.schemeOranges
  ];
  let points = d3.range(n-1).map(function(i) {
    return Math.random()  
  }).sort()
  points.push(1);
  let stops = [];
  let start = 0;
  let end = 1;
  let r = Math.floor(Math.random()*(colorSchemes.length));
  let colors = colorSchemes[r][n];
  for (let i = 0; i < n; i++) {
    stops.push(createStop(start,points[i],colors[i]));
    start = points[i];
  }
  return stops;
}

function randomGradients() {
  let gradients = [];
  let n = Math.floor(Math.random()*5)+1
  let m = Math.floor(Math.random()*3)+3;
  for(var i = 0; i < n; i++) {
     gradients.push(randomData(m));
  }
  return gradients;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/7.0.0/d3.min.js"></script>

<svg width="500" height="400"> 
  <defs>

  </defs>
  <rect width="300" height="50" fill="url(#grad_0)">
  </rect>
</svg>

不包括选项

我没有包含任何无法从旧数据轻松转换到新数据的选项,因此任何使用 selection.remove() 擦除石板或清理石板的一部分以便从头开始构建的东西此处不包括在内。

【讨论】:

  • 这个答案太棒了!非常感谢您解释的清晰和完整。做得好。这将帮助许多人了解如何使用 d3。再次感谢!
猜你喜欢
  • 2019-05-06
  • 1970-01-01
  • 1970-01-01
  • 2012-07-23
  • 2019-08-29
  • 1970-01-01
  • 1970-01-01
  • 2013-04-19
  • 1970-01-01
相关资源
最近更新 更多