UNPKG

highcharts-angular

Version:
289 lines (280 loc) 14 kB
import * as i0 from '@angular/core'; import { InjectionToken, signal, inject, Injectable, effect, untracked, input, model, output, DestroyRef, ElementRef, PLATFORM_ID, PendingTasks, Directive, ChangeDetectionStrategy, Component, makeEnvironmentProviders } from '@angular/core'; import { isPlatformServer } from '@angular/common'; const HIGHCHARTS_LOADER = new InjectionToken('HIGHCHARTS_LOADER'); const HIGHCHARTS_ROOT_MODULES = new InjectionToken('HIGHCHARTS_ROOT_MODULES'); const HIGHCHARTS_OPTIONS = new InjectionToken('HIGHCHARTS_OPTIONS'); const HIGHCHARTS_CONFIG = new InjectionToken('HIGHCHARTS_CONFIG'); const HIGHCHARTS_TIMEOUT = new InjectionToken('HIGHCHARTS_TIMEOUT'); class HighchartsChartService { constructor() { this.highcharts = signal(null); this.loader = inject(HIGHCHARTS_LOADER); this.globalOptions = inject(HIGHCHARTS_OPTIONS, { optional: true, }); this.globalModules = inject(HIGHCHARTS_ROOT_MODULES, { optional: true, }); this.sharedHighchartsPromise = null; this.moduleLoadCache = new WeakMap(); } async loadModules(modulesFactory) { if (!modulesFactory) { return; } const cachedLoad = this.moduleLoadCache.get(modulesFactory); if (cachedLoad) { return cachedLoad; } const moduleLoad = Promise.allSettled(modulesFactory()).then(moduleResults => { const rejectedModules = moduleResults.filter((result) => result.status === 'rejected'); if (rejectedModules.length) { const reasons = rejectedModules.map(({ reason }) => reason instanceof Error ? reason.message : String(reason)); throw new Error(`Failed to load Highcharts modules: ${reasons.join('; ')}`); } }); this.moduleLoadCache.set(modulesFactory, moduleLoad); // Drop the memo if the load fails so an identical factory can be retried // instead of reusing a permanently rejected promise. moduleLoad.catch(() => { if (this.moduleLoadCache.get(modulesFactory) === moduleLoad) { this.moduleLoadCache.delete(modulesFactory); } }); return moduleLoad; } async ensureSharedHighcharts() { if (!this.sharedHighchartsPromise) { const load = (async () => { const highcharts = await this.loader(); // Root-level modules and options mutate a shared Highcharts singleton, // so initialize them once and reuse the same ready instance afterwards. await this.loadModules(this.globalModules); if (this.globalOptions) { highcharts.setOptions(this.globalOptions); } return highcharts; })(); // Cache the in-flight load, but drop the memo if it fails so a later // load() can retry instead of reusing a permanently rejected promise. this.sharedHighchartsPromise = load; load.catch(() => { if (this.sharedHighchartsPromise === load) { this.sharedHighchartsPromise = null; } }); } return this.sharedHighchartsPromise; } async load(partialConfig) { const highcharts = await this.ensureSharedHighcharts(); // Component-level modules are still loaded per config, but cached by the // factory function so identical providers do not repeat the same work. await this.loadModules(partialConfig?.modules); this.highcharts.set(highcharts); return highcharts; } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.25", ngImport: i0, type: HighchartsChartService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); } static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.25", ngImport: i0, type: HighchartsChartService, providedIn: 'root' }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.25", ngImport: i0, type: HighchartsChartService, decorators: [{ type: Injectable, args: [{ providedIn: 'root' }] }] }); class HighchartsChartDirective { delay(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } getChartFactory(highcharts, constructorType) { if (constructorType === 'chart') { return highcharts.chart; } const highchartsWithModuleConstructors = highcharts; const chartFactory = highchartsWithModuleConstructors[constructorType]; if (!chartFactory) { throw new Error(`Highcharts constructor "${constructorType}" is not available. Did you load the required module?`); } return chartFactory; } createChart() { effect(onCleanup => { const highcharts = this.loadedHighcharts(); const constructorType = this.constructorType(); if (!highcharts || this.isDestroyed) { return; } const callback = (chart) => { if (chart.renderer.forExport || this.isDestroyed) return; return this.chartInstance.emit(chart); }; const chart = this.getChartFactory(highcharts, constructorType)(this.el.nativeElement, // Read options without tracking them here: option changes should update // the existing chart, not tear it down and create a new one. untracked(() => this.options()), callback); this.chart.set(chart); onCleanup(() => { if (this.chart() === chart) { this.chart.set(null); } chart.destroy(); }); }); } keepChartUpToDate() { let lastChart = null; effect(() => { const chart = this.chart(); // Deprecated inputs remain supported internally until they are removed. // eslint-disable-next-line @typescript-eslint/no-deprecated const update = this.update(); // eslint-disable-next-line @typescript-eslint/no-deprecated const oneToOne = this.oneToOne(); const options = this.options(); if (!chart) { return; } // Skip the first pass after creation. The constructor already consumed // the initial options, so calling `update` immediately would duplicate work. if (chart !== lastChart) { lastChart = chart; return; } if (update) { chart.update(options, true, oneToOne); } }); } async initializeHighcharts() { await this.pendingTasks.run(async () => { try { const highcharts = await this.highchartsChartService.load(this.relativeConfig); const delayMs = this.relativeConfig?.timeout ?? this.timeout ?? 0; // Optional escape hatch: defer chart creation after Highcharts is ready // (e.g. to let the container settle). Skipped entirely by default. if (delayMs > 0) { await this.delay(delayMs); } if (!this.isDestroyed) { this.loadedHighcharts.set(highcharts); } } catch (error) { // Core/module loading failed: leave the chart uncreated and surface the // reason instead of letting it become an unhandled promise rejection. console.error('Highcharts failed to load; chart was not created.', error); } }); } constructor() { /** * Type of the chart constructor. * Changing it recreates the chart because Highcharts constructors are not update-compatible. */ this.constructorType = input('chart'); /** * @deprecated Will be removed in a future release. * When enabled, Updates `series`, `xAxis`, `yAxis`, and `annotations` to match new options. * Items are added/removed as needed. Series with `id`s are matched by `id`; * unmatched items are removed. Omitted `series` leaves existing ones unchanged. */ this.oneToOne = input(false); /** * Options for the Highcharts chart. */ this.options = input.required(); /** * @deprecated Will be removed in a future release. * Whether to redraw the chart. * Check how update works in Highcharts * API doc here: https://api.highcharts.com/class-reference/Highcharts.Chart#update */ this.update = model(true); this.chartInstance = output(); this.destroyRef = inject(DestroyRef); this.el = inject(ElementRef); this.platformId = inject(PLATFORM_ID); this.relativeConfig = inject(HIGHCHARTS_CONFIG, { optional: true }); this.timeout = inject(HIGHCHARTS_TIMEOUT, { optional: true }); this.highchartsChartService = inject(HighchartsChartService); this.pendingTasks = inject(PendingTasks); this.loadedHighcharts = signal(null); this.chart = signal(null); this.isDestroyed = false; if (this.platformId && isPlatformServer(this.platformId)) { return; } this.destroyRef.onDestroy(() => { this.isDestroyed = true; }); this.createChart(); this.keepChartUpToDate(); void this.initializeHighcharts(); } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.25", ngImport: i0, type: HighchartsChartDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); } static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "19.2.25", type: HighchartsChartDirective, isStandalone: true, selector: "[highchartsChart]", inputs: { constructorType: { classPropertyName: "constructorType", publicName: "constructorType", isSignal: true, isRequired: false, transformFunction: null }, oneToOne: { classPropertyName: "oneToOne", publicName: "oneToOne", isSignal: true, isRequired: false, transformFunction: null }, options: { classPropertyName: "options", publicName: "options", isSignal: true, isRequired: true, transformFunction: null }, update: { classPropertyName: "update", publicName: "update", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { update: "updateChange", chartInstance: "chartInstance" }, ngImport: i0 }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.25", ngImport: i0, type: HighchartsChartDirective, decorators: [{ type: Directive, args: [{ selector: '[highchartsChart]', }] }], ctorParameters: () => [] }); class HighchartsChartComponent { static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.25", ngImport: i0, type: HighchartsChartComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); } static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.25", type: HighchartsChartComponent, isStandalone: true, selector: "highcharts-chart", hostDirectives: [{ directive: HighchartsChartDirective, inputs: ["constructorType", "constructorType", "oneToOne", "oneToOne", "options", "options", "update", "update"], outputs: ["chartInstance", "chartInstance", "updateChange", "updateChange"] }], ngImport: i0, template: '', isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.25", ngImport: i0, type: HighchartsChartComponent, decorators: [{ type: Component, args: [{ selector: 'highcharts-chart', template: '', hostDirectives: [ { directive: HighchartsChartDirective, inputs: ['constructorType', 'oneToOne', 'options', 'update'], outputs: ['chartInstance', 'updateChange'], }, ], changeDetection: ChangeDetectionStrategy.OnPush, }] }] }); const emptyModuleFactoryFunction = () => []; const defaultInstanceFactoryFunction = () => import('highcharts/esm/highcharts').then(m => m.default); function provideHighchartsInstance(instance) { return makeEnvironmentProviders([ { provide: HIGHCHARTS_LOADER, useValue: instance ?? defaultInstanceFactoryFunction, }, ]); } function provideHighchartsOptions(options) { return makeEnvironmentProviders([{ provide: HIGHCHARTS_OPTIONS, useValue: options }]); } function provideHighchartsRootModules(modules) { return makeEnvironmentProviders([{ provide: HIGHCHARTS_ROOT_MODULES, useValue: modules }]); } function providePartialHighcharts(config) { return { provide: HIGHCHARTS_CONFIG, useValue: config }; } function provideHighcharts(config = {}) { const providers = [ provideHighchartsInstance(config.instance), provideHighchartsRootModules(config.modules ?? emptyModuleFactoryFunction), { provide: HIGHCHARTS_TIMEOUT, useValue: config.timeout }, ]; if (config.options) { providers.push(provideHighchartsOptions(config.options)); } return makeEnvironmentProviders(providers); } /* * Public API Surface of highcharts-angular */ /** * Generated bundle index. Do not edit. */ export { HighchartsChartComponent, HighchartsChartDirective, provideHighcharts, providePartialHighcharts }; //# sourceMappingURL=highcharts-angular.mjs.map