From d8877e7e80813a43cfbd32823ee70437bc7f66d5 Mon Sep 17 00:00:00 2001 From: Mohamed Ben Makhlouf Date: Thu, 4 Sep 2025 23:51:36 +0100 Subject: [PATCH 01/17] add dumbbell and tilemap chart components with styles and templates --- .../dumbbell-chart.component.css | 11 + .../dumbbell-chart.component.html | 5 + .../dumbbell-chart.component.ts | 197 ++++++++++++++++++ .../tilemap-chart/tilemap-chart.component.css | 11 + .../tilemap-chart.component.html | 10 + .../tilemap-chart/tilemap-chart.component.ts | 103 +++++++++ 6 files changed, 337 insertions(+) create mode 100644 src/app/dumbbell-chart/dumbbell-chart.component.css create mode 100644 src/app/dumbbell-chart/dumbbell-chart.component.html create mode 100644 src/app/dumbbell-chart/dumbbell-chart.component.ts create mode 100644 src/app/tilemap-chart/tilemap-chart.component.css create mode 100644 src/app/tilemap-chart/tilemap-chart.component.html create mode 100644 src/app/tilemap-chart/tilemap-chart.component.ts diff --git a/src/app/dumbbell-chart/dumbbell-chart.component.css b/src/app/dumbbell-chart/dumbbell-chart.component.css new file mode 100644 index 0000000..6a487ef --- /dev/null +++ b/src/app/dumbbell-chart/dumbbell-chart.component.css @@ -0,0 +1,11 @@ +h2 { + font-family: Arial, Helvetica, sans-serif; + font-size: 1.25rem; + margin: 0 0 1.5rem 0; +} + +.main { + width: 100%; + height: 650px; + display: block; +} diff --git a/src/app/dumbbell-chart/dumbbell-chart.component.html b/src/app/dumbbell-chart/dumbbell-chart.component.html new file mode 100644 index 0000000..e0d4f1b --- /dev/null +++ b/src/app/dumbbell-chart/dumbbell-chart.component.html @@ -0,0 +1,5 @@ +
+

Demo #8: Highcharts Dumbbell

+ +
+ diff --git a/src/app/dumbbell-chart/dumbbell-chart.component.ts b/src/app/dumbbell-chart/dumbbell-chart.component.ts new file mode 100644 index 0000000..c645211 --- /dev/null +++ b/src/app/dumbbell-chart/dumbbell-chart.component.ts @@ -0,0 +1,197 @@ +import { Component, ChangeDetectionStrategy } from '@angular/core'; +import { HighchartsChartComponent } from 'highcharts-angular'; +import { providePartialHighcharts } from 'highcharts-angular'; + +@Component({ + selector: 'app-dumbbell-chart', + imports: [HighchartsChartComponent], + templateUrl: './dumbbell-chart.component.html', + styleUrl: './dumbbell-chart.component.css', + providers: [ + providePartialHighcharts({ + modules: () => { + return [ + import('highcharts/esm/highcharts-more'), + import('highcharts/esm/modules/dumbbell'), + import('highcharts/esm/modules/pattern-fill'), + ]; + }, + }), + ], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class DumbbellChartComponent { + public options: Highcharts.Options = { + chart: { + plotBorderWidth: 1, + zooming: { + type: 'y', + resetButton: { + theme: { style: { display: 'none' } }, + }, + }, + spacingLeft: 1, + spacingRight: 1, + spacingTop: 7, + spacingBottom: 0, + animation: false, + panning: { + enabled: true, + type: 'y', + }, + panKey: 'shift', + }, + plotOptions: { + columnrange: { + grouping: false, + }, + }, + legend: { enabled: false }, + credits: { enabled: false }, + title: { text: '' }, + yAxis: { + reversed: true, + title: { text: '' }, + startOnTick: false, + endOnTick: false, + gridLineWidth: 1, + tickPixelInterval: 72, + minPadding: 0.1, + minRange: 0.1, + crosshair: { + color: 'red', + snap: false, + zIndex: 4, + }, + labels: { + enabled: true, + style: { fontSize: '11px' }, + x: -7, + }, + }, + xAxis: { + startOnTick: false, + endOnTick: false, + lineWidth: 0, + tickWidth: 0, + minRange: 0.1, + gridLineWidth: 1, + labels: { style: { fontSize: '11px' }, y: 14 }, + }, + series: [ + { + data: [['', 2060, 2000]], + type: 'dumbbell', + name: 'Surface_26: Cement', + color: 'lightgray', + groupPadding: 0.7, + connectorWidth: 14, + marker: { + radius: 0, + }, + }, + { + data: [['', 2060, 2000]], + type: 'dumbbell', + name: 'Surface_26: Tubular', + color: 'black', + zIndex: 1, + groupPadding: 0.7, + connectorWidth: 3, + marker: { + radius: 0, + }, + lowMarker: { + radius: 5, + fillColor: 'black', + symbol: 'flag', + }, + }, + { + data: [['', 2791.96, 2000]], + type: 'dumbbell', + name: 'Phase_16: Cement', + color: 'lightgray', + groupPadding: 0.7, + connectorWidth: 14, + marker: { + radius: 0, + }, + }, + { + data: [['', 2791.96, 2000]], + type: 'dumbbell', + name: 'Phase_16: Tubular', + color: 'black', + zIndex: 1, + groupPadding: 0.7, + connectorWidth: 3, + marker: { + radius: 0, + }, + lowMarker: { + radius: 5, + fillColor: 'black', + symbol: 'flag', + }, + }, + { + data: [['', 4500, 4000]], + type: 'dumbbell', + name: 'Phase_13_375: Cement', + color: 'lightgray', + groupPadding: 0.7, + connectorWidth: 14, + marker: { + radius: 0, + }, + }, + { + data: [['', 4500, 2000]], + type: 'dumbbell', + name: 'Phase_13_375: Tubular', + color: 'black', + zIndex: 1, + groupPadding: 0.7, + connectorWidth: 3, + marker: { + radius: 0, + }, + lowMarker: { + radius: 5, + fillColor: 'black', + symbol: 'flag', + }, + }, + { + data: [['', 5250, 4650]], + type: 'dumbbell', + name: 'Phase_10_74: Cement', + color: 'lightgray', + groupPadding: 0.7, + connectorWidth: 14, + marker: { + radius: 0, + }, + }, + { + data: [['', 5250, 4400]], + type: 'dumbbell', + name: 'Phase_10_74: Tubular', + color: 'black', + zIndex: 1, + groupPadding: 0.7, + connectorWidth: 3, + marker: { + radius: 0, + }, + lowMarker: { + radius: 5, + fillColor: 'black', + symbol: 'flag', + }, + }, + ], + }; + +} diff --git a/src/app/tilemap-chart/tilemap-chart.component.css b/src/app/tilemap-chart/tilemap-chart.component.css new file mode 100644 index 0000000..6a487ef --- /dev/null +++ b/src/app/tilemap-chart/tilemap-chart.component.css @@ -0,0 +1,11 @@ +h2 { + font-family: Arial, Helvetica, sans-serif; + font-size: 1.25rem; + margin: 0 0 1.5rem 0; +} + +.main { + width: 100%; + height: 650px; + display: block; +} diff --git a/src/app/tilemap-chart/tilemap-chart.component.html b/src/app/tilemap-chart/tilemap-chart.component.html new file mode 100644 index 0000000..396ec83 --- /dev/null +++ b/src/app/tilemap-chart/tilemap-chart.component.html @@ -0,0 +1,10 @@ +
+

Demo #5: Highcharts Tile Maps

+ +
+ diff --git a/src/app/tilemap-chart/tilemap-chart.component.ts b/src/app/tilemap-chart/tilemap-chart.component.ts new file mode 100644 index 0000000..9350bae --- /dev/null +++ b/src/app/tilemap-chart/tilemap-chart.component.ts @@ -0,0 +1,103 @@ +import { Component, ChangeDetectionStrategy } from '@angular/core'; +import { HighchartsChartComponent, providePartialHighcharts, ChartConstructorType } from 'highcharts-angular'; + +@Component({ + selector: 'app-tilemap-chart', + imports: [HighchartsChartComponent], + templateUrl: './tilemap-chart.component.html', + styleUrl: './tilemap-chart.component.css', + providers: [ + providePartialHighcharts({ + modules: () => [import('highcharts/esm/modules/map'), import('highcharts/esm/modules/tilemap')], + }) + ], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class TilemapChartComponent { + public get chartOptions(): Highcharts.Options { + return { + chart: { + type: 'tilemap', + inverted: true, + height: '80%' + }, + title: { + text: "My Title", + }, + xAxis: { + visible: false, + }, + yAxis: { + visible: false, + }, + colorAxis: { + dataClasses: [ + { + from: 0, + to: 1000000, + color: '#3b528b', + name: '< 1M', + }, + { + from: 1000000, + to: 5000000, + color: '#21918c', + name: '1M - 5M', + }, + { + from: 5000000, + to: 20000000, + color: '#5ec962', + name: '5M - 20M', + }, + { + from: 20000000, + color: '#fde725', + name: '> 20M', + }, + ], + }, + legend: { + enabled: false, + }, + tooltip: { + headerFormat: '', + pointFormat: 'Population de {point.name}: {point.value}', + }, + plotOptions: { + tilemap: { + tileShape: 'hexagon', + dataLabels: { + enabled: true, + format: '{point.hc-a2}', + style: { + textOutline: 'none', + }, + }, + }, + }, + series: [ + { + type: 'tilemap', + name: 'Population', + data: this.getTilemapData(), + }, + ], + }; + } + + private getTilemapData(): any[] { + return [ + { 'hc-a2': 'CA', name: 'California', x: 5, y: 2, value: 38965193 }, + { 'hc-a2': 'TX', name: 'Texas', x: 7, y: 4, value: 30503301 }, + { 'hc-a2': 'FL', name: 'Florida', x: 8, y: 8, value: 22610726 }, + { 'hc-a2': 'NY', name: 'New York', x: 2, y: 9, value: 19571216 }, + { 'hc-a2': 'IL', name: 'Illinois', x: 3, y: 6, value: 12882135 }, + { 'hc-a2': 'PA', name: 'Pennsylvania', x: 3, y: 8, value: 12801989 }, + ]; + } + + public chartConstructor: ChartConstructorType = 'chart'; + public updateFlag = false; + public oneToOneFlag = true; +} From a3fb2ac552c437f41e6ffc894b4ee484da4a7c82 Mon Sep 17 00:00:00 2001 From: Mohamed Ben Makhlouf Date: Thu, 4 Sep 2025 23:52:00 +0100 Subject: [PATCH 02/17] add tilemap and dumbbell chart components to app --- src/app/app.component.html | 2 ++ src/app/app.component.ts | 12 +++++++++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/src/app/app.component.html b/src/app/app.component.html index a22188c..be58e88 100644 --- a/src/app/app.component.html +++ b/src/app/app.component.html @@ -2,6 +2,8 @@ + + diff --git a/src/app/app.component.ts b/src/app/app.component.ts index b97868c..5ede2d4 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -4,12 +4,22 @@ import { StockChartComponent } from './stock-chart/stock-chart.component'; import { MapChartComponent } from './map-chart/map-chart.component'; import { GanttChartComponent } from './gantt-chart/gantt-chart.component'; import { LazyLoadingChartComponent } from './lazy-loading-chart/lazy-loading-chart.component'; +import { TilemapChartComponent } from './tilemap-chart/tilemap-chart.component'; +import { DumbbellChartComponent } from './dumbbell-chart/dumbbell-chart.component'; @Component({ selector: 'app-root', templateUrl: './app.component.html', styleUrl: './app.component.css', - imports: [LineChartComponent, StockChartComponent, MapChartComponent, GanttChartComponent, LazyLoadingChartComponent], + imports: [ + LineChartComponent, + StockChartComponent, + MapChartComponent, + GanttChartComponent, + LazyLoadingChartComponent, + TilemapChartComponent, + DumbbellChartComponent, + ], changeDetection: ChangeDetectionStrategy.OnPush, }) export class AppComponent {} From 750e57d9221f626067c920c8ac8c3b8d6ddc3e8a Mon Sep 17 00:00:00 2001 From: Mohamed Ben Makhlouf Date: Thu, 4 Sep 2025 23:52:22 +0100 Subject: [PATCH 03/17] update worldMap resource URL in MapChartComponent --- src/app/map-chart/map-chart.component.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/app/map-chart/map-chart.component.ts b/src/app/map-chart/map-chart.component.ts index 44bd377..475beb6 100644 --- a/src/app/map-chart/map-chart.component.ts +++ b/src/app/map-chart/map-chart.component.ts @@ -17,7 +17,8 @@ import { HighchartsChartComponent, providePartialHighcharts } from 'highcharts-a changeDetection: ChangeDetectionStrategy.OnPush, }) export class MapChartComponent { - public readonly worldMap = httpResource('/highcharts/world.geo.json'); + // https://code.highcharts.com/mapdata/ + public readonly worldMap = httpResource('https://code.highcharts.com/mapdata/custom/world.topo.json'); public readonly chartMap = computed(() => { return { chart: { From 22dd9c2838a7764754a640a40f8b5c9118fa5e1a Mon Sep 17 00:00:00 2001 From: Mohamed Ben Makhlouf Date: Thu, 4 Sep 2025 23:52:50 +0100 Subject: [PATCH 04/17] refactor: improve loading of Highcharts modules with delays --- highcharts-angular/src/lib/highcharts-chart.service.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/highcharts-angular/src/lib/highcharts-chart.service.ts b/highcharts-angular/src/lib/highcharts-chart.service.ts index 3cef6e6..ba8ede5 100644 --- a/highcharts-angular/src/lib/highcharts-chart.service.ts +++ b/highcharts-angular/src/lib/highcharts-chart.service.ts @@ -16,10 +16,17 @@ export class HighchartsChartService { optional: true, }); + private delay(ms: number): Promise { + return new Promise(resolve => setTimeout(resolve, ms)); + } + private async loadHighchartsWithModules(partialConfig: PartialHighchartsConfig | null): Promise { const highcharts = await this.loader(); // Ensure Highcharts core is loaded - await Promise.all([...(this.globalModules?.() ?? []), ...(partialConfig?.modules?.() ?? [])]); + await Promise.allSettled([...(this.globalModules?.() ?? [])]); + await this.delay(100); + await Promise.allSettled([...(partialConfig?.modules?.() ?? [])]); + await this.delay(100); // Return the Highcharts instance return highcharts; From 3b23edfeb50201f24b3289ae235498918f913832 Mon Sep 17 00:00:00 2001 From: Mohamed Ben Makhlouf Date: Fri, 5 Sep 2025 00:14:01 +0100 Subject: [PATCH 05/17] refactor: enhance linting command to include Prettier formatting --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 5495e83..3e289f0 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,7 @@ "build:ssr": "ng build my-ssr-app", "run:ssr": "node dist/my-ssr-app/server/server.mjs", "test": "ng test my-app", - "lint": "ng lint", + "lint": "ng lint && prettier . --write", "release": "cd ./highcharts-angular && standard-version && cd ../ && node tasks/build.js && node tasks/release.js", "release-minor": "cd ./highcharts-angular && standard-version --release-as minor && cd ../ && node tasks/build.js && node tasks/release.js", "release-major": "cd ./highcharts-angular && standard-version --release-as major && cd ../ && node tasks/build.js && node tasks/release.js", From bc4a91cc4141f4edc08b83d1fcac23728c4c0ba3 Mon Sep 17 00:00:00 2001 From: Mohamed Ben Makhlouf Date: Fri, 5 Sep 2025 00:14:20 +0100 Subject: [PATCH 06/17] refactor: clean up HTML and TypeScript files for dumbbell and tilemap charts --- src/app/dumbbell-chart/dumbbell-chart.component.html | 1 - src/app/dumbbell-chart/dumbbell-chart.component.ts | 1 - src/app/tilemap-chart/tilemap-chart.component.html | 4 ++-- src/app/tilemap-chart/tilemap-chart.component.ts | 6 +++--- 4 files changed, 5 insertions(+), 7 deletions(-) diff --git a/src/app/dumbbell-chart/dumbbell-chart.component.html b/src/app/dumbbell-chart/dumbbell-chart.component.html index e0d4f1b..f76f5ae 100644 --- a/src/app/dumbbell-chart/dumbbell-chart.component.html +++ b/src/app/dumbbell-chart/dumbbell-chart.component.html @@ -2,4 +2,3 @@

Demo #8: Highcharts Dumbbell

- diff --git a/src/app/dumbbell-chart/dumbbell-chart.component.ts b/src/app/dumbbell-chart/dumbbell-chart.component.ts index c645211..410490d 100644 --- a/src/app/dumbbell-chart/dumbbell-chart.component.ts +++ b/src/app/dumbbell-chart/dumbbell-chart.component.ts @@ -193,5 +193,4 @@ export class DumbbellChartComponent { }, ], }; - } diff --git a/src/app/tilemap-chart/tilemap-chart.component.html b/src/app/tilemap-chart/tilemap-chart.component.html index 396ec83..d976931 100644 --- a/src/app/tilemap-chart/tilemap-chart.component.html +++ b/src/app/tilemap-chart/tilemap-chart.component.html @@ -5,6 +5,6 @@

Demo #5: Highcharts Tile Maps

[constructorType]="chartConstructor" [options]="chartOptions" [oneToOne]="oneToOneFlag" - [(update)]="updateFlag" /> + [(update)]="updateFlag" + /> - diff --git a/src/app/tilemap-chart/tilemap-chart.component.ts b/src/app/tilemap-chart/tilemap-chart.component.ts index 9350bae..14cf508 100644 --- a/src/app/tilemap-chart/tilemap-chart.component.ts +++ b/src/app/tilemap-chart/tilemap-chart.component.ts @@ -9,7 +9,7 @@ import { HighchartsChartComponent, providePartialHighcharts, ChartConstructorTyp providers: [ providePartialHighcharts({ modules: () => [import('highcharts/esm/modules/map'), import('highcharts/esm/modules/tilemap')], - }) + }), ], changeDetection: ChangeDetectionStrategy.OnPush, }) @@ -19,10 +19,10 @@ export class TilemapChartComponent { chart: { type: 'tilemap', inverted: true, - height: '80%' + height: '80%', }, title: { - text: "My Title", + text: 'My Title', }, xAxis: { visible: false, From 4b8fb8c754d80ca1ffa887f10695da154ebf98cb Mon Sep 17 00:00:00 2001 From: Mohamed Ben Makhlouf Date: Fri, 5 Sep 2025 00:20:56 +0100 Subject: [PATCH 07/17] test: increase timeout duration in Highcharts loading tests --- highcharts-angular/src/lib/highcharts-chart.service.spec.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/highcharts-angular/src/lib/highcharts-chart.service.spec.ts b/highcharts-angular/src/lib/highcharts-chart.service.spec.ts index 67e692e..29d4b2c 100644 --- a/highcharts-angular/src/lib/highcharts-chart.service.spec.ts +++ b/highcharts-angular/src/lib/highcharts-chart.service.spec.ts @@ -39,14 +39,14 @@ describe('HighchartsChartService', () => { expect(service.highcharts()).toBeNull(); service.load(null); - tick(100); // Simulate the passage of time for the timeout in the load method + tick(300); // Simulate the passage of time for the timeout in the load method expect(service.highcharts()).toBe(mockLoader); // The Highcharts object should be emitted })); it('should call setOptions with global options if provided', fakeAsync(() => { service.load(null); - tick(100); // Simulate the passage of time for the timeout in the load method + tick(300); // Simulate the passage of time for the timeout in the load method // Check if setOptions was called with the global options expect(mockLoader.setOptions).toHaveBeenCalledWith(mockGlobalOptions); From 42b4d38b32f495bcc2e39a7cdb9759dc885302fb Mon Sep 17 00:00:00 2001 From: Mohamed Ben Makhlouf Date: Fri, 5 Sep 2025 22:28:47 +0100 Subject: [PATCH 08/17] refactor: separate Prettier command from linting script in package.json --- package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 3e289f0..a46e7a6 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,8 @@ "build:ssr": "ng build my-ssr-app", "run:ssr": "node dist/my-ssr-app/server/server.mjs", "test": "ng test my-app", - "lint": "ng lint && prettier . --write", + "lint": "ng lint", + "prettier": "prettier . --write", "release": "cd ./highcharts-angular && standard-version && cd ../ && node tasks/build.js && node tasks/release.js", "release-minor": "cd ./highcharts-angular && standard-version --release-as minor && cd ../ && node tasks/build.js && node tasks/release.js", "release-major": "cd ./highcharts-angular && standard-version --release-as major && cd ../ && node tasks/build.js && node tasks/release.js", From 034b9e3fac06f90e593019a026e3f988cddbd981 Mon Sep 17 00:00:00 2001 From: Mohamed Ben Makhlouf Date: Sat, 6 Sep 2025 00:06:59 +0100 Subject: [PATCH 09/17] feat: add timeout configuration for Highcharts loading --- highcharts-angular/src/lib/highcharts-chart.provider.ts | 4 +++- highcharts-angular/src/lib/highcharts-chart.token.ts | 1 + highcharts-angular/src/lib/types.ts | 6 ++++++ 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/highcharts-angular/src/lib/highcharts-chart.provider.ts b/highcharts-angular/src/lib/highcharts-chart.provider.ts index 78794cf..2283ecd 100644 --- a/highcharts-angular/src/lib/highcharts-chart.provider.ts +++ b/highcharts-angular/src/lib/highcharts-chart.provider.ts @@ -4,6 +4,7 @@ import { HIGHCHARTS_CONFIG, HIGHCHARTS_ROOT_MODULES, HIGHCHARTS_OPTIONS, + HIGHCHARTS_TIMEOUT } from './highcharts-chart.token'; import { ModuleFactoryFunction, HighchartsConfig, PartialHighchartsConfig, InstanceFactoryFunction } from './types'; import type Highcharts from 'highcharts/esm/highcharts'; @@ -34,9 +35,10 @@ export function providePartialHighcharts(config: PartialHighchartsConfig): Provi } export function provideHighcharts(config: HighchartsConfig = {}): EnvironmentProviders { - const providers: EnvironmentProviders[] = [ + const providers: (Provider | EnvironmentProviders)[] = [ provideHighchartsInstance(config.instance), provideHighchartsRootModules(config.modules ?? emptyModuleFactoryFunction), + { provide: HIGHCHARTS_TIMEOUT, useValue: config.timeout}, ]; if (config.options) { providers.push(provideHighchartsOptions(config.options)); diff --git a/highcharts-angular/src/lib/highcharts-chart.token.ts b/highcharts-angular/src/lib/highcharts-chart.token.ts index 1945255..0b0082c 100644 --- a/highcharts-angular/src/lib/highcharts-chart.token.ts +++ b/highcharts-angular/src/lib/highcharts-chart.token.ts @@ -6,3 +6,4 @@ export const HIGHCHARTS_LOADER = new InjectionToken('HI export const HIGHCHARTS_ROOT_MODULES = new InjectionToken('HIGHCHARTS_ROOT_MODULES'); export const HIGHCHARTS_OPTIONS = new InjectionToken('HIGHCHARTS_OPTIONS'); export const HIGHCHARTS_CONFIG = new InjectionToken('HIGHCHARTS_CONFIG'); +export const HIGHCHARTS_TIMEOUT = new InjectionToken('HIGHCHARTS_TIMEOUT'); diff --git a/highcharts-angular/src/lib/types.ts b/highcharts-angular/src/lib/types.ts index a0041dd..6d50c84 100644 --- a/highcharts-angular/src/lib/types.ts +++ b/highcharts-angular/src/lib/types.ts @@ -33,4 +33,10 @@ export type HighchartsConfig = { * Global chart options applied across all charts */ options?: Highcharts.Options; + + /** + * Timeout in milliseconds to wait for the Highcharts library to load + * Default is 500ms + */ + timeout?: number; } & PartialHighchartsConfig; From 57bf20dbec9f69a136f7e4974ed84e912b09cd57 Mon Sep 17 00:00:00 2001 From: Mohamed Ben Makhlouf Date: Sat, 6 Sep 2025 00:07:32 +0100 Subject: [PATCH 10/17] refactor: simplify Highcharts loading logic and remove unnecessary delays --- .../src/lib/highcharts-chart.service.ts | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/highcharts-angular/src/lib/highcharts-chart.service.ts b/highcharts-angular/src/lib/highcharts-chart.service.ts index ba8ede5..9a9ecf5 100644 --- a/highcharts-angular/src/lib/highcharts-chart.service.ts +++ b/highcharts-angular/src/lib/highcharts-chart.service.ts @@ -5,8 +5,7 @@ import type Highcharts from 'highcharts/esm/highcharts'; @Injectable({ providedIn: 'root' }) export class HighchartsChartService { - private readonly writableHighcharts = signal(null); - public readonly highcharts = this.writableHighcharts.asReadonly(); + public readonly highcharts = signal(null); private readonly loader = inject(HIGHCHARTS_LOADER); private readonly globalOptions = inject(HIGHCHARTS_OPTIONS, { @@ -16,17 +15,10 @@ export class HighchartsChartService { optional: true, }); - private delay(ms: number): Promise { - return new Promise(resolve => setTimeout(resolve, ms)); - } - private async loadHighchartsWithModules(partialConfig: PartialHighchartsConfig | null): Promise { const highcharts = await this.loader(); // Ensure Highcharts core is loaded - await Promise.allSettled([...(this.globalModules?.() ?? [])]); - await this.delay(100); - await Promise.allSettled([...(partialConfig?.modules?.() ?? [])]); - await this.delay(100); + await Promise.allSettled([...(this.globalModules?.() ?? []), ...(partialConfig?.modules?.() ?? [])]); // Return the Highcharts instance return highcharts; @@ -37,8 +29,7 @@ export class HighchartsChartService { if (this.globalOptions) { highcharts.setOptions(this.globalOptions); } - // add timeout to make sure the loader has attached all modules - setTimeout(() => this.writableHighcharts.set(highcharts), 100); + this.highcharts.set(highcharts); }); } } From 599056caf00b149ceb069a067f149768697210e0 Mon Sep 17 00:00:00 2001 From: Mohamed Ben Makhlouf Date: Sat, 6 Sep 2025 00:08:14 +0100 Subject: [PATCH 11/17] feat: add timeout handling for Highcharts chart creation and updates --- .../src/lib/highcharts-chart.directive.ts | 23 ++++++++++++++----- 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/highcharts-angular/src/lib/highcharts-chart.directive.ts b/highcharts-angular/src/lib/highcharts-chart.directive.ts index a60d4e4..efda875 100644 --- a/highcharts-angular/src/lib/highcharts-chart.directive.ts +++ b/highcharts-angular/src/lib/highcharts-chart.directive.ts @@ -14,7 +14,7 @@ import { } from '@angular/core'; import { isPlatformServer } from '@angular/common'; import { HighchartsChartService } from './highcharts-chart.service'; -import { HIGHCHARTS_CONFIG } from './highcharts-chart.token'; +import { HIGHCHARTS_CONFIG, HIGHCHARTS_TIMEOUT } from './highcharts-chart.token'; import { ChartConstructorType, ConstructorChart } from './types'; import type Highcharts from 'highcharts/esm/highcharts'; @@ -58,6 +58,10 @@ export class HighchartsChartDirective { optional: true, }); + private readonly timeout = inject(HIGHCHARTS_TIMEOUT, { + optional: true, + }); + private readonly highchartsChartService = inject(HighchartsChartService); private readonly constructorChart = computed(() => { @@ -68,8 +72,13 @@ export class HighchartsChartDirective { return undefined; }); + private delay(ms: number): Promise { + return new Promise(resolve => setTimeout(resolve, ms)); + } + // Create the chart as soon as we can - private readonly chart = computed(() => { + private readonly chart = computed(async () => { + await this.delay(this.timeout ?? 500); return this.constructorChart()?.( this.el.nativeElement, // Use untracked, so we don't re-create new chart everytime options change @@ -82,14 +91,16 @@ export class HighchartsChartDirective { }); private keepChartUpToDate(): void { - effect(() => { + effect(async () => { + // Wait for the chart to be created this.update(); - this.chart()?.update(this.options(), true, this.oneToOne()); + const chart = await this.chart(); + chart?.update(this.options(), true, this.oneToOne()); }); } - private destroyChart(): void { - const chart = this.chart(); + private async destroyChart(): Promise { + const chart = await this.chart(); if (chart) { // #56 chart.destroy(); From 60638ac41a46eb9485d55a338762d42c67903792 Mon Sep 17 00:00:00 2001 From: Mohamed Ben Makhlouf Date: Sat, 6 Sep 2025 00:08:19 +0100 Subject: [PATCH 12/17] docs: update README with Highcharts module loading order and promise chain usage --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.md b/README.md index f258731..c8e5d87 100644 --- a/README.md +++ b/README.md @@ -412,6 +412,12 @@ export class StockComponent { } ``` +**Note:** +- Some Highcharts modules have dependencies and must be loaded in a specific order. +- In such cases, use a promise chain (e.g., `import('highcharts/esm/highcharts-more').then(() => import('highcharts/esm/modules/dumbbell'))`) +- instead of just listing them as array items. This ensures the dependent module loads only after its dependency. + + ### To load a wrapper A wrapper is a [custom extension](https://www.highcharts.com/docs/extending-highcharts/extending-highcharts) for Highcharts. To load a wrapper in the same way as a module, save it as a JavaScript file and add the following code to the beginning and end of the file: From 24c9e2695372702092c8d112f72e0dd6d8db1443 Mon Sep 17 00:00:00 2001 From: Mohamed Ben Makhlouf Date: Sat, 6 Sep 2025 00:10:28 +0100 Subject: [PATCH 13/17] test: add unit tests for HighchartsChartService module loading behavior --- README.md | 2 +- .../lib/highcharts-chart.component.spec.ts | 165 ++++++++++++++++++ .../src/lib/highcharts-chart.provider.ts | 4 +- 3 files changed, 168 insertions(+), 3 deletions(-) create mode 100644 highcharts-angular/src/lib/highcharts-chart.component.spec.ts diff --git a/README.md b/README.md index c8e5d87..e04cf05 100644 --- a/README.md +++ b/README.md @@ -413,11 +413,11 @@ export class StockComponent { ``` **Note:** + - Some Highcharts modules have dependencies and must be loaded in a specific order. - In such cases, use a promise chain (e.g., `import('highcharts/esm/highcharts-more').then(() => import('highcharts/esm/modules/dumbbell'))`) - instead of just listing them as array items. This ensures the dependent module loads only after its dependency. - ### To load a wrapper A wrapper is a [custom extension](https://www.highcharts.com/docs/extending-highcharts/extending-highcharts) for Highcharts. To load a wrapper in the same way as a module, save it as a JavaScript file and add the following code to the beginning and end of the file: diff --git a/highcharts-angular/src/lib/highcharts-chart.component.spec.ts b/highcharts-angular/src/lib/highcharts-chart.component.spec.ts new file mode 100644 index 0000000..b0d7182 --- /dev/null +++ b/highcharts-angular/src/lib/highcharts-chart.component.spec.ts @@ -0,0 +1,165 @@ +import { Component, ChangeDetectionStrategy } from '@angular/core'; +import { TestBed } from '@angular/core/testing'; +import type Highcharts from 'highcharts/esm/highcharts'; +import { HighchartsChartComponent } from './highcharts-chart.component'; +import { HighchartsChartService } from './highcharts-chart.service'; +import { provideHighcharts, providePartialHighcharts } from './highcharts-chart.provider'; +import { ModuleFactoryFunction } from './types'; + +/** + * Minimal host component to attach per-test module providers via TestBed.overrideComponent. + * We keep it simple to focus on DI and async loading behavior. + */ +@Component({ + selector: 'highcharts-test', + template: ``, + changeDetection: ChangeDetectionStrategy.OnPush, + imports: [HighchartsChartComponent], + standalone: true, +}) +class TestComponent {} + +describe('TestComponent / HighchartsChartService (load module)', () => { + beforeEach(async () => { + // 1) Register the standalone host + core Highcharts provider. + // - provideHighcharts(): should wire up HIGHCHARTS_LOADER, etc., so the service can load core Highcharts. + await TestBed.configureTestingModule({ + imports: [TestComponent], + providers: [provideHighcharts()], + }).compileComponents(); + }); + + it('resolves HighchartsChartService from the component injector', () => { + const fixture = TestBed.createComponent(TestComponent); + fixture.detectChanges(); + + // 2) Resolve the service from the component’s injector to honor component-level provider overrides. + const service = fixture.debugElement.injector.get(HighchartsChartService); + expect(service).toBeTruthy(); + }); + + // --------------------------------------------------------------------------- + // Parameterized checks for different Highcharts modules. + // + // For each case, we: + // - override the component’s providers to supply the module list + // - create the fixture (activates providers) + // - wait for the microtasks/imports to settle (fixture.whenStable) + // - assert that the Highcharts instance contains the expected augmented APIs + // --------------------------------------------------------------------------- + + type Case = { + title: string; + modules: ModuleFactoryFunction; + assert(hc: any): void; + }; + + const CASES: Case[] = [ + { + title: 'map → exposes Highcharts.MapChart', + modules: () => [import('highcharts/esm/modules/map')], + assert: (hc: typeof Highcharts) => { + expect(hc.MapChart).toBeDefined(); + expect(typeof hc.MapChart).toBe('function'); + }, + }, + { + title: 'tilemap → exposes Highcharts.seriesTypes.tilemap', + modules: () => [import('highcharts/esm/modules/tilemap')], + assert: hc => { + expect(hc.seriesTypes?.tilemap).toBeDefined(); + }, + }, + { + title: 'highcharts-more → exposes arearange series + dumbbell → exposes Highcharts.seriesTypes.dumbbell', + modules: () => [import('highcharts/esm/highcharts-more').then(() => import('highcharts/esm/modules/dumbbell'))], + assert: hc => { + expect(hc.seriesTypes?.arearange).toBeDefined(); + expect(hc.seriesTypes?.dumbbell).toBeDefined(); + }, + }, + { + title: 'pattern-fill → adds SVGRenderer.addPattern', + modules: () => [import('highcharts/esm/modules/pattern-fill')], + assert: hc => { + // pattern-fill augments the renderer with addPattern utility + expect(typeof hc.SVGRenderer?.prototype?.addPattern).toBe('function'); + }, + }, + { + title: 'gantt → exposes Highcharts.ganttChart or Highcharts.GanttChart', + modules: () => [import('highcharts/esm/modules/gantt')], + assert: hc => { + // Some versions expose both; at least one must exist. + const ok = typeof hc.ganttChart === 'function' || typeof hc.GanttChart === 'function'; + expect(ok).toBeTrue(); + }, + }, + ]; + + for (const c of CASES) { + it(`attaches expected API when module is provided: ${c.title}`, async () => { + // 3) Provide the module(s) at the component level. This mirrors your real component’s + // `providers: [providePartialHighcharts({ modules: () => [...] })]`. + TestBed.overrideComponent(TestComponent, { + set: { providers: [providePartialHighcharts({ modules: c.modules })] }, + }); + + const fixture = TestBed.createComponent(TestComponent); + fixture.detectChanges(); + + // 4) Get the service from this component’s injector scope + const service = fixture.debugElement.injector.get(HighchartsChartService); + expect(service).toBeTruthy(); + + // 5) Wait for all async work to stabilize: + // - dynamic imports for modules + // - the service’s microtasks and internal timers (your working test already relies on whenStable) + await fixture.whenStable(); + + // 6) Read the Highcharts instance from the signal + const hc = service.highcharts(); + expect(hc).toBeTruthy(); + + // 7) Case-specific assertions for the module’s side effects + c.assert(hc); + }); + } + + it('can load multiple modules together (map + tilemap + more + dumbbell + pattern-fill + gantt)', async () => { + TestBed.overrideComponent(TestComponent, { + set: { + providers: [ + providePartialHighcharts({ + modules: () => [ + import('highcharts/esm/modules/map'), + import('highcharts/esm/modules/tilemap'), + import('highcharts/esm/modules/gantt'), + import('highcharts/esm/highcharts-more').then(() => import('highcharts/esm/modules/dumbbell')), + import('highcharts/esm/modules/pattern-fill'), + ], + }), + ], + }, + }); + + const fixture = TestBed.createComponent(TestComponent); + fixture.detectChanges(); + + const service = fixture.debugElement.injector.get(HighchartsChartService); + expect(service).toBeTruthy(); + + await fixture.whenStable(); + + const hc: any = service.highcharts(); + expect(hc).toBeTruthy(); + + // Consolidated assertions (quick smoke for “all together”) + expect(typeof hc.MapChart).toBe('function'); // map + expect(hc.seriesTypes?.tilemap).toBeDefined(); // tilemap + expect(hc.seriesTypes?.arearange).toBeDefined(); // highcharts-more + expect(hc.seriesTypes?.dumbbell).toBeDefined(); // dumbbell + expect(typeof hc.SVGRenderer?.prototype?.addPattern).toBe('function'); // pattern-fill + expect(typeof hc.ganttChart === 'function' || typeof hc.GanttChart === 'function').toBeTrue(); // gantt + }); +}); diff --git a/highcharts-angular/src/lib/highcharts-chart.provider.ts b/highcharts-angular/src/lib/highcharts-chart.provider.ts index 2283ecd..017e694 100644 --- a/highcharts-angular/src/lib/highcharts-chart.provider.ts +++ b/highcharts-angular/src/lib/highcharts-chart.provider.ts @@ -4,7 +4,7 @@ import { HIGHCHARTS_CONFIG, HIGHCHARTS_ROOT_MODULES, HIGHCHARTS_OPTIONS, - HIGHCHARTS_TIMEOUT + HIGHCHARTS_TIMEOUT, } from './highcharts-chart.token'; import { ModuleFactoryFunction, HighchartsConfig, PartialHighchartsConfig, InstanceFactoryFunction } from './types'; import type Highcharts from 'highcharts/esm/highcharts'; @@ -38,7 +38,7 @@ export function provideHighcharts(config: HighchartsConfig = {}): EnvironmentPro const providers: (Provider | EnvironmentProviders)[] = [ provideHighchartsInstance(config.instance), provideHighchartsRootModules(config.modules ?? emptyModuleFactoryFunction), - { provide: HIGHCHARTS_TIMEOUT, useValue: config.timeout}, + { provide: HIGHCHARTS_TIMEOUT, useValue: config.timeout }, ]; if (config.options) { providers.push(provideHighchartsOptions(config.options)); From 58509a32ccf6ffb7da43590e1cf44cde3c679a90 Mon Sep 17 00:00:00 2001 From: Mohamed Ben Makhlouf Date: Sat, 6 Sep 2025 00:12:15 +0100 Subject: [PATCH 14/17] test: reduce timeout duration in HighchartsChartService tests --- highcharts-angular/src/lib/highcharts-chart.service.spec.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/highcharts-angular/src/lib/highcharts-chart.service.spec.ts b/highcharts-angular/src/lib/highcharts-chart.service.spec.ts index 29d4b2c..67e692e 100644 --- a/highcharts-angular/src/lib/highcharts-chart.service.spec.ts +++ b/highcharts-angular/src/lib/highcharts-chart.service.spec.ts @@ -39,14 +39,14 @@ describe('HighchartsChartService', () => { expect(service.highcharts()).toBeNull(); service.load(null); - tick(300); // Simulate the passage of time for the timeout in the load method + tick(100); // Simulate the passage of time for the timeout in the load method expect(service.highcharts()).toBe(mockLoader); // The Highcharts object should be emitted })); it('should call setOptions with global options if provided', fakeAsync(() => { service.load(null); - tick(300); // Simulate the passage of time for the timeout in the load method + tick(100); // Simulate the passage of time for the timeout in the load method // Check if setOptions was called with the global options expect(mockLoader.setOptions).toHaveBeenCalledWith(mockGlobalOptions); From 0382051d14061be0a3556f3bdccc40e8d4a878c6 Mon Sep 17 00:00:00 2001 From: Mohamed Ben Makhlouf Date: Sat, 6 Sep 2025 00:19:12 +0100 Subject: [PATCH 15/17] feat: add configurable timeout for Highcharts module loading --- .../src/lib/highcharts-chart.component.spec.ts | 4 +++- .../src/lib/highcharts-chart.directive.ts | 2 +- highcharts-angular/src/lib/types.ts | 11 +++++------ src/app/dumbbell-chart/dumbbell-chart.component.ts | 1 + 4 files changed, 10 insertions(+), 8 deletions(-) diff --git a/highcharts-angular/src/lib/highcharts-chart.component.spec.ts b/highcharts-angular/src/lib/highcharts-chart.component.spec.ts index b0d7182..550f675 100644 --- a/highcharts-angular/src/lib/highcharts-chart.component.spec.ts +++ b/highcharts-angular/src/lib/highcharts-chart.component.spec.ts @@ -52,6 +52,7 @@ describe('TestComponent / HighchartsChartService (load module)', () => { title: string; modules: ModuleFactoryFunction; assert(hc: any): void; + timeout?: number; }; const CASES: Case[] = [ @@ -77,6 +78,7 @@ describe('TestComponent / HighchartsChartService (load module)', () => { expect(hc.seriesTypes?.arearange).toBeDefined(); expect(hc.seriesTypes?.dumbbell).toBeDefined(); }, + timeout: 2000 }, { title: 'pattern-fill → adds SVGRenderer.addPattern', @@ -102,7 +104,7 @@ describe('TestComponent / HighchartsChartService (load module)', () => { // 3) Provide the module(s) at the component level. This mirrors your real component’s // `providers: [providePartialHighcharts({ modules: () => [...] })]`. TestBed.overrideComponent(TestComponent, { - set: { providers: [providePartialHighcharts({ modules: c.modules })] }, + set: { providers: [providePartialHighcharts({ modules: c.modules, timeout: c.timeout })] }, }); const fixture = TestBed.createComponent(TestComponent); diff --git a/highcharts-angular/src/lib/highcharts-chart.directive.ts b/highcharts-angular/src/lib/highcharts-chart.directive.ts index efda875..20041aa 100644 --- a/highcharts-angular/src/lib/highcharts-chart.directive.ts +++ b/highcharts-angular/src/lib/highcharts-chart.directive.ts @@ -78,7 +78,7 @@ export class HighchartsChartDirective { // Create the chart as soon as we can private readonly chart = computed(async () => { - await this.delay(this.timeout ?? 500); + await this.delay(this.relativeConfig?.timeout ?? this.timeout ?? 500); return this.constructorChart()?.( this.el.nativeElement, // Use untracked, so we don't re-create new chart everytime options change diff --git a/highcharts-angular/src/lib/types.ts b/highcharts-angular/src/lib/types.ts index 6d50c84..df60bb5 100644 --- a/highcharts-angular/src/lib/types.ts +++ b/highcharts-angular/src/lib/types.ts @@ -21,6 +21,11 @@ export type PartialHighchartsConfig = { * Include Highcharts additional modules (e.g., exporting, accessibility) or custom themes */ modules?: ModuleFactoryFunction; + /** + * Timeout in milliseconds to wait for the Highcharts library to load + * Default is 500ms + */ + timeout?: number; }; export type HighchartsConfig = { @@ -33,10 +38,4 @@ export type HighchartsConfig = { * Global chart options applied across all charts */ options?: Highcharts.Options; - - /** - * Timeout in milliseconds to wait for the Highcharts library to load - * Default is 500ms - */ - timeout?: number; } & PartialHighchartsConfig; diff --git a/src/app/dumbbell-chart/dumbbell-chart.component.ts b/src/app/dumbbell-chart/dumbbell-chart.component.ts index 410490d..b09d299 100644 --- a/src/app/dumbbell-chart/dumbbell-chart.component.ts +++ b/src/app/dumbbell-chart/dumbbell-chart.component.ts @@ -16,6 +16,7 @@ import { providePartialHighcharts } from 'highcharts-angular'; import('highcharts/esm/modules/pattern-fill'), ]; }, + timeout: 900 }), ], changeDetection: ChangeDetectionStrategy.OnPush, From 6550751b54a030dcc6ce125469dbcea12e5cc31b Mon Sep 17 00:00:00 2001 From: Mohamed Ben Makhlouf Date: Sat, 6 Sep 2025 00:22:53 +0100 Subject: [PATCH 16/17] docs: add optional timeout configuration for module loading in README --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index e04cf05..903de63 100644 --- a/README.md +++ b/README.md @@ -331,6 +331,7 @@ import { HighchartsChartDirective } from 'highcharts-angular'; import('highcharts/esm/modules/exporting'), ]; }, + timeout: 900, // Optional: increase timeout for loading modules }), ], }) From 35fb36bef4b08609de5c6efe6bb0bf5be9465b9d Mon Sep 17 00:00:00 2001 From: Mohamed Ben Makhlouf Date: Sat, 6 Sep 2025 00:26:37 +0100 Subject: [PATCH 17/17] fix: add missing comma for timeout configuration in module loading --- highcharts-angular/src/lib/highcharts-chart.component.spec.ts | 2 +- src/app/dumbbell-chart/dumbbell-chart.component.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/highcharts-angular/src/lib/highcharts-chart.component.spec.ts b/highcharts-angular/src/lib/highcharts-chart.component.spec.ts index 550f675..2893315 100644 --- a/highcharts-angular/src/lib/highcharts-chart.component.spec.ts +++ b/highcharts-angular/src/lib/highcharts-chart.component.spec.ts @@ -78,7 +78,7 @@ describe('TestComponent / HighchartsChartService (load module)', () => { expect(hc.seriesTypes?.arearange).toBeDefined(); expect(hc.seriesTypes?.dumbbell).toBeDefined(); }, - timeout: 2000 + timeout: 2000, }, { title: 'pattern-fill → adds SVGRenderer.addPattern', diff --git a/src/app/dumbbell-chart/dumbbell-chart.component.ts b/src/app/dumbbell-chart/dumbbell-chart.component.ts index b09d299..5bde675 100644 --- a/src/app/dumbbell-chart/dumbbell-chart.component.ts +++ b/src/app/dumbbell-chart/dumbbell-chart.component.ts @@ -16,7 +16,7 @@ import { providePartialHighcharts } from 'highcharts-angular'; import('highcharts/esm/modules/pattern-fill'), ]; }, - timeout: 900 + timeout: 900, }), ], changeDetection: ChangeDetectionStrategy.OnPush,