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)
})
Related
Maybe I bit off more than I can chew as someone who has a vague memory of only seeing D3... but here I go. I am using R shiny and r2d3. I've copy pasted some very basic examples of r2d3 from here (https://rstudio.github.io/r2d3/) to kind of feel out how to actually incorporate any d3 into r/ 'kicking the tires'. For what I'm currently working with and what I actually want to modify for r2d3 is this here ->(https://observablehq.com/#d3/tree). I've made a few modifications just playing around with it, as pasted below. Now it comes to actually modifying it to be used for r2d3.
I know that the .js file already includes the svg, data which was passed into the function, width, height, options, theme. So I did my best to remove all those things, but it could be I missed it because I don't exactly know what Im looking at for each of these lines. Now I'm at a point where something is displaying; however, it is just what I believe to be only the root node circle with no labels. When the node is grey, it means it has no children, so this lonely node appearing has no children. Also when I hover my mouse it does recognize a mouseover and change in color. This makes me believe I'm missing something really small and dumb - At least I hope that is the case.
I inspected the element. I see an error that says
Uncaught TypeError: d3.select(...).append(...).duration is not a function
r2d3-script-454:192 at SVGCircleElement.
So I look this up and I come across this is stack overflow (Functions not recognised when using R2D3). I realize I don't actually know which of these functions need a dependency or how to find that out. If it is a dependency that I need to resolve this, I don't know what the URL would be or how to figure that out.
Server.R
library(shiny)
library(r2d3)
# download.file("https://d19vzq90twjlae.cloudfront.net/leaflet-0.7/leaflet.js", "leaflet-0.7.js")
# download.file("https://cdn.jsdelivr.net/gh/holtzy/D3-graph-gallery#master/LIB/d3-scale-radial.js", "d3-scale-radial.js")
# download.file("https://cdnjs.cloudflare.com/ajax/libs/d3-tip/0.7.1/d3-tip.min.js", "d3-tip.min.js")
server <- function(input, output) {
output$d3 <- renderD3({
r2d3(
runif(5, 0, input$bar_max),
script = system.file("examples/baranims.js", package = "r2d3")
)
})
output$d3dendrogram <- renderD3({
r2d3(
data = read.csv("flare.csv"),
d3_version = 4,
script = "r2d3dendrogram.js",
dependencies = list("leaflet-0.7.js",
"d3-scale-radial.js",
"d3-tip.min.js"))
})
}
ui.R
ui <- fluidPage(
inputPanel(
sliderInput("bar_max", label = "Max:",
min = 0, max = 1, value = 1, step = 0.05)
),
# d3Output("d3"),
d3Output("d3dendrogram")
)
r2d3dendrogram.js
//hardcoded variables
dy = 192
dy = width/6
dx = 30
tree = d3.tree().nodeSize([dx, dy])
margin = ({top: 10, right: 120, bottom: 10, left: 40})
diagonal = d3.linkHorizontal().x(d => d.y).y(d => d.x)
const root = d3.hierarchy(data);
root.x0 = dy / 2;
root.y0 = 0;
root.descendants().forEach((d, i) => {
d.id = i;
d._children = d.children;
if (d.depth && d.data.name.length !== 7) d.children = null;
});
const gLink = svg.append("g")
.attr("fill", "none")
.attr("stroke", "#555") //color of the tree link branches
.attr("stroke-opacity", 0.4)
.attr("stroke-width", 1.5);
const gNode = svg.append("g")
.attr("cursor", "pointer") //changes mouse into the pointing hand
.attr("pointer-events", "all");
//creates the div element to appear on hovering on a node
var div = d3.select("body").append("div")
//.attr("class", "tooltip")
.style("position", "absolute")
.style("text-align", "center")
.style("background", "black")
.style("border-radius", "8px")
.style("border", "solid 1px green")
.style("opacity", 0);
//controls the display that shows up on hovering on a node
function mouseover(d){
div.html("hello" + "<br/>"+"Everyone")
.style("left", (d3.event.pageX + 10) + "px")
.style("top", (d3.event.pageY - 15) + "px")
.style("opacity",1);
}
//controls how the div element from the hover acts when mouse goes away
function mouseout(){
div.style("opacity", 1e-6);
}
d3.selectAll("circle")
.on("mouseover", mouseover)
.on("mouseover", mouseout);
function update(source) {
const duration = d3.event && d3.event.altKey ? 2500 : 550;
const nodes = root.descendants().reverse();
const links = root.links();
// Compute the new tree layout.
tree(root);
let left = root;
let right = root;
root.eachBefore(node => {
if (node.x < left.x) left = node;
if (node.x > right.x) right = node;
});
const height = right.x - left.x + margin.top + margin.bottom;
const transition = svg.transition()
//.duration(duration)
.attr("viewBox", [-margin.left, left.x - margin.top, width, height])
.tween("resize", window.ResizeObserver ? null : () => () => svg.dispatch("toggle"));
// Update the nodes…
const node = gNode.selectAll("g")
.data(nodes, d => d.id);
// Enter any new nodes at the parent's previous position.
const nodeEnter = node.enter().append("g")
.attr("transform", d => `translate(${source.y0},${source.x0})`)
.attr("fill-opacity", 0)
.attr("stroke-opacity", 0)
.on("click", (event, d) => {
d.children = d.children ? null : d._children;
update(d)
})
//adds on the tooltip for the info that shows up on the hover
//content inside
nodeEnter.append("circle")
.attr("r", 6.5)
.attr("fill", d => d._children ? "#64B4FF" : "#999") /*creates the blue color or the grey color*/
.attr("stroke-width", 10)
.attr('transform', 'translate(0, 0)')
.on('mouseover', function (d, n, i) {
//Below is the hover feature for the nodes to change light blue
d3.select(this).transition()
.duration('10')
.attr('fill', '#E2F1FF') //fill color to be light blue
//Makes the new div appear on hover:
d3.select(this).append("div")
. duration('50')
.style("opacity", 1)
})
.on('mouseout', function (d,i) {
d3.select(this).transition()
//.duration('500')
.attr("fill", d => d._children ? "#64B4FF" : "#999"); /*creates the blue color || grey color*/
//Makes the new div disappear:
div.transition()
.duration('50')
.style("opacity", 1)
});
nodeEnter.append("text")
.attr("dy", "0.31em")
.attr("x", d => d._children ? -6 : 6)
.attr("text-anchor", d => d._children ? "end" : "start")
.text(d => d.data.name)
.clone(true).lower()
.attr("stroke-linejoin", "round")
.attr("stroke-width", 3)
.attr("stroke", "white");
// Transition nodes to their new position.
const nodeUpdate = node.merge(nodeEnter).transition(transition)
.attr("transform", d => `translate(${d.y},${d.x})`)
.attr("fill-opacity", 1)
.attr("stroke-opacity", 1);
// Transition exiting nodes to the parent's new position.
const nodeExit = node.exit().transition(transition).remove()
.attr("transform", d => `translate(${source.y},${source.x})`)
.attr("fill-opacity", 0)
.attr("stroke-opacity", 0);
// Update the links…
const link = gLink.selectAll("path")
.data(links, d => d.target.id);
// Enter any new links at the parent's previous position.
const linkEnter = link.enter().append("path")
.attr("d", d => {
const o = {x: source.x0, y: source.y0}
return diagonal({source: o, target: o});
});
// Transition links to their new position.
link.merge(linkEnter).transition(transition)
.attr("d", diagonal);
// Transition exiting nodes to the parent's new position.
link.exit().transition(transition).remove()
.attr("d", d => {
const o = {x: source.x, y: source.y};
return diagonal({source: o, target: o});
});
// Stash the old positions for transition.
root.eachBefore(d => {
d.x0 = d.x;
d.y0 = d.y;
});
}
update(root);
return svg.node();
Sidenote: I know d3 is huge. If anyone can offer any tips or anything else to learn about all of this (free would be a nice plus), let me know. I've watched some sections of a 19hr video online with Curran. It was somewhat useful, but this is some learning curve for me; I had to re-watch a few things over and over again.
Sidenote: Sorry, please ignore the slider bar. It shouldn't do anything.
edit - adding the javascript tag in case this is requires a javascript related solution
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 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.
I have an example of force directed graph. And i want to show arrowheads, but no matter what i tried i can't see them.
Here is my javascript
var nodes = {};
// Compute the distinct nodes from the links.
links.forEach(function(link) {
link.source = nodes[link.source] || (nodes[link.source] = {
name: link.source
});
link.target = nodes[link.target] || (nodes[link.target] = {
name: link.target
});
});
var width = 960,
height = 500;
var force = d3.layout.force()
.nodes(d3.values(nodes))
.links(links)
.size([width, height])
.linkDistance(100)
.charge(-300)
.on("tick", tick)
.start();
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
svg.append("svg:defs").selectAll("marker")
.data(["arrow"])
.enter().append("svg:marker")
.attr("id", "arrow")
.attr("viewBox","0 0 10 10")
.attr("refX","20")
.attr("refY","5")
.attr("markerUnits","strokeWidth")
.attr("markerWidth","9")
.attr("markerHeight","5")
.attr("orient","auto")
.append("svg:path")
.attr("d","M 0 0 L 10 5 L 0 10 z")
.attr("fill", "#f0f0f0");
var link = svg.append("svg:g").selectAll("line")
.data(force.links())
.enter().append("svg:line")
.attr("class", "link")
.attr("marker-mid", "url(#arrow)");
var node = svg.append("svg:g").selectAll(".node")
.data(force.nodes())
.enter().append("g")
.attr("class", "node")
.call(force.drag);
node.append("circle")
.attr("r", 8);
node.append("text")
.attr("x", -22)
.text(function(d) {
return d.name;
});
function tick() {
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;
});
link.attr("marker-mid", "url(#arrow)");
node.attr("transform", function(d) {
return "translate(" + d.x + "," + d.y + ")";
});
}
And here is my jsfiddle
From the MDN docu on marker-mid:
The marker-mid defines the arrowhead or polymarker that shall be drawn at every vertex other than the first and last vertex of the given element or basic shape.
So the marker will be inserted at every vertex, but the start and end.
A simple line segment, however, has no other vertices, but its start and end. Thus there are no markers shown in your code.
If you change your marker-mid to, e.g., marker-end, you will see the arrow heads (although right now, they are not that pretty): fiddle.
Another way, would be to change the line-elements to path elements and add a additional vertex in the mid. This, however, would require some more sufficient code.