How to Use d3-quadtree for Efficient Spatial Data Operations

d3-quadtree implements a mutable point-quadtree that recursively subdivides two-dimensional space into quadrants, providing O(log n) insertion, removal, and nearest-neighbor searches while automatically expanding its bounding extent to accommodate new points.

The d3-quadtree module in the d3/d3 repository offers a lightweight spatial index optimized for 2D point data. Whether you are building collision detection systems, implementing force-directed graph layouts, or performing geographic proximity searches, understanding how to configure accessors, manage spatial extents, and leverage tree traversal methods will help you achieve logarithmic time complexity for spatial operations.

What Is d3-quadtree?

At its core, d3-quadtree implements a point-quadtree data structure. The algorithm recursively subdivides a two-dimensional bounding box into four equal quadrants (northwest, northeast, southwest, southeast). Each point is stored in a leaf node, and coincident points occupying the exact same coordinates are organized into a linked list within that leaf.

Internal Node Representation

According to the implementation in src/quadtree.js, the tree uses a memory-efficient representation:

  • Internal nodes are sparse four-element arrays where indices 0–3 correspond to the four quadrants. Empty quadrants remain undefined, minimizing memory overhead.
  • Leaf nodes contain {data, next} objects, where data holds the point and next references the next coincident point, forming a linked list.

Automatic Extent Expansion

Unlike static spatial indices, d3-quadtree automatically expands its bounding box to accommodate points outside the current extent. The cover method in src/extent.js computes the smallest square containing both the existing extent and the new point, growing the tree lazily as needed.

Creating and Configuring a Quadtree

To initialize a quadtree, import the constructor from the D3 library and configure coordinate accessors for your data format.

Setting Custom Accessors

By default, d3-quadtree expects objects with x and y properties. For arrays or custom objects, define accessors using the x and y methods (implemented in src/x.js and src/y.js):

import {quadtree} from "d3";

// Data as [longitude, latitude] arrays
const points = [[-122, 37], [-74, 40], [-87, 41]];

const tree = quadtree()
  .x(d => d[0])  // Extract x from index 0
  .y(d => d[1])  // Extract y from index 1
  .addAll(points);

Pre-defining Spatial Extents

For static datasets with known bounds, pre-defining the extent prevents rebalancing overhead during insertion. Use the extent method to set the initial bounding box before adding data:

const tree = quadtree()
  .extent([[0, 0], [800, 600]])  // [x0, y0], [x1, y1]
  .addAll(dataPoints);

Inserting and Removing Points

The d3-quadtree API supports both single-point operations and bulk updates with logarithmic time complexity.

Bulk Insertion with addAll

For initial data loading, addAll (implemented in src/add.js) is significantly faster than iterative add calls because it optimizes extent calculations and tree restructuring:

// Efficient bulk loading
const tree = quadtree().addAll(largeDataset);  // O(n log n) optimized

Dynamic Point Removal

To remove a specific datum, use the remove method. According to src/remove.js, the implementation navigates to the correct leaf and splices the point from the linked list of coincident points:

// Remove a specific point object
tree.remove(specificPoint);

// Remove multiple points efficiently
tree.removeAll(outdatedPoints);

Spatial Search Operations

The primary advantage of d3-quadtree is its ability to perform spatial queries without examining every point.

The find method (defined in src/find.js) implements a branch-and-bound algorithm that prunes quadrants whose bounds cannot contain a closer point than the current best:

// Find closest point to coordinates (250, 250)
const nearest = tree.find(250, 250);
console.log(nearest);  // Returns the data point, not the node

Pass a third argument to find to limit the search to a maximum distance, useful for proximity triggers or collision detection:

// Search within 30 pixels
const neighbor = tree.find(250, 250, 30);

if (neighbor) {
  console.log('Found within radius:', neighbor);
} else {
  console.log('No points inside radius');
}

Rectangular Region Queries

For arbitrary region queries, combine visit with spatial bounds checking. The callback receives the node's bounding box (x0, y0, x1, y1) and can return true to skip that quadrant entirely:

function queryRectangle(tree, x0, y0, x1, y1) {
  const results = [];
  
  tree.visit((node, nx0, ny0, nx1, ny1) => {
    // Skip quadrants outside search area
    if (nx1 < x0 || ny1 < y0 || nx0 > x1 || ny0 > y1) return true;
    
    // Leaf node: traverse linked list of coincident points
    if (!node.length) {
      let d = node;
      while (d) {
        const [x, y] = d.data;
        if (x >= x0 && x <= x1 && y >= y0 && y <= y1) {
          results.push(d.data);
        }
        d = d.next;
      }
    }
    return false;
  });
  
  return results;
}

// Find points in box [100,100] to [200,200]
const matches = queryRectangle(tree, 100, 100, 200, 200);

Advanced Traversal Techniques

Beyond simple searches, d3-quadtree supports sophisticated traversal patterns for complex spatial algorithms.

Pre-order Traversal with visit

The visit method (in src/visit.js) performs a pre-order traversal, invoking the callback before visiting children. This is optimal for pruning searches where parent bounds determine child relevance:

tree.visit((node, x0, y0, x1, y1) => {
  console.log(`Visiting node covering [${x0},${y0}] to [${x1},${y1}]`);
  // Return true to skip descending into this quadrant
  return false;
});

Post-order Traversal with visitAfter

For bottom-up aggregations such as computing center of mass in Barnes-Hut force simulations, use visitAfter (in src/visitAfter.js). This visits children before their parent, allowing you to aggregate child values into parent nodes:

tree.visitAfter((node, x0, y0, x1, y1) => {
  if (node.length) {
    // Internal node: aggregate child calculations
    console.log('Processing internal node after children');
  }
});

Managing Spatial Extents

Understanding extent management is crucial for optimizing tree performance and ensuring all points remain searchable.

Lazy Expansion Behavior

When inserting a point outside the current bounding box, d3-quadtree automatically invokes the cover method (in src/extent.js). This calculates the smallest square that encompasses both the existing extent and the new point, potentially doubling the tree size and adding new root levels:

const tree = quadtree().add([50, 50]);  // Extent: [50,50] to [50,50]
tree.add([1000, 1000]);  // Extent expands to cover [0,0] to [1024,1024]

Explicit Extent Configuration

For datasets with known bounds, explicitly setting the extent prevents the overhead of automatic expansion and ensures optimal tree depth:

const tree = quadtree()
  .extent([[0, 0], [1024, 1024]])
  .addAll(points);

Visualization and Debugging

The D3 repository includes reference implementations for visualizing quadtree operations, useful for educational purposes and debugging spatial algorithms.

Instrumented Search Paths

The file docs/components/quadtreeFindVisited.js contains an instrumented version of the find algorithm that records every visited quadrant during a nearest-neighbor search. This enables visualization of the pruning efficiency—showing which quadrants were examined versus skipped—when rendering search animations.

Node Geometry Extraction

For rendering the tree structure itself, docs/components/quadtreeNodes.js provides utilities to extract the bounding boxes of all internal nodes. This allows you to draw the recursive subdivision lines overlaid on your data points, illustrating how the space is partitioned.

Summary

  • d3-quadtree implements a mutable point-quadtree in src/quadtree.js that recursively subdivides 2D space, using sparse four-element arrays for internal nodes and {data, next} linked lists for leaf nodes to handle coincident points.
  • Configure coordinate extraction using the x and y accessors (implemented in src/x.js and src/y.js) to support arbitrary data formats such as arrays or GeoJSON objects.
  • Achieve O(log n) performance for insertion (add, addAll in src/add.js), removal (remove in src/remove.js), and nearest-neighbor search (find in src/find.js).
  • Execute rectangular region queries and custom spatial algorithms using visit (src/visit.js) for pre-order traversal and visitAfter (src/visitAfter.js) for post-order aggregation.
  • Manage spatial bounds using the extent and cover methods in src/extent.js, with automatic lazy expansion when inserting points outside current boundaries.

Frequently Asked Questions

How does d3-quadtree handle duplicate points at the same coordinates?

When multiple points occupy identical x and y coordinates, d3-quadtree stores them in a linked list within the leaf node. As implemented in src/quadtree.js, each leaf contains data (the point) and next (a reference to the next coincident point). This allows unlimited coincident points without breaking the recursive structure, though traversal methods like visit must iterate through the next chain to access all points at that location.

What is the time complexity of d3-quadtree operations?

d3-quadtree achieves O(log n) average time complexity for core spatial operations. According to the implementations in src/add.js and src/find.js, both insertion and nearest-neighbor search navigate the tree depth, which scales logarithmically with the number of points for balanced distributions. Bulk insertion via addAll operates in O(n log n) time, which is more efficient than n individual add calls due to optimized extent handling and reduced tree restructuring.

Can I use d3-quadtree with geographic coordinates or non-standard data formats?

Yes, d3-quadtree supports arbitrary data formats through configurable accessor functions. By default, it expects objects with x and y properties, but you can redefine these using the x and y methods (implemented in src/x.js and src/y.js) to extract coordinates from arrays [longitude, latitude], GeoJSON features, or custom class instances. The tree stores the original data objects, so search results return complete records with all original properties intact.

How do I find all points within a rectangular region using d3-quadtree?

To perform a rectangular region query, use the visit method (defined in src/visit.js) with a callback that implements spatial pruning. The callback receives the node's bounding box (node, x0, y0, x1, y1); return true to skip quadrants that lie completely outside your search rectangle. For leaf nodes (where !node.length), traverse the linked list via node.data and node.next to collect individual points that fall within the bounds. This approach examines only relevant quadrants, achieving efficient sub-linear performance.

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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →