"use client";
import { AnimatePresence, motion } from "motion/react";
import { memo, useEffect, useMemo, useRef, useState } from "react";
import { createPortal } from "react-dom";
import { useChartStable } from "./chart-context";
// ---------------------------------------------------------------------------
// Interval picker (inspired by liveline's pickInterval)
// Finds a "nice" step size that keeps labels ~minGap pixels apart.
// Uses hysteresis: keeps the previous interval if it still fits, preventing
// jittery step changes when the range oscillates near a boundary.
// ---------------------------------------------------------------------------
function pickNiceInterval(
valRange: number,
chartHeight: number,
minGap: number,
prevInterval: number
): number {
if (valRange <= 0 || chartHeight <= 0) {
return 1;
}
const pxPerUnit = chartHeight / valRange;
// Keep previous interval if it still produces reasonable spacing
if (prevInterval > 0) {
const px = prevInterval * pxPerUnit;
if (px >= minGap * 0.5 && px <= minGap * 3) {
return prevInterval;
}
}
// Try multiple divisor sequences to find the best nice step
const divisorSets = [
[2, 2.5, 2],
[2, 2, 2.5],
[2.5, 2, 2],
];
let best = Number.POSITIVE_INFINITY;
for (const divs of divisorSets) {
let span = 10 ** Math.ceil(Math.log10(valRange));
let i = 0;
let d = divs[i % 3] ?? 2;
while ((span / d) * pxPerUnit >= minGap) {
span /= d;
i++;
d = divs[i % 3] ?? 2;
}
if (span < best) {
best = span;
}
}
return best === Number.POSITIVE_INFINITY ? valRange / 5 : best;
}
// ---------------------------------------------------------------------------
// Edge fade: labels near the top/bottom of the chart area fade out
// ---------------------------------------------------------------------------
const EDGE_FADE_PX = 28;
function edgeOpacity(y: number, chartHeight: number): number {
const fromEdge = Math.min(y, chartHeight - y);
if (fromEdge >= EDGE_FADE_PX) {
return 1;
}
if (fromEdge <= 0) {
return 0;
}
return fromEdge / EDGE_FADE_PX;
}
// ---------------------------------------------------------------------------
// Component
// ---------------------------------------------------------------------------
export interface LiveYAxisProps {
/** Minimum pixel gap between labels. Default: 36 */
minGap?: number;
/** Position. Default: "left" */
position?: "left" | "right";
/** Value formatter */
formatValue?: (v: number) => string;
/** Allow decimal tick values. Default: true */
allowDecimals?: boolean;
}
const tickSpring = { type: "spring" as const, stiffness: 180, damping: 24 };
export function LiveYAxis(props: LiveYAxisProps) {
const { containerRef } = useChartStable();
const [mounted, setMounted] = useState(false);
useEffect(() => {
setMounted(true);
}, []);
const container = containerRef.current;
if (!(mounted && container)) {
return null;
}
return