D3.js child map failure ? Any gurus spot my error?

2019-08-25 02:45发布

问题:

First the code can be found here: working - almost - code

I've had a few pointers and learnt "quite a bit" along to way trying to get this to work.

Basically I'm building, or trying to build a hierachy tree for office staff with some basic functions.

Its all going pretty well except one last issue and no matter how I look at it or approach it, I cannot see why it isnt working.

Problem:

If you hold the mouse over a node, 4 little pop up menus appear - the green and the red add and remove nodes - this works.

At the top of the canvas is a "Save" button, which I'm trying to get to go through all the nodes giving their parent-to-child relationship - again this works until you add a node and then another node, it will not see the a child of a new node.

If anyone knows the way to refresh the "child map" that Im using in the snippit below, it would be much appreciated:

d3.selectAll('g.node')
      .each(function(p) {
        p.children.map(function(c) {
          alert(c.name + "(" + c.id + ")" + "- PARENT TO -" + p.name + "(" + 
p.id + ")")
        });
      });

回答1:

I don't know if I got your problem right, maybe I'm totally wrong with my assumption but your data is seems fine to me. The example you linked will throw an error when clicking on save for those nodes without children on line 25:

p.children.map(function(c) {
  alert(c.name + "(" + c.id + ")" + "- PARENT TO -" + p.name + "(" + p.id + ")")
});

Since in some cases p.children is undefined (there is none) the loop will break.

Since the application is working visually, there is no reason to think the data bindings aren't correct. Just to be sure, I wrote a slightly modified version of your "save" loop. Add some children and check the console.

Now, a little bit out of scope to your question but may save you some headaches the next time: d3 just maps your data to your elements. When creating, destroying and updating stuff, leave all the DOM manipulation to d3 and concentrate on your model.

var diameter = 1000;
var height = diameter - 150;
var n = {
  "name": "A",
  "id": 1,
  "target": 0,
  "children": [{
      "name": "B",
      "id": 2,
      "target": 1,
      "children": [{
        "name": "Cr",
        "id": 8,
        "target": 2,
        "children": [{
          "name": "D",
          "id": 7,
          "target": 2
        }, {
          "name": "E",
          "id": 9,
          "target": 8
        }, {
          "name": "F",
          "id": 10,
          "target": 8
        }]
      }]
    },
    {
      "name": "G",
      "id": 3,
      "target": 0
    }, {
      "name": "H",
      "id": 4,
      "target": 0
    }, {
      "name": "I",
      "id": 5,
      "target": 0
    }, {
      "name": "J",
      "id": 6,
      "target": 0
    }
  ]
}


var tree = d3.layout.tree()
  .size([260, diameter / 2 - 120])
  .separation(function(a, b) {
    return (a.parent == b.parent ? 1 : 2) / a.depth;
  });

var diagonal = d3.svg.diagonal.radial()
  .projection(function(d) {
    return [d.y, d.x / 180 * Math.PI];
  });

var myZoom = d3.behavior.zoom()
  .scaleExtent([.5, 10])
  .on("zoom", zoom);

var container = d3.select("body").append("svg")
  .attr("width", diameter)
  .attr("height", height)
  .style('border', '3px solid black')
  .call(myZoom);


//I am centering my node here
var svg = container.append("g")
  .attr("transform", "translate(" + diameter / 2 + "," + height / 2 + ")");


myZoom.translate([diameter / 2, height / 2]);

var init = true;

function zoom() {
  svg.attr("transform", "translate(" + (d3.event.translate[0]) + "," + (d3.event.translate[1]) + ")scale(" + d3.event.scale + ")");
}

var nodes = tree(n);
//make sure to set the parent x and y for all nodes 

nodes.forEach(function(node) {
  if (node.id == 1) {
    node.px = node.x = 500;
    node.py = node.y = 304;

  } else {
    node.px = node.parent.x;
    node.py = node.parent.y;

  }
});

// Build a array for borken tree case 
var myCords = d3.range(50);
buildSingleTreeData();

var id = ++nodes.length;

function update(root) {

  var node = svg.selectAll(".node");
  var link = svg.selectAll(".link");



  nodes = tree.nodes(root);
  if (checkBrokenTree(root)) {
    if (!root.children || root.children.length == 0) {
      id = 2;
    } else {
      var returnId = resetIds(root, 1);
      id = nodes.length + 1;
    }
    singleNodeBuild(nodes);
  }

  links = tree.links(nodes);




  /*This is a data join on all nodes and links 
  if a node is added a link will also be added 
  they are based parsing of the root*/
  node = node.data(nodes, function(d) {
    return d.id;
  });
  link = link.data(links, function(d) {
    return d.source.id + "-" + d.target.id;
  });



  var enterNodes = node.enter().append("g")
    .attr("class", "node")
    .attr("transform", function(d) {
      d.tx = (d.parent ? d.parent.x : d.px) - 90;
      return "rotate(" + ((d.parent ? d.parent.x : d.px) - 90) +
        ")translate(" + d.py + ")";
    })


  enterNodes.append('g')
    .attr('class', 'label')
    .attr('transform', function(d) {
      return 'rotate(' + -d.px + ')';
    })
    .append('text')
    .attr("dx", "-1.6em")
    .attr("dy", "2.5em")
    .text(function(d) {
      return d.name;
    })
    .call(make_editable, function(d) {
      return d.name;
    });


  var circlesGroup = enterNodes.append('g')
    .attr('class', 'circles');

  var mainCircles = circlesGroup.append("circle")
    .attr('class', 'main')
    .attr("r", 9);

  circlesGroup.append("circle")
    .attr('class', 'delete')
    .attr('cx', 0)
    .attr('cy', 0)
    .attr('fill', 'red')
    .attr('opacity', 0.5)
    .attr("r", 0);

  circlesGroup.append("circle")
    .attr('class', 'add')
    .attr('cx', 0)
    .attr('cy', 0)
    .attr('fill', 'green')
    .attr('opacity', 0.5)
    .attr("r", 0);

  circlesGroup.append("circle")
    .attr('class', 'admin')
    .attr('cx', 0)
    .attr('cy', 0)
    .attr('fill', 'blue')
    .attr('opacity', 0.5)
    .attr("r", 0);

  circlesGroup.append("circle")
    .attr('class', 'userid')
    .attr('cx', 0)
    .attr('cy', 0)
    .attr('fill', 'yellow')
    .attr('opacity', 0.5)
    .attr("r", 0);



  circlesGroup.on("mouseenter", function() {
    var elem = this.__data__;
    elem1 = d3.selectAll(".delete").filter(function(d, i) {
      return elem.id == d.id ? this : null;
    });
    elem2 = d3.selectAll(".add").filter(function(d, i) {
      return elem.id == d.id ? this : null;
    });
    elem3 = d3.selectAll(".admin").filter(function(d, i) {
      return elem.id == d.id ? this : null;
    });
    elem4 = d3.selectAll(".userid").filter(function(d, i) {
      return elem.id == d.id ? this : null;
    });


    elem2.transition()
      .duration(duration)
      .attr('cx', -20)
      .attr('cy', 0)
      .attr("r", 8);

    elem1.transition()
      .duration(duration)
      .attr('cx', 20)
      .attr('cy', 0)
      .attr("r", 8);

    elem3.transition()
      .duration(duration)
      .attr('cx', -10)
      .attr('cy', -20)
      .attr("r", 8);

    elem4.transition()
      .duration(duration)
      .attr('cx', 10)
      .attr('cy', -20)
      .attr("r", 8);


  });




  circlesGroup.on("mouseleave", function() {
    var elem = this.__data__;
    elem1 = d3.selectAll(".delete").filter(function(d, i) {
      return elem.id == d.id ? this : null;
    });
    elem2 = d3.selectAll(".add").filter(function(d, i) {
      return elem.id == d.id ? this : null;
    });
    elem3 = d3.selectAll(".admin").filter(function(d, i) {
      return elem.id == d.id ? this : null;
    });


    elem2.transition()
      .duration(duration)
      .attr('cy', 0)
      .attr('cx', 0)
      .attr("r", 0);

    elem1.transition()
      .duration(duration)
      .attr('cy', 0)
      .attr('cx', 0)
      .attr("r", 0);

    elem3.transition()
      .duration(duration)
      .attr('cy', 0)
      .attr('cx', 0)
      .attr("r", 0);

    elem4.transition()
      .duration(duration)
      .attr('cy', 0)
      .attr('cx', 0)
      .attr("r", 0);

  });

  var linkEnter = link.enter()
    .insert("path", '.node')
    .attr("class", "link")
    .attr("d", function(d) {
      var o = {
        x: d.source.px,
        y: d.source.py
      };
      return diagonal({
        source: o,
        target: o
      });
    });



  // UserID node event handeler 
  node.select('.userid').on('click', function() {
    var p = this.__data__;
    alert(p.name + " Userid (" + p.username + ")" + "-->" + p.id + "<--" + p.children);

  });


  // Admin node event handeler 
  node.select('.admin').on('click', function() {
    var p = this.__data__;
    alert(p.name + " password is (" + p.pword + ")");

  });

  // Delete node event handeler 

  node.select('.delete').on('click', function() {
    var p = this.__data__;







    if (p.id != 1) {
      removeNode(p);
      var childArr = p.parent.children;
      childArr = childArr.splice(childArr.indexOf(p), 1);
      update(n);
    }

    function removeNode(p) {
      if (!p.children) {
        if (p.id) {
          p.id = null;
        }
        return p;
      } else {
        for (var i = 0; i < p.children.length; i++) {
          p.children[i].id == null;
          removeNode(p.children[i]);
        }
        p.children = null;
        return p;
      }
    }

    node.exit().remove();
    link.exit().remove();
    // alertify.alert(p.name + " has left the building..");






  });


  // The add node even handeler 
  node.select('.add').on('click', function() {
    var p = this.__data__;
    var aId = id++;
    var d = {
      name: 'name' + aId
    };
    d.id = aId;

    if (p.children) {
      p.children.push(d);
      //top node add
    } else {
      p.children = [d];
      //child of child 
    }

    d.px = p.x;
    d.py = p.x;
    d3.event.preventDefault();



    update(n)
    node.exit().remove();
    link.exit().remove();
  });




  /* this is the update section of the graph and nodes will be updated to their current positions*/

  var duration = 700;


  node.transition()
    .duration(duration)
    .attr("transform", function(d) {
      d.utx = (d.x - 90);
      return "rotate(" + (d.x - 90) + ")translate(" + d.y + ")";
    })

  link.transition()
    .duration(duration).attr("d", diagonal);

  node.select('g')
    .transition()
    .duration(duration)
    .attr('transform', function(d) {
      return 'rotate(' + -d.utx + ')';
    });

  node.select('.circles').attr('transform', function(d) {
    return 'rotate(' + -d.utx + ')';
  });
  node.exit().remove();
  link.exit().remove();

}

update(n);

/** make a manual tree for when it is just 
  a linked list. For some reason the algorithm will break down
  all nodes in tree only have one child.
*/
function buildSingleTreeData() {
  myCords = d3.range(50);
  var offset = 130;
  myCords = myCords.map(function(n, i) {
    return {
      x: 90,
      y: offset * i
    };
  });

}


/**
  This function will build single node tree where every node
  has 1 child. From testing this layout does not support 
  a layout for nodes tree less than size 3 so they must 
  be manually drawn. Also if evey node has one child then 
  the tree will also break down and as a result this fucntion
  is there to manually build a singe tree up to 50 nodes
*/
function resetIds(aNode, aId) {
  if (aNode.children) {
    for (var i = 0; i < aNode.children.length; i++) {
      aNode.children[i].id = ++aId;
      resetIds(aNode.children[i], aId);
    }
    return aId;
  }

}

/*
builds a liner tree D3 does not support this
and so it must be hard coded
*/
function singleNodeBuild(nodes) {
  nodes.forEach(function(elem) {
    var i = elem.id - 1;
    elem.x = myCords[i].x;
    elem.y = myCords[i].y;
  });
}

/* D3 does not support operations 
on where root nodes does not have atlest 
2 children. this case need to be check for
and hard coded
*/
function checkBrokenTree(rootNode) {
  var size = nodes.length;

  var val = 0;

  function recur(nod, i) {
    if (nod.children) {
      return recur(nod.children[0], i + 1);
    } else {
      return i + 1;
    }
  }
  return recur(rootNode, val) == nodes.length;
}

/*
Credit https://gist.github.com/GerHobbelt/2653660
This funciton make a text node editable 
*/
function make_editable(d, field) {
  this
    .on("mouseover", function() {
      d3.select(this).style("fill", "red");
    })
    .on("mouseout", function() {
      d3.select(this).style("fill", null);
    })
    .on("click", function(d) {

      var p = this.parentNode;

      //console.log(this, arguments);


      // inject a HTML form to edit the content here...

      // bug in the getBBox logic here, but don't know what I've done wrong here;
      // anyhow, the coordinates are completely off & wrong. :-((
      var xy = this.getBBox();
      var p_xy = p.getBBox();

      xy.x -= p_xy.x;
      xy.y -= p_xy.y;

      var el = d3.select(this);
      var p_el = d3.select(p);

      var frm = p_el.append("foreignObject");

      var inp = frm
        .attr("x", xy.x - 40)
        .attr("y", xy.y + 40)
        .attr("dx", "2em")
        .attr("dy", "-3em")
        .attr("width", 100)
        .attr("height", 25)
        .append("xhtml:form")
        .append("input")
        .attr("value", function() {
          // nasty spot to place this call, but here we are sure that the <input> tag is available
          // and is handily pointed at by 'this':
          this.focus();
          //console.log( d);

          return d.name;
        })
        .attr({
          maxlength: 16
        })
        .style({
          width: "100px"
        })
        // make the form go away when you jump out (form looses focus) or hit ENTER:
        .on("blur", function() {
          //console.log("blur", this, arguments);

          var txt = inp.node().value;

          d.name = txt;
          if (txt) {
            el
              .text(function(d) {
                return d.name;
              });
          }
          // Note to self: frm.remove() will remove the entire <g> group! Remember the D3 selection logic!
          p_el.select("foreignObject").remove();
        })
        .on("keypress", function() {
          // console.log("keypress", this, arguments);

          // IE fix
          if (!d3.event)
            d3.event = window.event;

          var e = d3.event;
          if (e.keyCode == 13) {
            if (typeof(e.cancelBubble) !== 'undefined') // IE
              e.cancelBubble = true;
            if (e.stopPropagation)
              e.stopPropagation();
            e.preventDefault();

            var txt = inp.node().value;
            if (txt) {

              d.name = txt;
              el
                .text(function(d) {
                  return d.name;
                });
            }
            // odd. Should work in Safari, but the debugger crashes on this instead.
            // Anyway, it SHOULD be here and it doesn't hurt otherwise.
            p_el.select("foreignObject").remove();

          }
        });
    });
}
.node .main {
  fill: #fff;
  stroke: steelblue;
  stroke-width: 1.5px;
}

h1 {
  text-align: center;
}

.node .delete {
  stroke-width: 1.5px;
}

.node {
  font: 10px sans-serif;
}

.link {
  fill: none;
  stroke: #ccc;
  stroke-width: 1.5px;
}

svg {
  margin-left: auto;
  margin-right: auto;
  display: block;
}

text {
  font: 10px "Helvetica Neue", Helvetica, Arial, sans-serif;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.17/d3.min.js"></script>

<html lang="en">

  <head>
    <meta charset="utf-8">
    <title> Staff</title>




  </head>
  <h1> STAFF </h1>
  <input type="button" id="button" value="Save" />

  <body style="text-align:center">



    <script src="https://ajax.googleapis.com/ajax/libs/jquery/2.2.4/jquery.min.js"></script>

    <script>
      var onClick = function() {

        d3.selectAll('g.node')
          .each(function(p) {
          	if (p.children) {
            	console.log(`node ${p.name} has ${p.children.length} children: `, p.children.map(child => child.name));
              [...p.children].forEach((child) => {
                console.log(`${child.name} is child of ${child.parent.name}`);
              });
            } else {
            	console.log(`node ${p.name} has no children`);
            }
            
            console.log('----------------')
          });

      };

      $('#button').click(onClick);

    </script>

  </body>

  </head>