You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Chart.js 3迁移报错:Class does not have id,标注线失效求助

Chart.js 3迁移后自定义标注线消失并报错class does not have id的解决方法

问题原因分析

  • 自定义插件缺少Chart.js 3强制要求的id属性,导致注册时报错。
  • Chart.js 3的API结构变更,原插件中部分属性访问路径已失效。
  • 配置存在拼写错误(cales应为scales),导致坐标轴配置不生效。
  • 重复引入脚本文件引发潜在冲突。
  • 标注文本的坐标参数顺序错误,导致文本无法正常显示。

解决方案步骤

1. 给自定义插件添加唯一ID

Chart.js 3要求所有注册的插件必须包含id字段用于标识,修改插件代码:

const annotationLinePlugin = {
  id: 'annotationLine', // 添加唯一ID
  renderAnnotationLine: function(chart, line) {
    // ... 后续代码
  },
  afterDatasetsDraw: function(chart, easing) {
    // ... 后续代码
  }
};

2. 适配Chart.js 3的API变化

  • 将chartInstance.chart.ctx改为chart.ctx(Chart.js 3中chart实例直接包含ctx属性)。
  • 其他API如getDatasetMeta和scales的访问方式基本兼容,确保上下文正确即可。

3. 修正配置中的拼写错误

将配置里的cales改为scales,同时适配Chart.js 3的坐标轴配置格式(从数组改为对象键值对):

scales: { // 原拼写为cales,修正为scales
  x: { // 替换xAxes数组为x对象
    stacked: true,
    scaleLabel: {
      display: true,
      labelString: "Период",
    },
  },
  y: { // 替换yAxes数组为y对象
    id: "L",
    type: "linear",
    position: "left",
    stacked: true,
    scaleLabel: {
      display: true,
      labelString: "Трудозатраты, чел-мес",
    },
  },
},

4. 修正标注文本的坐标顺序

原代码中fillText的参数顺序错误(应为文本, x坐标, y坐标),修正后:

if (typeof line.label !== typeof undefined) {
    context.textAlign = 'left';
    context.fillStyle = line.color;
    context.fillText(line.label, xEnd + 7, yCoordinate); // 交换x和y参数位置
}

5. 移除重复的脚本引入

保留一个版本的脚本文件即可,避免冲突:

<script src="./chartjs3/chart.min.js"></script>
<script src="./chartjs3/chartjs-plugin-annotation.min.js"></script>
<script src="./chartjs3/chartjs-plugin-annotationLine.js"></script>
<script src="./chartjs3/chartjs-plugin-datalabels.min.js"></script>
<script src="./chartjs3/hammer.min.js"></script>

完整修正后的代码

自定义插件代码

const annotationLinePlugin = {
  id: 'annotationLine',
  renderAnnotationLine: function(chart, line) {
    let datasetMeta = chart.getDatasetMeta(line.datasetIndex);
    let context = chart.ctx; // 适配Chart.js 3的ctx获取方式
    let datasetModel = datasetMeta.data[line.dataIndex]._model;

    const xBarCenter = datasetModel.x;
    const barWidth = datasetModel.width;
    const xStart = xBarCenter - (barWidth / 2) - 4;
    const xEnd = xBarCenter + (barWidth / 2) + 4;

    const yAxisID = datasetMeta.yAxisID;
    const yCoordinate = chart.scales[yAxisID].getPixelForValue(line.yCoordinate);

    context.beginPath();
    context.strokeStyle = line.color;
    context.lineWidth = line.width;
    context.moveTo(xStart, yCoordinate);
    context.lineTo(xEnd, yCoordinate);
    context.stroke();

    if (typeof line.label !== typeof undefined) {
        context.textAlign = 'left';
        context.fillStyle = line.color;
        context.fillText(line.label, xEnd + 7, yCoordinate); // 修正坐标顺序
    }
  },

  afterDatasetsDraw: function(chart, easing) {
      if (chart.data.datasets.length < 1) {
          return;
      }
      if (chart.config.options.lines) { // 从options读取lines配置
          chart.config.options.lines.map(line => {
              this.renderAnnotationLine(chart, line);
          });
      }
  }
};

Chart.register(annotationLinePlugin);

index.html中的配置修正

const ctx = document.getElementById("сhart").getContext("2d"); 
const options = {
    type: "bar",
    responsive: true,
    maintainAspectRatio: false,
    title: { display: false },
    tooltips: {
      mode: "index",
      intersect: true,
      filter: (tooltipItem, chartData) => {
        return (
          !chartData.datasets[tooltipItem.datasetIndex].hideFromTooltip &&
          (chartData.datasets[tooltipItem.datasetIndex].yAxisID == "R" ||
            tooltipItem.yLabel > 0)
        );
      },
      callbacks: {
        label: function (tooltipItem, chartData) {
          return (
            chartData.datasets[tooltipItem.datasetIndex].label +
            ": " +
            tooltipItem.yLabel.round(0)
          );
        },
      },
    },
    animation: {
      duration: 500,
      easing: "linear",
    },
    plugins: {
      datalabels: {
        display: function (context) {
          const axisMax = context.chart.scales["L"]
            ? context.chart.scales["L"].max
            : 0;
          const datasetLabelsSettings =
            context.chart.data.datasets[context.datasetIndex].datalabels;
          const isLabelOutside =
            datasetLabelsSettings &&
            datasetLabelsSettings.anchor === "end" &&
            datasetLabelsSettings.align === "end";

          // 处理未定义的全局变量,避免报错
          return (
            context.chart.isDatasetVisible(context.datasetIndex) &&
            (typeof report !== 'undefined' ? report.chartSettings.showDataLabels : true) &&
            ((isLabelOutside &&
              context.dataset.data[context.dataIndex] > 0) ||
              context.dataset.data[context.dataIndex] >
                axisMax * (typeof HIDE_DATALABELS_Y_LEVEL !== 'undefined' ? HIDE_DATALABELS_Y_LEVEL : 0.1)) &&
            context.dataset.type !== "line"
          );
        },
        formatter: (x) => {
          return (x || 0).round(0);
        },
      },
    },
    legend: {
      display: true,
      onClick: function (event, legendItem) {
        var index = legendItem.datasetIndex,
          meta = null;
        var tag = chart.data.datasets[index].tag;
        var meta = chart.getDatasetMeta(index);

        if (tag || !isNaN(tag)) { // 替换自定义isNumeric为原生判断
          chart.data.datasets
            .filter((x) => x.tag == tag)
            .forEach((item) => {
              item.hidden = item.forcedHidden || !item.hidden;
            });
        } else {
          meta.hidden = meta.hidden === null ? !meta.hidden : null;
        }
        chart.update();
      },
       position: "top",
      labels: {
        usePointStyle: true,
        filter: function (legendItem, chartData) {
          return (
            chartData.datasets[legendItem.datasetIndex].displayInLegend ||
            chartData.datasets[legendItem.datasetIndex].displayInLegend ==
              undefined
          );
        },
      },
    },
    scales: { // 修正拼写错误cales为scales
      x: { // 替换xAxes数组为x对象
        stacked: true,
        scaleLabel: {
          display: true,
          labelString: "Период",
        },
      },
      y: { // 替换yAxes数组为y对象
        id: "L",
        type: "linear",
        position: "left",
        stacked: true,
        scaleLabel: {
          display: true,
          labelString: "Трудозатраты, чел-мес",
        },
      },
    },
    lines: [
        5553, 5454, 5399, 5290, 5260, 4927, 4565, 4649, 5324, 5624, 5710, 5766,
        5827, 5829, 5831, 5832, 5833, 5835, 5835, 5836, 5836, 5836, 5837, 583,
      ].map((x, i) => {
        return {
          datasetIndex: 0,
          dataIndex: i,
          yCoordinate: x,
          color: "salmon",
          width: 3,
          // 可添加label属性测试文本显示:label: x.toString()
        };
    }),
     data: {
      labels: [
        "2022-01",
        "2022-02",
        "2022-03",
        "2022-04",
        "2022-05",
        "2022-06",
        "2022-07",
        "2022-08",
        "2022-09",
        "2022-10",
        "2022-11",
        "2022-12",
        "2023-01",
        "2023-02",
        "2023-03",
        "2023-04",
        "2024-05",
        "2023-06",
        "2023-07",
        "2023-08",
        "2023-09",
        "2023-10",
        "2023-11",
        "2023-12",
      ],
      datasets: [
        {
          label: "ОПИ (ресурсный профиль)",
          findId: "ОПИ",
          tag: "ОПИ",
          type: "bar",
          borderWidth: 1,
          stack: "cnt",
          groupType: "cnt",
          backgroundColor: "#2196f3",
          fill: false,
          pointStyle: "rect",
          detailMode: "point",
          data: [
            5356, 5802, 6105, 6258, 6524, 6690, 6697, 7348, 6578, 5742, 4626,
            3678, 4809, 6038, 5891, 4526, 3637, 3601, 3471, 3618, 3548, 3428,
            3330, 3448, 5031,
          ],
          displayInLegend: false,
          datalabels: {
            anchor: "end",
            align: "end",
            offset: 0,
            color: function (context) {
              return "#2196f3";
            },
          },
        },
      ],
      
    },
  };
  const сhart = new Chart(ctx, options);

内容的提问来源于stack exchange,提问作者Andrey

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.17 00:31:39