Custom tooltips
The main challenge we faced when creating tooltips for RosenCharts was balancing server and client rendering. This is because tooltips generally require client side interactivity (hovering, clicking, etc) - but we still wanted to keep the charts themselves rendered on the server by default.
The solution
For Bars, Pies, Donuts, Treemaps and other charts where the tooltip trigger is a div or svg with significant surface area, we can simply wrap our tooltip around each div or svg:
If you don't want to use our custom Tooltip component, you should be able to make this work with something like Shadcn/UI. In fact, our own tooltip is influenced by the Radix UI Tooltip.
// Bars with Rounded Right Corners */
{
data.map((d, index) => {
const barWidth = xScale(d.value);
const barHeight = yScale.bandwidth();
return (
<ClientTooltip key={index}>
<TooltipTrigger>
<div
style={{
position: "absolute",
left: "0",
top: `${yScale(d.key)}%`,
width: `${barWidth}%`,
height: `${barHeight}%`,
borderRadius: "0 6px 6px 0",
}}
className={`${d.color}`}
/>
</TooltipTrigger>
<TooltipContent>
<div>{d.key}</div>
<div className="text-gray-500 text-sm">{d.value}</div>
</TooltipContent>
</ClientTooltip>
);
});
}
Going even further
For charts where the tooltip trigger is not a div or svg with significant surface area (such as Line or Area Charts), we came up with a way for you to still be able to server-side render everything. The main idea is to have an invisible div in each important section of the chart, and then use that div as the tooltip trigger.
This is a bit of a hack, and we wouldn't recommend it for anything more than simple charts. As your Line/Area charts get more complex, consider creating a Tooltip specific to each purpose, or converting the charts to client components instead.
Here is an example of a Line Chart with a hidden trigger, because the line itself is not a good enough trigger:
// Line Chart with hidden trigger
{
data.map((d, index) => (
<ClientTooltip key={index}>
<TooltipTrigger>
{/* Purple Line */}
<path
key={index}
d={`M ${xScale(d.date)} ${yScale(d.value)} l 0.0001 0`}
vectorEffect="non-scaling-stroke"
strokeWidth="7"
strokeLinecap="round"
fill="none"
stroke="currentColor"
className="text-violet-300"
/>
<g className="group/tooltip">
{/* Vertical Tooltip marker on hover */}
<line
x1={xScale(d.date)}
y1={0}
x2={xScale(d.date)}
y2={100}
stroke="currentColor"
strokeWidth={1}
className="opacity-0 group-hover/tooltip:opacity-100 text-zinc-300 dark:text-zinc-700 transition-opacity"
vectorEffect="non-scaling-stroke"
style={{ pointerEvents: "none" }}
/>
{/* Invisible area closest to a specific point for the tooltip trigger */}
<rect
x={(() => {
const prevX = index > 0 ? xScale(data[index - 1].date) : xScale(d.date);
return (prevX + xScale(d.date)) / 2;
})()}
y={0}
width={(() => {
const prevX = index > 0 ? xScale(data[index - 1].date) : xScale(d.date);
const nextX = index < data.length - 1 ? xScale(data[index + 1].date) : xScale(d.date);
const leftBound = (prevX + xScale(d.date)) / 2;
const rightBound = (xScale(d.date) + nextX) / 2;
return rightBound - leftBound;
})()}
height={100}
fill="transparent"
/>
</g>
</TooltipTrigger>
<TooltipContent>
<div>
{d.date.toLocaleDateString("en-US", {
month: "short",
day: "2-digit",
})}
</div>
<div className="text-gray-500 text-sm">{d.value.toLocaleString("en-US")}</div>
</TooltipContent>
</ClientTooltip>
));
}