Skip to main content
Back to Blog
Tutorials
4 min read
December 10, 2024

How to Build an Animated Chart Dashboard with D3 and React

Create animated, interactive charts with D3.js in React including line charts, bar charts, donut charts, and tooltips.

Ryel Banfield

Founder & Lead Developer

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.

D3chartsdata visualizationanimationReacttutorial

Ready to Start Your Project?

RCB Software builds world-class websites and applications for businesses worldwide.

Get in Touch

Related Articles