
A multiple line chart tracking automated resolutions vs critical incident volume built with TanStack Charts.
components/tanstack-charts/line/line-multiple.tsx
import { defineChart, lineY } from "@tanstack/charts";
import { d3Curve } from "@tanstack/charts/d3/shape";
import { motion } from "@tanstack/charts/motion";
import { tooltip } from "@tanstack/charts/tooltip";
import { Chart } from "@tanstack/react-charts/core";
import { useMemo } from "react";
import { max } from "d3-array";
import { scaleLinear, scalePoint } from "d3-scale";
import { curveMonotoneX } from "d3-shape";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
const motionRenderer = motion({
initial: "always",
transition: { type: "spring", stiffness: 170, damping: 18, mass: 1 },
});
const chartData = [
{ month: "January", resolvedAlerts: 420, criticalAlerts: 480 },
{ month: "February", resolvedAlerts: 500, criticalAlerts: 460 },
{ month: "March", resolvedAlerts: 470, criticalAlerts: 490 },
{ month: "April", resolvedAlerts: 560, criticalAlerts: 450 },
{ month: "May", resolvedAlerts: 530, criticalAlerts: 470 },
{ month: "June", resolvedAlerts: 580, criticalAlerts: 650 },
{ month: "July", resolvedAlerts: 580, criticalAlerts: 540 },
{ month: "August", resolvedAlerts: 690, criticalAlerts: 510 },
{ month: "September", resolvedAlerts: 660, criticalAlerts: 490 },
{ month: "October", resolvedAlerts: 760, criticalAlerts: 470 },
];
export function LineMultiple() {
const definition = useMemo(() => {
const yMax = max(chartData, (d) => Math.max(d.resolvedAlerts, d.criticalAlerts)) ?? 0;
return defineChart({
renderer: motionRenderer,
motion: {
transition: { type: "spring", stiffness: 170, damping: 18, mass: 1 },
},
focus: "group-x",
tooltip: {
use: tooltip,
motion: { type: "spring", stiffness: 170, damping: 18 },
className: "font-sans! shadow-none! capitalize!",
content: (points) => {
const datum = points[0]?.datum as (typeof chartData)[number];
if (!datum) return { rows: [] };
return {
title: String(points[0].xValue),
rows: [
{
label: "Resolved Alerts",
value: String(datum.resolvedAlerts),
color: "var(--chart-1)",
},
{
label: "Critical Alerts",
value: String(datum.criticalAlerts),
color: "var(--chart-2)",
},
],
};
},
},
marks: [
lineY(chartData, {
id: "resolvedAlerts",
x: (d) => d.month.slice(0, 3),
y: (d) => d.resolvedAlerts,
stroke: "var(--chart-1)",
strokeWidth: 2,
curve: d3Curve(curveMonotoneX),
points: false,
}),
lineY(chartData, {
id: "criticalAlerts",
x: (d) => d.month.slice(0, 3),
y: (d) => d.criticalAlerts,
stroke: "var(--chart-2)",
strokeWidth: 2,
curve: d3Curve(curveMonotoneX),
points: false,
}),
],
scales: {
x: {
scale: scalePoint().domain(chartData.map((d) => d.month.slice(0, 3))),
},
y: {
scale: scaleLinear().domain([0, yMax]).nice(),
grid: true,
},
},
});
}, []);
return (
<Card className="gap-4">
<CardHeader>
<CardTitle className="text-lg">Security Operations Center Metrics</CardTitle>
<CardDescription>Automated Resolutions vs Unhandled Critical Incident Volume</CardDescription>
</CardHeader>
<CardContent>
<Chart
definition={definition}
renderer={motionRenderer}
height={250}
ariaLabel="Multiple line chart showing security incident resolution trends over ten months"
/>
</CardContent>
</Card>
);
}