{ "name": "anomaly-chart", "type": "registry:ui", "title": "Anomaly Chart", "description": "A metric against its expected range, with outliers marked. The band is the point — 400ms at 03:00 may be an incident while 400ms at peak is Tuesday.", "category": "AIOps", "dependencies": [], "registryDependencies": [ "lib-motion", "lib-utils" ], "files": [ { "path": "components/ui/anomaly-chart.tsx", "type": "registry:ui", "content": "'use client'\n\nimport { useId, useMemo, type ComponentProps, type ReactNode } from 'react'\nimport { enterFade } from '@/lib/motion'\nimport { cn } from '@/lib/utils'\n\n/**\n * A metric plotted against its expected range, with outliers marked.\n *\n * The band is the point. A spike means nothing without knowing what normal\n * looked like at that moment — 400ms at 03:00 may be an incident while 400ms at\n * peak is Tuesday. Detection stays outside this component: it renders the\n * `expected` range you give it and marks whatever falls outside, rather than\n * inventing a threshold of its own.\n *\n * Points are drawn in a second, unstretched layer. The plot is stretched to fit\n * its container, and a circle inside that stretch would render as an ellipse.\n */\nexport type AnomalyPoint = {\n value: number\n /** Expected [low, high] at this moment. */\n expected?: [number, number]\n label?: string\n}\n\nconst PLOT = { width: 100, height: 60 } as const\n\nfunction AnomalyChart({\n points,\n height = 160,\n color = 'var(--blue)',\n bandColor = 'var(--blue)',\n anomalyColor = 'var(--destructive)',\n labels,\n observedLabel = 'Observed',\n expectedLabel = 'Expected range',\n className,\n ...props\n}: Omit, 'height'> & {\n points: AnomalyPoint[]\n height?: number\n color?: string\n bandColor?: string\n anomalyColor?: string\n labels?: string[]\n /** Legend entries. */\n observedLabel?: ReactNode\n expectedLabel?: ReactNode\n}) {\n const titleId = useId()\n\n const { min, span } = useMemo(() => {\n const values = points.flatMap((point) => [\n point.value,\n ...(point.expected ?? []),\n ])\n const low = Math.min(...values)\n const high = Math.max(...values)\n // A little headroom, so an anomaly never sits exactly on the edge.\n const pad = (high - low || 1) * 0.1\n return { min: low - pad, span: high - low + pad * 2 || 1 }\n }, [points])\n\n const x = (index: number) =>\n points.length === 1 ? PLOT.width / 2 : (index / (points.length - 1)) * PLOT.width\n const y = (value: number) => PLOT.height - ((value - min) / span) * PLOT.height\n\n const anomalies = points\n .map((point, index) => ({ point, index }))\n .filter(\n ({ point }) =>\n point.expected &&\n (point.value < point.expected[0] || point.value > point.expected[1]),\n )\n\n const banded = points.filter((point) => point.expected)\n const bandPath =\n banded.length === points.length && points.length > 0\n ? [\n ...points.map((point, index) => `${x(index)},${y(point.expected![1])}`),\n ...[...points]\n .reverse()\n .map((point, index) => `${x(points.length - 1 - index)},${y(point.expected![0])}`),\n ].join(' ')\n : undefined\n\n return (\n \n
\n \n {/* One string: React cannot join multiple children into a . */}\n <title id={titleId}>\n {`Metric against expected range${\n anomalies.length > 0 ? `, ${anomalies.length} outside the band` : ''\n }`}\n \n\n {bandPath && }\n\n `${x(index)},${y(point.value)}`).join(' ')}\n fill=\"none\"\n stroke={color}\n strokeWidth={1.75}\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n vectorEffect=\"non-scaling-stroke\"\n />\n \n\n {/* Unstretched overlay: a circle in the layer above keeps its shape. */}\n \n {anomalies.map(({ point, index }) => (\n \n ))}\n \n
\n\n {labels && (\n
\n {labels.map((label, index) => (\n {label}\n ))}\n
\n )}\n\n
\n \n \n {observedLabel}\n \n \n \n {expectedLabel}\n \n {anomalies.length > 0 && (\n \n \n {anomalies.length} anomal{anomalies.length === 1 ? 'y' : 'ies'}\n \n )}\n
\n \n )\n}\n\nexport { AnomalyChart }\n" } ] }