TypeScript type confusion (TypeError: Cannot read property 'slice' of undefined)

Viewed 586

I have the following data in a csv file in my Angular project that also imports the D3.js library:

group,Nitrogen,normal,stress
banana,12,1,13
poacee,6,6,33
sorgho,11,28,12
triticum,19,6,1

I also have the following code in the typescript file to display the stacked bar:

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

@Component({
  selector: 'app-stacked-bar',
  templateUrl: './stacked-bar.component.html',
  styleUrls: ['./stacked-bar.component.css']
})
export class StackedBarComponent implements OnInit {

  data = [
    {"group": "banana", "Nitrogen": "12", "normal": "1", "stress": "13"},
    {"group": "poacee", "Nitrogen": "6", "normal": "6", "stress": "33"},
    {"group": "sorgho", "Nitrogen": "11", "normal": "28", "stress": "12"},
    {"group": "triticum", "Nitrogen": "19", "normal": "6", "stress": "1"}
  ];

  svg: any;

  margin = 50;
  width = 750 - (this.margin * 2);
  height = 400 - (this.margin * 2);

  ngOnInit(): void {

    this.createSvg();
    this.drawBars(this.data);
  }

  createSvg(): void {

    this.svg = d3.select("figure#stacked-bar")
    .append("svg")
    .attr("width", this.width + (this.margin * 2))
    .attr("height", this.height + (this.margin * 2))
    .append("g")
    .attr("transform", "translate(" + this.margin + "," + this.margin + ")");
  }

  drawBars(data): void {
 
    // List of subgroups - Header of the csv file.
    // ["Nitrogen", "normal", "stress"]
    const subgroups = data.columns.slice(1);
            
    // List of groups - Value of the first column, called group.
    const groups = data.map(d => (d.group));

    // Create the X-axis band scale.
    const x = d3.scaleBand()
    .domain(groups)
    .range([0, this.width])
    //.domain(data.map(d => d.groups))
    .padding(0.2);

    // Draw the X-axis on the DOM.
    this.svg.append("g")
    .attr("transform", "translate(0," + this.height + ")")
    .call(d3.axisBottom(x).tickSizeOuter(0));
    
    // Create the Y-axis band scale.
    const y = d3.scaleLinear()
    .domain([0, 60])
    .range([this.height, 0]);

    // Draw the Y-axis on the DOM.
    this.svg.append("g")
    .call(d3.axisLeft(y));

    // color palette = one color per subgroup
    const color = d3.scaleOrdinal()
    .domain(subgroups)
    .range(['#e41a1c','#377eb8','#4daf4a']);

    // Stack the data per subgroup.
    const stackedData = d3.stack()
    .keys(subgroups)
    (data);

    // Create and fill the bars.
    this.svg.append("g")
    .selectAll("g")
    .data(stackedData)
    .join("g")
    .attr("fill", d => color(d.key))
    .selectAll("rect")    
    .data(d => d)
    .join("rect")
    .attr("x", d => x(d.data.group))
    .attr("y", d => y(d[1]))
    .attr("height", d => y(d[0]) - y(d[1]))
    .attr("width", x.bandwidth());
  }
}

But I only get an empty plot. When I inspect the page, I have this console error:

TypeError: Cannot read property 'slice' of undefined

Due to some type ambiguity in data, the slice method cannot be applied, but when I just copy and paste ["Nitrogen", "normal", "stress"] in place of subgroups in code, it all works fine!

Can somebody please help me with this?

p.s. This is how the chart should look like:

enter image description here

4 Answers

Assuming you are working from this example, then note that this uses d3.csv to convert csv input to an array of objects. D3 creates this array plus a custom columns property that is used in the example.

const csv = `group,Nitrogen,normal,stress
banana,12,1,13
poacee,6,6,33
sorgho,11,28,12
triticum,19,6,1`;

const data = d3.csvParse(csv);
console.log(data.columns);
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.13.0/d3.min.js"></script>

In your code, you have the same array of objects, but you don't show the csv import method so we may assume you are not using d3.csv and using some other method to get the data.

In this case, the equivalent to data.columns is Object.keys(data[0]). From there you can use slice to get the columns by which you want to group for the stack:

const data = [
  {"group": "banana", "Nitrogen": "12", "normal": "1", "stress": "13"},
  {"group": "poacee", "Nitrogen": "6", "normal": "6", "stress": "33"},
  {"group": "sorgho", "Nitrogen": "11", "normal": "28", "stress": "12"},
  {"group": "triticum", "Nitrogen": "19", "normal": "6", "stress": "1"}
];

const dataColumns = Object.keys(data[0]);
console.log(dataColumns);

const subgroups = dataColumns.slice(1)
console.log(subgroups);
    

Can you please try below code?

 const subgroups = data.columns?.slice(1) || [];

From what I understand of your code there is no property columns in you data object.

You defined data like this at the beginning of the file :

data = [
    {"group": "banana", "Nitrogen": "12", "normal": "1", "stress": "13"},
    {"group": "poacee", "Nitrogen": "6", "normal": "6", "stress": "33"},
    {"group": "sorgho", "Nitrogen": "11", "normal": "28", "stress": "12"},
    {"group": "triticum", "Nitrogen": "19", "normal": "6", "stress": "1"}
  ];

So I don't see any property columns.

And data is already an array so I think you should just do :

const subgroups = data.slice(1)

And, by the way, you seems to do it correctly for the groups variable.

I don't have the possibility to test this myself right now, but what I learned from my coding in TypeScript a lot of similar problems were solved by setting the type of the variable in question to 'any' like this:

const subgroups = (data as any).columns.slice(1)

Please take this with a grain of salt since my excursion into web programming was short, so I can't say if this method could negatively impact the overall code in some way. Comments on this question are welcome.

Related