components/tanstack-charts/line/vector-field.tsx
import { type ChartPoint, defineChart, vector } from "@tanstack/charts";
import { motion } from "@tanstack/charts/motion";
import { tooltip } from "@tanstack/charts/tooltip";
import { Chart } from "@tanstack/react-charts/core";
import { useMemo } from "react";
import { scaleLinear, scaleSqrt } from "d3-scale";
import { Compass } from "lucide-react";
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
interface VectorDatum {
x: number;
y: number;
u: number;
v: number;
}
const motionRenderer = motion({
initial: "always",
transition: { type: "spring", stiffness: 170, damping: 18, mass: 1 },
});
function generateDemoData(): VectorDatum[] {
const results: VectorDatum[] = [];
for (let x = -5; x <= 5; x += 1) {
for (let y = -5; y <= 5; y += 1) {
const u = -y * 1.5;
const v = x * 1.5;
results.push({ x, y, u, v });
}
}
return results;
}
function shadcnTooltipContent<TDatum>(points: readonly ChartPoint<TDatum>[]) {
if (points.length === 0) return { title: "", rows: [] };
const d = points[0].datum as VectorDatum;
if (!d) return { title: "", rows: [] };
const speedVal = Math.hypot(d.u, d.v).toFixed(1);
const angleDeg = ((Math.atan2(d.u, d.v) * 180) / Math.PI).toFixed(0);
return {
title: `Grid Position (${d.x}, ${d.y})`,
rows: [
{ label: "Velocity Magnitude", value: `${speedVal} m/s`, color: "var(--chart-1)" },
{ label: "Direction Angle", value: `${angleDeg}°`, color: "var(--foreground)" },
],
};
}
export function VectorField() {
const data = useMemo(() => generateDemoData(), []);
const speedScale = useMemo(() => scaleSqrt().domain([0, 15]).range([2, 16]), []);
const definition = useMemo(
() =>
defineChart(
{
renderer: motionRenderer,
marks: [
vector(data, {
id: "wind-vectors",
x: "x",
y: "y",
length: (row: VectorDatum) => speedScale(Math.hypot(row.u, row.v)),
rotate: (row: VectorDatum) => (Math.atan2(row.u, row.v) * 180) / Math.PI,
stroke: "var(--chart-1)",
strokeWidth: 1.5,
}),
],
scales: {
x: { scale: scaleLinear, grid: true, axis: { label: "Longitude X" } },
y: { scale: scaleLinear, grid: true, axis: { label: "Latitude Y" } },
},
margin: { top: 24, right: 24, bottom: 32, left: 45 },
theme: {
foreground: "var(--muted-foreground)",
grid: "var(--border)",
background: "transparent",
},
},
{
keyboard: true,
tooltip: {
use: tooltip,
className:
"font-sans shadow-none rounded-md bg-popover text-popover-foreground border p-2 text-xs",
anchor: "pointer",
placement: "auto",
content: (points) => shadcnTooltipContent(points),
},
},
),
[data, speedScale],
);
return (
<Card className="w-full max-w-xl">
<CardHeader>
<CardTitle>Rotational Velocity Vector Field</CardTitle>
<CardDescription>
Directional vector arrows mapping circulation and flow field magnitude
</CardDescription>
</CardHeader>
<CardContent>
<div className="h-[250px] w-full">
<Chart
definition={definition}
renderer={motionRenderer}
height={250}
ariaLabel="Rotational Velocity Vector Field Chart"
/>
</div>
</CardContent>
<CardFooter className="flex-col items-start gap-2 text-sm">
<div className="flex items-center gap-2 leading-none font-medium">
Counter-clockwise vortex circulation around origin (0,0){" "}
<Compass className="h-4 w-4 text-emerald-500" />
</div>
<div className="text-muted-foreground leading-none">
Arrow length indicates vector speed; rotation angle represents directional heading
</div>
</CardFooter>
</Card>
);
}
export default VectorField;