# D3 Chart Not Showing — Empty SVG, NaN Paths and Version-Mismatch Fixes

Canonical: https://commareports.com/fix/d3-chart-not-showing
Published: 2026-10-17

> Your D3.js chart renders nothing, or an empty SVG. Match the console — d3 is not defined, attribute d: Expected number MNaN, d3.event undefined, d3.nest is not a function — to its fix, plus the silent cases: empty selections, string data and v4 callbacks in v5+.

# 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:

```html
<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](/html-report-broken-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

```js
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:

  ```js
  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](/glossary/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](/fix/html-report-tiny-on-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'](/fix/cors-error-file-origin-null). Run
`python3 -m http.server` in the folder while developing, and inline the
data for anything you send:

```html
<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](/tools/csv-to-chart) or [JSON to chart](/tools/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 →](https://commareports.com/)**

### Related

- [Chart.js chart not showing](/fix/chartjs-chart-not-showing)
- [Plotly chart missing from the exported HTML](/fix/plotly-chart-not-showing-in-html)
- [CORS error from origin 'null'](/fix/cors-error-file-origin-null)
- [Interactive HTML reports](/interactive-html-reports)
- [CSV to chart](/tools/csv-to-chart) · [JSON to chart](/tools/json-to-chart)
