Latency Percentiles
freep50 / p95 / p99 request latency over time, auto-flagging the worst p99 deviation by z-score in a plain-language narrative.
Live preview
npx shadcn add @sextant/ops-latency-percentilesState
Size
Mode
API Gateway · request latency
297 ms
p99 · latest
- p50
- p95
- p99
p99 latency spiked to 536 on May 21, 2026, 4.1σ above a 285 baseline — driven by a deploy regression.
APM telemetry · updated every 60s
Source — this is the whole widget
"use client";
import * as React from "react";
import { CartesianGrid, Line, LineChart, XAxis, YAxis } from "recharts";
import { format, parseISO } from "date-fns";
import { cn } from "@/lib/utils";
import { ChartCard } from "@/registry/default/widget-kit/chart-card";
import {
ChartContainer,
ChartTooltip,
ChartTooltipContent,
type ChartConfig,
} from "@/components/ui/chart";
import {
resolveNarrative,
type NarrativeProp,
} from "@/registry/default/chart-narrative/chart-narrative";
import { detectAnomalies } from "@/registry/default/sextant-insights/stats";
import type { Insight } from "@/registry/default/sextant-insights/types";
import { StudioTarget, useStudio } from "@/registry/default/widget-kit/studio";
import type { SizeProp } from "@/registry/default/widget-kit/chart-size";
import type {
ChartClassNames,
SeriesConfig,
} from "@/registry/default/widget-kit/contract";
import { safeData } from "@/registry/default/sextant-contracts/contracts";
import { ChartStates } from "@/registry/default/chart-states/chart-states";
import { latencySeriesSchema, latencySeries, type LatencyPoint } from "./data";
// Re-export the data contract so consumers get the schema + types from a
// single import (no reaching into ./data).
export { latencySeriesSchema, type LatencyPoint } from "./data";
/** The three percentile lines, each independently recolorable / hideable. */
export type LatencySeries = {
p50?: SeriesConfig;
p95?: SeriesConfig;
p99?: SeriesConfig;
};
type Props = {
// ── Data ────────────────────────────────────────────────────────────────
data?: LatencyPoint[];
// ── Card chrome ───────────────────────────────────────────────────────────
label?: string;
attribution?: string;
className?: string;
classNames?: ChartClassNames;
size?: SizeProp;
// ── Customize ───────────────────────────────────────────────────────────
series?: LatencySeries;
/** Override latency formatting (axis, hero, tooltip). Default appends "ms". */
valueFormatter?: (n: number) => string;
/** Anomaly sensitivity on the p99 line, in σ. Default 2. */
threshold?: number;
/** Replace, hide (`false`), or fully render the one-line narrative. */
narrative?: NarrativeProp;
};
const fmtMs = (n: number): string => `${Math.round(n)} ms`;
const PERCENTILES = [
{ key: "p50", label: "p50", color: "var(--chart-3)" },
{ key: "p95", label: "p95", color: "var(--chart-2)" },
{ key: "p99", label: "p99", color: "var(--chart-1)" },
] as const;
export function OpsLatencyPercentiles(props: Props) {
// Validate consumer data at the boundary: a bad shape shows the error
// state instead of crashing. The built-in demo data is trusted, so skip it.
const parsed =
props.data === undefined ? null : safeData(latencySeriesSchema, props.data);
if (parsed && !parsed.ok) {
return (
<ChartStates status="error" shape="line" error={{ message: parsed.error }}>
{null}
</ChartStates>
);
}
return <OpsLatencyPercentilesImpl {...props} />;
}
function OpsLatencyPercentilesImpl({
data = latencySeries,
label,
attribution,
className,
classNames,
size = "auto",
series,
valueFormatter,
threshold = 2,
narrative,
}: Props) {
const studio = useStudio();
const title = studio.text("title", label ?? "API Gateway · request latency", {
label: "Chart title",
});
const fmt = valueFormatter ?? fmtMs;
// Highest-|z| p99 reading drives the narrative.
const insight = React.useMemo<Insight | undefined>(() => {
const anomalies = detectAnomalies(data, (r) => r.p99, threshold);
if (anomalies.length === 0) return undefined;
const top = anomalies.reduce((b, c) => (Math.abs(c.z) > Math.abs(b.z) ? c : b));
return {
kind: "anomaly",
metric: "p99 latency",
sigma: top.z,
date: format(parseISO(top.date), "MMM d, yyyy"),
value: top.p99,
baseline: data[0]?.p99 ?? 0,
driver: "a deploy regression",
};
}, [data, threshold]);
const latestP99 = data[data.length - 1]?.p99 ?? 0;
const config = Object.fromEntries(
PERCENTILES.map((p) => [
p.key,
{
label: series?.[p.key]?.label ?? p.label,
color: series?.[p.key]?.color ?? p.color,
},
]),
) satisfies ChartConfig;
return (
<ChartCard.Root size={size} className={cn(className, classNames?.root)}>
<ChartCard.Header className={classNames?.header}>
<ChartCard.HeaderMain>
<StudioTarget fieldKey="title" variant="inline">
<ChartCard.Label>{title}</ChartCard.Label>
</StudioTarget>
<ChartCard.HeroNumber>{fmt(latestP99)}</ChartCard.HeroNumber>
<p className="tabular text-sm text-muted-foreground">p99 · latest</p>
</ChartCard.HeaderMain>
<ChartCard.Legend
items={PERCENTILES.filter((p) => !series?.[p.key]?.hidden).map((p) => ({
label: config[p.key].label,
color: config[p.key].color as string,
}))}
/>
</ChartCard.Header>
{resolveNarrative(narrative, insight, { className: classNames?.narrative })}
<ChartCard.Body className={classNames?.body}>
<ChartContainer config={config} className="h-56 w-full">
<LineChart data={data} margin={{ left: 0, right: 8, top: 8 }}>
<CartesianGrid vertical={false} stroke="var(--chart-grid)" />
<XAxis
dataKey="date"
tickFormatter={(v: string) => format(parseISO(v), "MMM d").toUpperCase()}
tickLine={false}
axisLine={false}
tickMargin={10}
fontSize={10}
minTickGap={32}
className="smallcaps"
/>
<YAxis
tickFormatter={(v: number) => `${v}`}
tickLine={false}
axisLine={false}
width={40}
fontSize={10}
className="smallcaps tabular"
/>
<ChartTooltip
cursor={{
stroke: "var(--foreground)",
strokeOpacity: 0.25,
strokeDasharray: "2 4",
}}
content={
<ChartTooltipContent
labelFormatter={(v) => format(parseISO(String(v)), "MMM d, yyyy")}
formatter={(value, name, item) => (
<>
<span
aria-hidden
className="h-2.5 w-2.5 shrink-0 rounded-[2px]"
style={{ backgroundColor: (item as { color?: string }).color }}
/>
<span className="flex flex-1 items-center justify-between gap-3 leading-none">
<span className="text-muted-foreground">
{config[name as string]?.label ?? String(name)}
</span>
<span className="font-mono font-medium text-foreground tabular-nums">
{fmt(Number(value))}
</span>
</span>
</>
)}
/>
}
/>
{PERCENTILES.filter((p) => !series?.[p.key]?.hidden).map((p) => (
<Line
key={p.key}
dataKey={p.key}
type="monotone"
stroke={config[p.key].color as string}
strokeWidth={p.key === "p99" ? 1.75 : 1.25}
dot={false}
/>
))}
</LineChart>
</ChartContainer>
</ChartCard.Body>
<ChartCard.Footnote className={classNames?.footnote}>
{attribution ?? "APM telemetry · updated every 60s"}
</ChartCard.Footnote>
</ChartCard.Root>
);
}