angularhighchartsangular-highcharts

Cannot read property 'forExport' of undefined when leaving the page with Highcharts chart


I can reproduce the problem in a very simple Angular-Highcharts application. When I retrieve the data from the server, and then leave the page with Highcharts chart, I am getting this error:

Cannot read property 'forExport' of undefined at a.destroy (highcharts.js:394) at HighchartsChartComponent.ngOnDestroy (highcharts-angular.js:44)

Not surprisingly, HighchartsChartComponent.ngOnDestroy is

ngOnDestroy() {
    if (this.chart) { // #56
        this.chart.destroy();
        this.chart = null;
    }
}

Here is the minimal example:

export class WeatherForecastComponent implements OnInit {
  highcharts: typeof Highcharts = Highcharts;
  chartOptions: Highcharts.Options = { ... };

constructor(private http: HttpClient) { }

ngOnInit(): void {
    this.http.get<any[]>(`http://localhost:57432/WeatherForecast`, {
        headers: new HttpHeaders({
            "Content-Type": "application/json"
    })})
    .subscribe(result => {
        let categories: string[] = [];
        let dataSeries: number[] = [];
        for (let i = 0; i < result.length; i++) {
            categories.push(result[i]['location']);
            dataSeries.push(result[i]['temperatureC']);
        }
        this.chartOptions.xAxis = {
            categories: categories
        };
        this.chartOptions.series = [{
          type: "column",
          name: "Cities",
          data: dataSeries
        }];
     this.highcharts.chart('container', this.chartOptions);
     }, error => {
         console.log(error);
     });
  }
}

I saw a number of similar issues both on Stack Overflow and on Github. I understand that the root cause is that it's trying delete the chart that is already deleted. But all of them are deleting (or attempting to delete) the charts in application code.

I tried to delete the chart in ngOnDestroy(), or to unsubscribe - but no difference.

Here is the repo, including Server-side code.

Note, that the problem doesn't happen when it is a "hardcoded" chart.

Angular: 12.2; Highchars 9.2.2; Highcharts-Angular: 2.10.0


Solution

  • There are two main things that don't work. But before I could explain let's look at the code responsible for creating the chart in the wrapper.

    ngOnChanges(changes) {
            const update = changes.update && changes.update.currentValue;
            if (changes.options || update) {
                this.wrappedUpdateOrCreateChart();
                if (update) {
                    this.updateChange.emit(false); // clear the flag after update
                }
            }
        }
        wrappedUpdateOrCreateChart() {
            if (this.runOutsideAngular) {
                this._zone.runOutsideAngular(() => {
                    this.updateOrCreateChart();
                });
            }
            else {
                this.updateOrCreateChart();
            }
        }
        updateOrCreateChart() {
            if (this.chart && this.chart.update) {
                this.chart.update(this.options, true, this.oneToOne || false);
            }
            else {
                this.chart = this.Highcharts[this.constructorType || 'chart'](this.el.nativeElement, this.options, this.callbackFunction || null);
                // emit chart instance on init
                this.chartInstance.emit(this.chart);
            }
        }
    

    As you can see, everything is happening in the ngOnChanges hook. If the change is detected the series of methods are fired to finally update the chart.

    And now the things which don't work:

    1. In the case when there is any asynchronous code happening in the subscribe (like for example HTTP request - on my example mocked by setTimeout) you have to manually set to wrapper that is should be updated by setting the updateFlag. Because Angular doesn't detect changes in some properties in the chartOptions.

    2. If you split assigning properties to the chartOptions like in the code snippet below set the oneToOne flag to true

         this.chartOptions.xAxis = {
                categories: categories
              };
              this.chartOptions.series = [{
                type: "column",
                name: "Cities",
                data: dataSeries
              }];
    

    Docs: https://github.com/highcharts/highcharts-angular#options-details
    Demo: https://stackblitz.com/edit/highcharts-angular-basic-line-vwrrmx