packages/template-prompt-to-motion-graphics/src/skills/charts.md
Stagger bar entrances with 3-5 frame delays and use spring() for organic motion.
Incorrect (all bars animate together):
const bars = data.map((item, i) => {
const height = spring({ frame, fps, config: { damping: 18 } });
return <div style={{ height: height * item.value }} />;
});
Correct (staggered entrances):
const STAGGER_DELAY = 5;
const bars = data.map((item, i) => {
const delay = i * STAGGER_DELAY;
const height = spring({
frame: frame - delay,
fps,
config: { damping: 18, stiffness: 80 },
});
return <div style={{ height: height * item.value }} />;
});
Charts without axis labels are hard to read. Always add labeled tick marks.
Incorrect (no axis):
<div style={{ display: "flex", alignItems: "flex-end", gap: 8 }}>{bars}</div>
Correct (with Y-axis):
const yAxisSteps = [0, 25, 50, 75, 100];
<div style={{ display: "flex" }}>
<div
style={{
display: "flex",
flexDirection: "column",
justifyContent: "space-between",
}}
>
{yAxisSteps.reverse().map((step) => (
<span style={{ fontSize: 12, color: "#888" }}>{step}</span>
))}
</div>
<div
style={{
display: "flex",
alignItems: "flex-end",
gap: 8,
borderLeft: "1px solid #333",
}}
>
{bars}
</div>
</div>;
Position value labels inside bars when height is sufficient, fade in after bar animates.
const barHeight = normalizedHeight * progress;
<div style={{ height: barHeight, backgroundColor: COLOR_BAR }}>
{barHeight > 30 && (
<span style={{ opacity: progress, fontSize: 11 }}>
{item.value.toLocaleString()}
</span>
)}
</div>;
Animate segments using stroke-dashoffset, starting from 12 o'clock.
const circumference = 2 * Math.PI * radius;
const segmentLength = (value / total) * circumference;
const offset = interpolate(progress, [0, 1], [segmentLength, 0]);
<circle
r={radius}
cx={center}
cy={center}
fill="none"
stroke={color}
strokeWidth={strokeWidth}
strokeDasharray={`${segmentLength} ${circumference}`}
strokeDashoffset={offset}
transform={`rotate(-90 ${center} ${center})`}
/>;