10

I am having issue with the default tooltip that chartjs provides as I can not add html inside the tooltips. I had been looking at how i can add the html/jsx inside the tooltip. I see an example with using customized tooltips here Chart JS Show HTML in Tooltip. can someone point me an example how to achieve the same with react-chartjs-2 library?

varun
  • 2,014
  • 5
  • 17
  • 20
  • I was also trying to customize the tooltip for react-chartjs-2 and the answer present here helped me implement it quickly https://stackoverflow.com/a/44010778/4554386 – Sagar Mar 11 '21 at 04:58

4 Answers4

8

You have to use the custom callback in the tooltip property to define your own positioning and set the hovered dataset in the component state

state = {
  top: 0,
  left: 0,
  date: '',
  value: 0,
};

_chartRef = React.createRef();

setPositionAndData = (top, left, date, value) => {
  this.setState({top, left, date, value});
};

render() {
  chartOptions = {
    "tooltips": {
      "enabled": false,
      "mode": "x",
      "intersect": false,
      "custom": (tooltipModel) => {
        // if chart is not defined, return early
        chart = this._chartRef.current;
        if (!chart) {
          return;
        }

        // hide the tooltip when chartjs determines you've hovered out
        if (tooltipModel.opacity === 0) {
          this.hide();
          return;
        }

        const position = chart.chartInstance.canvas.getBoundingClientRect();

        // assuming your tooltip is `position: fixed`
        // set position of tooltip
        const left = position.left + tooltipModel.caretX;
        const top = position.top + tooltipModel.caretY;

        // set values for display of data in the tooltip
        const date = tooltipModel.dataPoints[0].xLabel;
        const value = tooltipModel.dataPoints[0].yLabel;

        this.setPositionAndData({top, left, date, value});
      },
    }
  }

  return (
    <div>
      <Line data={data} options={chartOptions} ref={this._chartRef} />
      { this.state.showTooltip
        ? <Tooltip style={{top: this.state.top, left: this.state.left}}>
            <div>Date: {this.state.date}</div>
            <div>Value: {this.state.value}</div>
          </Tooltip>
        : null
      }
    </div>
  );
}

You can use the tooltips supplied by React Popper Tooltip or roll your own - pass the top and left to the tooltip for positioning, and the date and value (in my example) should be used to show the data in the tooltip.

kumarharsh
  • 18,961
  • 8
  • 72
  • 100
  • 4
    Great answer, but please update it to include `pageXOffset` for left and `pageYOffset` for top, otherwise it doesn't take into account scrolling. The docs on react-chartjs don't take this into account either. `const left = position.left + pageXOffset + tooltipModel.caretX;` `const top = position.top + pageYOffset + tooltipModel.caretY;` – Martin Dawson Oct 26 '17 at 12:33
  • Thanks @MartinDawson. Your comment has value though it depends on the situation. If your tooltip is `position:absolute`, then pageOffsets are needed, but if it's `position:fixed`, then it's not. – kumarharsh Nov 03 '17 at 12:17
  • This answer breaks when the chart is redrawn because canvas is null. I raised an issue for it: https://github.com/jerairrest/react-chartjs-2/issues/225 – Martin Dawson Dec 06 '17 at 11:21
  • I'm not really sure about the reason, but the same thing started happening to me lately while creating charts which rerender. (Maybe I had just never encountered this earlier). I solved it by setting the ref as such: ``, and `_setRef = (c) => { this._chart = c; }`. Then just doing `this._chart.chart_instance.canvas.getBoundingClientRect()` in the tooltip callback. – kumarharsh Dec 06 '17 at 13:00
  • can you help me with this https://stackoverflow.com/questions/58709699/change-tool-tip-direction-in-react-chartjs2?noredirect=1#comment103716906_58709699 – CraZyDroiD Nov 05 '19 at 12:58
4

If anyone looking answer customization of tooltip and gradient chart here is my code:

My Packages:

"react": "^17.0.2"
"chart.js": "^3.7.1"
"react-chartjs-2": "^4.1.0"
"tailwindcss": "^3.0.23"

ToopTip Component:

import React, { memo } from "react";
import { monetarySuffix } from "@src/helpers/util";
// tooltip.js
const GraphTooltip = ({ data, position, visibility }) => {
  return (
    <div
      className={`absolute px-4 py-3.5 rounded-lg shadow-lg bg-chart-label-gradient text-white overflow-hidden transition-all duration-300 hover:!visible
      ${visibility ? "visible" : "invisible"}
        `}
      style={{
        top: position?.top,
        left: position?.left,
      }}
    >
      {data && (
        <>
          <h5 className="w-full mb-1.5 block text-[12px] uppercase">
            {data.title}
          </h5>

          <ul className="divide-y divide-gray-100/60">
            {data.dataPoints.map((val, index) => {
              return (
                <li
                  key={index}
                  className="m-0 py-1.5 text-base font-rubik font-medium text-left capitalize last:pb-0"
                >
                  {val?.dataset.label}
                  {":"} {monetarySuffix(val?.raw)}
                </li>
              );
            })}
          </ul>
        </>
      )}
    </div>
  );
};

export default memo(GraphTooltip);

Chart Component

import React, { useMemo, useState, useRef, useCallback } from 'react';
import {
  Chart as ChartJS,
  CategoryScale,
  LinearScale,
  PointElement,
  LineElement,
  Title,
  Tooltip,
  Legend,
  Filler,
} from 'chart.js';
import { Line } from 'react-chartjs-2';
import GraphTooltip from './chart-tooltip';

ChartJS.register(
  CategoryScale,
  LinearScale,
  PointElement,
  LineElement,
  Title,
  Tooltip,
  Legend,
  Filler
);

const GradientChart = () => {
  const [tooltipVisible, setTooltipVisible] = useState(false);
  const [tooltipData, setTooltipData] = useState(null);
  const [tooltipPos, setTooltipPos] = useState(null);

  const chartRef = useRef(null);

  const data = {
    labels: [
      'January',
      'February',
      'March',
      'April',
      'May',
      'June',
      'July',
      'Agust',
      'September',
      'October',
      'November',
      'December',
    ],
    datasets: [
      {
        fill: true,
        backgroundColor: (context) => {
          const chart = context.chart;
          const { ctx, chartArea } = chart;

          if (!chartArea) {
            return;
          }
          return createGradient(
            ctx,
            chartArea,
            '#F46079',
            '#F46079',
            'rgba(255,255,255,0)'
          );
        },
        borderColor: '#F46079',
        lineTension: 0.4,
        pointRadius: 5,
        pointHoverRadius: 10,
        pointBackgroundColor: '#FE5670',
        pointBorderColor: '#ffffff',
        pointBorderWidth: 1.5,
        label: 'Sales',
        data: [
          4500, 2800, 4400, 2800, 3000, 2500, 3500, 2800, 3000, 4000, 2600,
          3000,
        ],
      },
      {
        fill: true,
        backgroundColor: (context) => {
          const chart = context.chart;
          const { ctx, chartArea } = chart;

          if (!chartArea) {
            return;
          }
          return createGradient(
            ctx,
            chartArea,
            '#2f4b7c',
            '#2f4b7c',
            'rgba(255,255,255,0)'
          );
        },
        borderColor: '#2f4b7c',
        lineTension: 0.4,
        pointRadius: 5,
        pointHoverRadius: 10,
        pointBackgroundColor: '#FE5670',
        pointBorderColor: '#ffffff',
        pointBorderWidth: 1.5,
        label: 'Commision',
        data: [
          5000, 3500, 3000, 5500, 5000, 3500, 6000, 1500, 2000, 1800, 1500,
          2800,
        ],
      },
      {
        fill: true,
        backgroundColor: (context) => {
          const chart = context.chart;
          const { ctx, chartArea } = chart;

          if (!chartArea) {
            return;
          }
          return createGradient(
            ctx,
            chartArea,
            '#665191',
            '#665191',
            'rgba(255,255,255,0)'
          );
        },
        borderColor: '#665191',
        lineTension: 0.4,
        pointRadius: 5,
        pointHoverRadius: 10,
        pointBackgroundColor: '#FE5670',
        pointBorderColor: '#ffffff',
        pointBorderWidth: 1.5,
        label: 'Transaction',
        data: [
          1000, 2000, 1500, 2000, 1800, 1500, 2800, 2800, 3000, 2500, 3500,
          2800,
        ],
      },
    ],
  };

  const createGradient = (ctx, chartArea, c1, c2, c3) => {
    const chartWidth = chartArea.right - chartArea.left;
    const chartHeight = chartArea.bottom - chartArea.top;
    const gradient = '';
    const width = '';
    const height = '';
    if (!gradient || width !== chartWidth || height !== chartHeight) {
      width = chartWidth;
      height = chartHeight;
      gradient = ctx.createLinearGradient(
        0,
        chartArea.bottom,
        0,
        chartArea.top
      );
      gradient.addColorStop(0, c3);
      gradient.addColorStop(0.5, c2);
      gradient.addColorStop(1, c1);
    }
    return gradient;
  };

  const customTooltip = useCallback((context) => {
    if (context.tooltip.opacity == 0) {
      // hide tooltip visibilty
      setTooltipVisible(false);
      return;
    }

    const chart = chartRef.current;
    const canvas = chart.canvas;
    if (canvas) {
      // enable tooltip visibilty
      setTooltipVisible(true);

      // set position of tooltip
      const left = context.tooltip.x;
      const top = context.tooltip.y;

      // handle tooltip multiple rerender
      if (tooltipPos?.top != top) {
        setTooltipPos({ top: top, left: left });
        setTooltipData(context.tooltip);
      }
    }
  });

  const options = useMemo(() => ({
    responsive: true,
    scales: {
      y: {
        grid: {
          display: false,
        },
      },
    },
    interaction: {
      mode: 'index',
      intersect: false,
    },
    plugins: {
      legend: {
        display: false,
      },
      title: {
        display: false,
      },
      tooltip: {
        enabled: false,
        position: 'nearest',
        external: customTooltip,
      },
    },
  }));

  return (
    <div className="grad-chart-wrapper w-full relative">
      <Line options={{ ...options }} data={data} ref={chartRef} />

      {tooltipPos && (
        <GraphTooltip
          data={tooltipData}
          position={tooltipPos}
          visibility={tooltipVisible}
        />
      )}
    </div>
  );
};

export default GradientChart;
0

Remember to think in React here (which is not always easy). Use the mycustomtooltipfunction to set state in your React class (specifically, add the tooltip that is passed to mycustometooltipfunction to the state - this will result in render being invoked. Now in the render function of your class, check if that state exists and add the JSX for your tooltip.

class MyChart extends Component {

    constructor(props) {
        super(props);
        this.state = {
            tooltip : undefined
        };
    }

    showTooltip = (tooltip) => {
        if (tooltip.opacity === 0) {
            this.setState({
                tooltip : undefined
            });
        } else {
            this.setState({ 
                tooltip
            });
        }
     }

     render() {
         const { tooltip } = this.state;

         let options = {
             ...
             tooltips : {
                 enabled : false,
                 custom : this.showTooltip,
             }
         }

         let myTooltip;
         if (tooltip) {
           // MAKE YOUR TOOLTIP HERE - using the tooltip from this.state.tooltip, or even have a tooltip JSX class
         }

         return (
             <div>
                 {myTooltip}
                 <Line ref="mygraph" key={graphKey} data={data} options={options} height={graphHeight} width={graphWidth}/>
             </div>
         )
    }
}

`

JonT
  • 539
  • 4
  • 4
  • appreciate your reply, your answer is really vague. Do you have a working example of what you are trying to do? – varun Jul 03 '17 at 18:10
0
this.chart.chart_instance.canvas.getBoundingClientRect();

If you get some error with chart_instance you should check parent of element value.

Try this:

this.chart.chartInstance.canvas.getBoundingClientRect();
Lorelorelore
  • 3,335
  • 8
  • 29
  • 40
kokojustin
  • 190
  • 3
  • 12