# d3-node: drawing D3 charts on a server with no browser attached

> d3-node runs D3 in Node by giving it a jsdom document to draw into, then hands back an SVG string, a block of HTML, or a PNG stream. It is a small, focused library for static chart and map generation, and its release tags have gone quiet while the package keeps moving.

**d3-node/d3-node** — Server-side D3 for static chart/map generation ✨📊

- Repository: https://github.com/d3-node/d3-node
- Website: https://bradoyler.com/projects/d3-node/
- Stars: 684 · Forks: 70
- Language: JavaScript
- License: not declared
- Published: 2026-10-21 · Updated: 2026-10-21 · Language: en
- Canonical page: https://hysenlabs.com/projects/d3-node-d3-node

## D3 assumes a document, so d3-node supplies a fake one

D3 is a browser library. Its selection machinery reaches into a DOM, appends elements, sets attributes and reads computed structure, so running it on a server means first solving a problem D3 never intended to have. d3-node solves it by pairing D3 with jsdom, a JavaScript implementation of a DOM. The package dependencies are minimal and say a lot:

```json
  "dependencies": {
    "d3": "^7.9.0",
    "jsdom": "^16.7.0"
  },
```

A jsdom document stands in for the browser document, D3 selects and mutates that document, and the result is serialised back to a string. Nothing is rendered to a screen and no layout engine runs, which is exactly why the output is portable SVG with embedded stylesheets rather than a screenshot of pixels. The project states the goal in one line, server-side D3 with ease, and lists the reasons it gives for pre-rendering: pre-rendering offloads data processing and reduces network overhead, the whole npm ecosystem is available to the chart code, and examples from bl.ocks.org can be adapted. The intended user is a Node developer who already knows D3 and needs the same chart in a build step, a report or an API response.

## The smallest useful render is four lines of JavaScript

The basic usage snippet in the README creates an SVG element of a given size, appends a group to it and prints the resulting markup:

```javascript
 import { D3Node } from 'd3-node'
 const d3n = new D3Node()      // initializes D3 with container element
 d3n.createSVG(10,20).append('g') // create SVG w/ 'g' tag and width/height
 d3n.svgString() // output: <svg width="10" height="20" xmlns="http://www.w3.org/2000/svg"><g></g></svg>
```

Three methods carry the whole basic flow. The constructor sets up the jsdom environment and initialises D3 against it. createSVG(width, height) returns a selection bound to a new svg element, so ordinary D3 chaining continues from there. svgString() serialises the document back to markup. The output shown in the comment is worth staring at, because it is a complete, self-contained SVG element with its namespace declared, which is the property that makes server-rendered D3 output droppable into an email, a PDF toolchain or a static page without a script tag. The README notes that the library has been tested on Nodejs v16 and up, so the runtime floor is a modern Node rather than an old one.

## Selectors and containers let you render a whole HTML fragment

Basic mode gives you an SVG in isolation. The advanced mode is where the library earns its keep, because you can hand it a container and a selector, and then ask for a full HTML document or just the chart element:

```javascript
 const options = { selector: '#chart', container: '<div id="container"><div id="chart"></div></div>' }
 const d3n = new D3Node(options) // initializes D3 with container element
 const d3 = d3n.d3
 d3.select(d3n.document.querySelector('#chart')).append('span') // insert span tag into #chart
 d3n.html()   // output: <html><head></head><body><div id="container"><div id="chart"><span></span></div></div></body></html>
 d3n.chartHTML()   // output: <div id="chart"><span></span></div>
```

The d3n.d3 property exposes the underlying D3 instance, and d3n.document exposes the jsdom document, so a chart can be written against ordinary D3 selectors even though the real D3 entry point is not a global. The two output methods differ in a way that matters for embedding: html() returns a complete document with head and body, while chartHTML() returns only the selected container, which is the one to use when the markup has to sit inside a page you already control. The cost of this flexibility is that you are assembling strings and DOM fragments by hand, so the library helps with the drawing, not with layout.

## Styles are inlined into the SVG so the file stands alone

A static SVG that references an external stylesheet breaks the moment it is emailed or embedded as an image, so the library offers an option to inline styles. Passing a styles string to the constructor and then serialising produces a document with a style element in defs:

```javascript
 const d3n = new D3Node({styles:'.test {fill:#000;}'})
 d3n.createSVG().append('g')
 d3n.svgString()
```

The resulting output carries the rule inside the SVG itself, wrapped in a CDATA section:

```html
<svg xmlns="http://www.w3.org/2000/svg">
   <defs>
     <style type="text/css"><![CDATA[ .test{fill:#000;} ]]></style>
   </defs>
   <g></g>
</svg>
```

The CDATA wrapper matters in practice, because a rule containing characters such as ampersands or angle brackets would otherwise break XML parsing, and the defs placement keeps the style out of the rendered tree. The tradeoff is that this is a static snapshot: anything the stylesheet targets has to be inside the same SVG, so dynamic theming or CSS variables from the host page will not carry over. A chart that depends on inherited styles from a larger document needs a different approach, and the README's own TODO list includes adding an option to inject css and js into the html output, which shows the authors consider that gap open.

## Canvas output turns the same drawing code into a PNG

SVG is not always what the consumer needs, so the library also supports a canvas route. The canvas module is not a dependency of the package; you pass it in, and the README notes it supports node-canvas v1 and v2.x:

```javascript
 const canvasModule = require('canvas'); // supports node-canvas v1 & v2.x
 const d3n = new D3Node({ canvasModule }); // pass it node-canvas
 const canvas = d3n.createCanvas(960, 500);
 const context = canvas.getContext('2d');
 // draw on your canvas, then output canvas to png
 canvas.pngStream().pipe(fs.createWriteStream('output.png'));
```

The canvas is created with explicit pixel dimensions, and the resulting object exposes a normal 2D context plus a pngStream, so a pipeline can pipe straight to a file. Because node-canvas is an optional peer that you supply, the version compatibility burden moves to the adopter, and the devDependencies still list svg2png at a fixed version alongside topojson, which is a hint about how the maintainers convert between the two representations themselves. A reasonable caveat: the README documents the canvas path in a short snippet only, with no explanation of which D3 features behave differently there, so treat it as the less travelled branch.

## The npm version and the release tags tell different stories

Maintenance signals in this repository need reading with care. The package manifest declares version 4.1.0, while the listed GitHub releases stop at v2.1.0 dated 2018-10-14, with v1.1.2 from 2017-11-24 and v1.0.3 from 2017-06-08 before it. The release notes for v2.1.0 say it adds a param object for svg attributes, and v1.0.3 added canvas support. So the tagged history stops in 2018 while the package itself has moved on to a much later version, which in practice means the npm tarball is the artefact that changes and the tags are not a reliable changelog. The last push to the default branch was on 2026-10-16, and the repository is not archived, so work is happening. The package manifest lists the licence as MIT, which is permissive, though the top level of the repository does not show a licence entry in its file list, so check the terms in the package you actually pull from npm. The TODO list in the README still mentions more examples, a Gulp task and the css and js injection option.

## Against a headless browser, d3-node trades pixel fidelity for speed of setup

The real alternative is to drive a real browser on the server and take a screenshot or extract the DOM from it. The difference in approach is real and it is not only about speed. A headless browser gives you accurate layout, real font metrics and correct rendering of anything a browser can draw, which is what you need when the chart depends on text measurement, on a CSS framework, or on a plugin that assumes a live browser. d3-node gives you something narrower and cheaper: a DOM without layout, an SVG string, and no browser process to start, configure or keep alive. For a build step that renders hundreds of charts, that difference dominates. For a chart whose labels overlap because jsdom never measured the text, the headless approach is the correct one, and there is no way to close that gap inside d3-node because the omission is structural rather than a missing feature. The examples directory shows the intended range, with a histogram, a datagrid, a pie chart and several map examples including a legacy US map, plus a lib/ folder, and an examples/dist/ directory that suggests pre-rendered output is checked in.

## Conclusion

d3-node suits anyone who needs to produce D3 output away from a browser, such as a report generator, a static site build step or a service that returns an image for a share card. It is the wrong choice for interactive charts, since the library exists to emit a finished artifact, and it is the wrong choice for a long-lived service that must keep a DOM around, because the cost of jsdom is paid on every render. Before adopting it, check the version you actually install, because the package manifest declares 4.1.0 while the newest listed GitHub release tag is v2.1.0 from 2018-10-14, and confirm that the node-canvas API you need matches the version you depend on, since the README states support for node-canvas v1 and v2.x. Then run npm test, which lints the source and executes the mocha suite in test/.

## FAQ

### How do I install d3-node and render a chart?

The package is published on npm as d3-node. Import D3Node from 'd3-node', create an instance, call createSVG with a width and a height, append elements with normal D3 chaining and read the markup back with svgString(). The README states the library is tested on Nodejs v16 and up.

### Can d3-node output HTML instead of just an SVG string?

Yes. Pass a container and a selector to the constructor, and html() returns a complete HTML document while chartHTML() returns only the selected container. The underlying D3 instance and the jsdom document are available as d3n.d3 and d3n.document.

### How do I generate a PNG with d3-node?

Install node-canvas yourself and pass it to the constructor as canvasModule. The README notes support for node-canvas v1 and v2.x, after which createCanvas gives you a canvas whose 2D context you can draw on and whose pngStream you can pipe to a file.

## Sources

- [d3-node/d3-node on GitHub](https://github.com/d3-node/d3-node)
- [Issues](https://github.com/d3-node/d3-node/issues)
- [Project website](https://bradoyler.com/projects/d3-node/)
- [README](https://github.com/d3-node/d3-node/blob/master/README.md)
- [Releases](https://github.com/d3-node/d3-node/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/d3-node-d3-node
