D3.js gives you full control over data visualizations. Here is how to create animated, responsive charts in React.
Install Dependencies
pnpm add d3
pnpm add -D @types/d3
Responsive SVG Container
"use client";
import { useEffect, useRef, useState, type ReactNode } from "react";
interface ChartContainerProps {
aspectRatio?: number;
children: (width: number, height: number) => ReactNode;
}
export function ChartContainer({
aspectRatio = 16 / 9,
children,
}: ChartContainerProps) {
const containerRef = useRef<HTMLDivElement>(null);
const [dimensions, setDimensions] = useState({ width: 0, height: 0 });
useEffect(() => {
const container = containerRef.current;
if (!container) return;
const observer = new ResizeObserver((entries) => {
const { width } = entries[0].contentRect;
setDimensions({ width, height: width / aspectRatio });
});
observer.observe(container);
return () => observer.disconnect();
}, [aspectRatio]);
return (
<div ref={containerRef} className="w-full">
{dimensions.width > 0 && children(dimensions.width, dimensions.height)}
</div>
);
}
Animated Line Chart
"use client";
import { useEffect, useRef } from "react";
import * as d3 from "d3";
interface DataPoint {
date: Date;
value: number;
}
interface LineChartProps {
data: DataPoint[];
width: number;
height: number;
color?: string;
}
export function LineChart({
data,
width,
height,
color = "#3b82f6",
}: LineChartProps) {
const svgRef = useRef<SVGSVGElement>(null);
const margin = { top: 20, right: 20, bottom: 40, left: 50 };
const innerWidth = width - margin.left - margin.right;
const innerHeight = height - margin.top - margin.bottom;
useEffect(() => {
if (!svgRef.current || data.length === 0) return;
const svg = d3.select(svgRef.current);
svg.selectAll("*").remove();
const g = svg
.append("g")
.attr("transform", `translate(${margin.left},${margin.top})`);
// Scales
const xScale = d3
.scaleTime()
.domain(d3.extent(data, (d) => d.date) as [Date, Date])
.range([0, innerWidth]);
const yScale = d3
.scaleLinear()
.domain([0, d3.max(data, (d) => d.value) ?? 0])
.nice()
.range([innerHeight, 0]);
// Grid lines
g.append("g")
.attr("class", "grid")
.call(
d3
.axisLeft(yScale)
.tickSize(-innerWidth)
.tickFormat(() => "")
)
.selectAll("line")
.attr("stroke", "#e5e7eb")
.attr("stroke-dasharray", "3,3");
g.selectAll(".grid .domain").remove();
// Axes
g.append("g")
.attr("transform", `translate(0,${innerHeight})`)
.call(d3.axisBottom(xScale).ticks(6))
.selectAll("text")
.attr("fill", "#6b7280")
.style("font-size", "12px");
g.append("g")
.call(d3.axisLeft(yScale).ticks(5))
.selectAll("text")
.attr("fill", "#6b7280")
.style("font-size", "12px");
// Area gradient
const gradient = svg
.append("defs")
.append("linearGradient")
.attr("id", "area-gradient")
.attr("x1", "0%")
.attr("y1", "0%")
.attr("x2", "0%")
.attr("y2", "100%");
gradient.append("stop").attr("offset", "0%").attr("stop-color", color).attr("stop-opacity", 0.3);
gradient.append("stop").attr("offset", "100%").attr("stop-color", color).attr("stop-opacity", 0);
// Area
const area = d3
.area<DataPoint>()
.x((d) => xScale(d.date))
.y0(innerHeight)
.y1((d) => yScale(d.value))
.curve(d3.curveMonotoneX);
g.append("path")
.datum(data)
.attr("fill", "url(#area-gradient)")
.attr("d", area);
// Line
const line = d3
.line<DataPoint>()
.x((d) => xScale(d.date))
.y((d) => yScale(d.value))
.curve(d3.curveMonotoneX);
const path = g
.append("path")
.datum(data)
.attr("fill", "none")
.attr("stroke", color)
.attr("stroke-width", 2.5)
.attr("d", line);
// Animate line drawing
const totalLength = path.node()?.getTotalLength() ?? 0;
path
.attr("stroke-dasharray", `${totalLength} ${totalLength}`)
.attr("stroke-dashoffset", totalLength)
.transition()
.duration(1500)
.ease(d3.easeCubicInOut)
.attr("stroke-dashoffset", 0);
// Tooltip
const tooltip = d3
.select("body")
.append("div")
.attr("class", "chart-tooltip")
.style("position", "absolute")
.style("background", "white")
.style("border", "1px solid #e5e7eb")
.style("border-radius", "6px")
.style("padding", "8px 12px")
.style("font-size", "12px")
.style("pointer-events", "none")
.style("opacity", 0)
.style("box-shadow", "0 2px 8px rgba(0,0,0,0.1)");
// Hover overlay
const bisect = d3.bisector<DataPoint, Date>((d) => d.date).left;
g.append("rect")
.attr("width", innerWidth)
.attr("height", innerHeight)
.attr("fill", "transparent")
.on("mousemove", (event) => {
const [mx] = d3.pointer(event);
const x0 = xScale.invert(mx);
const i = bisect(data, x0, 1);
const d0 = data[i - 1];
const d1 = data[i];
if (!d0 || !d1) return;
const d = x0.getTime() - d0.date.getTime() > d1.date.getTime() - x0.getTime() ? d1 : d0;
// Show dot
g.selectAll(".hover-dot").remove();
g.append("circle")
.attr("class", "hover-dot")
.attr("cx", xScale(d.date))
.attr("cy", yScale(d.value))
.attr("r", 5)
.attr("fill", color)
.attr("stroke", "white")
.attr("stroke-width", 2);
tooltip
.style("opacity", 1)
.html(
`<strong>${d3.timeFormat("%b %d")(d.date)}</strong><br/>Value: ${d.value.toLocaleString()}`
)
.style("left", `${event.pageX + 12}px`)
.style("top", `${event.pageY - 28}px`);
})
.on("mouseleave", () => {
g.selectAll(".hover-dot").remove();
tooltip.style("opacity", 0);
});
return () => {
tooltip.remove();
};
}, [data, width, height, color, innerWidth, innerHeight, margin.left, margin.top]);
return <svg ref={svgRef} width={width} height={height} />;
}
Animated Bar Chart
"use client";
import { useEffect, useRef } from "react";
import * as d3 from "d3";
interface BarData {
label: string;
value: number;
}
interface BarChartProps {
data: BarData[];
width: number;
height: number;
color?: string;
}
export function BarChart({
data,
width,
height,
color = "#3b82f6",
}: BarChartProps) {
const svgRef = useRef<SVGSVGElement>(null);
const margin = { top: 20, right: 20, bottom: 40, left: 50 };
const innerWidth = width - margin.left - margin.right;
const innerHeight = height - margin.top - margin.bottom;
useEffect(() => {
if (!svgRef.current || data.length === 0) return;
const svg = d3.select(svgRef.current);
svg.selectAll("*").remove();
const g = svg
.append("g")
.attr("transform", `translate(${margin.left},${margin.top})`);
const xScale = d3
.scaleBand()
.domain(data.map((d) => d.label))
.range([0, innerWidth])
.padding(0.3);
const yScale = d3
.scaleLinear()
.domain([0, d3.max(data, (d) => d.value) ?? 0])
.nice()
.range([innerHeight, 0]);
// X axis
g.append("g")
.attr("transform", `translate(0,${innerHeight})`)
.call(d3.axisBottom(xScale))
.selectAll("text")
.attr("fill", "#6b7280")
.style("font-size", "12px");
// Y axis
g.append("g")
.call(d3.axisLeft(yScale).ticks(5))
.selectAll("text")
.attr("fill", "#6b7280")
.style("font-size", "12px");
// Bars with animation
g.selectAll(".bar")
.data(data)
.enter()
.append("rect")
.attr("class", "bar")
.attr("x", (d) => xScale(d.label) ?? 0)
.attr("width", xScale.bandwidth())
.attr("y", innerHeight)
.attr("height", 0)
.attr("fill", color)
.attr("rx", 4)
.transition()
.duration(800)
.delay((_, i) => i * 100)
.ease(d3.easeBackOut)
.attr("y", (d) => yScale(d.value))
.attr("height", (d) => innerHeight - yScale(d.value));
// Value labels
g.selectAll(".label")
.data(data)
.enter()
.append("text")
.attr("class", "label")
.attr("x", (d) => (xScale(d.label) ?? 0) + xScale.bandwidth() / 2)
.attr("y", (d) => yScale(d.value) - 8)
.attr("text-anchor", "middle")
.attr("fill", "#374151")
.style("font-size", "12px")
.style("font-weight", "600")
.style("opacity", 0)
.text((d) => d.value.toLocaleString())
.transition()
.duration(800)
.delay((_, i) => i * 100 + 400)
.style("opacity", 1);
}, [data, width, height, color, innerWidth, innerHeight, margin.left, margin.top]);
return <svg ref={svgRef} width={width} height={height} />;
}
Usage: Dashboard with Multiple Charts
"use client";
import { ChartContainer } from "@/components/charts/ChartContainer";
import { LineChart } from "@/components/charts/LineChart";
import { BarChart } from "@/components/charts/BarChart";
const revenueData = Array.from({ length: 30 }, (_, i) => ({
date: new Date(2026, 0, i + 1),
value: Math.floor(2000 + Math.random() * 5000),
}));
const categoryData = [
{ label: "Web", value: 45000 },
{ label: "Mobile", value: 32000 },
{ label: "Design", value: 28000 },
{ label: "SEO", value: 18000 },
{ label: "Other", value: 12000 },
];
export default function AnalyticsDashboard() {
return (
<div className="space-y-8 p-6">
<div>
<h2 className="text-lg font-semibold mb-4">Revenue Over Time</h2>
<ChartContainer aspectRatio={2.5}>
{(width, height) => (
<LineChart data={revenueData} width={width} height={height} />
)}
</ChartContainer>
</div>
<div>
<h2 className="text-lg font-semibold mb-4">Revenue by Category</h2>
<ChartContainer aspectRatio={2}>
{(width, height) => (
<BarChart data={categoryData} width={width} height={height} />
)}
</ChartContainer>
</div>
</div>
);
}
Need Custom Data Dashboards?
We build interactive analytics dashboards with real-time data visualizations. Contact us to create your custom dashboard.