mrkeyoor.com_
Wed 05 Aug 05:03 UTC
npmDataupdated 05 Aug 2026

d3

D3 is a low-level toolkit for building custom data visualizations in the browser. Instead of chart types it gives you primitives: scales that map data to pixels, selections that bind data to DOM and SVG elements, shape generators for lines and arcs, plus force layouts, geo projections, transitions, and zoom behavior. You assemble every chart yourself, which is both the point and the price. Most higher-level chart libraries are built on top of its modules.

Verdict

Unmatched when the visualization is the product and nothing off the shelf fits. For ordinary charts it is the expensive route; reach for Observable Plot or a chart library first and drop down to D3 modules when they run out.

API stability4/5v7 has been the current major since 2021 and the module APIs barely move; you get stability partly through inactivity, and the stale next tag on npm still points at a v6 release candidate.
Docs4/5d3js.org documents every module and the Observable gallery has hundreds of live examples; the catch is that most third-party tutorials on the internet target older majors.
Maintenance3/5Last push May 2026, 20 open issues and PRs, 7.9.0 shipped back in 2024; nothing is broken, but release cadence is slow and momentum has visibly shifted to Observable Plot.
Ecosystem5/5113k stars and the foundation of a large slice of web dataviz; chart libraries and countless examples build on the d3-* modules, and each module works standalone.

Use it if

  • The chart you need does not exist in any charting library and you must control every pixel
  • You are building bespoke interactive graphics with custom transitions, brushing, or zooming, editorial-style work
  • You only need one utility: d3-scale, d3-array, or d3-format installs standalone and is useful under any rendering stack
  • You need force-directed layouts, hierarchies, or map projections, areas where D3 is still the reference implementation
Skip it if

Setup reality

npm install d3 pulls the whole bundle of about 30 sub-modules, roughly 90 kB gzipped, so production apps usually install d3-scale, d3-selection and friends individually instead. D3 has been ESM-only since v7, which still breaks older Jest and CommonJS setups until you configure transforms. The real cost is conceptual: nothing renders until you understand selections, data joins, and scales, and a large share of the tutorials you will find target the pre-v6 API with different event handling.

Patterns

Bind data to elements with a joindata-join

import * as d3 from 'd3';

d3.select('#chart')
  .selectAll('circle')
  .data(points)
  .join('circle')
  .attr('cx', (d) => d.x)
  .attr('cy', (d) => d.y)
  .attr('r', 4);

join() handles enter, update, and exit in one call; if you are copying old enter().append() tutorials you are writing 2017 D3.

Create scales and draw axesscales-axes

const x = d3.scaleLinear().domain([0, 100]).range([40, 600]);
const y = d3.scaleLinear()
  .domain([0, d3.max(data, (d) => d.value)])
  .range([360, 20]);

svg.append('g')
  .attr('transform', 'translate(0,360)')
  .call(d3.axisBottom(x));
svg.append('g')
  .attr('transform', 'translate(40,0)')
  .call(d3.axisLeft(y));

The y range runs high-to-low because SVG pixel 0 is the top; forgetting that inverts every chart.

Draw a line from dataline-chart

const line = d3.line()
  .x((d) => x(d.date))
  .y((d) => y(d.value));

svg.append('path')
  .datum(data)
  .attr('fill', 'none')
  .attr('stroke', 'steelblue')
  .attr('stroke-width', 1.5)
  .attr('d', line);

Use datum (singular) for a single path over the whole array; fill defaults to black, so set fill none or your line becomes a blob.

Draw bars with a band scalebar-chart

const x = d3.scaleBand()
  .domain(data.map((d) => d.name))
  .range([40, 600])
  .padding(0.1);

svg.selectAll('rect')
  .data(data)
  .join('rect')
  .attr('x', (d) => x(d.name))
  .attr('y', (d) => y(d.value))
  .attr('width', x.bandwidth())
  .attr('height', (d) => y(0) - y(d.value));

Compute height as y(0) minus y(value), not the raw value, or bars are wrong the moment the y domain is not [0, max].

Load and type a CSV fileload-csv

const data = await d3.csv('data.csv', d3.autoType);
// without autoType every column is a string:
const manual = await d3.csv('data.csv', (d) => ({
  date: new Date(d.date),
  value: +d.value,
}));

d3.csv returns strings for every column unless you pass d3.autoType or coerce in a row function; math on strings fails silently via concatenation.

Animate attribute changestransitions

svg.selectAll('rect')
  .transition()
  .duration(750)
  .ease(d3.easeCubicOut)
  .attr('y', (d) => y(d.value))
  .attr('height', (d) => y(0) - y(d.value));

A new transition on the same element interrupts the running one by default, which is usually what you want but surprises people mid-animation.

Add zoom and pan to an SVGzoom-pan

const zoom = d3.zoom()
  .scaleExtent([1, 8])
  .on('zoom', (event) => {
    g.attr('transform', event.transform);
  });

svg.call(zoom);

Since v6 the handler receives the event as the first argument; the old d3.event global is gone, which breaks most pre-2020 snippets.

Run a force-directed network layoutforce-graph

const simulation = d3.forceSimulation(nodes)
  .force('link', d3.forceLink(links).id((d) => d.id))
  .force('charge', d3.forceManyBody().strength(-200))
  .force('center', d3.forceCenter(width / 2, height / 2))
  .on('tick', () => {
    link.attr('x1', (d) => d.source.x).attr('y1', (d) => d.source.y)
        .attr('x2', (d) => d.target.x).attr('y2', (d) => d.target.y);
    node.attr('cx', (d) => d.x).attr('cy', (d) => d.y);
  });

The simulation mutates your nodes and links arrays in place (adding x, y, vx, vy), so pass copies if you need the originals untouched.

Plot dates on a time scaletime-axis

const x = d3.scaleTime()
  .domain(d3.extent(data, (d) => d.date))
  .range([40, 600]);

svg.append('g')
  .attr('transform', 'translate(0,360)')
  .call(d3.axisBottom(x).ticks(6).tickFormat(d3.timeFormat('%b %Y')));

scaleTime needs real Date objects in the domain; ISO strings straight from JSON will not work until you parse them.

Map data to colorscolor-scale

// categorical
const color = d3.scaleOrdinal(d3.schemeCategory10);
circle.attr('fill', (d) => color(d.group));

// continuous
const heat = d3.scaleSequential(d3.interpolateViridis)
  .domain([0, d3.max(data, (d) => d.value)]);

scaleOrdinal assigns colors in first-seen order, so the same category can get different colors across charts unless you fix the domain.

Alternatives

PackageRegistryPick it when
@observablehq/plotnpmYou want D3-quality output for standard chart types with a tenth of the code, from the same authors.
chart.jsnpmYou need common canvas charts with a config-object API and no interest in low-level control.
echartsnpmYou want a batteries-included chart library with heavy interactivity and big-data rendering out of the box.