diff --git a/frontend/src/components/dashboard/chart-tooltip.tsx b/frontend/src/components/dashboard/chart-tooltip.tsx index 56bbd73d..b9ae3b2b 100644 --- a/frontend/src/components/dashboard/chart-tooltip.tsx +++ b/frontend/src/components/dashboard/chart-tooltip.tsx @@ -1,3 +1,5 @@ +import { useEffect, useRef } from 'react'; + import { formatNumber } from '@/lib/utils/format'; export type ChartTooltipPayloadEntry = { @@ -11,14 +13,39 @@ export const ChartTooltip = ({ formatter, label, labelFormatter, + onFirstActive, payload, + sessionKey, }: { active?: boolean; formatter?: (value: number, name: string) => string; label?: string; labelFormatter?: (label: string) => string; + onFirstActive?: () => void; payload?: Array; + sessionKey?: number; }) => { + // Store in ref to avoid stale closures in effects + const onFirstActiveRef = useRef(onFirstActive); + useEffect(() => { + onFirstActiveRef.current = onFirstActive; + }, [onFirstActive]); + + const shownInSessionRef = useRef(false); + + // New mouse-entry session: reset "already shown" tracking + useEffect(() => { + shownInSessionRef.current = false; + }, [sessionKey]); + + // Notify parent the moment the tooltip first becomes visible in this session + useEffect(() => { + if (active && !shownInSessionRef.current) { + shownInSessionRef.current = true; + onFirstActiveRef.current?.(); + } + }, [active]); + if (!active || !payload?.length) { return null; } diff --git a/frontend/src/pages/dashboard/dashboard-analytics.tsx b/frontend/src/pages/dashboard/dashboard-analytics.tsx index a7f6c77f..1852385d 100644 --- a/frontend/src/pages/dashboard/dashboard-analytics.tsx +++ b/frontend/src/pages/dashboard/dashboard-analytics.tsx @@ -1,6 +1,6 @@ import { format } from 'date-fns'; import { ChevronRight, Clock, Loader2, Wrench } from 'lucide-react'; -import { useMemo, useState } from 'react'; +import { useMemo, useRef, useState } from 'react'; import { Area, AreaChart, Bar, BarChart, CartesianGrid, Tooltip, XAxis, YAxis } from 'recharts'; import type { FlowFragmentFragment, UsageStatsPeriod } from '@/graphql/types'; @@ -37,6 +37,37 @@ const formatDateLabel = (dateString: string): string => { const axisTickStyle = { fill: 'var(--color-muted-foreground)', fontSize: 12 }; +// Disables tooltip animation on chart entry and re-enables it only after the +// tooltip has rendered at the correct position for the first time in the session. +// This prevents the tooltip from flying from (0,0) to the cursor on entry, +// while keeping smooth follow animation for all subsequent movements. +const useChartTooltipAnimation = () => { + const [isAnimationActive, setIsAnimationActive] = useState(false); + const [sessionKey, setSessionKey] = useState(0); + const rafRef = useRef(undefined); + + const onMouseEnter = () => { + cancelAnimationFrame(rafRef.current!); + setIsAnimationActive(false); + setSessionKey((k) => k + 1); + }; + + const onMouseLeave = () => { + cancelAnimationFrame(rafRef.current!); + setIsAnimationActive(false); + }; + + // Called by ChartTooltip the first time it becomes visible in this session. + // One rAF ensures the tooltip has painted at its initial position before + // animation is re-enabled for subsequent cursor movements. + const onFirstActive = () => { + cancelAnimationFrame(rafRef.current!); + rafRef.current = requestAnimationFrame(() => setIsAnimationActive(true)); + }; + + return { isAnimationActive, onFirstActive, onMouseEnter, onMouseLeave, sessionKey }; +}; + export const DashboardAnalytics = ({ period }: { period: UsageStatsPeriod }) => { const { data: usageByPeriodData, loading: usageByPeriodLoading } = useUsageStatsByPeriodQuery({ variables: { period }, @@ -52,6 +83,11 @@ export const DashboardAnalytics = ({ period }: { period: UsageStatsPeriod }) => }); const { data: flowsData } = useFlowsQuery(); + const flowsTooltip = useChartTooltipAnimation(); + const toolcallsTooltip = useChartTooltipAnimation(); + const tokenUsageTooltip = useChartTooltipAnimation(); + const costTooltip = useChartTooltipAnimation(); + const flowsById = useMemo(() => { const map = new Map(); (flowsData?.flows ?? []).forEach((flow) => { @@ -96,7 +132,11 @@ export const DashboardAnalytics = ({ period }: { period: UsageStatsPeriod }) => loading={flowsByPeriodLoading} title="Flows Activity Over Time" > - + tickMargin={8} /> } + content={ + + } cursor={{ fill: 'var(--color-muted-foreground)', fillOpacity: 0.1 }} + isAnimationActive={flowsTooltip.isAnimationActive} /> loading={toolcallsByPeriodLoading} title="Tool Calls Over Time" > - + tickMargin={8} /> } + content={ + + } cursor={{ fill: 'var(--color-muted-foreground)', fillOpacity: 0.1 }} + isAnimationActive={toolcallsTooltip.isAnimationActive} /> loading={usageByPeriodLoading} title="Token Usage Over Time" > - + formatTokenCount(value)} labelFormatter={formatDateLabel} + onFirstActive={tokenUsageTooltip.onFirstActive} + sessionKey={tokenUsageTooltip.sessionKey} /> } + isAnimationActive={tokenUsageTooltip.isAnimationActive} /> loading={usageByPeriodLoading} title="Cost Over Time" > - + formatCost(value)} labelFormatter={formatDateLabel} + onFirstActive={costTooltip.onFirstActive} + sessionKey={costTooltip.sessionKey} /> } + isAnimationActive={costTooltip.isAnimationActive} />