使用“克隆”数据时,D3中的数据绑定失败

时间:2015-10-01 15:20:03

标签: d3.js

使用原始数据对象时,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));

}

有人可以对这种行为有所了解吗?我相信我正在使用关键功能,但可能是错误的。在我的应用程序中,我需要在每次更新表之前重新生成数据结构,并且我没有重用原始对象的选项。

2 个答案:

答案 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