How to Create Contour Plots with d3-contour: A Complete Developer's Guide
d3-contour transforms rectangular grids of numeric values into GeoJSON MultiPolygon contour shapes that render as SVG paths via d3.geoPath() or Observable Plot's declarative API.
The d3-contour module is the dedicated library within the D3 ecosystem for computing contour plots from continuous data. Whether you are visualizing topographic elevation, temperature gradients, or mathematical surfaces, knowing how to create contour plots with d3-contour enables you to convert raw numeric arrays into standardized GeoJSON geometries suitable for web-based visualization.
Core Architecture of d3-contour
The module exposes a single factory function that generates a configurable contour generator. Understanding these components is essential for controlling the output resolution and appearance.
| Component | Role | Configuration |
|---|---|---|
Contour generator (d3.contours()) |
Factory creating a generator object that stores grid dimensions, thresholds, and smoothing options. | Instantiated once per dataset. |
.size([width, height]) |
Defines the grid dimensions. The input values array must contain exactly width × height elements in row-major order. |
Required before computing contours. |
.thresholds(array | count) |
Specifies the contour levels. Accepts an explicit array of threshold values or a number (uses Sturges' formula for automatic binning). | Optional; defaults to automatic binning. |
.smooth(boolean) |
Toggles linear interpolation smoothing on polygon boundaries. Default is true. |
Set to false for pixelated, blocky contours. |
.contour(values, threshold) |
Computes a single contour level for the given grid values. Returns a GeoJSON MultiPolygon. | Use for individual level control. |
.contours(values) |
Computes all configured threshold levels at once. Returns an array of GeoJSON MultiPolygon objects. | Primary method for full plots. |
The generated GeoJSON objects include a value property on the geometry indicating the threshold level, which enables color scaling during rendering.
Step-by-Step Implementation Workflow
Creating a contour plot follows a predictable data flow from raw numbers to rendered SVG.
- Prepare the grid data as a flat numeric array in row-major order with length equal to
width × height. - Configure the generator by calling
d3.contours()and chaining.size(),.thresholds(), and.smooth(). - Compute geometries by passing the values array to
.contours()or.contour(). - Render the paths using
d3.geoPath()with the identity projection (since coordinates are already in pixel space).
Practical Code Examples
Minimal SVG Contour Plot
This complete example generates a contour plot of the Goldstein-Price function, a standard test function for optimization algorithms.
import * as d3 from "d3";
// 1️⃣ Create synthetic grid data (Goldstein-Price function)
const width = 200;
const height = 200;
const values = new Float32Array(width * height);
for (let j = 0, k = 0; j < height; ++j) {
for (let i = 0; i < width; ++i, ++k) {
const x = (i / width) * 4 - 2; // Map to -2 → 2
const y = 1 - (j / height) * 3; // Map to -2 → 1
values[k] = goldsteinPrice(x, y);
}
}
function goldsteinPrice(x, y) {
return (1 + Math.pow(x + y + 1, 2) *
(19 - 14*x + 3*x*x - 14*y + 6*x*y + 3*y*y)) *
(30 + Math.pow(2*x - 3*y, 2) *
(18 - 32*x + 12*x*x + 48*y - 36*x*y + 27*y*y));
}
// 2️⃣ Configure the contour generator
const contours = d3.contours()
.size([width, height])
.thresholds(d3.ticks(d3.min(values), d3.max(values), 10))
.smooth(true);
// 3️⃣ Compute contour geometries
const polygons = contours(values);
// 4️⃣ Render as SVG paths
const svg = d3.select("body")
.append("svg")
.attr("viewBox", `0 0 ${width} ${height}`)
.attr("width", 600)
.attr("height", 600);
svg.selectAll("path")
.data(polygons)
.enter()
.append("path")
.attr("d", d3.geoPath())
.attr("fill", d => d3.interpolateMagma(d.value / d3.max(polygons, p => p.value)))
.attr("stroke", "#000")
.attr("stroke-width", 0.3);
Key implementation details:
- The
valuesarray uses row-major order where indexk = j * width + i. d3.ticks()generates evenly spaced threshold values across the data domain.d3.geoPath()uses the identity projection because contour coordinates are already in pixel space.
Observable Plot Integration
For users of Observable Plot, the Plot.contour mark provides a declarative wrapper that handles generator configuration internally:
Plot.plot({
axis: null,
aspectRatio: 1,
marks: [
Plot.contour(volcano.values, {
width: volcano.width,
height: volcano.height,
fill: Plot.identity,
stroke: "black",
interval: 5
})
]
})
This approach abstracts the d3.contours() generator and d3.geoPath() rendering into a single mark configuration, as demonstrated in the official documentation at docs/d3-contour.md.
Source Code Reference
Understanding the module's implementation helps debug complex visualizations:
| File | Purpose |
|---|---|
src/index.js |
Central export table re-exporting d3-contour from the main D3 bundle. |
docs/d3-contour/contour.md |
Complete API documentation for the contour generator configuration. |
docs/d3-contour.md |
High-level overview with Observable Plot integration examples. |
src/contours.js (d3-contour package) |
Core implementation of the marching-squares algorithm, smoothing logic, and threshold handling. |
The marching-squares algorithm implemented in src/contours.js processes the grid cell-by-cell to extract iso-contour lines, then polygonizes them into GeoJSON MultiPolygons with proper hole handling for nested contour levels.
Summary
- d3-contour generates GeoJSON MultiPolygon contours from flat numeric grids via the
d3.contours()factory. - Configure the generator with
.size([w, h])for grid dimensions,.thresholds()for contour levels, and.smooth()for boundary interpolation. - The output renders directly with
d3.geoPath()using the identity projection, or through high-level abstractions likePlot.contour. - Source implementation resides in the d3-contour package (
src/contours.js) and is re-exported from the main D3 bundle (src/index.js).
Frequently Asked Questions
What data format does d3-contour require?
d3-contour expects a flat numeric array (TypedArray or standard Array) containing width × height values in row-major order. The first width elements represent the top row of the grid. You must specify the grid dimensions via .size([width, height]) so the generator knows how to map the 1D index to 2D coordinates.
How do I choose appropriate threshold values?
You can pass an explicit array of threshold values to .thresholds() (e.g., [0, 5, 10, 15]), or provide a count number to use automatic binning based on Sturges' formula. For continuous color scales, use d3.ticks(min, max, count) to generate evenly spaced thresholds across your data domain, as demonstrated in the Goldstein-Price example.
Why are my contours appearing pixelated or blocky?
The .smooth() method controls boundary interpolation. By default, smoothing is enabled (true), which uses linear interpolation to place contour lines at sub-pixel precision between grid cells. If you set .smooth(false), contours align exactly with grid cell edges, producing the characteristic "blocky" appearance of raw marching-squares output without interpolation.
Can I use d3-contour with Canvas instead of SVG?
Yes. While the standard examples use SVG with d3.geoPath(), you can render the generated GeoJSON MultiPolygons to HTML5 Canvas by passing a 2D rendering context to d3.geoPath(context). Calling path(geometry) will then stroke or fill the contours directly on the canvas using the standard Canvas 2D API.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →