File

projects/ngx-echarts/src/lib/ngx-echarts.directive.ts

Implements

OnChanges OnDestroy OnInit AfterViewInit

Metadata

Index

Properties
Methods
Inputs
Outputs

Constructor

constructor(config: NgxEchartsConfig, el: ElementRef, ngZone: NgZone)
Parameters :
Name Type Optional
config NgxEchartsConfig No
el ElementRef No
ngZone NgZone No

Inputs

autoResize
Type : boolean
Default value : true
initOpts
Type : literal type | null
Default value : null
loading
Type : boolean
Default value : false
loadingOpts
Type : object | null
Default value : null
loadingType
Type : string
Default value : 'default'
merge
Type : EChartsOption | null
Default value : null
options
Type : EChartsOption | null
Default value : null
theme
Type : string | ThemeOption | null
Default value : null

Outputs

chartAxisAreaSelected
chartBrush
chartBrushEnd
chartBrushSelected
chartClick
chartContextMenu
chartDataRangeSelected
chartDataViewChanged
chartDataZoom
chartDblClick
chartDownplay
chartFinished
chartGeoRoam
chartGeoSelectChanged
chartGeoSelected
chartGeoUnselected
chartGlobalCursorTaken
chartGlobalOut
chartGraphRoam
chartHighlight
chartInit
Type : EventEmitter
chartLegendLegendInverseSelect
chartLegendLegendSelectAll
chartLegendScroll
chartLegendSelectChanged
chartLegendSelected
chartLegendUnselected
chartMagicTypeChanged
chartMouseDown
chartMouseMove
chartMouseOut
chartMouseOver
chartMouseUp
chartRendered
chartRestore
chartSelectChanged
chartTimelineChanged
chartTimelinePlayChanged
chartTreeRoam
optionsError
Type : EventEmitter

Methods

Private createChart
createChart()
Returns : any
Private createLazyEvent
createLazyEvent(eventName: string)
Type parameters :
  • T
Parameters :
Name Type Optional
eventName string No
Returns : EventEmitter<T>
Private dispose
dispose()
Returns : void
Private Async initChart
initChart()
Returns : any
ngAfterViewInit
ngAfterViewInit()
Returns : void
ngOnChanges
ngOnChanges(changes: SimpleChanges)
Parameters :
Name Type Optional
changes SimpleChanges No
Returns : void
ngOnDestroy
ngOnDestroy()
Returns : void
ngOnInit
ngOnInit()
Returns : void
Private Async onOptionsChange
onOptionsChange(opt: any)
Parameters :
Name Type Optional
opt any No
Returns : any
Async refreshChart
refreshChart()

dispose old chart and create a new one.

Returns : any
resize
resize()

resize chart

Returns : void
Private setOption
setOption(option: any, opts?: any)
Parameters :
Name Type Optional
option any No
opts any Yes
Returns : void
Private toggleLoading
toggleLoading(loading: boolean)
Parameters :
Name Type Optional
loading boolean No
Returns : void

Properties

Public animationFrameID
Type : null
Default value : null
Private changeFilter
Default value : new ChangeFilterV2()
Private chart
Type : ECharts
Private chart$
Default value : new ReplaySubject<ECharts>(1)
Private echarts
Type : any
Private Optional initChartTimer
Type : number
Private loadingSub
Type : Subscription
Private resize$
Default value : new Subject<void>()
Private resizeOb
Type : ResizeObserver
Private resizeObFired
Type : boolean
Default value : false
Private resizeSub
Type : Subscription
import {
  AfterViewInit,
  Directive,
  ElementRef,
  EventEmitter,
  Inject,
  InjectionToken,
  Input,
  NgZone,
  OnChanges,
  OnDestroy,
  OnInit,
  Output,
  SimpleChanges,
} from '@angular/core';
import { Observable, ReplaySubject, Subject, Subscription, asyncScheduler } from 'rxjs';
import { switchMap, throttleTime } from 'rxjs/operators';
import { ChangeFilterV2 } from './change-filter-v2';
import type { EChartsOption, ECharts, ECElementEvent } from 'echarts';

export interface NgxEchartsConfig {
  echarts: any | (() => Promise<any>);
  theme?: string | ThemeOption;
}

export type ThemeOption = Record<string, any>;

export const NGX_ECHARTS_CONFIG = new InjectionToken<NgxEchartsConfig>('NGX_ECHARTS_CONFIG');

@Directive({
  standalone: true,
  selector: 'echarts, [echarts]',
  exportAs: 'echarts',
})
export class NgxEchartsDirective implements OnChanges, OnDestroy, OnInit, AfterViewInit {
  @Input() options: EChartsOption | null = null;
  @Input() theme: string | ThemeOption | null = null;
  @Input() initOpts: {
    devicePixelRatio?: number;
    renderer?: string;
    width?: number | string;
    height?: number | string;
    locale?: string;
  } | null = null;
  @Input() merge: EChartsOption | null = null;
  @Input() autoResize = true;
  @Input() loading = false;
  @Input() loadingType = 'default';
  @Input() loadingOpts: object | null = null;

  // ngx-echarts events
  @Output() chartInit = new EventEmitter<any>();
  @Output() optionsError = new EventEmitter<Error>();

  // echarts mouse events
  @Output() chartClick = this.createLazyEvent<ECElementEvent>('click');
  @Output() chartDblClick = this.createLazyEvent<ECElementEvent>('dblclick');
  @Output() chartMouseDown = this.createLazyEvent<ECElementEvent>('mousedown');
  @Output() chartMouseMove = this.createLazyEvent<ECElementEvent>('mousemove');
  @Output() chartMouseUp = this.createLazyEvent<ECElementEvent>('mouseup');
  @Output() chartMouseOver = this.createLazyEvent<ECElementEvent>('mouseover');
  @Output() chartMouseOut = this.createLazyEvent<ECElementEvent>('mouseout');
  @Output() chartGlobalOut = this.createLazyEvent<ECElementEvent>('globalout');
  @Output() chartContextMenu = this.createLazyEvent<ECElementEvent>('contextmenu');

  // echarts events
  @Output() chartHighlight = this.createLazyEvent<any>('highlight');
  @Output() chartDownplay = this.createLazyEvent<any>('downplay');
  @Output() chartSelectChanged = this.createLazyEvent<any>('selectchanged');
  @Output() chartLegendSelectChanged = this.createLazyEvent<any>('legendselectchanged');
  @Output() chartLegendSelected = this.createLazyEvent<any>('legendselected');
  @Output() chartLegendUnselected = this.createLazyEvent<any>('legendunselected');
  @Output() chartLegendLegendSelectAll = this.createLazyEvent<any>('legendselectall');
  @Output() chartLegendLegendInverseSelect = this.createLazyEvent<any>('legendinverseselect');
  @Output() chartLegendScroll = this.createLazyEvent<any>('legendscroll');
  @Output() chartDataZoom = this.createLazyEvent<any>('datazoom');
  @Output() chartDataRangeSelected = this.createLazyEvent<any>('datarangeselected');
  @Output() chartGraphRoam = this.createLazyEvent<any>('graphroam');
  @Output() chartGeoRoam = this.createLazyEvent<any>('georoam');
  @Output() chartTreeRoam = this.createLazyEvent<any>('treeroam');
  @Output() chartTimelineChanged = this.createLazyEvent<any>('timelinechanged');
  @Output() chartTimelinePlayChanged = this.createLazyEvent<any>('timelineplaychanged');
  @Output() chartRestore = this.createLazyEvent<any>('restore');
  @Output() chartDataViewChanged = this.createLazyEvent<any>('dataviewchanged');
  @Output() chartMagicTypeChanged = this.createLazyEvent<any>('magictypechanged');
  @Output() chartGeoSelectChanged = this.createLazyEvent<any>('geoselectchanged');
  @Output() chartGeoSelected = this.createLazyEvent<any>('geoselected');
  @Output() chartGeoUnselected = this.createLazyEvent<any>('geounselected');
  @Output() chartAxisAreaSelected = this.createLazyEvent<any>('axisareaselected');
  @Output() chartBrush = this.createLazyEvent<any>('brush');
  @Output() chartBrushEnd = this.createLazyEvent<any>('brushend');
  @Output() chartBrushSelected = this.createLazyEvent<any>('brushselected');
  @Output() chartGlobalCursorTaken = this.createLazyEvent<any>('globalcursortaken');
  @Output() chartRendered = this.createLazyEvent<any>('rendered');
  @Output() chartFinished = this.createLazyEvent<any>('finished');

  public animationFrameID = null;
  private chart: ECharts;
  private chart$ = new ReplaySubject<ECharts>(1);
  private echarts: any;
  private resizeOb: ResizeObserver;
  private resize$ = new Subject<void>();
  private resizeSub: Subscription;
  private initChartTimer?: number;
  private changeFilter = new ChangeFilterV2();
  private loadingSub: Subscription;
  private resizeObFired: boolean = false;

  constructor(
    @Inject(NGX_ECHARTS_CONFIG) config: NgxEchartsConfig,
    private el: ElementRef,
    private ngZone: NgZone
  ) {
    this.echarts = config.echarts;
    this.theme = config.theme || null;
  }

  ngOnChanges(changes: SimpleChanges) {
    this.changeFilter.doFilter(changes);
  }

  ngOnInit() {
    if (!window.ResizeObserver) {
      throw new Error('please install a polyfill for ResizeObserver');
    }
    this.resizeSub = this.resize$
      .pipe(throttleTime(100, asyncScheduler, { leading: false, trailing: true }))
      .subscribe(() => this.resize());

    if (this.autoResize) {
      // https://github.com/xieziyu/ngx-echarts/issues/413
      this.resizeOb = this.ngZone.runOutsideAngular(
        () =>
          new window.ResizeObserver(entries => {
            for (const entry of entries) {
              if (entry.target === this.el.nativeElement) {
                // Ignore first fire on insertion, no resize actually happened
                if (!this.resizeObFired) {
                  this.resizeObFired = true;
                } else {
                  this.animationFrameID = window.requestAnimationFrame(() => {
                    this.resize$.next();
                  });
                }
              }
            }
          })
      );
      this.resizeOb.observe(this.el.nativeElement);
    }

    this.changeFilter.notFirstAndEmpty('options', opt => this.onOptionsChange(opt));
    this.changeFilter.notFirstAndEmpty('merge', opt => this.setOption(opt));
    this.changeFilter.has<boolean>('loading', v => this.toggleLoading(!!v));
    this.changeFilter.notFirst<string | ThemeOption>('theme', () => this.refreshChart());
  }

  ngOnDestroy() {
    window.clearTimeout(this.initChartTimer);
    if (this.resizeSub) {
      this.resizeSub.unsubscribe();
    }
    if (this.animationFrameID) {
      window.cancelAnimationFrame(this.animationFrameID);
    }
    if (this.resizeOb) {
      this.resizeOb.unobserve(this.el.nativeElement);
    }
    if (this.loadingSub) {
      this.loadingSub.unsubscribe();
    }
    this.changeFilter.dispose();
    this.dispose();
  }

  ngAfterViewInit() {
    this.initChartTimer = window.setTimeout(() => this.initChart());
  }

  private dispose() {
    if (this.chart) {
      if (!this.chart.isDisposed()) {
        this.chart.dispose();
      }
      this.chart = null;
    }
  }

  /**
   * resize chart
   */
  resize() {
    if (this.chart) {
      this.chart.resize();
    }
  }

  private toggleLoading(loading: boolean) {
    if (this.chart) {
      loading
        ? this.chart.showLoading(this.loadingType, this.loadingOpts)
        : this.chart.hideLoading();
    } else {
      this.loadingSub = this.chart$.subscribe(chart =>
        loading ? chart.showLoading(this.loadingType, this.loadingOpts) : chart.hideLoading()
      );
    }
  }

  private setOption(option: any, opts?: any) {
    if (this.chart) {
      try {
        this.chart.setOption(option, opts);
      } catch (e) {
        console.error(e);
        this.optionsError.emit(e);
      }
    }
  }

  /**
   * dispose old chart and create a new one.
   */
  async refreshChart() {
    this.dispose();
    await this.initChart();
  }

  private createChart() {
    const dom = this.el.nativeElement;

    if (window && window.getComputedStyle) {
      const prop = window.getComputedStyle(dom, null).getPropertyValue('height');
      if ((!prop || prop === '0px') && (!dom.style.height || dom.style.height === '0px')) {
        dom.style.height = '400px';
      }
    }

    // here a bit tricky: we check if the echarts module is provided as function returning native import('...') then use the promise
    // otherwise create the function that imitates behaviour above with a provided as is module
    return this.ngZone.runOutsideAngular(() => {
      const load =
        typeof this.echarts === 'function' ? this.echarts : () => Promise.resolve(this.echarts);

      return load().then(({ init }) => init(dom, this.theme, this.initOpts));
    });
  }

  private async initChart() {
    await this.onOptionsChange(this.options);

    if (this.merge && this.chart) {
      this.setOption(this.merge);
    }
  }

  private async onOptionsChange(opt: any) {
    if (!opt) {
      return;
    }

    if (this.chart) {
      this.setOption(this.options, true);
    } else {
      this.chart = await this.createChart();
      this.chart$.next(this.chart);
      this.chartInit.emit(this.chart);
      this.setOption(this.options, true);
    }
  }

  // allows to lazily bind to only those events that are requested through the `@Output` by parent components
  // see https://stackoverflow.com/questions/51787972/optimal-reentering-the-ngzone-from-eventemitter-event for more info
  private createLazyEvent<T>(eventName: string): EventEmitter<T> {
    return this.chartInit.pipe(
      switchMap(
        (chart: any) =>
          new Observable(observer => {
            chart.on(eventName, (data: T) => this.ngZone.run(() => observer.next(data)));
            return () => {
              if (this.chart) {
                if (!this.chart.isDisposed()) {
                  chart.off(eventName);
                }
              }
            };
          })
      )
    ) as EventEmitter<T>;
  }
}

results matching ""

    No results matching ""