All files / src/components/Chart CustomTooltip.tsx

100% Statements 65/65
78.57% Branches 11/14
100% Functions 2/2
100% Lines 65/65

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 991x 1x     1x     1x                                 1x 36x 36x 36x 36x 36x 36x 36x 36x 36x 36x 36x 36x 36x 36x   34x 34x 34x 14x 14x 6x 34x 6x 6x 6x     6x   6x 6x 6x 6x     6x 6x 6x 6x   6x 6x   6x 6x 6x 6x   6x 6x 6x 6x 6x   6x 6x 6x 6x 6x   6x 6x 6x 6x 6x 6x 6x 28x 34x 36x     1x  
import ReactDOM from 'react-dom';
import { Tooltip } from 'recharts';
 
// Constants
import { ICONS_BY_KEY } from '@/constants/chart';
 
// Utils
import { formatResult } from '@/utils/statistic';
 
// Types
import { ChartKey } from '@/types/chart';
 
type CustomTooltipProps = {
  cursor?: boolean;
  isAnimationActive?: boolean;
  colorsByKey?: Record<string, string>;
  iconsByKey?: Record<string, React.ComponentType<{ className?: string }>>;
  modeKey?: ChartKey;
  containerRef?: React.RefObject<HTMLDivElement | null>;
  offset?: number;
  dotRef?: React.RefObject<Record<number, { cx: number; cy: number }>>;
  activeIndex?: number | null;
};
 
const CustomTooltip = ({
  cursor = false,
  isAnimationActive = false,
  colorsByKey,
  iconsByKey = ICONS_BY_KEY,
  modeKey,
  containerRef,
  offset = 16,
  dotRef,
  activeIndex = null,
}: CustomTooltipProps) => (
  <Tooltip
    cursor={cursor}
    isAnimationActive={isAnimationActive}
    content={({ active = false, payload = [], label = '', coordinate }) => {
      // Only render when tooltip is active, has data, and a valid container
      if (
        active &&
        activeIndex !== null &&
        !!payload.length &&
        coordinate &&
        containerRef?.current
      ) {
        const rect = containerRef.current.getBoundingClientRect();
        const activeDot = dotRef?.current?.[activeIndex ?? 0];
        const dotCy = activeDot?.cy ?? coordinate.y;
 
        // Adjust for container scroll
        const scrollLeft = containerRef.current.scrollLeft;
        // Calculate absolute position relative to container
        const left = rect.left + coordinate.x - scrollLeft;
        const top = rect.top + dotCy - offset;
        const row = payload[0]?.payload;
        const title = row?.title || '';
 
        // Render tooltip in a portal so it's positioned globally
        return ReactDOM.createPortal(
          <div
            className="fixed bg-primary-750 px-2.75 py-2 rounded-lg -translate-x-1/2 -translate-y-full pointer-events-none z-50 whitespace-nowrap"
            style={{ left, top }}
          >
            {title && (
              <p className="font-medium text-white text-5xs mb-1">{title}</p>
            )}
            <ul className="space-y-0.5 gap-1 flex flex-col -ml-0.5">
              {payload.map((entry: any, idx: number) => {
                const key = modeKey || entry.dataKey;
                const Icon = iconsByKey[key];
 
                return (
                  <li
                    key={idx}
                    className="flex flex-row items-center gap-2.5"
                    style={{ color: colorsByKey?.[entry.dataKey] }}
                  >
                    {Icon && <Icon className="w-6 h-6" />}
                    <span className="text-3xs font-medium text-white mt-0.5">
                      {formatResult(entry.value)}
                    </span>
                  </li>
                );
              })}
            </ul>
            <div className="absolute left-1/2 -bottom-1 transform -translate-x-1/2 w-0 h-0 border-l-[6px] border-r-[6px] border-t-[6px] border-l-transparent border-r-transparent border-t-primary-750" />
          </div>,
          document.body,
        );
      }
      return null;
    }}
  />
);
 
export default CustomTooltip;