You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Angular集成AMCharts时API传入dataSource在代码内为空问题

问题描述

在Angular组件中使用AMCharts图表时,父组件通过API获取的数据传入子组件后,子组件模板能正常渲染数据,但AMCharts代码块内访问该数据数组时为空。使用硬编码数据时AMCharts可正常工作,仅当数据来自API时出现此问题。


父组件(LogbookComponent)代码

import { Component, OnInit, ViewChild } from '@angular/core';
import { ApiService } from '../api.service';
import { NgForm } from '@angular/forms';
import { Logbook } from '../logbook';

@Component({
  selector: 'app-logbook',
  templateUrl: './logbook.component.html',
  styleUrls: ['./logbook.component.css'],
})
export class LogbookComponent implements OnInit {
  displayedColumns: string[] = [
    'date',
    'sitename',
    'noofflights',
    'tflighttime',
    'windspeed',
    'winddirec',
    'comments',
    'actions',
  ];
  dataSource: Logbook[] = [];
  tftime: number = 0;
  noofflights: number = 0;
  noofsites: number = 0;

  constructor(private apiService: ApiService) {}

  ngOnInit(): void {
    this.getflights();
  }

  getflights(): void {
    this.apiService.getFlights().subscribe((result) => {
      this.dataSource = result.sort(
        (a, b) => new Date(b.date).getTime() - new Date(a.date).getTime()
      );
      console.log(this.dataSource)
      this.noofsites = result.length;
      result.forEach((entry) => {
        this.tftime += entry.tflighttime;
        this.noofflights += entry.noofflights;
      });
    });
  }
}

父组件模板(传入子组件)

<app-charter [dataSource]="dataSource"></app-charter>

子组件(CharterComponent,含AMCharts代码)代码

import { Component, Input, Inject, NgZone, PLATFORM_ID } from '@angular/core';
import { isPlatformBrowser } from '@angular/common';

// amCharts imports
import * as am5 from '@amcharts/amcharts5';
import * as am5xy from '@amcharts/amcharts5/xy';
import am5themes_Animated from '@amcharts/amcharts5/themes/Animated';
import { Logbook } from '../logbook';

@Component({
  selector: 'app-charter',
  templateUrl: './charter.component.html',
  styleUrls: ['./charter.component.css'],
})
export class CharterComponent {
  private root!: am5.Root;
  @Input()
  dataSource!: Logbook[];

  constructor(
    @Inject(PLATFORM_ID) private platformId: Object,
    private zone: NgZone
  ) {}

  // Run the function only in the browser
  browserOnly(f: () => void) {
    if (isPlatformBrowser(this.platformId)) {
      this.zone.runOutsideAngular(() => {
        f();
      });
    }
  }

  ngAfterViewInit() {
    // Chart code goes in here
    this.browserOnly(() => {
      /* Chart code */
      // Create root element
      let root = am5.Root.new('chartdiv');

      // Set themes
      root.setThemes([am5themes_Animated.new(root)]);

      // Create chart
      let chart = root.container.children.push(
        am5xy.XYChart.new(root, {
          panX: true,
          panY: true,
          wheelX: 'panX',
          wheelY: 'zoomX',
          pinchZoomX: true,
        })
      );

      // Add cursor
      let cursor = chart.set('cursor', am5xy.XYCursor.new(root, {}));
      cursor.lineY.set('visible', false);

      // Create axes
      let xRenderer = am5xy.AxisRendererX.new(root, { minGridDistance: 30 });
      xRenderer.labels.template.setAll({
        rotation: -90,
        centerY: am5.p50,
        centerX: am5.p100,
        paddingRight: 15,
      });

      let xAxis = chart.xAxes.push(
        am5xy.CategoryAxis.new(root, {
          maxDeviation: 0.3,
          categoryField: 'country',
          renderer: xRenderer,
          tooltip: am5.Tooltip.new(root, {}),
        })
      );

      let yAxis = chart.yAxes.push(
        am5xy.ValueAxis.new(root, {
          maxDeviation: 0.3,
          renderer: am5xy.AxisRendererY.new(root, {}),
        })
      );

      // Create series
      let series = chart.series.push(
        am5xy.ColumnSeries.new(root, {
          name: 'Series 1',
          xAxis: xAxis,
          yAxis: yAxis,
          valueYField: 'value',
          sequencedInterpolation: true,
          categoryXField: 'country',
          tooltip: am5.Tooltip.new(root, {
            labelText: '{valueY}',
          }),
        })
      );

      series.columns.template.setAll({ cornerRadiusTL: 5, cornerRadiusTR: 5 });

      let populateddata: any[] = [];
      console.log(this.dataSource);
      this.dataSource.forEach((data) => {
        populateddata.push({
          country: data.date,
          value: data.tflighttime,
        });
      });

      // Set data
      let data = populateddata;

      xAxis.data.setAll(data);
      series.data.setAll(data);

      // Make stuff animate on load
      series.appear(1000);
      chart.appear(1000, 100);
    });
  }

  ngOnDestroy() {
    // Clean up chart when the component is removed
    this.browserOnly(() => {
      if (this.root) {
        this.root.dispose();
      }
    });
  }
}

子组件模板

<div id="chartdiv" style="width: 100%; height: 500px"></div>
<div class="">{{ dataSource }}</div>
<li *ngFor="let data of dataSource">
    {{data.date | date: 'dd/MM/yyyy' }}
    {{data.tflighttime}}
</li>

问题原因

核心是异步数据加载时机不匹配:

  1. 父组件的dataSource初始为空数组,API请求是异步操作,数据返回后才会更新dataSource。
  2. 子组件的ngAfterViewInit在视图初始化完成后立即执行,此时API数据尚未返回,dataSource仍为空,导致AMCharts代码无法获取数据。
  3. Angular模板会自动响应输入属性的变更(触发变更检测),所以能正常显示后续加载的数据;但AMCharts代码仅在ngAfterViewInit中执行一次,不会自动更新。

解决方案

方案1:监听输入属性变化(ngOnChanges)

在子组件中添加ngOnChanges钩子,当dataSource更新时重新初始化或更新图表:

import { SimpleChanges } from '@angular/core';

// ... 其他代码

ngOnChanges(changes: SimpleChanges) {
  if (changes['dataSource'] && changes['dataSource'].currentValue.length > 0) {
    // 销毁旧图表
    if (this.root) {
      this.root.dispose();
    }
    // 重新初始化图表
    this.initChart();
  }
}

// 提取图表初始化逻辑为独立方法
private initChart() {
  this.browserOnly(() => {
    let root = am5.Root.new('chartdiv');
    this.root = root; // 保存引用用于销毁

    root.setThemes([am5themes_Animated.new(root)]);

    let chart = root.container.children.push(
      am5xy.XYChart.new(root, {
        panX: true,
        panY: true,
        wheelX: 'panX',
        wheelY: 'zoomX',
        pinchZoomX: true,
      })
    );

    let cursor = chart.set('cursor', am5xy.XYCursor.new(root, {}));
    cursor.lineY.set('visible', false);

    let xRenderer = am5xy.AxisRendererX.new(root, { minGridDistance: 30 });
    xRenderer.labels.template.setAll({
      rotation: -90,
      centerY: am5.p50,
      centerX: am5.p100,
      paddingRight: 15,
    });

    let xAxis = chart.xAxes.push(
      am5xy.CategoryAxis.new(root, {
        maxDeviation: 0.3,
        categoryField: 'country',
        renderer: xRenderer,
        tooltip: am5.Tooltip.new(root, {}),
      })
    );

    let yAxis = chart.yAxes.push(
      am5xy.ValueAxis.new(root, {
        maxDeviation: 0.3,
        renderer: am5xy.AxisRendererY.new(root, {}),
      })
    );

    let series = chart.series.push(
      am5xy.ColumnSeries.new(root, {
        name: 'Series 1',
        xAxis: xAxis,
        yAxis: yAxis,
        valueYField: 'value',
        sequencedInterpolation: true,
        categoryXField: 'country',
        tooltip: am5.Tooltip.new(root, {
          labelText: '{valueY}',
        }),
      })
    );

    series.columns.template.setAll({ cornerRadiusTL: 5, cornerRadiusTR: 5 });

    let populateddata = this.dataSource.map(data => ({
      country: data.date,
      value: data.tflighttime
    }));

    xAxis.data.setAll(populateddata);
    series.data.setAll(populateddata);

    series.appear(1000);
    chart.appear(1000, 100);
  });
}

// 修改ngAfterViewInit,仅初始有数据时执行
ngAfterViewInit() {
  if (this.dataSource.length > 0) {
    this.initChart();
  }
}

方案2:父组件条件渲染子组件

在父组件模板中添加条件,确保只有dataSource有数据时才渲染子组件:

<app-charter [dataSource]="dataSource" *ngIf="dataSource.length > 0"></app-charter>

这样子组件初始化时dataSource已包含API返回的数据,无需额外处理。

方案3:使用RxJS监听输入属性(复杂场景)

通过setter将输入属性转换为可观察对象,监听数据变化并更新图表:

import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';

// ... 其他代码

private destroy$ = new Subject<void>();
@Input() set dataSource(value: Logbook[]) {
  if (value && value.length > 0) {
    this.updateChart(value);
  }
}

private updateChart(data: Logbook[]) {
  this.browserOnly(() => {
    if (!this.root) {
      // 初始化图表
      this.initChart();
    }
    // 转换数据并更新图表
    let populateddata = data.map(item => ({
      country: item.date,
      value: item.tflighttime
    }));
    this.root?.refs?.xAxis?.data.setAll(populateddata);
    this.root?.refs?.series?.data.setAll(populateddata);
  });
}

ngOnDestroy() {
  this.destroy$.next();
  this.destroy$.complete();
  if (this.root) {
    this.root.dispose();
  }
}

内容的提问来源于stack exchange,提问作者AMSort

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.14 11:01:14