Live Line Chart

Real-time streaming line chart with smooth scroll animation for live data feeds

Installation

pnpm dlx shadcn@latest add "https://vritti.thesatyajit.com/r/live-line-chart"

Dependencies

pnpm add @visx/responsive @visx/scale @visx/shape @visx/group @visx/event d3-array motion

Examples

Heartrate

"use client"
import { useCallback, useEffect, useRef, useState } from "react"
import LiveLineChart, {
  LiveLine,
  LiveXAxis,
  LiveYAxis,
  type LiveLinePoint,
} from "@/components/vritti/live-line-chart"

function useHeartRateData(intervalMs: number) {
  const [data, setData] = useState<LiveLinePoint[]>([])
  const [value, setValue] = useState(72)
  const startRef = useRef(Date.now())

  useEffect(() => {
    const now = Date.now() / 1000
    const seed: LiveLinePoint[] = []
    for (let i = 59; i >= 0; i--) {
      seed.push({
        time: now - i * (intervalMs / 1000),
        value: 68 + Math.random() * 14,
      })
    }
    setData(seed)
    startRef.current = Date.now() - 30000 // start mid-warmup
  }, [intervalMs])

  useEffect(() => {
    const id = setInterval(() => {
      const elapsedSec = (Date.now() - startRef.current) / 1000
      const cycle = elapsedSec % 120 // 2-minute workout cycle

      let target: number
      if (cycle < 30) {
        // warmup: 68 → 85 BPM
        target = 68 + (cycle / 30) * 17
      } else if (cycle < 90) {
        // cardio: 140 → 178 BPM
        const t = (cycle - 30) / 60
        target = 140 + t * 38
      } else {
        // cooldown: 118 → 72 BPM
        const t = (cycle - 90) / 30
        target = 118 - t * 46
      }

      const bpm = Math.round(target + (Math.random() - 0.5) * 8)
      const now = Date.now() / 1000
      setValue(bpm)
      setData((prev) => [
        ...prev.filter((p) => p.time >= now - 120),
        { time: now, value: bpm },
      ])
    }, intervalMs)
    return () => clearInterval(id)
  }, [intervalMs])

  return { data, value }
}

export function LiveLineChartHeartRateDemo() {
  const { data, value } = useHeartRateData(500)
  const formatBpm = useCallback((v: number) => `${Math.round(v)} BPM`, [])

  return (
    <div className="w-full p-4">
      <LiveLineChart
        data={data}
        value={value}
        window={60}
        margin={{ top: 16, right: 16, bottom: 40, left: 72 }}
        style={{ height: 260 }}
      >
        <LiveLine
          dataKey="value"
          stroke="hsl(0 84% 60%)"
          formatValue={formatBpm}
        />
        <LiveXAxis />
        <LiveYAxis formatValue={formatBpm} position="left" />
      </LiveLineChart>
    </div>
  )
}

Price

"use client"
import { useCallback, useEffect, useRef, useState } from "react"
import LiveLineChart, {
  LiveLine,
  LiveXAxis,
  LiveYAxis,
  type LiveLinePoint,
} from "@/components/vritti/live-line-chart"

function useLiveData(initialPrice: number, intervalMs: number) {
  const [data, setData] = useState<LiveLinePoint[]>([])
  const [value, setValue] = useState(initialPrice)
  const priceRef = useRef(initialPrice)
  const momentumRef = useRef(0)

  useEffect(() => {
    const nowSec = Date.now() / 1000
    const seed: LiveLinePoint[] = []
    let p = initialPrice
    let mom = 0
    for (let i = 30; i > 0; i--) {
      mom = mom * 0.92 + (Math.random() - 0.48) * 0.012
      p *= 1 + mom
      p = Math.max(p, 1)
      seed.push({
        time: nowSec - i * (intervalMs / 1000),
        value: Math.round(p * 100) / 100,
      })
    }
    priceRef.current = p
    momentumRef.current = mom
    setData(seed)
    setValue(p)
  }, [initialPrice, intervalMs])

  useEffect(() => {
    const id = setInterval(() => {
      momentumRef.current =
        momentumRef.current * 0.88 + (Math.random() - 0.48) * 0.008
      momentumRef.current *= 0.995
      priceRef.current *= 1 + momentumRef.current
      priceRef.current = Math.max(priceRef.current, 1)
      const rounded = Math.round(priceRef.current * 100) / 100
      setData((prev) => {
        const cutoff = Date.now() / 1000 - 60
        return [
          ...prev.filter((p) => p.time >= cutoff),
          { time: Date.now() / 1000, value: rounded },
        ]
      })
      setValue(rounded)
    }, intervalMs)
    return () => clearInterval(id)
  }, [intervalMs])

  return { data, value }
}

export function LiveLineChartPriceDemo() {
  const { data, value } = useLiveData(142.5, 600)
  const formatUsd = useCallback((v: number) => `$${v.toFixed(2)}`, [])

  return (
    <div className="w-full p-4">
      <LiveLineChart
        data={data}
        margin={{ top: 16, right: 16, bottom: 40, left: 56 }}
        style={{ height: 260 }}
        value={value}
        window={30}
      >
        <LiveLine
          dataKey="value"
          formatValue={formatUsd}
          stroke="var(--chart-line-primary)"
        />
        <LiveXAxis />
        <LiveYAxis formatValue={formatUsd} position="left" />
      </LiveLineChart>
    </div>
  )
}

Server

"use client"
import { useEffect, useRef, useState } from "react"
import LiveLineChart, {
  LiveLine,
  LiveXAxis,
  LiveYAxis,
  type LiveLinePoint,
} from "@/components/vritti/live-line-chart"

type ServerPoint = { time: number; cpu: number; memory: number }

function useLiveServerData(intervalMs: number) {
  const [data, setData] = useState<ServerPoint[]>([])
  const [lastCpu, setLastCpu] = useState(35)
  const cpuRef = useRef(35)
  const memRef = useRef(58)
  const tickRef = useRef(0)

  useEffect(() => {
    const now = Date.now() / 1000
    const seed: ServerPoint[] = []
    let cpu = 35
    let mem = 58
    for (let i = 29; i >= 0; i--) {
      cpu = Math.max(5, Math.min(95, cpu + (Math.random() - 0.5) * 8))
      mem = Math.max(40, Math.min(85, mem + (Math.random() - 0.5) * 3))
      seed.push({ time: now - i * (intervalMs / 1000), cpu, memory: mem })
    }
    cpuRef.current = cpu
    memRef.current = mem
    setData(seed)
  }, [intervalMs])

  useEffect(() => {
    const id = setInterval(() => {
      tickRef.current++
      // simulate CPU spike every ~10 seconds
      if (tickRef.current % 12 === 0) {
        cpuRef.current = Math.min(95, cpuRef.current + 35 + Math.random() * 20)
      } else {
        cpuRef.current = Math.max(5, cpuRef.current * 0.85 + Math.random() * 8)
      }
      memRef.current = Math.max(
        40,
        Math.min(85, memRef.current + (Math.random() - 0.48) * 2),
      )
      const now = Date.now() / 1000
      const cpu = Math.round(cpuRef.current * 10) / 10
      const point: ServerPoint = {
        time: now,
        cpu,
        memory: Math.round(memRef.current * 10) / 10,
      }
      setLastCpu(cpu)
      setData((prev) => [
        ...prev.filter((p) => p.time >= now - 60),
        point,
      ])
    }, intervalMs)
    return () => clearInterval(id)
  }, [intervalMs])

  return { data, lastCpu }
}

export function LiveLineChartServerDemo() {
  const { data, lastCpu } = useLiveServerData(800)
  const formatPct = (v: number) => `${v.toFixed(1)}%`

  return (
    <div className="w-full p-4">
      <LiveLineChart
        data={data as unknown as LiveLinePoint[]}
        value={lastCpu}
        window={30}
        margin={{ top: 16, right: 16, bottom: 40, left: 56 }}
        style={{ height: 260 }}
      >
        <LiveLine
          dataKey="cpu"
          stroke="var(--chart-1)"
          formatValue={formatPct}
        />
        <LiveLine
          dataKey="memory"
          stroke="var(--chart-2)"
          formatValue={formatPct}
        />
        <LiveXAxis />
        <LiveYAxis formatValue={formatPct} position="left" />
      </LiveLineChart>
    </div>
  )
}