twgardner2
twgardner2

Reputation: 660

d3.js: How to fix my interactive piechart?

I'm trying to teach myself how to make interactive visualizations in d3.js, currently working through Elijah Meeks' D3.js In Action. I'm trying to make his pie chart example interactive using three buttons. I'm doing something wrong with my tweening - I'm trying to save the currently displayed pie so that the transition goes between it and the newly chosen pie. However, my current pie keeps resetting to the initial pie. I think it's probably something simple, but I just can't figure out what I'm doing wrong.

Can someone tell me what to change to make my transitions work? To demonstrate the problem:

  1. Run the code below,
  2. Click on the 'Stat 2' button,
  3. Click on the 'Stat 2' button again - you will see the pie resets to 'Stat 1', then smoothly transition to 'Stat 2'.

.as-console-wrapper { max-height: 20% !important;}
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <script src="https://d3js.org/d3.v4.min.js"></script>
  </head>

  <body>
    <div id="viz">
      <button id="0"> Stat 1 </button>
      <button id="1"> Stat 2 </button>
      <button id="2"> Stat 3 </button>
      <br>
      <svg style="width:400px;height:300px;border:1px lightgray solid;" />
    </div>
  </body>

  <script>
    var obj = [{
      name: "a",
      stat1: 10,
      stat2: 20,
      stat3: 30,
    }, {
      name: "b",
      stat1: 30,
      stat2: 20,
      stat3: 10,
    }, {
      name: "c",
      stat1: 15,
      stat2: 25,
      stat3: 50,
    }];

    function piechart(data) {

      var currentPie = 0; //Initialize to stat1

      var fillScale = d3.scaleOrdinal(d3.schemeCategory10);
      var pieChart = d3.pie().sort(null);
      var newArc = d3.arc().innerRadius(50).outerRadius(100);

      // Create each pie chart 
      pieChart.value(d => d.stat1);
      var stat1Pie = pieChart(data);

      pieChart.value(d => d.stat2);
      var stat2Pie = pieChart(data);

      pieChart.value(d => d.stat3);
      var stat3Pie = pieChart(data);

      // Embed slices on each name
      data.forEach((d, i) => {
        var slices = [stat1Pie[i], stat2Pie[i], stat3Pie[i]];
        d.slices = slices;
      });

      d3.select("svg")
        .append("g")
        .attr("transform", "translate(200, 150)")
        .selectAll("path")
        .data(data)
        .enter()
        .append("path")
        .attr("d", d => newArc(d.slices[currentPie]))
        .attr("fill", (d, i) => fillScale(i))
        .attr("stroke", "black")
        .attr("stroke-width", "2px");

      function transPie(d) {

        var newPie = this.id;
        console.log("Transition from pie " + currentPie + " to pie " + newPie);

        d3.selectAll("path")
          .transition()
          .delay(500)
          .duration(1500)
          .attrTween("d", tweenPies)

        function tweenPies(d, i) {
          console.log(i + ":start tween function \n current pie = " + currentPie + "\n new pie = " + newPie);
          var currentAngleStart = d.slices[currentPie].startAngle;
          var newAngleStart = d.slices[newPie].startAngle;

          var currentAngleEnd = d.slices[currentPie].endAngle;
          var newAngleEnd = d.slices[newPie].endAngle;

          return t => {
            var interpolateStartAngle = d3.interpolate(currentAngleStart, newAngleStart);
            var interpolateEndAngle = d3.interpolate(currentAngleEnd, newAngleEnd);
            d.startAngle = interpolateStartAngle(t);
            d.endAngle = interpolateEndAngle(t);
            return newArc(d);
          };
        };
      };

      d3.selectAll("button").on("click", transPie);
    };
    piechart(obj);

  </script>

</html>

Upvotes: 2

Views: 317

Answers (1)

Mark
Mark

Reputation: 108512

You never set the state of currentPie to the new state after a selection. I've added a .on('end', handler to the transition to set this state:

.on('end', function(){
  currentPie = newPie;
});

Running code:

<html>

  <head>
    <script src="https://d3js.org/d3.v4.min.js"></script>
  </head>

  <body>
    <div id="viz">
      <button id="0"> Stat 1 </button>
      <button id="1"> Stat 2 </button>
      <button id="2"> Stat 3 </button>
      <br />
      <svg style="width:1000px;height:500px;border:1px lightgray solid;"></svg>
    </div>
    <script>
    var obj = [{name: "a",stat1: 10,stat2: 20,stat3: 30,},
               {name: "b",stat1: 30,stat2: 20,stat3: 10,},
               {name: "c",stat1: 15,stat2: 25,stat3: 50,}];

    function piechart(data){

        var currentPie = 0; //Initialize to stat1

        var fillScale = d3.scaleOrdinal(d3.schemeCategory10);
        var pieChart = d3.pie().sort(null);
        var newArc = d3.arc().innerRadius(50).outerRadius(100);              

        // Create each pie chart 
        pieChart.value(d => d.stat1);
        var stat1Pie = pieChart(data);

        pieChart.value(d => d.stat2);
        var stat2Pie = pieChart(data);

        pieChart.value(d => d.stat3);
        var stat3Pie = pieChart(data);

        // Embed slices on each name
        data.forEach( (d,i) => {
            var slices = [stat1Pie[i], stat2Pie[i], stat3Pie[i]];
            d.slices = slices;
        });

        d3.select("svg")
            .append("g")
            .attr("transform", "translate(250, 250)")
            .selectAll("path")
            .data(data)
            .enter()
            .append("path")
            .attr("d", d => newArc(d.slices[currentPie]))
            .attr("fill", (d,i) => fillScale(i))
            .attr("stroke", "black")
            .attr("stroke-width", "2px");

        function transPie(d) {

            var newPie = +this.id;
            console.log("Transition from pie " +currentPie+ " to pie " + newPie);

            d3.selectAll("path")
                .transition()
                .delay(500)
                .duration(1500)
                .attrTween("d", tweenPies)
                .on('end', function(){
                  currentPie = newPie;
                })

            function tweenPies(d, i) {
                console.log(i + ":start tween function \n current pie = " + currentPie + "\n new pie = "+newPie); 
                var currentAngleStart   = d.slices[currentPie].startAngle;
                var newAngleStart       = d.slices[newPie].startAngle;

                var currentAngleEnd     = d.slices[currentPie].endAngle;
                var newAngleEnd         = d.slices[newPie].endAngle;

                return t => {
                    var interpolateStartAngle = d3.interpolate(currentAngleStart, newAngleStart);
                    var interpolateEndAngle = d3.interpolate(currentAngleEnd, newAngleEnd);
                    d.startAngle = interpolateStartAngle(t);
                    d.endAngle = interpolateEndAngle(t);
                    return newArc(d);
                };
            };
        };

        d3.selectAll("button").on("click", transPie);  
    };
    piechart(obj);
    </script>
  </body>

</html>

Upvotes: 1

Related Questions