LabHub

Blog

Web Data Visualization Libraries 2026 — D3, Plot, Visx, Recharts, ECharts, Vega-Lite Compared (Deep Dive)

한국어English日本語

Prologue — Why are there so many chart libraries?

Anyone shipping their first web chart eventually asks: "Which one should I use?" The answer is always the same: "What do you want to draw?"

It sounds dismissive, but it is the real answer. The 2026 chart ecosystem doesn't collapse to a single dimension. There are at least four axes.

Where you stand on those four axes turns the same "bar chart" into 80 lines of D3, 3 lines of Plot, or 12 lines of Recharts. Fewer lines is not better. It is less freedom.

As of May 2026, the landscape in one breath:

This article walks that landscape as an abstraction ladder — from the lowest (D3) to the highest (Vega-Lite, Superset/Metabase). Then we write the same chart in four libraries and leave a decision table for what to pick when.


1. The abstraction ladder — D3 to Plot to wrappers

The first thing to look at when choosing a chart library is abstraction level. At which layer are you solving the same problem (turn data into pixels)?

[ highest ]
  Superset / Metabase           (SQL -> chart, the user clicks; done)
  Vega-Lite (JSON spec)          (declarative grammar)
  Recharts / Nivo / Chart.js    (component defaults)
  Observable Plot                (grammar + JS API)
  Visx / AntV G2                 (D3-on-top component / language abstractions)
  D3.js                          (scales, axes, shapes, selections)
  Canvas 2D / SVG                (browser primitives)
  WebGL / WebGPU                 (GPU)
[ lowest ]

The higher you climb, the faster you start, but the narrower the freedom. The lower you go, the more pixels you grab — and the more lines you write.

Three practical rules:

  1. If the chart you need looks like something in a library's gallery, start one or two rungs up. Recharts / Nivo / ECharts / Plot covers 80% of dashboards.
  2. If it's not in any gallery, drop one rung. "Two axes, an arrow floating on top, click opens a side panel" — that's Plot or Visx territory.
  3. If that still won't fly, drop to D3. Sankey / Force / Geo and similar non-standard shapes, or extreme performance ("500k points zooming at 60fps").

For pieces meant for an audience (blog diagrams, research reports), do 80% with Plot, 20% with D3. For dashboards inside a product, do 80% with Recharts / ECharts, 20% with Visx / D3.


2. D3.js — the mother of every abstraction

D3 is not a chart library. Precisely, it is a data-to-DOM mapping library, and charts are just one application of it.

Three core abstractions make D3 click.

Scale

Maps a data domain (e.g. 0–1,000,000) to a pixel range (e.g. 0–600). Variants:

import * as d3 from 'd3'

const x = d3.scaleBand()
  .domain(data.map(d => d.month))
  .range([60, 740])
  .padding(0.2)

const y = d3.scaleLinear()
  .domain([0, d3.max(data, d => d.revenue)])
  .range([460, 20])

Selection and join

Bind DOM elements to data. d3.select, selectAll, .data(), .join().

const svg = d3.select('#chart').append('svg')
  .attr('width', 800).attr('height', 480)

svg.selectAll('rect.bar')
  .data(data)
  .join('rect')
    .attr('class', 'bar')
    .attr('x', d => x(d.month))
    .attr('y', d => y(d.revenue))
    .attr('width', x.bandwidth())
    .attr('height', d => 460 - y(d.revenue))
    .attr('fill', '#4f46e5')

This is the "heart" of D3 code. When data changes, a single .join() handles enter / update / exit.

Shape generator

Produces line / area / arc path strings. d3.line, d3.area, d3.arc, d3.pie. Import per module (tree-shaking).

When you still reach for D3 directly in 2026

Honestly, less and less. The rungs above have gotten too good. Still, D3 directly when:

One D3 trap. Since v6/7 the modules are split. Importing only d3-scale, d3-shape, d3-array keeps the bundle in the ~50KB range. import * as d3 from 'd3' is convenient but pulls close to 250KB.


3. Observable Plot — the grammar layer on top of D3

The high-level API built by D3's maintainers. One-line summary: "ggplot2 in JavaScript."

If you don't know ggplot2, the idea of a "grammar of graphics" is to decompose a chart into layers, marks, scales, encodings and compose them. Bars, points, lines are all the same abstraction called a "mark".

The same bar chart that took 25 lines of D3 becomes:

import * as Plot from '@observablehq/plot'

const chart = Plot.plot({
  marginLeft: 60,
  y: { grid: true, label: 'Revenue (USD)' },
  x: { label: 'Month' },
  marks: [
    Plot.barY(data, { x: 'month', y: 'revenue', fill: '#4f46e5' }),
    Plot.ruleY([0])
  ]
})

document.getElementById('chart').append(chart)

Five lines. And those five lines sit directly on D3, so when you need to, you can drop a rung. Grid, scales, axes, and legends come with reasonable defaults, and Plot.plot handles interactions, areas, heatmaps, boxplots, and maps in one place.

Where Plot stands in 2026

Where Plot doesn't fit

One line: for blog diagrams, research reports, and data journalism, Plot is almost always enough.


4. Visx — Airbnb's React + D3 primitives

Visx re-wraps D3 modules as React components. Not a chart library, a toolkit for building charts.

The strengths are clear.

import { scaleBand, scaleLinear } from '@visx/scale'
import { Bar } from '@visx/shape'
import { Group } from '@visx/group'
import { AxisBottom, AxisLeft } from '@visx/axis'

function RevenueChart({ data, width = 800, height = 480 }) {
  const xScale = scaleBand({
    domain: data.map(d => d.month),
    range: [60, width - 20],
    padding: 0.2,
  })
  const yScale = scaleLinear({
    domain: [0, Math.max(...data.map(d => d.revenue))],
    range: [height - 30, 20],
  })
  return (
    <svg width={width} height={height}>
      <Group>
        {data.map(d => (
          <Bar
            key={d.month}
            x={xScale(d.month)}
            y={yScale(d.revenue)}
            width={xScale.bandwidth()}
            height={(height - 30) - yScale(d.revenue)}
            fill="#4f46e5"
          />
        ))}
      </Group>
      <AxisLeft scale={yScale} left={60} />
      <AxisBottom scale={xScale} top={height - 30} />
    </svg>
  )
}

Where Visx stands in 2026

Honestly, the activity has slowed. Majors come less often, issue throughput has dropped. Still:

One line: the exact spot where "Recharts / Nivo feel too rigid, but I'm not going down to raw D3" lands.


5. Recharts — king of React-friendly defaults

Recharts is the poster child of "start fast, go far enough". As of May 2026 it's at v2.15. There's a v3 beta around but 2.15 is the production standard.

The same bar chart:

import { BarChart, Bar, XAxis, YAxis, Tooltip, ResponsiveContainer } from 'recharts'

function RevenueChart({ data }) {
  return (
    <ResponsiveContainer width="100%" height={480}>
      <BarChart data={data}>
        <XAxis dataKey="month" />
        <YAxis />
        <Tooltip />
        <Bar dataKey="revenue" fill="#4f46e5" />
      </BarChart>
    </ResponsiveContainer>
  )
}

Twelve lines. Responsive, tooltip, axes, grid — all on by default. 80% of SaaS dashboards stop here.

Where Recharts really shines

Recharts weaknesses

One line: for internal dashboards, BI widgets, and blog charts, start with Recharts. If it stops working, drop a rung.


6. Apache ECharts 6 — the enterprise beast

The ECharts 6 that landed in November 2025 is closer to a visualization platform than a chart library. An official Apache Foundation project.

Quick characteristics:

Same bar chart:

import * as echarts from 'echarts/core'
import { BarChart } from 'echarts/charts'
import { GridComponent, TooltipComponent } from 'echarts/components'
import { CanvasRenderer } from 'echarts/renderers'

echarts.use([BarChart, GridComponent, TooltipComponent, CanvasRenderer])

const chart = echarts.init(document.getElementById('chart'))
chart.setOption({
  xAxis: { type: 'category', data: data.map(d => d.month) },
  yAxis: { type: 'value' },
  tooltip: { trigger: 'axis' },
  series: [{ type: 'bar', data: data.map(d => d.revenue), itemStyle: { color: '#4f46e5' } }],
})

Why ECharts is strong

ECharts weaknesses

One line: for "a BI with a wild chart variety" or "a heavy-interaction dashboard with lots of data", reach for ECharts.


7. Vega-Lite — JSON blob becomes a chart

A declarative grammar out of the University of Washington. One JSON object defines a chart.

import { default as vegaEmbed } from 'vega-embed'

vegaEmbed('#chart', {
  $schema: 'https://vega.github.io/schema/vega-lite/v5.json',
  data: { values: data },
  mark: { type: 'bar', color: '#4f46e5' },
  encoding: {
    x: { field: 'month', type: 'ordinal' },
    y: { field: 'revenue', type: 'quantitative' },
  },
})

That's it. And that JSON spec is versionable, easy for humans to read and write, and trivial to auto-generate.

Where it shines

Weaknesses

One line: if you want to treat the chart's data / spec systemically, pick Vega-Lite.


8. The rest — Plotly, Chart.js, AntV, Nivo

One line each.


9. The same bar chart in four libraries — at a glance

Monthly revenue ([{month: 'Jan', revenue: 12400}, ...]) as bars. Same data, four approaches.

D3 (low level)

import * as d3 from 'd3'

const width = 800, height = 480, margin = { top: 20, right: 20, bottom: 30, left: 60 }
const svg = d3.select('#chart').append('svg').attr('width', width).attr('height', height)

const x = d3.scaleBand().domain(data.map(d => d.month))
  .range([margin.left, width - margin.right]).padding(0.2)
const y = d3.scaleLinear().domain([0, d3.max(data, d => d.revenue)]).nice()
  .range([height - margin.bottom, margin.top])

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))

svg.selectAll('rect.bar').data(data).join('rect')
  .attr('class', 'bar')
  .attr('x', d => x(d.month))
  .attr('y', d => y(d.revenue))
  .attr('width', x.bandwidth())
  .attr('height', d => y(0) - y(d.revenue))
  .attr('fill', '#4f46e5')

Freedom 100. Lines 25. Interactions added separately.

Observable Plot (grammar)

import * as Plot from '@observablehq/plot'

const chart = Plot.plot({
  marginLeft: 60,
  y: { grid: true, label: 'Revenue (USD)' },
  marks: [
    Plot.barY(data, { x: 'month', y: 'revenue', fill: '#4f46e5', tip: true }),
    Plot.ruleY([0]),
  ],
})
document.getElementById('chart').append(chart)

Freedom 70. Lines 8. Tooltip in one option.

Recharts (React components)

import { BarChart, Bar, XAxis, YAxis, Tooltip, CartesianGrid, ResponsiveContainer } from 'recharts'

function RevenueChart({ data }) {
  return (
    <ResponsiveContainer width="100%" height={480}>
      <BarChart data={data} margin={{ top: 20, right: 20, bottom: 30, left: 60 }}>
        <CartesianGrid strokeDasharray="3 3" />
        <XAxis dataKey="month" />
        <YAxis />
        <Tooltip />
        <Bar dataKey="revenue" fill="#4f46e5" />
      </BarChart>
    </ResponsiveContainer>
  )
}

Freedom 60. Lines 12. React composition friendly.

ECharts (declarative options)

import * as echarts from 'echarts'

const chart = echarts.init(document.getElementById('chart'))
chart.setOption({
  grid: { left: 60, right: 20, top: 20, bottom: 30 },
  xAxis: { type: 'category', data: data.map(d => d.month) },
  yAxis: { type: 'value', name: 'Revenue (USD)' },
  tooltip: { trigger: 'axis' },
  series: [{ type: 'bar', data: data.map(d => d.revenue), itemStyle: { color: '#4f46e5' } }],
})
window.addEventListener('resize', () => chart.resize())

Freedom 80. Lines 11. One option blob.

Line counts look similar — what really differs?

Looking only at line counts hides the real distance. The real differences:

If the chart is one piece of the app, lean Recharts / Visx. If data turning into a picture is the essence, lean Plot / ECharts / D3.


10. Big data — Canvas, WebGL, Deck.gl, regl

This is a different game. Up to ~10k points SVG holds. Past 100k, go Canvas. Past 1M, go WebGL.

Why SVG slows down

Each SVG point is a DOM node. 100,000 points = 100,000 nodes. Browser layout / paint cost scales linearly with node count. Past 5k–10k nodes interactions go sticky, past 100k the page itself is heavy.

Canvas 2D — first step

Canvas paints pixels inside one node (the canvas element). One DOM node for a million points. The catch: you write hit-testing (which point is under the mouse) by hand.

Canvas modes per library:

WebGL — second step

GPU draws points. A million points is light.

WebGPU — next step

January 2026 brought WebGPU to baseline across major browsers. Viz libraries follow slowly.

Million-point scatter — what do you use?

Decision flow:

  1. On a map? Deck.gl — no second thought.
  2. No map, just scatter? regl + D3 scales — 100–300 lines covers it.
  3. Want quick start with a familiar chart pattern? ECharts 6 + progressive rendering — one option line.
  4. Already on Three.js? InstancedMesh.

11. Decision framework — what to use when

One table.

ScenarioFirst pickSecond pickNote
React internal dashboard (less than 5,000 points)RechartsNivoComposition / responsive are natural
Vue / Svelte / vanilla dashboardEChartsPlotFramework-neutral
Blog diagrams, research reportsPlotEChartsFive lines, prints cleanly
Analytical / exploratory UIVisxRaw D3React composition + freedom
BI widgets, wild chart varietyECharts 6PlotlySankey / Sunburst / Calendar
Non-standard shapes (Sankey, Force, Chord)Raw D3AntV G6Not in any gallery
Points / heatmap on a mapDeck.glMapbox GLGPU + maps
Million-point scatterregl + D3ECharts (Canvas)One DOM, GPU
Science / finance (3D, OHLC)PlotlyEChartsDomain-specific
Auto-generated from a JSON specVega-LitePlotLLM-friendly
Graph / networkAntV G6D3-ForceG6's algorithm catalog
Demo / portfolioNivoPlotPretty-first
Self-service / systemic dataSuperset / MetabaseLightdashClick = chart

12. Anti-patterns — common mistakes in the wild

Five things you see repeatedly.

  1. Live 5,000-point scatter with Recharts (SVG) — switch to Canvas / ECharts; it springs back to life immediately.
  2. Importing the full Plotly build to draw one chart — flee to plotly.js-basic-dist or ECharts.
  3. Writing 80 lines of D3 for a bar chart — Plot / Recharts ends it in 5–12 lines.
  4. Missing dispose() for ECharts inside React — memory leak. The useEffect cleanup function is mandatory.
  5. Mixing multiple chart libraries on one page — bundle explosion plus design-tone mismatch. Pick one and go all the way.

13. The BI consumer side — Superset, Metabase, Lightdash

One more branch. The path of not writing charts yourself.

The era where data teams write charts in code is gradually ending. The charts developers ship are shrinking; the charts BI tools ship are growing. That's the macro picture of 2026.


Epilogue — Which rung do you stand on?

Web data visualization is a ladder. The higher you climb, the faster but the narrower. The lower you go, the slower but the freer. One line to remember from the 2026 landscape.

That ratio roughly tracks library popularity. And in the middle, D3 is invisibly underneath everything. Each time you pick a library, peek once at how that library uses D3 modules — that's how you build the taste for picking the next one.

12-item checklist

  1. Does the chart compose naturally as a React component?
  2. When data crosses 5,000 points, did you move from SVG to Canvas / WebGL?
  3. For giant libraries like ECharts / Plotly, are you using modular imports?
  4. Are two or more chart libraries on the same page?
  5. Is responsiveness handled (ResizeObserver or the library's default)?
  6. Is the color palette colorblind-friendly (viridis / cividis etc.)?
  7. Is a non-zero y-axis baseline made explicit to the user?
  8. Are tooltips / focus reachable via keyboard?
  9. Can the underlying data be downloaded as CSV / JSON?
  10. Is there a print / PDF-safe SVG mode?
  11. Did the bundle analyzer confirm tree-shaking for the chart library?
  12. Was i18n (labels, dates, number format) built in from the start?

Next post preview

Candidate next titles: "D3 + WebGL million-point scatter at 60fps", "Letting an LLM draw your charts with Vega-Lite", "Observable Framework as a static data-page workflow".

"A chart is the data's final word. Which rung of the ladder you shout it from sets the tone of the whole article."

— Web data visualization libraries 2026, end.


References

Comments

No comments yet.

Sign in to leave a comment