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:

// 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.

Here is an example of a Line Chart with a hidden trigger, because the line itself is not a good enough trigger:

0
2
4
6
8
10
12
4/30
5/5
5/9
// 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>
  ));
}