NativeScript / nativescript-ui-charts

NativeScript wrapper around HiCharts library
Apache License 2.0
26 stars 6 forks source link

Chart randomly not rendering on IOS #28

Closed cjohn001 closed 3 years ago

cjohn001 commented 3 years ago

I have a weird problem with the library on IOS. I display a chart on a page. Then I leave this page and the page should be destroyed. On another page, I than delete my data model and see the following output in the console:

CONSOLE LOG file: node_modules/@nativescript/ui-charts/ui-charts.ios.js:83:0 HighchartsViewDelegateImpl Did load chart: <HIChartView: 0x106544470; frame = (0 0; 343 198.333); hidden = YES; layer = <CALayer: 0x280f1fa80>> CONSOLE LOG file: node_modules/@nativescript/ui-charts/ui-charts.ios.js:83:0 HighchartsViewDelegateImpl Did load chart: <HIChartView: 0x106542520; frame = (0 0; 343 198.333); layer = <CALayer: 0x280f0bf20>> CONSOLE LOG file: node_modules/@nativescript/ui-charts/ui-charts.ios.js:83:0 HighchartsViewDelegateImpl Did load chart: <HIChartView: 0x106546570; frame = (0 0; 343 198.333); layer = <CALayer: 0x280f2d2a0>> CONSOLE LOG file: node_modules/@nativescript/ui-charts/ui-charts.ios.js:83:0 HighchartsViewDelegateImpl Did load chart: <HIChartView: 0x106472850; frame = (0 0; 343 198.333); layer = <CALayer: 0x280f58040>> CONSOLE LOG file: node_modules/@nativescript/ui-charts/ui-charts.ios.js:83:0 HighchartsViewDelegateImpl Did load chart: <HIChartView: 0x106544470; frame = (0 0; 343 198.333); hidden = YES; layer = <CALayer: 0x280f1fa80>> CONSOLE LOG file: node_modules/@nativescript/ui-charts/ui-charts.ios.js:83:0 HighchartsViewDelegateImpl Did load chart: <HIChartView: 0x106542520; frame = (0 0; 343 198.333); layer = <CALayer: 0x280f0bf20>> CONSOLE LOG file: node_modules/@nativescript/ui-charts/ui-charts.ios.js:83:0 HighchartsViewDelegateImpl Did load chart: <HIChartView: 0x106472850; frame = (0 0; 343 198.333); layer = <CALayer: 0x280f58040>> CONSOLE LOG file: node_modules/@nativescript/ui-charts/ui-charts.ios.js:83:0 HighchartsViewDelegateImpl Did load chart: <HIChartView: 0x106546570; frame = (0 0; 343 198.333); layer = <CALayer: 0x280f2d2a0>>

When I visit the page with the chart afterwords, in very rare situations, maybe each 10th time, no chart, which should be display, on the page shows up (no content at all, nothing rendered). I checked the chart options that are provided and they are ok. The situation looks very weird to me. Seems like when the page with the chart is destroyed, the chart itself is not destroyed. Otherwise, I should not see the output above. Is there maybe a deinitialisation step missing in the plugin?

As the reset of the data model likely results in the runtime freeing memory where the chart content is located, I assume the output above is a consequence of it which in rare cases also breaks the first chart which shall be displayed and is shown after the reset.

Any ideas how this could be fixed?

cjohn001 commented 3 years ago

@shiv19: I am not sure if this is the source of the problem. Anyhow, calling destroy or reload does not help. But it might be, that the problem is related to angular change detection. When my app reloads content, then potentially the options update for the highcharts view, does not lay within the page transition cycle. When I set a red background color to the UIChartsView, I see that in cases where the chart is not drawn, the red background is given. Hence, there are only two options.

  1. the highcharts library does not draw the chart due to a bug
  2. the view is not rendered again due to angular change detection not acting. I tried with onpush method, but also does not help. However, not sure if I did it right :(

In the past version 0.4 of the lib, there was a property interface for Angular available to set the options. This would probably solve the issue. I think we should reintroduce it to the plugin. The current set and update options are. a nightmare to use with Angular. As the load events of the native controls are not in sync with the Angular component lifecycle one has to guess prety hard on the right setTimeout parameters. @shiv19: Are the 0.4 sources still available anywhere. I would like to look how the Angular bindings were implemented in the past. Thx.

shiv19 commented 3 years ago

The 0.4 sources should be available in the same repo of you go back in the commit history. I'll double check that the chart view implements the unload method correctly once I get to work today.

cjohn001 commented 3 years ago

Hello @shiv19 , in the meantime I think my problem is not directly arising from the plugin itself, but from the way I try to use it. When my component below is triggered from an input binding after the page has been display, the chart is not rendered. Attached you see my try to implement a generic wrapper around the plugin. Unfortunately as the chart loaded event and the Angular component lifecycle are not in sync, I tried to use a setInterval or as in the code below a setTimeout in order to enforce setting of the options. However, I cannot get change detection working on the component. Already tried with onpush and the default detection here. However, I cannot get it working properly. Have you an idea what I am doing wrong? Having an Angular binding for options and maybe a second one for switching between setOptions and updateOptions would realy be nice to have for Angular.

<UIChartsView [width]="width" [height]="height" [translateX]="translateX" [translateY]="translateY" [rotate]="rotate"
    (loaded)="chartLoaded($event)" class="chart-view">
</UIChartsView>
@Component({
    selector: 'ns-chart-view',
    templateUrl: './chart-view.component.html',
    styleUrls: ['chart-view-common.scss']
})
export class ChartViewComponent implements OnChanges, OnDestroy {
    private _timeout = null;
    private _chartInitialized = false;
    private _chartLoaded = false;
    private _chart: UIChartsView = null;
    @Input() _chartOpts: any = null;
    @Input() _replaceMode = false;
    @Input() rotate = 0;
    @Input() width = '100%';
    @Input() height = '100%';
    @Input() translateX = 0;
    @Input() translateY = 0;
    //////////////////////////////////////////////////////////////////////////////////////////////////
    constructor(private ngZone: NgZone, private cdr: ChangeDetectorRef) {}
    //////////////////////////////////////////////////////////////////////////////////////////////////
    ngOnDestroy() {
        if (this._timeout) {
            clearTimeout(this._timeout);
            this._timeout = null;
        }
    }
    //////////////////////////////////////////////////////////////////////////////////////////////////
    ngOnChanges(changes: SimpleChanges) {
        for (const propName in changes) {
            if (changes.hasOwnProperty(propName)) {
                switch (propName) {
                    case '_chartOpts':
                        if (changes._chartOpts.currentValue) {
                            this._chartOpts = Object.assign({}, changes._chartOpts.currentValue);
                            if (this._chartLoaded) {
                                this.updateChart();
                            } else {
                                const updateFunc = () => {
                                    console.log('inTimeout');
                                    if (this._chartLoaded) {
                                        this.updateChart();
                                    } else {
                                        this._timeout = setTimeout(updateFunc, 200);
                                    }
                                };

                                if (!this._timeout) {
                                    this._timeout = setTimeout(updateFunc, 200);
                                }
                            }
                        }
                        break;
                }
            }
        }
    }
    //////////////////////////////////////////////////////////////////////////////////////////////////
    private updateChart() {
        if (this._chartOpts) {
            this.ngZone.run(() => {
                if (this._chartInitialized) {
                    if (this._replaceMode) {
                        console.log('replace mode set options ');
                        this._chart.setOptions(this._chartOpts);
                    } else {
                        console.log('update options');
                        this._chart.updateOptions(this._chartOpts);
                    }
                } else {
                    console.log('set options');
                    this._chart.setOptions(this._chartOpts);
                    this._chartInitialized = true;
                }
            });
        }
    }
    //////////////////////////////////////////////////////////////////////////////////////////////////
    public chartLoaded(args: EventData) {
        this._chart = args.object as UIChartsView;
        const momentLocalized = moment().locale(DeviceInfo.locale());
        this._chart.setLangOptions({
            months: momentLocalized.localeData().months(),
            weekdays: momentLocalized.localeData().weekdays(),
            shortMonths: momentLocalized.localeData().monthsShort(),
            resetZoom: localize('general.strResetZoom')
        });
        console.log('loaded event fired');
        this._chartLoaded = true;
    }
}
cjohn001 commented 3 years ago

@shiv19 : I was now able to resolve the issue. It is not directly related to the plugin, but related to Angular not rendering the view after update. The following solution now works on IOS and Android. I comment it here, in hope to help others experiencing the same issue. I think it would be very helpful if you could bring in Angular interface again. As a matter of principle it would be good to have both, setOptions and updateOptions available in the interface. as shown in the code below. setOptions for setup of new charts in the same view and updateOptions for updating chart data in the same chart.

<UIChartsView [width]="width" [height]="height" [translateX]="translateX" [translateY]="translateY" [rotate]="rotate"
    (loaded)="chartLoaded($event)" class="chart-view">
</UIChartsView>
@Component({
    selector: 'ns-chart-view',
    templateUrl: './chart-view.component.html',
    styleUrls: ['chart-view-common.scss']
})
export class ChartViewComponent implements OnInit, OnChanges, OnDestroy {
    private _chartOpts$: BehaviorSubject<any>;
    private _timeout = null;
    private _chartInitialized = false;
    private _chartLoaded = false;
    private _chart: UIChartsView = null;
    @Input() _chartOpts: any = null;
    @Input() _replaceMode = false;
    @Input() rotate = 0;
    @Input() width = '100%';
    @Input() height = '100%';
    @Input() translateX = 0;
    @Input() translateY = 0;
    //////////////////////////////////////////////////////////////////////////////////////////////////
    constructor(private ngZone: NgZone) {
        this._chartOpts$ = new BehaviorSubject<any>(this._chartOpts);
    }
    //////////////////////////////////////////////////////////////////////////////////////////////////
    ngOnInit() {
        this._chartOpts$.subscribe(chartOpts => {
            if (chartOpts) {
                this._chartOpts = chartOpts;
                if (this._chartLoaded) {
                    if (!this._timeout) {
                        this._timeout = setTimeout(() => {
                            if (this._chartInitialized) {
                                if (this._replaceMode) {
                                    this.ngZone.run(() => {
                                        this._chart.setOptions(this._chartOpts);
                                    });
                                } else {
                                    this.ngZone.run(() => {
                                        this._chart.updateOptions(this._chartOpts);
                                    });
                                }
                            } else {
                                this.ngZone.run(() => {
                                    this._chart.setOptions(this._chartOpts);
                                });
                                this._chartInitialized = true;
                            }
                            clearTimeout(this._timeout);
                            this._timeout = null;
                        }, 1);
                    }
                } else {
                    if (!this._timeout) {
                        this._timeout = setTimeout(() => {
                            clearTimeout(this._timeout);
                            this._timeout = null;
                            this._chartOpts$.next(chartOpts);
                        }, 100);
                    }
                }
            }
        });
    }
    //////////////////////////////////////////////////////////////////////////////////////////////////
    ngOnChanges(changes: SimpleChanges) {
        for (const propName in changes) {
            if (changes.hasOwnProperty(propName)) {
                switch (propName) {
                    case '_chartOpts':
                        if (changes._chartOpts.currentValue) {
                            this._chartOpts$.next(Object.assign({}, changes._chartOpts.currentValue));
                        }
                        break;
                }
            }
        }
    }
    //////////////////////////////////////////////////////////////////////////////////////////////////
    ngOnDestroy() {
        if (this._timeout) {
            clearTimeout(this._timeout);
            this._timeout = null;
        }
    }
    //////////////////////////////////////////////////////////////////////////////////////////////////
    public chartLoaded(args: EventData) {
        this._chart = args.object as UIChartsView;
        const momentLocalized = moment().locale(DeviceInfo.locale());
        this._chart.setLangOptions({
            months: momentLocalized.localeData().months(),
            weekdays: momentLocalized.localeData().weekdays(),
            shortMonths: momentLocalized.localeData().monthsShort(),
            resetZoom: localize('general.strResetZoom')
        });
        this._chartLoaded = true;
    }
}