Tooltip throws "Cannot read properties of null (reading 'getParsed')" after a responsive chart's canvas is detached

Aperta Adatta ai principianti
#12,397 0 commenti 0 reazioni 0 assegnatari Vedi su GitHub

I maintainer di solito rispondono entro 1 giorno

Nessuno ha ancora preso questa issue.

Valutazione

Difficoltà
2/5
Tempo stimato
1-3 ore
Idoneità per principianti
82/100
Tipo di issue
Bug
Chiarezza
Specificata chiaramente
Stato di attività
Attiva
Stack tecnologico
javascript

Direzione di ricerca

Inizia da src/plugins/plugin.tooltip.js ed esamina Tooltip._getActiveElements, che l’issue identifica come il filtro che non funziona; confronta il controllo del dataset esistente con il ciclo di vita del controller descritto per i grafici scollegati. Riproduci il problema con la pagina fornita e un evento del puntatore accodato durante lo scollegamento. Il lavoro è completato quando l’evento non genera più errori dopo lo scollegamento e il tooltip continua a funzionare correttamente per i grafici collegati.

Scritto dal modello di indicizzazione a partire dal testo della issue.

Descrizione

Expected behavior

A pointer event that reaches a chart after its canvas leaves the DOM does nothing.

Current behavior

The tooltip throws:

TypeError: Cannot read properties of null (reading 'getParsed')
    at Tooltip._getActiveElements (plugin.tooltip.js)
    at Tooltip.handleEvent
    at Object.afterEvent
    at Chart._eventHandler
    at listener
    at platform.dom.js (throttled proxy, requestAnimationFrame)

When a responsive chart's canvas is detached, the detached handler in bindResponsiveEvents calls this._stop(), which destroys every dataset meta, and then _resize(0, 0). The user event listeners stay bound and chart.ctx stays set, so the chart.ctx !== null check in createProxyAndListen lets a pointer event that was queued for the next frame through.

The tooltip still holds its last active elements. The chart is now 0×0, so the event is outside the chart area, and _getActiveElements filters lastActive:

return lastActive.filter(i =>
  this.chart.data.datasets[i.datasetIndex] &&
  this.chart.getDatasetMeta(i.datasetIndex).controller.getParsed(i.index) !== undefined
);

The dataset is still in chart.data.datasets, so getDatasetMeta() builds a new meta with controller: null, and .getParsed() throws.

#11596 added the datasets[i.datasetIndex] check for a dataset that was removed. This is a case where the dataset is still there but its controller is gone.

Reproducible sample

Self-contained page:

<div id="wrap" style="width:400px;height:300px"><canvas id="c"></canvas></div>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/chart.umd.min.js"></script>
<script>
  const chart = new Chart(document.getElementById('c'), {
    type: 'bar',
    data: { labels: ['a', 'b', 'c'], datasets: [{ label: 'x', data: [3, 5, 2] }] },
    options: { animation: false },
  });
  const pointAtBar = () => {
    const rect = chart.canvas.getBoundingClientRect();
    const bar = chart.getDatasetMeta(0).data[1];
    chart.canvas.dispatchEvent(new MouseEvent('mousemove', {
      clientX: rect.left + bar.x,
      clientY: rect.top + bar.y + 5,
    }));
  };
  const frame = () => new Promise((resolve) => requestAnimationFrame(resolve));

  (async () => {
    await frame();
    await frame();
    pointAtBar(); // the tooltip becomes active
    await frame();
    await frame();
    pointAtBar(); // throttled: runs on the next frame
    document.getElementById('wrap').remove(); // detach: chart._stop()
  })();
</script>

The second event runs after the detach and throws.

Optional extra steps/info to reproduce

In an app, this happens when a framework removes a chart from the page while the pointer is over it and destroys the chart a little later. Examples are a route transition, or vue-chartjs's destroyDelay, which keeps the chart alive for a short time after its component unmounts. We see it in production from Chrome 156 on Windows.

Possible solution

Skip a meta that has no controller in the filter:

return lastActive.filter(i => {
  const { controller } = this.chart.getDatasetMeta(i.datasetIndex);

  return this.chart.data.datasets[i.datasetIndex]
    && controller
    && controller.getParsed(i.index) !== undefined;
});

Another option is to clear the active elements of the chart and the tooltip in _stop(), so nothing refers to a destroyed controller.

Context

No response

chart.js version

v4.6.1 (the same code is on master, src/plugins/plugin.tooltip.js)

Browser name and version

Chrome 156; also reproduced in headless Chromium

Link to your project

No response

Lingua principale
JavaScript
Stelle
67.8k
Fork
12k
Merge medio
6h 15m
PR unite (30g)
3

Preparare l'ambiente

  • Nessun Dockerfile né file Docker Compose
  • Ha un modello di pull request
  • Nessuna guida per i contributori

Come iniziare

  1. Leggi tutta la issue e poi la guida ai contributi del progetto.
  2. Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
  3. Fai un fork del repository e lavora su un branch.
  4. Apri una pull request che faccia riferimento al numero della issue.

Altre issue di chartjs/Chart.js

Tutte le issue di chartjs/Chart.js

Issue simili

Altre issue su JavaScript

Ricevi le nuove issue nella tua casella

Un breve riepilogo di issue GitHub adatte ai principianti.