highcharts-angular
Version:
Highcharts component for Angular.
289 lines (280 loc) • 14 kB
JavaScript
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