Interrupt scrolling transitions in D3.js - d3.js

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

Related

Rendering in the background of a dc.js chart with renderlet

I use dc.js for showing the results of multiple classification algorithms. More specifically, I want to show a precision recall chart (each point corresponds to a result of a classification system).
I already used a dc.js scatter chart for this which works fine.
Additionally I would like to have a d3 contour in the background of the chart which shows the F-measure.
This is already implemented. The only issue is that the contour part is in the foreground and not in the background of the chart.
Please have a look at the jsfiddle for a full example.
Two questions are still open for me because I'm not a dc.js or d3 expert:
Is there a way to put the contour in the background or the symbols(cycles) of the scatter chart in the foreground (I already tried it with the help of this stackoverflow question but with no success)
I used the 'g.brush' selector to get the area of the inner chart. This works fine as long as the brushing is turned on. Is the selector a good way to go or are there better alternatives (which may also work if brushing is switched off).
In my example I put the contour part in the upper left corner to see if it works but I also provide the code (currently uncommented) to increase the width and height of the contour to the correct size.
chart
.on('renderlet', function (chart) {
var innerChart = chart.select('g.brush');
var width = 300, height=300;
//getting the correct width, height
//var innerChartBoundingRect = innerChart.node().getBoundingClientRect();
//var width = innerChartBoundingRect.width, height=innerChartBoundingRect.height;
[contours, color] = generateFmeasureContours(width,height, 1);
innerChart
.selectAll("path")
.data(contours)
.enter()
.append("path")
.attr("d", d3.geoPath())
.attr("fill", d => color(d.value));
var symbols = chart.chartBodyG().selectAll('path.symbol');
symbols.moveToFront();
});
jsfiddle
Putting something in the background is a general purpose SVG skill.
SVG renders everything in the order it is declared, from back to front, so the key is to put your content syntactically before everything else in the chart.
I recommend encapsulating it in an svg <g> element, and to get the order right you can use d3-selection's insert method and the :first-child CSS selector instead of append:
.on('pretransition', function (chart) {
// add contour layer to back (beginning of svg) only if it doesn't exist
var contourLayer = chart.g().selectAll('g.contour-layer').data([0]);
contourLayer = contourLayer
.enter().insert('g', ':first-child')
.attr('class', 'contour-layer')
.attr('transform', 'translate(' + [chart.margins().left,chart.margins().top].join(',') + ')')
.merge(contourLayer);
A few more points on this implementation:
use dc's pretransition event because it happens immediately after rendering and redrawing (whereas renderlet waits for transitions to complete)
the pattern .data([0]).enter() adds the element only if it doesn't exist. (It binds a 1-element array; it doesn't matter what that element is.) This matters because the event handler will get called on every redraw and we don't want to keep adding layers.
we give our layer the distinct class name contour-layer so that we can identify it, and so the add-once pattern works
contourLayer = contourLayer.enter().insert(...)...merge(contourLayer) is another common D3 pattern to insert stuff and merge it back into the selection so that we treat insertion and modification the same later on. This would probably be simpler with the newer selection.join method but tbh I haven't tried that yet.
(I think there may also have been some improvements in ordering that might be easier than insert, but again, I'm going with what I know works.)
finally, we fetch the upper-left offset from the margin mixin
Next, we can retrieve the width and height of the actual chart body using
(sigh, undocumented) methods from dc.marginMixin:
var width = chart.effectiveWidth(), height = chart.effectiveHeight();
And we don't need to move dots to front or any of that; the rest of your code is as before except we use this new layer instead of drawing to the brushing layer:
contourLayer
.selectAll("path")
.data(contours)
.enter()
.append("path")
.attr("d", d3.geoPath())
.attr("fill", d => color(d.value));
Fork of your fiddle.
Again, if you'd like to collaborate on getting a contour example into dc.js, that would be awesome!

How can I make my D3 code cleaner

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
});

Using D3, how can I transition a line one point at time?

I'm working on a visualization project in which one component is a line chart overlayed on a bar graph. I delayed the bar transitions at a time. I would like the line to transition similarly so each point on the line remains "attached" to the bar.
Here's the code for the line:
var line = d3.svg.line()
.x(function(d, i) {
return xScale(i) + 20;
})
.y(function(d) {
return h - yScale(parseFloat(d.performance));
});
svg1.append("svg:path").attr("d", line(dataset[0].months));
And here's where I transition it:
svg1.select("path")
.transition()
.ease("linear")
.duration(1000)
.attr("d", line(dataset[count].months));
I've seen other questions addressing d3 line transitions but none that seem to address my issue, so I hope I'm not a repeater. Thanks in advance!
Note: I did try putting the delay() function in after transition which didn't work. I'm assuming this is because the line is a single <path> instead of multiple <rect> elements...
So this fell off my radar for a while, but I had some time the other day and figured out one approach for doing the delayed transition...
Here is the pen I wrote. I generated some random data and created a simple line chart to showing stock prices to play around with. The trick here is instead of iterating through a selection of elements using transition, we iterate through the dataset updating it point by point and transitioning the line as we go:
dataset.forEach(function(item, index) {
let set = dataset.slice();
// Update the current point in the copy
set[index].price = newPrice();
stock_line.transition()
.delay(index * 500)
.attr('d', line_generator(set));
});
Admittedly this is a bit hacky and possibly overkill, you could just update the whole line at once. Also #Lars mentioned the possibility of using the stroke-dashoffset trick to accomplish this as well. I have played around with that method to animate drawing the line but I'm not sure how I'd use it to accomplish the same delayed animation shown in the pen. If there is a less hacky implementation please let me know and I'll update this answer.

How to create slow simultaneous transitions of multiple attributes in force directed graphs?

In a previous post called "D3: How to create slow transition of Circles for nodes in Force Directed Graphs FDG?", I got a great answer for how to transition a single element (e.g. the radius for "just circles") in D3.
My followup question is now about how to transition "multiple D3 attributes" at the same time...
As a reminder, I'm using D3 generated Radio Buttons to toggle the size of Nodes in a FDG Layout (on mouse click) from a default size to a scaled magnitude. You can find the Radio Buttons in the upper left hand of the Node Cluster Diagram (http://nounz.if4it.com/Nouns/Applications/A__Application_1.NodeCluster.html)
The code that toggles the node circles between a default number and a scaled magnitude (now using transitions) looks as follows...
var densityControlClick = function() {
var thisObject = d3.select(this);
var typeValue = thisObject.attr("density_type");
var oppositeTypeValue = (function() {
if(typeValue=="On") {
return "Off";
} else {
return "On";
}
})();
var densityBulletSelector = "." + "densityControlBullet-" + typeValue;
var selectedBullet = d3.selectAll(densityBulletSelector);
selectedBullet.style("fill", "Black")
var oppositeDensityBulletSelector = "." + "densityControlBullet-" + oppositeTypeValue;
var selectedOppositeBullet = d3.selectAll(oppositeDensityBulletSelector);
selectedOppositeBullet.style("fill", "White")
if(typeValue=="On") {
var selectedNodeCircles = d3.selectAll("#NODE");
selectedNodeCircles.transition().duration(500).attr("r", function(d){ return rRange(d.rSize); });
}
else {
var selectedNodeCircles = d3.selectAll("#NODE"); selectedNodeCircles.transition().duration(500).attr("r", function(d) { if (d.id==focalNodeID) { return centerNodeSize; } else { return defaultNodeSize; } } );
}
}
Everything works great and you can see the slower node transitions when you select the radio buttons. However, I'd now like to learn how to transition multiple elements, such as the the radius and the edge lengths simultaneously, along with the theory behind doing so, in order to show off D3's dynamic nature.
My question is: Given that I already can successfully transition the radius of circles, how would I also transition other elements like the edge lengths based on attributes like "alpha", "friction", etc., and... what's the theory behind transitioning multiple elements (in other words, what does the code mean, in English)? The D3 API doesn't appear to clearly get into the theory behind transitioning multiple attributes, simultaneously.
So transitioning multiple attributes is the simple part of this question. Just like a regular selection you can set multiple attributes at a time on your transition:
selectedNodeCircles.transition().duration(500)
.attr("r", function(d){ return rRange(d.rSize); })
.attr("stroke", 'red');
This will transition your radius and your line colour. The transition is a property of the DOM element (in this case the circle) and it will transition as many DOM attributes as you like. The thing to remember is that there is only only one transition object on each DOM element. So if you create another you will overwrite the old one.
// This will NOT work
circles.transition().duration(1000).attr('r', 50);
// The radius transition will be overridden by the fill
// transition and so will not complete
circles.transition().duration(1000).attr('fill', 'red');
This can actually be quite useful because you don't have to worry about interrupting animations that are in progress and figure out how far along they are and then starting a new animation - this will generally be handled automatically.
In your case you want to transition edge lengths in your graph. These are determined by the positional attributes of the nodes. Judging by your finished product, these attributes are already being animated because you are updating the DOM on every iteration of the layout algorithm (not through transitions) probably in the tick() callback.
So you could use transitions inside your tick callback, which might look odd and may be a hassle to keep in synch with the radius transitions (you will have to set both attributes in the transition). But it might be just what you need.
Alternatively, if you can wait, don't update the DOM in the tick callback. Let the layout complete - it runs a lot faster when it is not rendering on each tick - and once it is complete you can animate the radius and x and y attributes to their final positions. Of course this means you'll want good starting positions.

how to animate and play over time in d3.js?

I am a novice while working on d3.js.
I wanted to know how can we Animate some data (eg. Change colors) with respect to time.
eg. Let's say, in Monitoring app, I am projecting cluster data over US Map. Projection is done by drawing a circle and filling it by RED, GREEN or YELLOW color depending on it's status.
When we start monitoring, ideally all circles will be filled with "GREEN" color and then over time color can change to "YELLOW" or "RED" depending on how cluster is behaving.
So if I need to play these color changes over time in some time window, how can it be done ?
If you can point me to any of the similar examples , that will help too ?
Thanks
Take a look at http://mbostock.github.com/d3/tutorial/bar-2.html. Basically you'll need a redraw function that you'll call whenever you want to update your chart. (Note: there is nothing special about the name of this function, you can call it whatever you want.)
You can use setInterval to create a basic timer, this is the rate that your chart will be updated.
setInterval(function() {
redraw(); // call the function you created to update the chart
}, 1500);
Then you define redraw to update the chart data. This is a redraw function for a bar chart, but yours would be similar. You would just be adjusting the color based on the data instead of the y position and height.
function redraw() {
// Update…
chart.selectAll("rect")
.data(data)
.transition()
.duration(1000)
.attr("y", function(d) { return h - y(d.value) - .5; })
.attr("height", function(d) { return y(d.value); });
}
Note that this is a simplified version, I recommend reading the page that I linked above for a more complete example.

Resources