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

Angular 18独立组件中ECharts控制台警告「已有图表实例初始化」的问题求助

Angular 18独立组件中ECharts控制台警告「已有图表实例初始化」的问题求助

大家好,我在开发Angular v18的Web应用时遇到了个棘手的问题,想请各位帮忙排查下:

我的应用里有多个仪表盘类的独立组件,里面用到了Apache ECharts的堆叠折线图、柱状图、仪表盘等多种图表。但每次在页面间导航的时候,控制台都会弹出警告:

[ECharts] There is a chart instance already initialized on the dom.

我尝试过在组件里用dispose方法来清理旧的图表实例,但当从其他页面返回原组件页面时,这个警告还是会出现,感觉之前的实例好像没被彻底清理干净。

下面是我的折线图组件代码,麻烦帮忙看看哪里处理得不对:

import { Component, ElementRef, Input, OnChanges, SimpleChanges, AfterViewInit, OnInit, Output, EventEmitter, OnDestroy } from '@angular/core';
import { EChartsOption } from 'echarts';
import { NgxEchartsModule } from 'ngx-echarts';
import * as echarts from 'echarts';
import { NgxEchartsDirective, provideEcharts } from 'ngx-echarts';
import { ColorService } from '../../services/color.service';
import { CommonModule } from '@angular/common';
import { Series } from '../../services/models/lineChartTimeSeriesDTO';

@Component({
  selector: 'app-line-chart',
  standalone: true,
  imports: [
    NgxEchartsDirective,
    NgxEchartsModule,
    CommonModule
  ],
  providers: [provideEcharts()],
  templateUrl: './line-chart.component.html',
  styleUrl: './line-chart.component.css'
})
export class LineChartComponent implements OnChanges, AfterViewInit, OnInit, OnDestroy {
  @Output() chartReady = new EventEmitter<echarts.ECharts>();
  @Input() data: Series[] = [{
    name: 'default',
    type: 'line',
    data: [],
    smooth: false,
    showSymbol: false,
    emphasis: { disabled: true }
  }];
  @Input() labels: string[] = [];
  @Input() title: string = "";
  @Input() unit: string = "";
  @Input() seriesName: string = "";
  @Input() legendData: string[] = [];
  @Input() chartHeight: number = 400;
  @Input() headerTitle: string = "";
  @Input() multiplicativeFactor: number = 1;
  @Input() rotateLabelXAxis: number = 0;
  @Input() showSeconds: boolean = false;

  chartOptions: EChartsOption = {};
  chartInstance: any = null;
  private resizeListener: () => void = () => {};

  constructor(private el: ElementRef, private colorService: ColorService) {
    this.chartOptions = this.getChartOptions();
  }

  ngOnInit(): void { }

  ngAfterViewInit() {
    const container = this.el.nativeElement.querySelector('.chart-container-line');
    if (!container) {
      return;
    }

    // 先清理可能存在的旧实例
    this.cleanupOldInstance(container);

    container.style.height = `${this.chartHeight}px`;
    container.style.width = '100%';

    requestAnimationFrame(() => {
      this.initChart(container);
      this.chartReady.emit(this.chartInstance);
    });
  }

  private initChart(container: HTMLElement) {
    // 再次检查并清理实例
    this.cleanupOldInstance(container);

    this.chartInstance = echarts.init(container);
    this.chartInstance.setOption(this.chartOptions);
    this.chartInstance.on('brushEnd', (params: any) => this.onChartInit(params));

    // 把resize事件的监听函数存起来,方便销毁时移除
    this.resizeListener = () => {
      if (this.chartInstance) {
        this.chartInstance.resize();
      }
    };
    window.addEventListener('resize', this.resizeListener);
  }

  private cleanupOldInstance(container: HTMLElement) {
    const existingInstance = echarts.getInstanceByDom(container);
    if (existingInstance) {
      existingInstance.dispose();
      container.removeAttribute('_echarts_instance_');
    }
  }

  ngOnChanges(changes: SimpleChanges): void {
    /* refresh chart on data change */
    if (changes['title'] || changes['unit'] || changes['data']) {
      this.updateChart();
    }
  }

  onChartInit(params: any) {
    /* brush events */
    if (params.areas && params.areas.length > 0) {
      const selectedArea = params.areas[0];
      const coordRange = selectedArea.coordRange;
      if (coordRange) {
        const [start, end] = coordRange;
        /* Update x interval */
        this.updateXAxisRange(start, end);
        /* Remove selection */
        this.chartInstance.dispatchAction({
          type: 'brush',
          areas: []
        });
      }
    }
  }

  private updateXAxisRange(start: number, end: number) {
    /* edit the visible range on x axis */
    this.chartInstance.setOption({
      xAxis: {
        min: start,
        max: end
      }
    });
  }

  private getChartOptions(): EChartsOption {
    const maxValue = this.findFullScale(this.data);
    return {
      animation: false,
      title: {
        text: this.title,
        left: "center",
        textStyle: {
          color: this.colorService.getColorFromCSSVariable("--whiteStandard")
        },
      },
      legend: {
        data: this.legendData,
        type: 'scroll',
        textStyle: {
          color: this.colorService.getColorFromCSSVariable("--whiteStandard")
        },
        pageIconColor: this.colorService.getColorFromCSSVariable("--whiteStandard"),
        pageIconInactiveColor: this.colorService.getColorFromCSSVariable("--lightGrey"),
        pageTextStyle: {
          color: this.colorService.getColorFromCSSVariable("--whiteStandard")
        }
      },
      tooltip: {
        show: true,
        trigger: 'axis',
        formatter: (params: any) => {
          const timestamp = this.formatDate(params[0].axisValueLabel, this.showSeconds)
          let dataValues = params.map((item: any) => 
            `${item.seriesName}: ${(item.value * this.multiplicativeFactor).toFixed(2)} ${this.unit}`
          ).join('<br/>');
          return `${timestamp}<br/>${dataValues}`;
        }
      },
      xAxis: {
        type: 'category',
        data: this.labels,
        axisLabel: {
          rotate: this.rotateLabelXAxis,
          color: this.colorService.getColorFromCSSVariable("--whiteStandard")
        },
        axisLine: {
          lineStyle: {
            color: this.colorService.getColorFromCSSVariable("--lightGrey")
          }
        }
      },
      yAxis: {
        type: 'value',
        axisLabel: {
          color: this.colorService.getColorFromCSSVariable("--whiteStandard"),
          formatter: (value: number) => `${(value * this.multiplicativeFactor).toFixed(2)} ${this.unit}`
        },
        axisLine: {
          lineStyle: {
            color: this.colorService.getColorFromCSSVariable("--lightGrey")
          }
        },
        splitLine: {
          lineStyle: {
            color: this.colorService.getColorFromCSSVariable("--darkGrey")
          }
        },
        max: maxValue
      },
      series: this.data.map(series => ({
        ...series,
        lineStyle: {
          width: 2
        },
        areaStyle: {}
      }))
    };
  }

  private findFullScale(data: Series[]): number {
    return data.length ? Math.max(...data.flatMap(s => s.data as number[])) * 1.1 : 100;
  }

  private updateChart() {
    this.chartOptions = this.getChartOptions();
    if (this.chartInstance) {
      this.chartInstance.setOption(this.chartOptions);
    }
  }

  private formatDate(timestamp: string, showSeconds: boolean): string {
    const date = new Date(timestamp);
    return showSeconds 
      ? date.toLocaleString() 
      : `${date.toLocaleDateString()} ${date.toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'})}`;
  }

  ngOnDestroy(): void {
    // 组件销毁时必须清理实例和事件监听
    if (this.chartInstance) {
      this.chartInstance.dispose();
      this.chartInstance = null;
    }
    window.removeEventListener('resize', this.resizeListener);
  }
}

我自己调整了一些逻辑,比如把实例抽成单独的清理函数,还加了OnDestroy钩子来在组件销毁时彻底清理实例和resize事件监听,但还是没解决问题。现在的疑问是:是不是清理实例的时机不对?或者ngAfterViewInit里的处理有漏洞?有没有更可靠的方式在组件销毁时彻底清理ECharts实例,避免导航时出现重复实例的警告?

麻烦各位帮忙看看,谢谢啦!

内容来源于stack exchange

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.04.08 07:38:06