I have a very large csv file that that I'm visualizing with D3.js. One of the data fields is a timestamp, so (rather than display all this information at once) I want to create an svg element (based on the other data fields) after a delay proportional to the timestamp, and then fade it out over three seconds. Because of the size of the data, I cannot create all the elements up front, even if they are invisible; each element should only exist for three seconds. The desired effect is a bunch of dots popping into existence and then fading away.
My best attempt is below. The strategy is to use two transitions: a delay, and then the fade transition. It doesn't seem to work but rather creates all the elements all at once (the fading works though).
d3.csv(datafile).get(function(error, rows) {
for (var i = rows.length; i--;){
var d = rows[i];
plot.select("foo") // empty selection
.transition()
.delay(/*expression with d.timestamp*/)
.call(function(){
plot.append("circle")
.attr(/*several, snip */)
.style("opacity", 1)
.transition()
.duration(3000)
.style("opacity", 0)
.remove()
});
}
});
EDIT April 2015 Having learned a lot since asking the question, the obvious thing seems to be to insert everything immediately with 0 opacity. Then create a duration 0, variable delay transition to snap to 1 opacity, and then fade from there. Also use nested selections to avoid the explicit for-loop. I haven't tried it but it should work.
Have you tried using setTimeout instead?
rows.forEach(function(d, i){
setTimeout(function(){
plot.append("circle")
.attr(/*several, snip */)
.style("opacity", 1)
.transition()
.duration(3000)
.style("opacity", 0)
.remove()
}, /*expression with d.timestamp*/);
})
I know this is old, but I was having the same problem and was trying to resolve it with a promise, then found a solution that did it way better than mine.
The full post is here
const sleep = (milliseconds) => {
return new Promise(resolve => setTimeout(resolve, milliseconds))
}
Then
sleep(500).then(() => {
//do stuff
})
Related
d3.select('#' + linkId[0]).transition().duration(2500).attr('stroke', 'green');
d3.select('#' + nodeId[0]).transition().duration(5000).attr('fill', 'blue');
I have the above code that is animating a graph traversal. I want the second transition to only activate (and preferably remove the duration) once the link has been transitioned. How would i accomplish this? I have tried putting the whole second line of code within a timeout like so:
setTimeout(() => { d3 transition here }, 2500);
However this completely messed up the timing of events. Im basically looking for something similar to python where you can call .sleep(milliseconds) to specify code execution wait a certain amount of time.
There are two quick options available:
transition.delay()
You could use transition.delay(time) which allows you to specify a delay before a transition starts. This would look like:
d3.select('#' + linkId[0]).transition().duration(2500).attr('stroke', 'green');
d3.select('#' + nodeId[0]).transition().delay(2500).duration(5000).attr('fill', 'blue');
While simple, I'd suggest using the next approach instead.
transition.on("end", ... )
Another option is to use transition.on("end", function() { /* set up next transition */ }). Now .on("end",callbackFunction) will trigger on the end of each transition (if transitioning many elements, it'll trigger when each element finishes its transition), but you are transitioning single elements (as IDs are unique), so you could use something like this:
d3.select('#' + linkId[0]).transition()
.duration(2500)
.attr('stroke', 'green')
.on("end", function() {
d3.select('#' + nodeId[0]).transition().duration(5000).attr('fill', 'blue');
})
If you had many elements transitioning simultaneously you'd need to modify this slightly to check to see if any transitions were still in progress before starting the next transition.
I'm using the scrollama javascript library to write a "scrollytelling" article that involves transitioning D3 graphs in and out of view as the user scrolls. It is mostly working, but the graphs pile up on top of each other if I scroll too quickly.
Here is a jsfiddle based on this example by the scrollama author. In my example, the colored dots should fade in one at a time. If you were to scroll quickly to the end, the intermittent dots should not show up. The following snippets show how I've set up the transitions:
I define some functions that create my "graphs", and then call them.
var makeCircle0 = function(){
g.append("circle")
.attr("cx", 50)
.attr("cy", 100)
.attr("r", 20)
.attr("fill", "red")
.attr("class", "redcircle")
g.selectAll(".redcircle")
.attr("opacity", 0)
}
makeCircle0();
// Do this for makeCircle1, 2, and 3, also.
Then, I make functions to handle the transitions. This one says to make the red circle fade in and put the other circles at 0 opacity. I do this for all the circles/stages.
var showCircle0 = function(){
g.selectAll(".redcircle")
.transition()
.duration(1000)
.attr("opacity", 1)
g.selectAll(".yellowcircle").attr("opacity", 0)
g.selectAll(".greencircle").attr("opacity", 0)
g.selectAll(".bluecircle").attr("opacity", 0)
}
This section creates an array of my transition functions so that I can call them at specific steps in the page as you scroll. This is similar to how Jim Vallandingham handled his scroller.
var activateFunctions = [];
activateFunctions[0] = showCircle0;
activateFunctions[1] = showCircle1;
activateFunctions[2] = showCircle2;
activateFunctions[3] = showCircle3;
Finally, this calls the desired function at the right step in the page. Which it does... but not without halting the other transitions that got triggered in a previous step, resulting in multiple dots showing up at various stages.
function handleStepEnter(response) {
step.classed('is-active', function (d, i) {
return i === response.index;
})
figure.call(activateFunctions[response.index])
}
How can I prevent this?
If you need to interrupt a transition, d3-transition has a method for that:
selection.interrupt();
This will cancel a transition on an selection. If using named transitions you can specify a name by providing interrupt with one argument indicating the name of the transition to cancel.
If this is a generic version of your function to show an element:
function show() {
selectionToHide.attr("opacity",0);
selectionToShow.transition()
.attr("opacity",1);
}
Without using selection.interrupt you set the opacity to zero, and then the next tick of any transition in progress continues to update the opacity and finishes carrying out the transition. By adding interrupt we avoid that. Here's an updated fiddle.
However, there is another solution - we can apply another transition on the elements that we want to not show. To do so we just replace the transition with a new one:
function show() {
selectionToHide.transition()
.attr("opacity",0);
selectionToShow.transition()
.attr("opacity",1);
}
This will replace existing unnamed transitions (as yours are not named) and fade out elements, rather than simply hiding them all at once. Here's a fiddle of that. Of course if you have many elements this can be refined as to only apply a transition on any elements that are transitioning (not those that are already hidden) to reduce the amount of active transitions.
I haven't touched the scrolling, the circle that is shown should have its index match the displayed number, but it seems the number doesn't always match the scroll position, but this is a separate issue
i've managed to hack together a d3 transition, check it out here.
https://jsfiddle.net/guanzo/k1xw6ace/14/
Here's the basic idea:
I need to handle the possibility of 2 columns in case the svg height is not enough.
All transitions are a fade in/out and a translate +/- 30px on the x-axis;
On initialLoad, all legends transition into view.
When you click the "Reposition" button and data exits, exiting legends will immediately transition out, after the last legend exits the remaining legends will transition to their new position.
When you click the button again and data enters, updated legends will immediately transition to their new positions (taking into account entering legends), after the last update transition, the entering legends will transition in.
My problem is that my code is relying on setTimeouts and a couple hacks to deal with clumsy selections and conflicting enter/update states.
For example when data enters i need to selectAll update AND enter legends, in order to determine their new position. However i need to set different transitions to both groups.
The transition order depends on if data is entering or exiting. If data is entering, the update selection transitions first, then the enter selection. if data is exiting, the exit selection transitions first, then the update selection. How do i elegantly code this inverse relationship?
I'm just confused on the best way to do this.
I learnt this trick to chain transitions after the first transition has completed:
function endall(transition, callback) {
if (transition.size() === 0) {
callback()
}
var n = 0;
transition
.each(function() {
++n;
})
.each("end", function() {
if (!--n) callback.apply(this, arguments);
});
}
then in your transition function:
selection.transition()
.attr("cx", xMap)
.attr("cy", yMap)
.call(endall, function() {
console.log("all loaded");
// do your next transition
});
Everyone knows the update/enter/exit pattern:
function update(data) {
// DATA JOIN
// Join new data with old elements, if any.
var text = svg.selectAll("text")
.data(data);
// UPDATE
// Update old elements as needed.
text.attr("class", "update");
// ENTER
// Create new elements as needed.
text.enter().append("text")
.attr("class", "enter")
.attr("x", function(d, i) { return i * 32; })
.attr("dy", ".35em");
// ENTER + UPDATE
// Appending to the enter selection expands the update selection to include
// entering elements; so, operations on the update selection after appending to
// the enter selection will apply to both entering and updating nodes.
text.text(function(d) { return d; });
// EXIT
// Remove old elements as needed.
text.exit().transition().duration(2000).attr('transform',
rotation = 25;
return 'translate(' + d.x + ',' + d.y + ') rotate('+ rotation +' 30 30)';
}).remove();
}
My question is:
If I run this function while the exit is still occurring (during the 2000 ms transition window), how can I prevent the transition being called on elements already undergoing the exit transition.
In other words, when the original selection is made, it includes elements already in the exit().transition() state. So the exit transition is called on these elements a second time if they are not finished with their transition and removed().
Would I need to make a filter? Is there a test for in transition while exiting?
D3.js: Stop transitions being interrupted?
says give the exiting element a special class and then use that to only admit freshly exiting elements to the exit transition
.exit()
.filter(function(d,i) {
return !d3.select(this).classed("exiting");
})
.classed("exiting", true)
.transition()
...
A bigger problem will be any data that's re-introduced on your second click while it's still subject to an exit transition from the first. This in your example would disappear elements you want to keep as the exit transition finishes and calls a remove on that element. To get round this in the past I've added immediate (zero delay, zero duration) transitions to my update and enter selections to override/write the exit selection, if they don't have their own transitions already (if there's a better way someone let me know).
This leads to another point: set .classed("exiting", false) in the enter and update, so in the case of 'exiting'>'re-introduced'>'exiting again' elements the .exiting class isn't set and the exiting transition is activated.
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