Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 3 additions & 5 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,16 @@ All notable changes to this project will be documented in this file. See [standa

## [5.4.0](https://github.com/highcharts/highcharts-angular/compare/v3.0.0...v5.4.0) (2026-01-26)


### Bug Fixes

* Signals are now read before `await` in `computed()` to ensure proper reactivity.
- Signals are now read before `await` in `computed()` to ensure proper reactivity.

## [5.3.0](https://github.com/highcharts/highcharts-angular/compare/v3.0.0...v5.3.0) (2026-01-23)


### Features

* Deprecation: Marked `[(update)]` and `[(oneToOne)]` inputs as deprecated- they will be removed in a future 6.0.0 release.
* Simplified Logic: Shifted the responsibility of triggering updates from the wrapper's internal state checking to the developer via manual `.update()` calls
- Deprecation: Marked `[(update)]` and `[(oneToOne)]` inputs as deprecated- they will be removed in a future 6.0.0 release.
- Simplified Logic: Shifted the responsibility of triggering updates from the wrapper's internal state checking to the developer via manual `.update()` calls

## [5.2.0](https://github.com/highcharts/highcharts-angular/compare/v3.0.0...v5.2.0) (2025-11-05)

Expand Down
12 changes: 6 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@ Official minimal Highcharts integration for Angular.

## Links

* Official website: [www.highcharts.com](https://www.highcharts.com)
* Product page: [www.highcharts.com/integrations/angular](https://www.highcharts.com/integrations/angular)
* Download: [www.highcharts.com/download](https://www.highcharts.com/download)
* License: [www.highcharts.com/license](https://www.highcharts.com/license)
* Support: [www.highcharts.com/support](https://www.highcharts.com/support)
* Issues: [Working repo](https://github.com/highcharts/highcharts/issues)
- Official website: [www.highcharts.com](https://www.highcharts.com)
- Product page: [www.highcharts.com/integrations/angular](https://www.highcharts.com/integrations/angular)
- Download: [www.highcharts.com/download](https://www.highcharts.com/download)
- License: [www.highcharts.com/license](https://www.highcharts.com/license)
- Support: [www.highcharts.com/support](https://www.highcharts.com/support)
- Issues: [Working repo](https://github.com/highcharts/highcharts/issues)

## Table of Contents

Expand Down
8 changes: 3 additions & 5 deletions highcharts-angular/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,16 @@ All notable changes to this project will be documented in this file. See [standa

## [5.4.0](https://github.com/highcharts/highcharts-angular/compare/v3.0.0...v5.4.0) (2026-01-26)


### Bug Fixes

* Signals are now read before `await` in `computed()` to ensure proper reactivity.
- Signals are now read before `await` in `computed()` to ensure proper reactivity.

## [5.3.0](https://github.com/highcharts/highcharts-angular/compare/v3.0.0...v5.3.0) (2026-01-23)


### Features

* Deprecation: Marked `[(update)]` and `[(oneToOne)]` inputs as deprecated- they will be removed in a future 6.0.0 release.
* Simplified Logic: Shifted the responsibility of triggering updates from the wrapper's internal state checking to the developer via manual `.update()` calls
- Deprecation: Marked `[(update)]` and `[(oneToOne)]` inputs as deprecated- they will be removed in a future 6.0.0 release.
- Simplified Logic: Shifted the responsibility of triggering updates from the wrapper's internal state checking to the developer via manual `.update()` calls

## [5.2.0](https://github.com/highcharts/highcharts-angular/compare/v3.0.0...v5.2.0) (2025-11-05)

Expand Down
39 changes: 36 additions & 3 deletions highcharts-angular/src/lib/highcharts-chart.directive.spec.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/// <reference types="jasmine" />
import { TestBed } from '@angular/core/testing';
import { ChangeDetectionStrategy, Component, DebugElement } from '@angular/core';
import { By } from '@angular/platform-browser';
Expand All @@ -17,6 +18,20 @@ class TestHostComponent {
public options: Highcharts.Options = {};
}

// Added to simulate the dashboard performance scenario
@Component({
selector: 'highcharts-multi-test-host',
template: `
<div highchartsChart [options]="options"></div>
<div highchartsChart [options]="options"></div>
`,
imports: [HighchartsChartDirective],
changeDetection: ChangeDetectionStrategy.OnPush,
})
class MultiTestHostComponent {
public options: Highcharts.Options = {};
}

describe('HighchartsChartDirective', () => {
let debugElement: DebugElement;
let directive: HighchartsChartDirective;
Expand All @@ -25,11 +40,11 @@ describe('HighchartsChartDirective', () => {
beforeEach(() => {
loadSpy = jasmine.createSpy('load');
TestBed.configureTestingModule({
imports: [TestHostComponent, HighchartsChartDirective],
imports: [TestHostComponent, MultiTestHostComponent, HighchartsChartDirective],
providers: [
{
provide: HIGHCHARTS_CONFIG,
useValue: {},
useValue: { timeout: 500 },
},
{
provide: HighchartsChartService,
Expand All @@ -56,6 +71,24 @@ describe('HighchartsChartDirective', () => {
});

it('should load global config on initialization', () => {
expect(loadSpy).toHaveBeenCalledWith({});
expect(loadSpy).toHaveBeenCalledWith({ timeout: 500 });
});

it('should natively stagger simultaneous chart initializations to prevent main thread blocking', () => {
// eslint-disable-next-line no-restricted-globals
const setTimeoutSpy = spyOn(window, 'setTimeout').and.callThrough();

// Create a multi-chart host so they process in the exact same synchronous execution frame
const multiFixture = TestBed.createComponent(MultiTestHostComponent);
multiFixture.detectChanges();

const allTimeouts = setTimeoutSpy.calls.allArgs().map(args => args[1]);
const chartDelays = allTimeouts.filter(ms => typeof ms === 'number' && ms >= 500);

// Verify that the charts received staggered delays (a difference of exactly 16ms between calls).
// Testing the difference safely ignores any timer increments stolen by the beforeEach() chart!
const difference = (chartDelays[1] ?? 0) - (chartDelays[0] ?? 0);

expect(difference).withContext(`Expected a 16ms stagger difference between chart delays`).toBe(16);
});
});
77 changes: 29 additions & 48 deletions highcharts-angular/src/lib/highcharts-chart.directive.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,89 +17,74 @@ import { HIGHCHARTS_CONFIG, HIGHCHARTS_TIMEOUT } from './highcharts-chart.token'
import { ChartConstructorType, ConstructorChart } from './types';
import type Highcharts from 'highcharts/esm/highcharts';

// --- STAGGER STATE ---
// Module-level variables to safely space out parallel chart creations
let staggerCount = 0;
let staggerResetTimer: any;

@Directive({
selector: '[highchartsChart]',
})
export class HighchartsChartDirective {
/**
* Type of the chart constructor.
*/
public readonly constructorType = input<ChartConstructorType>('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.
*/
public readonly oneToOne = input<boolean>(false);

/**
* Options for the Highcharts chart.
*/
public readonly options = input.required<Highcharts.Options>();

/**
* @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
*/
public readonly update = model<boolean>(true);

public readonly chartInstance = output<Highcharts.Chart>(); // #26
public readonly chartInstance = output<Highcharts.Chart>();

private readonly destroyRef = inject(DestroyRef);

private readonly el = inject<ElementRef<HTMLElement>>(ElementRef);

private readonly platformId = inject(PLATFORM_ID);

private readonly relativeConfig = inject(HIGHCHARTS_CONFIG, {
optional: true,
});

private readonly timeout = inject(HIGHCHARTS_TIMEOUT, {
optional: true,
});

private readonly relativeConfig = inject(HIGHCHARTS_CONFIG, { optional: true });
private readonly timeout = inject(HIGHCHARTS_TIMEOUT, { optional: true });
private readonly highchartsChartService = inject(HighchartsChartService);

private chartCreated = false;

private _chartInstance: Highcharts.Chart | undefined;

private isDestroyed = false;

private delay(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}

// Create the chart as soon as we can
private readonly chart = computed(async () => {
const highCharts = this.highchartsChartService.highcharts();
const constructorType = this.constructorType();
await this.delay(this.relativeConfig?.timeout ?? this.timeout ?? 500);

// 1. Grab the current stagger increment (0 for single charts)
const currentStaggerDelay = staggerCount * 16;
staggerCount++;

// 2. Safely debounce the reset so independent charts start fresh at 0
clearTimeout(staggerResetTimer);
staggerResetTimer = setTimeout(() => {
staggerCount = 0;
}, 50);

// 3. Apply the stagger natively to the existing delay. No extra Promises required!
const baseTimeout = this.relativeConfig?.timeout ?? this.timeout ?? 500;
await this.delay(baseTimeout + currentStaggerDelay);

if (!highCharts) return;

const callback: Highcharts.ChartCallbackFunction = (chart: Highcharts.Chart) => {
if (chart.renderer.forExport || this.isDestroyed) return;
return this.chartInstance.emit(chart);
};

const chartFactories: Record<ChartConstructorType, ConstructorChart> = {
chart: highCharts.chart,
ganttChart: (highCharts as any).ganttChart,
mapChart: (highCharts as any).mapChart,
stockChart: (highCharts as any).stockChart,
};

// Return exactly as the original codebase did to satisfy all strict component tests
return chartFactories[constructorType](
this.el.nativeElement,
// Use untracked, so we don't re-create new chart everytime options change
untracked(() => this.options()),
// Use Highcharts callback to emit chart instance, so it is available as early
// as possible. So that Angular is already aware of the instance if Highcharts raise
// events during the initialization that happens before coming back to Angular
callback,
);
) as Highcharts.Chart;
});

private keepChartUpToDate(): void {
Expand All @@ -121,20 +106,16 @@ export class HighchartsChartDirective {
}

public constructor() {
// should stop loading on the server side for SSR
if (this.platformId && isPlatformServer(this.platformId)) {
return;
}
// make sure to load global config + modules on demand
this.highchartsChartService.load(this.relativeConfig);
// destroy the chart when the directive is destroyed
this.destroyRef.onDestroy(() => {
this._chartInstance?.destroy();
this._chartInstance = undefined;
this.isDestroyed = true;
}); // #44
});

// Keep the chart up to date whenever options change or the update special input is set to true
this.keepChartUpToDate();
}
}
8 changes: 4 additions & 4 deletions src/main.server.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { bootstrapApplication } from '@angular/platform-browser';
import { bootstrapApplication, BootstrapContext } from '@angular/platform-browser';
import { AppComponent } from './app/app.component';
import { config } from './app/app.config.server';
import { ApplicationRef } from '@angular/core';

const bootstrap = (): Promise<ApplicationRef> => bootstrapApplication(AppComponent, config);
const bootstrap = (context: BootstrapContext): Promise<ApplicationRef> =>
bootstrapApplication(AppComponent, config, context);


export default bootstrap;
export default bootstrap;
Loading