Search code examples
javascriptd3.jsecmascript-6pie-chartes6-class

Drawing Pie chart with ES6


I try to create my own chart library build upon D3.js and ES6 with animations and interactivity.

My problem is that drawing Pie chart requires some tween functions to animate pie chart nicely. I try to write those tween functions with ES6.

My chart structure looks something like this:

class PieChart {
    constructor({w, h} = {}) {
        this.w = w;
        this.h = h;

        ...

        this.onInit();
    }

    onInit() {
        this.radius = Math.min(this.w, this.h) / 2;

        this.arc = d3.arc()
            .innerRadius(this.radius - 20)
            .outerRadius(this.radius);

        this.pie = d3.pie();

        ...

        this.svg = d3.select("#id")
                .append("svg")
                .attr("width", this.w)
                .attr("height", this.h)

        this.drawChart();
    }

    drawChart() {
        this.arcs = this.svg.append("g")
            .attr("transform", `translate(${this.w / 2}, ${this.h / 2})`)
            .attr("class", "slices")
                .selectAll(".arc")
                .data(this.dataset)
                .enter()
                .append("path")
                    .attr("d", this.arc)
                    .each(function(d) { this._current = d; });

        ...

        const curryAttrTween = function() {
            let outerArc = this.arc;
            let radius = this.radius;

            return function(d) {                // <- PROBLEM: This inner function is never called
                this._current = this._current || d;
                let interpolate = d3.interpolate(this._current, d);
                this._current = interpolate(0);
                return function(t) {
                    let d2 = interpolate(t);
                    let pos = outerArc.centroid(d2);
                    pos[0] = radius * (midAngle(d2) < Math.PI ? 1 : -1);
                    return `translate(${pos})`;
                }
            }
        };

        let labels = this.svg.select(".label-name").selectAll("text")
            .data(this.pie(this.dataset), "key");

        labels
            .enter()
            .append("text")
                .attr("dy", ".35em")
                .attr("class", "text")
                .text((d) => `${d.data.column}: ${d.data.data.count}`);

        labels
            .transition()
            .duration(666)
            .attrTween("d", curryAttrTween.bind(this)());

        labels
            .exit()
            .remove();    
    }
}

I also tried:

drawChart() {
    ...

    const attrTween = function(d) {
        this._current = this._current || d;            // <- PROBLEM: Can't access scope 'this'
        let interpolate = d3.interpolate(this._current, d);
        this._current = interpolate(0);
        return function(t) {
            let d2 = interpolate(t);
            let pos = this.arc.centroid(d2);
            pos[0] = this.radius * (midAngle(d2) < Math.PI ? 1 : -1);
            return `translate(${pos})`;
        }
    }

    labels
        .transition()
        .duration(666)
        .attrTween("d", (d) => attrTween(d));

    ...
}

And my finally try:

drawChart() {
    ...

    labels
        .transition()
        .duration(666)
        .attrTween("d", function(d) {
            this._current = this._current || d;
            let interpolate = d3.interpolate(this._current, d);
            this._current = interpolate(0);
            return function(t) {
                let d2 = interpolate(t);
                let pos = this.arc.centroid(d2);                                // <- PROBLEM: Can't access this.arc
                pos[0] = this.radius * (midAngle(d2) < Math.PI ? 1 : -1);       // <- PROBLEM: Can't access this.radius
                return `translate(${pos})`;
            }
        });

    ...
}

All the above methods failed at some point. I pointed to the problems in my code, and I am not sure if and how this can be done in ES6.


Solution

  • Since I can't comment on the accepted answer ( not enough reputation :( ) I just want to point out that the newly created inner function (interpolator) won't have access to interpolate declared inside the attrTween:

    drawChart() {
      //...
    
      // Use arrow function to lexically capture this scope.
      const interpolator = t => {
        let d2 = interpolate(t);                                     // <-- Reference Error
        let pos = this.arc.centroid(d2);
        pos[0] = this.radius * (midAngle(d2) < Math.PI ? 1 : -1);
        return `translate(${pos})`;
      };
    
      labels
        .transition()
        .duration(666)
        .attrTween("d", function(d) {
          this._current = this._current || d;
          let interpolate = d3.interpolate(this._current, d);       // <-- Declared here
          this._current = interpolate(0);
          return interpolator;
        });
    
      //...
    }
    

    ps. I find the:

    const self = this;
    

    to be a good solution in this case, as its easier to read and reason about, even if its not exactly the best ES6 way.