Search code examples
javascriptreactjschartschart.js

Chart.js Hover Lines Not Disappearing on Mouse Leave


I have a chart that I have created using the react-chartjs-2 library. I have created a custom plugin to draw vertical and horizontal hover lines. The hover lines appear correctly when hovering over the chart, but when I move the mouse off of the chart area, the hover lines do not disappear.

Here is the relevant code:

import React from 'react';
import { Line } from 'react-chartjs-2';
import { Chart, registerables } from 'chart.js';
import dayjs from 'dayjs';

Chart.register(...registerables);

  const hoverLinePlugin = {
    id: 'hoverLine',
    tooltip: null,
    beforeInit: (chart) => {
      const canvas = chart.canvas;
      hoverLinePlugin.tooltip = document.createElement('div');
      hoverLinePlugin.tooltip.id = 'CanvasPriceGraph@#!'
      const tooltip = hoverLinePlugin.tooltip;
      tooltip.style.position = 'absolute';
      tooltip.style.background = '#1f232e';
      tooltip.style.padding = '1px 12px';
      tooltip.style.borderRadius = '4px';
      tooltip.style.display = 'none';
      tooltip.style.boxShadow = '-3px 3px 5px rgba(0, 0, 0, 0.2)';
      tooltip.style.zIndex = 25;

      document.body.appendChild(tooltip);
  
      canvas.addEventListener('mousemove', (event) => {
        
        const { left } = canvas.getBoundingClientRect();
        const mouseX = event.clientX - left;
  
        // Get the x-coordinate of datapoint 0
        const datapointX0 = chart.scales.x.getPixelForValue(0);
  
        // Only update the chart's mouseX if mouseX is greater than or equal to datapointX0
        if (mouseX >= datapointX0 && mouseX <= chart.chartArea.right) {
          chart.mouseX = mouseX;
          chart.update();
  
          // Update tooltip position and content
          const datasetIndex = 0; // Adjust this based on your dataset
          const dataIndex = chart.scales.x.getValueForPixel(mouseX);
          const dataset = chart.data.datasets[datasetIndex];
          if (dataset && dataset.data[dataIndex] !== undefined) {
            tooltip.style.display = 'none';
            tooltip.style.left = '';
            const yValue = dataset.data[dataIndex].toFixed(2).replace(/\d(?=(\d{3})+\.)/g, '$&,');
            const xLabel = chart.data.labels[dataIndex];
            
            // Check if the tooltip would extend beyond the right edge of the chart
            const chartRight = canvas.getBoundingClientRect().right;
            if (mouseX > chartRight / 2) {
              tooltip.style.left = '';
              tooltip.style.right = window.innerWidth - event.x + 20 + 'px';
            } else {
              tooltip.style.right = '';
              tooltip.style.left = event.x + 20 + 'px';
            }
            
            tooltip.style.top = event.pageY + 'px';

            tooltip.innerHTML = `
            <div style='position: relative;'>
              <div style='font-size: 16px; font-weight: 100; color: #ffffff;'>$${yValue}</div>
              <div style='font-size: 10px; color: #E2E8F0;'>${dayjs(xLabel).format('DD MMM YY')}</div>
              ${hashSet.has(memory[1]) ? 
              "<div style='font-size: 10px; color: #E2E8F0;'>" + dayjs(xLabel).format('HH:mm') + "</div>" :
              ''
              }
            </div>`;
            tooltip.style.display = 'block';
          } else {
            tooltip.style.display = 'none';
          }
        }
      });
      // Hide the tooltip when leaving the canvas
      canvas.addEventListener('mouseleave', () => {
        hoverLinePlugin.tooltip.style.display = 'none';
      });

      // Hide the tooltip when window is resized
      window.addEventListener('resize', () => {
        hoverLinePlugin.tooltip.style.display = 'none';
      });
    },

    beforeDraw: (chart) => {
      const { ctx, chartArea: { bottom, top, left, right }, mouseX } = chart;
      if (mouseX !== undefined) {
        ctx.save();
        ctx.beginPath();
        ctx.lineWidth = 1;
        ctx.strokeStyle = '#D1D4DC';
  
        // Get the x-coordinate of datapoint 0
        const datapointX0 = chart.scales.x.getPixelForValue(0);
  
        // Use Math.max to ensure the hoverline doesn't go to the left of datapoint 0
        const hoverlineX = Math.max(mouseX, datapointX0);
  
        // Find the corresponding dataset index and data point index
        const dataIndex = chart.scales.x.getValueForPixel(hoverlineX);    
  
        // Retrieve the y-coordinate value for the given x-coordinate
        const pointElement = chartRef.current.getDatasetMeta(0).data[dataIndex];
        const hoveredDataX = pointElement?.x;
        const hoveredDataY = pointElement?.y;
  
        ctx.moveTo(hoveredDataX, bottom);
        ctx.lineTo(hoveredDataX, top);
        ctx.stroke();
        ctx.closePath();

        ctx.beginPath();
        ctx.moveTo(left, hoveredDataY);
        ctx.lineTo(right, hoveredDataY);
        ctx.stroke();
        ctx.closePath();

        ctx.beginPath();
        ctx.strokeStyle = memory[0];
        ctx.arc(hoveredDataX, pointElement?.y, 3, 0 * Math.PI, 2 * Math.PI);
        ctx.fillStyle = memory[0];
        ctx.fill();
        ctx.closePath();
      }
    },

    beforeDestroy: () => {
      hoverLinePlugin.tooltip.style.display = 'none';
    },
    
  };

return chartData.labels ? <Line id='canvas' ref={chartRef} data={data} options={options} plugins={[hoverLinePlugin]}/> : null;

How can I modify this code to remove the hover lines on mouse leave?

Any help would be appreciated. Thanks!


Solution

  • on mouse out we are removing the tooltip, we just need to stop the execution of beforeDraw when the tooltip is hidden, this line change seems to do the job.

    ...
    beforeDraw: (chart) => {
        if(hoverLinePlugin.tooltip.style.display === 'none') {
          return;
        }
        ...
    

    stackblitz