使用原始数据对象时,D3数据绑定的行为似乎与使用克隆版本的数据对象相比有所不同。我有一个函数 updateTable ,它根据传递的数组数组更新表数组。如果一个数组(表示一个新表行)被添加到数组数组,并传递给 updateFunction ,则所有数组都按预期工作(该行将添加到表中)。但是,如果我们对此数据结构进行浅层复制(克隆)并将其传递给 updateFunction ,则数据绑定将失败,并且不会添加任何表行。请注意,原始数据结构和克隆是两个不同的对象,但相同的值。
请参阅this JSFiddle示例。生成两个表,一个表用原始数据,另一个表用克隆数据。这两个表明显不同,因为第二个表(使用克隆数据构建) NOT 包含第三行。
'use strict';
d3.select("body").append("h3").text("D3 Data Binding Issue");
// create two divs to hold one table each
var tableDiv1 = d3.select("body").append("div");
d3.select("body").append("hr");
var tableDiv2 = d3.select("body").append("div");
// define data
// here, an array of a single item (which represents a table), containing an array of arrays,
// each destined for a table row
var data = [
{ table: "Table1", rows: [
{ table: "Table1", row: "Row1", data: "DataT1R1" },
{ table: "Table1", row: "Row2", data: "DataT1R2" }
]
}
];
// run update on the initial data
update(data);
// add 3rd array to the data structure (which should add a third row in each table)
data[0].rows.push({ table: "Table1", row: "Row3", data: "DataT1R3" });
// run update again
// observe that the Lower table (which is using cloned data) does NOT update
update(data);
/*
// remove first array of the data structure
data[0].rows.shift();
// run update again
// observe that the Lower table (which again is using cloned data) does NOT update
update(data);
*/
// function to run the tableUpdate function targeting two different divs, one with the
// original data, and the other with cloned data
function update(data) {
// the contents of the two data structures are equal
console.log("\nAre object values equal? ", JSON.stringify(data) == JSON.stringify(clone(data)));
tableUpdate(data, tableDiv1, "Using Original Data"); // update first table
tableUpdate(clone(data), tableDiv2, "Using Cloned Data"); // update second table
}
// generic function to manage array of tables (in this simple example only one table is managed)
function tableUpdate(data, tableDiv, title) {
console.log("data", JSON.stringify(data));
// get all divs in this table div
var divs = tableDiv.selectAll("div")
.data(data, function(d) { return d.table }); // disable default by-index eval
// remove div(s)
divs.exit().remove();
// add new div(s)
var divsEnter = divs.enter().append("div");
// append header(s) in new div(s)
divsEnter.append("h4").text(title);
// append table(s) in new div(s)
var tableEnter = divsEnter.append("table")
.attr("id", function(d) { return d.table });
// append table body in new table(s)
tableEnter.append("tbody");
// select all tr elements in the divs update selection
var tr = divs.selectAll("table").selectAll("tbody").selectAll("tr")
.data(function(d, i, a) { return d.rows; }, function(d, i, a) { return d.row; }); // disable by-index eval
// remove any row(s) with missing data array(s)
tr.exit().remove();
// add row(s) for new data array(s)
tr.enter().append("tr");
// bind data to table cells
var td = tr.selectAll("td")
.data(function(d, i) { return d3.values(d); });
// add new cells
td.enter().append("td");
// update contents of table cells
td.text(function(d) { return d; });
}
// source: https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm
function clone(objectToBeCloned) {
return JSON.parse(JSON.stringify(objectToBeCloned));
}
有人可以对这种行为有所了解吗?我相信我正在使用关键功能,但可能是错误的。在我的应用程序中,我需要在每次更新表之前重新生成数据结构,并且我没有重用原始对象的选项。
答案 0 :(得分:2)
问题的根源在于你有一个嵌套结构,.selectAll()
没有更新绑定到元素的数据(但.append()
会自动"继承"数据)。因此,用于呈现表格的数据根本不会更新 - 您可以使用.select()
代替.selectAll()
来解决此问题(请参阅updated example)。
.select()
和.selectAll()
之间的细微差别在于前者(类似于.append()
)"继承"数据绑定到当前选择中的元素到新选择的元素,而.selectAll()
则没有。
那为什么它适用于原始数据?好吧,D3在将数据绑定到元素时不会复制数据,但会引用它。通过修改原始数据,您还可以修改与元素绑定的内容。因此,只需运行代码而无需重新绑定任何数据。克隆的数据未经更新,因为您不直接修改它。
答案 1 :(得分:1)
实际上,问题是由于您正在使用的反模式"肌肉" tr
结构。
在第二次通过tableUpdate期间,key函数在d.table上找到原始数据和未克隆数据的匹配项。这是因为在绑定过程中键被转换为字符串,所以即使
d.table === data.table; // false
它仍然匹配,因为
d.table == data.table; // true
因此,在两种情况下以及所有此代码中,输入选择都是空的
var divsEnter = divs.enter().append("div");
// append header(s) in new div(s)
divsEnter.append("h4").text(title);
// append table(s) in new div(s)
var tableEnter = divsEnter.append("table")
.attr("id", function(d) { return d.table });
// append table body in new table(s)
tableEnter.append("tbody");
什么都不做
因此原始数据不会被重新绑定,并且新的克隆数据不会被绑定。但是... 的
绑定到第一个表的数据现在有三行,因为正如Lars指出的那样,它被引用绑定。所以,对于第一张表,
divs.datum() === data; // true
它现在有三行。
对于克隆数据,键函数也返回true,因为您还没有更改它。即使它有一个额外的行,data.key仍然是"表1和#34;。所以你告诉关键功能它是同一张桌子。因此,输入选择也是空的,因此,对于第二个表,新的克隆数据也不受约束,
divs.datum() === data; // false
d.table == data.table == "Table1" // um, true true
它仍然有两行。
问题是您使用反模式来绑定数据并构建tr
元素。
不是按照其结构的层次结构选择和绑定数据,而是去 off piste 并返回div
并将其向下拉到{构造结构的{1}}元素。这很危险,因为返回的tr
元素不合格,您通过仔细选择/创建正确的tr
元素获得的重要上下文都不会用于确保这些元素是事实上,正确的tbody
元素,无论tr
元素恰好存在于哪个元素 - 无论它们属于哪个tr
- 都在table
内。
在这两种情况下,您只需使用仍然附加的原始数组重建tr元素,这对于第一个表是好的,但对于第二个表...不是那么多。
我的当前理论"最佳实践是构建您的数据结构,以便首先对可视化的预期结构进行建模,然后通过遍历该数据结构来构建DOM元素,在每个级别绑定并在您前进时将剩余数据踢到前面,直到最后,它和#39;全部受约束。
你需要真正的"数据驱动"在构建和绑定元素时严格遵循数据结构。我重新构建了你的updateTable函数......
div

'use strict';
d3.select("body").append("h3").text("D3 Data Binding Issue").style({margin:0});
// create two divs to hold one table each
var tableDiv1 = d3.select("body").append("div");
var tableDiv2 = d3.select("body").append("div");
// define data
// here, an array of a single item (which represents a table), containing an array of arrays,
// each destined for a table row
var data = [{
table: "Table1",
rows: [{
table: "Table1",
row: "Row1",
data: "DataT1R1"
}, {
table: "Table1",
row: "Row2",
data: "DataT1R2"
}]
}];
// run update on the initial data
update(data);
update(data);
// add 3rd array to the data structure (which should add a third row in each table)
data[0].rows.push({
table: "Table1",
row: "Row3",
data: "DataT1R3"
});
// run update again
// observe that the Lower table (which is using cloned data) does NOT update
update(data);
/*
// remove first array of the data structure
data[0].rows.shift();
// run update again
// observe that the Lower table (which again is using cloned data) does NOT update
update(data);
*/
// function to run the tableUpdate function targeting two different divs, one with the
// original data, and the other with cloned data
function update(data) {
// the contents of the two data structures are equal
console.log("\nAre object values equal? ", JSON.stringify(data) == JSON.stringify(clone(data)));
tableUpdate(data, tableDiv1, "Using Original Data"); // update first table
tableUpdate(clone(data), tableDiv2, "Using Cloned Data"); // update second table
}
// generic function to manage array of tables (in this simple example only one table is managed)
function tableUpdate(data, tableDiv, title) {
console.log("data", JSON.stringify(data));
// get all divs in this table div
var divs = tableDiv.selectAll("div")
.data(data, function (d) {
return d.table
}); // disable default by-index eval
// remove div(s)
divs.exit().remove();
// add new div(s)
var divsEnter = divs.enter().append("div");
// append header(s) in new div(s)
divsEnter.append("h4").text(title);
// append or replace table(s) in new div(s)
var table = divs.selectAll("table")
.data(function (d) {
// the 1st dimension determines the number of elements
// this needs to be 1 (one table)
return [d.rows];
}, function (d) {
// need a unique key to diferenciate table generations
var sha256 = new jsSHA("SHA-256", "TEXT");
return (sha256.update(JSON.stringify(d)),
console.log([this.length ? "data" : "node", sha256.getHash('HEX')].join("\t")),
sha256.getHash('HEX'));
});
table.exit().remove();
// the table body will have the same data pushed down from the table
// it will also be the array of array of rows
table.enter().append("table").append("tbody");
console.log(table.enter().size() ? "new table" : "same table")
var tBody = table.selectAll("tbody");
// select all tr elements in the divs update selection
var tr = tBody.selectAll("tr")
.data(function (d, i, a) {
// return one element of the rows array
return d;
}, function (d, i, a) {
return d.row;
}); // disable by-index eval
// remove any row(s) with missing data array(s)
tr.exit().remove();
// add row(s) for new data array(s)
tr.enter().append("tr");
// bind data to table cells
var td = tr.selectAll("td")
.data(function (d, i) {
return d3.values(d);
});
// add new cells
td.enter().append("td");
// update contents of table cells
td.text(function (d) {
return d;
});
}
// source: https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm
function clone(objectToBeCloned) {
return JSON.parse(JSON.stringify(objectToBeCloned));
}

table, th, td {
border: 1px solid gray;
}
body>div { display: inline-block; margin: 10px;}

有趣的是,绑定到原始数据的表永远不会被替换。原因是,正如@Lars所提到的那样,数据受到引用的约束 作为一个实验(并且受到我与git的爱恨交织的启发),我使用256位sha作为键,将字符串化数据提供给它。如果你在同一个空间管理一堆表,那么可能就是这样。如果你总是克隆数据并计算一个sha,那么这感觉就像一个非常安全的方法。
作为说明,这里是一个编辑日志(我在开始时添加了第二次更新并使用相同的数据...)
这是第一个没有节点的通行证。键函数仅在每个数据元素上调用一次,因为更新选择为空。
<body>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.6/d3.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jsSHA/2.0.1/sha.js"></script>
这是具有相同数据的第二次调用。您可以看到每个表调用两次键函数,并且两者的sha都相同,因此&#34;相同的表&#34; anotation。
Are object values equal? true
data [{"table":"Table1","rows":[{"tab...,"data":"DataT1R2"}]}]
data a09a5ef8f6b81669eed13c93f609884...
new table ...
data [{"table":"Table1","rows":[{"tab...,"data":"DataT1R2"}]}]
data a09a5ef8f6b81669eed13c93f609884...
new table ...
...
这是一个有趣的案例,即使数据已经改变,关键函数也会为第一个表的节点和数据返回相同的sha。第二个表是预期的,节点和数据具有不同的sha,并生成一个新表。
Are object values equal? true ...
data [{"table":"Table1","rows":[{"tab...,"data":"DataT1R2"}]}]
node a09a5ef8f6b81669eed13c93f609884...
data a09a5ef8f6b81669eed13c93f609884...
same table ...
data [{"table":"Table1","rows":[{"tab...,"data":"DataT1R2"}]}]
node a09a5ef8f6b81669eed13c93f60...
data a09a5ef8f6b81669eed13c93f60...
same table