Skip to content
Merged
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
13 changes: 6 additions & 7 deletions console/src/components/charts/latency-overview-chart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
ResponsiveContainer,
Legend,
} from "recharts"
import { formatMs } from "@/lib/format"
import { formatMs, formatAxisTime } from "@/lib/format"
import type { TimeseriesData } from "@/types/api"

const SERIES_CONFIG = [
Expand All @@ -18,11 +18,6 @@ const SERIES_CONFIG = [
{ key: "e2e_p95", label: "E2E p95", color: "#3b82f6", dash: "5 3" },
]

function formatAxisTime(epoch: number): string {
const d = new Date(epoch * 1000)
return `${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}`
}

interface Props {
data: TimeseriesData | null
}
Expand All @@ -44,14 +39,18 @@ export function LatencyOverviewChart({ data }: Props) {
}
return point
})
const spanSec =
data.timestamps.length > 1
? data.timestamps[data.timestamps.length - 1] - data.timestamps[0]
: 0

return (
<ResponsiveContainer width="100%" height={240}>
<LineChart data={chartData} margin={{ top: 4, right: 8, bottom: 0, left: 8 }}>
<CartesianGrid strokeDasharray="3 3" className="stroke-border" />
<XAxis
dataKey="time"
tickFormatter={formatAxisTime}
tickFormatter={(v: number) => formatAxisTime(v, spanSec)}
className="text-[11px] fill-muted-foreground"
tickLine={false}
axisLine={false}
Expand Down
15 changes: 6 additions & 9 deletions console/src/components/charts/request-volume-chart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
ResponsiveContainer,
Legend,
} from "recharts"
import { formatNumber } from "@/lib/format"
import { formatNumber, formatAxisTime } from "@/lib/format"
import type { TimeseriesData } from "@/types/api"

// Stable color palette for wire APIs
Expand All @@ -23,13 +23,6 @@ const SERIES_COLORS = [
"#84cc16", // lime
]

function formatAxisTime(epoch: number): string {
const d = new Date(epoch * 1000)
const hh = String(d.getHours()).padStart(2, "0")
const mm = String(d.getMinutes()).padStart(2, "0")
return `${hh}:${mm}`
}

interface Props {
data: TimeseriesData | null
}
Expand All @@ -55,14 +48,18 @@ export function RequestVolumeChart({ data }: Props) {
}
return point
})
const spanSec =
data.timestamps.length > 1
? data.timestamps[data.timestamps.length - 1] - data.timestamps[0]
: 0

return (
<ResponsiveContainer width="100%" height={240}>
<AreaChart data={chartData} margin={{ top: 4, right: 8, bottom: 0, left: -12 }}>
<CartesianGrid strokeDasharray="3 3" className="stroke-border" />
<XAxis
dataKey="time"
tickFormatter={formatAxisTime}
tickFormatter={(v: number) => formatAxisTime(v, spanSec)}
className="text-[11px] fill-muted-foreground"
tickLine={false}
axisLine={false}
Expand Down
13 changes: 6 additions & 7 deletions console/src/components/charts/stacked-bar-chart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
ResponsiveContainer,
Legend,
} from "recharts"
import { formatNumber } from "@/lib/format"
import { formatNumber, formatAxisTime } from "@/lib/format"
import type { TimeseriesData } from "@/types/api"

const GROUP_COLORS = [
Expand All @@ -22,11 +22,6 @@ const GROUP_COLORS = [
"#84cc16", // lime
]

function formatAxisTime(epoch: number): string {
const d = new Date(epoch * 1000)
return `${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}`
}

interface Props {
data: TimeseriesData | null
field: string
Expand Down Expand Up @@ -56,14 +51,18 @@ export function StackedBarChart({ data, field, height = 240, yFormatter = format
}
return point
})
const spanSec =
data.timestamps.length > 1
? data.timestamps[data.timestamps.length - 1] - data.timestamps[0]
: 0

return (
<ResponsiveContainer width="100%" height={height}>
<BarChart data={chartData} margin={{ top: 4, right: 8, bottom: 0, left: 8 }}>
<CartesianGrid strokeDasharray="3 3" className="stroke-border" />
<XAxis
dataKey="time"
tickFormatter={formatAxisTime}
tickFormatter={(v: number) => formatAxisTime(v, spanSec)}
className="text-[11px] fill-muted-foreground"
tickLine={false}
axisLine={false}
Expand Down
35 changes: 5 additions & 30 deletions console/src/components/charts/timeseries-line-chart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
Legend,
} from "recharts"
import type { TimeseriesData } from "@/types/api"
import { formatAxisTime } from "@/lib/format"

interface SeriesConfig {
key: string
Expand All @@ -27,33 +28,6 @@ interface Props {
variant?: "line" | "area"
}

/**
* Pick a *short* X-axis tick formatter based on the plotted span. Tick
* labels must stay narrow because the chart container is often only
* 300-400 px wide; the precise datetime lives in the hover tooltip
* (`labelFormatter` below), not on the axis itself.
*
* < 24 h : `HH:mm` (intra-day buckets, time is what matters)
* ≥ 24 h : `MM-DD` (one tick per day; granularity is 1h+, so
* intra-day detail on the axis is noise)
*/
function pickAxisFormatter(spanSec: number): (epoch: number) => string {
if (spanSec < 86400) {
return (epoch) => {
const d = new Date(epoch * 1000)
return `${pad(d.getHours())}:${pad(d.getMinutes())}`
}
}
return (epoch) => {
const d = new Date(epoch * 1000)
return `${pad(d.getMonth() + 1)}-${pad(d.getDate())}`
}
}

function pad(n: number): string {
return String(n).padStart(2, "0")
}

export function TimeseriesLineChart({
data,
series,
Expand All @@ -79,12 +53,13 @@ export function TimeseriesLineChart({
}
return point
})

// Span of the visible data window — drives axis tick density (HH:MM
// vs MM-DD HH:MM vs MM-DD). Falls back to 0 (sub-day format) for
// single-point series.
const spanSec =
data.timestamps.length > 1
? data.timestamps[data.timestamps.length - 1] - data.timestamps[0]
: 0
const formatAxisTime = pickAxisFormatter(spanSec)

const ChartComponent = variant === "area" ? AreaChart : LineChart

Expand All @@ -94,7 +69,7 @@ export function TimeseriesLineChart({
<CartesianGrid strokeDasharray="3 3" className="stroke-border" />
<XAxis
dataKey="time"
tickFormatter={formatAxisTime}
tickFormatter={(v: number) => formatAxisTime(v, spanSec)}
className="text-[11px] fill-muted-foreground"
tickLine={false}
axisLine={false}
Expand Down
48 changes: 48 additions & 0 deletions console/src/lib/format.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { describe, expect, it } from "bun:test"
import { formatAxisTime } from "./format"

// Note: formatAxisTime renders in the local timezone (via Date.getHours()
// / getMonth() etc.). Tests assert the *shape* (number of segments,
// presence of date) rather than literal values, so they pass regardless
// of the runner's TZ.

const MINUTE = 60
const HOUR = 60 * MINUTE
const DAY = 24 * HOUR
const EPOCH = 1_780_000_000 // mid-2026, arbitrary

describe("formatAxisTime", () => {
it("renders HH:MM only when the span is under 24h", () => {
for (const span of [15 * MINUTE, HOUR, 6 * HOUR, 23 * HOUR]) {
const s = formatAxisTime(EPOCH, span)
expect(s).toMatch(/^\d{2}:\d{2}$/)
}
})

it("renders MM-DD HH:MM when the span is between 24h and 7d", () => {
for (const span of [DAY, 2 * DAY, 3 * DAY, 6 * DAY]) {
const s = formatAxisTime(EPOCH, span)
expect(s).toMatch(/^\d{2}-\d{2} \d{2}:\d{2}$/)
}
})

it("renders date-only (MM-DD) at 7d or longer", () => {
for (const span of [7 * DAY, 14 * DAY, 30 * DAY]) {
const s = formatAxisTime(EPOCH, span)
expect(s).toMatch(/^\d{2}-\d{2}$/)
}
})

it("falls back to HH:MM when the span is 0 (single-point data)", () => {
expect(formatAxisTime(EPOCH, 0)).toMatch(/^\d{2}:\d{2}$/)
})

it("treats the 24h boundary inclusively as multi-day", () => {
// Exactly 24h: still in the [24h, 7d) bucket → date prefix included.
expect(formatAxisTime(EPOCH, DAY)).toMatch(/^\d{2}-\d{2} \d{2}:\d{2}$/)
})

it("treats the 7d boundary inclusively as date-only", () => {
expect(formatAxisTime(EPOCH, 7 * DAY)).toMatch(/^\d{2}-\d{2}$/)
})
})
32 changes: 32 additions & 0 deletions console/src/lib/format.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,35 @@
const DAY_SECONDS = 86_400

/**
* Chart x-axis tick label that adapts to the visible window's duration.
*
* spanSec < 24h → `HH:MM` (5m / 15m / 1h / 6h presets)
* 24h ≤ < 7d → `MM-DD HH:MM` (24h preset; gives the date on a
* few ticks so multi-day windows
* don't all read like the same
* wrapping clock face)
* ≥ 7d → `MM-DD` (7d preset; ticks come ~daily,
* the time-of-day is noise)
*
* Caller supplies `spanSec` (typically `end - start` of the toolbar
* window, or `lastTs - firstTs` of the data). Centralized here so the
* four chart components share one rule.
*/
export function formatAxisTime(epochSec: number, spanSec: number): string {
const d = new Date(epochSec * 1000)
const hh = String(d.getHours()).padStart(2, "0")
const mm = String(d.getMinutes()).padStart(2, "0")
if (spanSec < DAY_SECONDS) {
return `${hh}:${mm}`
}
const mo = String(d.getMonth() + 1).padStart(2, "0")
const da = String(d.getDate()).padStart(2, "0")
if (spanSec < 7 * DAY_SECONDS) {
return `${mo}-${da} ${hh}:${mm}`
}
return `${mo}-${da}`
}

export function formatTime(epochMs: number): string {
const d = new Date(epochMs)
const hh = String(d.getHours()).padStart(2, "0")
Expand Down
Loading