Timeline Histogram

The portal's delivery timeline: stacked status volume with a drag-to-select scrubber, scale and frame tabs, and jump controls.

"use client"

import { useMemo, useState } from "react"

This is the timeline from the Repost portal, not a plain chart. Drag across the plot to select a range, drag the handles to resize it, use the frame tabs to zoom the visible window, and the chevrons to walk backwards and forwards through history. The selection is reported through onApplyRange so your feed can filter to it.

Installation

pnpm dlx shadcn@latest add @repost/timeline-histogram

Installs this item, every component it depends on, and the npm packages listed below.

Usage

useTimelineScale owns the visible window (scale + frame + jumping); the component owns selection. Feed it histogram slots, from useLogsHistogram (@repost/portal-react) or any source with the same shape.

components/timeline-histogram-demo.tsx
"use client"

import { useMemo, useState } from "react"
import {
  TIMELINE_SCALE_OPTIONS,
  TimelineHistogram,
  useTimelineScale,
  type TimelineRange,
  type TimelineSeries,
  type TimelineSlot,
} from "@/components/timeline-histogram"

const HOUR_MS = 60 * 60 * 1000

const SERIES: TimelineSeries[] = [
  { key: "2xx", label: "2xx", color: "var(--status-success)", statuses: ["2xx"] },
  { key: "3xx", label: "3xx", color: "var(--status-info)", statuses: ["3xx"] },
  { key: "4xx", label: "4xx", color: "var(--status-warning)", statuses: ["4xx"] },
  { key: "5xx", label: "5xx", color: "var(--status-error)", statuses: ["5xx"] },
]

/** Deterministic delivery volume so the preview never flickers between renders. */
function buildSlots(from: Date, to: Date, intervalSecs: number): TimelineSlot[] {
  const step = intervalSecs * 1000
  const slots: TimelineSlot[] = []
  for (let t = from.getTime(), i = 0; t < to.getTime(); t += step, i++) {
    const base = 40 + ((i * 37) % 90)
    const spike = i % 17 === 0 ? 60 : 0
    const counts = {
      "2xx": base + spike,
      "3xx": (i * 5) % 7,
      "4xx": i % 11 === 3 ? 6 + (i % 5) : i % 3,
      "5xx": i % 13 === 5 ? 9 + (i % 6) : 0,
    }
    slots.push({
      timestamp: new Date(t),
      total: Object.values(counts).reduce((sum, value) => sum + value, 0),
      counts,
    })
  }
  return slots
}

/** Start with a pinned range so the scrubber (glass overlay + handles) is visible. */
const initialSelection = (): TimelineRange => {
  const to = new Date()
  return { from: new Date(to.getTime() - 8 * HOUR_MS), to: new Date(to.getTime() - 2 * HOUR_MS) }
}

export function TimelineHistogramDemo() {
  const [selection, setSelection] = useState<TimelineRange | null>(initialSelection)

  const {
    scale,
    options,
    activeOptionKey,
    selectSpan,
    frameOptions,
    activeFrameKey,
    selectFrame,
    jump,
    canJumpBack,
    canJumpForward,
  } = useTimelineScale({
    selection,
    liveDateWindow: "today",
    planWindowDays: 90,
  })

  // One bucket per ~120 columns of the visible frame, like the portal.
  const intervalSecs = useMemo(
    () => Math.max(60, Math.round((scale.to.getTime() - scale.from.getTime()) / 120 / 1000)),
    [scale.from, scale.to]
  )

  const slots = useMemo(
    () => buildSlots(scale.from, scale.to, intervalSecs),
    [scale.from, scale.to, intervalSecs]
  )

  return (
    <TimelineHistogram
      slots={slots}
      intervalSecs={intervalSecs}
      dateFrom={scale.from}
      dateTo={scale.to}
      series={SERIES}
      isLoading={false}
      selection={selection}
      onApplyRange={setSelection}
      scaleOptions={options ?? TIMELINE_SCALE_OPTIONS}
      activeOptionKey={activeOptionKey}
      onSelectSpan={selectSpan}
      frameOptions={frameOptions}
      activeFrameKey={activeFrameKey}
      onSelectFrame={selectFrame}
      onJump={jump}
      canJumpBack={canJumpBack}
      canJumpForward={canJumpForward}
    />
  )
}

Props

PropTypeDescription
slotsTimelineSlot[]Time-bucketed counts keyed by series.
intervalSecsnumberBucket width; drives bar geometry and tooltips.
dateFrom / dateToDateThe visible window.
seriesTimelineSeries[]Stacked series with label, color, and statuses.
selectionTimelineRange | nullThe applied range, drawn as the on-chart selection.
onApplyRange(range) => voidFired when a drag or bucket click commits a range.
scaleOptions / activeOptionKey / onSelectSpanThe scale rail (Last hour, Today, This week, This month).
frameOptions / activeFrameKey / onSelectFrameThe zoom rail (24h, 7D, 30D, 90D).
onJump / canJumpBack / canJumpForwardWalk the window backwards and forwards.
dateRangeTimelineDateRangeControlOptional date-range picker rendered in the header.
isLoadingbooleanRenders the skeleton state.