D3 General update pattern transition not working on pie chart

I have a project where I am using a Pie/Doughnut chart to visualize my data. I have added the general update pattern in order to create a smooth transition when my data changes/updates.

In order to accomplish this I have followed an example which uses the general update pattern on a pie chart: Bl.ocks example.

The problem I am facing is that the chart doesn’t update smoothly when updating the data. The chart instantly swaps from one state into the next.

In this and other examples they define a arcTween method where d3 interpolates between the previous angles and the angles from the newly updated data:

    arcTween(a) {
      let i = d3.interpolate(this._current, a);
      this._current = i(0);
      return t => {
        return this.arc(i(t));
      };
    }

I also have added the code where I join, enter and update my data to the pie chart. Here I first create the group element where the pie is being drawn in. I also define a transition using the ‘arcTween’ method to transition between the states. And lastly I also define the ‘this._current’ when the pie is created:

      this.g = this.svg
        .selectAll("doughnut")
        .data(data_ready)
        .enter()
        .append("g");

      this.g
        .transition()
        .duration(1500)
        .attrTween("d", this.arcTween);

      this.g
        .append("path")
        .attr("d", d => {
          return this.arc(d);
        })
        .attr("fill", "#206BF3")
        .attr("class", "slice")
        .attr("stroke", "#2D3546")
        .style("stroke-width", "2px")
        .each(d => {
          this._current = d;
        });

This is my full code. This is being written in Vue.js. I have tried to get this to work inside a snippet. But I couldn’t get it to work.

The images that are shown on top of the doughnut slices are locally stored:

<template>
  <div class="p-5 flex flex-col h-full">
    <h2 class="mb-3">{{ title }}</h2>
    <div ref="my_dataviz" class="flex justify-center"></div>
    <div class="grid grid-cols-2 gap-7 m-7">
      <div v-for="item in data" :key="item.key" class="flex">
        <img
          :src="require('@/assets/img/doughnut/' + item.icon)"
          alt=""
          class="doughnutIcon mr-4"
        />
        <div class="flex flex-col">
          <h3>{{ item.key }}</h3>
          <p class="opacity-50">
            {{ formatNumberValue(item.value) }} {{ unit }}
          </p>
          <p class="opacity-50">{{ percentageOfTotal(item.value) }} %</p>
        </div>
      </div>
    </div>
  </div>
</template>
<script>
import { converter } from "@/shared";
import * as d3 from "d3";

export default {
  name: "DoughnutChartItem",
  props: {
    title: {
      type: String,
      required: true
    },
    data: {
      type: Array,
      required: true
    },
    height: {
      type: Number,
      required: true
    },
    width: {
      type: Number,
      required: true
    },
    unit: {
      type: String
    }
  },
  data() {
    return {
      totalAmount: 0,
      svg: undefined,
      arc: undefined,
      radius: undefined,
      g: undefined
    };
  },
  created() {
    let total = 0;
    this.data.forEach(item => {
      total += item.value;
    });
    this.totalAmount = total;
  },
  mounted() {
    // set the dimensions and margins of the graph
    var margin = 1;

    // The radius of the pieplot is half the width or half the height (smallest one). I subtract a bit of margin.
    this.radius = Math.min(this.width, this.height) / 2 - margin;

    // append the svg object to the div called 'my_dataviz'
    this.svg = d3
      .select(this.$refs.my_dataviz)
      .append("svg")
      .attr("width", this.width)
      .attr("height", this.height)
      .append("g")
      .attr(
        "transform",
        "translate(" + this.width / 2 + "," + this.height / 2 + ")"
      );

    // Compute the position of each group on the pie:
    this.pie = d3.pie().value(function(d) {
      return d[1];
    });

    // declare an arc generator function
    this.arc = d3
      .arc()
      .outerRadius(100)
      .innerRadius(50);

    this.setSlicesOnDoughnut(this.data);

    this.addImagesToSlices();
  },
  methods: {
    animateSliceOnHover(radius, path, dir) {
      switch (dir) {
        case 0:
          path
            .transition()
            .duration(500)
            .ease(d3.easeBounce)
            .attr(
              "d",
              d3
                .arc()
                .innerRadius(100)
                .outerRadius(50)
            );
          path.style("fill", "#206BF3");
          break;

        case 1:
          path.transition().attr(
            "d",
            d3
              .arc()
              .innerRadius(50)
              .outerRadius(110)
          );
          path.style("fill", "white");
          break;
      }
    },
    percentageOfTotal(amount) {
      return Math.round((amount / this.totalAmount) * 100);
    },
    formatNumberValue(amount) {
      return converter.formatNumberValue(amount);
    },
    setSlicesOnDoughnut(data) {
      // Build the pie chart: Basically, each part of the pie is a path that we build using the arc function.
      var data_ready = this.pie(
        data.map(function(d) {
          return [d["key"], d["value"], d["icon"], d["hover"]];
        })
      );

      this.g = this.svg
        .selectAll("doughnut")
        .data(data_ready)
        .enter()
        .append("g");

      this.g
        .transition()
        .duration(1500)
        .attrTween("d", this.arcTween);

      this.g
        .append("path")
        .attr("d", d => {
          return this.arc(d);
        })
        .attr("fill", "#206BF3")
        .attr("class", "slice")
        .attr("stroke", "#2D3546")
        .style("stroke-width", "2px")
        .each(d => {
          this._current = d;
        });

      // Add tooltip
      d3.selectAll(".slice")
        .on("mouseover", this.mouseover)
        .on("mousemove", this.mousemove)
        .on("mouseout", this.mouseout);
    },
    addImagesToSlices() {
      var image_width = 20;
      var image_height = 20;

      this.g.selectAll(".logo").remove();

      this.g
        .append("svg:image")
        .attr("transform", d => {
          var x = this.arc.centroid(d)[0] - image_width / 2;
          var y = this.arc.centroid(d)[1] - image_height / 2;
          return "translate(" + x + "," + y + ")";
        })
        .attr("class", "logo")
        .attr("class", function(d) {
          return `${d.data[0]}-logo`;
        })
        .attr("href", function(d) {
          return require("@/assets/img/doughnut/" + d.data[2]);
        })
        .attr("width", image_width)
        .attr("height", image_height);
    },
    mouseover(event, data) {
      //Swap doughnut icon to blue icon
      d3.selectAll("." + data.data[0] + "-logo").attr("href", d => {
        return require("@/assets/img/doughnut/" + d.data[3]);
      });

      this.animateSliceOnHover(this.radius, d3.select(event.currentTarget), 1);

      const tip = d3.select(".tooltip");

      tip
        .style("left", `${event.clientX + 15}px`)
        .style("top", `${event.clientY}px`)
        .transition()
        .style("opacity", 0.98);

      tip.select("h3").html(`${data.data[0]}`);
      tip
        .select("h4")
        .html(`${this.formatNumberValue(data.data[1])} ${this.unit}`);
    },
    mousemove(event) {
      // Move tooltip
      d3.select(".tooltip")
        .style("left", `${event.clientX + 15}px`)
        .style("top", `${event.clientY}px`);
    },
    mouseout(event, data) {
      //Swap doughnut icon to white icon
      d3.selectAll("." + data.data[0] + "-logo").attr("href", function(d) {
        return require("@/assets/img/doughnut/" + d.data[2]);
      });

      // Animate slice
      var thisPath = d3.select(event.currentTarget);
      this.animateSliceOnHover(this.radius, thisPath, 0);

      // if (!thisPath.classed("clicked")) {
      //   this.animateSliceOnHover(this.radius, thisPath, 0);
      // }

      // Hide tooltip
      d3.select(".tooltip")
        .transition()
        .style("opacity", 0);
    },
    arcTween(a) {
      let i = d3.interpolate(this._current, a);
      this._current = i(0);
      return t => {
        return this.arc(i(t));
      };
    }
  },
  watch: {
    data() {
      this.setSlicesOnDoughnut(this.data);

      this.addImagesToSlices();
    }
  }
};
</script>

15 thoughts on “D3 General update pattern transition not working on pie chart”

Leave a Comment