Skip to content

Instantly share code, notes, and snippets.

@dankremniov
Last active February 21, 2024 06:41
Show Gist options
  • Save dankremniov/a9a6b969e63dfc4f0f83e6f82b82eb4f to your computer and use it in GitHub Desktop.
Save dankremniov/a9a6b969e63dfc4f0f83e6f82b82eb4f to your computer and use it in GitHub Desktop.
Render React component for Highcharts tooltip

This is an example of how to render React component for Highcharts tooltip using React portals. See Chart.tsx and Tooltip.tsx files below. Latest Highcharts version is required for the example to work correctly (9.0.0 at the time of writting).

Codesandbox: https://codesandbox.io/s/highcharts-react-tooltip-ut1uy

import React, { useState, useCallback } from "react";
import Highcharts, { Chart as HighchartsChart } from "highcharts";
import HighchartsReact from "highcharts-react-official";
import { Tooltip } from "./Tooltip";
const options = {
title: {
text: "Custom tooltip as React component"
},
series: [
{
type: "line",
data: [1, 2, 3]
}
],
tooltip: {
style: {
pointerEvents: "auto"
}
}
};
export const Chart = () => {
const [chart, setChart] = useState<HighchartsChart | null>(null);
const callback = useCallback((chart: HighchartsChart) => {
setChart(chart);
}, []);
return (
<>
<HighchartsReact
highcharts={Highcharts}
options={options}
callback={callback}
/>
<Tooltip chart={chart}>
{(formatterContext) => {
const { x, y } = formatterContext;
return (
<>
<div>x: {x}</div>
<div>y: {y}</div>
<br />
<button onClick={() => alert(`x: ${x}, y: ${y}`)}>Action</button>
</>
);
}}
</Tooltip>
</>
);
};
import {
Chart,
TooltipFormatterCallbackFunction,
TooltipFormatterContextObject
} from "highcharts";
import { useEffect, useState, useRef } from "react";
import ReactDOM from "react-dom";
const generateTooltipId = (chartId: number) =>
`highcharts-custom-tooltip-${chartId}`;
interface Props {
chart: Chart | null;
children(formatterContext: TooltipFormatterContextObject): JSX.Element;
}
export const Tooltip = ({ chart, children }: Props) => {
const isInit = useRef(false);
const [context, setContext] = useState<TooltipFormatterContextObject | null>(
null
);
useEffect(() => {
if (chart) {
const formatter: TooltipFormatterCallbackFunction = function () {
// Ensures that tooltip DOM container is rendered before React portal is created.
if (!isInit.current) {
isInit.current = true;
// TODO: Is there a better way to create tooltip DOM container?
chart.tooltip.refresh.apply(chart.tooltip, [this.point]);
chart.tooltip.hide(0);
}
setContext(this);
return `<div id="${generateTooltipId(chart.index)}"></div>`;
};
chart.update({
tooltip: {
formatter,
useHTML: true
}
});
}
}, [chart]);
const node = chart && document.getElementById(generateTooltipId(chart.index));
return node && context
? ReactDOM.createPortal(children(context), node)
: null;
};
@dankremniov
Copy link
Author

@danimaxi54, can you provide an example of what you need to achieve with such tooltip (e.g. should it contain interactive elements or just static text)? There is a way to achieve dynamic width/height by rendering to string within formatter; however, all the event listeners won't be attached declaratively as in the example above.

@dankremniov
Copy link
Author

@galangel, in case of outside: true the tooltip is hidden due to:

tooltip: {
  style: {
    pointerEvents: "auto"
  }
}

However, this is needed for the tooltip to be clickable. Unfortunately, I can't suggest any solution if you need both outside: true and clickable tooltip.

@borglin
Copy link

borglin commented May 9, 2023

Is there a way to have the tooltip adapt to the content for each point? I'd like to render different content based on the based on the data for each point. Ideally I would also be able to render a different tooltip if the point is clicked. Currently the tooltip gets the dimensions from the first rendered tooltip and then the same dimensions are used for every other point. I've tried using tooltip.refresh(...) but I haven't seen any difference.

Example, the second point has different tooltip content than the other: https://codesandbox.io/s/highcharts-react-tooltip-forked-jjxo43?file=/src/Chart.tsx

@knutmarius
Copy link

@borglin I've also used this gist as a way of being able to render complex React components inside a highcharts tooltip, and I think i was able to make a fairly decent solution for the height problem. I realized that the SVG container rendered by highcharts that was having a static height, and the solution I found was to just set opacity: 0 on the options.tooltip.style, and instead make a custom TooltipContainer component that wraps the content of the Tooltips. Hope this could help anyone with similar issues.

BTW: Also noticed that the error that is thrown from tooltip.refresh function went away after upgrading to Highcharts 11. Here is an updated sandbox:

https://codesandbox.io/s/highcharts-react-tooltip-forked-s6xyqc?file=/src/TooltipContainer.tsx

@dankremniov
Copy link
Author

Thanks @knutmarius - great solution!

I think this addresses @danimaxi54 question too.

@knutmarius
Copy link

Small update: As I was trying to roll out this solution to all our charts, I hit a roadblock when I came to some gannt charts that were having <ReactHighcharts immutable={true} ... /> . When the immutable flag is set to true, it crashes inside highcharts when the tooltip tries to do chart.update(..)to set the formatter function. However, I cannot replicate this in the sandbox, but it's very reproducable with all the charts in my application. Not sure why it acts differently from the sandbox. If the chart is rendered as immutable it crashes. Is this something that you have experienced before?

I started thinking about a solution that would not involve using chart.update, but rather set the formatter function on the chart options object at initialization, but then the rapid updating of the context in state causes the entire chart to rerender as well, so that would need to be solved. Is this a solution you have considered as well?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment