From 42cebfa26d5a197e654f664f859097dfaa8855ef Mon Sep 17 00:00:00 2001 From: "dak-agent[bot]" <284037069+dak-agent[bot]@users.noreply.github.com> Date: Tue, 9 Jun 2026 17:54:32 +0000 Subject: [PATCH 1/2] feat(frontend): smooth TPS chart transitions on new datapoints Tween the trailing tip of the TPS line from the previous value to the newly received value over ~400ms (one block) using requestAnimationFrame instead of letting recharts snap the chart whenever useTps emits a new point. If a new datapoint arrives mid-tween, the animation restarts from the current interpolated position so the line stays continuous. --- .../network-activity-tracker/tps-chart.tsx | 6 +- frontend/hooks/use-smoothed-tps-history.ts | 107 ++++++++++++++++++ 2 files changed, 111 insertions(+), 2 deletions(-) create mode 100644 frontend/hooks/use-smoothed-tps-history.ts diff --git a/frontend/components/network-activity-tracker/tps-chart.tsx b/frontend/components/network-activity-tracker/tps-chart.tsx index 1394b5a..2609ff3 100644 --- a/frontend/components/network-activity-tracker/tps-chart.tsx +++ b/frontend/components/network-activity-tracker/tps-chart.tsx @@ -8,6 +8,7 @@ import { ChartTooltip, ChartTooltipContent, } from '@/components/ui/chart' +import { useSmoothedTpsHistory } from '@/hooks/use-smoothed-tps-history' import { useTotalTransactions } from '@/hooks/use-total-transactions' import { useTps } from '@/hooks/use-tps' import { formatRelativeTime, formatTimeHMS } from '@/lib/timestamp' @@ -23,8 +24,9 @@ const chartConfig = { export function TpsChart() { const { currentTps, peakTps, history } = useTps() + const smoothedHistory = useSmoothedTpsHistory(history) const totalTransactions = useTotalTransactions() - const hasData = history.length > 0 + const hasData = smoothedHistory.length > 0 return (
@@ -59,7 +61,7 @@ export function TpsChart() { className="h-full min-w-2xl w-full p-0" > diff --git a/frontend/hooks/use-smoothed-tps-history.ts b/frontend/hooks/use-smoothed-tps-history.ts new file mode 100644 index 0000000..36efbb6 --- /dev/null +++ b/frontend/hooks/use-smoothed-tps-history.ts @@ -0,0 +1,107 @@ +'use client' + +import { useEffect, useRef, useState } from 'react' +import type { TpsDataPoint } from '@/hooks/use-tps' + +/** + * Matches the backend's TPS cadence (~one update per block at ~400ms). + * Picking the inter-arrival time lets the tween finish right as the next + * datapoint arrives, producing continuous motion instead of a snap. + */ +const ANIMATION_DURATION_MS = 400 + +function easeOutCubic(t: number): number { + return 1 - (1 - t) ** 3 +} + +interface AnimationState { + startTime: number + startValue: number + startTimestamp: number + targetValue: number + targetTimestamp: number +} + +function pointAt(anim: AnimationState, now: number): TpsDataPoint { + const t = Math.min(1, (now - anim.startTime) / ANIMATION_DURATION_MS) + const eased = easeOutCubic(t) + return { + tps: anim.startValue + (anim.targetValue - anim.startValue) * eased, + timestamp: + anim.startTimestamp + + (anim.targetTimestamp - anim.startTimestamp) * eased, + } +} + +/** + * Tweens the trailing tip of a TPS history so the chart extends smoothly to + * each new datapoint rather than snapping. When a new point arrives mid-tween, + * the tween restarts from the current interpolated position so the line never + * jumps. + */ +export function useSmoothedTpsHistory(history: TpsDataPoint[]): TpsDataPoint[] { + const [rendered, setRendered] = useState(history) + const animRef = useRef(null) + const rafRef = useRef(null) + + useEffect(() => { + if (history.length === 0) { + animRef.current = null + setRendered([]) + return + } + + const latest = history[history.length - 1] + + if ( + animRef.current !== null && + animRef.current.targetTimestamp === latest.timestamp + ) { + return + } + + const prev = history[history.length - 2] + const now = performance.now() + const tip: TpsDataPoint = + animRef.current !== null + ? pointAt(animRef.current, now) + : { + tps: prev?.tps ?? latest.tps, + timestamp: prev?.timestamp ?? latest.timestamp, + } + + animRef.current = { + startTime: now, + startValue: tip.tps, + startTimestamp: tip.timestamp, + targetValue: latest.tps, + targetTimestamp: latest.timestamp, + } + + const head = history.slice(0, -1) + + const tick = () => { + const anim = animRef.current + if (anim === null) return + const current = pointAt(anim, performance.now()) + setRendered([...head, current]) + if (performance.now() - anim.startTime < ANIMATION_DURATION_MS) { + rafRef.current = requestAnimationFrame(tick) + } else { + rafRef.current = null + } + } + + if (rafRef.current !== null) cancelAnimationFrame(rafRef.current) + rafRef.current = requestAnimationFrame(tick) + }, [history]) + + useEffect( + () => () => { + if (rafRef.current !== null) cancelAnimationFrame(rafRef.current) + }, + [], + ) + + return rendered +} From c9f7b2e72ded42aa52ea4f89ecee438d5bc2d548 Mon Sep 17 00:00:00 2001 From: "dak-agent[bot]" <284037069+dak-agent[bot]@users.noreply.github.com> Date: Tue, 9 Jun 2026 18:03:32 +0000 Subject: [PATCH 2/2] feat(frontend): slide TPS chart x-axis ticks instead of snapping MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Switch the XAxis to a continuous numeric scale and pin both edges of the domain to the smoothly-advancing tip — left edge at `latest - 5min`, right edge at `latest`. With the previous category axis, every new datapoint re-spaced the entire tick set; now ticks are placed at fixed 30-second absolute boundaries within the window, so their domain values are stable and their pixel positions slide continuously with the tween. Anchoring the left edge to `latest - 5min` (instead of the oldest history point) also removes the jolt that happened every time `useTps` dropped an expired point. Old data outside the window is clipped via `allowDataOverflow`. Export TPS_HISTORY_DURATION_MS from use-tps so the chart's visible window stays in sync with the hook's retention. --- .../network-activity-tracker/tps-chart.tsx | 42 ++++++++++++++++++- frontend/hooks/use-tps.ts | 2 +- 2 files changed, 42 insertions(+), 2 deletions(-) diff --git a/frontend/components/network-activity-tracker/tps-chart.tsx b/frontend/components/network-activity-tracker/tps-chart.tsx index 2609ff3..2b58aef 100644 --- a/frontend/components/network-activity-tracker/tps-chart.tsx +++ b/frontend/components/network-activity-tracker/tps-chart.tsx @@ -1,6 +1,7 @@ 'use client' import Image from 'next/image' +import { useMemo } from 'react' import { Area, AreaChart, CartesianGrid, XAxis, YAxis } from 'recharts' import { type ChartConfig, @@ -10,7 +11,7 @@ import { } from '@/components/ui/chart' import { useSmoothedTpsHistory } from '@/hooks/use-smoothed-tps-history' import { useTotalTransactions } from '@/hooks/use-total-transactions' -import { useTps } from '@/hooks/use-tps' +import { TPS_HISTORY_DURATION_MS, useTps } from '@/hooks/use-tps' import { formatRelativeTime, formatTimeHMS } from '@/lib/timestamp' import { formatIntNumber } from '@/lib/ui' import { NetworkActivityStats } from './network-activity-stats' @@ -22,12 +23,47 @@ const chartConfig = { }, } satisfies ChartConfig +/** + * Pin tick labels to absolute 30-second boundaries within the visible domain. + * Because each tick has a fixed timestamp, its pixel position is purely a + * function of the smoothly-advancing domain — so ticks slide left continuously + * with the chart instead of being re-picked by recharts when the data set + * grows. Once a minute the leftmost tick slides off-screen and a new one + * appears at the right edge. + */ +const TICK_INTERVAL_MS = 30_000 + +function buildTicks(minTimestamp: number, maxTimestamp: number): number[] { + const ticks: number[] = [] + let t = Math.floor(maxTimestamp / TICK_INTERVAL_MS) * TICK_INTERVAL_MS + while (t >= minTimestamp) { + ticks.push(t) + t -= TICK_INTERVAL_MS + } + return ticks.reverse() +} + export function TpsChart() { const { currentTps, peakTps, history } = useTps() const smoothedHistory = useSmoothedTpsHistory(history) const totalTransactions = useTotalTransactions() const hasData = smoothedHistory.length > 0 + // Anchor both edges of the domain to the smoothly-advancing tip so the + // axis scrolls continuously. Anchoring the left edge to `latest - window` + // (instead of the oldest history point) prevents a jump at the left every + // time `useTps` drops an expired point. + const xDomain = useMemo<[number, number] | undefined>(() => { + if (smoothedHistory.length === 0) return undefined + const latest = smoothedHistory[smoothedHistory.length - 1].timestamp + return [latest - TPS_HISTORY_DURATION_MS, latest] + }, [smoothedHistory]) + + const xTicks = useMemo(() => { + if (xDomain === undefined) return undefined + return buildTicks(xDomain[0], xDomain[1]) + }, [xDomain]) + return (
@@ -79,12 +115,16 @@ export function TpsChart() {