Reputation: 7253
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]'.
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
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