Best JavaScript Charting Libraries in 2026
TL;DR
Chart.js for quick charts; Recharts for React-native data viz; D3 for custom, complex visualizations. Chart.js (~10M weekly downloads) is the most popular charting library with a simple config API. Recharts (~3M) wraps D3 in idiomatic React components. D3.js (~9M) is the power tool — a data transformation library, not just charting — steep learning curve but unlimited flexibility. For React apps, Recharts or Visx hits the right balance.
Key Takeaways
- Chart.js: ~10M weekly downloads — most popular, canvas-based, simple config
- D3.js: ~9M downloads — power tool for custom viz, SVG manipulation
- Recharts: ~3M downloads — React-first, built on D3 scales + SVG
- Visx: ~200K downloads — low-level React primitives from Airbnb, pairs with D3
- ECharts: ~2M downloads — Apache project, best for dashboards, mobile-friendly
Choosing the Right Level of Abstraction
JavaScript charting libraries sit on a spectrum from "configure a chart with a JSON object" to "build custom data visualizations from mathematical primitives." The right choice depends on two factors: how far your requirements deviate from standard chart types, and how much control you need over rendering performance.
Standard charts — line, bar, pie, scatter — are well-served by Chart.js, Recharts, and ECharts. If your design system matches what these libraries produce with minimal customization, using a lower-level library like D3 or Visx is over-engineering. The tradeoff changes when you need: animated transitions that respond to state changes in non-standard ways, geographic visualizations, network/force-directed graphs, custom interactive elements like brushing and linking, or chart types that don't exist in config-driven libraries.
In React applications specifically, the framework choice intersects with the charting choice. Chart.js is canvas-based — it renders to a <canvas> element outside React's render tree, requiring imperative cleanup. Recharts and Visx render SVG through React, making them feel like native React components. D3 in React typically means managing a useRef with imperative DOM manipulation, which cuts against React's declarative model.
Chart.js (Simple, Fast)
// Chart.js — canvas-based, config-driven
import { Chart, registerables } from 'chart.js';
import { useEffect, useRef } from 'react';
Chart.register(...registerables);
function LineChart({ data }: { data: number[] }) {
const canvasRef = useRef<HTMLCanvasElement>(null);
useEffect(() => {
if (!canvasRef.current) return;
const chart = new Chart(canvasRef.current, {
type: 'line',
data: {
labels: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun'],
datasets: [{
label: 'Downloads',
data,
borderColor: '#3B82F6',
backgroundColor: 'rgba(59, 130, 246, 0.1)',
tension: 0.4,
fill: true,
}],
},
options: {
responsive: true,
plugins: {
legend: { display: true },
tooltip: { mode: 'index' },
},
scales: {
y: { beginAtZero: true },
},
},
});
return () => chart.destroy(); // Cleanup on unmount
}, [data]);
return <canvas ref={canvasRef} />;
}
// react-chartjs-2 — official React wrapper
import { Line, Bar, Pie, Doughnut } from 'react-chartjs-2';
import { Chart as ChartJS, CategoryScale, LinearScale, PointElement, LineElement, Title, Tooltip, Legend } from 'chart.js';
// Tree-shakeable imports (only register what you need)
ChartJS.register(CategoryScale, LinearScale, PointElement, LineElement, Title, Tooltip, Legend);
function DownloadTrends({ chartData }) {
return (
<Line
data={chartData}
options={{
responsive: true,
plugins: { legend: { position: 'top' } },
}}
/>
);
}
Chart.js's canvas-based rendering has a key advantage over SVG approaches: performance at scale. When you have 10,000+ data points, canvas rendering maintains 60fps while SVG-based libraries (which create a DOM node per data point) can become sluggish. This is why Chart.js is popular for real-time dashboards that update frequently.
The react-chartjs-2 wrapper handles the React lifecycle properly — creating the chart instance in useEffect, updating it when props change, and destroying it on unmount. The v5 wrapper is fully compatible with Chart.js 4.x and handles the ref management that the raw imperative API requires.
Chart.js's limitation is customization depth. The config API covers standard chart behavior well, but unusual requirements — custom drawing, non-standard interaction models, charts that animate in response to user interaction beyond hover states — require Chart.js plugins, which have a steeper API than the main config. At that point, D3 or Visx provides more direct control.
Bundle size is a consideration: importing all registerables pulls in ~200KB. The tree-shakeable approach (registering only what you need) can bring this down to ~60-80KB for a single chart type.
Best for: Dashboards with standard chart types, non-React projects, simplicity over customization.
Recharts (React-Native)
// Recharts — declarative React components
import {
LineChart, Line, BarChart, Bar,
XAxis, YAxis, CartesianGrid, Tooltip, Legend,
ResponsiveContainer, Area, AreaChart,
} from 'recharts';
const data = [
{ month: 'Jan', downloads: 4000, installs: 2400 },
{ month: 'Feb', downloads: 3000, installs: 1398 },
{ month: 'Mar', downloads: 6000, installs: 5800 },
{ month: 'Apr', downloads: 8000, installs: 7000 },
];
function DownloadChart() {
return (
<ResponsiveContainer width="100%" height={400}>
<AreaChart data={data} margin={{ top: 10, right: 30, left: 0, bottom: 0 }}>
<defs>
<linearGradient id="colorDownloads" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor="#3B82F6" stopOpacity={0.8} />
<stop offset="95%" stopColor="#3B82F6" stopOpacity={0} />
</linearGradient>
</defs>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="month" />
<YAxis />
<Tooltip />
<Legend />
<Area
type="monotone"
dataKey="downloads"
stroke="#3B82F6"
fillOpacity={1}
fill="url(#colorDownloads)"
/>
<Area
type="monotone"
dataKey="installs"
stroke="#10B981"
fillOpacity={0.3}
fill="#10B981"
/>
</AreaChart>
</ResponsiveContainer>
);
}
// Recharts — custom tooltip
const CustomTooltip = ({ active, payload, label }) => {
if (!active || !payload?.length) return null;
return (
<div className="bg-white border rounded p-3 shadow">
<p className="font-semibold">{label}</p>
{payload.map((entry) => (
<p key={entry.name} style={{ color: entry.color }}>
{entry.name}: {entry.value.toLocaleString()}
</p>
))}
</div>
);
};
<AreaChart data={data}>
<Tooltip content={<CustomTooltip />} />
{/* ... */}
</AreaChart>
Recharts' value proposition is React integration without the imperative API. The chart composition model — <LineChart> wrapping <Line>, <XAxis>, <YAxis> — maps naturally to how React developers think about component composition. Custom tooltips are React components, which means they have access to your full component ecosystem (state, context, styling libraries).
The SVG rendering means Recharts works correctly with SSR. Canvas-based libraries like Chart.js require document and window, which are unavailable in Node.js environments. Recharts renders SVG strings that work in both server and browser contexts.
The 3M weekly downloads understates Recharts' position in the React ecosystem — it's the de facto standard charting library for React applications that need a few standard chart types without complex customization requirements.
Recharts' limitation is performance with large datasets. Each data point maps to SVG elements — a line chart with 50,000 points creates 50,000 SVG circle and path nodes. At that scale, Chart.js's canvas rendering is dramatically faster. For real-time data with thousands of updates per second, Recharts isn't the right tool.
Best for: React apps needing standard charts with React idioms, custom tooltips, and composability.
D3.js (Power Tool)
// D3 — data transformations (not just charts)
import * as d3 from 'd3';
// D3 scales — the core building block
const xScale = d3.scaleTime()
.domain([new Date('2026-01-01'), new Date('2026-12-31')])
.range([0, 800]);
const yScale = d3.scaleLinear()
.domain([0, d3.max(data, d => d.value) ?? 0])
.range([400, 0]);
// D3 line generator
const line = d3.line<DataPoint>()
.x(d => xScale(d.date))
.y(d => yScale(d.value))
.curve(d3.curveMonotoneX);
// D3 in React — using refs
import { useEffect, useRef } from 'react';
import * as d3 from 'd3';
function D3LineChart({ data }) {
const svgRef = useRef<SVGSVGElement>(null);
useEffect(() => {
if (!svgRef.current) return;
const svg = d3.select(svgRef.current);
svg.selectAll('*').remove(); // Clear on redraw
const width = 800, height = 400;
const margin = { top: 20, right: 30, bottom: 30, left: 40 };
const x = d3.scaleUtc()
.domain(d3.extent(data, d => d.date) as [Date, Date])
.range([margin.left, width - margin.right]);
const y = d3.scaleLinear()
.domain([0, d3.max(data, d => d.value) ?? 0])
.range([height - margin.bottom, margin.top]);
const lineGen = d3.line<typeof data[0]>()
.x(d => x(d.date))
.y(d => y(d.value));
svg.append('path')
.datum(data)
.attr('fill', 'none')
.attr('stroke', '#3B82F6')
.attr('stroke-width', 2)
.attr('d', lineGen);
svg.append('g')
.attr('transform', `translate(0,${height - margin.bottom})`)
.call(d3.axisBottom(x));
svg.append('g')
.attr('transform', `translate(${margin.left},0)`)
.call(d3.axisLeft(y));
}, [data]);
return <svg ref={svgRef} width="100%" viewBox="0 0 800 400" />;
}
D3 is fundamentally a data transformation and visualization math library that happens to include SVG/canvas manipulation utilities. The core value is in d3-scale, d3-shape, d3-axis, and d3-array — these solve the hard mathematical problems in data visualization: mapping data domains to pixel ranges, generating path strings for curves, calculating bins and statistics. The chart libraries above (including Recharts) use D3 scales internally.
Using D3 directly means writing more code but having complete control. There's no abstraction to fight when you need a brush for interactive date range selection, a force-directed graph for network visualization, or a geographic projection for choropleth maps. These aren't available in Chart.js or Recharts — D3 is the only option.
D3 in React requires managing the tension between D3's imperative DOM manipulation and React's virtual DOM. The pattern above (D3 owns an SVG ref, React provides data via props, useEffect redraws on change) works but means React has no knowledge of the chart's DOM state. An alternative is using D3 only for math (scales, generators) and expressing the SVG as JSX — this is essentially what Visx does.
Best for: Custom, complex visualizations — force graphs, geographic maps, tree diagrams, anything not in a standard chart library.
Visx (Airbnb)
// Visx — low-level React primitives
import { LinePath } from '@visx/shape';
import { scaleLinear, scaleTime } from '@visx/scale';
import { extent, max } from '@visx/vendor/d3-array';
import { AxisBottom, AxisLeft } from '@visx/axis';
import { GridRows } from '@visx/grid';
function VxLineChart({ data, width = 800, height = 400 }) {
const margin = { top: 20, right: 20, bottom: 40, left: 50 };
const innerWidth = width - margin.left - margin.right;
const innerHeight = height - margin.top - margin.bottom;
const xScale = scaleTime({
domain: extent(data, d => d.date) as [Date, Date],
range: [0, innerWidth],
});
const yScale = scaleLinear({
domain: [0, max(data, d => d.value) ?? 0],
range: [innerHeight, 0],
});
return (
<svg width={width} height={height}>
<g transform={`translate(${margin.left},${margin.top})`}>
<GridRows scale={yScale} width={innerWidth} strokeDasharray="2,3" />
<LinePath
data={data}
x={d => xScale(d.date)}
y={d => yScale(d.value)}
stroke="#3B82F6"
strokeWidth={2}
/>
<AxisBottom top={innerHeight} scale={xScale} />
<AxisLeft scale={yScale} />
</g>
</svg>
);
}
Visx bridges D3's power and React's component model by expressing visualization primitives as React components instead of imperative DOM mutations. <LinePath> is a React component that renders an SVG path — you pass it data and scale functions, and it renders correctly in JSX. This keeps the rendering inside React's reconciler, enabling correct behavior with React concurrent features and suspense.
Visx is modular — you install only the packages you need (@visx/shape, @visx/scale, @visx/axis) — which keeps bundle sizes minimal. The total cost for a typical chart is 30-50KB, competitive with Recharts.
The tradeoff is boilerplate. Visx doesn't build charts — it provides the building blocks. Every chart requires explicit scale definitions, axis configurations, and margin calculations. Recharts handles all of this declaratively. Visx is the better choice when your design requirements differ from Recharts' defaults or when you need interaction patterns (brushing, zooming, linked views) that Recharts doesn't support.
Best for: React teams who want D3's power with React's component model.
Comparison Table
| Library | Downloads | Bundle | Approach | Learning Curve | Best Use Case |
|---|---|---|---|---|---|
| Chart.js | 10M | ~200KB | Config-driven | Easy | Quick dashboards |
| D3.js | 9M | ~500KB | Imperative | Hard | Custom viz |
| Recharts | 3M | ~300KB | React components | Medium | React apps |
| ECharts | 2M | ~1MB | Config-driven | Medium | Data-heavy dashboards |
| Visx | 200K | ~50KB* | React primitives | Hard | Custom React charts |
*Visx is modular — pay only for what you use.
SSR and Accessibility
Chart.js's canvas elements have no intrinsic accessibility semantics — you need to provide role="img" and aria-label attributes explicitly, and screen readers can't interpret canvas content. SVG-based libraries (Recharts, Visx, D3) fare better because SVG elements can have title and desc elements for accessibility, and screen readers can sometimes navigate SVG content.
For SSR (Next.js, Remix, Astro), canvas-based libraries like Chart.js require client-side only rendering via dynamic imports (next/dynamic with ssr: false). SVG libraries like Recharts and Visx render server-side correctly since they produce SVG markup, not canvas operations.
When to Choose
| Scenario | Pick |
|---|---|
| Quick dashboard, any framework | Chart.js |
| React app with standard charts | Recharts |
| Custom, complex visualization | D3.js |
| React app needing D3 power | Visx |
| Mobile performance matters | ECharts |
| Heatmaps, geographic viz | D3.js |
| Financial/candlestick charts | lightweight-charts (TradingView) |
| SSR with no canvas API | Recharts or Visx (SVG-based) |
| 10K+ data points, real-time updates | Chart.js (canvas) |
| Accessible charts | Recharts or Visx with ARIA attributes |
Production Architecture Considerations
The chart library choice has lasting architectural consequences that become apparent 6-12 months into a project. Teams frequently discover they underestimated customization needs, then face the choice of fighting library abstractions or migrating to D3.
State Management and Chart Updates
How charts respond to data changes differs meaningfully across libraries. Recharts re-renders the entire chart tree on each prop change — clean from a React perspective but potentially jarring if data updates are frequent. The animationBegin and animationDuration props help, but controlling exactly which visual elements animate requires per-chart configuration.
Chart.js updates can be fine-grained. The imperative API allows updating specific dataset values without destroying and recreating the chart instance, which produces smoother animations for real-time data:
// Chart.js — incremental data update (no full redraw)
chart.data.datasets[0].data.push(newValue);
chart.data.labels?.push(newLabel);
chart.update(); // Animates only the new data point
D3's update pattern gives complete control over which elements animate and how, using the enter/update/exit selection model. This level of control is unavailable in Chart.js and Recharts — but requires writing the transition logic yourself.
Mobile and Touch Interactions
ECharts, less prominent in npm download comparisons because it's more popular in Asia-Pacific markets, has the best mobile touch interaction support of the major libraries. It handles pinch-to-zoom on time series charts, touch-based tooltip positioning, and responsive layout changes without additional configuration. For applications with significant mobile traffic displaying data dashboards, ECharts warrants consideration despite the larger bundle (~1MB) compared to Chart.js (~200KB).
Recharts handles touch events reasonably for standard interactions (tooltip on touch, basic panning), but complex interactions like brush selection require additional event handling. D3's interaction model is fully customizable for touch, but requires explicit implementation.
Integration with React Component Libraries
Recharts and Visx integrate naturally with React component libraries like shadcn/ui or Radix. Custom tooltips rendered as React components have access to your design system's styling tokens, and Tailwind classes apply directly. Chart.js's canvas-based rendering sits outside the React component tree, meaning custom tooltips require an additional portal implementation and break the natural data flow.
For dashboards where charts and interactive components coexist, this React-native integration simplifies state management: chart selections can update application state normally via callbacks, and state changes automatically propagate to charts via props — no imperative Chart.js update cycle required.
Testing Chart Logic
Snapshot testing provides limited value for charts — the generated SVG is verbose and change-prone. More useful is testing the data transformation logic that feeds charts: sorting, aggregation, date binning, and normalization functions that convert raw data to chart-ready format. These are pure functions, easily unit-tested with any test runner independent of the charting library. Keeping transformation logic separate from rendering logic is the key architectural decision that makes charting code maintainable at scale.
Performance with Large Datasets
Charting library performance diverges dramatically when data volume increases beyond a few hundred points. Understanding the rendering model of each library helps you choose one that won't require switching later.
Canvas vs SVG Performance Characteristics
SVG-based charts (Recharts, Visx, D3.js) represent each data point as a DOM element. At 1,000 points, an SVG chart has 1,000+ DOM nodes. At 10,000 points, the browser's layout engine becomes the bottleneck — rendering and interaction slow significantly. The advantage of SVG is that CSS transitions, hover effects, and accessibility attributes integrate naturally with the DOM.
Canvas-based charts (Chart.js, ECharts) draw directly to a <canvas> element. There are no individual DOM elements per data point — the entire chart is a single pixel buffer. This scales to 50,000+ data points with consistent performance. The trade-off is that hover detection requires manual hit testing (each library implements this differently) and CSS animations don't apply.
Chart.js's canvas approach makes it the right choice for real-time dashboards, time-series data with high update frequency, and any chart that needs to handle more than 5,000 data points smoothly. ECharts's WebGL renderer (optional) pushes this further — millions of data points become feasible for scatter plots and heatmaps.
Virtual Rendering and Windowing
For very large datasets in SVG-based charts, virtual rendering renders only visible data points. Visx includes a <DataProvider> that supports viewport-based rendering. D3.js requires implementing this yourself, but the control is complete. If you're plotting 100,000 time-series data points in a zoomable chart, a windowing approach with D3.js often outperforms switching to a canvas library entirely.
Animation and Transitions
Chart.js has built-in animation configuration. Recharts uses CSS transitions on SVG elements. D3.js gives you complete control over transitions using its own d3.transition() API. ECharts has a comprehensive animation system built in, including entrance animations, update transitions, and legend interactions.
For dashboards with frequent data updates (polling, WebSockets), animation behavior matters. Chart.js's update animation can feel jarring if not tuned — use animation: false for high-frequency updates and reserve animation for initial render. Recharts handles incremental updates reasonably but can flicker with rapid data changes if not managed with isAnimationActive={false} during update cycles.
Accessibility in Charts
Charting accessibility is an area where most libraries provide limited out-of-box support. A canvas element is opaque to screen readers by default. Chart.js provides a plugins.accessibility option (via chartjs-plugin-a11y) that generates an ARIA description and data table fallback. D3.js and Visx require manually adding ARIA attributes to SVG elements.
For compliance-sensitive applications, Recharts's SVG output is the most accessible starting point — SVG elements can carry aria-label, role, and tabIndex attributes that screen readers interpret. The data table pattern (hiding a <table> visually while keeping it accessible) provides a robust fallback regardless of which charting library you use.
Compare charting library package health on PkgPulse. Related reading: Best JavaScript Date Libraries 2026 and Best React Component Libraries 2026.
See the live comparison
View chartjs vs. d3 on PkgPulse →