Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Add Node to D3 Tree v4

I am using D3 v4 to build a tree.

Fiddle: https://jsfiddle.net/a6pLqpxw/

I am now trying to add support for dynamically adding (and removing) children from a selected node.

However I cannot get the chart to redraw without having to perform a complete redraw. I have modified the code from the collapsible tree diagram code at: https://bl.ocks.org/d3noob/43a860bc0024792f8803bba8ca0d5ecd

Specifically the following block does not perform a recalculation of the layout for its children.

document.getElementById('add-child').onclick = function() {
  console.log(selected);
  selected.children.push({
    type: 'resource-delete',
    name: new Date().getTime(),
    attributes: [],
    children: []
  });

  update(selected);
};

Does anyone have any good examples of dynamically adding/removing nodes to tree's in D3.js v4?

like image 857
fungus1487 Avatar asked Mar 31 '17 12:03

fungus1487


2 Answers

The height calculation in the accepted answer fails to update the ancestors of the created node. This means, for example, that the height of the root will never increase, even as many children are added.

The following code fixes these problems:

function insert(par, data) {
   let newNode = d3.hierarchy(data);
   newNode.depth = par.depth + 1;
   newNode.parent = par;
   // Walk up the tree, updating the heights of ancestors as needed.
   for(let height = 1, anc = par; anc != null; height++, anc=anc.parent) {
     anc.height = Math.max(anc.height, height);
   }
   if (!par.data.children) {
      par.children = [];
      par.data.children = [];
   }
   par.children.push(newNode);
   par.data.children.push(newNode.data);
}

It should be noted that the d3.tree layout algorithm doesn't actually use the height parameter, which is probably why it wasn't noted before.

If we take this route of "minimum code that makes the code work", we can also get rid of the par.data update and just use:

function insert(par, data) {
   let newNode = d3.hierarchy(data);
   newNode.depth = par.depth + 1;
   newNode.parent = par;
   if (!par.children)
      par.children = [];
   par.children.push(newNode);
}

To be functionally equivalent to the previous answer, we would write:

insert(selected, {
   type: 'node-type',
   name: new Date().getTime()
});
update(selected);
like image 117
Thomas Ahle Avatar answered Nov 20 '22 13:11

Thomas Ahle


I came up with this solution for Adding new Node dynamically to D3 Tree v4. .

D3 v4 tree requires Nodes.

Create Nodes from your tree data (json) using d3.hierarchy(..) and pushed it into it's parent.children array and update the tree.

Code Snippet

//Adding a new node (as a child) to selected Node (code snippet)
var newNode = {
    type: 'node-type',
    name: new Date().getTime(),
    children: []
  };
  //Creates a Node from newNode object using d3.hierarchy(.)
  var newNode = d3.hierarchy(newNode);

  //later added some properties to Node like child,parent,depth
  newNode.depth = selected.depth + 1; 
  newNode.height = selected.height - 1;
  newNode.parent = selected; 
  newNode.id = Date.now();

  //Selected is a node, to which we are adding the new node as a child
  //If no child array, create an empty array
  if(!selected.children){
    selected.children = [];
    selected.data.children = [];
  }

  //Push it to parent.children array  
  selected.children.push(newNode);
  selected.data.children.push(newNode.data);

  //Update tree
  update(selected);


Fiddle
// ### DATA MODEL START

var data = {
	type: 'action',
  name: '1',
  attributes: [],
  children: [{
  	type: 'children',
  	name: '2',
    attributes: [{
    	'source-type-property-value': 'streetlight'
    }],
    children: [{
    	type: 'parents',
  		name: '3',
      attributes: [{
      	'source-type-property-value': 'cable'
      }],
      children: [{
      	type: 'resource-delete',
  			name: '4',
        attributes: [],
        children: []
      }]
    }, {
    	type: 'children',
  		name: '5',
      attributes: [{
      	'source-type-property-value': 'lantern'
      }],
      children: []
    }]
  }]
};

// ### DATA MODEL END

// Set the dimensions and margins of the diagram
var margin = {top: 20, right: 90, bottom: 30, left: 90},
	width = 960 - margin.left - margin.right,
  height = 500 - 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("body").
	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(data, function(d) {
	return d.children;
});
root.x0 = height / 2;
root.y0 = 0;

update(root);

var selected = 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 * 180
  });

  // ### LINKS

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

  // Enter any new links at the parent's previous position.
  var linkEnter = link.enter().
  	append('line').
    attr("class", "link").
    attr("stroke-width", 2).
    attr("stroke", 'black').
    attr('x1', function(d) {
    	return source.y0;
    }).
    attr('y1', function(d) {
    	return source.x0;
    }).
    attr('x2', function(d) {
    	return source.y0;
    }).
    attr('y2', function(d) {
    	return source.x0;
    });
    
  var linkUpdate = linkEnter.merge(link);
  
  linkUpdate.transition().
  	duration(duration).
    attr('x1', function(d) {
    	return d.parent.y;
    }).
    attr('y1', function(d) {
    	return d.parent.x;
    }).
    attr('x2', function(d) {
    	return d.y;
    }).
    attr('y2', function(d) {
    	return d.x;
    });

  // Transition back to the parent element position
  linkUpdate.transition().
  	duration(duration).
    attr('x1', function(d) {
    	return d.parent.y;
    }).
    attr('y1', function(d) {
    	return d.parent.x;
    }).
    attr('x2', function(d) {
    	return d.y;
    }).
    attr('y2', function(d) {
    	return d.x;
    });

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

	// ### CIRCLES

  // 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', 25).
    style("fill", function(d) {
    	return "#0e4677";
    });

  // 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', 25).
    style("fill", function(d) {
    	return "#0e4677";
  	}).
    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', 0);
  
  // Store the old positions for transition.
  nodes.forEach(function(d){
    d.x0 = d.x;
    d.y0 = d.y;
  });

  // Toggle children on click.
  function click(d) {
    selected = d;
    document.getElementById('add-child').disabled = false;
    document.getElementById('remove').disabled = false;
    update(d);
  }
}

document.getElementById('add-child').onclick = function() {
  
  //creates New OBJECT
  var newNodeObj = {
  	type: 'resource-delete',
    name: new Date().getTime(),
    attributes: [],
    children: []
  };
  //Creates new Node 
  var newNode = d3.hierarchy(newNodeObj);
  newNode.depth = selected.depth + 1; 
  newNode.height = selected.height - 1;
  newNode.parent = selected; 
  newNode.id = Date.now();
  
  if(!selected.children){
    selected.children = [];
  	    selected.data.children = [];
  }
  selected.children.push(newNode);
  selected.data.children.push(newNode.data);
  
  update(selected);
};
<script src="https://d3js.org/d3.v4.min.js"></script>
<button id="add-child" disabled="disabled">Add Child</button>
like image 40
rhitz Avatar answered Nov 20 '22 13:11

rhitz