chartjs / chartjs-plugin-annotation

Annotation plugin for Chart.js
MIT License
609 stars 329 forks source link

Cannot set properties of undefined (setting 'display') at resolveLabelElementProperties #786

Closed dbauszus-glx closed 2 years ago

dbauszus-glx commented 2 years ago

We seem to be stuck with plugin-annotation v1.3.0 and chartjs v3.7.0 where following example for a line chart with multiple datasets works with multiple annotations.

https://codepen.io/dbauszus-glx/pen/MWOjGzw

Upgrading plugin-annotation to v2.0.0 causes following TypeError: Cannot set properties of undefined (setting 'display') at resolveLabelElementProperties.

Upgrading chartjs to v3.9.1 causes a different TypeError: Cannot read properties of undefined (reading 'lastIndexOf')

The same error persists with chartjs on v3.9.1 and plugin-aanotation on v2.0.0

stockiNail commented 2 years ago

@dbauszus-glx having a look to the codepen, I have seen errors on chart configuration (responsive and tooltip options were in wrong position). In the annotation plugin, you are suing enabled option which has been remove in version 2.0.0 (see migration guide), where display option is used.

I have created a codepen using you code (removing async load of JS, and usinf chart,js 3.9.1 and annotation 2.0.0): https://codepen.io/stockinail/pen/PoRyMpv It seems working.

The issue seems to be related to async import and registration of plugin.

dbauszus-glx commented 2 years ago

Interesting. Thanks for pointing out the differences in the migration guide.

I can see that async import from https://cdn.skypack.dev/chartjs-plugin-annotation@2.0.0 fails because the options argument is a proxy with callout.display undefined.

function resolveLabelElementProperties(chart, properties, options) {
  options.callout.display = false;

Is the use of a proxy new in version 2 or can you think of any other changes which may cause the registration to fail from a dynamic import?

stockiNail commented 2 years ago

@dbauszus-glx the problem is not related to the undefined callout because otherwise it wouldn't work with importing javascript by <script> tag. And the issue is a missing registrations of annotation elements and their defaults (for this reason is undefined).

Anyway, I think I found the issue. There is something weird when we are importing chart.js 3.9.1 from cdn.skypack.dev.

This is the imported file from skypack:

*
 * Skypack CDN - chart.js@3.9.1
 *
 * Learn more:
 *   📙 Package Documentation: https://www.skypack.dev/view/chart.js
 *   📘 Skypack Documentation: https://www.skypack.dev/docs
 *
 * Pinned URL: (Optimized for Production)
 *   ▶️ Normal: https://cdn.skypack.dev/pin/chart.js@v3.9.1-PE3H4IRdC3uTYyKwatR2/mode=imports/optimized/chartjs.js
 *   ⏩ Minified: https://cdn.skypack.dev/pin/chart.js@v3.9.1-PE3H4IRdC3uTYyKwatR2/mode=imports,min/optimized/chartjs.js
 *
 */

// Browser-Optimized Imports (Don't directly import the URLs below in your application!)
export * from '/-/chart.js@v3.9.1-PE3H4IRdC3uTYyKwatR2/dist=es2019,mode=imports/optimized/chartjs.js';
export {default} from '/-/chart.js@v3.9.1-PE3H4IRdC3uTYyKwatR2/dist=es2019,mode=imports/optimized/chartjs.js';

But in the browser FF there is 1 file tagged as 3.9.0.

image

It seems an issue in skypack for version 3.9.1, because using 3.9.0 it works, perfectly, without any issue.

Code:

  const mod = await import("https://cdn.skypack.dev/chart.js@3.9.0");
  mod.Chart.register(...mod.registerables);

  const pluginAnnotation = await import(
    "https://cdn.skypack.dev/chartjs-plugin-annotation@2.0.0"
  );
  mod.Chart.register(pluginAnnotation.default);

See codepen: https://codepen.io/stockinail/pen/eYMbaee

Let me know if it works for you as well.

dbauszus-glx commented 2 years ago

Amazing. Thanks so much for looking into this. chartjs 3.9.0 works perfect with chartjs-plugin-annotation 2.0.0.

dbauszus-glx commented 2 years ago

I found another oddity unrelated to the dynamic import which is working with the correct versions.

In the forked codepen. If the data is not provided in the chart construction but set after the chart has been created like so:

myChart.data = data;
myChart.update();

Then the labels do not print. The lines are drawn correctly but just the labels are missing. There is no error message.

Would you like me to open a new ticket for this issue?

stockiNail commented 2 years ago

Would you like me to open a new ticket for this issue?

Maybe it's better new issue! ;) If you can send also the link to forked codepen to have a look it could be helpful.

EDIT

let's stay on this issue!

stockiNail commented 2 years ago

anyway I'm having a look.

stockiNail commented 2 years ago

The issue should be solved by PR https://github.com/chartjs/chartjs-plugin-annotation/pull/790 and release with version 2.0,1. using 2.0.1 from skypack, There is the issue outlined above. It seems that skypack will dowload also chart.js 3.9.1 because used for plugin annotation to be built. It's really weird. Let me take time to have a look more in deep.

In the meanwhile, let me propose you a workaround, adding a custom plugin to enable the labels, if the lines are visible (the solution will be to go version 2.0.1):

      plugins: [{
          id:'my',
            afterUpdate(chart){
              const elements = pluginAnnotation.default._getState(chart).elements;
              for (el of elements) {
                if (el.options.display) {
                    el.label.options.display = true;
                  }
              }
            }
        }],

See codepen: https://codepen.io/stockinail/pen/eYMbaee

stockiNail commented 2 years ago

It's weird because when you are importing the plugin. Ihave tested importing ONLY the plugin

  const { default:pluginAnnotation } = await import(
    "https://cdn.skypack.dev/chartjs-plugin-annotation@2.0.1"
  );

it seems that skypack is importing also Chart.js at a specific version related to the plugin.

image

I'm not an expert about how skypack is working but it doesn't seem correct (but maybe I'm wrong).

stockiNail commented 2 years ago

@dbauszus-glx Found the solution:

Pass to chart.js 3.9.1 and annotation 2.0.1

  const mod = await import("https://cdn.skypack.dev/chart.js@3.9.1");
  mod.Chart.register(...mod.registerables);

  const pluginAnnotation = await import(
    "https://cdn.skypack.dev/chartjs-plugin-annotation@2.0.1"
  );
  mod.Chart.register(pluginAnnotation.default);

Codepen changed accordingly: https://codepen.io/stockinail/pen/eYMbaee

I think, every time new version of plugin will be delivered, you should check which version of chartjs is related to the plugin in skypack and use that. It doesn't seem so flexible.

Let me know if it works.

dbauszus-glx commented 2 years ago

Sorry for the late reply as I was on holidays. I just checked this now and chart 3.9.1 with plugin annotation 2.0.1 works as expected if imported through skypack. Thanks for looking into this.

CavalcanteLeo commented 2 years ago

I still get this error on 2.0.1

package.json:

    "chart.js": "^3.9.1",
    "chartjs-plugin-annotation": "^2.0.1",
    "chartjs-plugin-datalabels": "^2.1.0",

config


const annotation1 = {
  type: 'line',
  borderColor: 'black',
  borderWidth: 1,
  scaleID: 'x',
  value: '10',
}

const barChartOptions = {
  indexAxis: 'y',
  responsive: true,
  maintainAspectRatio: false,
  scales: {
    x: {
      grid: {
        drawBorder: false,
        drawOnChartArea: false,
        drawTicks: false,
      },
    },
    y: {
      grid: {
        drawBorder: false,
        drawOnChartArea: false,
        drawTicks: false,
      },
    },
  },
  animation: {
    onComplete: () => {
      delayed = true
    },
    delay: (context) => {
      let delay = 0
      if (context.type === 'data' && context.mode === 'default' && !delayed) {
        delay = context.dataIndex * 50 + context.datasetIndex * 100
      }
      return delay
    },
  },
  plugins: {
    annotation: {
      common: {
        drawTime: 'beforeDraw',
      },
      annotations: {
        annotation1,
        // annotation2,
        // annotation3,
      },
    },
    datalabels: {
      color: '#ffffff',
      anchor: 'end',
      align: 'left',
      labels: {
        title: {
          font: {
            weight: 'bold',
            family: 'Outfit',
            size: 14,
          },
        },
      },
      formatter: function (value, context) {
        switch (context.dataIndex) {
          case 0:
            return value + ' kg'
          case 1:
            return value + ' kg'
          case 2:
            return value + ' %'
        }
      },
    },
    legend: {
      display: true,
    },
    tooltip: {
      // enabled: false,
      display: true,
    },
  },
}
const barChart = ref({
  labels: ['Peso', 'Músculo', 'Gordura'],
  datasets: [
    {
      borderColor: '#3B323D',
      backgroundColor: '#1E1020',
      borderWidth: 2,
      borderRadius: 8,
      data: [65, 59, 80],
    },
  ],
})
Screen Shot 2022-09-26 at 22 42 57
stockiNail commented 2 years ago

@CavalcanteLeo it seems that the line annotation element is not registered. Can you provide a codesandbox in order to reproduce the issue?

dbauszus-glx commented 2 years ago

@CavalcanteLeo I forked my codepen for you to check. https://codepen.io/dbauszus-glx/pen/rNvpdVO

I added the datalabels and annotation plugin configuration which you provided and this seems to work.

janthurau commented 2 years ago

just had the same issue. Somehow some graphs did draw fine with annotations, but others (just with different datapoints) failed with this error. What solved it is registering DataLabelsPlugin with Chart.register

stockiNail commented 2 years ago

just had the same issue. Somehow some graphs did draw fine with annotations, but others (just with different datapoints) failed with this error. What solved it is registering DataLabelsPlugin with Chart.register

I'm curious to understand better why you have an issue on annotation plugin, solved registering the datalabels plugin. Do you have a codepen/codesandbox to go more in deep?

janthurau commented 2 years ago

hey @stockiNail, I've just tried to reproduce this in a sandbox, but it does work there.

In my setup, I'm generating the charts on the backend with nodejs (using ChartJSNodeCanvas). Probably that's something on their side then..

robbytx commented 1 year ago

I'm trying to use this plugin in an Ember (4.x) app, and I've run across this issue as well. From what I can tell, the issue for me stems from the fact that:

  1. ember-auto-import generates the following code for Webpack to compile:
    d('chart.js/auto', [], function() { return require('chart.js/auto'); });
    d('chartjs-plugin-annotation', [], function() { return require('chartjs-plugin-annotation'); });
  2. Webpack then apparently maps:
    • chart.js/auto (version 4.1.x) to chart.js/auto/auto.cjs (the CommonJS module), which then imports chart.js/dist/chart.cjs, while
    • chartjs-plugin-annotation (version 2.1.1) gets mapped to chartjs-plugin-annotation/dist/chartjs-plugin-annotation.esm.js (the ESM module), which then imports chart.js/dist/chart.js.
  3. Because they import Chart.js via different files, then two separate copies of Chart.js end up in the bundle, and this plugin ends up registering its elements in the "wrong" version of Chart.js.
  4. And because the elements are not registered in the version of Chart.js that is used by the app, then the various defaults for config resolution are missing, and the plugin fails with an error like:
    main.js:1564 Uncaught TypeError: Cannot read properties of undefined (reading 'borderWidth')
    at resolveLabelElementProperties (main.js:1564:31)
    at LineAnnotation.resolveElementProperties (main.js:1380:29)
    at updateElements (main.js:2082:32)
    at Object.afterUpdate (main.js:2249:5)
    at Object.callback (main.js:14474:19)
    at PluginService._notify (main.js:8082:33)
    at PluginService.notify (main.js:8065:29)
    at Chart.notifyPlugins (main.js:9320:30)
    at Chart.update (main.js:8881:14)
    at Chart.<anonymous> (main.js:8623:64)

Minimal Repro

I am able to reproduce the issue in a minimal project with:

{
  "dependencies": {
    "chart.js": "^4.1.2",
    "chartjs-plugin-annotation": "^2.1.1"
  },
  "devDependencies": {
    "webpack": "^5.75.0",
    "webpack-cli": "^5.0.1"
  }
}

using the command line:

npx webpack --mode development  --devtool source-map ./index.js

and the following index.js file modeled off the basic line sample:

const Chart = require('chart.js/auto');
const annotationPlugin = require('chartjs-plugin-annotation');
Chart.register(annotationPlugin.default);

// From https://www.chartjs.org/chartjs-plugin-annotation/latest/samples/line/basic.html
const DATA_COUNT = 8;

const labels = [];
for (let i = 0; i < DATA_COUNT; ++i) {
    labels.push('' + i);
}

const data = {
    labels: labels,
    datasets: [{
        data: [ 57.726, 91.99, 45.051, 64.686, 45.34, 88.123, 30.814, 27.856 ] // Utils.numbers(numberCfg)
    }]
};

const annotation = {
    type: 'line',
    borderColor: 'black',
    borderWidth: 3,
    scaleID: 'y',
    value: 50
};

const config = {
    type: 'line',
    data,
    options: {
        scales: {
            y: {
                stacked: true
            }
        },
        plugins: {
            annotation: {
                annotations: {
                    annotation
                }
            }
        }
    }
};

new Chart(document.getElementById('canvas'), config);

And the following index.html:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8" />
    <title>Reproduce chartjs-plugin-annotation#786</title>
</head>
<body>
<canvas id="canvas" width="400"></canvas>
<script src="dist/main.js"></script>
</body>
</html>

Analysis

Obviously the issue is that Webpack is choosing inconsistent module types for chart.js vs this plugin.

If I replace the first lines with:

import Chart from 'chart.js/auto';
import annotationPlugin from 'chartjs-plugin-annotation';
Chart.register(annotationPlugin);

Then the issue goes away, because Webpack uses the ESM versions of each.

It's clear to me why Webpack is choosing the CJS when using require('chart.js/auto') but it was less clear to me why it was using ESM for require('chartjs-plugin-annotation'). Probably because it prefers module to main by default?

Fortunately I just noticed that #832 was merged recently, and if I apply that change to my local node_modules/chartjs-plugin-annotation/package.json, then this issue is resolved because Webpack correctly chooses chartjs-plugin-annotation/dist/chartjs-plugin-annotation.min.js, which then uses require("chart.js"), which Webpack maps to chart.js/dist/chart.cjs, thereby eliminating the inconsistent versions.

So I think this issue (at least for me) should be resolved with the next version of this plugin that includes #832.

As a workaround (for now or in perpetuity), I can introduce an intermediate ESM module that uses import for all Chart.js plugins, so that Webpack properly and consistently chooses the ESM version of each.