I was trying to update a bar chart dataset by appending newer data to it using Chartjs. According to doc here, I need to update the datasets.data array and labels. I created the following component in Ionic:
chart.component.ts
import { SensorDataService } from './sensor-data.service';
import { Component, Input, AfterViewInit, ViewChild, ElementRef } from '#angular/core';
declare var Chart: any;
#Component({
selector: 'app-chart',
templateUrl: './chart.component.html',
styleUrls: ['./chart.component.scss'],
})
export class ChartComponent implements AfterViewInit {
//#Input() chartId: string;
//#Input() chartTitle: string;
#Input() data: Array<number>;
#Input() labels: Array<string>;
//#Input() datasetLabel: string;
interval: any;
count: number;
#ViewChild('chart') chartRef: ElementRef;
chart: any;
constructor(private dataService: SensorDataService) { }
ngAfterViewInit() {
console.log(this.data);
console.log(this.labels);
this.chart = new Chart(this.chartRef.nativeElement, {
type: 'bar',
data: {
labels: this.labels,
datasets: [{
label: 'label',
data: this.data,
backgroundColor: 'rgb(0, 0, 255)',
borderWidth: 1
}]
},
options: {
scales: {
yAxes: [
{
ticks: {
beginAtZero: true
}
}
]
}
}
});
this.count = 0;
this.interval = setInterval(this.loadData.bind(this), 5000);
}
loadData() {
if (this.count > 10) {
return clearInterval(this.interval);
}
const { data, labels } = this.dataService.getData();
console.log('data loaded');
console.log(this.chart.data.labels);
console.log(this.chart.data.datasets[0].data);
this.chart.data.labels.concat(labels);
this.chart.data.datasets.forEach(ds => ds.data.concat(data));
this.count++;
this.chart.update();
}
}
chart.component.html
<ion-card>
<ion-card-header>
<ion-card-title>Title</ion-card-title>
</ion-card-header>
<ion-card-content>
<canvas #chart></canvas>
</ion-card-content>
</ion-card>
From the console log, I don't see any changes in the size of the array. The graph remains the same as well. I created this as a test before connecting the data from http endpoint where, each request will return newer data and I have to append it to the existing chart. What's wrong with my code as given here and why is the chart not updating?
The Array.concat() method is used to merge two or more arrays. This method does not change the existing arrays, but instead returns a new array.
Therefore, you should change your loadData() method as follows:
loadData() {
if (this.count > 10) {
return clearInterval(this.interval);
}
const { data, labels } = this.dataService.getData();
this.chart.data.labels = this.chart.data.labels.concat(labels);
this.chart.data.datasets.forEach(ds => ds.data = ds.data.concat(data));
this.count++;
this.chart.update();
}
Related
I'm trying to make a simple Vue3 app which show graphs using Chart.js
For this I'm trying to replicate the code shown in the vue-chart-3 plugin doc, which shows an example using a Doughnut chart
My objective is to show a Line graph with a horizontal time axis
The code is a simple App.vue which template is
<template>
<LineChart v-bind="lineChartProps" />
</template>
And the script part:
<script lang="ts">
import { computed, defineComponent, ref } from "vue";
import { LineChart, useLineChart } from "vue-chart-3";
import { Chart, ChartData, ChartOptions, registerables } from "chart.js";
Chart.register(...registerables);
export default defineComponent({
name: "App",
components: { LineChart },
setup() {
const dataValues = ref([30, 40, 60, 70, 5]);
const dataLabels = ref(["Paris", "Nîmes", "Toulon", "Perpignan", "Autre"]);
const toggleLegend = ref(true);
const testData = computed<ChartData<"doughnut">>(() => ({
labels: dataLabels.value,
datasets: [
{
data: dataValues.value,
backgroundColor: [
"#77CEFF",
"#0079AF",
"#123E6B",
"#97B0C4",
"#A5C8ED",
],
},
],
}));
const options = computed<ChartOptions<"doughnut">>(() => ({
scales: {
myScale: {
type: "logarithmic",
position: toggleLegend.value ? "left" : "right",
},
},
plugins: {
legend: {
position: toggleLegend.value ? "top" : "bottom",
},
title: {
display: true,
text: "Chart.js Doughnut Chart",
},
},
}));
const { lineChartProps, lineChartRef } = useLineChart({
chartData: testData,
options,
});
function switchLegend() {
toggleLegend.value = !toggleLegend.value;
}
return {
switchLegend,
testData,
options,
lineChartRef,
lineChartProps,
};
},
mounted() {
if (localStorage.data == undefined) {
localStorage.data = JSON.stringify(this.data);
} else {
this.data = localStorage.data;
}
let dataProcessed = JSON.parse(this.data);
// console.log(JSON.parse(this.data));
console.log(dataProcessed.data);
var dates = [];
// Obtain dates from dataProcessed Array
for (var i = 0; i < dataProcessed.data.length; i++) {
dates.push(dataProcessed.data[i].date);
}
this.testData.labes = dates;
console.log(dates);
},
});
</script>
The objective is that the mounted hook gets certain parameters of the LocalStorage and put them in the "labels" array of the "testData" variable, which is the one which aparently stores the X axis data of the chart.
In the VUE developer tool, it can be seen how this assignation process is done correctly, but in the chart of the left side, the data have not been updated.
Thank you for your help :D
I have a line chart that gets data from the back end. I am able to plot the data but not the labels from the back end. This is my code:
import {Line} from 'vue-chartjs'
import axios from 'axios';
export default {
extends: Line,
props: ["data"],
methods: {
getScore() {
axios({
method: 'get',
url: 'http://localhost:5000/time',
}).then((response) => {
this.renderChart(
{
labels: [],
datasets: [
{
labels: response.data.score,
label: 'Stream',
backgroundColor: "#42c9d5",
data: response.data.score
}
]
},
{responsive: true, maintainApsectRatio: false}
)
})
.catch((error) => {
// eslint-disable-next-line
console.error(error);
});
}
},
mounted() {
this.getScore();
}
}
because I am getting the data from the getScore method. how Can I get the labels from another method? Or do I need to send two json responses? Also how do I loop through the json responses inside the this.renderchart?
I figured out how to do it. For those interested I passed a list of of dictionaries from the back end:
#app.route('/time')
def timeData():
response_object = {'status': 'success'}
SCORES = []
score = {}
score["value"] = [23,38,12]
score["date"] = ["Jan", "Feb", "Mar"]
SCORES.append(score)
response_object['score'] = SCORES
return jsonify(response_object)
I then added ... before the date key value pair:
import {Line} from 'vue-chartjs'
import axios from 'axios';
export default {
extends: Line,
props: ["data"],
methods: {
getScore() {
axios({
method: 'get',
url: 'http://localhost:5000/time',
}).then((response) => {
this.renderChart(
{
labels: [...response.data.score[0].date],
datasets: [
{
label: 'Stream',
backgroundColor: "#42c9d5",
data: response.data.score[0].value
}
]
},
{responsive: true, maintainApsectRatio: false}
)
})
.catch((error) => {
// eslint-disable-next-line
console.error(error);
});
}
},
mounted() {
this.getScore();
}
I am using chartjs in a nuxt component.
The problem is, I am getting referrence error, chartLabels and chartDataPoints are not defined.
If I replace chartLabels and chartDataPoints with this.$store.state.chartLabels and this.$store.state.chartDataPoints, my chart gets rendered, but then, it is no longer reactive since I need to use computed for that.
The store has been set up properly, and I am getting the correct data in vue devtools so the store doesn't seem to be the issue.
What am I doing wrong?
<canvas id="daily-chart"></canvas>
The chart is getting called in mounted() as so:
mounted() {
const ctx = document.getElementById("daily-chart")
new Chart(ctx, this.chartData)
},
Here is the chartData:
chartData: {
type: "line",
data: {
labels: chartLabels,
datasets: [
{
data: chartDataPoints,
},
],
},
options: {
responsive: true,
lineTension: 1,
},
}
chartDataPoints and chartLabels are being fetched from the store's state:
computed: {
...mapState(['chartDataPoints', 'chartLabels'])
},
If you are using vue-chart-js you may want to declare your chartData inside a specific js file that you would put in your template file.
Here is an example :
import {Line} from "vue-chartjs"
export default {
extends: Line,
data: function(){
return {
datacollection: {
labels: chartLabels,
datasets: [{
data: chartDataPoints,
}],
},
options: {
responsive: true,
lineTension: 1,
},
},
props:{
chartDataPoints : Array,
},
}
Then put the line chart in your template as :
<template>
<line-chart
:chartDataPoints="charData"
>
</line-chart>
</template>
I would also recommand using MapGetters from vuex instead of mapState and putting this in to a computed properties.
computed: {
...mapGetters({
chartDataPoints : 'chartData/getChartDataPoints'
})
},
And in your chartData.js (store) :
export const state = () => ({
charData: [],
})
export const getters = {
getChartDataPoints: (state) => () => {
return this.charData
}
}
Then, you would change your template to :
<template>
<line-chart
:chartDataPoints="chartDataPoints()"
>
</line-chart>
</template>
I am using vue-chartjs and chartjs-plugin-colorschemes to style a doughnut graph. I'm trying to allow the user to choose from a select which theme they prefer. I have it 90% working; the user can select a theme, hit update, and the doughnut plus its label correctly change color. What doesn't work though, is on initial page load, the doughnut has a color scheme but the legend does not.
I am currently passing a default theme down as props, and I am using a watch method to watch for changes to the theme. The error occurs inside of this watch method.
How can I dynamically update the legend label colors? Here is a minimal example of my component:
<script>
/* eslint-disable no-new */
import convert from 'convert-units';
import { Doughnut } from 'vue-chartjs';
import Chart from 'chart.js';
import { calculateCategoryWeight } from '~/helpers/functions';
import 'chartjs-plugin-colorschemes';
export default {
extends: Doughnut,
props: {
selected: {
type: Object,
default: () => {}
},
theme: {
type: String,
default: ''
}
},
data () {
const vm = this;
return {
chartData: {
labels: this.selected.categories.map(category => {
return this.$options.filters.truncate(category.name, 20);
}),
datasets: [
{
label: 'Selected Graph',
data: this.selected.categories.map(category => {
return parseFloat(convert(category).from('g').to('oz')).toFixed(2);
})
}
]
},
options: {
cutoutPercentage: 75,
legend: {
display: true,
position: 'right'
},
plugins: {
colorschemes: {
scheme: this.theme
}
},
responsive: true,
maintainAspectRatio: false,
tooltips: {
enabled: false
},
}
};
},
mounted () {
this.renderChart(this.chartData, this.options);
},
watch: {
theme (newVal, oldVal) {
const { chart } = this.$data._chart;
chart.options.plugins.colorschemes.scheme = newVal; //<--- updates chart only
chart.update();
}
}
};
</script>
Well I discovered the fix finally.
Essentially in the watch method, I was digging in too deep into the chart instance. By moving up a level in the chart object, both the legend and chart colors are both updated correctly.
watch: {
theme (newVal, oldVal) {
const chart = this.$data._chart; //<-- changed here
chart.options.plugins.colorschemes.scheme = newVal;
chart.update();
}
}
With angular 7, I integrated a chart.js for days of electricity charges. And in various part of my application I have similar charts which load with next/previous day button, etc.
When recreating the chart (on any next or previous day clicked) I do basically following
this.mainChart = new Chart('idOfCanvasOnHtmlComponent', {....})
this.mainChart.update()
It seems that it remains the old chart(s) which is sometimes showed on mouseover on some points and appears strangely, like there are multiple charts which are loaded on mouseover which is anoying!
ChargeUnitDailyComponent
import { Component, OnInit, AfterContentInit } from '#angular/core';
import { FormControl } from '#angular/forms';
import { MatDatepickerInputEvent } from '#angular/material/datepicker';
import { MiscHelper } from 'src/app/helpers/MiscHelper';
import { ChargeUnitService } from 'src/app/services/charge.unit.service';
import { ChargeUnit } from 'src/app/entities/charge.unit';
import { UserMessage, UserMessageType } from 'src/app/entities/user.message';
import { MessageService } from 'src/app/services/message.service';
import { Constants } from 'src/app/Constants';
import * as moment from 'moment';
import { MatIconRegistry } from '#angular/material';
import { DomSanitizer } from '#angular/platform-browser';
import Chart = require('chart.js')
import { MeasuringPoint } from 'src/app/entities/measuring.point';
import { MeasuringPointService } from 'src/app/services/measuring.point.service';
#Component({
selector: 'app-charge-unit-daily',
templateUrl: './charge-unit-daily.component.html',
styleUrls: [
'./charge-unit-daily.component.css',
'../../entities-list.component.css'
]
})
export class ChargeUnitDailyComponent implements AfterContentInit {
static readonly CHART_ID = 'canvasDaily'
currentDate: Date = moment('2019-03-06T00:00:01').toDate() //set default here
/** Header */
dateFormControl = new FormControl(this.currentDate)
statusMessage = 'Default status'
refreshButtonDisabled: boolean = false
/** CHART */
mainChart: Chart;
protected loadingDate: Date = new Date()
constructor(
iconRegistry: MatIconRegistry,
sanitizer: DomSanitizer,
protected messageService: MessageService,
protected entitiesService: ChargeUnitService,
protected parentService: MeasuringPointService,
) {
iconRegistry.addSvgIcon(
'refresh',
sanitizer.bypassSecurityTrustResourceUrl('assets/img/refresh-icon.svg'));
//Do nothing
}
public ngAfterContentInit() {
setTimeout(() => { //to avoid error...
if (this.parentService.currentEntity == null){
this.parentService.currentEntity = new MeasuringPoint(7) //TODO: remove me
}
console.debug('currentDate 1 :', this.currentDate)
this.setChart(this.currentDate)
this.dateFormControl.setValue(this.currentDate)
}, 10);
// this.setStatus('Default canvas...', true)
}//End ngOnInit
/**
*
* #param aDate
*/
protected setChart(aDate: Date){
let lStartDate = new Date(aDate)
lStartDate.setHours(0)
lStartDate.setMinutes(0)
lStartDate.setSeconds(1)
let lEndDate = new Date(aDate)
lEndDate.setHours(23)
lEndDate.setMinutes(59)
lEndDate.setSeconds(59)
this.setStatus('Loading...', false)
this.loadingDate = new Date()
console.debug('----- setChart->aDate:', aDate)
this.resetChart()
this.entitiesService.getBetween(lStartDate, lEndDate).subscribe(
lData => {
console.debug('Received data from entitiesService:', lData);
let lDataArray = (lData as unknown) as []
let lChargeUnitsArray: ChargeUnit[] = []
lDataArray.forEach(element => {
lChargeUnitsArray.push(new ChargeUnit().deserialize(element))
})
this.setChartDataFromEntities(lStartDate, lChargeUnitsArray)
},
lError => {
this.messageService.add(new UserMessage('charge-unit-daily.component->setChart Error:', lError, UserMessageType.Error));
this.setStatus('Error loading chart data:' + lError.toString(), true)
},
() => {
//loading terminated
}
);
}
onDateChanged(anEventType: string, anEvent: MatDatepickerInputEvent<Date> ) {
console.debug('onDateChanged clicked', anEvent)
let lDate = anEvent.value as Date
this.currentDate = lDate
this.setChart(lDate)
}
/**
*
* #param aDate
* #param aChargeUnitArray
*/
setChartDataFromEntities( aDate: Date, aChargeUnitArray: ChargeUnit[] ){
console.debug('setChartDataFromEntities->aChargeUnitArray', aChargeUnitArray)
let lChartDataArray = []
let lChartDataLineDataArray: Array<number> = []
let lChartLabelsArray: string[] = []
//Lines and labels
aChargeUnitArray.forEach(element => {
lChartDataLineDataArray.push(element.charge)
lChartLabelsArray.push(MiscHelper.dateTimeHMSForChart(element.timestamp))
});
//setting chart data
lChartDataArray[0] = {
data: lChartDataLineDataArray,
label: MiscHelper.dateForChartTooltips(aDate),
borderColor: Constants.CHART_DATASETS_BORDER_COLORS[0],
backgroundColor: Constants.CHART_DATASETS_BACKGROUND_COLORS[0],// removed otherwise not working
borderWidth: 2,
fill: 'origin'
}
console.debug('setChartDataFromEntities->lChartDataArray', lChartDataArray)
this.mainChart = new Chart(ChargeUnitDailyComponent.CHART_ID, {
type: 'line',
data: {
labels: lChartLabelsArray,
datasets: lChartDataArray,
},
options: {
legend: {
display: false
},
scales: {
xAxes: [{
display: true
}],
yAxes: [{
display: true
}],
},
responsive: true,
},
})
this.mainChart.update()
let lDiff = new Date().getTime() - this.loadingDate.getTime()
this.setStatus('Chart loaded:' + moment(lDiff).format('mm\'ss\'\''), true)
}
/**
*
*/
public resetChart(){
this.mainChart = new Chart(ChargeUnitDailyComponent.CHART_ID, {
type: 'line',
fillOpacity: .3,
data: {
labels: [],
datasets: []
},
options: {
legend: {
display: true
},
scales: {
xAxes: [{
display: true
}],
yAxes: [{
display: true
}],
},
events: ["mousemove", "mouseout", "click", "touchstart", "touchmove", "touchend"]
}
})
this.mainChart.update()
}
onRefreshClicked(anEvent){
console.debug('onRefreshClicked', anEvent)
this.setChart(this.currentDate)
}
onPreviousClicked(anEvent){
console.debug('onPreviousClicked', anEvent)
this.currentDate.setDate(this.currentDate.getDate() - 1)
this.dateFormControl.setValue(this.currentDate)
this.setChart(this.currentDate)
}
onNextClicked(anEvent){
console.debug('onNextClicked', anEvent)
this.currentDate.setDate(this.currentDate.getDate() + 1)
this.dateFormControl.setValue(this.currentDate)
this.setChart(this.currentDate)
}
}
Component.html
<mat-toolbar>
<span>
<mat-form-field>
<input matInput
[matDatepicker]="matDatepicker"
[formControl]="dateFormControl"
(dateChange)="onDateChanged('change', $event)"
>
<mat-datepicker-toggle matSuffix [for]="matDatepicker"></mat-datepicker-toggle>
<mat-datepicker #matDatepicker></mat-datepicker>
</mat-form-field>
</span>
<span class="fill-remaining-space">
<button mat-raised-button (click)="onPreviousClicked($event)" color="accent"><</button>
<button mat-raised-button (click)="onNextClicked($event)" color="accent">></button>
<button matTooltip="Refresh" mat-icon-button (click)="onRefreshClicked($event)" [disabled]="refreshButtonDisabled">
<mat-icon svgIcon="refresh" class="mat-icon-">Refresh</mat-icon>
</button>
</span>
<span><p>Status:{{statusMessage}}</p></span>
<script src="node_modules/chart.js/src/chart.js"></script>
</mat-toolbar>
<div class="canvasContainer" style="display: block; "><!--Mandatory div including chart-->
<canvas id="canvasDaily">{{mainChart}}</canvas>
</div>
The behaviour I figured out is that the chart creation has to be done only one time.
And then the properties set like this.mainChart.data.datasets = [...] or this.mainChart.data.datasets.push (...)
Otherwise there seem to be a zombie chart remaining in background and appearing on mouseover.
Why?? big question....