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() {