d3.js 插入交替的兄弟姐妹 selection.join

d3.js insert alternated siblings with selection.join

我在 d3.js v.7.

我需要建立一个带有硬停止的线性渐变(即 颜色 00x, 颜色 1xy, ... 颜色 nz1)。 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);

});

然而,stop不遵循矩阵嵌套,它们不被插入。这是我用上面的代码得到的:

<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

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

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

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

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

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

有时您可以欺骗并重复使用输入选择来加倍元素或使用 类 多次输入元素,但这些方法的前提是对 DOM 中元素的顺序无关紧要.

但是,当顺序很重要时,我们需要 DOM 中的多个元素用于数据数组中的每个项目,我们需要一种不同的方法。如 中所述,等效问题是 update/exiting/entering 对 <dd><dt> 元素,因为严格来说,描述的列表不允许任何可以对这些对进行分组的父元素用于订购和组织。与 <linearGradient> 中的 <stop> 个元素相同,您的情况也是如此。

想到了四种类解决方案:

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

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

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

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) 元素中的每个项目追加相同类型的元素时,这会起作用,但如果我们有不同类型的项目,我们需要更有创意在 dd/dt 的情况下。 append() 确实允许附加 (链接答案中列出的第四种方法),但更新时事情会变得有点复杂。

滥用d3选择

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

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

// 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 方法,但不是最典型的方法。在某些情况下它可能会变得混乱,但在这个用例中是相当干净的 - 尽管许多项目可能比其他项目慢。

我们将enter/update/exit数据基于类,并根据属性元素对数据进行排序。然而,这有点具有挑战性,因为我们不能使用数据来存储索引,因为数据在元素之间共享。幸运的是,当我们首先附加“开始”停止和“停止”停止时,只要我们在所有“停止”之前附加所有“开始”,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 惯用语,但有时这种方法可能是合理的。我们将像往常一样加入 linearGradient,然后使用 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() 擦除记录或按顺序清洁部分记录的内容此处不包括从头开始构建。