chartjs图表更新数据

Update data of chartjs chart

首先,我想说我是一名学习编程一个月左右的学生,所以希望看到很多错误。

我正在使用 ChartJs 库中的图表的网站上工作。用于此图表的数据是通过对服务器的请求获取的。我正致力于每隔 X 秒向服务器发出一次请求,因此图表上显示的数据会自动更新,而无需刷新或执行任何操作。

我会说我已经完成了大部分工作,但我对 ChartJs 有疑问。我有图表和对服务器的请求,所有这些都在一个函数中,然后我在 windows.onload 上调用它。我已经使用 setInterval 每 5 秒调用一次此函数,在实践中这有效,但我收到此错误:

Uncaught Error: Canvas is already in use. Chart with ID '0' must be destroyed before the canvas can be reused.

所以我明白发生了什么,我试图在同一个 canvas 元素上一遍又一遍地创建图表,这当然行不通。我已经看到了 destroy() 方法和 update() 方法,但我无法找到适合我的具体情况的解决方案。如果有人能告诉我在这种情况下如何做到这一点的任何想法,我将非常感激。这是代码:

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>

第一次 运行 您的函数时,您要放置图表(a.k.a new Chart() 的返回值” ) 在 outterChart 变量中。

如果您在函数之外实例化该变量(在本例中,在 chartRender 之外),它的值将在函数的多个 运行 中保留,您将能够选择是创建图表还是仅根据 outterChart.

的当前值更新图表

原则上:

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({
        ...
      });
    }
  }
}

编辑:查看您的 mcve 后,这是您需要的部分:

  ...
  if (outterChart) {
    data.datasets.forEach((ds, i) => {
      outterChart.data.datasets[i].data = ds.data;
    })
    outterChart.update();
  } else {
    outterChart = new Chart(chart, { ... })
  }
...

我本可以简单地完成:

if (outterChart) {
  outterChart.data = data;
} else {
  outterChart = new Chart(chart, { ... })
}

...,但这会完全替换您的数据集,而不是更新它们的数据。如果您替换任何当前数据集,图表将执行 "enter" 动画(将从头开始制作动画);而如果您只替换数据集的 data,它将从当前值动画到新值。

看看效果如何

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>

取决于您的版本,但在 Chart JS 中执行此操作的方法是更新数据集并调用我们图表对象的更新方法。

像这样:

this.chart.data.datasets = data; // assuming your chart already exists
this.chart.update();

同样,您使用的 Chart JS 版本很重要,因为如迁移指南中所述,V2 和 V3 之间发生了重大变化 https://www.chartjs.org/docs/latest/getting-started/v3-migration.html