Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 94 additions & 2 deletions frontend/components/network-activity-tracker/tps-chart.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
'use client'

import Image from 'next/image'
import { useEffect, useState } from 'react'
import { Area, AreaChart, CartesianGrid, XAxis, YAxis } from 'recharts'
import {
type ChartConfig,
Expand All @@ -9,7 +10,7 @@ import {
ChartTooltipContent,
} from '@/components/ui/chart'
import { useTotalTransactions } from '@/hooks/use-total-transactions'
import { useTps } from '@/hooks/use-tps'
import { type TpsDataPoint, useTps } from '@/hooks/use-tps'
import { formatRelativeTime, formatTimeHMS } from '@/lib/timestamp'
import { formatIntNumber } from '@/lib/ui'
import { NetworkActivityStats } from './network-activity-stats'
Expand All @@ -21,10 +22,96 @@ const chartConfig = {
},
} satisfies ChartConfig

/** Maximum visible time window of the chart, matching the TPS history retained. */
const CHART_WINDOW_MS = 5 * 60 * 1000

/** Smallest window to show early on, so the chart starts zoomed in rather than mostly empty. */
const MIN_WINDOW_MS = 3 * 1000

/**
* Drives a smoothly advancing "now" so the chart's x-domain slides
* continuously instead of jumping by one slot as each point arrives.
* Updates every animation frame for the smoothest motion; rAF auto-pauses
* when the tab is hidden.
*/
function useSlidingNow(): number {
const [now, setNow] = useState(() => Date.now())

useEffect(() => {
let raf: number
const tick = () => {
setNow(Date.now())
raf = requestAnimationFrame(tick)
}
raf = requestAnimationFrame(tick)
return () => cancelAnimationFrame(raf)
}, [])

return now
}

/**
* Returns the history with its newest segment progressively "drawn": instead of
* the last point appearing fully-formed, a head vertex travels from the previous
* point to the newest one over the inter-arrival interval. `now` advances every
* frame, so the head moves smoothly. Once it reaches the newest point the segment
* is complete and the next arrival starts drawing the following one.
*/
function drawHistory(history: TpsDataPoint[], now: number): TpsDataPoint[] {
if (history.length < 2) return history

const target = history[history.length - 1]
const from = history[history.length - 2]
const duration = target.timestamp - from.timestamp
if (duration <= 0) return history

const progress = Math.min(1, Math.max(0, (now - target.timestamp) / duration))
const head: TpsDataPoint = {
timestamp: from.timestamp + (target.timestamp - from.timestamp) * progress,
tps: from.tps + (target.tps - from.tps) * progress,
}

return [...history.slice(0, -1), head]
}

/** Nice, human-friendly tick steps in ms for the relative-time x-axis. */
const TICK_STEPS_MS = [1, 2, 5, 10, 15, 30, 60, 120, 300].map((s) => s * 1000)

/**
* Builds evenly-spaced ticks anchored at the sliding edge (`end`) and stepping
* backwards. Anchoring at `end` keeps a single "now" pinned to the far right;
* supplying ticks explicitly also avoids recharts' auto-generated ticks landing
* both at the edge and at the current second (which both format as "now").
*/
function buildTicks(start: number, end: number): number[] {
const step =
TICK_STEPS_MS.find((s) => s >= (end - start) / 6) ??
TICK_STEPS_MS[TICK_STEPS_MS.length - 1]

const ticks: number[] = []
for (let t = end; t >= start; t -= step) {
ticks.push(t)
}
return ticks.reverse()
}

export function TpsChart() {
const { currentTps, peakTps, history } = useTps()
const totalTransactions = useTotalTransactions()
const hasData = history.length > 0
const now = useSlidingNow()

// Start zoomed in to the earliest data point and expand the window as data
// accumulates, capping at CHART_WINDOW_MS once we have 5 minutes of history.
const earliest = history[0]?.timestamp ?? now
const windowStart = Math.max(
now - CHART_WINDOW_MS,
Math.min(earliest, now - MIN_WINDOW_MS),
)

// Progressively draw the newest segment rather than snapping it into place.
const chartData = drawHistory(history, now)
const ticks = buildTicks(windowStart, now)

return (
<div className="flex flex-col h-full">
Expand Down Expand Up @@ -59,7 +146,7 @@ export function TpsChart() {
className="h-full min-w-2xl w-full p-0"
>
<AreaChart
data={history}
data={chartData}
margin={{ top: 8, right: 8, bottom: 0, left: 0 }}
>
<defs>
Expand All @@ -77,6 +164,11 @@ export function TpsChart() {
<CartesianGrid stroke="var(--chart-grid)" vertical={false} />
<XAxis
dataKey="timestamp"
type="number"
scale="time"
domain={[windowStart, now]}
ticks={ticks}
allowDataOverflow={true}
tickLine={false}
axisLine={false}
tickMargin={8}
Expand Down
Loading
Loading