I am having some issues with the edges for the collapsible tree in D3. I have tried swapping the x and y positions and was able to move the nodes but not the edges? I tried to swap the x0 and y0 but still not working?
// 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});
});
http://jsfiddle.net/6FkBd/383/
You had to change this values of x and y too.
Check this one out http://jsfiddle.net/6FkBd/394/
var diagonal = d3.svg.diagonal()
.projection(function(d) { return [d.x, d.y]; });
By the way, I think we both working on same thing, I am trying to create OrgChart that will support collapsing/expanding. But not being lucky yet.
Related
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 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
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)
})
Summary
Using the example below I've created a database driven tree view with blocks of information instead of the circle nodes.
Interactive d3.js tree diagram
Please see the example screenshot below:
The idea is for the lines to start from where the block ends. I assume it's something to do with the following function:
// Custom projection
var linkProjection = d3.svg.diagonal()
.source(function (d) {
return { "y": d.source.y, "x": d.source.x };
})
.target(function (d) {
return { "y": d.target.y, "x": d.target.x };
})
.projection(function (d) {
return [d.y, d.x];
});
// Enter any new links at the parent's previous position.
link.enter().insert("path", "g")
.attr("class", "link")
.style("fill", "none")
.style("stroke", "#d1d6da")
.style("stroke-width", "1")
.attr("d", function (d) {
var s = { x: source.x0, y: source.y0 };
var t = { x: source.x0, y: source.y0 };
return linkProjection({ source: s, target: t });
});
I have tried adding the block width to the y coordinate but although it starts from the correct position with the drawing it ends at the start of the block again.
Any suggestions?
Without more code it's hard to say, but I think it has to do with the coordinate system. x,y point to the center of the object I think. So you'll have to translate y to the the right by half the length of your box.
i.e. The parents previous position (x,y) points to the center of the parent.
I'm having a bit of trouble getting a something to work with D3.js. Namely, I'm trying to make a tree of nodes, using the basic code from http://bl.ocks.org/mbostock/1021953.
I switched it to load the data inline, as opposed to loading from file, because I'm using it with a Rails application and don't want to have repetitive information. I switched the line so that you could see the format of my data.
Anyways, here's the bulk of my code:
<%= javascript_tag do %>
var nodes = [{"title":"Duncan's Death","id":"265"},{"title":"Nature Thrown Off","id":"266"},{"title":"Cows Dead","id":"267"},{"title":"Weather Bad","id":"268"},{"title":"Lighting kills man","id":"269"},{"title":"Macbeth's Rise","id":"270"}];
var links = [{"source":"265","target":"266","weight":"1"},{"source":"266","target":"267","weight":"1"},{"source":"266","target":"268","weight":"1"},{"source":"268","target":"269","weight":"1"}];
var firstelement = +links[0].source;
links.forEach(function(l) {
l.source = +l.source;
l.source = l.source-firstelement;
l.target = +l.target
l.target = l.target-firstelement;
});
var width = 960,
height = 500;
var color = d3.scale.category20();
var force = d3.layout.force()
.charge(-1000)
.linkDistance(300)
.size([width, height]);
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
force
.nodes(nodes)
.links(links)
.start();
var link = svg.selectAll(".link")
.data(links)
.enter().append("line")
.attr("class", "link")
.style("stroke-width", function(d) { return Math.sqrt(d.weight); });
var node = svg.selectAll(".node")
.data(nodes)
.enter().append("g")
.attr("class", "node")
.call(force.drag);
node.append("circle")
.attr("class", "circle_node")
.attr("r", 50)
.style("fill", function(d) { return color(d.id); })
node.append("title")
.text(function(d) { return d.title; });
node.append("text")
.attr("x", function(d) { return d.x; } )
.attr("y", function(d) { return d.y; })
.text(function(d) { return d.title; });
force.on("tick", function() {
link.attr("x1", function(d) { return d.source.x; })
.attr("y1", function(d) { return d.source.y; })
.attr("x2", function(d) { return d.target.x; })
.attr("y2", function(d) { return d.target.y; });
node.attr("x", function(a) { return a.x; })
.attr("y", function(a) { return a.y; });
});
<% end %>
This seems like it should work to me, but I can seem to manage it. The links work, but the nodes all remain in the top left corner. I've tried just entering the circles directly and appending the text to them (staying close to the source code I listed above,) but while the circles behave properly, it doesn't display the text. I'd like the title to be centered in the nodes.
More generally, I'm kind of confused by how this is working. What does "d" refer to within lines like
function(d) { return d.source.x; }
It seems to be declaring a function and calling it simultaneously. I know that it doesn't have to be specifically the character "d," (for instance, switching the "d" to an "a" seems to make no difference as long as it's done both in the declaration and within the function.) But what is it referring to? The data entered into the object that's being modified? For instance, if I wanted to print that out, (outside of the attribute,) how would I do it?
Sorry, I'm new to D3 (and fairly new to JavaScript in general,) so I have a feeling the answer is obvious, but I've been looking it up and through tutorials and I'm still lost. Thanks in advance.
First, there's a simple problem with your code that is causing all your nodes to stay in the top left corner. You are trying to position each node using the code:
node.attr("x", function(a) { return a.x; })
.attr("y", function(a) { return a.y; });
However, node is a selection of gs which do not take x and y attributes. Instead, you can move each node using translate transform, e.g.
node.attr("transform", function(d) { return "translate(" + d.x + "," + d.y + ")"; });
Making this change should allow the nodes to move around.
Next, moving to your question about "d", I think the first thing you need to understand is what you can do with a selection of elements in D3. From the docs: a selection (such as nodes) "is an array of elements pulled from the current document." Once you have a selection of elements, you can apply operators to change the attributes or style of the elements. You can also bind data to each element.
In your case, you are binding data to a selection of gs (nodes):
var node = svg.selectAll(".node")
.data(nodes)
.enter().append("g")
You are then using attr to change the position of each node. However, instead of setting the x and y attributes of each element to the same value, you are passing attr an anonymous function that will return a (presumably different) position for each node:
node.attr("x", function(a) { return a.x; })
.attr("y", function(a) { return a.y; });
This behavior is also explained in the docs for attr:
Attribute values and such are specified as either constants or
functions; the latter are evaluated for each element.
Thus, d represents an individual element (Object) in nodes.
So going back to your code, on each tick two things are happening:
The position of each node (data) is being recalculated by force.
Each corresponding element is then being moved to its new location by the anonymous function you pass to force.on.