mirror of
https://github.com/vxcontrol/pentagi.git
synced 2026-08-28 05:56:42 +00:00
feat: implement tooltip animation for chart components to enhance user experience
- Added a custom hook to manage tooltip animation state and session tracking. - Updated ChartTooltip component to handle first active state and session key. - Integrated tooltip animation logic into various chart components in the DashboardAnalytics page.
This commit is contained in:
@@ -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<ChartTooltipPayloadEntry>;
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -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<number | undefined>(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<string, FlowFragmentFragment>();
|
||||
(flowsData?.flows ?? []).forEach((flow) => {
|
||||
@@ -96,7 +132,11 @@ export const DashboardAnalytics = ({ period }: { period: UsageStatsPeriod }) =>
|
||||
loading={flowsByPeriodLoading}
|
||||
title="Flows Activity Over Time"
|
||||
>
|
||||
<BarChart data={flowsChartData}>
|
||||
<BarChart
|
||||
data={flowsChartData}
|
||||
onMouseEnter={flowsTooltip.onMouseEnter}
|
||||
onMouseLeave={flowsTooltip.onMouseLeave}
|
||||
>
|
||||
<CartesianGrid
|
||||
className="stroke-border"
|
||||
strokeDasharray="3 3"
|
||||
@@ -112,8 +152,15 @@ export const DashboardAnalytics = ({ period }: { period: UsageStatsPeriod }) =>
|
||||
tickMargin={8}
|
||||
/>
|
||||
<Tooltip
|
||||
content={<ChartTooltip labelFormatter={formatDateLabel} />}
|
||||
content={
|
||||
<ChartTooltip
|
||||
labelFormatter={formatDateLabel}
|
||||
onFirstActive={flowsTooltip.onFirstActive}
|
||||
sessionKey={flowsTooltip.sessionKey}
|
||||
/>
|
||||
}
|
||||
cursor={{ fill: 'var(--color-muted-foreground)', fillOpacity: 0.1 }}
|
||||
isAnimationActive={flowsTooltip.isAnimationActive}
|
||||
/>
|
||||
<Bar
|
||||
dataKey="flows"
|
||||
@@ -143,7 +190,11 @@ export const DashboardAnalytics = ({ period }: { period: UsageStatsPeriod }) =>
|
||||
loading={toolcallsByPeriodLoading}
|
||||
title="Tool Calls Over Time"
|
||||
>
|
||||
<BarChart data={toolcallsChartData}>
|
||||
<BarChart
|
||||
data={toolcallsChartData}
|
||||
onMouseEnter={toolcallsTooltip.onMouseEnter}
|
||||
onMouseLeave={toolcallsTooltip.onMouseLeave}
|
||||
>
|
||||
<CartesianGrid
|
||||
className="stroke-border"
|
||||
strokeDasharray="3 3"
|
||||
@@ -159,8 +210,15 @@ export const DashboardAnalytics = ({ period }: { period: UsageStatsPeriod }) =>
|
||||
tickMargin={8}
|
||||
/>
|
||||
<Tooltip
|
||||
content={<ChartTooltip labelFormatter={formatDateLabel} />}
|
||||
content={
|
||||
<ChartTooltip
|
||||
labelFormatter={formatDateLabel}
|
||||
onFirstActive={toolcallsTooltip.onFirstActive}
|
||||
sessionKey={toolcallsTooltip.sessionKey}
|
||||
/>
|
||||
}
|
||||
cursor={{ fill: 'var(--color-muted-foreground)', fillOpacity: 0.1 }}
|
||||
isAnimationActive={toolcallsTooltip.isAnimationActive}
|
||||
/>
|
||||
<Bar
|
||||
dataKey="count"
|
||||
@@ -177,7 +235,11 @@ export const DashboardAnalytics = ({ period }: { period: UsageStatsPeriod }) =>
|
||||
loading={usageByPeriodLoading}
|
||||
title="Token Usage Over Time"
|
||||
>
|
||||
<AreaChart data={usageChartData}>
|
||||
<AreaChart
|
||||
data={usageChartData}
|
||||
onMouseEnter={tokenUsageTooltip.onMouseEnter}
|
||||
onMouseLeave={tokenUsageTooltip.onMouseLeave}
|
||||
>
|
||||
<CartesianGrid
|
||||
className="stroke-border"
|
||||
strokeDasharray="3 3"
|
||||
@@ -198,8 +260,11 @@ export const DashboardAnalytics = ({ period }: { period: UsageStatsPeriod }) =>
|
||||
<ChartTooltip
|
||||
formatter={(value) => formatTokenCount(value)}
|
||||
labelFormatter={formatDateLabel}
|
||||
onFirstActive={tokenUsageTooltip.onFirstActive}
|
||||
sessionKey={tokenUsageTooltip.sessionKey}
|
||||
/>
|
||||
}
|
||||
isAnimationActive={tokenUsageTooltip.isAnimationActive}
|
||||
/>
|
||||
<Area
|
||||
dataKey="tokensIn"
|
||||
@@ -228,7 +293,11 @@ export const DashboardAnalytics = ({ period }: { period: UsageStatsPeriod }) =>
|
||||
loading={usageByPeriodLoading}
|
||||
title="Cost Over Time"
|
||||
>
|
||||
<AreaChart data={usageChartData}>
|
||||
<AreaChart
|
||||
data={usageChartData}
|
||||
onMouseEnter={costTooltip.onMouseEnter}
|
||||
onMouseLeave={costTooltip.onMouseLeave}
|
||||
>
|
||||
<CartesianGrid
|
||||
className="stroke-border"
|
||||
strokeDasharray="3 3"
|
||||
@@ -249,8 +318,11 @@ export const DashboardAnalytics = ({ period }: { period: UsageStatsPeriod }) =>
|
||||
<ChartTooltip
|
||||
formatter={(value) => formatCost(value)}
|
||||
labelFormatter={formatDateLabel}
|
||||
onFirstActive={costTooltip.onFirstActive}
|
||||
sessionKey={costTooltip.sessionKey}
|
||||
/>
|
||||
}
|
||||
isAnimationActive={costTooltip.isAnimationActive}
|
||||
/>
|
||||
<Area
|
||||
dataKey="costIn"
|
||||
|
||||
Reference in New Issue
Block a user