Julio Rodriguez
Julio Rodriguez

Reputation: 515

D3JS chart: Hovering on some transitioning elements while others are still transitioning too stops the execution of the chart animation

I have been working on a horizontal bar chart. I added transition to the width attr of the rects from 0 to the desired value and they worked fine. Then I added some circle elements and made them "appear" one after another using delay, tweening the r attribute from 0 to the desired r value as well, and everything went fine too. The visualization works as expected if I don't hover over any element before all of them have finished their transitions.

Both the rects and circles are tweening their width and r attributes at different delays, which makes some of them to be shown before others.

The problem: if I hover over any rect while the other elements have not finished their transitions, all of them suddenly stop transitioning their attributes. Hence, the desired final state of the whole chart is not reached, actually it becomes a mess. I can't figure out why hovering over an element can interfere with the expected behavior of other "apparently" independent elements.

function draw(){
				
  var width =  $( window ).width() ;
  var height =  document.body.clientHeight 	;

  var data = [
    {country:"Pichonita", growth: 15},
    {country:"Andromeda", growth: 12},
    {country:"India", growth: 33},
    {country:"Indonesia", growth: 22},
    {country:"Russia", growth: 6},
    {country:"Mars", growth: 41},
    {country:"Pluton", growth: 16},
    {country:"Earth", growth: 24},
    {country:"Neptune", growth: 8}
  ]; 

    //set margins
    var margin = {top:30, right:30, bottom:30, left:40};
    var width = width - margin.left - margin.right*2.5;
    var height = height - margin.top - margin.bottom;

    //set scales & ranges

    var xScale = d3.scaleLinear()
      .range([0,  width - 100])

    var yScale = d3.scaleBand()
      .range([0, height]).padding(.2)

    //draw the svg

    var svg = d3.select("body")
      .append("svg")
      .attr("width", width + margin.left + margin.right * 3)
      .attr("height",height + margin.top + margin.bottom)
      .append("g")
      .attr("transform","translate(" + margin.left*2 + "," + margin.top  + ")")

      //force data

       data.forEach(function(d){
      return d.growth = +d.growth;
       });

      //set domains

      yScale.domain(data.map(d => d.country))

      xScale.domain([0, d3.max(data, d=> d.growth)])

      //add X & Y axes and append the bars to Y axis

      var xAxis = svg.append("g")
          .attr("class",xAxis)
          .attr("transform","translate(" + 0 + "," + height + ")")
						.call(d3.axisBottom(xScale))

     var yAxis =  svg.append("g")
           .attr("class",yAxis)
           .call(d3.axisLeft(yScale))
           .selectAll("rect")
           .data(data)
           .enter()
           .append("rect")
           .attr("stroke","transparent")
           .attr("stroke-width",4)
           .on("mouseover", function(){d3.select(this).transition().duration(600).attr("stroke","#6D2077").attr("stroke-width",3).style("fill","#6D2077")
            d3.selectAll(".textCircle").transition().duration(600)
           .attr("r",yScale.bandwidth() / 1.9)
           .attr("stroke","#6D2077")
           .attr("stroke-width",1)
			    }) 
          
		  .on("mouseout", function(){d3.select(this)
      .transition()
      .duration(600)
		  .attr("stroke","transparent")
          .attr("stroke-width",0)
          .style("fill","#00338D")
					 d3.selectAll(".textCircle")
          .transition().duration(600)
					.attr("r", yScale.bandwidth() / 2)
          .attr("stroke","transparent")
				}) 
          .attr("class","bar")
          .attr("height",yScale.bandwidth())
          .attr("x",0.5)
          .attr("y",function(d){
           return  yScale(d.country)
         })
         .attr("width",0)
         .transition()
         .duration(3800)
         .delay( (d,i)=> (i+1) *200)
         .ease(d3.easeElastic)
         .attr("width", function(d){
           return xScale(d.growth)
         })
        .style("fill","#00338D")

        var newG = svg.append("g")
        
         newG.selectAll("circle")
        .data(data)
        .enter()
        .append("circle")
        .attr("class","textCircle")
        .attr("cx",d=> xScale(d.growth) )
        .attr("cy",d=> yScale(d.country) + yScale.bandwidth() / 2)
        .attr("r",0)
        .transition()
          .duration(1200)
        .delay( (d,i)=> (i+1) *450)
        .attr("r",yScale.bandwidth() / 2)
        .attr("opacity",1)
        .style("fill","#0091DA")
        .attr("stroke","transparent")
        }

  draw();

  $( window ).resize(function() {
    $("body").empty();	
    draw();
  });
html{ 
  height: 98%;
  margin: 0;
  padding: 0;
}

body{
  min-height: 98%;
  margin: 0;
  padding: 0;
}

svg{
  text-rendering: geometricPrecision;
  shape-rendering:geometricPrecision;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
	<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.13.0/d3.min.js"></script>

Upvotes: 1

Views: 416

Answers (1)

i alarmed alien
i alarmed alien

Reputation: 9520

Things are getting messed up because you've added mouseover and mouseout event listeners that try to perform actions that conflict with the ongoing transition events. To fix the problem, don't add the mouseover/mouseout listeners until after the chart bars have performed their initial transition. You can add a listener for the end of the transition using transition.on('end', function(){...} and then add the mouse event listeners to the DOM elements once the transition has completed.

    d3.select('#whateverItIs')
    // stuff to do prior to transition
    .transition()
    // transition stuff
    .on('end', function() {
      d3.select(this)
        .on("mouseover", function() {
        // handler code here
        })
        .on("mouseout", function() {
        // handler code here
        })
    })

With your code:

function draw() {

  var width = $(window).width();
  var height = document.body.clientHeight;

  var data = [{
      country: "Pichonita",
      growth: 15
    },
    {
      country: "Andromeda",
      growth: 12
    },
    {
      country: "India",
      growth: 33
    },
    {
      country: "Indonesia",
      growth: 22
    },
    {
      country: "Russia",
      growth: 6
    },
    {
      country: "Mars",
      growth: 41
    },
    {
      country: "Pluton",
      growth: 16
    },
    {
      country: "Earth",
      growth: 24
    },
    {
      country: "Neptune",
      growth: 8
    }
  ];

  //set margins
  var margin = {
    top: 30,
    right: 30,
    bottom: 30,
    left: 40
  };
  var width = width - margin.left - margin.right * 2.5;
  var height = height - margin.top - margin.bottom;

  //set scales & ranges

  var xScale = d3.scaleLinear()
    .range([0, width - 100])

  var yScale = d3.scaleBand()
    .range([0, height]).padding(.2)

  //draw the svg

  var svg = d3.select("body")
    .append("svg")
    .attr("width", width + margin.left + margin.right * 3)
    .attr("height", height + margin.top + margin.bottom)
    .append("g")
    .attr("transform", "translate(" + margin.left * 2 + "," + margin.top + ")")

  //force data

  data.forEach(function(d) {
    return d.growth = +d.growth;
  });

  //set domains

  yScale.domain(data.map(d => d.country))

  xScale.domain([0, d3.max(data, d => d.growth)])

  //add X & Y axes and append the bars to Y axis

  var xAxis = svg.append("g")
    .attr("class", xAxis)
    .attr("transform", "translate(" + 0 + "," + height + ")")
    .call(d3.axisBottom(xScale))

  var yAxis = svg.append("g")
    .attr("class", yAxis)
    .call(d3.axisLeft(yScale))
    .selectAll("rect")
    .data(data)
    .enter()
    .append("rect")
    .attr("stroke", "transparent")
    .attr("stroke-width", 4)
    .attr("class", "bar")
    .attr("height", yScale.bandwidth())
    .attr("x", 0.5)
    .attr("y", function(d) {
      return yScale(d.country)
    })
    .attr("width", 0)
    .transition()
    .duration(3800)
    .delay((d, i) => (i + 1) * 200)
    .ease(d3.easeElastic)
    .attr("width", function(d) {
      return xScale(d.growth)
    })
    .style("fill", "#00338D")
    .on('end', function() {
      d3.select(this)
        .on("mouseover", function() {
          d3.select(this)
            .transition().duration(600)
            .attr("stroke", "#6D2077")
            .attr("stroke-width", 3)
            .style("fill", "#6D2077")
          d3.selectAll(".textCircle")
            .transition().duration(600)
            .attr("r", yScale.bandwidth() / 1.9)
            .attr("stroke", "#6D2077")
            .attr("stroke-width", 1)
        })
        .on("mouseout", function() {
          d3.select(this)
            .transition()
            .duration(600)
            .attr("stroke", "transparent")
            .attr("stroke-width", 0)
            .style("fill", "#00338D")
          d3.selectAll(".textCircle")
            .transition().duration(600)
            .attr("r", yScale.bandwidth() / 2)
            .attr("stroke", "transparent")
        })

    })

  var newG = svg.append("g")

  newG.selectAll("circle")
    .data(data)
    .enter()
    .append("circle")
    .attr("class", "textCircle")
    .attr("cx", d => xScale(d.growth))
    .attr("cy", d => yScale(d.country) + yScale.bandwidth() / 2)
    .attr("r", 0)
    .transition()
    .duration(1200)
    .delay((d, i) => (i + 1) * 450)
    .attr("r", yScale.bandwidth() / 2)
    .attr("opacity", 1)
    .style("fill", "#0091DA")
    .attr("stroke", "transparent")
}

draw();

$(window).resize(function() {
  $("body").empty();
  draw();
});
html{ 
  height: 98%;
  margin: 0;
  padding: 0;
}

body{
  min-height: 98%;
  margin: 0;
  padding: 0;
}

svg{
  text-rendering: geometricPrecision;
  shape-rendering:geometricPrecision;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
	<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.13.0/d3.min.js"></script>

Upvotes: 4

Related Questions