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>
问题原因
核心是异步数据加载时机不匹配:
- 父组件的
dataSource初始为空数组,API请求是异步操作,数据返回后才会更新dataSource。 - 子组件的
ngAfterViewInit在视图初始化完成后立即执行,此时API数据尚未返回,dataSource仍为空,导致AMCharts代码无法获取数据。 - 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
相关产品推荐
相关产品推荐

