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

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