Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Dynamically Import Highcharts #163

Open
Robinyo opened this issue Sep 23, 2019 · 16 comments
Open

Dynamically Import Highcharts #163

Robinyo opened this issue Sep 23, 2019 · 16 comments

Comments

@Robinyo
Copy link

Robinyo commented Sep 23, 2019

Requested feature description

Take advantage of ECMAScript's support for dynamic import() in order to load Highcharts dynamically at runtime.

As we're using Angular and the Angular CLI uses webpack, we can rely on webpack to replace calls to import() with its own dynamic loading function.

See: Dynamically Importing Highcharts

@KacperMadej
Copy link
Contributor

Hi @Robinyo

Thank you for suggesting the enhancement.

What will be required for the new feature to work?
I imagine that a type-less version of the component could do the trick, but maybe there are other requirements? The import of Highcharts in the wrapper component is purely for TS definitions, so without them the import could be removed.

BTW: Nice article!

@Robinyo
Copy link
Author

Robinyo commented Sep 25, 2019

Hi @KacperMadej,

-> What will be required for the new feature to work?

tsconfig.json:

"module": "esnext"

-> I imagine that a type-less version of the component could do the trick?

Yes, that's what I tried and its working for me.

At the start of the file you will notice the following import statement:

import * as Highcharts from 'highcharts';

However, as the HighchartsChartComponent accepts Highcharts as an @input() I commented out this line and I updated all references to Highcharts types to <any>.

-> The import of Highcharts in the wrapper component is purely for TS definitions, so without them the import could be removed.

Yes.

-> BTW: Nice article!

Thanks :)

Cheers
Rob

@kumar-tadepalli
Copy link

Any update on ETA?

@KacperMadej
Copy link
Contributor

Please vote up with your reaction ( 👍 ) on the opening comment of this issue thread to let us know how much the feature is requested. As far as I can tell right now, it looks like the changes would require creating another wrapper that will be a type-less version (when it comes to the Highcharts properties) to keep the code clean and clear.

@stevethemacguy
Copy link

@Robinyo, Does this remove the ability to use HighCharts' types? For example, would it still be possible to specify types when creating a chart?

legend: {
  enabled: false
} as LegendOptions,
title: {
  text: 'hello',
} as TitleOptions
...

@jdelaune
Copy link

+1

@alexmuch
Copy link

Any updates on this feature? It would be great if we could dynamically load natively. In OP's example it looks like to use this in a component, you have to make a separate component that acts as an override for highcharts-angular. It would be stellar if we didn't have to make this go-between.

@mateuszkornecki
Copy link
Contributor

@alexmuch
I can see more and more people voting for adding this functionality, and I can agree that it might be a pretty neat feature. However, we need to keep all available highcharts wrappers(highcharts-react-official, highcharts-vue...) consistent.
I will consult all possibilities with my colleagues responsible for other wrappers and post an update if anything changes regarding this feature.

@kumar-tadepalli
Copy link

Any update on this feature?

@mateuszkornecki
Copy link
Contributor

Unfortunately for now the solution posted by Robinyo is the only solution. #163 (comment)

@BregtDeLange
Copy link

Any updates on this one?

@PowerKiKi
Copy link

Original article by @Robinyo is now available at https://rob-ferguson.me/dynamically-importing-highcharts/

Has anybody succeeded in using Highcharts with ESM and proper typing (what I'd consider bare minimum for an Angular app), and also lazy-loading the entire Highchart library to avoid the hit on Initial Total size ?

I've been struggling with this issue on and off for months now. And everytime I only end up being frustrated by the state of this repos and lack of documentation for lazy loading the lib 😞

@mateuszkornecki has anything changed on your side in the meantime ?

@karolkolodziej
Copy link
Contributor

@PowerKiKi It is possible to import Highcharts as ESM- see the demo.

import Highcharts from 'highcharts/es-modules/masters/highcharts.src';

You are right we don't mention how to do it anywhere in the documentation. We will try to improve it and prepare a demo where we dynamically import Higcharts.

@PowerKiKi
Copy link

@karolkolodziej one thing that would really improve usage of this lib would be to leverage Angular DI via providers.

So in HighchartsChartComponent, instead of:

- @Input() Highcharts: typeof Highcharts | typeof HighchartsESM;

we would have an injectable, so something more like:

  constructor(
    private el: ElementRef,
    private _zone: NgZone // #75
+   @Inject(HIGHCHARTS) Highcharts: typeof HighchartsESM,
  ) {}

Notice how it only accepts HighchartsESM, and not Highcharts, because non-ESM must not be used anymore in Angular ecosystem.

Then we can provide HIGHCHARTS with a new built-in provider function provideHighcharts(). The provider function would accept arguments to configure optional features with ease in a tree-shakable way.

So final usage for end-user would look more like:

@Component({
    selector: 'app-chart',
    standalone: true,
    template: `<highcharts-chart [options]="options" />`,
    imports: [HighchartsChartComponent],
    providers: [
        provideHighcharts({
            // ... some configuration here, included which modules should be included
        }),
    ],
})
export class ChartComponent {
    public options = {}; // ... real options here

    public constructor(
        @Inject(HIGHCHARTS) Highcharts: typeof HighchartsESM, // Optionally, if we need to access some functions provided by Highcharts in our component
    ) {
        // not much to do...
    }
}

The fact that provideHighcharts() is used in the component itself, and not at the application level, makes it easy to be tree-shakable, as long as the route is lazy-loaded or the template is @defered. It is overall even simpler to use and more idiomatic to Angular.

This kind of approach is used in ng2-charts since v6 with success. I think it should be adopted here too, or some similar alternatives.

The only thing to figure out is how to write the new provideHighcharts() and its arguments to be ESM and tree-shakeable.

@karolkolodziej
Copy link
Contributor

karolkolodziej commented Jun 4, 2024

@PowerKiKi Thank you for all the valid points!
I like the idea of provideHighcharts! I'll try that then.

In the meantime please look at the repo where I use import() to load Highcharts dynamically (without using highcharts-angular ).

@PowerKiKi
Copy link

PowerKiKi commented Jul 31, 2024

I finally came up with a solution that is:

  • 100% ESM
  • 100% typed
  • lazy loading of Highcharts to avoid big impact on "Initial total" as reported by Angular
  • loading additional modules of Highcharts
  • using <highcharts-chart>

Most of the magic is in loadHighcharts(). That is a reusable function to lazy-load Highcharts. It should typically be used when initializing the component. And the usage of <highcharts-chart> should not be shown until it is loaded. That is easily done via a @if in the template.

This is the kind of thing that I would like to have in the documentation, as the first (and only?) example of usage.

import {Component} from '@angular/core';
import type HighchartsESM from 'highcharts/es-modules/masters/highcharts.src';
import type {Options} from 'highcharts';
import {HighchartsChartModule} from 'highcharts-angular';

let highcharts: typeof HighchartsESM | null = null;

async function loadHighcharts(): Promise<typeof HighchartsESM> {
    if (!highcharts) {
        // First load core
        const core = await import('highcharts/es-modules/masters/highcharts.src').then(m => m.default);
        highcharts = core;

        // Then load all modules in parallel, and in an unspecified order
        await Promise.all([
            import('highcharts/es-modules/masters/highcharts-more.src'),
            import('highcharts/es-modules/masters/modules/exporting.src'),
            import('highcharts/es-modules/masters/modules/pattern-fill.src'),
            import('highcharts/es-modules/masters/modules/gantt.src'),
        ]);

        // Set some global options
        core.setOptions({
            lang: {
                months: [
                    $localize`Janvier`,
                    $localize`Février`,
                    $localize`Mars`,
                    $localize`Avril`,
                    $localize`Mai`,
                    $localize`Juin`,
                    $localize`Juillet`,
                    $localize`Août`,
                    $localize`Septembre`,
                    $localize`Octobre`,
                    $localize`Novembre`,
                    $localize`Décembre`,
                ],
                weekdays: [
                    $localize`Dimanche`,
                    $localize`Lundi`,
                    $localize`Mardi`,
                    $localize`Mercredi`,
                    $localize`Jeudi`,
                    $localize`Vendredi`,
                    $localize`Samedi`,
                ],
                rangeSelectorFrom: $localize`De`,
                rangeSelectorTo: $localize`À`,
                rangeSelectorZoom: $localize`Zoom`,
            },
        });
    }

    return highcharts;
}

@Component({
    selector: 'app-chart',
    template: `
        @if (Highcharts) {
            <highcharts-chart
                    [Highcharts]="Highcharts"
                    [oneToOne]="true"
                    [options]="options"
            />
        } @else {
            <div>Loading...</div>
        }
    `,
    standalone: true,
    imports: [HighchartsChartModule],
})
export class ChartComponent {

    public Highcharts: typeof HighchartsESM | null = null;

    public options: Options = {
        chart: {
            renderTo: 'container',
        },
        title: {
            text: 'My chart',
        },
        series: [
            {
                type: 'line',
                data: [1, 2, 3, 4, 5],
            },
        ],
    };

    public constructor() {
        loadHighcharts().then(Highcharts => (this.Highcharts = Highcharts));
    }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

10 participants