Remocn
componentanimated-line-chartsource: ok · hit
Animated Line Chart
Line chart whose SVG path draws on from left to right with a leading dot.
npx shadcn add "https://www.remocn.dev/r/animated-line-chart.json"free
No selection tags yet — empty means no evidence, not lack of support.
Dependencies
Bundle weight · lightremotion
Files1 · source cached 10/15/2026, 12:02:06 AM
registry/remocn/animated-line-chart/index.tsx
"use client";
import { spring, useCurrentFrame, useVideoConfig } from "remotion";
export interface AnimatedLineChartProps {
data?: number[];
width?: number;
height?: number;
strokeColor?: string;
strokeWidth?: number;
gridColor?: string;
showDot?: boolean;
speed?: number;
className?: string;
}
export function AnimatedLineChart({
data = [12, 19, 8, 15, 22, 18, 28, 25, 32],
width = 1000,
height = 500,
strokeColor = "#22c55e",
strokeWidth = 4,
gridColor = "#27272a",
showDot = true,
speed = 1,
className,
}: AnimatedLineChartProps) {
const frame = useCurrentFrame() * speed;
const { fps, durationInFrames } = useVideoConfig();
const padding = 60;
const innerWidth = width - padding * 2;
const innerHeight = height - padding * 2;
const min = Math.min(...data);
const max = Math.max(...data);
const range = max - min || 1;
const points = data.map((value, index) => {
const x = padding + (index / (data.length - 1)) * innerWidth;
const y = padding + innerHeight - ((value - min) / range) * innerHeight;
return { x, y };
});
// Analytical path length: sum of segment distances
let pathLength = 0;
for (let i = 1; i < points.length; i++) {
const dx = points[i].x - points[i - 1].x;
const dy = points[i].y - points[i - 1].y;
pathLength += Math.sqrt(dx * dx + dy * dy);
}
const d = points
.map((p, i) => `${i === 0 ? "M" : "L"}${p.x.toFixed(2)},${p.y.toFixed(2)}`)
.join(" ");
// Spring-driven progress: smooth, no bounce. Matches the "smooth" preset
// from remotion-best-practices/timing.md.
const progress = spring({
frame,
fps,
durationInFrames: Math.round(durationInFrames * 0.85),
config: { damping: 200 },
});
const dashOffset = pathLength * (1 - progress);
// Find dot position along the path at current progress
const targetLen = pathLength * progress;
let traveled = 0;
let dotX = points[0].x;
let dotY = points[0].y;
for (let i = 1; i < points.length; i++) {
const dx = points[i].x - points[i - 1].x;
const dy = points[i].y - points[i - 1].y;
const segLen = Math.sqrt(dx * dx + dy * dy);
if (traveled + segLen >= targetLen) {
const t = (targetLen - traveled) / segLen;
dotX = points[i - 1].x + dx * t;
dotY = points[i - 1].y + dy * t;
break;
}
traveled += segLen;
dotX = points[i].x;
dotY = points[i].y;
}
const gridRows = 4;
const gridCols = data.length - 1;
return (
<div
className={className}
style={{
position: "absolute",
inset: 0,
display: "flex",
alignItems: "center",
justifyContent: "center",
fontFamily:
"var(--font-geist-sans), -apple-system, BlinkMacSystemFont, sans-serif",
}}
>
<svg
width={width}
height={height}
viewBox={`0 0 ${width} ${height}`}
style={{ overflow: "visible" }}
>
{/* Horizontal grid */}
{Array.from({ length: gridRows + 1 }).map((_, i) => {
const y = padding + (i / gridRows) * innerHeight;
return (
<line
key={`h-${i}`}
x1={padding}
x2={padding + innerWidth}
y1={y}
y2={y}
stroke={gridColor}
strokeWidth={1}
/>
);
})}
{/* Vertical grid */}
{Array.from({ length: gridCols + 1 }).map((_, i) => {
const x = padding + (i / gridCols) * innerWidth;
return (
<line
key={`v-${i}`}
x1={x}
x2={x}
y1={padding}
y2={padding + innerHeight}
stroke={gridColor}
strokeWidth={1}
/>
);
})}
{/* Axis */}
<line
x1={padding}
x2={padding}
y1={padding}
y2={padding + innerHeight}
stroke={gridColor}
strokeWidth={2}
/>
<line
x1={padding}
x2={padding + innerWidth}
y1={padding + innerHeight}
y2={padding + innerHeight}
stroke={gridColor}
strokeWidth={2}
/>
{/* Animated line */}
<path
d={d}
fill="none"
stroke={strokeColor}
strokeWidth={strokeWidth}
strokeLinecap="round"
strokeLinejoin="round"
strokeDasharray={pathLength}
strokeDashoffset={dashOffset}
style={{
filter: `drop-shadow(0 0 12px ${strokeColor}55)`,
}}
/>
{showDot && progress > 0 && progress < 1 && (
<circle
cx={dotX}
cy={dotY}
r={strokeWidth * 2}
fill={strokeColor}
style={{
filter: `drop-shadow(0 0 8px ${strokeColor})`,
}}
/>
)}
</svg>
</div>
);
}