I am creating a sankey diagram using D3. I am trying to redraw the diagram with additional node and link and using transition to animate the previous diagram to the new diagram. I was able to add in new node and link but the old nodes and links did not change position. Since the new node and link could be added at any place within the diagram, I do not want to clear and redraw the entire svg, but use transition to get from the old diagram to the new one. The code to draw the sankey diagram is this:
function draw(data){
// Set the sankey diagram properties
var sankey = d3sankey()
.nodeWidth(17)
.nodePadding(27)
.size([width, height]);
var path = sankey.link();
var graph = data;
sankey.nodes(graph.nodes)
.links(graph.links)
.layout(32);
sankey.relayout();
// add in the links
link.selectAll(".link")
.data(graph.links)
.enter().append("path")
.attr("class", "link")
.attr("d", path)
.style("fill", "none")
.style("stroke", function(d){
return "grey";
})
.style("stroke-opacity", "0.4")
.on("mouseover", function() { d3.select(this).style("stroke-opacity", "0.7") } )
.on("mouseout", function() { d3.select(this).style("stroke-opacity", "0.4") } )
.style("stroke-width", function (d) {
return Math.max(1, d.dy);
})
.sort(function (a, b) {
return b.dy - a.dy;
});
link.transition().duration(750);
//link.exit();
// add in the nodes
var node = nodes.selectAll(".node")
.data(graph.nodes)
.enter().append("g")
.attr("class", "node")
.attr("transform", function (d) {
return "translate(" + d.x + "," + d.y + ")";
});
// add the rectangles for the nodes
node.append("rect")
.attr("height", function (d) {
return d.dy;
})
.attr("width", sankey.nodeWidth())
.style("fill", function (d) {
return d.color = color(d.name.replace(/ .*/, ""));
})
.style("fill-opacity", ".9")
.style("shape-rendering", "crispEdges")
.style("stroke", function (d) {
return d3.rgb(d.color).darker(2);
})
.append("title")
.text(function (d) {
return d.name + "\n" + format(d.value);
});
// add in the title for the nodes
node.append("text")
.attr("x", -6)
.attr("y", function (d) {
return d.dy / 2;
})
.attr("dy", ".35em")
.attr("text-anchor", "end")
.attr("text-shadow", "0 1px 0 #fff")
.attr("transform", null)
.text(function (d) {
return d.name;
})
.filter(function (d) {
return d.x < width / 2;
})
.attr("x", 6 + sankey.nodeWidth())
.attr("text-anchor", "start");
node.transition().duration(750);
}
The JSFiddle
Is it possible to use transition to add in new node and link and reposition
old nodes and links?
Thanks!
I was able to do this by using moving the nodes and links to new position. The code for that is:
var nodes = d3.selectAll(".node")
.transition().duration(750)
.attr('opacity', 1.0)
.attr("transform", function (d) {
if(d.node == 3){
console.log(d.x, d.y);
}
return "translate(" + d.x + "," + d.y + ")";
});
var nodeRects = d3.selectAll(".node rect")
.attr("height", function (d) {
if(d.node == 3){
console.log(d.dy);
}
return d.dy;
})
var links = d3.selectAll(".link")
.transition().duration(750)
.attr('d', path)
.attr('opacity', 1.0)
Updated JSFiddle
Related
I'm splitting data into categories e.g. rich poor and all. Using dropdown to get those values to display on a scatterplot. First transition happens, everything works as expected. Text labels are correctly displayed too, however when another option is selected and second transition happened half of circles are disappearing and every other transition is messed up. Only works if option all selected than again, first transition works, after that it is all messed up.
Codepen
function render(someData) {
xScale
.domain([
d3.min(someData, function(d) {
return +d.poorToys;
}),
d3.max(someData, function(d) {
return +d.poorToys;
})
]);
yScale
.domain([
d3.min(someData, function(d) {
return +d.richToys;
}),
d3.max(someData, function(d) {
return +d.richToys;
})+ 20
]);
//Adding circles
var circles = svg.selectAll("circle")
.data(someData, function(d) {
return d.country;
});
I believe a problem starts here.
circles
.enter()
.append("circle")
.attr("cx", function(d) {
if (currentSelection === "rich") {
return width - margin.right;
} else if (currentSelection === "poor") {
return margin.left;
} else if (currentSelection === "all") {}
return xScale(+d.poorToys);
})
.attr("cy", function(d) {
if (currentSelection === "rich") {
return margin.top;
} else if (currentSelection === "poor") {
return height - margin.bottom;
} else if (currentSelection === "all") {}
return yScale(+d.richToys);
})
.attr("r", function(d) {
if (currentSelection === "all") {
return rad;
}
})
.style("fill", "red")
.append("title")
.text(function(d) {
return d.country + " reports books for " + d.poorToys + "% in poor areas and " + d.richToys + "% in rich areas.";
});
circles
.transition()
.duration(2000)
.attr("cx", function(d) {
return xScale(+d.poorToys);
})
.attr("cy", function(d) {
return yScale(+d.richToys);
})
.attr("r", function() {
if (currentSelection !== "all") {
return rad * 1.5;
} else {
return rad;
}
});
circles
.exit()
.transition()
.duration(1000)
.style("opacity", 0)
.remove();
//Update x axis
svg.select(".x.axis")
.transition()
.duration(1000)
.call(xAxis);
//Update y axis
svg.select(".y.axis")
.transition()
.duration(1000)
.call(yAxis);
if (currentSelection !== "all"){
var labels = svg.selectAll("text.labels")
.data(someData, function(d){
return d.country;
});
labels
.enter()
.append("text")
.attr("transform", function(d){
return "translate(" + xScale(+d.poorToys) + "," + yScale(+d.richToys) + ")";
})
.attr("dx", 2)
.attr("dy", 1)
.attr("class", "labels")
.style("fill", "white")
.style("font-size", "5px")
.text(function(d){
return d.country;
});
labels
.transition()
.duration(2000)
.style("opacity", 1);
labels
.exit()
.remove();
} else {
svg.selectAll("text.labels")
.transition()
.duration(1000)
.style("opacity", 0)
.remove();
}
}
You incorrectly give your x axis a class of x_axis on line 57 then later try to select it as x.axis in your render function on line 179.
Once you fix that up, I think it should work as expected.
svg
.append("g")
.attr("class", "x axis")
.attr("transform", "translate(" + -14 + "," + (height + 30) + ")")
.call(xAxis);
Updated Pen
Apart from the axis problem found by #ksav your main problem is that you don't position the labels. Many labels are present in rich and poor.
var labels = svg.selectAll("text.labels")
.data(someData, function(d){ return d.country; });
labels
.enter()
.append("text")
.attr("x", function(d){ return xScale(+d.poorToys); })
.attr("y", function(d){ return yScale(+d.richToys); })
.attr("dx", 2)
.attr("dy", 1)
.attr("class", "labels")
.attr("opacity", 0)
.style("fill", "white")
.style("font-size", "8px")
.text(function(d){ return d.country; })
.merge(labels)
.transition()
.duration(2000)
.attr("x", function(d){ return xScale(+d.poorToys); })
.attr("y", function(d){ return yScale(+d.richToys); })
.attr("opacity", 1);
Also don't position the circles based on the selection
circles
.enter()
.append("circle")
.attr("cx", function(d) { return xScale(+d.poorToys); })
.attr("cy", function(d) { return yScale(+d.richToys); })
.attr("r", function(d) { return rad; })
.style("fill", "red")
.append("title")
.text(function(d) {
return d.country + " reports books for " + d.poorToys + "% in poor areas and " + d.richToys + "% in rich areas.";
});
I'm trying to modify Mike's Force-Directed Graph example to use rectangles instead of circles as nodes. Also, I want text inside the rectangle.
I have rectangles showing up with text correctly, however they're not attached to the links, and they do not move.
Here's a codepen link: http://codepen.io/anon/pen/gpgWaz
var width = 960,
height = 500;
var color = d3.scale.category20();
var force = d3.layout.force()
.charge(-120)
.linkDistance(30)
.size([width, height]);
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
force
.nodes(graph.nodes)
.links(graph.links)
.start();
var link = svg.selectAll(".link")
.data(graph.links)
.enter().append("line")
.attr("class", "link")
.style("stroke-width", function(d) {
return Math.sqrt(d.value);
});
var node = svg.selectAll(".node")
.data(graph.nodes)
.enter()
.append("g")
.attr("transform", function(d) {
return "translate(" + d.x + "," + d.y + ")";
})
.call(force.drag);
node.append("rect")
.attr("class", "node")
.attr("width", 100)
.attr("height", 35)
.style("fill", function(d) {
return color(d.group);
})
.style("stroke", "black")
.style("stroke-width", "1px");
node.append("text")
.text(function(d) {
return d.name;
})
.style("font-size", "12px")
.attr("dy", "1em");
node.append("title")
.text(function(d) {
return d.name;
});
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(d) {
return d.x;
})
.attr("y", function(d) {
return d.y;
});
});
Update
Thanks to Lars's comment, and his codepen it works now.
Updates I made to my code:
Add transform illustrated by Lars
Changed links to connect at the center of rectangles
Added rounded corners to rectangles
Gave the text a slight margin indentation
Changed to use window width/height
Here my new codepen: http://codepen.io/anon/pen/bdgREd
var width = window.innerWidth,
height = window.innerHeight,
nodeWidth = 100,
nodeHeight = 35;
var color = d3.scale.category20();
var force = d3.layout.force()
.charge(-1500)
.linkDistance(100)
.friction(0.5)
.size([width, height]);
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
force
.nodes(graph.nodes)
.links(graph.links)
.start();
var link = svg.selectAll(".link")
.data(graph.links)
.enter().append("line")
.attr("class", "link")
.style("stroke-width", function(d) {
return Math.sqrt(d.value);
});
var node = svg.selectAll(".node")
.data(graph.nodes)
.enter()
.append("g")
.attr("transform", function(d) {
return "translate(" + d.x + "," + d.y + ")";
})
.call(force.drag);
node.append("rect")
.attr("class", "node")
.attr("width", nodeWidth)
.attr("height", nodeHeight)
.attr("rx", 5)
.attr("ry", 5)
.style("fill", function(d) {
return color(d.group);
})
.style("stroke", "black")
.style("stroke-width", "1px");
node.append("text")
.attr("x", 5)
.attr("y", 2)
.text(function(d) {
return d.name;
})
.style("font-size", "12px")
.attr("dy", "1em");
node.append("title")
.text(function(d) {
return d.name;
});
force.on("tick", function() {
link.attr("x1", function(d) {
return d.source.x + (nodeWidth / 2);
})
.attr("y1", function(d) {
return d.source.y + (nodeHeight / 2);
})
.attr("x2", function(d) {
return d.target.x + (nodeWidth / 2);
})
.attr("y2", function(d) {
return d.target.y + (nodeHeight / 2);
});
node.attr("transform", function(d) {
return "translate(" + d.x + "," + d.y + ")";
});
});
You're setting the x and y attributes on the g elements to change their positions -- this won't do anything. You need to set the transform attribute instead, like you're doing when adding the g elements. So your tick handler function would contain
node.attr("transform", function(d) {
return "translate(" + d.x + "," + d.y + ")";
});
instead of
node.attr("x", function(d) {
return d.x;
})
.attr("y", function(d) {
return d.y;
});
Complete demo here.
I am having some issues with d3js and I can't figure out what is going on. The idea is to draw initial graph from some endpoint data (first img), that's fine works well. Each node is clickable, on click ajax call is made for that node and data is returned, based on some criteria at that point nodes.push(xx), links.push(xx) happens to add new nodes and restart() is called to draw new nodes and links. The issue is that the main graph is doing the correct thing (Not showed on screenshots as I had to put fake data on the first graph i.e. calling an endpoint /record/id/first doesn't return a data) but there are bunch of random nodes showing up in the right bottom corner.
You can also see on the example below, even if the data doesn't change after clicking on first/second/third something wrong goes with node.enter() after restart() with the same data passed in...
JS FIDDLE: http://jsfiddle.net/5754j86e/
var w = 1200,
h = 1200;
var nodes = [];
var links = [];
var node;
var link;
var texts;
var ids = [];
var circleWidth = 10;
var initialIdentifier = "marcin";
nodes = initialBuildNodes(initialIdentifier, sparql);
links = initialBuildLinks(sparql);
//Add SVG
var svg = d3.select('#chart').append('svg')
.attr('width', w)
.attr('height', h);
var linkGroup = svg.append("svg:g").attr("id", "link-group");
var nodeGroup = svg.append("svg:g").attr("id", "node-group");
var textGroup = svg.append("svg:g").attr("id", "text-group");
//Add Force Layout
var force = d3.layout.force()
.size([w, h])
.gravity(.05)
.charge(-1040);
force.linkDistance(120);
restart();
function restart() {
force.links(links)
console.log("LINKS ARE: ", links)
link = linkGroup.selectAll(".link").data (links);
link.enter().append('line')
.attr("class", "link");
link.exit().remove();
force.nodes(nodes)
console.log("NODES ARE: ", nodes)
node = nodeGroup.selectAll(".node").data (nodes);
node.enter().append("svg:g")
.attr("class", "node")
.call(force.drag);
node.append('circle')
.attr('cx', function(d) { return d.x; })
.attr('cy', function(d) { return d.y; })
.attr('r', circleWidth )
.attr('fill', function(d, i) {
if (i>0) { return palette.pink }
else { return palette.blue }
})
.on("click", function(d) {
nodeClicked (d);
})
.on('mouseenter', function(d){
nodeMouseEnter(d)
})
.on('mouseout', function(d){
nodeMouseOut(d)
});
node.exit().remove();
var annotation = textGroup.selectAll(".annotation").data (nodes);
annotation.enter().append("svg:g")
.attr("class", "annotation")
.append("text")
.attr("x", function(d) { return d.radius + 4 })
.attr("y", ".31em")
.attr("class", "label")
.text(function(d) { return d.name; });
annotation.exit().remove();
force.start();
}
function nodeClicked (d) {
// AJAX CALL happens here and bunch of nodes.push({name: "new name"}) happen
}
force.on('tick', function(e) {
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('transform', function(d, i) {
return 'translate('+ d.x +', '+ d.y +')';
})
svg.selectAll(".annotation").attr("transform", function(d) {
var labelx = d.x + 13;
return "translate(" + labelx + "," + d.y + ")";
})
});
Okay I got it, based on the docs (https://github.com/mbostock/d3/wiki/Selections#enter):
var update_sel = svg.selectAll("circle").data(data)
update_sel.attr(/* operate on old elements only */)
update_sel.enter().append("circle").attr(/* operate on new elements only */)
update_sel.attr(/* operate on old and new elements */)
update_sel.exit().remove() /* complete the enter-update-exit pattern */
From my code you can see I do enter() and then once again I add circle on node in a separate statement.
node = nodeGroup.selectAll(".node").data (nodes);
node.enter().append("svg:g")
.attr("class", "node")
.call(force.drag);
node.append('circle')
.attr('cx', function(d) { return d.x; })
.attr('cy', function(d) { return d.y; })
.attr('r', circleWidth )
.attr('fill', function(d, i) {
if (i>0) { return palette.pink }
else { return palette.blue }
});
Adding circle should be within the scope of enter() otherwise it happens to all nodes not only the new nodes therefore it should be :
node.enter().append("svg:g")
.attr("class", "node")
.call(force.drag)
.append('circle')
.attr('cx', function(d) { return d.x; })
.attr('cy', function(d) { return d.y; })
.attr('r', circleWidth )
.attr('fill', function(d, i) {
if (i>0) { return palette.pink }
else { return palette.blue }
});
Actually I've integrated collapsible feature inside bounded force directed graph. But when I tried to put a label on each node, I got unexpected output.
I used bellow code to append labels on node:
node.enter().append("text")
.attr("class","node")
.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; })
.on("click",click)
.text(function(d){return d.name})
.call(force.drag);
And below code I've written inside a tick function:
node.attr("transform", function(d) {
return "translate(" + d.x + "," + d.y + ")";
});
What I might be doing wrong?
I need to append the g tag and then circle and text:
var nodeEnter = node.enter().append("g")
.attr("class", "node")
.on("click", click)
.call(force.drag);
nodeEnter.append("circle")
.attr("r", function(d) { return Math.sqrt(d.size) / 10 || 8.5; });
nodeEnter.append("text")
.attr("dy", ".35em")
.text(function(d) { return d.name; });
node.select("circle")
.style("fill", color);
I am unable to add a segment to a D3.js pie chart. I know I need to use .enter() and .append() to stage the new data -- but I am not sure how to apply that when I have the arcs grouped (which I need for the labels).
Here is my update function:
var updateChart = function(dataset) {
arcs.data(donut(dataset));
arcs.transition()
.duration(duration)
.attrTween("d", arcTween);
sliceLabel.data(donut(dataset));
sliceLabel.transition()
.duration(duration)
.attr("transform", function(d) { return "translate(" + (arc.centroid(d)) + ")"; })
.style("fill-opacity", function(d) {
if (d.value === 0) { return 1e-6; }
else { return 1; }
});
};
How I setup the initial graph:
var arc = d3.svg.arc()
.innerRadius(radius * .4)
.outerRadius(radius);
var svg = d3.select("body")
.append("svg")
.append("svg")
.attr("width", width)
.attr("height", height);
var arc_grp = svg.append("g")
.attr("class", "arcGrp")
.attr("transform", "translate(" + (width / 2) + "," + (height / 2) + ")");
var label_group = svg.append("g")
.attr("class", "lblGroup")
.attr("transform", "translate(" + (width / 2) + "," + (height / 2) + ")");
var arcs = arc_grp.selectAll("path")
.data(donut(data));
arcs.enter()
.append("path")
.attr("stroke", "white")
.attr("stroke-width", 0.8)
.attr("fill", function(d, i) { return color(i); })
.attr("d", arc)
.each(function(d) { return this.current = d; });
var sliceLabel = label_group.selectAll("text")
.data(donut(data));
sliceLabel.enter()
.append("text")
.attr("class", "arcLabel")
.attr("transform", function(d) { return "translate(" + (arc.centroid(d)) + ")"; })
.attr("text-anchor", "middle")
.style("fill-opacity", function(d) {
if (d.value === 0) { return 1e-6; }
else { return 1; }
})
.text(function(d) { return d.data.label; });
Complete jsfiddle: http://jsfiddle.net/kPM5L/
What is a clean way to add the new data to the chart?
To get the transition to work smoothly, you need to add the code that you're using initially to your update function as well. Working jsfiddle here.
And some code to make SO happy -- this is what needs to be in the update function as well:
.enter()
.append("path")
.attr("stroke", "white")
.attr("stroke-width", 0.8)
.attr("fill", function(d, i) { return color(i); })
.attr("d", arc)
.each(function(d) { return this.current = d; });
.enter()
.append("text")
.attr("class", "arcLabel")
.attr("transform", function(d) { return "translate(" + (arc.centroid(d)) + ")"; })
.attr("text-anchor", "middle")
.style("fill-opacity", function(d) {
if (d.value === 0) { return 1e-6; }
else { return 1; }
})
.text(function(d) { return d.data.label; });