Ng2-charts: Calling update to refresh the chart.

Created on 5 Jul 2016  路  45Comments  路  Source: valor-software/ng2-charts

How do you go about updating the chart? After getting the chart to display, I changed the color of points to red if they are clicked. If the user clicks multiple points, then the older points go back to their original color. However, it only goes back if I hover my mouse over the old point. I would like this behavior to be automatic - I would imagine that calling update on the chart would work, but I get the error:

TypeError: this.chartObject.update is not a function...

Any ideas on how to do this with or without calling update?

Most helpful comment

This looks like the closest topics, and I don't want to open a new issue since mine's similar.

I was having some serious problems getting the chart to properly initialize, and none of these solutions worked straight forward (data was being loaded async, but the chart would draw before the process finished and wouldn't re-render).

A quick, easy, solution is to just wait to draw the canvas until your asyncs are finished. In your component:

isDataAvailable:boolean = false;
ngOnInit() {
    asyncFnWithCallback(()=>{ this.isDataAvailable = true});
}

and wrap your entire template with:

<div *ngIf="isDataAvailable">
. . . chart canvas + any other template code  . . .
</div>

Spend several hours testing other workarounds, and thought this might help someone (it also seems a lot more straight-forward w/o need for adding any new imports). It's not a solution for later updates, but it's definitely a solution for a lot of the people that seem to have stumbled on this and the similar topics posted above that just want their chart initialized properly.

All 45 comments

Well, in my case i just change the values of the data and it updates by itself, however idk if it works the same for those components.

i try to push values to my dataset and label arrays, but no refresh is triggered.
Current workaround for me:

@ViewChild(BaseChartComponent) private _chart;
_chart.ngOnChanges()

@michaelknoch I am having the same problem, however I can't seem to implement your workaround for it to work for me. Did you set the _chart.ngOnChanges() within some interval?

in my workaround, _chart.ngOnChanges() is called after data has changed. Think of $scope.$apply from angular 1. @jrmoser

@michaelknoch's workaround worked for me when adding data to the chart. ...going to test this on the chart point colors later

@michaelknoch Thanks, I ended up putting it inside of an ngDoCheck... probably not the best for optimization, but it's what is working for now

hmm, doesn't work when changing point attributes like size and color - seems like calling ngOnChanges resets all points to their default style

in my case I'm trying to display a realtime chart. So I'd like to update chart whenever a new data point is available.

Workaround suggested by @michaelknoch indeed refreshes chart, but it re-draws the whole chart rather than just adding a new data-point making it very hard to understand that it's a realtime data.

I know chart.js is capable of this functionality. Even this older example shows that it's possible. I just don't know how to achieve this via this wrapper.

This is because Angular 2 Change Detection only runs when value or reference changes.

So you can manually trigger the change detection like the ways provided in this post, or give a different reference.

This similar issue may help you understand: https://github.com/valor-software/ng2-charts/issues/378

For these having an issue with updating chart in realtime, this is actually an issue with charts.js itself. In v2 they removed these helpers that allowed to do this in v1. See issue 1997

@DmitryEfimenko cool, glad you find the issue. Thanks for help. I was actually writing you to let you create a new feature request.

Now I can close this I guess.

I think maybe we can provide a more friendly way to refresh the chart? @valorkin

@Hongbo-Miao @valorkin Now, if the data change, chart change more troublesome.I suggest make a new API to update chart when datasets/labels changed

Yeah, the suggested fix works, but it is really hacky. An update method would be nice and clean. I ended up with the following for a one-time async data load:

import {SimpleChanges} from '@angular/core';

// ... all the things ...

someFunction() {
  // ... get data into chartLabels / chartDatasets
  this.chartComponent.ngOnChanges({} as SimpleChanges);
}

I found that using the public chart attribute of the BaseChartDirective is a clean way to have an update method.

@ViewChild(BaseChartDirective)
public chart: BaseChartDirective;

    ...
    this.chart.chart.update();
    ...

This looks like the closest topics, and I don't want to open a new issue since mine's similar.

I was having some serious problems getting the chart to properly initialize, and none of these solutions worked straight forward (data was being loaded async, but the chart would draw before the process finished and wouldn't re-render).

A quick, easy, solution is to just wait to draw the canvas until your asyncs are finished. In your component:

isDataAvailable:boolean = false;
ngOnInit() {
    asyncFnWithCallback(()=>{ this.isDataAvailable = true});
}

and wrap your entire template with:

<div *ngIf="isDataAvailable">
. . . chart canvas + any other template code  . . .
</div>

Spend several hours testing other workarounds, and thought this might help someone (it also seems a lot more straight-forward w/o need for adding any new imports). It's not a solution for later updates, but it's definitely a solution for a lot of the people that seem to have stumbled on this and the similar topics posted above that just want their chart initialized properly.

@zbagley In my case it did update when i change the data, however the animation restarts so it's like if the animation cuts in the middle of it and starts all over, looks ugly.

I did though something like your solution, but i had to revert to previous scenario, in my case, being an Ionic 2 app (mobile) not rendering anything for a few seconds (0.5 - 2 depending on hardware) would make the app look unresponsive, i prefer it to chop the animation instead, at least it looks like it's doing something.

I handle the issue by implementing:
@ViewChild(BaseChartDirective) private _chart;
this._chart.chart.update();

However, i cannot update another chart. I need to update two different line charts as data comes in. Only one of them works.

@zbagley your solution worked for me. Thanks a ton for that. I've been stuck for 2 days straight because I couldn't asynchronously update value in datasets of my bar chart. Now it works like a charm!

@zbagley 's solution is a pretty standard way to do this kind of thing. But the library should have some async functionality.

Fyi, it appears that under some circumstances (multiple charts), the solution using .ngOnChanges() calls breaks with an error during the resize operation:

Uncaught TypeError: Cannot read property 'parentNode' of null

However the solution from @tfellner-tgm works without errors, even in the latest (1.5.0)

Calling this.baseChart.ngOnChanges({} as SimpleChanges) redraws the whole chart when no data nor datasets are found in changes object (see ngOnChanges implementation here).

I do the following to update data asynchronously. It updates the chart lines according to new data without redrawing the whole chart. Animation works correctly:

// compute new data and store in this.datasets
this.datasets[0].data = computeSomething();

// once new data is computed and datasets are updated, tell our baseChart the datasets changed
this.baseChart.ngOnChanges({
    datasets: {
        currentValue: this.datasets,
        previousValue: null,
        firstChange: false,
        isFirstChange: () => false
    }
});

@nlaplante, thx for this hint. it almost works perfectly for me.

But there remains one single issue: if I push new values the animation (curve) works as it should, but if I shift the first element the animation does not work. The chart does not fully rerender, but instead of showing the animation to move the new first value from position 1 to position 0 to the axis it shows a white area until the new first value is at position 0.

The ng-version does not work:
http://plnkr.co/edit/y0gKnQrv3PlIW5NgmXm0?p=preview

The normal chart.js versions shows the right animation:
https://plnkr.co/edit/rNcSj13xTSwPBJaRCaV6?p=preview

Here is a solution for the wrong animation:
http://plnkr.co/edit/PLp8H1HnmtpiU2qLREKe?p=preview

You need to create a new data array (not a reference to the present data array only). There you can create, delete, modify values. The new array needs to be assigned to the dataset so that the animation works correctly.

```
let newData = this.datasets[0].data.slice();
newData.shift();
newData.push(random);

this.datasets = [{
label: "Labelname",
data: newData
}];

True, the solution is to modify the current array (labels/values) reference (push/slice, etc...) and then call chart.update(). Then the chart will update properly without triggering the whole animation.

On the other hand if you reassign values to the arrays like chartValue = newcChartValues it will indeed restart the animation and.. bam! Ugly effect !

@adadgio, as described above (including example) I experienced the opposite:

  • modifying the array leads to the wrong/ugly animation.
  • reassigning a new array leads to a correct animation.

That's strange, i have the latest version and the behavior is exactly what i described... hmmm.

@adadgio, yes it is indeed.
Though the Plunker example uses version 1.5.0 of ng2-charts, version 1.6.0 does not bring any changes for me.

If I use chart.update() as suggested by you, the animation for the removed first item does not work.
If I use chart.ngOnChanges({} as SimpleChanges) the whole chart redraws.

As several issues deal with change management in ng2-charts and related to this with correctly working animations, it would be helpful if you share your code. Maybe we can find a better alternative than mine. This would be interesting to me.

All right, i'm going to post a bit of raw code because i don't have much time, sorry.

So here you have: a working example of pushing data that succesfuly redraws the chart when updated (ie. chart does redraw only last values correctly without triggering the whole animation).

My angular2 app is a very simple bare bone app with just an app component and using "ng2-charts": "^1.6.0".

I created a wrapper component for the chart just to have clean code, this is what i am going to post here. It basically fetches values from a remote file (you can even test with the IP given in the code as such).

Note i am not using ngOnChanges here or anything, just pushing into chartData.

app.component.html

<!-- dont mind input here, irrelevant for this issue, its just to know which remote sensor values to draw from the logs file -->
<chart-component flex-fill [name]="'Soil moisture'" [showValsOf]="'sh'"></chart-component>

chart.component.html

Info: I am using this component 3 times for 3 charts, thus a few if in the following component depending on what values i want to render. But whatever.

<canvas baseChart #chart
        [chartType]="'line'"
        [options]="chartOptions"
        [labels]="chartLabels"
        [colors]="chartColors"
        [datasets]="chartData"
    ></canvas>

chart.component.ts

What this component does is fetch data via GET request and parse multiple sensor values, remove and format some data and push the relevant data into chartData (see method refresh and end of method parseChartDataFromLogs for the relevant parts).

import * as moment from 'moment';
import { Http }                 from '@angular/http';
import { Component, OnInit }    from '@angular/core';
import { ViewChild }            from '@angular/core';
import { Input }                from '@angular/core';
import { BaseChartDirective }   from 'ng2-charts/ng2-charts';

const CHART_POINT_EVERY_X_MIN = 3;

@Component({
    selector: 'chart-component',
    templateUrl: './chart.component.html',
    styleUrls: ['./chart.component.scss']
})
export class ChartComponent implements OnInit {
    @ViewChild(BaseChartDirective) chart: BaseChartDirective;
    @Input('name') name: string = null;

    // @docs-A01
    // the logs contains all sensor data such as
    // Wed Aug 23 2017 06:50:02 GMT+0000 (UTC)| 563;50;24
    // the 563;50;24 is soil humidity, air humidity and air temperature
    // by convention (also named "sh", "ah" and "at" in arduino)
    @Input('showValsOf') showValsOf: string = null;

    // history hold previous data when user opened the browser
    // so the chart does not redraw every time (prevent setting a whole new chartData(s)...)
    private _dataHistory: any = { "sh": {}, "ah": {}, "at": {} };
    private _showValsOfMapIndex: any = { "sh": 0, "ah": 1, "at": 2 };

    chartLabels: Array<any> = [];

    chartData: Array<any> = [
        {
            data: [],
            label: null,
        }
    ];

    chartColors: Array<any> = [
        {
            pointRadius: 0,
            pointBorderWidth: 0,
            pointBorderColor: '#123752',
            pointBackgroundColor: '#123752',
            borderColor: '#123752',
            backgroundColor: 'rgba(112, 142, 164, 0.7)',
            fill: true,
        }
    ];

    chartOptions: any = {
        legend: {
            labels: {
                fontColor: '#fff',
            }
        },
        scales: {
            xAxes: [{
                display: true,
                gridLines: {
                    display: true,
                },
                ticks: {
                  fontColor: '#fff',
                },
            }],
            yAxes: [{
                display: true,
                gridLines: {
                    display: true,
                },
                ticks: {
                    // min: -10,
                    // max: 40,
                    fontColor: '#fff',
                    callback: (originalValue) => {
                        // depending on the chart to show for each sensor, i want ticks labels to 
                        // have celcius, precentage, or no unit at all
                        if (this.showValsOf === 'at') {
                            return `${originalValue}掳`;
                        }聽else if (this.showValsOf === 'ah') {
                            return `${originalValue}%`;
                        } else {
                            return originalValue;
                        }
                    }
                },
            }],
        }
    };

    constructor(private http: Http)
    {

    }

    ngOnInit()
    {
        this.chartData[0].label = this.name;

        // bug/ng2-chart or angular, its not updated here
        // but strangely works from ngOnInit (and not ngAfterViewInit !!)...

        // min and max values differ if mode is "sh", "ah" or "at"
        // configure different axis scales and other colors
        // depending on the graph we would like to show
        switch(this.showValsOf) {
            case 'sh':
                // soil humidity is a value between 100, theoritically 0 and 1024 (with 5v, othervise 600 or 800 i think)
                this.chartOptions.scales.yAxes[0].ticks.min = 0;
                this.chartOptions.scales.yAxes[0].ticks.max = 1050;
                this.chartOptions.scales.yAxes[0].ticks.maxTicksLimit = 5;
                this.chartOptions.scales.yAxes[0].ticks.stepSize = ((1050-0)/5); // 210
            break;
            case 'ah':
                // air humidity is in percentages
                this.chartOptions.scales.yAxes[0].ticks.min = 0;
                this.chartOptions.scales.yAxes[0].ticks.max = 120;
                this.chartOptions.scales.yAxes[0].ticks.maxTicksLimit = 8;
                this.chartOptions.scales.yAxes[0].ticks.stepSize = 20;
            break;
            case 'at':
                // air temperature is in C掳 
                this.chartOptions.scales.yAxes[0].ticks.min = -20;
                this.chartOptions.scales.yAxes[0].ticks.max = 60;
                this.chartColors[0].borderColor = '#da3b15';
                this.chartColors[0].fill = false;
                this.chartOptions.scales.yAxes[0].ticks.maxTicksLimit = 8;
                this.chartOptions.scales.yAxes[0].ticks.stepSize = 10;
            break;
            default:
                // noooothing to do here!
            break;
        }
    }

    ngAfterViewInit()
    {

    }

    refresh()
    {
        const url = '';
        const req = this.http.get('http://37.139.14.78:1880/api/garden/logs').toPromise();

        req.then((res: any) => {
            this.parseChartDataFromLogs(res._body);
        }).catch(e => {
            console.log(e);
        });
    }

    private parseChartDataFromLogs(data: any)
    {
        let prevTime = null;
        const lines = data.split("\n");

       //for chart one, two or three, i need to know wich sensor vals to draw (values from "sh", "ah", or "at" ?
        const valIndex = this._showValsOfMapIndex[this.showValsOf];

        for (let line of lines) {
            // skip empty lines
            if (line === "") { continue; }

            const parts = line.split("| ");
            if (parts.length !== 2) { continue; }

            const date = moment(parts[0].trim()).utc();
            const values = parts[1].split(";");
            // const sh = parseInt(values[0]);
            // const ah = parseInt(values[1]);
            // const at = parseInt(values[2]);
            // see documentation at @docs-A01
            const pointValue = parseInt(values[valIndex]);

            const label = date.format("hh:mm"); // "dd Mo, hh:mm"
            const uuid = date.toString();

            // skip some value, and lets just use a tick every 30 seconds
            const timestamp = date.valueOf();
            if (prevTime === null) { prevTime = timestamp; }

            if (timestamp - prevTime < 1000*60*CHART_POINT_EVERY_X_MIN) {
                continue;
            } else {
                prevTime = timestamp;
            }

            // if this is already in the data history...
            if (typeof this._dataHistory[this.showValsOf][uuid] === 'undefined') {

                // THIS IS THE RELEVANT PART FOR THIS ISSUE!
                this.chartData[0].data.push(pointValue);
                this.chartLabels.push(label);
                this._dataHistory[this.showValsOf][uuid] = true; // and push bool to data history (point exists)
            }
        }

        // i DO CALL chart update...
        this.chart.chart.update();
    }
}

Note: the use of _dataHistory is not a hacky part nor relevant for this issue, its just here to help me skip values already parsed inside my foreach data point loop{...} just because i fetch the entire logs file at every http request and not the last values only (which is highly inefficient - i agree - but does not really matter for this example... ;-) ).

@adadgio, thanks for sharing your code.
I had to fix same issues before it was able to run (e.g. the API result and parseChartDataFromLogs(data: any) did not fit).

I don't know how and when refresh() gets called resp. how many values are emitted. This is the relevant part for the questions about the working animation I was talking about above. If you do not remove values from the chart with shift(), than you can not reproduce the issue thus, as mentioned above, chart.update() can not cause a wrong animation behavior.

Yep sorry I changed the API this morning ! Refresh is only called when user clicks a button, or programmatically when i'm sure new data is there. Not very relevant either.

It works for me when I remove values. I'm going to pseudo code , but basicaly what i did was:

this.chartData[0].data.push(48);
this.chartLabels.push("hey");
this.chart.chart.update();

... works

this.chartData[0].data.shift()
this.chartLabels.shift()
this.chart.chart.update();

... also works.

If you would like to investigate further, here is the full, updated code as up today (look at the same files). You should even be able to view the graph in real time if you run the app.

https://github.com/adadgio/angular-garden

No problem - understandable as you are working on this project. 馃槂

To make it clear: if I constantly add new values and therefore the number of data points increases everything is fine with chart.update().

But if I have a constant number of data values and remove the most left item with shift() as I push() a new one, then the animation works with the code I posted above only.

Also be aware that the animation for newly added values is different than for a constant number of values which move form the right to the left.

See:
http://plnkr.co/edit/PLp8H1HnmtpiU2qLREKe?p=preview

You can change the (click) call to (click)="clickData_WrongAnimation() to see the wrong animation behavior.

I see. And what animation did you expect from the clickData_WrongAnimation again ? Just going the other way ? (i have okay-ish animations any time on your plunker, the "wrong" one seems strange but at least it does not redraw...)

I expected to get another animation than the white area after the shift(). Would be good if the last value moves out of the scale.

My workaround solves this, but the values do not move from right to left, but instead each value transforms form the old to the new value, which is better than the white area, but not that good for a live chart that constantly gets new values.

I opened an issue in the charts.js repo: https://github.com/chartjs/Chart.js/issues/4695.

@katsuragi545 how did you change the color to red if they're clicked? Whenever I click mine, they only stay red for a second before going back to its default.. :(

EDIT
nevermind, I saw one of your other posts
https://github.com/chartjs/Chart.js/issues/2989

@zbagley This is great, don't know why I didn't think of this! *ngIf="chartData" on the chart wrapper and it's happy days!

This worked for me after trying a few of the above solutions with no luck: http://plnkr.co/edit/Ra6cfYkctsq3ZnqxTHYf?p=preview

There is another way to do it:

In your HTML you have

<canvas baseChart 
            [datasets]="ChartData"
            //...other stuff >
</canvas>

and in the component I have a function which update the chart with new data, and then I clone the datasets and re-assign it

drawChart(){
    this.ChartData=[{data: this.dataX, label: 'X'}]; // this.dataX has new values from some place in my code
    //nothing happened with my chart yet, until I add this lines:        
    let clone = JSON.parse(JSON.stringify(this.ChartData));
    this.ChartData=clone;
   //other stuff like labels etc.
}

this works for me, hope it works for you too

I am using angular 4, and this is what works for me
at the html

<canvas *ngIf="lineChartData.length" baseChart width="800" height="300"
                            [datasets]="lineChartData"
                            [labels]="lineChartLabels"
                            [options]="lineChartOptions"
                            [colors]="lineChartColors"
                            [legend]="lineChartLegend"
                            [chartType]="lineChartType"
                            (chartHover)="chartHovered($event)"
                            (chartClick)="chartClicked($event)"></canvas>

at the component.ts

export class ResultsGraphPanelComponent implements OnInit {
  @ViewChild(BaseChartDirective) public chart: BaseChartDirective;
   // other initializing component
}

function to populate the lineCharData Array (datasets)

buildCountingDataSet() {
       // code to populate lineCharData

        if (this.chart !== undefined && this.chart.chart !== undefined) {
        this.lineChartData.push({data: dataEnter, label: enterLabel});
        this.lineChartData.push({data: dataExit, label: exitLabel});

       // this is what actually update the chart 
        this.lineChartLabels = xLabels;
        this.chart.labels = xLabels;
        this.chart.chart.data.labels = xLabels;

        this.chart.chart.update();
      } else {
        this.lineChartData = [{data: dataEnter, label: enterLabel},
          {data: dataExit, label: exitLabel}];
        this.lineChartLabels = xLabels;
      }
}

With angular 4, I got it to work with the timeout method above:

import { BaseChartDirective } from '../ng2-charts';

...

@ViewChild(BaseChartDirective)
public chart: BaseChartDirective;

...

setTimeout(() => {
    this.chart.getChartBuilder(this.chart.ctx);
}, 10);

@zbagley Your code block from Mar 18, 2017 is epic. #wearenotworthy

[datasets]="ChartData"
//...other stuff >

and in the component I have a function which update the chart with new data, and then I clone the datasets and re-assign it

drawChart(){
this.ChartData=[{data: this.dataX, label: 'X'}]; // this.dataX has new values from some place in my code
//nothing happened with my chart yet, until I add this lines:
let clone = JSON.parse(JSON.stringify(this.ChartData));
this.ChartData=clone;
//other stuff like labels etc.
}

you can assing a value to ChartOptions even if it's the original one it dosen't matter

@zbagley
works for me!
tks

Was this page helpful?
0 / 5 - 0 ratings

Related issues

Adwind picture Adwind  路  3Comments

tushartgsit picture tushartgsit  路  5Comments

mrpotato3 picture mrpotato3  路  5Comments

RKornu picture RKornu  路  3Comments

egervari picture egervari  路  4Comments