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>