I have a d3 graph with force enabled and I want to test the position is as expected after a drag action.
This is the force function:
var simulation = d3.forceSimulation()
.force("center", d3.forceCenter().x(width / 2).y(height / 2))
.force("charge", d3.forceManyBody().strength(1))
.force("collide", d3.forceCollide().strength(.1).radius(30).iterations(1))
simulation
.nodes(data)
.on("tick", function(d){
node
.attr("cx", function(d){ return d.x; })
.attr("cy", function(d){ return d.y; })
});
Testing with
cy.get('svg circle').eq(0)
.should('have.attr', 'cx', '342.0317014443128')
The problem is the position attributes of the elements never stop changing, due to the force simulation.
You have a couple of options.
Try increasing the timeout, the graph adjustments are a bit like an an asynchronous network call in that they should resolve eventually.
cy.get('svg circle').eq(0, {timeout:10000})
.should('have.attr', 'cx', '342.0317014443128')
Failing that, change the assertion criteria to be good enough, i.e close to the integer pixel value.
cy.get('svg circle').eq(0)
.invoke('attr', 'cx')
.then(parseInt)
.should(value => expect(value).to.be.closeTo(342, 1))
If the force simulation is using setTimeout() or setInterval(), you can us cy.clock() to freeze the animation.
Try the following:
cy.clock()
cy.visit('/')
cy.get('svg circle').eq(0)
.should('have.attr', 'cx', '342.0317014443128')
Related
I am doing a number of transitions when my bar chart renders.
After these transitions have completed, I would like the values to render.
I am trying to use d3 transition.end but it looks like the code has changed from previous versions - I am using v6.
The code below runs without any delay - it doesn't wait for the transition to complete before invoking the function.
I have also tried .end(renderValuesInBars(data, metric, countryID, measurements) ) but the same thing happens.
Where am I going wrong?
function renderVerticalBars(data, measurements, metric, countryID) {
let selectDataForBarCharts = d3.select("svg")
.selectAll("rect")
.data(data, d => d[countryID])
selectDataForBarCharts
.enter()
.append("rect")
.attr('width', measurements.xScale.bandwidth())
.attr("height", 0)
.attr('y', d => measurements.yScale(0))
.merge(selectDataForBarCharts)
.transition().delay(500)
.attr("transform", `translate(0, ${measurements.margin.top})`)
.attr('width', measurements.xScale.bandwidth())
.attr('x', (d) => measurements.xScale(d[countryID]))
.transition()
.ease(d3.easeLinear)
.duration(setSpeed())
.attr("height", d => measurements.innerHeight - measurements.yScale(d[metric]))
.attr("y", (d) => measurements.yScale(d[metric]))
.attr("fill", d => setBarColor(d))
.on("end", renderValuesInBars(data, metric, countryID, measurements) )
selectDataForBarCharts.exit()
.transition().duration(500).attr("height", 0).attr("y", d => measurements.yScale(0)).remove()
}
Note that the .on("end", ...) method takes a callback for the second argument, which is executed when the transition ends. The code you posted is not passing a callback, but already evaluating the renderValuesInBars function at the moment of declaration. Instead, you want to pass a callback that tells d3 that the evaluation should occur at a later time (in that case, after the transition)
Instead of:
.on("end", renderValuesInBars(data, metric, countryID, measurements))
You can pass a callback that evaluates the function:
on("end", ( ) => renderValuesInBars(data, metric, countryID, measurements))
That way you're passing a callback that says "at the end of the transition, evaluate renderValuesInBars"
Has been changed from version 4.0
Previously it was:
element.each("end", callback)
Now it is:
element.on("end", callback)
Does someone know of a way to 'flush' a transition.
I have a transition defined as follows:
this.paths.attr('transform', null)
.transition()
.duration(this.duration)
.ease(d3.easeLinear)
.attr('transform', 'translate(' + this.xScale(translationX) + ', 0)')
I am aware I can do
this.paths.interrupt();
to stop the transition, but that doesn't finish my animation. I would like to be able to 'flush' the transition which would immediately finish the animation.
If I understand correctly (and I might not) there is no out of the box solution for this without going under the hood a bit. However, I believe you could build the functionality in a relatively straightforward manner if selection.interrupt() is of the form you are looking for.
To do so, you'll want to create a new method for d3 selections that access the transition data (located at: selection.node().__transition). The transition data includes the data on the tweens, the timer, and other transition details, but the most simple solution would be to set the duration to zero which will force the transition to end and place it in its end state:
The __transition data variable can have empty slots (of a variable number), which can cause grief in firefox (as far as I'm aware, when using forEach loops), so I've used a keys approach to get the non-empty slot that contains the transition.
d3.selection.prototype.finish = function() {
var slots = this.node().__transition;
var keys = Object.keys(slots);
keys.forEach(function(d,i) {
if(slots[d]) slots[d].duration = 0;
})
}
If working with delays, you can also trigger the timer callback with something like: if(slots[d]) slots[d].timer._call();, as setting the delay to zero does not affect the transition.
Using this code block you call selection.finish() which will force the transition to its end state, click a circle to invoke the method:
d3.selection.prototype.finish = function() {
var slots = this.node().__transition;
var keys = Object.keys(slots);
keys.forEach(function(d,i) {
if(slots[d]) slots[d].timer._call();
})
}
var svg = d3.select("body")
.append("svg")
.attr("width", 500)
.attr("height", 500);
var circle = svg.selectAll("circle")
.data([1,2,3,4,5,6,7,8])
.enter()
.append("circle")
.attr("cx",50)
.attr("cy",function(d) { return d * 50 })
.attr("r",20)
.on("click", function() { d3.select(this).finish() })
circle
.transition()
.delay(function(d) { return d * 500; })
.duration(function(d) { return d* 5000; })
.attr("cx", 460)
.on("end", function() {
d3.select(this).attr("fill","steelblue"); // to visualize end event
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.12.0/d3.min.js"></script>
Of course, if you wanted to keep the method d3-ish, return the selection so you can chain additional methods on after. And for completeness, you'll want to ensure that there is a transition to finish. With these additions, the new method might look something like:
d3.selection.prototype.finish = function() {
// check if there is a transition to finish:
if (this.node().__transition) {
// if there is transition data in any slot in the transition array, call the timer callback:
var slots = this.node().__transition;
var keys = Object.keys(slots);
keys.forEach(function(d,i) {
if(slots[d]) slots[d].timer._call();
})
}
// return the selection:
return this;
}
Here's a bl.ock of this more complete implementation.
The above is for version 4 and 5 of D3. To replicate this in version 3 is a little more difficult as timers and transitions were reworked a bit for version 4. In version three they are a bit less friendly, but the behavior can be achieved with slight modification. For completeness, here's a block of a d3v3 example.
Andrew's answer is a great one. However, just for the sake of curiosity, I believe it can be done without extending prototypes, using .on("interrupt" as the listener.
Here I'm shamelessly copying Andrew code for the transitions and this answer for getting the target attribute.
selection.on("click", function() {
d3.select(this).interrupt()
})
transition.on("interrupt", function() {
var elem = this;
var targetValue = d3.active(this)
.attrTween("cx")
.call(this)(1);
d3.select(this).attr("cx", targetValue)
})
Here is the demo:
var svg = d3.select("svg")
var circle = svg.selectAll("circle")
.data([1, 2, 3, 4, 5, 6, 7, 8])
.enter()
.append("circle")
.attr("cx", 50)
.attr("cy", function(d) {
return d * 50
})
.attr("r", 20)
.on("click", function() {
d3.select(this).interrupt()
})
circle
.transition()
.delay(function(d) {
return d * 500;
})
.duration(function(d) {
return d * 5000;
})
.attr("cx", 460)
.on("interrupt", function() {
var elem = this;
var targetValue = d3.active(this)
.attrTween("cx")
.call(this)(1);
d3.select(this).attr("cx", targetValue)
})
<script src="https://d3js.org/d3.v4.min.js"></script>
<svg width="500" height="500"></svg>
PS: Unlike Andrew's answer, since I'm using d3.active(node) here, the click only works if the transition had started already.
The original Code can be found at: http://bl.ocks.org/Guerino1/3a51eeb95d3a8345bc27370e8c9d5b77
I have numerous polygons that are transitioning onto an svg canvas (from left to right, at the bottom of the HTML page).
The code I use to create an transition the chevrons leverages D3 Polygon:
// Create Polygons for SDLC
svgCanvas.selectAll("a")
.data(dataSet)
.enter().append("a")
.attr("xlink:href", function(d) { return d.link; })
.append("svg:polygon")
//svgCanvas.selectAll("polygon")
//.data(dataSet)
//.enter().append("polygon")
.attr("id", function(d,i){ return (selectString.replace(/ /g,'_').replace(/#/g,'') + "_index_" + i); })
.attr("originalcolor","violet")
.style("stroke","blue")
.style("fill","violet")
.style("stroke-width",2)
.attr("points", origin)
.on('mouseover', chevronMouseOver)
.on("mouseout", chevronMouseOut)
.on("click", chevronMouseOut)
.transition() // <------- TRANSITION STARTS HERE --------
.duration(3000)
.attr("points", calculateChevron);
Currently, all polygons transition into the svg canvas, together. I'd like to put a delay between each of them, so that it looks more like dealing from a deck of cards, one at a time.
How would I properly add a D3 delay to make this happen?
Thanks for any help you can offer.
try this..
.transition() // <------- TRANSITION STARTS HERE --------
.delay(function(d,i){ return 100*i; })
.duration(3000)
.attr("points", calculateChevron);
I have two functions that perform animations on lines. The first function, one is executed at the beginning, in order to perform operations on the enter selection, and only animates the lines' horizontal movement (x1 and x2). The second function two only animates the lines' height (only y2,y1 stays fixed).
Through user events, function one cannot be interrupted by function two, but vice versa (also because the animation in two is considerably longer). This means that, when the transition from two is still running, the user can trigger one.
This gave me serious headaches, because one would somehow take the values of the last state of the running transition of two instead of correctly assigning a data-driven value (i.e. .attr('y2', function(d){ ... });).
http://jsfiddle.net/h39WN/6/ - Please do the following: Click on one. You see that only the horizontal movement is animated as the data changes. You also see that at the end of its execution, the lines should always be ordered from lowest to highest.
Click on two once and wait the full 2 seconds until its animation is completed. Then click on one again. This is the desired behavior.
Now click on two, wait a few ms and then click on one - you see that the lines keep the height of the last state of two's animation, even though they are correctly ordered. (I know the data are not realistic and maybe a bit confusing in this example, but they still allow to replicate the problem).
I then came up with the solution to schedule another, "empty", transition on the lines in one - according to the docs this should cancel the one still running in two when one is invoked:
var one = function () {
var svg = d3.select('svg');
vis.mobileTeams = svg
.selectAll('.teamGroup')
.data(data.values, function (d) {
return d.ID;
});
// ENTER
var teamEnter = vis.mobileTeams
.enter()
.append('g')
.attr('class', 'teamGroup');
// enter line
teamEnter
.append('line')
.attr('class', 'teamWeightedLine');
// UPDATE THE LINE
// HEIGHT - SHOULD NOT BE ANIMATED
svg
.selectAll('.teamGroup line')
.attr('y1', paddingY)
// I inserted a transition here to cancel
// the one triggered by the other function
var lineTransition = svg
.selectAll('.teamGroup line')
.transition()
.attr('y2', function(d){ ... });
// I need to use transition chaining so changing 'y2'
// with a transition does not get
// overwritten by the following transition
// HORIZONTAL POSITION - SHOULD BE ANIMATED
lineTransition
.transition()
.duration(500)
// 'x1' and 'x2' are the only values that
// need to be animated in this function
.attr('x1', function (d) {
return function(d){ ... });
})
.attr('x2', function (d) {
return function(d){ ... });
});
};
And here is the second function.
var two = function () {
var svg = d3.select('svg');
// this is where the only transition concerning HEIGHT is supposed to take place
svg
.selectAll('.teamGroup line')
.transition()
.ease('circle-out')
.duration(2000)
.attr('y2', vis.mobileCalculateScoreToHeight);
console.log('mobile vis updated');
};
Even though this fixes the "interference" problem as two's transition is canceled because another one is scheduled, it brings another problem:
var lineTransition = svg
.selectAll('.teamGroup line')
.transition()
.attr('y2', function(d){ ... });
http://jsfiddle.net/96uN6/8/ This is the fiddle that incorporates this change. Even when two is interrupted by one, do the correct heights result in the end - but:
y2 is now being animated in one too! I know that .transition() brings with it a default duration of 250ms, so I did this:
var lineTransition = svg
.selectAll('.teamGroup line')
.transition()
.duration(0)
.attr('y2', function(d){ ... });
This, in turn, brings another problem: y2 is not set at all now, i.e. the <line>s don't even have it as an attribute:
Weirdly, it works when using a very short duration (so the animation is barely visible), but it only works sometimes and is probably browser-dependent:
var lineTransition = svg
.selectAll('.teamGroup line')
.transition()
.duration(10)
.attr('y2', function(d){ ... });
Setting y2 in the regular selection instead of the transition selection does not work either as it brings back the "interference" problem - as y2 is set when the animation from two is still running.
svg
.selectAll('.teamGroup line')
.attr('y1', paddingY)
.attr('y2', function(d){ ... });
var lineTransition = svg
.selectAll('.teamGroup line')
.transition();
The approach without transition chaining does not work either, of course, because the first transition is immediately canceled by the second and y2 is never set.
svg
.selectAll('.teamGroup line')
.transition()
.duration(10)
.attr('y2', function(d){ ... });
svg
.selectAll('.teamGroup line')
.transition()
.duration(TRANSDURATION)
.attr('x1', function (d) {
return function(d){ ... };
})
.attr('x2', function (d) {
return function(d){ ... };
});
So the only possible solution working for me (the one with the short duration) seems very quirky, there must be a better solution, mustn't it?
Feel free to ask if something is unclear.
Through Mike Bostock (https://github.com/mbostock/d3/issues/1877), I found out that I can use selection.interrupt() to cancel any previous transitions, i.e., already running transitions.
So, the quirky
var lineTransition = svg
.selectAll('.teamGroup line')
.transition()
.duration(0)
.attr('y2', function(d){ ... });
becomes
var lineTransition = svg
.selectAll('.teamGroup line')
.interrupt()
.attr('y2', function(d){ ... });
It's as easy as that.
See: http://jsfiddle.net/96uN6/9/
I thought I understood the D3 enter/update/exit, but I'm having issues implementing an example. The code in http://jsfiddle.net/eamonnmag/47TtN/ illustrates what I'm doing. As you'll see, at each 5 second interval, I increase the rating of an item, and update the display again. This works, in some way. The issue is in only updating what has changed - D3 is updating everything in this case. The enter and exit methods, displayed in the console output that nothing has changed, which makes my think that it's treating each array as a completely new instance.
My understanding of the selectAll() and data() calls was that it would 'bind' all data to a map called 'chocolates' somewhere behind the scenes, then do some logic to detect what was different.
var chocolate = svg.selectAll("chocolates").data(data);
In this case, that is not what's happening. This is the update code. Any pointers to what I've missed are most appreciated!
function update(data){
var chocolate = svg.selectAll("chocolates").data(data);
var chocolateEnter = chocolate.enter().append("g").attr("class", "node");
chocolateEnter.append("circle")
.attr("r", 5)
.attr("class","dot")
.attr("cx", function(d) {return x(d.price)})
.attr("cy", function(d) {
//put the item off screen, to the bottom. The data item will slide up.
return height+100;})
.style("fill", function(d){ return colors(d.manufacturer); });
chocolateEnter
.append("text")
.text(function(d) {
return d.name;})
.attr("x", function(d) {return x(d.price) -10})
.attr("y", function(d){return y(d.rating+step)-10});
chocolateEnter.on("mouseover", function(d) {
d3.select(this).style("opacity", 1);
}).on("mouseout", function(d) {
d3.select(this).style("opacity", .7);
})
chocolate.selectAll('circle')
.transition().duration(500)
.attr('cy', function(d) {return y(d.rating+step)});
var chocolateExit = chocolate.exit().remove();
chocolateExit.selectAll('circle')
.attr('r', 0);
}
setInterval(function() {
chocolates[3].rating = Math.min(chocolates[3].rating+1, 5);
update(chocolates);
}, 5000);
Easy as apple pie!
Why are you doing svg.selectAll("chocolates")? There is no HTML element in your DOM called chocolates.
You need to change that to svg.selectAll(".node"). That will fix the problem.
There are a couple of issues in your code. First, the logic to detect what's different is, by default, to use the index of the item. That is, the first data item is matched to the first DOM element, and so on. This works in your case, but will break if you ever pass in partial data. I would suggest using the second argument to .data() to tell it how to match:
var chocolate = svg.selectAll("g.node").data(data, function(d) { return d.name; });
Second, as the other poster has pointed out, selecting "chocolate" will select nothing, as there are no such DOM elements. Just select the actual elements instead.
Finally, since you're adding g elements for the data items, you might as well use them. What I mean is that currently, you're treating the circles and text separately and have to update both of them. You can however just put everything underneath g elements. Then you have to update only those, which simplifies your code.
I've made all the above changes in your modified fiddle here.