Reputation: 127
First of all, I would like to say that I'm a student learning programming for around a month, so expect to see many mistakes.
I'm working on a website where I use a chart from the ChartJs library. The data used for this chart is taken through requests to a server. I am working on making requests to the server every X number of seconds, so the data displayed on the chart gets updated automatically without having to refresh or do anything.
I would say I have most of it done, but I have a problem with ChartJs. I have the chart together with the request to server, all in one function which I then call on windows.onload
. I have used setInterval
to call this function every 5 seconds, and in practice this works, but I get this error :
Uncaught Error: Canvas is already in use. Chart with ID '0' must be destroyed before the canvas can be reused.
So I understand what is happening, I am trying to create the chart over and over again on the same canvas
element, and that of course doesn't work. I have seen about the destroy()
method and update()
method, but I haven't been able to find a solution for my specific case. If anyone could tell me any ideas on how to do this in this case, I would really appreciate it. Here is the code:
let serverData;
let stundenGesamt;
let date;
const url = 'https://urlsample.de/'; // Hidden the actual URL as it is the actual server from my company
const chart = document.getElementById("multie-pie-chart");
// Function that calculates the workdays passed up until today
const workdaysCount = () =>
[...new Array(new Date().getDate())]
.reduce((acc, _, monthDay) => {
const date = new Date()
date.setDate(1+monthDay)
![0, 6].includes(date.getDay()) && acc++
return acc
}, 0)
window.onload = function() {
chartRender(); // Calling the function that renders the chart
setInterval(chartRender, 5000); // Rendering the chart every 5 seconds
};
let chartRender = () => {
console.log('test');
let http = new XMLHttpRequest();
http.open("GET", url);
http.setRequestHeader('key', 'key-sample'); // Hidden the actual key as it is from the actual server from my company
http.onload = () => {
// Parsing the JSON file and storing it into a variable (Console.Log() to make sure it works)
serverData = JSON.parse(http.responseText);
console.log(serverData);
stundenGesamt = serverData.abzurechnen.gesamt; // Storing the value of total hours from the database in a variable
Chart.register(ChartDataLabels);
// Basic UI of the pie chart
const data = {
labels: ['Summe', 'Noch um Ziel zu erreichen', 'Arbeitstage', 'Verbleibende Tage im Monat'],
datasets: [
{
backgroundColor: ['#5ce1e6', '#2acaea'],
data: [stundenGesamt, (800 - stundenGesamt)]
},
{
backgroundColor: ['#cd1076', '#8b0a50'],
data: [workdaysCount(), (22 - workdaysCount())]
},
]
};
// Configuration of the pie chart
let outterChart = new Chart(chart, {
type: 'pie',
data: data,
options: {
responsive: true,
plugins: {
datalabels: {
font: {
weight: 'bold',
size: 20
},
color: 'white',
formatter: (val, chart) => {
const totalDatasetSum = chart.chart.data.datasets[chart.datasetIndex].data.reduce((a, b) => (a + b), 0);
const percentage = val * 100 / totalDatasetSum;
const roundedPercentage = Math.round(percentage * 100) / 100
return `${roundedPercentage}%`
}
},
legend: {
labels: {
color: 'white',
font: {
size: 14,
family: "'Helvetica Neue', 'Helvetica', 'Arial', sans-serif",
weight: 'bold'
},
generateLabels: function(chart) {
// Get the default label list
const original = Chart.overrides.pie.plugins.legend.labels.generateLabels;
const labelsOriginal = original.call(this, chart);
// Build an array of colors used in the datasets of the chart
var datasetColors = chart.data.datasets.map(function(e) {
return e.backgroundColor;
});
datasetColors = datasetColors.flat();
// Modify the color and hide state of each label
labelsOriginal.forEach(label => {
// Change the color to match the dataset
label.fillStyle = datasetColors[label.index];
});
return labelsOriginal;
}
},
onClick: function(mouseEvent, legendItem, legend) {
// toggle the visibility of the dataset from what it currently is
legend.chart.getDatasetMeta(
legendItem.datasetIndex
).hidden = legend.chart.isDatasetVisible(legendItem.datasetIndex);
legend.chart.update();
}
},
tooltip: {
callbacks: {
label: function(context) {
const labelIndex = (context.datasetIndex * 2) + context.dataIndex;
return context.chart.data.labels[labelIndex] + ': ' + context.formattedValue;
}
}
},
}
},
});
};
http.send();
};
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" type="text/css" href="style.css">
<title>Redmine Monitor</title>
</head>
<body>
<div class="container">
<canvas id="multie-pie-chart" height="200" width="200"></canvas>
<div class="titles">
<h3>Ziel: 800 Stunden</h3>
<h3>Monat: September</h3>
</div>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/3.5.1/chart.min.js"></script>
<script src="script.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/chartjs-plugin-datalabels/2.0.0/chartjs-plugin-datalabels.js"></script>
</body>
</html>
let serverData;
let stundenGesamt;
let date;
let outterChart;
const chart = document.getElementById("multie-pie-chart");
// Function that calculates the workdays passed up until today
const workdaysCount = () =>
[...new Array(new Date().getDate())]
.reduce((acc, _, monthDay) => {
const date = new Date()
date.setDate(1+monthDay)
![0, 6].includes(date.getDay()) && acc++
return acc
}, 0)
window.onload = function() {
chartRender(); // Calling the function that renders the chart
// setInterval(chartRender, 5000); // Rendering the chart every 5 seconds
};
let chartRender = () => {
Chart.register(ChartDataLabels);
// Basic UI of the pie chart
const data = {
labels: ['Summe', 'Noch um Ziel zu erreichen', 'Arbeitstage', 'Verbleibende Tage im Monat'],
datasets: [
{
backgroundColor: ['#5ce1e6', '#2acaea'],
data: [476.5, (800 - 476.5)] //Instead of 476.5, it would be the variable stundenGesamt, which contains the number of hours worked until this moment, which has been extracted from the server through the JSON file that has been parsed into the serverData variable. These variables are not available in the code snippet as I had to remove part of the codes to make it work, but you can see them on the code I posted above, which is the most accurante one. This code snippet is only used to display the graph and how it is structured
},
{
backgroundColor: ['#cd1076', '#8b0a50'],
data: [workdaysCount(), (22 - workdaysCount())]
},
]
};
// Configuration of the pie chart
outterChart = new Chart(chart, {
type: 'pie',
data: data,
options: {
responsive: true,
plugins: {
datalabels: {
font: {
weight: 'bold',
size: 20
},
color: 'white',
formatter: (val, chart) => {
const totalDatasetSum = chart.chart.data.datasets[chart.datasetIndex].data.reduce((a, b) => (a + b), 0);
const percentage = val * 100 / totalDatasetSum;
const roundedPercentage = Math.round(percentage * 100) / 100
return `${roundedPercentage}%`
}
},
legend: {
labels: {
color: 'white',
font: {
size: 14,
family: "'Helvetica Neue', 'Helvetica', 'Arial', sans-serif",
weight: 'bold'
},
generateLabels: function(chart) {
// Get the default label list
const original = Chart.overrides.pie.plugins.legend.labels.generateLabels;
const labelsOriginal = original.call(this, chart);
// Build an array of colors used in the datasets of the chart
var datasetColors = chart.data.datasets.map(function(e) {
return e.backgroundColor;
});
datasetColors = datasetColors.flat();
// Modify the color and hide state of each label
labelsOriginal.forEach(label => {
// Change the color to match the dataset
label.fillStyle = datasetColors[label.index];
});
return labelsOriginal;
}
},
onClick: function(mouseEvent, legendItem, legend) {
// toggle the visibility of the dataset from what it currently is
legend.chart.getDatasetMeta(
legendItem.datasetIndex
).hidden = legend.chart.isDatasetVisible(legendItem.datasetIndex);
legend.chart.update();
}
},
tooltip: {
callbacks: {
label: function(context) {
const labelIndex = (context.datasetIndex * 2) + context.dataIndex;
return context.chart.data.labels[labelIndex] + ': ' + context.formattedValue;
}
}
},
}
},
});
};
body {
font-family: Arial, Helvetica, sans-serif;
background-color: #343E59;
}
.container {
display: flex;
width: 800px;
margin: auto;
flex-direction: column-reverse;
justify-content: center;
color: white;
}
.titles {
display: flex;
justify-content: space-evenly;
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" type="text/css" href="style.css">
<title>Redmine Monitor</title>
</head>
<body>
<div class="container">
<canvas id="multie-pie-chart" height="200" width="200"></canvas>
<div class="titles">
<h3>Ziel: 800 Stunden</h3>
<h3>Monat: September</h3>
</div>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/3.5.1/chart.min.js"></script>
<script src="script.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/chartjs-plugin-datalabels/2.0.0/chartjs-plugin-datalabels.js"></script>
</body>
</html>
Upvotes: 1
Views: 9882
Reputation: 90247
The first time you run your function, you're placing the chart (a.k.a "the returned value of new Chart()
") inside the outterChart
variable.
If you instantiate that variable outside of your function (in this case, outside chartRender
), its value will be preserved across multiple runs of the function, and you'll be able to choose whether or not to create the chart or just update it based on current value of outterChart
.
In principle:
let outterChart; // undefined at first...
let chartRender = () => {
...
http.onLoad = () => {
...
if (outterChart) {
// update outterChart here. it has already been created
/*
* The update syntax depends on the structure of the data returned by server
* docs here: https://www.chartjs.org/docs/3.5.0/developers/updates.html
*/
} else {
outterChart = new Chart({
...
});
}
}
}
Edit: after having a look at your mcve, here's the bit that you need:
...
if (outterChart) {
data.datasets.forEach((ds, i) => {
outterChart.data.datasets[i].data = ds.data;
})
outterChart.update();
} else {
outterChart = new Chart(chart, { ... })
}
...
I could have simply done:
if (outterChart) {
outterChart.data = data;
} else {
outterChart = new Chart(chart, { ... })
}
..., but this would have completely replaced your datasets, instead of updating their data. If you replace any of the current datasets, the chart will perform an "enter" animation (will animate from scratch); whereas if you only replace the dataset's data
it will animate from current values to the new ones.
See it working
let serverData;
let stundenGesamt = 476.5;
let date;
let outterChart;
const chart = document.getElementById("multie-pie-chart");
// Function that calculates the workdays passed up until today
const workdaysCount = () =>
[...new Array(new Date().getDate())]
.reduce((acc, _, monthDay) => {
const date = new Date()
date.setDate(1+monthDay)
![0, 6].includes(date.getDay()) && acc++
return acc
}, 0)
window.onload = function() {
chartRender(); // Calling the function that renders the chart
setInterval(() => {
// randomizing stundenGesamt so it generates different values.
// instead, you get the value from server and put it into `studentGesamt`
stundenGesamt = Math.floor(Math.random() * (600 - 200 + 1) + 200);
// and then call `chartRender()`
chartRender();
}, 5000); // Rendering the chart every 5 seconds
};
let chartRender = () => {
Chart.register(ChartDataLabels);
// Basic UI of the pie chart
const data = {
labels: ['Summe', 'Noch um Ziel zu erreichen', 'Arbeitstage', 'Verbleibende Tage im Monat'],
datasets: [
{
backgroundColor: ['#5ce1e6', '#2acaea'],
// use current value of stundenGesamt in data
data: [stundenGesamt, (800 - stundenGesamt)] //Instead of 476.5, it would be the variable stundenGesamt, which contains the number of hours worked until this moment, which has been extracted from the server through the JSON file that has been parsed into the serverData variable. These variables are not available in the code snippet as I had to remove part of the codes to make it work, but you can see them on the code I posted above, which is the most accurante one. This code snippet is only used to display the graph and how it is structured
},
{
backgroundColor: ['#cd1076', '#8b0a50'],
data: [workdaysCount(), (22 - workdaysCount())]
},
]
};
if (outterChart) {
data.datasets.forEach((ds, i) => {
outterChart.data.datasets[i].data = ds.data;
})
outterChart.update();
} else {
outterChart = new Chart(chart, {
type: 'pie',
data: data,
options: {
responsive: true,
plugins: {
datalabels: {
font: {
weight: 'bold',
size: 20
},
color: 'white',
formatter: (val, chart) => {
const totalDatasetSum = chart.chart.data.datasets[chart.datasetIndex].data.reduce((a, b) => (a + b), 0);
const percentage = val * 100 / totalDatasetSum;
const roundedPercentage = Math.round(percentage * 100) / 100
return `${roundedPercentage}%`
}
},
legend: {
labels: {
color: 'white',
font: {
size: 14,
family: "'Helvetica Neue', 'Helvetica', 'Arial', sans-serif",
weight: 'bold'
},
generateLabels: function (chart) {
// Get the default label list
const original = Chart.overrides.pie.plugins.legend.labels.generateLabels;
const labelsOriginal = original.call(this, chart);
// Build an array of colors used in the datasets of the chart
var datasetColors = chart.data.datasets.map(function (e) {
return e.backgroundColor;
});
datasetColors = datasetColors.flat();
// Modify the color and hide state of each label
labelsOriginal.forEach(label => {
// Change the color to match the dataset
label.fillStyle = datasetColors[label.index];
});
return labelsOriginal;
}
},
onClick: function (mouseEvent, legendItem, legend) {
// toggle the visibility of the dataset from what it currently is
legend.chart.getDatasetMeta(
legendItem.datasetIndex
).hidden = legend.chart.isDatasetVisible(legendItem.datasetIndex);
legend.chart.update();
}
},
tooltip: {
callbacks: {
label: function (context) {
const labelIndex = (context.datasetIndex * 2) + context.dataIndex;
return context.chart.data.labels[labelIndex] + ': ' + context.formattedValue;
}
}
},
}
}
});
}
};
body {
font-family: Arial, Helvetica, sans-serif;
background-color: #343E59;
}
.container {
display: flex;
width: 800px;
margin: auto;
flex-direction: column-reverse;
justify-content: center;
color: white;
}
.titles {
display: flex;
justify-content: space-evenly;
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" type="text/css" href="style.css">
<title>Redmine Monitor</title>
</head>
<body>
<div class="container">
<canvas id="multie-pie-chart" height="200" width="200"></canvas>
<div class="titles">
<h3>Ziel: 800 Stunden</h3>
<h3>Monat: September</h3>
</div>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/3.5.1/chart.min.js"></script>
<script src="script.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/chartjs-plugin-datalabels/2.0.0/chartjs-plugin-datalabels.js"></script>
</body>
</html>
Upvotes: 1
Reputation: 198
Depending on your version but the way to do it in Chart JS is to update the data set and call the update method of our chart object.
Like so:
this.chart.data.datasets = data; // assuming your chart already exists
this.chart.update();
Again, the version of Chart JS you are using is important since there have been breaking changes between V2 and V3 as described in the migration guide https://www.chartjs.org/docs/latest/getting-started/v3-migration.html
Upvotes: 3