onTheInternet
onTheInternet

Reputation: 7253

D3 property X and Property Y doesn't exist on type [number, number] in Angular Project

I have a simple line graph that I've made in D3

https://jsfiddle.net/goanhefy/

I'm trying to integrate this chart into an Angular project and have the data managed by the app component. I've actually recreated it in Angular and posted it online

https://stackblitz.com/edit/angular-irjxus?file=app%2FShared%2Fline-graph%2Fline-graph.component.css

The problem is that I'm seeing an error in the angular CLI

ERROR in src/app/shared/line-graph/line-graph.component.ts(84,40): error TS2339: Property 'x' does not exist on type '[number, number]'. src/app/shared/line-graph/line-graph.component.ts(85,40): error TS2339: Property 'y' does not exist on type '[number, number]'.

enter image description here While my project still runs, I'd like to figure out what's causing this issue.

Here is my full line graph typescript

import { Component, OnInit, OnChanges, ViewChild, ElementRef, Input, ViewEncapsulation } from '@angular/core';
import * as d3 from 'd3';

@Component({
  selector: 'app-line-graph',
  templateUrl: './line-graph.component.html',
  styleUrls: ['./line-graph.component.css'],
  encapsulation: ViewEncapsulation.None
})
export class LineGraphComponent implements OnInit, OnChanges {
  @ViewChild('line') private lineContainer: ElementRef;
  @Input() private lineData: Array<any>;
  private margin: any = 25;
  private chart: any;
  private width = 500;
  private height = 500;
  private xScale: any;
  private yScale: any;
  private colors: any;
  private xAxis: any;
  private yAxis: any;
  private axisLength: any = this.width - 2 * this.margin;


  constructor() { }

  ngOnInit() {
    console.log(this.lineData);
    this.createChart();
    if (this.lineData) {
      this.updateChart();
    }
  }

  ngOnChanges() {
    if (this.chart) {
      this.updateChart();
    }
  }

  createChart() {
    /*
    const data: Array<any> = [
      {x: 0, y: 4},
      {x: 1, y: 9},
      {x: 2, y: 6},
      {x: 4, y: 5},
      {x: 6, y: 7},
      {x: 7, y: 3},
      {x: 9, y: 2}
  ];*/

    const element = this.lineContainer.nativeElement;
    const svg = d3.select(element).append('svg')
                .attr('width', this.width)
                .attr('height', this.height)
                .style('border', '1px solid');

    // Create Scales
    const xScale = d3.scaleLinear()
                   .domain([0, 10])
                   .range([0, this.axisLength]);

    const yScale = d3.scaleLinear()
                   .domain([0, 10])
                   .range([0, this.axisLength]);

    // Create axis
    this.xAxis = d3.axisBottom(xScale);
    this.yAxis = d3.axisLeft(yScale);

    svg.append('g')
       .classed('x-axis', true)
       .attr('transform', () => 'translate(' + this.margin + ',' + (this.height - this.margin) + ')')
       .call(this.xAxis);

    svg.append('g')
       .classed('y-axis', true)
       .attr('transform', () => 'translate(' + this.margin + ',' + this.margin + ')')
       .call(this.yAxis);


    const line = d3.line()
                   .x( (d) => xScale(d.x))
                   .y( (d) => yScale(d.y));

    svg.append('path')
       .attr('d', line(this.lineData))
       .attr('fill', 'none')
       .attr('stroke', 'red')
       .attr('transform', () => 'translate(' + this.margin + ',' + this.margin + ')');
  }


  updateChart() {

  }

}

And here is my app component

import { Component, OnInit } from '@angular/core';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit {
  private chartData: Array<any>;
  private lineData: Array<object>;

  ngOnInit() {

    this.lineData = [
      {x: 0, y: 4},
      {x: 1, y: 9},
      {x: 2, y: 6},
      {x: 4, y: 5},
      {x: 6, y: 7},
      {x: 7, y: 3},
      {x: 9, y: 2}
    ];
    console.log(this.lineData, 'Hello from app component');

    // give everything a chance to load before starting animation
    setTimeout(() => {
      this.generateData();

      // change data periodically
      setInterval(() => this.generateData(), 10000);
    }, 1000);
  }


  generateData() {
    this.chartData = [];
    for (let i = 0; i < (8 + Math.floor(Math.random() * 10)); i++) {
      this.chartData.push([
        `Index ${i}`,
        Math.floor(Math.random() * 100)
      ]);
    }
     //console.log(this.chartData);
  }
}

Upvotes: 3

Views: 3717

Answers (1)

peinearydevelopment
peinearydevelopment

Reputation: 11474

If you look at the typings file for d3 and the line function it provides, you will see that it has two overloads: export function line(): Line<[number, number]>; and export function line<Datum>(): Line<Datum>;.

The Line<Datum> interface in turn declares three overloads for the x method: x(): (d: Datum, index: number, data: Datum[]) => number;, x(x: number): this; and x(x: (d: Datum, index: number, data: Datum[]) => number): this;. While the naming makes this a little confusing, Datum is really just a generic type(often represented as T in other languages) and therefore the above two methods both return objects that implement this interface, one with a type that you declare for it and the other with a Tuple.

The types referenced are in the index.d.ts file in the @types/d3-shape directory.

When you are calling your method as follows: .x( (d) => xScale(d.x)). Typescript will look at that and knows that your d variable is supposed to be the Datum type that is supposed to be passed into the method x, but you haven't told it what that type is, so it will complain that it doesn't know about a property x on your object d. Since you haven't told it what type to expect, it assumes the overload for the method that does have a type(a.k.a. the [number, number] Tuple type).

There are two ways that you can get the transpilation time error message to go away:

Solution 1: The quick fix would be to update your method call to this: .x( (d: any) => xScale(d.x)). You are then telling Typescript that the d object you are passing is of type any and therefor it won't complain regardless of the property you try to access on it.

Solution 2: The more proper fix though would be to utilize Typescript to help you out. Create an interface for the type of objects that the graph will display. In your instance, it looks like you are passing objects with an x and a y property so:

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

Then update your method call to .x( (d: IGraphDatum) => xScale(d.x)).

The reason that the code continues to work is that your object actually has the x and y properties on it, so the transpiled JavaScript executes without error, but the transpiler is throwing the error as you haven't given it enough information to enforce that both of those properties will be on the passed object.

Upvotes: 7

Related Questions