
Arbitrary XY continuous crosshairs with dynamic coordinate popup tooltip.
components/tanstack-charts/line/echarts-free-cursor.tsx
import { defineChart, dot, lineY } from "@tanstack/charts";
import { type ContinuousCursorPosition, continuousCursor } from "@tanstack/charts/interaction/cursor";
import { controlledSignal } from "@tanstack/charts/interaction/signal";
import { motion } from "@tanstack/charts/motion";
import { Chart } from "@tanstack/react-charts/core";
import { useMemo, useState } from "react";
import { scaleLinear } from "d3-scale";
import { Crosshair } from "lucide-react";
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
interface FreeCursorDatum {
hp: number;
mpg: number;
}
const motionRenderer = motion({
initial: "always",
transition: { type: "spring", stiffness: 170, damping: 18, mass: 1 },
});
function generateDemoData(): FreeCursorDatum[] {
const results: FreeCursorDatum[] = [];
for (let hp = 60; hp <= 300; hp += 15) {
const mpg = 45 - (hp - 60) * 0.12 + (Math.random() - 0.5) * 4;
results.push({ hp, mpg: Number(Math.max(10, mpg).toFixed(1)) });
}
return results;
}
export function EchartsFreeCursor() {
const data = useMemo(() => generateDemoData(), []);
const [cursorPos, setCursorPos] = useState<ContinuousCursorPosition<number, number> | null>({
x: 180,
y: 28,
});
const definition = useMemo(
() =>
defineChart(
{
renderer: motionRenderer,
marks: [
lineY(data, {
id: "free-cursor-line",
x: "hp",
y: "mpg",
stroke: "var(--chart-1)",
strokeWidth: 2,
}),
dot(data, {
id: "free-cursor-dots",
x: "hp",
y: "mpg",
fill: "var(--chart-1)",
r: 3.5,
stroke: "var(--background)",
strokeWidth: 1,
}),
],
scales: {
x: {
scale: scaleLinear().domain([50, 310]),
axis: { label: "Horsepower (HP)" },
},
y: {
scale: scaleLinear().domain([5, 50]),
grid: true,
axis: { label: "Fuel Economy (MPG)" },
},
},
controls: [
continuousCursor({
position: controlledSignal<ContinuousCursorPosition<number, number> | null, any>(
cursorPos,
(next) => setCursorPos(next),
),
xRule: {
stroke: "var(--muted-foreground)",
strokeWidth: 1,
strokeDasharray: "4 4",
},
yRule: {
stroke: "var(--muted-foreground)",
strokeWidth: 1,
strokeDasharray: "4 4",
},
marker: {
radius: 5,
fill: "var(--background)",
stroke: "var(--chart-1)",
strokeWidth: 2,
},
xLabel: {
background: "var(--foreground)",
color: "var(--background)",
fontSize: 11,
fontWeight: 600,
radius: 4,
paddingX: 6,
paddingY: 3,
format: (v: number) => `${Math.round(v)} HP`,
},
yLabel: {
side: "start",
background: "var(--foreground)",
color: "var(--background)",
fontSize: 11,
fontWeight: 600,
radius: 4,
paddingX: 6,
paddingY: 3,
format: (v: number) => `${v.toFixed(1)} MPG`,
},
}),
],
margin: { top: 24, right: 24, bottom: 32, left: 45 },
theme: {
foreground: "var(--muted-foreground)",
grid: "var(--border)",
background: "transparent",
},
},
{ keyboard: false },
),
[cursorPos, data],
);
return (
<Card className="w-full max-w-xl">
<CardHeader>
<CardTitle>Free 2D Continuous Crosshair Cursor</CardTitle>
<CardDescription>
Arbitrary XY continuous crosshairs with floating dynamic tick axis coordinate readouts
</CardDescription>
</CardHeader>
<CardContent>
<div className="h-[250px] w-full">
<Chart
definition={definition}
renderer={motionRenderer}
height={250}
ariaLabel="Free 2D Continuous Crosshair Cursor Chart"
/>
</div>
</CardContent>
<CardFooter className="flex-col items-start gap-2 text-sm">
<div className="flex items-center gap-2 leading-none font-medium">
{cursorPos
? `Coordinates: ${Math.round(cursorPos.x)} HP · ${cursorPos.y.toFixed(1)} MPG`
: "Move mouse over plot area"}{" "}
<Crosshair className="h-4 w-4 text-emerald-500" />
</div>
<div className="text-muted-foreground leading-none">
`continuousCursor` evaluates non-discrete 2D coordinates across spatial plane
</div>
</CardFooter>
</Card>
);
}
export default EchartsFreeCursor;