Unable to remove d3 transition despite calling interrupt on selection - d3.js

I have a transition running on some circles in d3. The transition brings the circles into view, and updates a text display with their time stamp:
var component = this;
select(this.node).select("#circles").selectAll(".pin")
.data(this.props.data)
.enter()
.append("circle", ".pin")
.attr("r", 5/component.state.zoomScale)
.style("fill", "#ff0000")
.style("opacity", "0.0")
.transition()
.on("start", function(d, i) {
if (i % component.props.multiplier == 0) {
select("#timer").text(d.time);
}
})
.style("opacity", "1.0")
.delay(function(d, i) {return d.delay/component.props.multiplier;});
If my component receives new data, I want to stop any running transitions on the circles, clear the text and remove the circles:
var circles = select(this.node).select("#circles").selectAll(".pin");
if (!circles.empty()) {
circles.interrupt();
select("#timer").text(""); //This is my time display that I want to clear
circles.remove();
While the circles are removed fine, the text reappears after being removed suggesting the transition was never actually stopped. How do I correctly stop the transition running on my circles? I am using d3.js v4 within ReactJS.

you have to set the class separate and not with the append call.
Your selection to interrupt does select nothing, there is no circle with that class.
var component = this;
select(this.node).select("#circles").selectAll(".pin")
.data(this.props.data)
.enter()
.append("circle")
.attr("class", "pin")
.attr("r", 5/component.state.zoomScale)
.style("fill", "#ff0000")
.style("opacity", "0.0")
.transition()
.on("start", function(d, i) {
if (i % component.props.multiplier == 0) {
select("#timer").text(d.time);
}
})
.style("opacity", "1.0")
.delay(function(d, i) {return d.delay/component.props.multiplier;});

Related

Transition trigger to not work till another transition is complete

I have 2 circles that move right when clicked. Transition takes 10 sec.
What I want
Requirement 1: If circle 1 is in transition, clicking circle 2 should not trigger transition
Requirement 2: If circle 1 is in transition, clicking circle 2 should stop circle 1 at its current position and start transition for circle 2
here is my code
let svg = d3.select("body").append("svg").attr("width", 1800).attr("height", 1800)
svg.selectAll("circles")
.data([100,200])
.enter()
.append("circle")
.attr("class","zubi")
.attr("cx",50)
.attr("cy",d=>d)
.attr("r",30)
.on("click",function(){
d3.select(this)
.transition()
.duration(10000)
.attr("transform","translate(800)")
})
One way to block any transitions is to just remove the handler when the transition is started, and put it back when it's done:
let svg = d3.select("body").append("svg").attr("width", 600).attr("height", 300);
function myClickFunction(d, i) {
circles.on("click", null);
d3.select(this)
.transition()
.duration(5000)
.attr("transform", "translate(300)")
.on("end", () => circles.on("click", myClickFunction));
}
let circles = svg.selectAll("circles")
.data([100, 200])
.enter()
.append("circle")
.attr("class", "zubi")
.attr("cx", 50)
.attr("cy", d => d)
.attr("r", 30)
.on("click", myClickFunction)
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
Other possibilities are to add/remove classes, or to get the active transition using d3.active.
You can use d3.selection.interrupt to cancel any currently running transitions on a selection. I gave the transition a name so you can more fine-grained control. You can have multiple transitions run side by side if you want - if no name is given all transitions are interrupted.
let svg = d3.select("body").append("svg").attr("width", 600).attr("height", 300)
let circles = svg.selectAll("circles")
.data([100, 200])
.enter()
.append("circle")
.attr("class", "zubi")
.attr("cx", 50)
.attr("cy", d => d)
.attr("r", 30)
.on("click", function(d, i) {
// Cancel any running transitions
circles.interrupt("circle-transform");
d3.select(this)
.transition("circle-transform")
.duration(5000)
.attr("transform", "translate(300)")
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>

D3: use transition .attr('x') to horizontally slide <text> elements

I want to
display some text one after another
after each of the text is displayed, I would like to slide them horizontally from the left side of the page to the right side of the page: so I would like to change the texts' x= 10 to x= 500.
I am able to make step 1) but not 2).
Here is my script:
var textall =["one", "two", "three"]
var number = -1
d3.select('svg')
.transition()
.duration(0)
.delay(0)
.on("start", function addtext() {
number+=1;
if (number < textall.length){
d3.select('svg').append('g').append("text")
.attr("class", "one")
.attr('x', 10)
.attr('y', function(d,i) {return (number+1)*30; })
.text(textall[number])
.attr("font-family", "sans-serif")
.attr("font-size", "20px")
.attr("fill", "black")
.transition()
.duration(0)
.delay(2000)
.on("start", addtext);
}
<!-- }; -->
});
d3.selectAll(".one")
.transition()
.duration(1000)
.delay(6000)
.attr('x', 90)
On jsfiddle
Updated Fiddle I have updated your fiddle, not with the end result but with enough that you can fill in the exact values. Basically I added and .on("end") function that moves the text after they have been added. Further tweaking can happen to improve the aesthetic.

Warbling circle using d3

I wanted to draw a pulsating warbling circle on a geomap using d3, using this example for guidance. However—and I know this is a very basic question, so apologies—I can't seem to get the selector to fire right. Here is the relevant code:
layer2
.attr("id", "locations")
.selectAll(".state")
.data(columbia.features)
.enter().append("circle")
.attr("class", "location")
.attr("r", "4px")
.attr("cx", function(d) { return proj(d['geometry']['coordinates'])[0]; })
.attr("cy", function(d) { return proj(d['geometry']['coordinates'])[1]; })
.attr("d", path)
.each(pulse);
function pulse() {
var circle = svg.select(".location");
console.log(circle);
(function repeat() {
circle = circle.transition()
.duration(2000)
.attr("stroke-width", 20)
.attr("r", 10)
.transition()
.duration(2000)
.attr('stroke-width', 0.5)
.attr("r", 200)
.ease('sine')
.each("end", repeat);
})();
}
Right now I have:
var circle = svg.select(".location");
What should I have instead of .location?
Here is the full file.
Edit: got it; I need selectAll, notselect.

Represent the same item twice in a D3 visualisation

The effect I'm going for in a map based visualisation is:
Red circles on a map represent data (things at locations)
When something happens at a location, we briefly (2 seconds) show another, blue circle fading out over the top.
I'm trying to work out how to do this in the D3 paradigm. Specifically: how do you represent the same thing twice?
The problem I run into is that when I try to add the same dataset twice to a given SVG canvas group, nothing gets added. That is, using code like this:
g = svg.append("g");
var feature = g.selectAll("circle")
.data(stations)
.enter().append("circle")
.style("stroke", "black")
.style("opacity", .6)
.style("fill", "red")
.attr("r", function(d, i) { return d.free_bikes; });
var emphasis = g.selectAll("circle")
.data(stations)
.enter().append("circle")
.style("stroke", "black")
.style("opacity", .6)
.style("fill", "blue")
.attr("r", function(d, i) { return d.free_bikes; });
This workaround is ok, but kludgy and potentially limiting:
g2 = svg.append("g");
var emphasis = g2.selectAll("circle")
That is, adding the second group of elements to a different SVG group.
The proper way to do this is to use classes to select the circles (and applying that class when you create them). So you create the features like so:
var feature = g.selectAll("circle.feature")
.data(stations, function (d) { return d.id; } )
.enter().append("circle")
.attr("class", "feature") // <- assign the class
....
Similarly, for the emphasis:
var feature = g.selectAll("circle.emphasis")
.data(stations, function (d) { return d.id; } )
.enter().append("circle")
.attr("class", "emphasis") // <- assign the class
....
I've finally (sort of) figured it out. The two sets of data are treated as one because they share the same key, according to the rules of D3 constancy. So an easy way around is to give each set a key that can't overlap:
var feature = g.selectAll("circle")
.data(stations, function (d) { return d.id; } )
.enter().append("circle")
.style("stroke", "black")
.style("opacity", .6)
.style("fill", "red")
.attr("r", function(d, i) { return d.free_bikes * 1; });
var emphasis = g.selectAll("notathing")
.data(stations, function (d) { return d.id + " cannot possibly overlap"; } )
.enter().append("circle")
.style("stroke", "black")
.style("opacity", .6)
.style("fill", "blue")
.attr("r", function(d, i) { return d.free_bikes * 1; });
The only slight quirk is I have to modify the second selector (g.selectAll("notathing")) so it doesn't match any of the circles created by the first one.

.data() binding only first element

I am building an epidemic simulation using D3's force-directed diagram.
When a transmission event occurs, I want to move a circle from the transmitter to the newly infected individual.
PROBLEM: Only the first element is created and moved according to the bound data.
First, I gather the coordinates:
xyCoords = getPathogen_xyCoords(newInfections);
Where xyCoords looks like the following:
{receiverX: newInfections[i].x, receiverY: newInfections[i].y, transmitterX: newInfections[i].infectedBy.x, transmitterY: newInfections[i].infectedBy.y}
Then I create the circles and bind them to xyCoords:
d3.select(".svg").append("circle")
.attr("class", "pathogen")
d3.selectAll(".pathogen")
.data(xyCoords)
.attr("cx", function(d) { return d.transmitterX})
.attr("cy", function(d) { return d.transmitterY})
.attr("r", 4)
.style("fill", "green")
Finally, the circle is moved with a transition:
d3.selectAll(".pathogen")
.transition()
.duration(500)
.attr("cx", function(d) { return d.receiverX} )
.attr("cy", function(d) { return d.receiverY} );
EDIT: The game has been up for a few months now and doing quite well! Check it out at http://vax.herokuapp.com!
I've solved my problem...
At creation of the circles, I was not "entering" new circles to be associated with the newly bound data.
It was displaying only the first element on bind because there was only one circle to begin with.
Creation of the circles now looks like this:
xyCoords = getPathogen_xyCoords(newInfections);
var pathogen = svg.selectAll(".pathogen")
.data(xyCoords)
.enter()
.append("circle")
.attr("class", "pathogen")
.attr("cx", function(d) { return d.transmitterX })
.attr("cy", function(d) { return d.transmitterY })
.attr("r", 4)
.style("fill", "green")

Resources