Line Chart
Multi-series line chart with animated paths, highlight segments, and interactive tooltips
Installation
Dependencies
Examples
Dashed
"use client"
import LineChart, {
Line,
Grid,
XAxis,
YAxis,
ChartTooltip,
useChart,
} from "@/components/vritti/line-chart"
import { LinePath } from "@visx/shape"
import { curveNatural } from "@visx/curve"
const data = [
{ date: new Date(2024, 0, 1), actual: 3200, projected: 3200 },
{ date: new Date(2024, 1, 1), actual: 3600, projected: 3500 },
{ date: new Date(2024, 2, 1), actual: 4100, projected: 3800 },
{ date: new Date(2024, 3, 1), actual: 3800, projected: 4100 },
{ date: new Date(2024, 4, 1), actual: 4500, projected: 4400 },
{ date: new Date(2024, 5, 1), actual: 5200, projected: 4700 },
{ date: new Date(2024, 6, 1), actual: 5000, projected: 5000 },
{ date: new Date(2024, 7, 1), actual: 5800, projected: 5300 },
]
function DashedLine() {
const { data, xScale, yScale, xAccessor, isLoaded } = useChart()
if (!isLoaded) return null
return (
<LinePath
data={data}
x={(d) => xScale(xAccessor(d)) ?? 0}
y={(d) => {
const val = d.projected
return typeof val === "number" ? (yScale(val) ?? 0) : 0
}}
stroke="var(--chart-line-secondary)"
strokeWidth={2.5}
strokeLinecap="round"
strokeDasharray="8,6"
curve={curveNatural}
/>
)
}
export function LineChartDashedDemo() {
return (
<div className="w-full p-4">
<LineChart data={data}>
<Grid horizontal />
<Line dataKey="actual" stroke="var(--chart-line-primary)" />
{/* Register projected for tooltip, render as dashed via custom component */}
<Line dataKey="projected" stroke="var(--chart-line-secondary)" strokeWidth={0} fadeEdges={false} showHighlight={false} animate={false} />
<DashedLine />
<XAxis />
<YAxis />
<ChartTooltip
rows={(point) => [
{
color: "var(--chart-line-primary)",
label: "Actual",
value: (point.actual as number)?.toLocaleString() ?? "0",
},
{
color: "var(--chart-line-secondary)",
label: "Projected",
value: (point.projected as number)?.toLocaleString() ?? "0",
},
]}
/>
</LineChart>
</div>
)
}
Dots
"use client"
import LineChart, {
Line,
Grid,
XAxis,
YAxis,
ChartTooltip,
useChart,
chartCssVars,
} from "@/components/vritti/line-chart"
const data = [
{ date: new Date(2024, 0, 1), revenue: 4200 },
{ date: new Date(2024, 1, 1), revenue: 5800 },
{ date: new Date(2024, 2, 1), revenue: 5100 },
{ date: new Date(2024, 3, 1), revenue: 7200 },
{ date: new Date(2024, 4, 1), revenue: 6400 },
{ date: new Date(2024, 5, 1), revenue: 8100 },
{ date: new Date(2024, 6, 1), revenue: 7600 },
{ date: new Date(2024, 7, 1), revenue: 9300 },
]
function DataDots() {
const { data, xScale, yScale, xAccessor, isLoaded } = useChart()
if (!isLoaded) return null
return (
<g>
{data.map((d, i) => {
const x = xScale(xAccessor(d)) ?? 0
const val = d.revenue
if (typeof val !== "number") return null
const y = yScale(val) ?? 0
return (
<circle
key={i}
cx={x}
cy={y}
r={4}
fill="var(--chart-line-primary)"
stroke={chartCssVars.background}
strokeWidth={2}
/>
)
})}
</g>
)
}
export function LineChartDotsDemo() {
return (
<div className="w-full p-4">
<LineChart data={data}>
<Grid horizontal />
<Line dataKey="revenue" stroke="var(--chart-line-primary)" showHighlight={false} />
<DataDots />
<XAxis />
<YAxis />
<ChartTooltip />
</LineChart>
</div>
)
}
Gradient
"use client"
import { useId } from "react"
import LineChart, {
Line,
Grid,
XAxis,
YAxis,
ChartTooltip,
useChart,
} from "@/components/vritti/line-chart"
import { LinePath } from "@visx/shape"
import { curveNatural } from "@visx/curve"
const data = [
{ date: new Date(2024, 0, 1), temperature: 32 },
{ date: new Date(2024, 1, 1), temperature: 35 },
{ date: new Date(2024, 2, 1), temperature: 48 },
{ date: new Date(2024, 3, 1), temperature: 58 },
{ date: new Date(2024, 4, 1), temperature: 70 },
{ date: new Date(2024, 5, 1), temperature: 82 },
{ date: new Date(2024, 6, 1), temperature: 88 },
{ date: new Date(2024, 7, 1), temperature: 85 },
{ date: new Date(2024, 8, 1), temperature: 76 },
{ date: new Date(2024, 9, 1), temperature: 62 },
{ date: new Date(2024, 10, 1), temperature: 45 },
{ date: new Date(2024, 11, 1), temperature: 34 },
]
function GradientLine() {
const { data, xScale, yScale, xAccessor, innerWidth, isLoaded } = useChart()
const gradientId = useId()
if (!isLoaded) return null
return (
<>
<defs>
<linearGradient id={gradientId} x1="0%" x2="100%" y1="0%" y2="0%">
<stop offset="0%" stopColor="hsl(210, 100%, 56%)" />
<stop offset="35%" stopColor="hsl(280, 87%, 65%)" />
<stop offset="65%" stopColor="hsl(340, 82%, 60%)" />
<stop offset="100%" stopColor="hsl(30, 100%, 55%)" />
</linearGradient>
</defs>
<LinePath
data={data}
x={(d) => xScale(xAccessor(d)) ?? 0}
y={(d) => {
const val = d.temperature
return typeof val === "number" ? (yScale(val) ?? 0) : 0
}}
stroke={`url(#${gradientId})`}
strokeWidth={3}
strokeLinecap="round"
curve={curveNatural}
/>
</>
)
}
export function LineChartGradientDemo() {
return (
<div className="w-full p-4">
<LineChart data={data}>
<Grid horizontal />
{/* Hidden line for tooltip data registration */}
<Line dataKey="temperature" stroke="hsl(280, 87%, 65%)" strokeWidth={0} fadeEdges={false} showHighlight={false} animate={false} />
<GradientLine />
<XAxis />
<YAxis />
<ChartTooltip />
</LineChart>
</div>
)
}
Interactive
"use client"
import { useState } from "react"
import LineChart, {
Line,
Grid,
XAxis,
YAxis,
ChartTooltip,
useChart,
} from "@/components/vritti/line-chart"
import { LinePath } from "@visx/shape"
import { curveNatural } from "@visx/curve"
import { motion } from "motion/react"
const data = [
{ date: new Date(2024, 0, 1), downloads: 12000, stars: 3200, forks: 1100 },
{ date: new Date(2024, 1, 1), downloads: 14500, stars: 3800, forks: 1300 },
{ date: new Date(2024, 2, 1), downloads: 13200, stars: 4100, forks: 1250 },
{ date: new Date(2024, 3, 1), downloads: 16800, stars: 4600, forks: 1500 },
{ date: new Date(2024, 4, 1), downloads: 18200, stars: 5200, forks: 1700 },
{ date: new Date(2024, 5, 1), downloads: 17500, stars: 5800, forks: 1900 },
{ date: new Date(2024, 6, 1), downloads: 21000, stars: 6400, forks: 2100 },
{ date: new Date(2024, 7, 1), downloads: 19800, stars: 7000, forks: 2300 },
{ date: new Date(2024, 8, 1), downloads: 23500, stars: 7500, forks: 2500 },
{ date: new Date(2024, 9, 1), downloads: 22100, stars: 8200, forks: 2800 },
]
const series = [
{ dataKey: "downloads", stroke: "hsl(217, 91%, 60%)", label: "Downloads" },
{ dataKey: "stars", stroke: "hsl(45, 93%, 58%)", label: "Stars" },
{ dataKey: "forks", stroke: "hsl(160, 84%, 39%)", label: "Forks" },
]
function InteractiveLines({ hoveredKey }: { hoveredKey: string | null }) {
const { data, xScale, yScale, xAccessor, isLoaded } = useChart()
if (!isLoaded) return null
return (
<>
{series.map((s) => {
const isActive = hoveredKey === null || hoveredKey === s.dataKey
return (
<motion.g
key={s.dataKey}
animate={{ opacity: isActive ? 1 : 0.15 }}
transition={{ duration: 0.3 }}
>
<LinePath
data={data}
x={(d) => xScale(xAccessor(d)) ?? 0}
y={(d) => {
const val = d[s.dataKey]
return typeof val === "number" ? (yScale(val) ?? 0) : 0
}}
stroke={s.stroke}
strokeWidth={hoveredKey === s.dataKey ? 3.5 : 2.5}
strokeLinecap="round"
curve={curveNatural}
/>
</motion.g>
)
})}
</>
)
}
export function LineChartInteractiveDemo() {
const [hoveredKey, setHoveredKey] = useState<string | null>(null)
return (
<div className="w-full p-4">
<LineChart data={data}>
<Grid horizontal />
{/* Register lines for tooltip data extraction (rendered invisible) */}
{series.map((s) => (
<Line
key={s.dataKey}
dataKey={s.dataKey}
stroke={s.stroke}
strokeWidth={0}
fadeEdges={false}
showHighlight={false}
animate={false}
/>
))}
<InteractiveLines hoveredKey={hoveredKey} />
<XAxis />
<YAxis />
<ChartTooltip
rows={(point) =>
series.map((s) => ({
color: s.stroke,
label: s.label,
value: (point[s.dataKey] as number)?.toLocaleString() ?? "0",
}))
}
/>
</LineChart>
{/* Interactive legend for hover targeting */}
<div className="mt-4 flex items-center justify-center gap-6">
{series.map((s) => (
<button
key={s.dataKey}
type="button"
className="flex items-center gap-2 rounded-md px-2 py-1 text-sm transition-opacity"
style={{
opacity: hoveredKey === null || hoveredKey === s.dataKey ? 1 : 0.3,
}}
onMouseEnter={() => setHoveredKey(s.dataKey)}
onMouseLeave={() => setHoveredKey(null)}
>
<span
className="h-2.5 w-2.5 rounded-full"
style={{ backgroundColor: s.stroke }}
/>
<span>{s.label}</span>
</button>
))}
</div>
</div>
)
}
Legend
Organic
6,800Paid
4,500Referral
2,900"use client"
import LineChart, {
Line,
Grid,
XAxis,
YAxis,
ChartTooltip,
Legend,
LegendItem,
LegendMarker,
LegendLabel,
LegendValue,
} from "@/components/vritti/line-chart"
const data = [
{ date: new Date(2024, 0, 1), organic: 4200, paid: 2400, referral: 1800 },
{ date: new Date(2024, 1, 1), organic: 4800, paid: 2900, referral: 2100 },
{ date: new Date(2024, 2, 1), organic: 5100, paid: 3100, referral: 1900 },
{ date: new Date(2024, 3, 1), organic: 4600, paid: 3500, referral: 2400 },
{ date: new Date(2024, 4, 1), organic: 5800, paid: 3200, referral: 2700 },
{ date: new Date(2024, 5, 1), organic: 6200, paid: 3800, referral: 2500 },
{ date: new Date(2024, 6, 1), organic: 5900, paid: 4100, referral: 3100 },
{ date: new Date(2024, 7, 1), organic: 6800, paid: 4500, referral: 2900 },
]
const legendItems = [
{ label: "Organic", value: 6800, color: "hsl(217, 91%, 60%)" },
{ label: "Paid", value: 4500, color: "hsl(160, 84%, 39%)" },
{ label: "Referral", value: 2900, color: "hsl(280, 87%, 65%)" },
]
export function LineChartLegendDemo() {
return (
<div className="w-full p-4">
<LineChart data={data}>
<Grid horizontal />
<Line dataKey="organic" stroke="hsl(217, 91%, 60%)" />
<Line dataKey="paid" stroke="hsl(160, 84%, 39%)" />
<Line dataKey="referral" stroke="hsl(280, 87%, 65%)" />
<XAxis />
<YAxis />
<ChartTooltip
rows={(point) => [
{
color: "hsl(217, 91%, 60%)",
label: "Organic",
value: (point.organic as number)?.toLocaleString() ?? "0",
},
{
color: "hsl(160, 84%, 39%)",
label: "Paid",
value: (point.paid as number)?.toLocaleString() ?? "0",
},
{
color: "hsl(280, 87%, 65%)",
label: "Referral",
value: (point.referral as number)?.toLocaleString() ?? "0",
},
]}
/>
</LineChart>
<div className="mt-4">
<Legend items={legendItems}>
<LegendItem className="flex items-center justify-between">
<div className="flex items-center gap-2">
<LegendMarker />
<LegendLabel />
</div>
<LegendValue />
</LegendItem>
</Legend>
</div>
</div>
)
}
Multi
"use client"
import LineChart, { Line, Grid, XAxis, YAxis, ChartTooltip } from "@/components/vritti/line-chart"
const data = [
{ date: new Date(2024, 0, 1), desktop: 186, mobile: 80 },
{ date: new Date(2024, 1, 1), desktop: 305, mobile: 200 },
{ date: new Date(2024, 2, 1), desktop: 237, mobile: 120 },
{ date: new Date(2024, 3, 1), desktop: 73, mobile: 190 },
{ date: new Date(2024, 4, 1), desktop: 209, mobile: 130 },
{ date: new Date(2024, 5, 1), desktop: 214, mobile: 140 },
]
export function LineChartMultiDemo() {
return (
<div className="w-full p-4">
<LineChart data={data}>
<Grid horizontal />
<Line dataKey="desktop" stroke="hsl(217, 91%, 60%)" />
<Line dataKey="mobile" stroke="hsl(280, 87%, 65%)" />
<XAxis />
<YAxis />
<ChartTooltip />
</LineChart>
</div>
)
}
Segment
6,400sessions
+3,200 (+100.0%)Feb 22 – Mar 23
"use client"
import { useCallback, useEffect, useState } from "react"
import LineChart, {
ChartTooltip,
Grid,
Line,
SegmentBackground,
SegmentLineFrom,
SegmentLineTo,
XAxis,
useChart,
} from "@/components/vritti/line-chart"
const chartData = [
{ date: new Date(Date.now() - 29 * 24 * 60 * 60 * 1000), sessions: 3200 },
{ date: new Date(Date.now() - 27 * 24 * 60 * 60 * 1000), sessions: 2900 },
{ date: new Date(Date.now() - 25 * 24 * 60 * 60 * 1000), sessions: 3550 },
{ date: new Date(Date.now() - 23 * 24 * 60 * 60 * 1000), sessions: 4100 },
{ date: new Date(Date.now() - 21 * 24 * 60 * 60 * 1000), sessions: 3900 },
{ date: new Date(Date.now() - 19 * 24 * 60 * 60 * 1000), sessions: 4350 },
{ date: new Date(Date.now() - 17 * 24 * 60 * 60 * 1000), sessions: 4200 },
{ date: new Date(Date.now() - 15 * 24 * 60 * 60 * 1000), sessions: 4600 },
{ date: new Date(Date.now() - 13 * 24 * 60 * 60 * 1000), sessions: 4800 },
{ date: new Date(Date.now() - 11 * 24 * 60 * 60 * 1000), sessions: 5100 },
{ date: new Date(Date.now() - 9 * 24 * 60 * 60 * 1000), sessions: 4750 },
{ date: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000), sessions: 5400 },
{ date: new Date(Date.now() - 5 * 24 * 60 * 60 * 1000), sessions: 5600 },
{ date: new Date(Date.now() - 3 * 24 * 60 * 60 * 1000), sessions: 5300 },
{ date: new Date(Date.now() - 1 * 24 * 60 * 60 * 1000), sessions: 6100 },
{ date: new Date(), sessions: 6400 },
]
interface SegmentStats {
value: number
change: number
changePct: number
startDate: Date
endDate: Date
}
function SegmentBridge({
onSegmentChange,
}: {
onSegmentChange: (stats: SegmentStats | null) => void
}) {
const { selection, data, xAccessor } = useChart()
useEffect(() => {
if (!selection?.active) {
onSegmentChange(null)
return
}
const startIdx = Math.max(0, selection.startIndex)
const endIdx = Math.min(data.length - 1, selection.endIndex)
if (startIdx >= endIdx) {
onSegmentChange(null)
return
}
const startPoint = data[startIdx] as { sessions?: number }
const endPoint = data[endIdx] as { sessions?: number }
if (!(startPoint && endPoint)) {
onSegmentChange(null)
return
}
const startVal = startPoint.sessions
const endVal = endPoint.sessions
if (typeof startVal !== "number" || typeof endVal !== "number") {
onSegmentChange(null)
return
}
onSegmentChange({
value: endVal,
change: endVal - startVal,
changePct: startVal !== 0 ? ((endVal - startVal) / startVal) * 100 : 0,
startDate: xAccessor(data[startIdx]),
endDate: xAccessor(data[endIdx]),
})
}, [selection, data, xAccessor, onSegmentChange])
return null
}
export function LineChartSegmentDemo() {
const [stats, setStats] = useState<SegmentStats | null>(null)
const handleSegmentChange = useCallback(
(s: SegmentStats | null) => setStats(s),
[]
)
const firstVal = chartData[0]?.sessions ?? 0
const lastVal = chartData.at(-1)?.sessions ?? 0
const displayValue = stats?.value ?? lastVal
const displayChange = stats?.change ?? lastVal - firstVal
const displayPct =
stats?.changePct ??
(firstVal > 0 ? ((lastVal - firstVal) / firstVal) * 100 : 0)
const isPositive = displayChange >= 0
const startLabel = (
stats?.startDate ?? chartData[0]?.date
)?.toLocaleDateString("en-US", { month: "short", day: "numeric" })
const endLabel = (
stats?.endDate ?? chartData.at(-1)?.date
)?.toLocaleDateString("en-US", { month: "short", day: "numeric" })
return (
<div className="w-full p-4">
<div className="mb-4 space-y-1">
<div className="flex items-baseline gap-2">
<span className="font-semibold text-2xl tabular-nums">
{displayValue.toLocaleString()}
</span>
<span className="text-muted-foreground text-sm">sessions</span>
</div>
<div className="flex items-baseline gap-2 text-sm">
<span className={isPositive ? "text-emerald-500" : "text-red-500"}>
{isPositive ? "+" : ""}
{displayChange.toLocaleString()} ({isPositive ? "+" : ""}
{displayPct.toFixed(1)}%)
</span>
<span className="text-muted-foreground">
{startLabel} – {endLabel}
</span>
</div>
</div>
<LineChart data={chartData}>
<Grid horizontal />
<Line dataKey="sessions" stroke="var(--chart-line-primary)" />
<SegmentBackground />
<SegmentLineFrom />
<SegmentLineTo />
<XAxis />
<ChartTooltip />
<SegmentBridge onSegmentChange={handleSegmentChange} />
</LineChart>
</div>
)
}
Step
"use client"
import LineChart, { Line, Grid, XAxis, YAxis, ChartTooltip } from "@/components/vritti/line-chart"
import { curveStepAfter } from "@visx/curve"
const data = [
{ date: new Date(2024, 0, 1), price: 29 },
{ date: new Date(2024, 1, 15), price: 29 },
{ date: new Date(2024, 3, 1), price: 39 },
{ date: new Date(2024, 4, 15), price: 39 },
{ date: new Date(2024, 6, 1), price: 49 },
{ date: new Date(2024, 7, 15), price: 49 },
{ date: new Date(2024, 9, 1), price: 59 },
{ date: new Date(2024, 10, 1), price: 59 },
{ date: new Date(2024, 11, 1), price: 69 },
]
export function LineChartStepDemo() {
return (
<div className="w-full p-4">
<LineChart data={data}>
<Grid horizontal />
<Line
dataKey="price"
stroke="var(--chart-line-primary)"
curve={curveStepAfter}
fadeEdges={false}
/>
<XAxis />
<YAxis formatValue={(v) => `$${v}`} />
<ChartTooltip
rows={(point) => [
{
color: "var(--chart-line-primary)",
label: "Price",
value: `$${(point.price as number) ?? 0}`,
},
]}
/>
</LineChart>
</div>
)
}
Tooltip
"use client"
import LineChart, { ChartTooltip, Grid, Line, XAxis, YAxis } from "@/components/vritti/line-chart"
const data = [
{ date: new Date(2024, 0, 1), pageviews: 28000, bounces: 8400 },
{ date: new Date(2024, 1, 1), pageviews: 35000, bounces: 10500 },
{ date: new Date(2024, 2, 1), pageviews: 29000, bounces: 7250 },
{ date: new Date(2024, 3, 1), pageviews: 42000, bounces: 12600 },
{ date: new Date(2024, 4, 1), pageviews: 38500, bounces: 11550 },
{ date: new Date(2024, 5, 1), pageviews: 47000, bounces: 14100 },
]
export function LineChartTooltipDemo() {
return (
<div className="w-full p-4">
<LineChart data={data}>
<Grid horizontal />
<Line dataKey="pageviews" stroke="var(--chart-line-primary)" />
<Line dataKey="bounces" stroke="var(--chart-line-secondary)" />
<XAxis />
<YAxis />
<ChartTooltip
rows={(point) => [
{
color: "var(--chart-line-primary)",
label: "Page Views",
value: (point.pageviews as number)?.toLocaleString() ?? "0",
},
{
color: "var(--chart-line-secondary)",
label: "Bounces",
value: (point.bounces as number)?.toLocaleString() ?? "0",
},
]}
/>
</LineChart>
</div>
)
}