My Goal:
I'm trying to make my timeline (with horizontal rectangles) timestamp-dependent and wondering how to move forward. In the future, the user of the application will have an option to select a start date. Let's say if start date is 04/01/2021 12:00 am, then I want the Text Timeline to be divided into 30 days or less based on how it looks on the UI. This functionality will enable me to put a dot if I want to, on the timeline based on the timestamp. For example, the data that I've, as mentioned inside InsightTimeLine.tsx, if I want to put a dot at "Timestamp": "04/06/2021 18:15:00", for Text 4, I might be able to do that on the horizontal line. With the current setup, I'm not able to do this.
Brief Description about Files:
File 1: App.tsx: This is where a Timeline component is called.
File 2: InsightTimeline.tsx:This is where I've my data defined and I'm making the timeline using makeTimelinetrace function
File 3: Plot.tsx :This is where the plotting of timeline is done based on some calculations.
I'm pasting my code below and also a CodeSandBox link for the existing setup:
App.tsx
export default function App() {
return (
<div>
<div className="pin">
<h2 style={{ display: "inline-block" }}>Text Timeline </h2>
<Timeline />
</div>
</div>
);
}
InsightTimeline.tsx:
function InsightTimeline() {
const iCanvasContainer = useRef(null);
const plot = d3.select(iCanvasContainer.current);
const [bins, setBins] = useState(14);
const [timeline, setTimeline] = useState(new Plot(plot, bins));
useEffect(() => {
if (bins) {
setTimeline(new Plot(plot, bins));
}
}, [bins]);
useEffect(() => {
if (iCanvasContainer.current) {
timeline.refreshTimeline();
var i = 0;
var data = [
{
ID: "3",
Object: "Text 1",
Timestamp: "05/12/2020 13:26:00"
},
{
ID: "6",
Object: "Text 2",
Timestamp: "01/07/2020 18:59:00"
},
{
ID: "7",
Object: "Text 3",
Timestamp: "01/07/2020 18:49:00"
},
{
ID: "57",
Object: "Text 4",
Timestamp: "04/06/2021 18:15:00"
}
];
if (data) {
data?.map((datatext: any) => {
i += 1;
timeline.makeTimelineTrace(i, datatext.Object.toUpperCase());
});
}
timeline.doRefresh();
}
}, [timeline]);
return <svg ref={iCanvasContainer} />;
}
export default InsightTimeline;
Plot.tsx
class Plot {
logname: string = "Plotter";
plot: any;
legendWidth: number = 50;
timelineBins: number = 14;
timelineSpace: number;
timelineRow: number = 22;
timelineThickness: number = 10;
timelineMarginTop: number = 25;
timelineDelta: any;
layer_base: any;
layer_text: any;
constructor(public inPlot: any, public inBins?: number) {
if (inBins) this.timelineBins = inBins;
this.timelineSpace = (1000 - this.legendWidth) / (this.timelineBins + 1);
try {
console.log(`${this.logname}: D3 Init: Creating Plot Container.`);
this.plot = inPlot;
this.plot.attr("class", "plot");
this.layer_base = this.plot.append("g").attr("class", "base_layer");
this.layer_text = this.plot.append("g").attr("class", "base_layer");
console.log(`${this.logname}: D3 Init Done.`);
} catch (error) {
console.log(`${this.logname}: ERROR - Could not create plot. (${error})`);
if (!this.plot) console.log(`${this.logname}: Reference Not Defined.`);
if (!this.timelineRow)
console.log(`${this.logname}: Timeline Row Not Defined.`);
}
}
getLogName() {
return this.logname;
}
doRefresh() {
console.log(`${this.logname}: ** REFRESH`);
this.plot.exit().remove();
}
destroy() {
this.plot = undefined;
}
makeTimelineTrace(row: number, label: string) {
this.layer_base
.append("rect")
.attr("class", "timeline_trace")
.attr("x", 0)
.attr("y", this.timelineRow * row + this.timelineThickness / 2);
this.layer_text
.append("text")
.attr("class", "timeline_text")
.attr("x", 15)
.attr("y", this.timelineRow * row + (this.timelineThickness - 5) / 2)
.classed("label", true)
.text(label);
}
refreshTimeline() {
// this.plot.selectAll("text").remove();
// this.plot.selectAll("rect").remove();
}
}
export default Plot;
The code sandbox browser should show the following on your end:
P.S. If for some reason you don't see anything below the Text Timeline text after opening the codesandbox, you can go to any file, for example, Plot,tsx hit space and once it gets saved it will show up in the browser. Not sure why it is happening in sandbox though but this is the hack I found.
I have Zabuto calendar showing booking dates, but some tours have many dates for the period, and loading them is slow.
I have changed the API to paginate the data, and send a next url if there is more data to load, but I can't see how to get zabuto calendar to update its data once it is displayed, and with Javascript being the ultimate asynchronous programming language, I figured there must be a way the calendar can display and upload data at the same time.
Posts such as this
How to load data from ajax to zabuto calendar plugin?
shows how to load the calendar data by ajax call, but not how to continuously upload more data asynchronously while the current calender data is displayed. Other posts indicate that the only way is to reload the entire calendar
reloading AJAX data for Zabuto Calendar after modal dismissal. I would prefer an asynchronous way.
The previous developer started to use the Vue framework, So what I have is a Zabuto calendar Vue module
<template>
<div id="my-calendar-a"></div>
</template>
<script>
import Vue from 'vue'
import moment from 'moment'
import { mapGetters } from 'vuex'
export default {
name: 'ZabutoCalendar',
methods: {
initialise: function () {
$('.calendar-month-navigation .glyphicon').click(function () {
Vue.bus.$emit('calendar-change-month')
})
}
},
props: ['tour'],
computed: {
...mapGetters('cart', [
'cartItems'
]),
...mapGetters('calendar', [
'tourDates'
])
},
mounted: function () {
var self = this
var currentDate = new Date()
/*
Use of the thrid party plugin zabuto calendar to
set up the calendar and check if dates are being clicked
https://github.com/zabuto/calendar
*/
var nextUrl = '/api/check-dates?year=' + moment().format('YYYY') +
'&month=' + moment().format('M') + '&tour=' + self.tour;
this.$store.dispatch('calendar/getTourDates', nextUrl).then(response => {
// I tried putting a while nextUrl loop here, but the calender wont display till dispatch returns
nextUrl = self.tourDates[1].next_url;
$(self.$el).zabuto_calendar({
data: self.tourDates[0].tourdates,
weekstartson: 0,
show_previous: false,
year: currentDate.getFullYear(),
month: currentDate.getMonth() + 1,
action: function () {
if ($(this).find('> div').hasClass('start_spots')) {
// reconstruct data
var selectedTour = {}
var id = this.id
var elem = $('#' + id)
$('.calendar-dow .selected').removeClass('selected')
$(this).find('> div').addClass('selected')
selectedTour = _.find(self.tourDates[0].tourdates, { 'tour_date_id': elem.data('tour_date_id') })
selectedTour['styled_date'] = moment(elem.data('date')).format('Do MMMM YYYY')
if ($(this).find('> div').hasClass('start_future')) {
selectedTour['available'] = 1
for (var index in self.cartItems) {
if (self.cartItems[index].date === elem.data('date')) {
selectedTour['available'] = 3
break
}
}
} else {
selectedTour['available'] = 2
}
self.$store.commit('calendar/setSelectedTour', selectedTour)
Vue.bus.$emit('date-click')
}
}
})
// while loop could surround above code
})
}
}
</script>
And a javascript module to do the ajax call to get all the data in one go
import axios from 'axios'
import moment from 'moment'
export const calendar_module = {
namespaced: true,
state: {
tourDates: [],
selectedTour: {}
},
getters: {
tourDates: (state) => {
return state.tourDates
},
selectedTour: (state) => {
return state.selectedTour
}
},
mutations: {
setSelectedTour (state, selectedTour) {
state.selectedTour = selectedTour
},
setTourDates (state, tourDates) {
state.tourDates = tourDates
}
},
actions: {
getTourDates ({ commit }, datesUrl) {
var response_data = axios.get(datesUrl).then((response) => {
commit('setTourDates', response.data)
});
return response_data;
}
}
}
The API response data is returned in the form
{ 'tourdates': array_data_object, 'next_url', url_string }
with next_url (within the response) set to an empty string if there is no more data. getTourDates actually returns the API response. I tried putting a while nextUrl loop around the code where commented, but zabuto calendar does not display till the dispatch function returns.
Does Zabuto Calendar have a built in way to asynchronously update its data while displaying? Otherwise how else can I get it to asynchronously display and load future dates?
Another way would be to get the ajax call to run several concurrently, and just return null in any that are redundant, but I would prefer to query the database first to see how many pages are needed, and would prefer not to waste an ajax call just to find out how many asynchronous hits are needed to get all data.
I could not put in a while nextUrl loop, so tried axios.all() instead, which allows for asynchronous calling of multiple gets at the same time. This made no improvement of load time, which was probably just as well, because it forced me to look at my REST API which had several inefficiencies that I would not have otherwise cleaned up.
This is my Async solution (which I no longer needed to use once the API was nice and quick) for posterity
import axios from 'axios'
import moment from 'moment'
export const calendar_module = {
namespaced: true,
state: {
tourDates: [],
selectedTour: {}
},
getters: {
tourDates: (state) => {
return state.tourDates
},
selectedTour: (state) => {
return state.selectedTour
}
},
mutations: {
setSelectedTour (state, selectedTour) {
state.selectedTour = selectedTour
},
setTourDates (state, tourDates) {
if (state.tourDates.length == 0) {
state.tourDates = tourDates[0].tourdates;
} else {
state.tourDates = state.tourDates.concat(tourDates[0].tourdates);
}
}
},
actions: {
getTourDates ({ commit }, datesUrl) {
var response_data = axios.all([
axios.get(datesUrl + '&page=1'),
axios.get(datesUrl + '&page=2'),
axios.get(datesUrl + '&page=3'),
axios.get(datesUrl + '&page=4'),
axios.get(datesUrl + '&page=5'),
axios.get(datesUrl + '&page=6')
]).then(axios.spread(function (response1, response2, response3, response4, response5, response6) {
commit('setTourDates', response1.data);
commit('setTourDates', response2.data);
commit('setTourDates', response3.data);
commit('setTourDates', response4.data);
commit('setTourDates', response5.data);
commit('setTourDates', response6.data)
}));
return response_data;
}
}
}
I have a loading screen that takes about 15-30 seconds depending on the data that's loaded. It loads about 50 items and displays on the page:
Loading item x
It uses an observable/subscription for each data call made to the DB. Upon receiving the data, the subscription fires off and adds it to an HTML string:
sync() {
this.syncStatus = "Starting sync"
this.syncService.sync().subscribe((status: string) => {
this.syncStatus += "<div>" + status + '</div>';
}, (error: string) => {
console.log(error);
}, () => {
this.Redirect();
});
}
<div class="description">
<span [innerHTML]="syncStatus"></span>
</div>
As of now, it simply shows the list and it cuts off the list display because it gets so long (again, 50+ items, sometimes hundreds). I was wondering, how do I show each individual item to the page for 5s then hide it?
You can create an array of objects with the insertion time of the item, then filter the array based on this property.
sync() {
this.syncStatus = [{ msg: 'Starting Sync', time: Date.now() }];
this.syncService.sync().subscribe((status: string) => {
this.syncStatus.unshift(status);
this.removeOldEntries();
}, (error: string) => {
console.log(error);
}, () => {
this.Redirect();
});
}
Then filter old entries:
removeOldEntries() {
this.syncStatus = this.syncStatus.filter((status) => status.time < Date.now() - 300000); // 5 minutes
}
It would be great if you take advantage of components in Angular
Stack Blitz, Source Code
Explanation
You don't need date creation to check when a data was received
You don't need filtering through hordes of data
If you use Angular component approach, life would be easier, Each component will be responsible for removing itself
Main Component.ts
export class AppComponent {
data = [
"Hello 0"
];
count = 1;
ngOnInit() {
// Think of this as your subscription to backend
setInterval(() => {
if (!this.data) {
this.data = []
}
this.data.push("Hello " + this.count ++);
}, 1000);
}
}
Main Component.html
<div class="description">
<div *ngFor="let datum of data; let i = index">
<hello [ref]="data" [index]="i">{{datum}}</hello>
</div>
</div>
Hello.ts
#Component({
selector: 'hello',
template: `<ng-content></ng-content>`
})
export class HelloComponent {
#Input() ref;
#Input() index: number;
ngOnInit() {
// This code will remove this component, upon
// the timeout you specify
setTimeout(() => {
this.ref.splice(this.index, 1);
}, 5000);
}
}
Trying to re-render charts every time when i have some change in API, here is the method:
ngOnChanges(changes: SimpleChanges) {
const data = changes['data'].currentValue;
this.draw(data);
}
and draw function
private draw(charts) {
this.charts = charts.map((chart, i) => {
this.options.title.text = chart.title;
const options = {
type: 'bar',
data: chart,
options: this.options
};
return new Chart(`chart${i}`, options);
});
this.charts.forEach(chart => {
chart.update();
});
}
you can see that i try to update/re-render charts in draw function. In console log i can see that chart object was updated with new data, but they just disappear from page. What's wrong in my update method?
I found a solution, i know it's a hack, but for the moment it's ok )))
So, basically i check if charts already exist in DOM, and if exist remove them and create new canvas elements.
private removeAction() {
const block = document.querySelector('#chartsBlock');
//remove
while (block.firstChild) {
block.removeChild(block.firstChild);
}
// create
this.charts.forEach((chart, i) => {
let canvas = document.createElement('canvas');
canvas.id = `chart${i}`;
block.appendChild(canvas);
});
}
You could try using ChangeDetectorRef:
import { ChangeDetectorRef } from '#angular/core';
constructor(private detector: ChangeDetectorRef) {}
ngOnChanges(changes: SimpleChanges) {
const data = changes['data'].currentValue;
this.draw(data);
this.detector.detectChanges()
// if detectChanges() doesn't work, try markForCheck()
}
I'm trying to create dynamically a chart using ng2-chart,
I get information from an angular 2 service, when I change only labels of chart it works and when I change data only it works, but When I change both just data are updated in the chart. have any one an explication for this strange behavior.
my template :
<canvas baseChart height="130" width="180"
[data]="doughnutChartData"
[labels]="doughnutChartLabels"
[chartType]="doughnutChartType"
(chartHover)="chartHovered($event)"
(chartClick)="chartClicked($event)">
</canvas>
my class :
export class PlDoughnutComponent implements OnInit {
constructor(private homeService: TileServiceService) { }
ngOnInit() {
this.updatePLdoughnut();
}
public util : UtilService = new UtilService();
public doughnutChartLabels:string[] = ['Download Sales'];
public doughnutChartData:number[] = [0,0,100];
public doughnutChartType:string = 'doughnut';
public updatePLdoughnut(){
this.homeService.getTile().
then(res => {
this.doughnutChartLabels = res.PLtypes;
this.doughnutChartData = this.util.objectToIntArray(res.PLByTypes);
})
}
}
Apparently, if you do not modify the original reference to the labels array, it seems to work, at least for me. I mean, if you want a completely different set of labels, you should do something like this:
In the template:
<canvas baseChart
[datasets]="lineChartData"
[labels]="lineChartLabels"
[options]="lineChartOptions"
[chartType]="'line'"></canvas>
In the ts component:
this.lineChartLabels.length = 0;
for (let i = tempLabels.length - 1; i >= 0; i--) {
this.lineChartLabels.push(tempLabels[i]);
}
Or, using new ECMAScript syntax:
this.lineChartLabels.length = 0;
this.lineChartLabels.push(...tempLabels);
The key is maybe the this.lineChartLabels.length = 0; statement, which practically 'empties' your array by setting its length to 0, without modifying the reference.
Hope this helps!
Recently i had to use ng2-charts and i was having a very big issues with updating my data untill i found this sollution:
<div class="chart">
<canvas baseChart [datasets]="datasets_lines" [labels]="labels_line" [colors]="chartColors" [options]="options" [chartType]="lineChartType">
</canvas>
</div>
and here what i have in my component :
import { Component, OnInit, Pipe, ViewChild, ElementRef } from '#angular/core';
import { BaseChartDirective } from 'ng2-charts/ng2-charts';
#Component({
moduleId: module.id,
selector: 'product-detail',
templateUrl: 'product-detail.component.html'
})
export class ProductDetailComponent {
#ViewChild(BaseChartDirective) chart: BaseChartDirective;
private datasets_lines: { label: string, backgroundColor: string, borderColor: string, data: Array<any> }[] = [
{
label: "Quantities",
data: Array<any>()
}
];
private labels_line = Array<any>();
private options = {
scales: {
yAxes: [{
ticks: {
beginAtZero: true
}
}]
}
};
constructor() { }
ngOnInit() {
this.getStats();
}
getStats() {
this._statsService.getStatistics(this.startDate, this.endDate, 'comparaison')
.subscribe(
res => {
console.log('getStats success');
this.stats = res;
this.labels_line = this.getDates();
this.datasets_lines = [];
let arr: any[];
arr = [];
for (let stat of this.stats) {
arr.push(stat.quantity);
}
this.datasets_lines.push({
label: 'title',
data: arr
});
this.refresh_chart();
},
err => {
console.log("getStats failed from component");
},
() => {
console.log('getStats finished');
});
}
refresh_chart() {
setTimeout(() => {
console.log(this.datasets_lines_copy);
console.log(this.datasets_lines);
if (this.chart && this.chart.chart && this.chart.chart.config) {
this.chart.chart.config.data.labels = this.labels_line;
this.chart.chart.config.data.datasets = this.datasets_lines;
this.chart.chart.update();
}
});
}
getDates() {
let dateArray: string[] = [];
let currentDate: Date = new Date();
currentDate.setTime(this.startDate.getTime());
let pushed: string;
for (let i = 1; i < this.daysNum; i++) {
pushed = currentDate == null ? '' : this._datePipe.transform(currentDate, 'dd/MM/yyyy');
dateArray.push(pushed);
currentDate.setTime(currentDate.getTime() + 24 * 60 * 60 * 1000);
}
re
turn dateArray;
}
}
i m sure this is the right way to do it, and hope this would be helpfull
Like Deyd pointed out before, this is caused by a combination of Angular 2+'s change detection and a bug in ng2-charts.
According to my own observations (correct me if I'm wrong), Angular merges several changes within a very short timeframe into a single collection (changes: SimpleChanges) when ngOnChanges is called.
Unfortunately, ng2-charts only checks if the dataset has been changed with this collection and updates it. Otherwise it completely rebuilds the entire chart. However, because of the way the change detection works, more than one property might have been changed. Then, only the dataset gets updated even if the labels and possibly other properties have been updated as well. See ngOnChanges in ng2-charts:
valor-software/ng2-charts/src/charts/charts.ts
And if you don't want to have a separate copy of ng2-charts in your app and fix the problem yourself, a possible workaround for this problem is to set the dataset with a short delay using JavaScript's built-in function setTimeout(callback: () => void, delay: number).
Before:
#Component({
selector: 'app-root',
template: `
<select (change)="onChange($event.target.value)">
<option value="" disabled selected>Select your option</option>
<option value="0">Option 0</option>
<option value="1">Option 1</option>
</select>
<canvas baseChart
chartType="bar"
[datasets]="barChartData"
[labels]="barChartLabels"
[colors]="barChartColors">
</canvas>
`
})
export class AppComponent implements OnInit {
chartData: string[];
chartLabels: string[];
chartColors: string[];
onChange(id: string) {
getFromApiById(id)
.then(result => this._setChart(result.data, result.labels, result.colors));
}
private _setChart(data: string[], labels: string[], colors: string[]) {
this.chartData = data;
this.chartLabels = labels;
this.chartColors = colors;
}
}
After:
#Component({
selector: 'app-root',
template: `
<select (change)="onChange($event.target.value)">
<option value="" disabled selected>Select your option</option>
<option value="0">Option 0</option>
<option value="1">Option 1</option>
</select>
<canvas baseChart
chartType="bar"
[datasets]="barChartData"
[labels]="barChartLabels"
[colors]="barChartColors">
</canvas>
`
})
export class AppComponent implements OnInit {
chartData: string[];
chartLabels: string[];
chartColors: string[];
onChange(id: string) {
getFromApiById(id)
.then(result => this._setChart(result.data, result.labels, result.colors));
}
private _setChart(data: string[], labels: string[], colors: string[]) {
this.chartLabels = labels;
this.chartColors = colors;
setTimeout(() => {
this.chartData = data;
}, 50);
}
}
Using BaseChartDirective i did chart update and it served the purpose. Sample below:
import { BaseChartDirective } from 'ng2-charts/ng2-charts';
inside the class add as below
#ViewChild(BaseChartDirective) chart: BaseChartDirective;
While you have the values to be changed, add as below
setTimeout(() => {
if (this.chart && this.chart.chart && this.chart.chart.config) {
this.chart.chart.config.data.labels = this.labels_pie;
this.chart.chart.update();
}
});
The trick is in clearing the label and data array, the below code didnt work for me :(
```
clearCharts() {
this.barChartLabels= [];
this.barChartData= [
{data: [], label: 'label1'},
{data: [], label: 'label2'}
];
}
However when I changed the way I cleared the data helped me (Using object reference)
clearCharts() {
this.barChartLabels= [];
this.emptyChartData(this.barChartData);
}
emptyChartData(obj) {
obj[0].data = [];
obj[1].data = [];
obj[0].label = 'label1';
obj[1].label = 'label2';
}
```
Using BaseChartDirective i did chart update and it served the purpose. Sample below:
import { BaseChartDirective } from 'ng2-charts';
inside the class add as below
#ViewChild(BaseChartDirective) chart: BaseChartDirective;
While you have the values to be changed, add as below
this.chart.ngOnChanges({});
This is an issue in the library ng2-charts, to resolve it I have cloned the github of ng2-charts in my app directory and have done following steps :
npm install
in appmodule import ng-2charts.ts from src directory.
add this updateChartLabels function to chart.ts file
call it in the onChanges function.
public ngOnChanges(changes: SimpleChanges): void {
if (this.initFlag) {
if(changes.hasOwnProperty('labels')){
console.log('labels changes ...');
this.updateChartLabels(changes['labels'].currentValue);
}
//..
//...
}
private updateChartLabels(newLabelsValues: string[] | any[]): void {
this.chart.data.labels = newLabelsValues;
}
For those looking for a walk around, for now you can put your labels and data in an object and put that object in an array and just loop through the array in your html. This will redraw the element every time your array changes.
in your type script every time there's a change.
data = [...]; labels = [...]; chartArray = [{data , labels }]
in your html
<canvas *ngFor="let chartData of chartArray " [datasets]="chartData.data" [labels]="chartData.labels" > </canvas>
This is an issue with the current ng2-charts library.
Try the new ng4-charts library which has fixed this issue.
https://www.npmjs.com/package/ng4-charts
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 was able to fix this issue by turning the handler into an arrow function
export class HistogramChartComponent implements OnInit {
constructor(private dataService: MyFruitService ) { }
barChartOptions: ChartOptions = { responsive: true };
barChartLabels: Label[] = ['Apple', 'Banana', 'Kiwifruit', 'Blueberry', 'Orange', 'Grapes'];
barChartType: ChartType = 'bar';
barChartLegend = true;
barChartPlugins = [];
barChartData: ChartDataSets[] = [
{ data: [45, 37, 60, 70, 46, 33], label: 'Best Fruits' }
];
ngOnInit() {
this.dataService
.getDocument("Foobar")
.subscribe(this.handleResponse);
}
handleResponse = (doc: MyFruitDocument) => {
console.log('document: ', doc);
let labels = doc.dataPoints.map(p => p.fruitName);
let data = { data: doc.dataPoints.map(p => p.value), label: 'Best Fruits' };
this.barChartLabels = labels;
this.barChartData = [ data ];
}
}
I have also faced this issue when trying to update the labels, (specifically when trying to send a shorter array), And this solved it:
#ViewChild(BaseChartDirective) chart!: BaseChartDirective;
and then, when updating the labels:
this.chart.chart!.config.data.labels = [...]
No need to call the update() method.
Since I didn't manage to get one of the above solutions to work properly, I want to contribute my solution, in case someone stumbles across this post and also got stuck with the present approaches.
I have the HTML similar to #mustafa918:
<div>
<canvas #canvas id="canvas"
baseChart [datasets]="lineChartData"
[labels]="lineChartLabels"
[colors]="lineChartColors"
[options]="lineChartOptions"
[chartType]="lineChartType"
[legend]="lineChartLegend"
(chartHover)="chartHovered($event)"
(chartClick)="chartClicked($event)">
</canvas>
</div>
And for the initialisation of the charts data in typescript i have:
public lineChartData: Array<any> = [
{ data: this.heights, label: 'Heights History', type: 'line', fill: false},
{ data: this.widths, label: 'Widths History', type: 'line', fill: false }];
And for me it worked only by setting the data and labels at the same time and don't use chart.update() - chart is the reference to the BaseChartDirective.
I loaded the respective data and labels beforehand, so that in this.heights, this.width and this.lineChartLabels are corresponding data.
E.g. : The entries on heights[i], widths[i] and lineChartLabels[i] match with the element in my elementArray at index i => element ={ "height":30, "width":20, "label":"box"}
setDatasets() {
//store data in chart references
var arrHeights = [];
for (var i in this.heights) {
arrHeights.push({ x: this.lineChartLabels[i], y: this.heights[i] });
}
var arrWidths= [];
for (var i in this.widths) {
arrWidths.push({ x: this.lineChartLabels[i], y: this.widths[i] });
}
this.lineChartData[0].data = arrHeights;
this.lineChartData[1].data = arrWidths;
}
I hope this helps someone :) Good Luck!
Today i struggled with similar problem, it appears there is a huge bug inside the updateChartData function of ng2-charts library version 1.6.0.
Here is the original function:
updateChartData = function (newDataValues) {
if (Array.isArray(newDataValues[0].data)) {
this.chart.data.datasets.forEach(function (dataset, i) {
dataset.data = newDataValues[i].data;
if (newDataValues[i].label) {
dataset.label = newDataValues[i].label;
}
});
}
else {
this.chart.data.datasets[0].data = newDataValues;
}
}
As you can see this updates only the data and the label, but all other properties are left behind. In my case i wanted to update also the pointBorderColor so i decided to override this.
First i get a reference to the ng2-charts library:
import { BaseChartDirective } from 'ng2-charts';
#ViewChild(BaseChartDirective) chart: any;
It is very important the type is "any", because otherwise typescript will not allow me to override a private function.
Then i fix the bug in the function and override it in afterVIew init:
ngAfterViewInit(){
if (this.chart) {
this.chart.updateChartData = function (newDataValues) {
if (Array.isArray(newDataValues[0].data)) {
this.chart.data.datasets.forEach(function (dataset, i) {
dataset.data = newDataValues[i].data;
if (newDataValues[i].pointBorderColor) {
dataset.pointBorderColor = newDataValues[i].pointBorderColor;
}
if (newDataValues[i].label) {
dataset.label = newDataValues[i].label;
}
});
}
else {
this.chart.data.datasets[0].data = newDataValues;
}
}.bind(this.chart);
}
}
Based on above answers, I extended this function and everything works fine now!
TS Code:
Declare : import { BaseChartDirective } from 'ng2-charts';
#ViewChild(BaseChartDirective) chart: BaseChartDirective;
public lineChartData: ChartDataSets[] = [
{ data: [0, 0, 0, 0, 0, 0, 0], label: 'Data 1' },
{ data: [0, 0, 0, 0, 0, 0, 0], label: 'Data 2' }
];
public lineChartLabels: Label[] = ['Label1', 'Label2', 'Label3', 'Label4',
'Label5', 'Label6';
TS Function:
refresh_chart(){
setTimeout(() => {
if (this.chart && this.chart.chart && this.chart.chart.config) {
this.chart.chart.config.data.datasets.forEach(x => {
x.data = [];
});
let index = 0;
this.chart.chart.config.data.datasets.forEach(x => {
x.data = this.lineChartData[index].data;
index++;
});
this.chart.chart.update();
}
}, 500);
}
HTML Code:
<canvas baseChart [datasets]="lineChartData" class="canvas-wh" [labels]="lineChartLabels"
[options]="lineChartOptions" [colors]="lineChartColors" [legend]="lineChartLegend"
[chartType]="lineChartType" [plugins]="lineChartPlugins">
For me, it worked only after using ViewChildren and not ViewChild.
TS:
#ViewChildren('baseLineChart1') chart !: QueryList<BaseChartDirective>;
this.chart.forEach((child) => { child.update() })
HTML:
<canvas class="card-line-chart" baseChart #baseLineChart1>