I am playing around with D3 charts. One of the examples they provide is a chart to draw a tree structure (https://observablehq.com/#d3/tidy-tree)
I took this chart and embeded in Power BI as per example here (https://azurebi-docs.jppp.org/powerbi-visuals/d3js.html?tabs=docs%2Cdocs-open#sample), but I hit a wall with the data I currently have. The chart uses json as an input (https://raw.githubusercontent.com/d3/d3-hierarchy/v1.1.8/test/data/flare.json). The script to generate such tree structure json is as follows:
function toJSON(data) {
var flare = { name: "ROOT", children: [] },
levels = ["parentname","categoryname", "categoryname"];
// For each data row, loop through the expected levels traversing the output tree
data.forEach(function(d){
// Keep this as a reference to the current level
var depthCursor = flare.children;
// Go down one level at a time
levels.forEach(function( property, depth ){
// Look to see if a branch has already been created
var index;
depthCursor.forEach(function(child,i){
if ( d[property] == child.name ) index = i;
});
// Add a branch if it isn't there
if ( isNaN(index) ) {
depthCursor.push({ name : d[property], children : []});
index = depthCursor.length - 1;
}
// Now reference the new child array as we go deeper into the tree
depthCursor = depthCursor[index].children;
// This is a leaf, so add the last element to the specified branch
if ( depth === levels.length - 1 ) depthCursor.push({ name : d.product, size : d.revenue });
});
});
// End of conversion
return flare;
}
The problem I am facing is that my data is of a different struture...
The script assumes that levels variable consists of all levels. However my data structure is such, that every row has a name, i.e. CategoryName and an indicator to the parent, i.e. ParentName from the same table. So effectively, the records are
CategoryName | ParentName
Category 1
Category 2 | Category 1
Category 3 | Category 2
Category 4 | Category 2
How should I approch modification of provided javascript to build up a json based on the data structure I currently have?
Thanks for any kind of support or refernces I could base on.
Edit
The format of the data is:
data = [
{categoryname: 'Category 1', parentname: 'null'},
{categoryname: 'Category 2', parentname: 'Category 1'},
{categoryname: 'Category 3', parentname: 'Category 2'},
{categoryname: 'Category 4', parentname: 'Category 2'},
];
The whole PowerBI D3 script looks like this:
/*
* All D3 visuals run in a frame with the following elements/variables:
*
* SVG element:
* - <svg xmlns="http://www.w3.org/2000/svg" class="chart" id="chart" >
*
* pbi object:
* - 'dsv' : function that retrieves the data via the provided callback: pbi.dsv(callback)
e.g. pbi.dsv(function(data) { //Process data function });
* - 'height' : height of the sandbox frame
* - 'width' : width of the sandbox frame
* - 'colors' : color array with 8 colors; changable via options
*
* Code is based on: https://bl.ocks.org/mbostock/4339083
*/
// ADD: translate function for the data
function toJSON(data) {
var flare = { name: "ROOT", children: [] },
levels = ["parentname","categoryname", "categoryname"];
// For each data row, loop through the expected levels traversing the output tree
data.forEach(function(d){
// Keep this as a reference to the current level
var depthCursor = flare.children;
// Go down one level at a time
levels.forEach(function( property, depth ){
// Look to see if a branch has already been created
var index;
depthCursor.forEach(function(child,i){
if ( d[property] == child.name ) index = i;
});
// Add a branch if it isn't there
if ( isNaN(index) ) {
depthCursor.push({ name : d[property], children : []});
index = depthCursor.length - 1;
}
// Now reference the new child array as we go deeper into the tree
depthCursor = depthCursor[index].children;
// This is a leaf, so add the last element to the specified branch
if ( depth === levels.length - 1 ) depthCursor.push({ name : d.product, size : d.revenue });
});
});
// End of conversion
return flare;
}
var margin = {top: 20, right: 120, bottom: 20, left: 120},
width = pbi.width - margin.left - margin.right, // ALTER: Changed fixed width with the 'pbi.width' variable
height = pbi.height - margin.top - margin.bottom; // ALTER: Changed fixed height with the 'pbi.height' variable
var i = 0,
duration = 750,
root;
var tree = d3.layout.tree()
.size([height, width]);
var diagonal = d3.svg.diagonal()
.projection(function(d) { return [d.y, d.x]; });
var svg = d3.select("#chart") // ALTER: Select SVG object; no need to create it
.attr("width", width + margin.left + margin.right) // ALTER: Add complete width
.attr("height", height + margin.top + margin.bottom) // ALTER: Add complete height
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
// ALTER: Replaced the d3.json function with the pbi variant: pbi.dsv
pbi.dsv(function(data) {
var flare = toJSON(data); // ALTER: add extra convertion step to parent/child JSON
root = flare;
root.x0 = height / 2;
root.y0 = 0;
function collapse(d) {
if (d.children) {
d._children = d.children;
d._children.forEach(collapse);
d.children = null;
}
}
root.children.forEach(collapse);
update(root);
});
d3.select(self.frameElement).style("height", height + margin.top + margin.bottom);
function update(source) {
// Compute the new tree layout.
var nodes = tree.nodes(root).reverse(),
links = tree.links(nodes);
// Normalize for fixed-depth.
nodes.forEach(function(d) { d.y = d.depth * 180; });
// Update the nodes…
var node = svg.selectAll("g.node")
.data(nodes, function(d) { return d.id || (d.id = ++i); });
// Enter any new nodes 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);
nodeEnter.append("circle")
.attr("r", 1e-6)
.style("fill", function(d) { return d._children ? pbi.colors[0] : pbi.colors[1]; });
nodeEnter.append("text")
.attr("x", function(d) { return d.children || d._children ? -10 : 10; })
.attr("dy", ".35em")
.attr("text-anchor", function(d) { return d.children || d._children ? "end" : "start"; })
.text(function(d) { return d.name; })
.style("fill-opacity", 1e-6);
// Transition nodes to their new position.
var nodeUpdate = node.transition()
.duration(duration)
.attr("transform", function(d) { return "translate(" + d.y + "," + d.x + ")"; });
nodeUpdate.select("circle")
.attr("r", 4.5)
.style("fill", function(d) { return d._children ? pbi.colors[0] : pbi.colors[1]; });
nodeUpdate.select("text")
.style("fill-opacity", 1);
// Transition exiting nodes to the parent's new position.
var nodeExit = node.exit().transition()
.duration(duration)
.attr("transform", function(d) { return "translate(" + source.y + "," + source.x + ")"; })
.remove();
nodeExit.select("circle")
.attr("r", 1e-6);
nodeExit.select("text")
.style("fill-opacity", 1e-6);
// Update the links…
var link = svg.selectAll("path.link")
.data(links, function(d) { return d.target.id; });
// Enter any new links at the parent's previous position.
link.enter().insert("path", "g")
.attr("class", "link")
.attr("d", function(d) {
var o = {x: source.x0, y: source.y0};
return diagonal({source: o, target: o});
});
// Transition links to their new position.
link.transition()
.duration(duration)
.attr("d", diagonal);
// Transition exiting nodes to the parent's new position.
link.exit().transition()
.duration(duration)
.attr("d", function(d) {
var o = {x: source.x, y: source.y};
return diagonal({source: o, target: o});
})
.remove();
// Stash the old positions for transition.
nodes.forEach(function(d) {
d.x0 = d.x;
d.y0 = d.y;
});
}
// Toggle children on click.
function click(d) {
if (d.children) {
d._children = d.children;
d.children = null;
} else {
d.children = d._children;
d._children = null;
}
update(d);
}
It seems to me that your question is another case of XY problem: instead of asking about the creation of the hierarchy itself you're asking about a function you believe is necessary for creating that hierarchy.
You don't need that function to create a hierarchical structure. Based on you data, you can just use d3.stratify:
const csv = `CategoryName,ParentName
Category 1,
Category 2,Category 1
Category 3,Category 2
Category 4,Category 2`;
const data = d3.csvParse(csv);
const stratify = d3.stratify()
.id(function(d) {
return d.CategoryName;
})
.parentId(function(d) {
return d.ParentName;
});
let root = stratify(data);
console.log(root)
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
Since you already have the data parsed, it's even easier:
const data = [{
categoryname: 'Category 1',
parentname: ''
},
{
categoryname: 'Category 2',
parentname: 'Category 1'
},
{
categoryname: 'Category 3',
parentname: 'Category 2'
},
{
categoryname: 'Category 4',
parentname: 'Category 2'
},
];
const stratify = d3.stratify()
.id(function(d) {
return d.categoryname;
})
.parentId(function(d) {
return d.parentname;
});
let root = stratify(data);
console.log(root)
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
PS: use your browser's console to check the root object, not the Stack snippet one.
Related
I have a problem where the node always move left. Here is my code:
<div id="tree-container"></div>
<script src="<?php echo base_url('d3js-nettree/js/d3.js');?>" type="text/javascript"></script>
<script>
<?php if(isset($params['user_code'])){ ?>
treeJSON = d3.json("<?php echo base_url('d3js-parse/'.$params['user_code'].'.json');?>", function(error, treeData) {
// Calculate total nodes, max label length
var totalNodes = 0;
var maxLabelLength = 0;
// Misc. variables
var zoomFactor = 1;
var i = 0;
var duration = 750;
var root;
var rectWidth = 160, rectHeight = 125;
// size of the diagram
var viewerWidth = $("#tree-container").width();
var viewerHeight = 500;
var tree = d3.layout.tree()
.size([viewerWidth, viewerHeight]);
// define a d3 diagonal projection for use by the node paths later on.
var diagonal = d3.svg.diagonal()
.projection(function(d) {
return [d.x, d.y];
});
// A recursive helper function for performing some setup by walking through all nodes
function visit(parent, visitFn, childrenFn) {
if (!parent) return;
visitFn(parent);
var children = childrenFn(parent);
if (children) {
var count = children.length;
for (var i = 0; i < count; i++) {
visit(children[i], visitFn, childrenFn);
}
}
}
// Define the zoom function for the zoomable tree
function zoom() {
svgGroup.attr("transform", "translate(" + d3.event.translate + ")scale(" + d3.event.scale + ")");
}
// define the zoomListener which calls the zoom function on the "zoom" event constrained within the scaleExtents
var zoomListener = d3.behavior.zoom().scaleExtent([0.1, 3]).on("zoom", zoom);
d3.selectAll('button').on('click', function(){
if(this.id === 'zoom_in') {
zoomFactor = zoomFactor + 0.2;
zoomListener.scale(zoomFactor).event(d3.select("#tree-container"));
}
else if(this.id === 'zoom_out') {
zoomFactor = zoomFactor - 0.2;
zoomListener.scale(zoomFactor).event(d3.select("#tree-container"));
}
else if(this.id === 'up_level') {
updateNewTree("ID04838614");
}
});
// define the baseSvg, attaching a class for styling and the zoomListener
var baseSvg = d3.select("#tree-container").append("svg")
.attr("width", viewerWidth)
.attr("height", viewerHeight)
.attr("class", "overlay")
.call(zoomListener);
// Function to center node when clicked/dropped so node doesn't get lost when collapsing/moving with large amount of children.
function centerNode(source) {
scale = zoomListener.scale();
x = -source.x0;
y = -source.y0;
x = x * scale + viewerWidth / 2;
y = y * scale + viewerHeight / 2;
d3.select('g').transition()
.duration(duration)
.attr("transform", "translate(" + x + "," + y + ")scale(" + scale + ")");
zoomListener.scale(scale);
zoomListener.translate([x, y]);
}
// Call visit function to establish maxLabelLength
visit(treeData, function(d) {
totalNodes++;
maxLabelLength = Math.max(d.distributor_code.length, maxLabelLength);
}, function(d) {
return d.children && d.children.length > 0 ? d.children : null;
});
// define click event
function click(d) {
console.log("clicked");
// if (d3.event.defaultPrevented) return; // click suppressed
//d = toggleChildren(d);
if(d.url !== "") {
window.open(d.url, "_self");
} else {
updateNewTree(d.distributor_code);
}
update(d);
centerNode(d);
}
function update(source) {
// Compute the new height, function counts total children of root node and sets tree height accordingly.
// This prevents the layout looking squashed when new nodes are made visible or looking sparse when nodes are removed
// This makes the layout more consistent.
var levelWidth = [1];
var childCount = function(level, n) {
if (n.children && n.children.length > 0) {
if (levelWidth.length <= level + 1) levelWidth.push(0);
levelWidth[level + 1] += n.children.length;
n.children.forEach(function(d) {
childCount(level + 1, d);
});
}
};
childCount(0, root);
var newWidth = d3.max(levelWidth) * 300; // 300 pixels per line
tree = tree.size([newWidth, viewerHeight]);
// Compute the new tree layout.
var nodes = tree.nodes(root).reverse(),
links = tree.links(nodes);
// Set widths between levels based on maxLabelLength.
nodes.forEach(function(d) {
//d.y = (d.depth * (maxLabelLength * 10)); //maxLabelLength * 10px
// alternatively to keep a fixed scale one can set a fixed depth per level
// Normalize for fixed-depth by commenting out below line
d.y = (d.depth * 200); //200px per level.
});
// Update the nodes…
node = svgGroup.selectAll("g.node")
.data(nodes, function(d) {
return d.id || (d.id = ++i);
});
// Enter any new nodes at the parent's previous position.
var nodeEnter = node.enter().append("g")
.attr("class", "node")
.attr("transform", function(d) {
return "translate(" + source.x0 + "," + source.y0 + ")";
})
.on('click', click);
nodeEnter.append("image")
.attr("href", "<?php echo base_url('assets/images/people.png');?>")
.attr("x", -rectWidth/4)
.attr("y", -rectHeight-75)
.attr("width", 75)
.attr("height", 75);
nodeEnter.append("rect")
.attr('class', 'nodeRect')
.attr("x", -rectWidth/2)
.attr("y", -rectHeight)
.attr("rx", 10)
.attr("ry", 10)
.attr("width", rectWidth)
.attr("height", rectHeight)
.style("fill", function(d) {
//return d._children ? "lightsteelblue" : "#fff";
});
nodeEnter.append("text")
.attr('class', 'txt1')
.attr("x", 0)
.attr("y", -rectHeight+15)
.attr('class', 'textBold')
.attr("text-anchor", "middle")
.text(function(d) {
if(d.distributor_code === "") return "";
else return d.user_code;
});
nodeEnter.append("text")
.attr('class', 'txt2')
.attr("x", 0)
.attr("y", -rectHeight+25)
.attr("text-anchor", "middle")
.text(function(d) {
if(d.distributor_code === "") return "";
else return d.fullname;
});
//IT GOES ON FOR SEVERAL MORE
// Transition nodes to their new position.
var nodeUpdate = node.transition()
.duration(duration)
.attr("transform", function(d) {
return "translate(" + d.x + "," + d.y + ")";
});
// Fade the text in
nodeUpdate.select("text")
.style("fill-opacity", 1);
// Transition exiting nodes to the parent's new position.
var nodeExit = node.exit().transition()
.duration(duration)
.attr("transform", function(d) {
return "translate(" + source.x + "," + source.y + ")";
})
.remove();
nodeExit.select("rect")
.attr("width", 0)
.attr("height", 0);
nodeExit.select("text")
.style("fill-opacity", 0);
nodeExit.select("image")
.style("display", "none");
// Update the links…
var link = svgGroup.selectAll("path.link")
.data(links, function(d) {
return d.target.id;
});
// Enter any new links at the parent's previous position.
link.enter().insert("path", "g")
.attr("class", "link")
.attr("d", function(d) {
var o = {
x: source.x0,
y: source.y0
};
return diagonal({
source: o,
target: o
});
});
// Transition links to their new position.
link.transition()
.duration(duration)
.attr("d", diagonal);
// Transition exiting nodes to the parent's new position.
link.exit().transition()
.duration(duration)
.attr("d", function(d) {
var o = {
x: source.x,
y: source.y
};
return diagonal({
source: o,
target: o
});
})
.remove();
// Stash the old positions for transition.
nodes.forEach(function(d) {
d.x0 = d.x;
d.y0 = d.y;
});
}
function updateNewTree($base_id) {
// Get the data again
d3.json("nettree-alt.json", function(error, treeData) {
// Call visit function to establish maxLabelLength
visit(treeData, function(d) {
totalNodes++;
maxLabelLength = Math.max(d.distributor_code.length, maxLabelLength);
}, function(d) {
return d.children && d.children.length > 0 ? d.children : null;
});
root = treeData;
root.x0 = viewerHeight / 2;
root.y0 = 0;
// Layout the tree initially and center on the root node.
update(root);
centerNode(root);
});
}
// Append a group which holds all nodes and which the zoom Listener can act upon.
var svgGroup = baseSvg.append("g");
// Define the root
root = treeData;
root.x0 = viewerHeight / 2;
root.y0 = -100;
// Layout the tree initially and center on the root node.
update(root);
centerNode(root);
});
<?php } ?>
function EnableTreeMode(){
$('.tree').treegrid({
expanderExpandedClass: 'glyphicon glyphicon-minus',
expanderCollapsedClass: 'glyphicon glyphicon-plus'
});
$('.tree').treegrid('collapseAll');
}
EnableTreeMode();
function collapse(){
$('.tree').treegrid('collapseAll');
}
function expand(){
$('.tree').treegrid('expandAll');
}
</script>
The problem is like this, i have a "tree" that consist of users which pulls data from a json file:
USER 1
/\
USER 2 USER 3
What i was expecting when i remove a user, say user 2, the "tree" should be like this:
USER 1
/\
NONE USER 3
But instead the result i got were like this:
USER 1
/\
USER 3 NONE
After i deleted USER 2 the USER 3, which is on the right node, moved on the left node. I have tried debugging my code line by line but still no result.
TIA.
I am programming a collapsibleTree in R, which is using JavaScript. Unfortunately it looks liek this:
I can choose the y axis of the label, but they are always threated the same height. I want to have them alternating a bit on top and bottom of the knot, so that long texts dont overlap. Here is the code, I really have no clue because I never programmed JavaScript.
Thank you
HTMLWidgets.widget({
name: 'collapsibleTree',
type: 'output',
factory: function(el, width, height) {
var i = 0,
duration = 750,
root = {},
options = {},
treemap;
// Optionally enable zooming, and limit to 1/5x or 5x of the original viewport
var zoom = d3.zoom()
.scaleExtent([1/5, 5])
.on('zoom', function () {
svg.attr('transform', d3.event.transform)
})
// create our tree object and bind it to the element
// appends a 'group' element to 'svg'
// moves the 'group' element to the top left margin
var svg = d3.select(el).append('svg')
.attr('width', width)
.attr('height', height)
.append('g');
// Define the div for the tooltip
var tooltip = d3.select(el).append('div')
.attr('class', 'tooltip')
.style('opacity', 0);
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 * options.linkLength});
// ****************** 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 tooltips, if specified in options
if (options.tooltip) {
nodeEnter = nodeEnter
.on('mouseover', mouseover)
.on('mouseout', mouseout);
}
// Enable zooming, if specified
if (options.zoomable) d3.select(el).select('svg').call(zoom)
// Add Circle for the nodes
nodeEnter.append('circle')
.attr('class', 'node')
.attr('r', 1e-6)
.style('fill', function(d) {
return d.data.fill || (d._children ? options.fill : '#fff');
})
.style('stroke-width', function(d) {
return d._children ? 3 : 1;
});
// Add labels for the nodes
nodeEnter.append('text')
// .attr('dy', '1em')
// .attr('class', 'place-label')
// .attr('text-anchor', 'middle')
// .attr('separation', '2')
.attr('x', function(d) {
// Scale padding for label to the size of node
var padding = (d.data.SizeOfNode || 10) + 3
return d.children || d._children ? -1 * padding : padding;
})
.attr('text-anchor', function(d) {
return d.children || d._children ? 'end' : 'start';
})
.style('font-size', options.fontSize + 'px')
.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', function(d) {
return d.data.SizeOfNode || 10; // default radius is 10
})
.style('fill', function(d) {
return d.data.fill || (d._children ? options.fill : '#fff');
})
.style('stroke-width', function(d) {
return d._children ? 3 : 1;
})
.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')
// Potentially, this may one day be mappable
// .style('stroke-width', function(d) { return d.data.linkWidth || 1 })
.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) {
d._children = d.children;
d.children = null;
} else {
d.children = d._children;
d._children = null;
}
update(d);
// Hide the tooltip after clicking
tooltip.transition()
.duration(100)
.style('opacity', 0)
// Update Shiny inputs, if applicable
if (options.input) {
var nest = {},
obj = d;
// Navigate up the list and recursively find parental nodes
for (var n = d.depth; n > 0; n--) {
nest[options.hierarchy[n-1]] = obj.data.name
obj = obj.parent
}
Shiny.onInputChange(options.input, nest)
}
}
// Show tooltip on mouseover
function mouseover(d) {
tooltip.transition()
.duration(200)
.style('opacity', .9);
// Show either a constructed tooltip, or override with one from the data
tooltip.html(
d.data.tooltip || d.data.name + '<br>' +
options.attribute + ': ' + d.data.WeightOfNode
)
// Make the tooltip font size just a little bit bigger
.style('font-size', (options.fontSize + 1) + 'px')
.style('left', (d3.event.layerX) + 'px')
.style('top', (d3.event.layerY - 30) + 'px');
}
// Hide tooltip on mouseout
function mouseout(d) {
tooltip.transition()
.duration(500)
.style('opacity', 0);
}
}
return {
renderValue: function(x) {
// Assigns parent, children, height, depth
root = d3.hierarchy(x.data, function(d) { return d.children; });
root.x0 = height / 2;
root.y0 = 0;
// Attach options as a property of the instance
options = x.options;
// Update the canvas with the new dimensions
svg = svg.attr('transform', 'translate('
+ options.margin.left + ',' + options.margin.top + ')')
// width and height, corrected for margins
var heightMargin = height - options.margin.top - options.margin.bottom,
widthMargin = width - options.margin.left - options.margin.right;
// declares a tree layout and assigns the size
treemap = d3.tree().size([heightMargin, widthMargin])
.separation(separationFun);
// Calculate a reasonable link length, if not otherwise specified
if (!options.linkLength) {
options.linkResponsive = true
options.linkLength = widthMargin / options.hierarchy.length
if (options.linkLength < 10) {
options.linkLength = 10 // Offscreen or too short
}
}
// Optionally collapse after the second level
if (options.collapsed) 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
}
}
},
resize: function(width, height) {
// Resize the canvas
d3.select(el).select('svg')
.attr('width', width)
.attr('height', height);
// width and height, corrected for margins
var heightMargin = height - options.margin.top - options.margin.bottom,
widthMargin = width - options.margin.left - options.margin.right;
// Calculate a reasonable link length, if not originally specified
if (options.linkResponsive) {
options.linkLength = widthMargin / options.hierarchy.length
if (options.linkLength < 10) {
options.linkLength = 10 // Offscreen or too short
}
}
// Update the treemap to fit the new canvas size
treemap = d3.tree().size([heightMargin, widthMargin])
.separation(separationFun);
update(root)
},
// Make the instance properties available as a property of the widget
svg: svg,
root: root,
options: options
};
}
});
function separationFun(a, b) {
var height = a.data.SizeOfNode + b.data.SizeOfNode,
// Scale distance to SizeOfNode, if defined
distance = (height || 20) / 10;
return (a.parent === b.parent ? 1 : distance);
};
I am a beginner in d3.js and I am trying to do a collapsible horizontal tree with d3.js. I started with this example :
http://blockbuilder.org/d3noob/43a860bc0024792f8803bba8ca0d5ecd
There is one flaw that I try to solve but without success. It is when you click multiple times quickly on the same circle, it launches the same animations many times and it broke the tree.
So I would like to know if there is a possibility to make the click event disabled during the animation and make it available at the end ?
I tried using this method to remove the event and put it back, but it does not work.
Thanks for your help.
Rather than using timeouts or removing and adding event listeners or tracking transitions, you can check to see if a transition is occurring on any individual node with d3.active(node). It will return either null (in the event of no transition) or the transition if one is taking place. If a transition is occurring, ignore the click event (or more accurately, don't carry out any actions on click events if a transition is happening).
First let's check to see if a selection has transitioning elements:
function isTransitioning(selection) {
var transitioning = false;
selection.each(function() { if(d3.active(this)) { transitioning = true; } })
return transitioning;
}
Then let's apply that in the click function in your linked example:
function click(d) {
// Are no transitions happening?
if(!isTransitioning(d3.selectAll("circle"))) {
// If none are continue with updating the graph:
if (d.children) {
d._children = d.children;
d.children = null;
} else {
d.children = d._children;
d._children = null;
}
update(d);
}
}
Giving us:
var treeData =
{
"name": "Top Level",
"children": [
{
"name": "Level 2: A",
"children": [
{ "name": "Son of A" },
{ "name": "Daughter of A" }
]
},
{ "name": "Level 2: B" }
]
};
// Set the dimensions and margins of the diagram
var margin = {top: 20, right: 90, bottom: 30, left: 90},
width = 600 - margin.left - margin.right,
height = 300 - 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(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 * 180});
// ****************** 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", ".35em")
.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', 10)
.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(!isTransitioning(d3.selectAll("circle"))) {
if (d.children) {
d._children = d.children;
d.children = null;
} else {
d.children = d._children;
d._children = null;
}
update(d);
}
}
}
function isTransitioning(selection) {
var transitioning = false;
selection.each(function() { if(d3.active(this)) { transitioning = true; } })
return transitioning;
}
.node circle {
fill: #fff;
stroke: steelblue;
stroke-width: 3px;
}
.node text {
font: 12px sans-serif;
}
.link {
fill: none;
stroke: #ccc;
stroke-width: 2px;
}
<script src="https://d3js.org/d3.v4.min.js"></script>
I had good results with selecting only circles (you can click twice without error and without stopping the transition from the looks of it), but in some cases you might want to select all items with a transition. In this example if selecting all the gs you have to wait until all transitions are done before updating again by click:
var treeData =
{
"name": "Top Level",
"children": [
{
"name": "Level 2: A",
"children": [
{ "name": "Son of A" },
{ "name": "Daughter of A" }
]
},
{ "name": "Level 2: B" }
]
};
// Set the dimensions and margins of the diagram
var margin = {top: 20, right: 90, bottom: 30, left: 90},
width = 600 - margin.left - margin.right,
height = 300 - 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(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 * 180});
// ****************** 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", ".35em")
.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', 10)
.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(!isTransitioning(d3.selectAll("g"))) {
if (d.children) {
d._children = d.children;
d.children = null;
} else {
d.children = d._children;
d._children = null;
}
update(d);
}
}
}
function isTransitioning(selection) {
var transitioning = false;
selection.each(function() { if(d3.active(this)) { transitioning = true; } })
return transitioning;
}
.node circle {
fill: #fff;
stroke: steelblue;
stroke-width: 3px;
}
.node text {
font: 12px sans-serif;
}
.link {
fill: none;
stroke: #ccc;
stroke-width: 2px;
}
<script src="https://d3js.org/d3.v4.min.js"></script>
You can maybe grab the onclik event and throttle the click the duration of the animation in this case 750 milisecs
You need to be able to tell if an animation is currently ongoing, and also deal with multiple animations being possible at once. I made a fork here: http://blockbuilder.org/WilliamNHarvey/4a39d034cef90be249b5ab003ecf775d
Make an array that holds current animations
var animations = []
Then on click, check if the current node is already animated. If so, cancel the click event. If not, add it to the animations queue. Set a timeout to remove it after 750 milliseconds
function click(d) {
if (isAnimated(d)) return;
animations.push(d);
setTimeout(function(){ removeAnimation(d) }, duration);
if (d.children) {
d._children = d.children;
d.children = null;
} else {
d.children = d._children;
d._children = null;
}
update(d);
}
The two functions here, isAnimated and removeAnimation, simply loop through the nodes to find the one you're looking for
function isAnimated(d) {
res = false;
animations.forEach(function(e, i) {
if(e.id == d.id) res = true;
});
return res;
}
function removeAnimation(d) {
animations.forEach(function(e, i) {
if(e.id == d.id) animations.splice(i, 1);
});
}
I am using d3.tree to create my tree hierarchy, I am relatively new to this lib, so I need a little help.
var treeData = {{ data | safe }};
// Set the dimensions and margins of the diagram
var margin = {top: 30, right: 90, bottom: 30, left: 150},
width = $('#tree-container').width(),
height = $('#tree-container').height();
// 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("#tree-container").append("svg")
.attr("width", width)
.attr("height", height)
.call(d3.zoom().on("zoom", function () {
svg.attr("transform", d3.event.transform)
}))
.append("g")
.attr("width", width)
.attr("height", height)
.attr("id", "place")
.attr("transform", "translate("
+ (width/2) + "," + margin.top + ")");
var i = 0,
duration = 750,
root;
// declares a tree layout and assigns the size
var treemap = d3.tree()
.nodeSize([70, 10]);
// Assigns parent, children, height, depth
root = d3.hierarchy(treeData, function(d) { return d.children; });
root.x0 = 0;
root.y0 = width / 2;
// 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
}
}
var nodes;
function update(source) {
// Assigns the x and y position for the nodes
var treeData = treemap(root);
// Compute the new tree layout.
nodes = treeData.descendants();
links = treeData.descendants().slice(1);
// Normalize for fixed-depth.
nodes.forEach(function(d){ //HERE
d.y = d.depth * 120;
});
// ****************** 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.x0 + "," + source.y0 + ")";
});
// Add Circle for the nodes
nodeEnter.append('circle')
.attr('class', 'node')
.attr('id', function(d) { return "circle-"+d.data.name; })
.attr('r', 1e-6)
.style("fill", function(d) {
return d._children ? "lightsteelblue" : "#fff";// make text color appear read or as data.color
}).on('click', click);
// Add labels for the nodes
nodeEnter.append('text')
.attr("dy", "0.45em")
.attr("y", function(d) {
return d.children || d._children ? -13 : 13;
})
.attr("text-anchor", function(d) {
return d.children || d._children ? "end" : "start";
})
.attr("cursor", "pointer")
.text(function(d) { return d.data.name; })
.on("click", textClick);
// 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.x + "," + d.y + ")";
});
// Update the node attributes and style
nodeUpdate.select('circle.node')
.attr('r', 10)
.style("fill", function(d) { return d._children ? d.data.ncolor : "#fff"; }) // change color of inner circle
.style("stroke", function (d) { return d.data.ncolor; }) // changing color of outere circle
.attr('cursor', 'pointer');
// Remove any exiting nodes
var nodeExit = node.exit().transition()
.duration(duration)
.attr("transform", function(d) {
return "translate(" + source.x + "," + source.y + ")";
})
.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")
.style("stroke", function (d) { return d.data.color; }) // Place where color of previous link changes
.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.x + "," + s.y +
"C" + (s.x + d.x) / 2 + " " + s.y + ","
+ (s.x + d.x) / 2 + " " + d.y + ","
+ d.x + " " + d.y;
return path
}
// Toggle children on click.
function click(d) {
conosle.log();
if (d.children) {
d._children = d.children;
d.children = null;
} else {
d.children = d._children;
d._children = null;
}
update(d);
}
It actually works, but what I am looking for is the way to have everything inside my viewing window. Like if someone click on node it expands and if children nodes overflow(go out of the window), I need to zoom out, so they will be visible. Hope there is d3 jedi-master to answer me :)))
This is very hard to answer without seeing your actual data or a running version of your code, but here is a suggestion. When you do this:
var treemap = d3.tree()
.nodeSize([70, 10]);
You're setting the size of each node, but not the whole layout. Also, according to the API,
When a node size is specified, the root node is always positioned at ⟨0, 0⟩.
Here is an example: I just forked Bostock's tree example, setting the nodeSize, you can see that the nodes are going outside the SVG: http://blockbuilder.org/anonymous/7a84944610c7e6b3b9ebf063977955c9
So, a possible solution is using size instead of nodeSize:
var treemap = d3.tree()
.nodeSize([width, height]);
Here is the original tree example using size, for comparison: http://blockbuilder.org/mbostock/4339083
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?
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>
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);