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>

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

  1. 799156 953935Hi there, just became aware of your blog through Google, and located that it is truly informative. Im gonna watch out for brussels. I will appreciate if you continue this in future. Lots of folks will probably be benefited from your writing. Cheers! 461172

    Reply
  2. 144302 956810amazing post, extremely informative. I wonder why the other experts of this sector do not notice this. You must continue your writing. Im confident, youve a great readers base already! 821618

    Reply

Leave a Comment