I have a d3 (v5) tree and have added labels to each path. All the methods to center the label on the path involve using a combination of dividing, subtracting x and y between the parent and the child. This approach is unable to find the true center of a path when nodes are moved, readjusted etc on a diagonal. Is there a approach to use the path and not the coordinates of the parent/child to find the center?
// Update the links...
var link = svg.selectAll('g.link')
.data(links, function(d) {
return d.id;
});
// Enter any new links at the parent's previous position.
var linkEnter = link.enter().insert('g', 'g')
.attr("class", "link")
.attr("stroke-width", function(d){
return d.data.toParentStrokeWidth;
})
.attr("stroke", function(d){
return '#868aa8';//added for future variable color
})
linkEnter.append('path')
.attr('d', function(d) {var o = {x: source.x0, y: source.y0}; return diagonal(o, o)});
linkEnter.append("foreignObject")
.attr("width", "40")
.attr("height", "40")
.append('xhtml').html(function(d) {
return '<div class="treeLabel">' + d.data.name + '</div>'
})
// UPDATE
var linkUpdate = linkEnter.merge(link);
// Transition back to the parent element position
linkUpdate.select('path').transition()
.duration(duration)
.attr('d', function(d) {
return diagonal(d, d.parent)
});
linkUpdate.select('foreignObject').transition()
.duration(duration)
.attr('transform', function(d){
if (d.parent) {
return 'translate(' + ((d.parent.y + d.y) / 2) + ',' + ((d.parent.x + d.x) / 2) + ')'
}
})
// Remove any exiting links
link.exit().each(function(d) {
d.parent.index = 0;
})
var linkExit = link.exit()
.transition()
.duration(duration);
linkExit.select('path')
.attr('d', function(d) {var o = {x: source.x, y: source.y}; return diagonal(o, o)})
linkExit.select('text')
.style('opacity', 0);
linkExit.remove();
UPDATE Modified to correct class issue
Related
I am new to stackoverflow, forgive me if i made any mistake.
I have updated my D3 v5 tree code so that the root starts from top left. By referring to one of the stack overflow (D3 Tree orientation) i was able to achieve root starting from top left, but the children are not starting with position of Y-axis as there parent, it starts from 0th position of y-axis. Here is the issue i am facing please find the screenshot..
enter image description here
Please suggest solution to fix this issue.
Here is my code..
<script>
// Set the dimensions and margins of the diagram
var margin = {top: 20, right: 90, bottom: 30, left: 90},
width = 2000 - margin.left - margin.right,
height = 2500 - margin.top - margin.bottom;
svgid = "svgid";
// 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)
.attr("id", svgid)
.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]);
function draw_tree(start_at){
/*document.getElementById("svgid").scrollIntoView({block: "center"});*/
d3.json(start_at).then(function(data){
var treeData = data;
// 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 * 340});*/
var nodeMap = {};
nodes.forEach(function(d) {
if (!nodeMap[d.depth] || d.x < nodeMap[d.depth]){
nodeMap[d.depth] = d.x;
}
});
nodes.forEach(function(d) {
d.y = d.depth * 300;
d.x -= nodeMap[d.depth];
});
// ****************** 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);
var rectHeight = 140, rectWidth = 240;
nodeEnter.append('rect')
.attr('class', 'node')
.attr("width", rectWidth)
.attr("height", rectHeight)
.attr("x", 0)
.attr("y", -4)
.attr("rx","5")
.on("mouseover", mouseover)
.on("mousemove", function(d){mousemove(d);})
.on("mouseout", mouseout)
.style("fill", function(d) {
return "#0091DA";
});
// Add labels for the nodes
nodeEnter.append('text')
.attr("dy", "-.35em")
.attr("x", function(d) {
return 13;
})
.attr("text-anchor", function(d) {
return "start";
})
.each(function (d) {
d3.select(this).append("tspan")
.attr("dy", "1.75em")
.html(function (d) {
return "Freq: "+d.data.frequency;
})
.attr("x", function(d) {
return 13;
})
d3.select(this).append("tspan")
.attr("dy", "1.75em")
.html(function (d) {
return "PD: "+d.data.page_domain;
})
.attr("x", function(d) {
return 13;
})
var lines = wordwrap(d.data.page_name, 35)
for (var i = 0; i < lines.length; i++) {
if(i == 0){
d3.select(this).append("tspan")
.attr("dy", "1.75em")
.text("PN: "+lines[i])
.attr("x", function(d) {
return 13;
})
}else{
d3.select(this).append("tspan")
.attr("dy", "1.75em")
.text(lines[i])
.attr("x", function(d) {
return 13;
})
}
}
d3.select(this).append("tspan")
.attr("dy", "1.75em")
.html(function (d) {
return "PL: <a href="+d.data.page_link+" target='_blank'>Link</a>";
})
.attr("x", function(d) {
return 13;
})
});
// 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)
});
// Transition links to their new position.
link.transition()
.duration(duration)
.attr("d", diagonal);
// 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;
}
if (d.parent) {
d.parent.children.forEach(function(element) {
if (d !== element) {
collapse(element);
}
});
}
update(d);
}
}
});
}
</script>
I am working on a treediagram in d3 and I am now trying to 'mark out' a path. So when the user click on a certain link in the three it turns red.
The problem I am facing is that. When I click my link I get the right id stored in "d.id" but I cannot give it style that way
d3.select('._' + d.id).style("stroke", "red")
This is the rest of the links code.
let link = svg.selectAll('path.link')
.data(links, function(d) { return d.id; })
// Enter any new links at the parent's previous position.
let linkEnter = link.enter().insert('path', "g")
.attr("class", "link")
.on('click', pathdir)
.attr('d', function(d){
let o = {x: source.x0, y: source.y0}
return diagonal(o, o)
});
// UPDATE
let 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
let linkExit = link.exit().transition()
.duration(duration)
.attr('d', function(d) {
let 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;
});
function diagonal(s, d) {
const 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
}
function pathdir(event, d){
d3.select('._' + d.id).style("stroke", "red")
}
It's my first time coding d3 so any help is useful!
I am trying the implementation of the collapsible tree in d3 v4. I was plying with this example and realized, it is using a custom function to create the link shape
// 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
}
Since d3 v 4.9 there is a built-in link generator and I wonder how can it be used in this example.
I have troubles understanding following calls
// 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)
});
....
// 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();
I understand, that this is creating a curved line from point(x,y) to point (x,y) - so basically from and to the same point ?
Furthermore, I tried to update
// Transition back to the parent element position
linkUpdate.transition()
.duration(duration)
.attr('d', function(d){ return diagonal(d, d.parent) });
with following code
// Transition back to the parent element position
linkUpdate.transition()
.duration(duration)
.attr('d', d3.linkHorizontal(d, d.parent))
but I got *Uncaught ReferenceError: d is not defined*
// Transition back to the parent element position
linkUpdate.transition()
.duration(duration)
.attr('d', d3.linkHorizontal()
.source(function (d) {return d.parent})
.target(function (d) {return d})
);
but I got lot of errors in the console
d3.v4.min.js:2 Error: <path> attribute d: Expected number, "MNaN,NaNCNaN,NaN,…".
Could someone explain my mistake or point me to some working code ? Many thanks!
I think I figured out most of the confusion from my original answer.
Creating the "zero length path" on link creation
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)
});
and on link removal
var linkExit = link.exit().transition()
.duration(duration)
.attr('d', function(d) {
var o = {x: source.x, y: source.y}
return diagonal(o, o)
})
.remove();
is used to create the animation, when the nodes, together with the related links slides-out/retrieves-back-in their parent nodes. The animation animates transformation of the path to/from their final shape from/to the "null" shape - thus the link starting and finishing at the same coordinates.
The link generator can be then used like this
// Enter any new links at the parent's previous position.
var linkEnter = link.enter().insert('path', "g")
.attr("class", "link")
.attr('d', d3.linkHorizontal()
.source(function(){ return [sourceNode.y0, sourceNode.x0]})
.target(function(){ return [sourceNode.y0, sourceNode.x0]}));
...
// Transition back to the parent element position
linkUpdate.transition()
.duration(duration)
.attr('d', d3.linkHorizontal()
.source(function (d) {return [d.parent.y, d.parent.x]})
.target(function (d) {return [d.y, d.x]})
);
...
// Remove any exiting links
var linkExit = link.exit().transition()
.duration(duration)
.attr('d', d3.linkHorizontal()
.source(function(){ return [sourceNode.y, sourceNode.x]})
.target(function(){ return [sourceNode.y, sourceNode.x]}))
.remove();
I know it is too late but here is a great example of how to animate the path https://observablehq.com/#onoratod/animate-a-path-in-d3. Just build a path then animate it.
const linkGen = d3.linkHorizontal();
const paths = linksData.map(d => {
return { path: linkGen({target:...,source:...}), color: d.color }
})
paths.map(path => {
var path = svg.append("path")
.attr("d", path.path)
.attr("fill", "none")
.attr("stroke-width", 2)
.attr("stroke", path.color);
const length = path.node().getTotalLength();
// This function will animate the path over and over again
// Animate the path by setting the initial offset and dasharray and then transition the offset to 0
path.attr("stroke-dasharray", length + " " + length)
.attr("stroke-dashoffset", length)
.transition()
.ease(d3.easeLinear)
.attr("stroke-dashoffset", 0)
.duration(3000)
})
I am using collapsible tree as shown in this link. My data is very big so the view come out of this is collapsing with each other. screenshot is given below :-
Currently I am looking into this link . It is helping me to increase size of the tree dynamically. By this, I can change the merging of node as shown below :-
Now my question is how to increase the frame height dynamically?.
When I expand all the nodes then the tree size is more than canvas/frame height.
I have find a solution of this. I am changing the update function of Collapsible tree code. Below is my code:-
function update(source) {
var duration = d3.event && d3.event.altKey ? 5000 : 500;
// compute the new height
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);
newHeight = d3.max(levelWidth) * 60; // 20 pixels per line
tree = tree.size([newHeight, width]);
d3.select("svg").remove();//TO REMOVE THE ALREADY SVG CONTENTS AND RELOAD ON EVERY UPDATE
svg = d3.select("body").append("svg");
svg.attr("width", width + margin.right + margin.left)
.attr("height", newHeight + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
// 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")
.style("fill", function(d) { return d._children ? "lightsteelblue" : "#fff"; });
nodeEnter.append("text")
.attr("x", function(d) { return d.children || d._children ? -10 : 10; })
.attr("dy", "-.75em")
.attr("text-anchor", function(d) { return d.children || d._children ? "end" : "start"; })
.text(function(d) { return d.name; })
.style("fill-opacity", 1e-2);
nodeEnter.append("text")
.attr("x", function(d) { return d.children || d._children ? -10 : 10; })
.attr("dy", "1.00em")
.attr("text-anchor", function(d) { return d.children || d._children ? "end" : "start"; })
.text(function(d) { return d.info1; })
.style("fill-opacity", 1e-2);
// 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", 6)
.style("fill", function(d) { return d._children ? "lightsteelblue" : "#fff"; });
nodeUpdate.selectAll("text")
.style("fill-opacity", 4);
// 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.selectAll("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;
});
}
I am using a d3.js tree, and would like to add nodes dynamically, rather than pre-loading the entire tree.
How would i modify the following in order to dynamically add additional JSON nodes when a node is clicked? (see link below, and code below)
http://mbostock.github.com/d3/talk/20111018/tree.html
So rather than pre-loading the entire tree, instead i would like to retrieve the child nodes only when the parent is clicked. I can retrieve the json for the child nodes in the toggle function, however i can't figure out how i can add them to the tree.
var m = [20, 120, 20, 120],
w = 1280 - m[1] - m[3],
h = 800 - m[0] - m[2],
i = 0,
root;
var tree = d3.layout.tree()
.size([h, w]);
var diagonal = d3.svg.diagonal()
.projection(function(d) { return [d.y, d.x]; });
var vis = d3.select("#body").append("svg:svg")
.attr("width", w + m[1] + m[3])
.attr("height", h + m[0] + m[2])
.append("svg:g")
.attr("transform", "translate(" + m[3] + "," + m[0] + ")");
d3.json("flare.json", function(json) {
root = json;
root.x0 = h / 2;
root.y0 = 0;
function toggleAll(d) {
if (d.children) {
d.children.forEach(toggleAll);
toggle(d);
}
}
// Initialize the display to show a few nodes.
root.children.forEach(toggleAll);
toggle(root.children[1]);
toggle(root.children[1].children[2]);
toggle(root.children[9]);
toggle(root.children[9].children[0]);
update(root);
});
function update(source) {
var duration = d3.event && d3.event.altKey ? 5000 : 500;
// Compute the new tree layout.
var nodes = tree.nodes(root).reverse();
// Normalize for fixed-depth.
nodes.forEach(function(d) { d.y = d.depth * 180; });
// Update the nodes…
var node = vis.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("svg:g")
.attr("class", "node")
.attr("transform", function(d) { return "translate(" + source.y0 + "," + source.x0 + ")"; })
.on("click", function(d) { toggle(d); update(d); });
nodeEnter.append("svg:circle")
.attr("r", 1e-6)
.style("fill", function(d) { return d._children ? "lightsteelblue" : "#fff"; });
nodeEnter.append("svg: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 ? "lightsteelblue" : "#fff"; });
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 = vis.selectAll("path.link")
.data(tree.links(nodes), function(d) { return d.target.id; });
// Enter any new links at the parent's previous position.
link.enter().insert("svg:path", "g")
.attr("class", "link")
.attr("d", function(d) {
var o = {x: source.x0, y: source.y0};
return diagonal({source: o, target: o});
})
.transition()
.duration(duration)
.attr("d", diagonal);
// 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.
function toggle(d) {
// I could retrieve the child nodes here, but how to add them to the tree?
if (d.children) {
d._children = d.children;
d.children = null;
} else {
d.children = d._children;
d._children = null;
}
}
I was able to dynamically add nodes by adding the following code in the toggle function:
$.getJSON(addthese.json, function(addTheseJSON) {
var newnodes = tree.nodes(addTheseJSON.children).reverse();
d.children = newnodes[0];
update(d);
});
Notes: I am using jQuery to retrieve the json file
I'm new to D3, but this raw code might be helpful. You can create a JSON object and push it into the tree/link. Then it's just a matter of redrawing the tree.
function createTreeNode(source){
var current_node = tree.nodes(source);
var myJSONObject = {"name": "new Node","children": []};
if(current_node[0]._children!=null){
current_node[0]._children.push(myJSONObject);
console.log(current_node[0]._children);
source.children = source._children;
source._children = null;
}
else if(current_node[0].children!=null && current_node[0]._children!=null){
current_node[0].children.push(myJSONObject);
console.log(current_node[0].children);
}
else{
current_node[0].children=[]
current_node[0].children.push(myJSONObject);
console.log(current_node[0].children);
}
tree.links(current_node).push(current_node[current_node.length-1]);
navigate_tree(source);}