"use client"; import { motion } from "motion/react"; import { memo, useEffect, useMemo, useState } from "react"; import { createPortal } from "react-dom"; import { cn } from "@/lib/utils"; import { useChart, useChartStable } from "./chart-context"; export interface BarXAxisProps { /** Width of the date ticker box for fade calculation. Default: 50 */ tickerHalfWidth?: number; /** Whether to show all labels or skip some for dense data. Default: false */ showAllLabels?: boolean; /** Maximum number of labels to show. Default: 12 */ maxLabels?: number; } interface BarXAxisLabelProps { label: string; x: number; crosshairX: number | null; isHovering: boolean; tickerHalfWidth: number; } function BarXAxisLabel({ label, x, crosshairX, isHovering, tickerHalfWidth, }: BarXAxisLabelProps) { const fadeBuffer = 20; const fadeRadius = tickerHalfWidth + fadeBuffer; let opacity = 1; if (isHovering && crosshairX !== null) { const distance = Math.abs(x - crosshairX); if (distance < tickerHalfWidth) { opacity = 0; } else if (distance < fadeRadius) { opacity = (distance - tickerHalfWidth) / fadeBuffer; } } // Zero-width container approach for perfect centering return (
{label}
); } export function BarXAxis(props: BarXAxisProps) { const { containerRef, barScale } = useChartStable(); const [mounted, setMounted] = useState(false); useEffect(() => { setMounted(true); }, []); const container = containerRef.current; if (!(mounted && container)) { return null; } if (!barScale) { return null; } return ; } const BarXAxisInner = memo(function BarXAxisInner({ tickerHalfWidth = 50, showAllLabels = false, maxLabels = 12, container, }: BarXAxisProps & { container: HTMLDivElement }) { const { margin, tooltipData, barScale, bandWidth, barXAccessor, data } = useChart(); // Generate labels for each bar const labelsToShow = useMemo(() => { if (!(barScale && bandWidth && barXAccessor)) { return []; } const allLabels = data.map((d) => { const label = barXAccessor(d); const bandX = barScale(label) ?? 0; // Center the label under the bar group const x = bandX + bandWidth / 2 + margin.left; return { label, x }; }); // If showAllLabels is true or we have fewer than maxLabels, show all if (showAllLabels || allLabels.length <= maxLabels) { return allLabels; } // Otherwise, skip some labels to avoid crowding const step = Math.ceil(allLabels.length / maxLabels); return allLabels.filter((_, i) => i % step === 0); }, [ barScale, bandWidth, barXAccessor, data, margin.left, showAllLabels, maxLabels, ]); const isHovering = tooltipData !== null; const crosshairX = tooltipData ? tooltipData.x + margin.left : null; return createPortal(
{labelsToShow.map((item) => ( ))}
, container ); }); BarXAxis.displayName = "BarXAxis"; export default BarXAxis;