如链接中所述:D3 通常期望数据数组中的一项对应 DOM 中的一个元素。这是其数据绑定方法基础的一部分。这种方法仍然允许:
- 将多个子级附加到父级数据包含数组(嵌套数据)的父级,或
- 与其父级具有一对一关系的多种不同类型的子级(例如:标签、节点)。
我们不能这样做,因为我们的嵌套数据不包含我们希望输入的所有元素的数组。
我们也不能做两个,因为我们在父子之间没有一对一的关系——毕竟我们有嵌套数据。
有时您可以作弊并重复使用输入选择来加倍元素或使用类多次输入元素,但这些方法的前提是不关心 DOM 中元素的顺序。
但是,当顺序很重要,并且我们需要在 DOM 中为数据数组中的每个项目提供多个元素时,我们需要一种不同的方法。正如comments 中所述,一个等效问题是更新/退出/输入<dd> 和<dt> 元素对,因为严格来说,描述的列表不允许任何父级可以将这些对分组以进行排序和组织。与<linearGradient> 中的<stop> 元素相同,您的情况也是如此。
我想到了四类解决方案:
- 修改数据,使一个元素对应数据数组中的一项
- 滥用 d3-selection
- 在排序中发挥创意
- 使用 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() 擦除石板或清理石板的一部分以便从头开始构建的东西此处不包括在内。