Search code examples
typescripthtml5-canvaschart.jssvelte

Cannot read properties of undefined (reading 'notifyPlugins') - chart.js


Why is my chart attempting to read notifyPlugins('beforeDestroy') when navigating away from the page of the chart?

I have a plugins file, a svelte action and a main file [ChartMetrics.svelte] (where the action is used). The current behaviour is that the chart loads completely fine in the main page, but when navigating to another page I am met with the notifyPlugins error. I have added the most relevant code from my project, as the entire codebase is quite large so would be unable to add it to a codesandbox. Has anyone recieved this error with notifyPlugins before when working with chart.js? If there is any more code you need to see let me know.

console error:

chart.js:6171 Uncaught (in promise) TypeError: Cannot read properties of undefined (reading 'notifyPlugins')
    at destroy (chart.js:6171:1)
    at Object.destroy [as d] (ChartMetrics.svelte:454:1)
    at Object.destroy [as d] (Div.svelte:127:1)
    at destroy_component (index.mjs:1974:1)
    at Object.destroy [as d] (ChartMetrics.svelte:127:1)
    at Object.destroy [as d] (ChartMetrics.svelte:535:1)
    at destroy_component (index.mjs:1974:1)
    at Object.destroy [as d] (EffectiveMaterialAssetsMetricsContainer.svelte:71:1)
    at Object.destroy [as d] (MetricWrapper.svelte:133:1)
    at Object.destroy [as d] (MetricWrapper.svelte:308:1)

chart.js (line 6171):

destroy() {
   this.notifyPlugins('beforeDestroy');
   ....
}

ChartMetrics (line 454):

        d: function destroy(detaching) {
            if (detaching) detach_dev(div0);
            if_blocks[current_block_type_index].d();
            destroy_component(tooltipinfo);
            destroy_component(anchor);
            if (detaching) detach_dev(t3);
            if (detaching) detach_dev(div1);
            mounted = false;
            dispose();
        }

ChartMetrics line 454 (image): chartMetrics

plugins.ts:

export function textPlugin(value: number, theme: Theme) {
  return {
    id: "chartTextPlugin",
    // Plugin for writing text in middle of chart
    beforeDraw: function (chart: ChartType) {
      plugin that draws some text on a chart.js chart
    },
  }
}

export function curvePlugin() {
  // Curve edges of donut
  return {
    id: "chartCurvePlugin",
    afterUpdate: function (chart: ChartType) {
      // some code to curve the edges of the chart.js chart
      }
    },
    afterDraw: function (chart: ChartType) {
      // some more code to do same as above
    },
  }
}

add-chart.ts (svelte action)

export const addChart = (node: HTMLElement, params: ChartProps): ChartGauge => {
  const text = textPlugin(params.value, params.theme)
  const curve = curvePlugin()
  const backgroundColor = mapTypeToColor(params.theme)

  return new Chart(node, {
    type: "doughnut",
    data: {
      datasets: [
        {
          //label: params.caption,
          data: [params.value, 100 - params.value],
          backgroundColor: backgroundColor,
          borderColor: ["rgba(255, 255, 255 ,1)"],
          borderWidth: 0,
        },
      ],
    },
    options: {
      rotation: -90,
      cutout: "85%",
      circumference: 180,
      radius: "85%",
      responsive: true,
      maintainAspectRatio: true,
      aspectRatio: 3,
    },
    plugins: [text, curve],
  })
}

ChartMetrics.svelte

{#if value >= 0 && value <= 100}
  <CardDiv p={4} pl={4} pr={4}>
    <div class="metrics-header">
      <h3>
        Controls -
        {#if titleUrl}
          <Anchor href={titleUrl} size={14}>{title}</Anchor>
        {:else}
          <span>{title}</span>
        {/if}
        <TooltipInfo content={tooltipContent} id="info" width="small" />
      </h3>
      <Anchor href={assuranceUrl} size={14}>View Assurance</Anchor>
    </div>
    <div class="chart">
      <canvas
        id="chart"
        data-test-id="chart"
        use:addChart={{ value, theme }}
        style="height:100%;width:100%;"
      />
    </div>
  </CardDiv>
{:else}
  <MetricsError message="Chart Metrics Widget Error: Invalid value." />
{/if}

Solution

  • You could try this patch of a solution: in add-chart.ts instead of

    return new Chart(node, {
    .....
    });
    

    use

    const chart = new Chart(node, {
        .....
    });
    chart.destroy = chart.destroy.bind(chart);
    return chart; 
    

    A more svelte-idiomatic way might be

    return {
       ...chart,
       destroy(){
          chart.destroy();
       }
    }
    

    but that would probably need some typescript acrobatics to pass type-checking.