Extending paths in D3 with transition - d3.js

I've been grappling with issues relating to transitions in D3. Consider this code:
svg.selectAll("path")
.data(data, key)
.enter().append("path")
.attr("d", someFunctionThatReturnsAPath);
});
And I call the following in a setTimeout a few seconds later:
svg.selectAll("path")
.transition()
.duration(2000)
.attr("d", someFunctionThatReturnsADifferentPath);
});
The second call correctly updates the paths but doesn't animate the transition. Why is there no transition when the d attribute is updated in the second call?
Note that the paths are very complex. In both calls, there's a noticeable delay before the paths are actually drawn. Perhaps that's related to the lack of transition?
I'm new to D3, but I've read up on transitions and can't seem to understand why this doesn't behave as I expect it.
Update
Per #Marjancek's answer, I'm providing more details regarding the two called functions.
Here is the definition of someFunctionThatReturnsAPath:
function(d) {
var coordinates = [];
for (var i = d.track.length - 1; i >= 0; i--) {
// We only care about the last 10 elements
if (coordinates.length >= 10)
break;
coordinates.push(d.track[i]);
}
return path({type: "LineString", coordinates: coordinates});
};
And someFunctionThatReturnsADifferentPath:
function(d) {
var coordinates = [];
for (var i = d.track.length - 1; i >= 0; i--) {
// We only care about the last 20 elements
if (coordinates.length >= 20)
break;
coordinates.push(d.track[i]);
}
return path({type: "LineString", coordinates: coordinates});
};
where path is defined as follows (projection is d3.geo.albersUsa()):
var path = d3.geo.path()
.projection(projection);
The objective is that on the second call, the line is extended with 10 newer data points.

If your paths do not have the same number of points, the transitions might not work as expected. Try .attrTween: http://github.com/mbostock/d3/wiki/Transitions#wiki-attrTween There is an example on bl.ocks.org but the site seems to be down at the moment so I can't link to it.
Added on edit: The gist I was thinking of was: https://gist.github.com/mbostock/3916621 the bl.ocks link will be http://bl.ocks.org/mbostock/3916621 when the site is back up.

It is impossible to know without looking at your someFunctionThatReturnsADifferentPath; but I'm guessing that your Different function does not take into account interpolation, from the three parameters it received.
Read the transitions documentation: https://github.com/mbostock/d3/wiki/Transitions

Related

d3 Exit / Update function in animation

I am trying to do an animation that shows data from ten different points. The graphic is coded so that the sensors (displayed through circles) change their color and size depending on the overall data obtained over one hour (total number of entries and average of speed).
Through this entry (Transition not working d3) and the code from this simulation of Gapminder (https://bost.ocks.org/mike/nations/), I have been able to animate the chart. However, because of the structure of the code, the exit and update function do not work. The first entry in the data at Hour 1 only has one object and therefore only one circle is drawn. This circle gets updated through time, but the other sensors are not drawn (and therefore not updated).
I am considering recreating a first empty object for each sensor to draw them at the beginning of the animation. However, I would like to avoid that.
The code is this:
//FUNCTION TO GET THE DATA BY HOUR
function getDataByHour (hour) {
var allBridges = new Array();
var found;
for (b = 0; b < boatsByHour.length; b++){
bridges.forEach(function(br,i){
if (boatsByHour[b].sensorID== br.id){
xy = projection([br.longitude, br.latitude])}
});
if (boatsByHour[b].hour == hour){
found = true;
bridgeNumber = boatsByHour[b].sensorID;
allBridges.push({
"numberBoats": (boatsByHour[b].numberBoats),
"speed": (boatsByHour[b].speedAvg),
"bridge": boatsByHour[b].sensorID,
"longitude": xy[0],
"latitude": xy[1],
"hour": boatsByHour[b].hour
})
}
}
return allBridges;
}
//SENSORS
var sensor = plot.append("g")
.attr("class","bridges")
.selectAll(".sensors")
.data(getDataByHour(timeRange[0]))
.call(animateSensors)
.enter()
.append("circle")
.attr("class","sensors")
.attr("cx", function(d,i){return d.longitude})
.attr("cy", function(d,i){return d.latitude})
.on("mouseover",function(d){console.log(d)});
sensor.exit().remove();
plot
.transition()
.duration(300000)
.ease(d3.easeLinear)
.tween("hour", tweenHour)
//FUNCTION THAT UPDATES THE ANIMATION
function animateSensors (sensor){
sensor
.attr("r",function(d){return radiusScale(d.numberBoats)})
.style("fill",function(d){return colorScale(d.speed)});
}
function tweenHour(){
var hour = d3.interpolateNumber (timeRange[0],timeRange[1]);
return function(h){
displayHour(hour(h))}
}
function displayHour(hour) {
sensor.data(getDataByHour(Math.floor(hour))).call(animateSensors);
}
I have tried different ways of including the enter() and exit (). If I add the enter() and append the circles inside the 'animateSensors' function, all the circles (sensors) are drawn. However they are not being updated, so at the end, I get thousands of circles drawn in the SVG even if the exit().remove() update is in it.
Thanks
Ok, now it works. Instead of calling the first part of the data (where there is only 1 circle is drawn instead of the total 10), I had to call data that had data for all the circles (like the last point):
var sensor = plot.append("g")
.attr("class","bridges")
.selectAll(".bridge")
.data(getDataByHour(timeRange[1])) // instead of getDataByHour(timeRange[0])
.enter()
.append("circle")
.attr("class",function(d){return "bridge " + d.bridge})
.attr("cx", function(d,i){return d.longitude})
.attr("cy", function(d,i){return d.latitude})
.on("mouseover",function(d){console.log(d)});
The animation starts correctly thanks to the function tweenHour
function tweenHour(){
var hour = d3.interpolateNumber (timeRange[0],timeRange[1]);
return function(h){
displayHour(hour(h))}
}

D3.js binding nested data

I'm really new to coding, and also to asking questions about coding. So let me know if my explanation is overly complex, or if you need more context on anything, etc.
I am creating an interactive map of migration flows on the Mediterranean Sea. The flows show origin and destination regions of the migrant flows, as well as the total number of migrants, for Italy and Greece. Flows should be displayed in a Sankey diagram like manner. Because I am displaying the flows on a map and not in a diagram fashion, I am not using D3’s Sankey plugin, but creating my own paths.
My flow map, as of now (curved flows are on top of each other, should line up next to each other)
For generating my flows I have four points:
2 points for the straight middle part of the flow (country total)
1 point each for the curved outer parts (origin and destination region), using the two points of the straight middle part as starting points
The straight middle and both curved outer parts are each generated independently from their own data source. Flow lines are updated by changing the data source and calling the function again. The flow lines are generated using the SVG path mini-language. In order for the curved outer parts of the flows to show correctly, I need them to be lined up next to each other. To line them up correctly, I need to shift their starting points. The distance of the shift for each path element is determined by the width of the path elements before it. So, grouping by country, each path element i needs to know the sum of the width of the elements 0-i in the same group.
After grouping my data with d3.nest(), which would allow me to iterate over each group, I am not able to bind the data correctly to the path elements
I also can't figure out a loop function that adds up values for all elements 0-i. Any help here? (Sorry if this is kind of unrelated to the issue of binding nested data)
Here is a working function for the curved paths, working for unnested data:
function lineFlow(data, flowSubGroup, flowDir) {
var flowSelect = svg.select(".flowGroup").select(flowSubGroup).selectAll("path");
var flow = flowSelect.data(data);
var flowDirection = flowDir;
flow.enter()
.append("path").append("title");
flow
.attr("stroke", "purple")
.attr("stroke-linecap", "butt")
.attr("fill", "none")
.attr("opacity", 0.75)
.transition()
.duration(transitionDur)
.ease(d3.easeCubic)
.attr("d", function(d) {
var
slope = (d.cy2-d.cy1)/(d.cx2-d.cx1),
dist = (Math.sqrt(Math.pow((d.rx2-d.rx1),2)+Math.pow((d.ry2-d.ry1),2)))*0.5,
ctrlx = d.rx1 + Math.sqrt((Math.pow(dist,2))/(1+Math.pow(slope,2)))*flowDirection,
ctrly = slope*(ctrlx-d.rx1)+d.ry1;
return "M"+d.rx1+","+d.ry1+"Q"+ctrlx+","+ctrly+","+d.rx2+","+d.ry2})
.attr("stroke-width", function(d) {return (d.totalmig)/flowScale});
flowSelect
.select("title")
.text(function(d) {
return d.region + "\n"
+ "Number of migrants: " + addSpaces(d.totalmig)});
};
I tried adapting the code to work with data grouped by country:
function lineFlowNested(data, flowSubGroup, flowDir) {
var g=svg.select(".flowGroup").select(flowSubGroup).append("g").data(data).enter();
var gflowSelect=g.selectAll("path");
var gflow=gflowSelect.data (function(d) {return d.values});
gflow.enter()
.append("path");
gflow.attr("stroke", "purple")
.attr("stroke-linecap", "butt")
.attr("fill", "none")
.attr("opacity", 0.75)
// .transition()
// .duration(transitionDur)
// .ease(d3.easeCubic)
.attr("d", function(d) {
var
slope = (d.cy2-d.cy1)/(d.cx2-d.cx1),
dist = (Math.sqrt(Math.pow((d.rx2-d.rx1),2)+Math.pow((d.ry2-d.ry1),2)))*0.5,
ctrlx = d.rx1 - Math.sqrt((Math.pow(dist,2))/(1+Math.pow(slope,2)))*flowDirection,
ctrly = slope*(ctrlx-d.rx1)+d.ry1;
return "M"+d.rx1+","+d.ry1+"Q"+ctrlx+","+ctrly+","+d.rx2+","+d.ry2})
.attr("stroke-width", function(d) {return (d.totalmig)/flowScale});
};
which isn't working. What am I doing wrong? Thanks for any hints!

Reload nested data in D3.js

I do not manage to update a bar-chart with nested data in D3.js with new data.
I have nested data of the form:
data = [[1,2,3,4,5,6],[6,5,4,3,2,1]];
I managed to visualize the data by first appending a group for every subarray.
In the groups I then add the arrays as data (simplified):
function createGraph(l, svg){
var g = svg.selectAll("g")
.data(l)
.enter().append("g");
var rect = g.selectAll("rect)
.data(function(d){return d;})
.enter().append("rect")
. ...
}
However, when call the function again with different data, nothing happens.
It seems like in the second row, the rects do not get updated.
I have created a full example over at jsBin: http://jsbin.com/UfeCaGe/1/edit?js,output
A little more explanation of Lars' bug-catch, since I'd already started playing around...
The key was in this section of the code:
var group = svg.selectAll("g")
.data(l)
.enter().append("g");
The variable group is assigned the enter selection, not the raw selection. Then in the next line:
var bar = group.selectAll("rect")
.data(function(d){
return d;
});
You end up defining bar as only the rectangles that are children of just-entered groups. So even though you were handling update correctly for the rectangles, that whole section of code wasn't even running. You need to save the group selection before branching the chain to deal with entering groups:
var group = chart.selectAll("g")
.data(dt);
group.enter().append("g");
var bar = group.selectAll("rect")
.data(function(d){
return d;
});
Also, you're missing a j in your function declaration in your update. And you can reduce code duplication by putting your rectangle update code after your rectangle enter code, and then any attributes that get set in the update don't have to be specified for enter. (Some older examples don't use this pattern, because the original versions of d3 didn't automatically transfer newly-entered elements to the main selection.)
// enter
bar.enter().append("rect")
.attr("fill", function(d,i,j){
return colors(j);})
.attr("height", 0);
// update
bar.attr("transform", function(d, i, j) {
x = "translate("+(i*2.2*w+j*w)+",0)";
return x; })
.transition()
.duration(750)
.attr("width", w)
.attr("height", function(d){return d*10;});

D3.js graph displaying only one dataset

I having trouble getting the data on the graph. I only get one data set bar in.
You can see it here : http://infinite-fjord-1599.herokuapp.com/page2.html
But when I console.log the foreach for it. It displays all the objects:
data.days.forEach(function(d) {
d.ages = ageNames.map(function(name) { return {name: name, value: +d.values[name]}; });
console.log(d.ages);
});
The code on jsFiddle. http://jsfiddle.net/arnir/DPM7y/
I'm very new to d3.js and working with json data so I'm kinda lost here. I took the example of the d3.js example site and modified it.
See the updated fiddle here: http://jsfiddle.net/nrabinowitz/NbuFJ/4/
You had a couple of issues here:
Your x0 scale was set to a domain that displayed a formatted date, but when you were calling it later you were passing in d.State (which didn't exist, so I assume it was a copy/paste error). So the later days were being rendered on top of the first day.
There was a mismatch between the way you were selecting the group g element and the way you were appending it - not actually a root cause here, but likely to cause problems later on.
To fix, move the date formatting to a different function:
function formatDate(d) {
var str = d.modified;
d.date = parseDate( str.substring(0, str.length - 3) );
var curr_month = d.date.getMonth() + 1;
var curr_date = d.date.getDate();
var nicedate = curr_date + "/" + curr_month;
return nicedate;
}
and then use the same function for the scale setup:
x0.domain(data.days.map(formatDate));
and the transform (note the fix in the selector and class here as well):
var state = svg.selectAll("g.day")
.data(data.days)
.enter().append("g")
.attr("class", "day")
.attr("transform", function(d) {
return "translate(" + x0(formatDate(d)) + ",0)";
});
There are a couple of small things that threw you off. First, the domain of the x0 scale should be an array of datetime objects, not an array of strings:
x0.domain(data.days.map(function(d) {
var str = d.modified;
d.date = parseDate( str.substring(0, str.length - 3) );
return d.date;
}));
will return datetimes, not strings like it was before (minor nitpick: really not a fan of this use of map, I would add the date property separately in a forEach function as the data is loaded).
Second, x0 needs to be passed a property that actually exists:
var state = svg.selectAll(".state")
.data(data.days)
.enter().append("g")
.attr("class", "g")
.attr("transform", function(d) { return "translate(" + x0(d.date) + ",0)"; });
Before, you were using x0(d.state) which is a vestige from the grouped bar example (several others still exist; I've changed the minimum to get your project working). Since the value didn't exist, all of the rectangles were getting drawn over each other.
Additionally, we need to format the axis labels so we aren't printing out the entire datetime object all over the labels:
var xAxis = d3.svg.axis()
.scale(x0)
.orient("bottom")
.tickFormat(d3.time.format("%m-%d"));
Finally, I noticed that the newest dates were being printed on the left instead of the right. You could sort the results of data.days.map( ... ) to fix that, I just reversed the range of x0:
var x0 = d3.scale.ordinal()
.rangeRoundBands([width, 0], .1);
fixed files

Is it possible to create pie charts with object consistency?

the pie chart update example on the bl.ocks site doesn't update the elements 'in place':
http://bl.ocks.org/j0hnsmith/5591116
function change() {
clearTimeout(timeout);
path = path.data(pie(dataset[this.value])); // update the data
// set the start and end angles to Math.PI * 2 so we can transition
// anticlockwise to the actual values later
path.enter().append("path")
.attr("fill", function (d, i) {
return color(i);
})
.attr("d", arc(enterAntiClockwise))
.each(function (d) {
this._current = {
data: d.data,
value: d.value,
startAngle: enterAntiClockwise.startAngle,
endAngle: enterAntiClockwise.endAngle
};
}); // store the initial values
path.exit()
.transition()
.duration(750)
.attrTween('d', arcTweenOut)
.remove() // now remove the exiting arcs
path.transition().duration(750).attrTween("d", arcTween); // redraw the arcs
}
Instead, it just treats the new array of value as brand new data and resizes the chart accordingly.
I've created a fiddle demonstrating the issue very simply:
http://jsfiddle.net/u9GBq/23/
If you press 'add', it add a random int to the array: this works as intended.
If you press 'remove', the only element getting transitioned out is always the last element to have entered the pie. In short, it behaves like a LIFO stack.
The expected behaviour is for the relevant pie arc to get transitioned out instead.
Is it possible to apply object consistency to pies? I've also tried adding a key function (not demonstrated on the fiddle) but that just breaks (oddly enough it works fine with my stacked graphs).
Thank you.
The easiest solution to this problem is to set missing values to zero, rather than removing them entirely, as in Part III of the Pie Chart Update series of examples. Then you get object constancy for free: you have the same number of elements, in the same order, across updates.
Alternatively, if you want a data join as in Part IV, you have to tell D3 where the entering arcs should enter from, and where the exiting arcs should exit to. A reasonable strategy is to find the closest neighboring arc from the opposite data: for a given entering arc, find the closest neighboring arc in the old data (pre-transition); likewise for a given exiting arc, find the closest neighboring arc in the new data (post-transition).
To continue the example, say you’re showing sales of apples in different regions, and want to switch to show oranges. You could use the following key function to maintain object constancy:
function key(d) {
return d.data.region;
}
(This assumes you’re using d3.layout.pie, which wraps your original data and exposes it as d.data.)
Now say when you transition to oranges, you have the following old data and new data:
var data0 = path.data(), // retrieve the old data
data1 = pie(region.values); // compute the new data
For each entering arc at index i (where d is data1[i]), you can step sequentially through preceding data in data1, and see if you can find a match in data0:
var m = data0.length;
while (--i >= 0) {
var k = key(data1[i]);
for (var j = 0; j < m; ++j) {
if (key(data0[j]) === k) return data0[j]; // a match!
}
}
If you find a match, your entering arcs can start from the matching arc’s end angle. If you don’t find a preceding match, you can then look for a following matching arc instead. If there are no matches, then there’s no overlap between the two datasets, so you might enter the arcs from angle 0°, or do a crossfade. You can likewise apply this technique to exiting arcs.
Putting it all together, here’s Part V:
Ok, found the solution.
The trick was to pass the key this way:
path = path.data(pie(dataset), function (d) {return d.data}); // this is good
as opposed to not passing it, or passing it the wrong way:
path = path.data(pie(dataset, function (d) {return d.data})); // this is bad
And here's an updated fiddle with a working transition on the right arc! :)
http://jsfiddle.net/StephanTual/PA7WD/1/

Resources