import React from 'react';
import { LegendShape } from '../legend/ChartLegend';
import {
ChartTooltipHeader,
ChartTooltipItem,
ChartTooltipItemsContainer,
ChartTooltipPortal,
ChartTooltipSeparator,
TooltipHeader,
} from '../common/ChartTooltip';
import { LineTimeSerieChartTooltipProps } from './LineTimeSerieChart.types';
import { getCurrentlyHoveredChartId } from './useChartHover';
import { formatISONumber } from '../../../utils';
/**
* Formats a numeric value for tooltip display
* - Non-finite values (NaN, null, undefined) → "-"
* - Zero → "0" with unit
* - Large values (>= 1000) → compact notation (1k, 1M)
* - Normal values (1-999) → up to 2 decimal places
* - Small values (0.01-0.99) → 2 decimal places
* - Very small values (< 0.01) → scientific notation (e.g., 4.7e-5)
*/
export const formatTooltipValue = (
value: number,
unitLabel?: string,
): string => {
if (!Number.isFinite(value)) return '-';
const formatted = formatISONumber(value, { fixedDecimals: true, compact: true });
return `${formatted}${unitLabel ? ` ${unitLabel}` : ''}`;
};
/**
* Custom tooltip component for LineTimeSerieChart
* Handles sorting, separator placement for symmetrical charts, and value formatting
*/
export const LineTimeSerieChartTooltip: React.FC<
LineTimeSerieChartTooltipProps
> = ({
unitLabel,
duration,
tooltipProps,
renderTooltip,
isSymmetrical,
belowSeriesLabels,
chartContainerRef,
chartId,
}) => {
const { active, payload, label, coordinate } = tooltipProps;
// Check at render time if this chart is the currently hovered one
// Using only the module-level variable avoids race conditions with React state updates
const isActiveChart = getCurrentlyHoveredChartId() === chartId;
if (!active || !payload || !payload.length || !label || !isActiveChart)
return null;
const tooltipContent = renderTooltip ? (
renderTooltip(tooltipProps, unitLabel, duration)
) : (
<>
{(() => {
// Sort payload: above series first (descending), then below series (ascending by absolute value)
const sortedPayload = [...payload].sort((a, b) => {
const aIsBelow = belowSeriesLabels?.has(a.name) ?? false;
const bIsBelow = belowSeriesLabels?.has(b.name) ?? false;
// Above series come before below series
if (aIsBelow !== bIsBelow) {
return aIsBelow ? 1 : -1;
}
// Within the same group:
// - Above series: higher values first (descending)
// - Below series: higher absolute values last (ascending)
if (aIsBelow) {
return Math.abs(a.value) - Math.abs(b.value);
}
return Math.abs(b.value) - Math.abs(a.value);
});
// Find the transition point between above and below series
const separatorIndex = sortedPayload.findIndex((entry) =>
belowSeriesLabels?.has(entry.name),
);
const hasBothAboveAndBelow =
isSymmetrical &&
belowSeriesLabels &&
belowSeriesLabels.size > 0 &&
separatorIndex > 0 &&
separatorIndex < sortedPayload.length;
return sortedPayload.map((entry, index) => {
const legendIcon = (
);
const formattedValue = formatTooltipValue(entry.value, unitLabel);
return (
{/* Add separator between above and below series for symmetrical charts */}
{hasBothAboveAndBelow && index === separatorIndex && (
)}
);
});
})()}
>
);
return (
{tooltipContent}
);
};