简体   繁体   中英

D3 Tree: edit Tree Node Data

I have used D3.js v4 to create a tree from flat data. Stratify is used to convert flat data into the hierarchical structure required for the tree. The nodes in the tree have an additional data field (score) that is not displayed. On clicking a leaf node the node's score is displayed (via a range slider in a separate <div> . I would like to be able to use the input (range slider) to change the value for the selected node's score. Any changes should be updated in the node's data field (as stored in JSON tree data) so that when clicking on that node again the new value is displayed.

I have created the following example to show the problem:

https://jsfiddle.net/shorgy/2d5epdqp/

// the flat data

var flatData = [

  {
    "name": "Root",
    "parent": null,
    "category": "test",
    "score": null
  },
  {
    "name": "Child 1",
    "parent": "Root",
    "category": "test",
    "score": "2"
  },
  {
    "name": "Child 2",
    "parent": "Root",
    "category": "test",
    "score": "3"
  },
  {
    "name": "Child 3",
    "parent": "Root",
    "category": "test",
    "score": "4"
  },

];


// convert the flat data into a hierarchy

var tree = d3.stratify()
  .id(function(d) { return d.name; })
  .parentId(function(d) { return d.parent; })

var treeData = tree(flatData);




// assign the name to each node
treeData.each(function(d) {
    d.name = d.id;
  });



// Set the dimensions and margins of the diagram
var margin = {top: 20, right: 60, bottom: 30, left: 110},
    width = document.getElementById("tree").clientWidth - margin.left - margin.right,
    height = document.getElementById("tree").clientHeight - margin.top - margin.bottom;

// append the svg object to the body of the page
// appends a 'group' element to 'svg'
// moves the 'group' element to the top left margin
var svg = d3.select('div#tree').append("svg")
  .attr("width", width + margin.right + margin.left)
  .attr("height", height + margin.top + margin.bottom)
  .append("g")
  .attr("transform", "translate("
    + margin.left + "," + margin.top + ")");

var i = 0,
    duration = 750,
    root;

// declares a tree layout and assigns the size
var treemap = d3.tree().size([height, width]);

// Assigns parent, children, height, depth
root = d3.hierarchy(treeData, function(d) { return d.children; });
root.x0 = height / 2;
root.y0 = 0;


// Collapse after the second level
root.children.forEach(collapse);

update(root);

// Collapse the node and all it's children
function collapse(d) {
  if(d.children) {
    d._children = d.children
    d._children.forEach(collapse)
    d.children = null
  }
}



function update(source) {

  // Assigns the x and y position for the nodes
  var treeData = treemap(root);

  // Compute the new tree layout.
  var nodes = treeData.descendants(),
      links = treeData.descendants().slice(1);

  // Normalize for fixed-depth.
  nodes.forEach(function(d){ d.y = d.depth * 250});





  // ****************** Nodes section ***************************

  // Update the nodes...
  var node = svg.selectAll('g.node')
      .data(nodes, function(d) {return d.id || (d.id = ++i); });

  // Enter any new modes at the parent's previous position.
  var nodeEnter = node.enter().append('g')
      .attr('class', 'node')
      .attr("transform", function(d) {
        return "translate(" + source.y0 + "," + source.x0 + ")";
    })
    .on('click', click);

  // Add Circle for the nodes
  nodeEnter.append('circle')
      .attr('class', 'node')
      .attr('r', 1e-6)
      .style("fill", function(d) {
          return d._children ? "lightsteelblue" : "#fff";
      });

  // Add labels for the nodes
  nodeEnter.append('text')
      .attr("dy", ".375em")
      .attr("x", function(d) {
          return d.children || d._children ? -13 : 13;
      })
      .attr("text-anchor", function(d) {
          return d.children || d._children ? "end" : "start";
      })
      .text(function(d) { return d.data.name; });

  // UPDATE
  var nodeUpdate = nodeEnter.merge(node);

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

  // Update the node attributes and style
  nodeUpdate.select('circle.node')
    .attr('r', 8)
    .style("fill", function(d) {
        return d._children ? "lightsteelblue" : "#fff";
    })
    .attr('cursor', 'pointer');


  // Remove any exiting nodes
  var nodeExit = node.exit().transition()
      .duration(duration)
      .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', 1e-6);

  // On exit reduce the opacity of text labels
  nodeExit.select('text')
    .style('fill-opacity', 1e-6);

  // ****************** links section ***************************

  // Update the links...
  var link = svg.selectAll('path.link')
      .data(links, function(d) { return d.id; });

  // Enter any new links at the parent's previous position.
  var linkEnter = link.enter().insert('path', "g")
      .attr("class", "link")
      .attr('d', function(d){
        var o = {x: source.x0, y: source.y0}
        return diagonal(o, o)
      });

  // UPDATE
  var linkUpdate = linkEnter.merge(link);

  // Transition back to the parent element position
  linkUpdate.transition()
      .duration(duration)
      .attr('d', function(d){ return diagonal(d, d.parent) });

  // Remove any exiting links
  var linkExit = link.exit().transition()
      .duration(duration)
      .attr('d', function(d) {
        var o = {x: source.x, y: source.y}
        return diagonal(o, o)
      })
      .remove();

  // Store the old positions for transition.
  nodes.forEach(function(d){
    d.x0 = d.x;
    d.y0 = d.y;
  });

  // Creates a curved (diagonal) path from parent to the child nodes
  function diagonal(s, d) {

    path = `M ${s.y} ${s.x}
            C ${(s.y + d.y) / 2} ${s.x},
              ${(s.y + d.y) / 2} ${d.x},
              ${d.y} ${d.x}`

    return path
  }

  // Toggle children on click.
  function click(d) {
    if (d.children) {
        //parent node, children expanded so collapse
        d._children = d.children;
        d.children = null;
        zzz();
      } else {
        //children not expanded (expand) or no children (nothing)
        if (d._children) { //if children exist but not shown (node is a parent node)
          d.children = d._children;
          d._children = null;
          zzz();
        } else { //if no c=children (i.e. node is a leaf node)
          //display node information in another div.
          //alert("score = " + d.data.data.score + " , category = " + d.data.data.category );
          xxx(d.data.name, d.data.data.score);
        }
      }
    update(d);
  }
}

function xxx(node, score) {
  $(".nist_cat").css({"display": "block"});
  //changevalue pscore_text to be node
  document.getElementById("pscore_text").textContent = node;
  //change value of pscore to be score
  document.getElementById("pscore").value = score;
}


function yyy(new_value){

}

function zzz() {
    $(".nist_cat").css({"display": "none"});
}

In summary - How can I use the input value from the range slider to actually change the associated score value for the selected node?

Instead of passing the values to the xxx function, pass the datum object.

function click(d){
    //more code
    xxx(d)
}

That way, you can change it when moving the slider:

d3.select("#pscore").on("change", function() {
    d.data.data.score = this.value;
})

Here is your code with that change:

 // the flat data var flatData = [ { "name": "Root", "parent": null, "category": "test", "score": null }, { "name": "Child 1", "parent": "Root", "category": "test", "score": "2" }, { "name": "Child 2", "parent": "Root", "category": "test", "score": "3" }, { "name": "Child 3", "parent": "Root", "category": "test", "score": "4" }, ]; // convert the flat data into a hierarchy var tree = d3.stratify() .id(function(d) { return d.name; }) .parentId(function(d) { return d.parent; }) var treeData = tree(flatData); // assign the name to each node treeData.each(function(d) { d.name = d.id; }); // Set the dimensions and margins of the diagram var margin = { top: 20, right: 60, bottom: 30, left: 110 }, width = document.getElementById("tree").clientWidth - margin.left - margin.right, height = document.getElementById("tree").clientHeight - margin.top - margin.bottom; // append the svg object to the body of the page // appends a 'group' element to 'svg' // moves the 'group' element to the top left margin var svg = d3.select('div#tree').append("svg") .attr("width", width + margin.right + margin.left) .attr("height", height + margin.top + margin.bottom) .append("g") .attr("transform", "translate(" + margin.left + "," + margin.top + ")"); var i = 0, duration = 750, root; // declares a tree layout and assigns the size var treemap = d3.tree().size([height, width]); // Assigns parent, children, height, depth root = d3.hierarchy(treeData, function(d) { return d.children; }); root.x0 = height / 2; root.y0 = 0; // Collapse after the second level root.children.forEach(collapse); update(root); // Collapse the node and all it's children function collapse(d) { if (d.children) { d._children = d.children d._children.forEach(collapse) d.children = null } } function update(source) { // Assigns the x and y position for the nodes var treeData = treemap(root); // Compute the new tree layout. var nodes = treeData.descendants(), links = treeData.descendants().slice(1); // Normalize for fixed-depth. nodes.forEach(function(d) { dy = d.depth * 250 }); // ****************** Nodes section *************************** // Update the nodes... var node = svg.selectAll('g.node') .data(nodes, function(d) { return d.id || (d.id = ++i); }); // Enter any new modes at the parent's previous position. var nodeEnter = node.enter().append('g') .attr('class', 'node') .attr("transform", function(d) { return "translate(" + source.y0 + "," + source.x0 + ")"; }) .on('click', click); // Add Circle for the nodes nodeEnter.append('circle') .attr('class', 'node') .attr('r', 1e-6) .style("fill", function(d) { return d._children ? "lightsteelblue" : "#fff"; }); // Add labels for the nodes nodeEnter.append('text') .attr("dy", ".375em") .attr("x", function(d) { return d.children || d._children ? -13 : 13; }) .attr("text-anchor", function(d) { return d.children || d._children ? "end" : "start"; }) .text(function(d) { return d.data.name; }); // UPDATE var nodeUpdate = nodeEnter.merge(node); // Transition to the proper position for the node nodeUpdate.transition() .duration(duration) .attr("transform", function(d) { return "translate(" + dy + "," + dx + ")"; }); // Update the node attributes and style nodeUpdate.select('circle.node') .attr('r', 8) .style("fill", function(d) { return d._children ? "lightsteelblue" : "#fff"; }) .attr('cursor', 'pointer'); // Remove any exiting nodes var nodeExit = node.exit().transition() .duration(duration) .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', 1e-6); // On exit reduce the opacity of text labels nodeExit.select('text') .style('fill-opacity', 1e-6); // ****************** links section *************************** // Update the links... var link = svg.selectAll('path.link') .data(links, function(d) { return d.id; }); // Enter any new links at the parent's previous position. var linkEnter = link.enter().insert('path', "g") .attr("class", "link") .attr('d', function(d) { var o = { x: source.x0, y: source.y0 } return diagonal(o, o) }); // UPDATE var linkUpdate = linkEnter.merge(link); // Transition back to the parent element position linkUpdate.transition() .duration(duration) .attr('d', function(d) { return diagonal(d, d.parent) }); // Remove any exiting links var linkExit = link.exit().transition() .duration(duration) .attr('d', function(d) { var o = { x: source.x, y: source.y } return diagonal(o, o) }) .remove(); // Store the old positions for transition. nodes.forEach(function(d) { d.x0 = dx; d.y0 = dy; }); // Creates a curved (diagonal) path from parent to the child nodes function diagonal(s, d) { path = `M ${sy} ${sx} C ${(sy + dy) / 2} ${sx}, ${(sy + dy) / 2} ${dx}, ${dy} ${dx}` return path } // Toggle children on click. function click(d) { if (d.children) { //parent node, children expanded so collapse d._children = d.children; d.children = null; zzz(); } else { //children not expanded (expand) or no children (nothing) if (d._children) { //if children exist but not shown (node is a parent node) d.children = d._children; d._children = null; zzz(); } else { //if no c=children (ie node is a leaf node) //display node information in another div. //alert("score = " + d.data.data.score + " , category = " + d.data.data.category ); xxx(d); } } update(d); } } function xxx(d) { $(".nist_cat").css({ "display": "block" }); //changevalue pscore_text to be node document.getElementById("pscore_text").textContent = d.data.name; //change value of pscore to be score document.getElementById("pscore").value = d.data.data.score; d3.select("#pscore").on("change", function() { d.data.data.score = this.value; }) } function yyy(new_value) { //called from input slider 'pscore' //change the node score value } function zzz() { $(".nist_cat").css({ "display": "none" }); } 
 html { overflow-y: scroll; } body, table { font-family: Helvetica Neue, Helvetica, Arial, sans-serif; font-size: 12px; color: gray; background-color: #fff; } hr { border: 0; width: 100%; background-color: steelblue; color: #eeeeee; height: 1px; } /* A row that can contain all other divs to make sure they float centrally*/ .row { width: 1360px; margin: 0 auto; overflow: hidden; } /*---------------------------------------Tree Nodes and Links----------------------------------*/ .node circle { fill: #fff; stroke: steelblue; stroke-width: 3px; } .node text { font: 12px sans-serif; color: #eeeeee; } .link { fill: none; stroke: #ccc; stroke-width: 2px; } /*---------------------------------------Main Layout Divs----------------------------------*/ .tree { width: 600px; height: 300px; } .nist_cat { width: 678px; height: 478px; display: none; float: left; border: 1px solid #eee; } /*---------------------------------------Input Sliders----------------------------------*/ input[type=range] { /*removes default webkit styles*/ -webkit-appearance: none; /*fix for FF unable to apply focus style bug */ border: 1px solid white; /*required for proper track sizing in FF*/ width: 150px; } input[type=range]::-webkit-slider-runnable-track { width: 150px; height: 5px; background: #ddd; border: none; border-radius: 3px; } input[type=range]::-webkit-slider-thumb { -webkit-appearance: none; border: none; height: 16px; width: 16px; border-radius: 50%; background: steelblue; margin-top: -4px; } input[type=range]:focus { outline: none; } input[type=range]:focus::-webkit-slider-runnable-track { background: #ccc; } input[type=range]::-moz-range-track { width: 150px; height: 5px; background: #ddd; border: none; border-radius: 3px; } input[type=range]::-moz-range-thumb { border: none; height: 16px; width: 16px; border-radius: 50%; background: steelblue; } /*hide the outline behind the border*/ input[type=range]:-moz-focusring { outline: 1px solid white; outline-offset: -1px; } input[type=range]::-ms-track { width: 150px; height: 5px; /*remove bg colour from the track, we'll use ms-fill-lower and ms-fill-upper instead */ background: transparent; /*leave room for the larger thumb to overflow with a transparent border */ border-color: transparent; border-width: 6px 0; /*remove default tick marks*/ color: transparent; } input[type=range]::-ms-fill-lower { background: #777; border-radius: 10px; } input[type=range]::-ms-fill-upper { background: #ddd; border-radius: 10px; } input[type=range]::-ms-thumb { border: none; height: 16px; width: 16px; border-radius: 50%; background: steelblue; } input[type=range]:focus::-ms-fill-lower { background: #888; } input[type=range]:focus::-ms-fill-upper { background: #ccc; } 
 <!DOCTYPE html> <meta charset="UTF-8"> <head> <link rel="stylesheet" href="tree2.css"> <!-- load the d3.js library --> <script src="https://d3js.org/d3.v4.min.js"></script> <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script> </head> <body> <div class="row"> <div id="tree" class="tree"></div> </div> <div class="row"> <hr> <div id="protect" class="nist_cat"> <span id="pscore_text" style="display: inline-block;width: 120px;text-align: right;"></span> <input type="range" id="pscore" min="1" max="4" step="1" onchange=yyy(value); /> </div> </div> </body> 


PS: You don't need jQuery here... actually, you normally don't need any jQuery in a D3 code. I suggest you to get rid of that.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM