With the Mobile Patent Suit example by Mike Bostock as an example, I was able to re-create using my own data, but I want to take it a step further and dynamically update the node relationships on a drop-down change. The code works almost every time, but occasionally I get a single node with no relationships (see image below) even though data are available. I think I may be doing something wrong when clearing and then updating the nodes on a selection change, but it's strange to me that is works some of the time. Any help is greatly appreciated.
http://jsfiddle.net/calanoue/r6BRr/
var selectedCode = '0';
function updateNodes() {
// Update the links and the nodes any time the data changes
svg.selectAll('g').remove();
links = allData[0][selectedCode];
// 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});
});
force = d3.layout.force()
.nodes(d3.values(nodes))
.links(links)
.size([width, height])
.linkDistance(80)
.charge(-400)
.on("tick", tick)
.start();
path = svg.append("g").selectAll("path")
.data(force.links())
.enter().append("path")
.attr("class", "link relationship")
.attr("marker-end", "url(#relationship)");
circle = svg.append("g").selectAll("circle")
.data(force.nodes())
.enter().append("circle")
.attr('class', "generic")
.attr("r", 8)
.call(force.drag);
}
function tick() {
// Use elliptical arc path segments to doubly-encode directionality.
path.attr("d", linkArc);
circle.attr("transform", transform);
}
function linkArc(d) {
var dx = d.target.x - d.source.x,
dy = d.target.y - d.source.y,
dr = Math.sqrt(dx * dx + dy * dy);
return "M" + d.source.x + "," + d.source.y + "A" + dr + "," + dr + " 0 0,1 " + d.target.x + "," + d.target.y;
}
function transform(d) {
return "translate(" + d.x + "," + d.y + ")";
}
function clearNodes() {
nodes = {};
links = [];
force.start();
d3.timer(force.resume);
}
function codeChange() {
selectedCode = this.value;
clearNodes();
updateNodes();
}
Missing Node Relationship Image:
The problem you were having is that your original data, allData, was being changed by the computation of links and nodes at each data change iteration. After all, you set links = cloneData[0][selectedCode];. You need to clone allData before each data change.
function codeChange() {
selectedCode = this.value;
cloneData = clone(allData); // added
clearNodes();
updateNodes();
}
Here is a FIDDLE with all the changes to make it work. I borrowed the clone function from this SO question.
NOTE: I also removed one of your dropdown choices since you only have 3 sets of data in allData. That was not the cause of your problems, per above, but it was throwing errors whenever you selected the inexistent set.
Related
I am trying to implement an update function for my D3 sunburst diagram where i can change the data that is displayed. I am able to successfully add or remove nodes.
However, i can't seem to be able to modify the existing data.
For example, if half of my chart is removed, i would like for the other half to fill the space leaved by the delete. Same thing goes when new data get added, i would like for the existing data to shrink and take less space
Here is my update function :
function update(newData) {
root = newData;
node = root;
g = svg.datum(root)
.selectAll("g")
.data(partition.nodes(root));
var newG = g.enter()
.append("g");
g.exit().selectAll("path").transition().duration(5000).attrTween("d", arcTween(0)).remove();
path = g.selectAll("path").transition().duration(5000).attr("d", arc);
newG.append("path")
.attr("d", arc);
};
Here is how the chart is built :
function render(data) {
root = data;
node = root;
width = $(".burst-chart-container").height();
height = ($(".burst-chart-container").width() / 2);
radius = Math.min(width, height) / 2;
x = d3.scale.linear().range([0, 2 * Math.PI]);
y = d3.scale.linear().range([0, radius]);
rad = Math.min(width, height) / Math.PI - 25;
partition = d3.layout.partition().sort(null).value(function (d) { return d.size; });
arc = d3.svg.arc()
.startAngle(function (d) { return Math.max(0, Math.min(2 * Math.PI, x(d.x))); })
.endAngle(function (d) { return Math.max(0, Math.min(2 * Math.PI, x(d.x + d.dx))); })
.innerRadius(function (d) { return Math.max(0, y(d.y)); })
.outerRadius(function (d) { return Math.max(0, y(d.y + d.dy)); });
svg = d3.select(element[0]).append('svg')
.attr("width", width).attr("height", height)
.attr("class", "svgDashboard")
.append("g")
.attr("transform", "translate(" + width / 2 + "," + (height / 2) + ")");
g = svg.datum(root).selectAll("g")
.data(partition.nodes)
.enter()
.append("g")
path = g.append("path")
.attr("d", arc)
}
I do know that path.attr("d",arc) should update the visual, but it doesn't work in my case.
I think that it has something to do with the partition layout who dosen't tell the existing arcs that they need to change, or the way that I do the selection to update the data, but I might be wrong.
Any help would be appreciated.
I found out that the path and text data were never updated. My update did only change g element data. To correct it, I simply take the parent data and put into into his child after I updated it.
g = svg.datum(root)
.selectAll("g")
.data(partition.nodes(root));
//Add this part to update child elements
g.selectAll("path").each(function () {
d3.select(this).datum(d3.select(this.parentNode).datum());
});
g.selectAll("text").each(function () {
d3.select(this).datum(d3.select(this.parentNode).datum());
});
With those changes, i am able to call my attrTween function which update the visual with the new data.
I want to centralize the node when I clicked it, so I used the following code:
node.on("click", function(d){
var cX = width/2;
var cY = height/2;
var dx = cX - d.x;
var dy = cY - d.y;
link.attr("transform", function(d) { return "translate(" + dx + "," + dy + ")"; });
node.attr("transform", function(d) { return "translate(" + dx + "," + dy + ")"; });
});
It works for the "normal node", but when changed the node to image, the link is moved while the node (image) is still here.
Here is my example: http://talk.huacishu.com/t11.html
I know a little bit about d3js\javascript, anyone can tell me where is wrong?
I finally made it by another way :
Using "var g_container = svg.append(g) "
to append a "g" label(g_container), and move the "g_container" to centralize the clicked node.
here is the final project:
https://myaisv.github.io/curry/renlifang/connan/t1.html
I'm drawing a pie chart with d3.js. I want to transition the pie slices when new data is added. (i'm using the reusable chart API). I'm first creating a chart group using enter and then appending the chart arc path to that:
http://jsfiddle.net/EuK6H/4/
var arcGroup = svg.select(".pie").selectAll(".arc " + selector)
.data(pie(data))
.enter().append("g")
.attr("class", "arc " + selector);
if (options.left) {
arcGroup.attr('transform', 'translate(' + options.left + ',' + options.top + ')');
} else {
arcGroup.attr('transform', 'translate(' + options.width / 2 + ',' + options.height / 2 + ')');
}
//append an arc path to each group
arcGroup.append("path")
.attr("d", arc)
//want to add the transition in here somewhere
.attr("class", function (d) { return 'slice-' + d.data.type; })
.style("fill", function (d, i) {
return color(d.data.amount)
});
//...
Problem is when new data comes in I need to be able to transition the path (and also the text nodes shown in the the fiddle) but the enter selection is made on the the parent group. How can I add a transition() so it applies to the path?
You can use .select(), which will propagate the data to the selected element. Then you can apply a transition based on the new data. The code would look something like this:
var sel = svg.selectAll(".pie").data(pie(newData));
// handle enter + exit selections
// update paths
sel.select("path")
.transition()
.attrTween("d", arcTween);
I have a d3 globe, and I have it scaling up (zooming in) when I doubleclick it. However, the zoom only works the first time I doubleclick. After that, I see that the program is entering the dblclick function, but no zooming is taking place. This is probably a stupid question, but I would be grateful if anyone were able to tell me how to make the zoom happen each time the globe is doubleclicked.
var width = 800,
height = 800,
centered;
var feature;
var projection = d3.geo.azimuthal()
.scale(380)
.origin([-71.03,42.37])
.mode("orthographic")
.translate([380, 400]);
var circle = d3.geo.greatCircle()
.origin(projection.origin());
// TODO fix d3.geo.azimuthal to be consistent with scale
var scale = {
orthographic: 380,
stereographic: 380,
gnomonic: 380,
equidistant: 380 / Math.PI * 2,
equalarea: 380 / Math.SQRT2
};
var path = d3.geo.path()
.projection(projection);
var svg = d3.select("#globe").append("svg:svg")
.attr("width", 800)
.attr("height", 800)
.on("dblclick", dblclick)
.on("mousedown", mousedown);
var g = svg.append("g");
d3.json("simplified.geojson", function(collection) {
g.append("g")
.attr("id", "countries")
.selectAll("path")
.data(collection.features)
.enter().append("svg:path")
.attr("d", clip)
.attr("id", function(d) { return d.properties.ISO3; })
.on("mouseover", pathOver)
.on("mouseout", pathOut)
.on( "dblclick", dblclick)
.on("click", click);
feature = svg.selectAll("path");
feature.append("svg:title")
.text(function(d) { return d.properties.NAME; });
});
...
function dblclick(d) {
var x, y, k;
/*if (d && centered !== d) {
var centroid = path.centroid(d);
x = centroid[0];
y = centroid[1];
k = 4;
centered = d;
} else {
x = width / 2;
y = height / 2;
k = 1;
centered = null;
}
g.selectAll("path")
.classed("active", centered && function(d) { return d === centered; });*/
g.transition()
.duration(750)
//.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")scale(" + k + ")translate(" + -x + "," + -y + ")")
.attr("transform", "scale(1.5)");
//.style("stroke-width", 1.5 / k + "px");
}
I agree with Erik E. Lorenz (no way to link to Erik's answer, it appears). Right now you're setting the zoomscale in the line
.attr("transform", "scale(1.5)");
The problem is that each time you call dblclick(), you're "resetting" it to 1.5. It's not multiplying by 1.5 it's just getting set. D3 doesn't remember what it used to be. That's why the first time you call dblclick() it works (because you're transforming the scale to 1.5 from 1). But from then on, the scale is already transformed to 1.5 and you just keep setting the scale transform to 1.5.
You need to keep track of "how far you've zoomed". And to do that you need a variable that keeps it's value between calls to dblclick(). I'd do something like this:
/* given the structure of your code, you can probably just declare the
variable before the function declaration. the function `dblclick` will
have access to the variable via closure */
var zoomScale = 1;
/* then you can just do this */
function dblclick(d) {
// you'll probably want to play with the math here
// that is, "1.5" might not be best
zoomScale = zoomScale * 1.5; // or some shorthand
g.transition()
.duration(750)
.attr("transform", "scale(" + zoomScale + ")");
}
I think that that scale(1.5) might be the problem. Have you tried dynamically increasing that factor every time dblclick() is called?
Here is my current fiddle. I'm using foreignObjects to define custom icons for nodes (the fiddle is just using a ? but locally I'm using font-awesome icons). This works great, however the problem is that the arrows on the paths point to the top-left corner of the element. I've tried changing toying with many of the existing parameters and have looked through the API documentation but am unable to find a solution. I'm guessing I could do some complicated math in the code below, but I'm hoping for a parameter that can set some sort of radius for the end of the path.
function linkArc(d) {
var dx = d.target.x - d.source.x,
dy = d.target.y - d.source.y,
dr = Math.sqrt(dx * dx + dy * dy);
return "M" + d.source.x + "," + d.source.y + "A" + dr + "," + dr + " 0 0,1 " + d.target.x + "," + d.target.y;
}
The only documentation I found on paths was geo-paths. I tried using pointRadius anyway but it didn't seem to do anything at all. Here is the path definition:
var path = svg.append("g").selectAll("path")
.data(force.links())
.enter().append("path")
.attr("class", function (d) { return "link " + d.type; })
.attr("marker-end", function (d) { return "url(#" + d.type + ")"; });
You can use the transform attribute to adjust the position:
var path = svg.append("g").selectAll("path")
.data(force.links())
.enter().append("path")
.attr("transform", "translate(10,10)")
.attr("class", function (d) { return "link " + d.type; })
.attr("marker-end", function (d) { return "url(#" + d.type + ")"; });