1

I've created an Angular service to update the DOM with d3.

As Angular uses TypeScript, I've packed the constructor with a bunch of items, including transition methods, like: arcPath and some tweens for animation.

I am new to Angular and it's been a while since I've worked with OO, so I'm getting lost in the woods with the context of this, specifically with regard to the .each() method, arcTweenUpdates, and ultimately the arcPath call in there. See "Here!" in code block below.

In brief, I'm using a non-arrow function in the each method, to be able to pass the _current value, and use this._current to get at it for tweening, but when I want to call this.arcPath, I'm "in the wrong" this context, and can't call it.

I know I'm doing something fundamentally wrong (i.e. dumb), but I can't remember how to deal with the double "this" scenario.

Any insight would be most appreciated.

import { Injectable } from "@angular/core";
import * as d3 from "d3";
import { legendColor } from "d3-svg-legend";
import { FirebaseService, ThreatId } from "../services/firebase.service";

interface Center {
  x: number;
  y: number;
}

@Injectable({
  providedIn: "root"
})
export class D3Service {
  private arcPath: any;
  private arcTweenEnter: any;
  private arcTweenExit: any;
  private arcTweenUpdate: any;
  private center: Center;
  private color = d3.scaleOrdinal(d3["schemeSet3"]);
  private dims = { height: 300, width: 300, radius: 150 };
  private graph: any;
  private legend: any;
  private legendGroup: any;
  private pie: any;
  private svg: any;
  private t = d3.transition().duration(2000);
  constructor(private firebaseService: FirebaseService) {
    this.firebaseService.getThreats().subscribe((threats: ThreatId[]) => {
      this.update(threats);
    });

    this.center = { x: this.dims.width / 2 + 5, y: this.dims.height / 2 + 5 };

    this.arcPath = d3
      .arc()
      .outerRadius(this.dims.radius)
      .innerRadius(this.dims.radius / 2);

    this.arcTweenEnter = (d: any) => {
      let i = d3.interpolate(d.endAngle, d.startAngle);

      return (t: any) => {
        d.startAngle = i(t);
        return this.arcPath(d);
      };
    };

    this.arcTweenExit = (d: any) => {
      let i = d3.interpolate(d.startAngle, d.endAngle);

      return (t: any) => {
        d.startAngle = i(t);
        return this.arcPath(d);
      };
    };

    this.arcTweenUpdate = function(d: any) {
      console.log(this.dims);
      let i = d3.interpolate(this._current, d);
      this._current = i(1);
      return (t: any) => {
        return this.arcPath(i(t)); <== Here!
      };
    };

    this.pie = d3
      .pie()
      .sort(null)
      .value((d: any) => d.value);
  }

  public renderPieGraph(): void {
    this.createSvg();
  }

  private update(data): void {
    this.color.domain(data.map((d: any) => d.name));
    this.legendGroup.call(this.legend);

    const paths = this.graph.selectAll("path").data(this.pie(data));

    paths
      .exit()
      .transition(this.t)
      .attrTween("d", this.arcTweenExit) // <== Here!
      .remove();

    paths.attr("d", this.arcPath)
    .transition(this.t)
    .attrTween("d", this.arcTweenUpdate);

    paths
      .enter()
      .append("path")
      .attr("class", "arc")
      .attr("stroke", "#fff")
      .attr("stroke-width", 3)
      .attr("fill", (d: any) => this.color(d.data.name))
      .each(function(d: any) { // <== Here!
        this._current = d;
      })
      .transition(this.t)
      .attrTween("d", this.arcTweenEnter);
  }

  private createSvg(): void {
    this.svg = d3
      .select(".canvas")
      .append("svg")
      .attr("width", this.dims.width + 300)
      .attr("height", this.dims.height + 50);

    this.graph = this.svg
      .append("g")
      .attr("transform", `translate(${this.center.x}, ${this.center.y})`);

    this.legendGroup = this.svg
      .append("g")
      .attr("transform", `translate(${this.dims.width + 40}, 10)`);

    this.legend = legendColor()
      .shape("circle")
      .shapePadding(10)
      .scale(this.color);
  }
}

SOLUTION: Based on the info that Andrew directed me toward, I found that refactoring the below function allowed me to get at the "_current" data item, not using 'this', but using n[i], while also being able to access this.arcPath().

this.arcTweenUpdate = (d: any, idx: any, n: any) => {
  console.log(n[idx]._current, d);
  let i = d3.interpolate(n[idx]._current, d);
  n[idx]._current = d;
  return (t: any) => {
    return this.arcPath(i(t));
  };
};
Kim
  • 856
  • 1
  • 11
  • 21
  • 1
    The second two parameters (described in the [docs here](https://github.com/d3/d3-selection#selection_each)) of selection.each() can replicate `this` in more vanilla environments: `selection.each(function(d,i,nodes) { d3.select(nodes[i]) ...` - the duplicates's answers go into greater depth as to details. – Andrew Reid Aug 08 '19 at 00:46
  • Thanks @AndrewReid for setting me on the right path. – Kim Aug 08 '19 at 01:31

0 Answers0