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
Related
I have the following code running:
var options = {
chart: {
type: 'donut',
fontFamily: 'Lato Light'
},
series: [1,2,3,4,5],
labels: ['1','2','3','4','5'],
theme: {
monochrome: {
enabled: true,
color: '#b19254',
shadeTo: 'dark',
shareIntensity: 0.15
}
},
//colors: ['#b19254', '#9f834c', '#8e7543', '#7c663b', '#b99d65', '#c8b387'],
legend: {
position: 'bottom'
},
plotOptions: {
pie: {
donut: {
labels: {
show: true,
name: {
show: false
},
value: {
offsetY: -1,
show: true
},
total: {
show: false,
showAlways: false,
formatter: function (w) { return String(Math.round(chart.w.globals.seriesTotals.reduce((a,b) => { return a+b}, 0) * 100) / 100) + ' ' + $currency}
}
}
}
}
},
}
var chart = new ApexCharts(document.querySelector("#investment-chart-wrapper"), options);
chart.render();
var $chartData = chart.dataURI();
$chartData.then(
(result) => {
document.querySelector('#chartimg').setAttribute('src',result.imgURI);
});
The bit I am fighting with is the promise result of the dataURI() method from here.
For some reason, the chart I get has all the information including the series labels, but the color for the series does not show, leaving me with this. The color is used for the legend at the bottom, however.
I am sure I am missing something here. Please let me know what.
I was running into this problem as well today. It was because the animation of the chart has not taken place yet. You have to get the dataURI() after it has fully rendered or turn off the chart animation.
I was able to get this working by setting the rendered chart to a variable at the top of my js file and then using it in a function like this:
function SetChartImage() {
chartHistoricalPCTArea.dataURI().then(({ imgURI }) => {
var image = document.querySelector('#HistoricalPCTImage');
image.src = imgURI;
})
}
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....
I need help to put the number of the pie chart in the legend
Chart Image
If i hover the chart with mouse i can see the number relative to each item
i want to display it in the legend either
the important code so far:
var tempData = {
labels: Status,
datasets: [
{
label: "Status",
data: Qtd,
backgroundColor: randColor
},
]
};
var ctx = $("#pieStatus").get(0).getContext("2d");
var chartInstance = new Chart(ctx, {
type: 'pie',
data: tempData,
options: {
title: {
display: true,
fontsize: 14,
text: 'Total de Pedidos por Situação'
},
legend: {
display: true,
position: 'bottom',
},
responsive: false
}
});
"Qtd","randColor" are "var" already with values
You need to edit the generateLabels property in your options :
options: {
legend: {
labels: {
generateLabels: function(chart) {
// Here
}
}
}
}
Since it is quite a mess to create on your own a great template. I suggest using the same function as in the source code and then edit what is needed.
Here are a small jsFiddle, where you can see how it works (edited lines - from 38 - are commented), and its result :
Maybe this is a hacky solution, but for me seems simpler.
The filter parameter
ChartJS legend options have a filter parameter. This is a function that is called for each legend item, and that returns true/false whether you want to show this item in the legend or not.
filter has 2 arguments:
legendItem : The legend item to show/omit. Its properties are described here
data : The data object passed to the chart.
The hack
Since JS passes objects by reference, and filter is called for each legend item, then you can mutate the legendItem object to show the text that you want.
legend : {
labels: {
filter: (legendItem, data) => {
// First, retrieve the data corresponding to that label
const label = legendItem.text
const labelIndex = _.findIndex(data.labels, (labelName) => labelName === label) // I'm using lodash here
const qtd = data.datasets[0].data[labelIndex]
// Second, mutate the legendItem to include the new text
legendItem.text = `${legendItem.text} : ${qtd}`
// Third, the filter method expects a bool, so return true to show the modified legendItem in the legend
return true
}
}
}
Following on from tektiv's answer, I've modified it for ES6 which my linter requires;
options: {
legend: {
labels: {
generateLabels: (chart) => {
const { data } = chart;
if (data.labels.length && data.datasets.length) {
return data.labels.map((label, i) => {
const meta = chart.getDatasetMeta(0);
const ds = data.datasets[0];
const arc = meta.data[i];
const custom = (arc && arc.custom) || {};
const { getValueAtIndexOrDefault } = Chart.helpers;
const arcOpts = chart.options.elements.arc;
const fill = custom.backgroundColor ? custom.backgroundColor : getValueAtIndexOrDefault(ds.backgroundColor, i, arcOpts.backgroundColor);
const stroke = custom.borderColor ? custom.borderColor : getValueAtIndexOrDefault(ds.borderColor, i, arcOpts.borderColor);
const bw = custom.borderWidth ? custom.borderWidth : getValueAtIndexOrDefault(ds.borderWidth, i, arcOpts.borderWidth);
const value = chart.config.data.datasets[arc._datasetIndex].data[arc._index];
return {
text: `${label}: ${value}`,
fillStyle: fill,
strokeStyle: stroke,
lineWidth: bw,
hidden: Number.isNaN(ds.data[i]) || meta.data[i].hidden,
index: i,
};
});
}
return [];
},
},
},
},
I wanted to let the user select from 100+ data sets, but rather than adding/removing them from my Chart I decided to set the showLine: false on any dataset that I want hidden. Unfortunately the default legend would show all 100+. So in my solution I generate the legend manually, filtering out any dataset that has showLine: false.
Your settings will have this:
legend: {
labels: {
generateLabels: (a) => {
return a.data.labels
}
}
And you'll generate your own labels with a helper function:
function updateAllLabels() {
const myNewLabels = [];
myChart.data.datasets.forEach((element) => {
if (element.showLine) {
myNewLabels.push(generateLabel(element));
}
});
myChart.data.labels = myNewLabels;
}
And you'll generate the label with another function:
function generateLabel(data) {
return {
fillStyle: data.borderColor,
lineWidth: 1,
strokeStyle: data.borderColor,
text: data.countyName, // I attach countryName to my datasets for convenience
}
}
Now just don't forget to call the function whenever updating your chart:
updateAllLabels();
myChart.update();
Happy graphing!