D3 V4:更新的数据被视为新数据吗?(更新功能)

his*_*ndy 7 html javascript updates d3.js

目前,我正在构建系统,并且在更新功能方面遇到一些麻烦。

本质上,我试图将新节点添加到D3树中。当用户单击节点的“添加按钮”时,可以添加新的子节点。每个添加按钮都可以在每个节点的左侧找到。

我遵循了Mike Bostock的一般更新模式。一旦单击按钮,唯一的“新”数据元素应该是新创建的子节点,但是看起来整个数据都被视为“新”。当我查看每个节点的类名称以及所有节点都过渡到中心节点并消失的明显事实时,我得出了这个结论。其他原始数据应“更新”,但不是。任何人都可以轻轻地指出为什么会这样吗?

我的代码的工作示例可以在此jfiddle链接中找到。

编辑06/09

根据戈登的建议,我为我的节点和链接都找到了一个唯一的字段。因此,为了唯一标识数据,我进行了以下更改:

节点

.data(d, d => d.data.name)
Run Code Online (Sandbox Code Playgroud)

链接

.data(d, d => d.source.data.name)
Run Code Online (Sandbox Code Playgroud)

该更改有效(大部分情况下),但我看到一些奇怪的行为仍在发生:(1)分支7.2.1仍被识别为新节点并消失了;(2)在第二个“添加”之后,链接未正确地与其各自的节点对齐。我认为我的两个小修改正在影响这一点,因为当我返回原始代码时,尽管事实上它们正在过渡,但线条已正确绘制。有什么想法吗?忠告?


的HTML

  <div id="div-mindMap">
Run Code Online (Sandbox Code Playgroud)

的CSS

.linkMindMap {
    fill: none;
    stroke: #555;
    stroke-opacity: 0.4;
}    
rect {
    fill: white;
    stroke: #3182bd;
    stroke-width: 1.5px;  
 }
Run Code Online (Sandbox Code Playgroud)

JS

const widthMindMap = 700;
const heightMindMap = 700;
let parsedData;

let parsedList = {
  "name": " Stapler",
  "children": [{
      "name": " Bind",
      "children": []
    },
    {
      "name": "   Nail",
      "children": []
    },
    {
      "name": "   String",
      "children": []
    },
    {
      "name": " Glue",
      "children": [{
          "name": "Gum",
          "children": []
        },
        {
          "name": "Sticky Gum",
          "children": []
        }
      ]
    },
    {
      "name": " Branch 3",
      "children": []
    },
    {
      "name": " Branch 4",
      "children": [{
          "name": "   Branch 4.1",
          "children": []
        },
        {
          "name": "   Branch 4.2",
          "children": []
        },
        {
          "name": "   Branch 4.1",
          "children": []
        }
      ]
    },
    {
      "name": " Branch 5",
      "children": []
    },
    {
      "name": " Branch 6",
      "children": []
    },
    {
      "name": " Branch 7",
      "children": []
    },
    {
      "name": "   Branch 7.1",
      "children": []
    },
    {
      "name": "   Branch 7.2",
      "children": [{
          "name": "   Branch 7.2.1",
          "children": []
        },
        {
          "name": "   Branch 7.2.1",
          "children": []
        }
      ]
    }
  ]
}


let svgMindMap = d3.select('#div-mindMap')
  .append("svg")
  .attr("id", "svg-mindMap")
  .attr("width", widthMindMap)
  .attr("height", heightMindMap);


let backgroundLayer = svgMindMap.append('g')
  .attr("width", widthMindMap)
  .attr("height", heightMindMap)
  .attr("class", "background")

let gLeft = backgroundLayer.append("g")
  .attr("transform", "translate(" + widthMindMap / 2 + ",0)")
  .attr("class", "g-left");
let gLeftLink = gLeft.append('g')
  .attr('class', 'g-left-link');
let gLeftNode = gLeft.append('g')
  .attr('class', 'g-left-node');


function loadMindMap(parsed) {
  var data = parsed;
  var split_index = Math.round(data.children.length / 2);

  parsedData = {
    "name": data.name,
    "children": JSON.parse(JSON.stringify(data.children.slice(split_index)))
  };

  var left = d3.hierarchy(parsedData, d => d.children);

  drawLeft(left, "left");
}

// draw single tree
function drawLeft(root, pos) {
  var SWITCH_CONST = 1;
  if (pos === "left") SWITCH_CONST = -1;

  update(root, SWITCH_CONST);
}

function update(source, SWITCH_CONST) {
  var tree = d3.tree()
    .size([heightMindMap, SWITCH_CONST * (widthMindMap - 150) / 2]);
  var root = tree(source);

  console.log(root)

  var nodes = root.descendants();
  var links = root.links();

  console.log(nodes)
  console.log(links)
  // Set both root nodes to be dead center vertically
  nodes[0].x = heightMindMap / 2

  //JOIN new data with old elements
  var link = gLeftLink.selectAll(".link-left")
    .data(links, d => d)
    .style('stroke-width', 1.5);

  var linkEnter = link.enter().append("path")
    .attr("class", "linkMindMap link-left")
    .attr("d", d3.linkHorizontal()
      .x(d => d.y)
      .y(d => d.x));

  var linkUpdate = linkEnter.merge(link);

  linkUpdate.transition()
    .duration(750)
  var linkExit = link.exit()
    .transition()
    .duration(750)
    .attr('x1', function(d) {
      return root.x;
    })
    .attr('y1', function(d) {
      return root.y;
    })
    .attr('x2', function(d) {
      return root.x;
    })
    .attr('y2', function(d) {
      return root.y;
    })
    .remove();

  //JOIN new data with old elements
  var node = gLeftNode.selectAll(".nodeMindMap-left")
    .data(nodes, d => d);

  console.log(nodes);


  //ENTER new elements present in new data
  var nodeEnter = node.enter().append("g").merge(node)
    .attr("class", function(d) {
      return "nodeMindMap-left " + "nodeMindMap" + (d.children ? " node--internal" : " node--leaf");
    })
    .classed("enter", true)
    .attr("transform", function(d) {
      return "translate(" + d.y + "," + d.x + ")";
    })
    .attr("id", function(d) {
      let str = d.data.name;
      str = str.replace(/\s/g, '');
      return str;
    });

  nodeEnter.append("circle")
    .attr("r", function(d, i) {
      return 2.5
    });

  var addLeftChild = nodeEnter.append("g")
    .attr("class", "addHandler")
    .attr("id", d => {
      let str = d.data.name;
      str = "addHandler-" + str.replace(/\s/g, '');
      return str;
    })
    .style("opacity", "1")
    .on("click", (d, i, nodes) => addNewLeftChild(d, i, nodes));

  addLeftChild.append("line")
    .attr("x1", -74)
    .attr("y1", 1)
    .attr("x2", -50)
    .attr("y2", 1)
    .attr("stroke", "#85e0e0")
    .style("stroke-width", "2");

  addLeftChild.append("rect")
    .attr("x", "-77")
    .attr("y", "-7")
    .attr("height", 15)
    .attr("width", 15)
    .attr("rx", 5)
    .attr("ry", 5)
    .style("stroke", "#444")
    .style("stroke-width", "1")
    .style("fill", "#ccc");

  addLeftChild.append("line")
    .attr("x1", -74)
    .attr("y1", 1)
    .attr("x2", -65)
    .attr("y2", 1)
    .attr("stroke", "#444")
    .style("stroke-width", "1.5");

  addLeftChild.append("line")
    .attr("x1", -69.5)
    .attr("y1", -3)
    .attr("x2", -69.5)
    .attr("y2", 5)
    .attr("stroke", "#444")
    .style("stroke-width", "1.5");

  // .call(d3.drag().on("drag", dragged));;

  nodeEnter.append("foreignObject")
    .style("fill", "blue")
    .attr("x", -50)
    .attr("y", -7)
    .attr("height", "20px")
    .attr("width", "100px")
    .append('xhtml:div')
    .append('div')
    .attr("class", 'clickable-node')
    .attr("id", function(d) {
      let str = d.data.name;
      str = "div-" + str.replace(/\s/g, '');
      return str;
    })
    .attr("ondblclick", "this.contentEditable=true")
    .attr("onblur", "this.contentEditable=false")
    .attr("contentEditable", "false")
    .style("text-align", "center")
    .text(d => d.data.name);

  //TODO: make it dynamic
  nodeEnter.insert("rect", "foreignObject")
    .attr("ry", 6)
    .attr("rx", 6)
    .attr("y", -10)
    .attr("height", 20)
    .attr("width", 100)
    // .filter(function(d) { return d.flipped; })
    .attr("x", -50)
    .classed("selected", false)
    .attr("id", function(d) {
      let str = d.data.name;
      str = "rect-" + str.replace(/\s/g, '');
      return str;
    });

  var nodeUpdate = nodeEnter.merge(node);
  // Transition to the proper position for the node
  nodeUpdate.transition()
    .duration(750)
    .attr("transform", function(d) {
      return "translate(" + d.y + "," + d.x + ")";
    });

  // Remove any exiting nodes
  var nodeExit = node.exit()
    .transition()
    .duration(750)
    .attr("transform", function(d) {
      return "translate(" + source.y + "," + source.x + ")";
    })
    .remove();

  // On exit reduce the node circles size to 0
  nodeExit.select('circle').attr('r', 0);
  // node = nodeEnter.merge(node)
}

function addNewLeftChild(d, i, nodes) {
  console.log("make new child");
  event.stopPropagation();
  var newNodeObj = {
    // name: new Date().getTime(),
    name: "New Child",
    children: []
  };

  console.log("this is ", parsedData)
  //Creates new Node
  var newNode = d3.hierarchy(newNodeObj);
  newNode.depth = d.depth + 1;
  newNode.height = d.height - 1;
  newNode.parent = d;
  newNode.id = Date.now();

  console.log(newNode);
  console.log(d)

  if (d.data.children.length == 0) {
    console.log("i have no children")
    d.children = []
  }
  d.children.push(newNode)
  d.data.children.push(newNode.data)

  console.log(d)
  let foo = d3.hierarchy(parsedData, d => d.children) 
  drawLeft(foo, "left");
}


loadMindMap(parsedList);
Run Code Online (Sandbox Code Playgroud)

Ste*_*eve 4

有几件事正在发生:

使用唯一键

对于键来说,使用名称并不是最好的选择,因为每个新节点都具有相同的名称(“新子节点”)。相反,使用某种身份识别系统可能会更好。这是一个使用 ID 标记每个节点的数据的快速函数。

let currNodeId = 0;
function idData(node) {
  node.nodeId = ++currNodeId;
  node.children.forEach(idData);
}
idData(parsedList);
Run Code Online (Sandbox Code Playgroud)

由于您要重新定义 中的数据parsedData,因此您也需要在那里使用 id 属性:

  parsedData = {
    "name": data.name,
    "nodeId": data.nodeId,
    "children": JSON.parse(JSON.stringify(data.children.slice(split_index)))
  };
Run Code Online (Sandbox Code Playgroud)

添加新节点时,也可以在nodeData中设置:

  var newNodeObj = {
    // name: new Date().getTime(),
    name: "New Child",
    nodeId: ++currNodeId,
    children: []
  };
Run Code Online (Sandbox Code Playgroud)

然后要实际用作.nodeId节点的键,请将其用作键函数:

    .data(nodes, d => d.data.nodeId);
Run Code Online (Sandbox Code Playgroud)

对于链接,您应该使用target代替source,因为这是一棵树,每个子项只有一个链接(而不是一个父项有多个链接)。

    .data(nodes, d => d.target.data.nodeId);
Run Code Online (Sandbox Code Playgroud)

防止添加多个节点元素

还有一个问题是,在添加新元素之前要合并新旧节点。为了防止这种情况,你应该改变

node.enter().append("g").merge(node)
Run Code Online (Sandbox Code Playgroud)

到:

node.enter().append("g")
Run Code Online (Sandbox Code Playgroud)

链接转换

最后,链接的转换不会随节点一起转换。要使它们过渡,请移动:

    .attr("d", d3.linkHorizontal()
      .x(d => d.y)
      .y(d => d.x));
Run Code Online (Sandbox Code Playgroud)

到以下

  linkUpdate.transition()
    .duration(750)
Run Code Online (Sandbox Code Playgroud)

总的来说,它看起来像这样: https: //jsfiddle.net/v9wyb6q4/

或者:

let currNodeId = 0;
function idData(node) {
  node.nodeId = ++currNodeId;
  node.children.forEach(idData);
}
idData(parsedList);
Run Code Online (Sandbox Code Playgroud)
  parsedData = {
    "name": data.name,
    "nodeId": data.nodeId,
    "children": JSON.parse(JSON.stringify(data.children.slice(split_index)))
  };
Run Code Online (Sandbox Code Playgroud)
  var newNodeObj = {
    // name: new Date().getTime(),
    name: "New Child",
    nodeId: ++currNodeId,
    children: []
  };
Run Code Online (Sandbox Code Playgroud)