The D3 chart isn't showing

D3 is quiet when it fails. Selecting an element that isn't there, scaling an undefined value, calling a loader the old way — none of these throw at the point of the mistake. You get an empty <svg>, or no <svg> at all. Open DevTools (F12) and check two places: the Console, and the Elements panel to see whether an <svg> was created and what's inside it.

Match the symptom

What you see Cause
Uncaught ReferenceError: d3 is not defined The library never loaded
No <svg> in Elements, no error d3.select() matched nothing
Error: <path> attribute d: Expected number, "MNaN,NaN…" A scale returned NaN
<svg> exists, chart flipped, squashed or clipped Wrong domain, range or size
Cannot read properties of undefined (reading 'pageX') d3.event — removed in v6
d3.nest is not a function / d3.mouse is not a function v5 code on v6+
Callback passed to d3.csv never runs as expected v4 callback style on v5+
Access to fetch at 'file:///…' … blocked by CORS policy Loading data from a local file
Line chart is a solid black shape The path has a fill

The library didn't load

d3 only exists once its <script> has run. Put it above your code and pin the major version — an unversioned CDN URL serves whatever is latest:

<script src="https://cdn.jsdelivr.net/npm/d3@7"></script>

If the Network tab shows the request failing, you are offline, behind a proxy that blocks the CDN, or inside a viewer whose Content-Security-Policy refuses it. A local d3.min.js works on your machine and 404s once only the .html file is sent — the same trap as lost CSS.

With a bundler, import * as d3 from "d3" is the full namespace; import d3 from "d3" gives undefined because D3 has no default export.

The selection was empty

const svg = d3.select("#chart").append("svg"); // nothing, if #chart doesn't exist yet

D3 doesn't throw on an empty selection — every chained call is a no-op. The script ran in <head> or above the target div, or the id is wrong. Move the script below the element or add defer. While debugging, console.log(d3.select("#chart").empty()) settles it in one line.

NaN in the path

attribute d: Expected number, "MNaN,NaN…" (or attribute cx: Expected length, "NaN") means a scale got something it couldn't map:

  • CSV values are strings. d3.csv returns every field as text, so d3.max(data, d => d.value) compares lexically ("9" > "10") and arithmetic yields NaN on blanks. Convert them:

    const data = await d3.csv("sales.csv", d3.autoType); // or d => ({ ...d, value: +d.value })
    
  • Dates weren't parsed. A time scale needs Date objects: d3.timeParse("%Y-%m-%d")(d.date).

  • The accessor names a field that doesn't exist. d.Value vs d.value, or a header with a trailing space or a byte order mark in front of the first name.

  • The domain was never set, or was set before the data arrived.

The chart is there but wrong

  • Upside down. SVG y grows downward. A y scale needs .range([height, 0]), not [0, height].
  • Tiny, 300×150. That is the default size of an <svg> with no width/height or viewBox. Set a viewBox and let CSS size it: svg.attr("viewBox", [0, 0, width, height]) with width: 100% — which also fixes charts that are tiny on a phone.
  • Axes or labels cut off. Nothing was left for margins. Translate the plot group by margin.left, margin.top and subtract the margins from the ranges.
  • A black blob instead of a line. Paths are filled by default — set .attr("fill", "none").attr("stroke", "currentColor").

Code from an older D3

Most broken D3 is a tutorial from one major version run on another.

Old code Since Now
d3.csv(url, function (error, data) { … }) v5 d3.csv(url).then(data => …) or await
d3.event.pageX inside .on() v6 .on("mousemove", (event, d) => …)
d3.mouse(this) v6 d3.pointer(event)
d3.nest().key(…).entries(data) v6 d3.group(data, d => d.key) / d3.rollup
d3.scale.linear(), d3.svg.axis() v4 d3.scaleLinear(), d3.axisBottom()

The v5 one is the sneakiest: the old callback is still accepted — as a row-conversion function — so nothing errors and nothing draws.

Data from a local file

d3.csv and d3.json use fetch(). A page opened by double-click is file://, its origin is null, and the browser refuses the request — CORS error from origin 'null'. Run python3 -m http.server in the folder while developing, and inline the data for anything you send:

<script>
  const data = d3.csvParse(
    `month,value
Jan,120
Feb,98`,
    d3.autoType,
  );
</script>

Or skip D3 for this chart

If the chart only has to be read — a bar, line or pie in a report or a pull request — it may not need D3 at all. Paste the numbers into CSV to chart or JSON to chart and get a static SVG with no script, no CDN and nothing to fail.

Sharing a D3 chart that works

When it renders, send a link, not the file. Published to Comma, the page runs in a sandboxed iframe (allow-scripts, no allow-same-origin), so transitions, tooltips and brushing work and D3 loads from the CDN inside the frame. Reviewers comment on the exact number or label they mean. One caveat: a published report can't fetch() sibling files, so inline the CSV or JSON before you publish.

Publish a D3 report →

Related