Ian
Ian

Reputation: 34489

Chaining transitions

I'm trying to chain a transition in D3 and I can't quite figure out how to make it work properly. I've read through some of the examples and I feel like I'm missing something with regards to the selections (possibly because my selections are across different layers).

You can see an example below, clicking 'Objectives' should animate a pulse of light to the "Service" node. Once the pulse arrives I want the Service node to fill to orange with a transition. At the moment I'm aware of the fact my selection will fill both circles - I'll fix that shortly.

What happens however is that when the pulse arrives nothing happens:

var t0 = svg.transition();

var t1 = t0.selectAll(".pulse")
    .duration(2000)
    .ease("easeInOutSine")
    .attr("cx", function(d) { return d.x2; })
    .attr("cy", function(d) { return d.y2; });

t1.selectAll(".node")
  .style("fill", "#F79646");

The only way I seem to be able to get a change is if I change the final bit of code to:

t0.selectAll(".node")
  .style("fill", "#F79646");

However that causes the node to fill instantly, rather than waiting for the pulse to arrive. It feels like the selection isn't "expanding" to select the .node instances, but I'm not quite sure

var nodes = [
    { x: 105, y: 105, r: 55, color: "#3BAF4A", title: "Objectives" }, 
    { x: 305, y: 505, r: 35, color: "#F79646", title: "Service" }
];
var links = [
    { x1: 105, y1: 105, x2: 305, y2: 505 }   
];

var svg = d3.select("svg");
var relationshipLayer =svg.append("g").attr("id", "relationships");
var nodeLayer = svg.append("g").attr("id", "nodes");

// Add the nodes
var nodeEnter = nodeLayer.selectAll("circle").data(nodes).enter();
    
var nodes = nodeEnter.append("g")
                     .attr("transform", function(d) { return "translate(" + d.x + "," + d.y + ")";})
                     .on("click", function (d) {
                         d3.select(this)
                            .select("circle")
                             .transition()
                             .style("stroke", "#397F42")
                             .style("fill", "#3BAF4A");
                         
                         pulse(d); 
                     });

var circles = nodes.append("circle")
                    .attr("class", "node")
                    .attr("r", function (d) { return d.r; })
                    .style("fill", "#1C1C1C")
                    .style("stroke-width", "4px")
                    .style("stroke", function (d) { return d.color; });

var texts = nodes.append("text")
                 .text(function (d) { return d.title; })
                 .attr("dx", function(d) { return -d.r / 2; })
                 .style("fill", "white");

function pulse(d) {
    function distanceFunction(x1, y1, x2, y2) {
      var a = (x2 - x1)  * (x2 - x1);
      var b = (y2 - y1) * (y2 - y1);
      return Math.sqrt(a + b);
    };
    
    var lineFunction = d3.svg.line()
        .x(function (d) { return d.x; })
        .y(function (d) { return d.y; })
        .interpolate("linear");   
    
    var lines = relationshipLayer
                    .selectAll("line")
    .data(links)
    .enter()
    .append("line")
    .attr("x1", function(d) { return d.x1; })
    .attr("y1", function(d) { return d.y1; })
    .attr("x2", function(d) { return d.x2; })
    .attr("y2", function(d) { return d.y2; })
    .attr("stroke-dasharray", function(d) { return distanceFunction(d.x1, d.y1, d.x2, d.y2); })
    .attr("stroke-dashoffset", function(d) { return distanceFunction(d.x1, d.y1, d.x2, d.y2); });
    
    var pulse = relationshipLayer
            .selectAll(".pulse")
            .data(links)
            .enter()
            .append("circle")
            .attr("class", "pulse")
            .attr("cx", function(d) { return d.x1; })
            .attr("cy", function(d) { return d.y1; })
            .attr("r", 50);
    
    lines.transition()
        .duration(2000)
        .ease("easeInOutSine")
        .attr("stroke-dashoffset", 0);
    
    var t0 = svg.transition();
        
    var t1 = t0.selectAll(".pulse")
        .duration(2000)
        .ease("easeInOutSine")
        .attr("cx", function(d) { return d.x2; })
        .attr("cy", function(d) { return d.y2; });
    
    t1.selectAll(".node")
      .style("fill", "#F79646");
};
svg {
    background: #222234;
    width: 600px;
    height: 600px;
    font-size: 10px;
    text-align: center;
    font-family: 'Open Sans', Arial, sans-serif;
}
circle {
    fill: url(#grad1);
}
line {
    fill: none;
    stroke: #fff;
    stroke-width: 2px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
<svg id="svg">
    <defs>
        <radialGradient id="grad1" cx="50%" cy="50%" r="50%" fx="50%" fy="50%">
            <stop offset="5%" style="stop-color:rgb(255,255,255); stop-opacity:1" />
            <stop offset="10%" style="stop-color:rgb(255,255,255); stop-opacity:0.8" />
            <stop offset="20%" style="stop-color:rgb(255,255,255); stop-opacity:0.6" />
            <stop offset="60%" style="stop-color:rgb(255,255,255);stop-opacity:0.0" />
        </radialGradient>
    </defs>
</svg>

Upvotes: 1

Views: 96

Answers (1)

Lars Kotthoff
Lars Kotthoff

Reputation: 109232

The reason why you're not seeing a change for the second transition is that it's not applied to anything. The selection for your first transition contains all the elements with class pulse, and then you're selecting the elements with class node from the elements of this first selection. There are no elements that have both classes, therefore your selection is empty and the change is applied to no elements.

In general, you can't chain transitions in the way that you're currently using when changing selections. Instead, use the .each() event handler of the transition, which allows you to install a handler function that is executed when the transition finishes. In your case, this would look like this:

svg.selectAll(".pulse")
    .transition()
    .duration(2000)
    .ease("easeInOutSine")
    .attr("cx", function(d) { return d.x2; })
    .attr("cy", function(d) { return d.y2; })
    .each("end", function() {
        svg.selectAll(".node")
            .transition()
            .duration(2000)
            .style("fill", "#F79646");
    });

This will select all the elements that have class node and change their fill to orange with a transition.

There are two problems with the above code -- first, as you have already observed, it changes the fill of all the nodes and not just the target, and second, the end event handler is executed for each element in the transition, not just once. For your particular example, this isn't a problem because you have only one link that's animated, but if you had several, the function (and therefore the transition) would be executed more than once.

Both problems can be fixed quite easily with the same code. The idea is to filter the selection of node elements to include only the target of the line. One way of doing this is to compare the target coordinates of the line with the coordinates of the elements in the selection:

svg.selectAll(".pulse")
    .transition()
    .duration(2000)
    .ease("easeInOutSine")
    .attr("cx", function(d) { return d.x2; })
    .attr("cy", function(d) { return d.y2; })
    .each("end", function(d) {
        svg.selectAll(".node")
            .filter(function(e) {
                return e.x == d.x2 && e.y == d.y2;
            })
            .transition()
            .duration(2000)
            .style("fill", "#F79646");
    });

The argument d to the handler function is the data bound to the element that is being transitioned, which contains the target coordinates. After the filter() line, the selection will contain only the circle that the line moves towards. It is safe to execute this code several times for multiple lines as long as their targets are different.

Complete demo here.

Upvotes: 1

Related Questions