Back to Blog

React Data Visualization: Recharts, Responsive Charts, Real-Time Updates, and Accessible Charts

Build data visualizations in React with Recharts. Covers line, bar, area, and pie charts, ResponsiveContainer for adaptive layouts, real-time updates with useEffect, custom tooltips, chart accessibility, and TypeScript types.

Viprasol Tech Team
April 19, 2027
13 min read

Dashboards are where SaaS products prove their value. A well-implemented chart communicates trends, anomalies, and comparisons in seconds. A poorly implemented one โ€” slow to render, broken on mobile, inaccessible to screen readers โ€” erodes trust. Recharts is the most widely used React charting library: it's built on SVG and D3, supports Server Components (for static charts), and has a clean declarative API.

This guide covers the chart types you'll need most, responsive layouts, real-time streaming data, custom tooltips, and accessibility.

Installation

npm install recharts
npm install -D @types/recharts  # Not needed โ€” recharts ships its own types

Responsive Line Chart

// components/charts/revenue-trend.tsx
"use client";

import {
  LineChart, Line, XAxis, YAxis, CartesianGrid,
  Tooltip, Legend, ResponsiveContainer
} from "recharts";
import { format } from "date-fns";

interface DataPoint {
  date: string;  // ISO date string
  revenue: number;
  mrr: number;
}

interface RevenueTrendChartProps {
  data: DataPoint[];
  height?: number;
}

// Always wrap in ResponsiveContainer โ€” never set fixed width on the chart itself
export function RevenueTrendChart({ data, height = 300 }: RevenueTrendChartProps) {
  const formatted = data.map((d) => ({
    ...d,
    label: format(new Date(d.date), "MMM d"),  // "Apr 1"
  }));

  return (
    <ResponsiveContainer width="100%" height={height}>
      <LineChart data={formatted} margin={{ top: 5, right: 20, bottom: 5, left: 0 }}>
        <CartesianGrid strokeDasharray="3 3" stroke="#f3f4f6" vertical={false} />
        <XAxis
          dataKey="label"
          tick={{ fontSize: 12, fill: "#6b7280" }}
          axisLine={false}
          tickLine={false}
        />
        <YAxis
          tick={{ fontSize: 12, fill: "#6b7280" }}
          axisLine={false}
          tickLine={false}
          tickFormatter={(v) => `$${(v / 1000).toFixed(0)}k`}
          width={48}
        />
        <Tooltip content={<RevenueTooltip />} />
        <Legend
          iconType="circle"
          iconSize={8}
          wrapperStyle={{ fontSize: "13px", color: "#6b7280" }}
        />
        <Line
          type="monotone"
          dataKey="revenue"
          stroke="#2563eb"
          strokeWidth={2}
          dot={false}
          activeDot={{ r: 4 }}
          name="Revenue"
        />
        <Line
          type="monotone"
          dataKey="mrr"
          stroke="#7c3aed"
          strokeWidth={2}
          strokeDasharray="5 5"
          dot={false}
          activeDot={{ r: 4 }}
          name="MRR"
        />
      </LineChart>
    </ResponsiveContainer>
  );
}

๐ŸŒ Looking for a Dev Team That Actually Delivers?

Most agencies sell you a project manager and assign juniors. Viprasol is different โ€” senior engineers only, direct Slack access, and a 5.0โ˜… Upwork record across 100+ projects.

  • React, Next.js, Node.js, TypeScript โ€” production-grade stack
  • Fixed-price contracts โ€” no surprise invoices
  • Full source code ownership from day one
  • 90-day post-launch support included

Custom Tooltip

// components/charts/revenue-tooltip.tsx
import { TooltipProps } from "recharts";

export function RevenueTooltip({
  active,
  payload,
  label,
}: TooltipProps<number, string>) {
  if (!active || !payload || payload.length === 0) return null;

  return (
    <div className="bg-white border border-gray-200 rounded-lg shadow-lg p-3 text-sm">
      <p className="font-medium text-gray-900 mb-2">{label}</p>
      {payload.map((entry) => (
        <div key={entry.dataKey} className="flex items-center justify-between gap-6">
          <div className="flex items-center gap-1.5">
            <div
              className="w-2 h-2 rounded-full flex-shrink-0"
              style={{ backgroundColor: entry.color }}
            />
            <span className="text-gray-600">{entry.name}</span>
          </div>
          <span className="font-semibold text-gray-900 tabular-nums">
            ${(entry.value ?? 0).toLocaleString()}
          </span>
        </div>
      ))}
    </div>
  );
}

Bar Chart: Monthly Comparison

// components/charts/monthly-bar.tsx
"use client";

import {
  BarChart, Bar, XAxis, YAxis, CartesianGrid,
  Tooltip, Legend, ResponsiveContainer, Cell
} from "recharts";

interface MonthlyData {
  month: string;
  current: number;
  previous: number;
}

export function MonthlyComparisonChart({ data }: { data: MonthlyData[] }) {
  return (
    <ResponsiveContainer width="100%" height={280}>
      <BarChart data={data} margin={{ top: 5, right: 20, bottom: 5, left: 0 }} barGap={4}>
        <CartesianGrid strokeDasharray="3 3" stroke="#f3f4f6" vertical={false} />
        <XAxis
          dataKey="month"
          tick={{ fontSize: 12, fill: "#6b7280" }}
          axisLine={false}
          tickLine={false}
        />
        <YAxis
          tick={{ fontSize: 12, fill: "#6b7280" }}
          axisLine={false}
          tickLine={false}
          tickFormatter={(v) => `$${(v / 1000).toFixed(0)}k`}
          width={48}
        />
        <Tooltip
          formatter={(value: number, name: string) => [
            `$${value.toLocaleString()}`,
            name === "current" ? "This period" : "Prior period",
          ]}
          contentStyle={{ borderRadius: "8px", border: "1px solid #e5e7eb", fontSize: "13px" }}
        />
        <Legend
          formatter={(v) => v === "current" ? "This period" : "Prior period"}
          iconType="circle" iconSize={8}
          wrapperStyle={{ fontSize: "13px", color: "#6b7280" }}
        />
        <Bar dataKey="current" fill="#2563eb" radius={[4, 4, 0, 0]} maxBarSize={40} />
        <Bar dataKey="previous" fill="#dbeafe" radius={[4, 4, 0, 0]} maxBarSize={40} />
      </BarChart>
    </ResponsiveContainer>
  );
}

๐Ÿš€ Senior Engineers. No Junior Handoffs. Ever.

You get the senior developer, not a project manager who relays your requirements to someone you never meet. Every Viprasol project has a senior lead from kickoff to launch.

  • MVPs in 4โ€“8 weeks, full platforms in 3โ€“5 months
  • Lighthouse 90+ performance scores standard
  • Works across US, UK, AU timezones
  • Free 30-min architecture review, no commitment

Area Chart: Stacked Metrics

// components/charts/funnel-area.tsx
"use client";

import {
  AreaChart, Area, XAxis, YAxis, CartesianGrid,
  Tooltip, ResponsiveContainer
} from "recharts";

interface FunnelData {
  date: string;
  visitors: number;
  signups: number;
  conversions: number;
}

export function FunnelAreaChart({ data }: { data: FunnelData[] }) {
  return (
    <ResponsiveContainer width="100%" height={280}>
      <AreaChart data={data} margin={{ top: 10, right: 20, bottom: 5, left: 0 }}>
        <defs>
          <linearGradient id="visitors" x1="0" y1="0" x2="0" y2="1">
            <stop offset="5%"  stopColor="#93c5fd" stopOpacity={0.3} />
            <stop offset="95%" stopColor="#93c5fd" stopOpacity={0} />
          </linearGradient>
          <linearGradient id="signups" x1="0" y1="0" x2="0" y2="1">
            <stop offset="5%"  stopColor="#2563eb" stopOpacity={0.3} />
            <stop offset="95%" stopColor="#2563eb" stopOpacity={0} />
          </linearGradient>
          <linearGradient id="conversions" x1="0" y1="0" x2="0" y2="1">
            <stop offset="5%"  stopColor="#7c3aed" stopOpacity={0.4} />
            <stop offset="95%" stopColor="#7c3aed" stopOpacity={0} />
          </linearGradient>
        </defs>
        <CartesianGrid strokeDasharray="3 3" stroke="#f3f4f6" vertical={false} />
        <XAxis dataKey="date" tick={{ fontSize: 12, fill: "#6b7280" }} axisLine={false} tickLine={false} />
        <YAxis tick={{ fontSize: 12, fill: "#6b7280" }} axisLine={false} tickLine={false} width={40} />
        <Tooltip contentStyle={{ borderRadius: "8px", border: "1px solid #e5e7eb", fontSize: "13px" }} />
        <Area type="monotone" dataKey="visitors"    stroke="#93c5fd" fill="url(#visitors)"    strokeWidth={2} />
        <Area type="monotone" dataKey="signups"     stroke="#2563eb" fill="url(#signups)"     strokeWidth={2} />
        <Area type="monotone" dataKey="conversions" stroke="#7c3aed" fill="url(#conversions)" strokeWidth={2} />
      </AreaChart>
    </ResponsiveContainer>
  );
}

Pie/Donut Chart

// components/charts/plan-distribution.tsx
"use client";

import { PieChart, Pie, Cell, Tooltip, ResponsiveContainer, Legend } from "recharts";

interface PlanData {
  name: string;
  value: number;
  color: string;
}

export function PlanDistributionChart({ data }: { data: PlanData[] }) {
  const total = data.reduce((sum, d) => sum + d.value, 0);

  return (
    <ResponsiveContainer width="100%" height={260}>
      <PieChart>
        <Pie
          data={data}
          cx="50%"
          cy="50%"
          innerRadius={70}   // innerRadius > 0 = donut chart
          outerRadius={100}
          paddingAngle={3}
          dataKey="value"
          strokeWidth={0}
        >
          {data.map((entry, i) => (
            <Cell key={entry.name} fill={entry.color} />
          ))}
        </Pie>
        <Tooltip
          formatter={(value: number, name: string) => [
            `${value.toLocaleString()} (${Math.round((value / total) * 100)}%)`,
            name,
          ]}
          contentStyle={{ borderRadius: "8px", border: "1px solid #e5e7eb", fontSize: "13px" }}
        />
        <Legend
          iconType="circle" iconSize={8}
          formatter={(name, entry: any) => (
            <span style={{ fontSize: "13px", color: "#374151" }}>
              {name}: {Math.round((entry.payload.value / total) * 100)}%
            </span>
          )}
        />
      </PieChart>
    </ResponsiveContainer>
  );
}

Real-Time Updates

// components/charts/realtime-chart.tsx
"use client";

import { useState, useEffect, useRef, useCallback } from "react";
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from "recharts";

interface MetricPoint {
  time: string;
  value: number;
}

const MAX_POINTS = 60; // Show last 60 seconds

export function RealtimeMetricChart({
  metricName,
  pollIntervalMs = 2000,
}: {
  metricName: string;
  pollIntervalMs?: number;
}) {
  const [data, setData] = useState<MetricPoint[]>([]);
  const intervalRef = useRef<NodeJS.Timeout>();

  const fetchMetric = useCallback(async () => {
    try {
      const response = await fetch(`/api/metrics/realtime?metric=${metricName}`);
      const { value } = await response.json();
      const point: MetricPoint = {
        time: new Date().toLocaleTimeString(),
        value,
      };
      setData((prev) => [...prev.slice(-MAX_POINTS + 1), point]);
    } catch {
      // Non-fatal โ€” chart shows gap
    }
  }, [metricName]);

  useEffect(() => {
    fetchMetric(); // Fetch immediately
    intervalRef.current = setInterval(fetchMetric, pollIntervalMs);
    return () => clearInterval(intervalRef.current);
  }, [fetchMetric, pollIntervalMs]);

  return (
    <ResponsiveContainer width="100%" height={200}>
      <LineChart data={data} margin={{ top: 5, right: 20, bottom: 5, left: 0 }}>
        <CartesianGrid strokeDasharray="3 3" stroke="#f3f4f6" vertical={false} />
        <XAxis
          dataKey="time"
          tick={{ fontSize: 11, fill: "#9ca3af" }}
          axisLine={false}
          tickLine={false}
          interval="preserveStartEnd"
        />
        <YAxis
          tick={{ fontSize: 11, fill: "#9ca3af" }}
          axisLine={false}
          tickLine={false}
          width={36}
        />
        <Tooltip contentStyle={{ fontSize: "12px", borderRadius: "6px" }} />
        <Line
          type="monotone"
          dataKey="value"
          stroke="#2563eb"
          strokeWidth={1.5}
          dot={false}
          isAnimationActive={false}  // Disable animation for real-time (prevents lag)
        />
      </LineChart>
    </ResponsiveContainer>
  );
}

Accessible Charts

// Recharts SVG includes title/desc for screen readers
// Wrap charts in a figure with figcaption
function AccessibleChart({ data }: { data: DataPoint[] }) {
  const min = Math.min(...data.map((d) => d.revenue));
  const max = Math.max(...data.map((d) => d.revenue));

  return (
    <figure>
      <figcaption className="text-sm font-medium text-gray-700 mb-3">
        Revenue trend โ€” last 30 days
      </figcaption>

      {/* Chart with aria-hidden: screen readers get the data table below */}
      <div aria-hidden="true">
        <RevenueTrendChart data={data} />
      </div>

      {/* Accessible data table for screen readers */}
      <table className="sr-only">
        <caption>Revenue data, last 30 days</caption>
        <thead>
          <tr>
            <th scope="col">Date</th>
            <th scope="col">Revenue</th>
            <th scope="col">MRR</th>
          </tr>
        </thead>
        <tbody>
          {data.map((d) => (
            <tr key={d.date}>
              <td>{new Date(d.date).toLocaleDateString()}</td>
              <td>${d.revenue.toLocaleString()}</td>
              <td>${d.mrr.toLocaleString()}</td>
            </tr>
          ))}
        </tbody>
      </table>

      <p className="text-xs text-gray-400 mt-2">
        Range: ${min.toLocaleString()} โ€“ ${max.toLocaleString()}
      </p>
    </figure>
  );
}

Cost and Timeline Estimates

ScopeChartsTeamTimelineCost Range
Single chart (line or bar)11 dev0.5โ€“1 day$150โ€“400
Analytics dashboard (5โ€“8 charts)5โ€“81 dev1โ€“2 weeks$2,500โ€“5,000
+ Real-time + accessible + custom brand5โ€“81โ€“2 devs2โ€“3 weeks$6,000โ€“12,000

See Also


Working With Viprasol

We build analytics dashboards that communicate clearly โ€” responsive across all screen sizes, accessible to keyboard and screen reader users, fast to render, and visually polished. Our team has built metric dashboards for SaaS, fintech, and trading platforms with custom chart designs, real-time polling, and data table fallbacks for accessibility.

What we deliver:

  • Line, bar, area, and donut charts with ResponsiveContainer
  • Custom tooltip components with branded styling
  • Real-time polling with isAnimationActive disabled for smooth updates
  • Screen reader accessible data tables (aria-hidden chart + sr-only table)
  • TypeScript-typed chart components with clean props API

Talk to our team about your data visualization needs โ†’

Or explore our web development services.

Share this article:

About the Author

V

Viprasol Tech Team

Custom Software Development Specialists

The Viprasol Tech team specialises in algorithmic trading software, AI agent systems, and SaaS development. With 100+ projects delivered across MT4/MT5 EAs, fintech platforms, and production AI systems, the team brings deep technical experience to every engagement. Based in India, serving clients globally.

MT4/MT5 EA DevelopmentAI Agent SystemsSaaS DevelopmentAlgorithmic Trading

Need a Modern Web Application?

From landing pages to complex SaaS platforms โ€” we build it all with Next.js and React.

Free consultation โ€ข No commitment โ€ข Response within 24 hours

Viprasol ยท Web Development

Need a custom web application built?

We build React and Next.js web applications with Lighthouse โ‰ฅ90 scores, mobile-first design, and full source code ownership. Senior engineers only โ€” from architecture through deployment.