d3-3d is meant for 3d visualizations. d3-3d allows the projection of 3d data onto the screen in the webbrowser. It is specially designed to work with d3.js.
See more examples
- ✅ First-class TypeScript support with full type definitions
- ✅ Custom data accessor functions for any data format
- ✅ Automatic centroid calculation for all shapes
- ✅ Counter-clockwise orientation detection for polygons
- ✅ Generic types for type-safe data transformations
- ✅ Orthographic projection for SVG rendering
- ✅ Sorting utilities for proper z-ordering (painter's algorithm)
If you use npm, npm install d3-3d. You can also download the latest release. Otherwise use unpkg to get the latest release. For example:
<script src="https://unpkg.com/d3-3d/build/d3-3d.js"></script>For a specific version:
<script src="https://unpkg.com/d3-3d@version/build/d3-3d.js"></script>TypeScript users: Type definitions are included automatically. No need to install @types/d3-3d.
ES6 / TypeScript:
import {
triangles3D,
cubes3D,
gridPlanes3D,
points3D,
lineStrips3D,
polygons3D,
planes3D,
lines3D
} from 'd3-3d';
// Import utility function for sorting
import { sort } from 'd3-3d';
// Import TypeScript types
import type { Point3D, Point2D, TransformedPoint } from 'd3-3d';Note: All shapes (
points3D,lines3D,lineStrips3D,triangles3D,planes3D,polygons3D,gridPlanes3D,cubes3D) share the same API. The methods below work for all shape types.
Core Methods:
Configuration Methods:
- .x() - set the x accessor.
- .y() - set the y accessor.
- .z() - set the z accessor.
- .scale() - sets the scale for the projected points.
- .rotateX() - set the angle for the x rotation.
- .rotateY() - set the angle for the y rotation.
- .rotateZ() - set the angle for the z rotation.
- .rotationCenter() - set the rotation center.
- .origin() - set the 2D rendering origin.
- .rows() - set the points per row (gridPlanes3D only).
Utility Functions:
- sort() - utility function to sort shapes by depth.
d3-3d uses the browser's coordinate system and orthographic projection to display your data on the screen. It will calculate the centroid for all elements and the orientation for your polygons. Due to the fact that SVG isn't very 3d compatible d3-3d adds 3d transformations to SVG.
With d3-3d you can easily visualize your 3d data with full TypeScript support.
Basic Example:
import { triangles3D, sort } from 'd3-3d';
const data3D = [
[
{ x: 0, y: -1, z: 0 },
{ x: -1, y: 1, z: 0 },
{ x: 1, y: 1, z: 0 }
]
];
// Create renderer with default Point3D type
const renderer = triangles3D()
.scale(100)
.origin({ x: 480, y: 250 })
.rotateY(Math.PI / 4);
// Transform data - returns array with computed properties
const transformedData = renderer.data(data3D);
// Each transformed triangle includes:
// - rotated: { x, y, z } - rotated 3D coordinates
// - projected: { x, y } - 2D screen coordinates
// - centroid: { x, y, z } - geometric center
// - ccw: boolean - counter-clockwise orientation
// Render with D3
const svg = d3.select('svg');
svg
.selectAll('path')
.data(transformedData)
.join('path')
.attr('d', renderer.draw)
.attr('fill', 'steelblue');TypeScript with Custom Data Types:
import { cubes3D, sort, type Point3D } from 'd3-3d';
// Define your domain-specific data type
interface Building {
lat: number;
lng: number;
height: number;
name: string;
color: string;
}
// Use generic type for full type safety
const renderer = cubes3D<Building>()
.x((d) => d.lng)
.y((d) => d.height)
.z((d) => d.lat)
.scale(50)
.origin({ x: 400, y: 300 });
const buildings: Building[][] = [
[
{ lat: 0, lng: 0, height: 10, name: 'Building A', color: '#ff6b6b' },
{ lat: 1, lng: 0, height: 15, name: 'Building B', color: '#4ecdc4' },
{ lat: 0.5, lng: 1, height: 20, name: 'Building C', color: '#45b7d1' }
]
];
const transformed = renderer.data(buildings);
// TypeScript knows about your custom properties!
transformed[0][0].name; // ✓ string
transformed[0][0].color; // ✓ string
transformed[0].centroid; // ✓ Point3D
transformed[0].ccw; // ✓ boolean
// Sort by depth for proper rendering (back-to-front)
const sorted = transformed.sort(sort);
svg
.selectAll('path')
.data(sorted)
.join('path')
.attr('d', renderer.draw)
.attr('fill', (d) => d[0].color)
.attr('stroke', 'black');All shapes share the same API for configuration, but differ in their input data format and output properties.
| Shape | SVG Element | Input Format | .draw() |
ccw Property |
|---|---|---|---|---|
| points3D | <circle> |
Datum[] - Array of points |
❌ | ❌ |
| lines3D | <line> |
Datum[][] - Array of line pairs |
❌ | ❌ |
| lineStrips3D | <path> |
Datum[][] - Array of point arrays |
✅ | ❌ |
| triangles3D | <path> |
Datum[][] - Array of 3-point arrays |
✅ | ✅ |
| planes3D | <path> |
Datum[][] - Array of 4-point arrays |
✅ | ✅ |
| polygons3D | <path> |
Datum[][] - Array of N-point arrays |
✅ | ✅ |
| gridPlanes3D | <path> |
Datum[] - Grid of points* |
✅ | ✅ |
| cubes3D | <path> |
Datum[][] - Array of 8-vertex arrays |
✅ | ✅ (per face) |
Notes:
- All shapes compute
centroid,rotated, andprojectedproperties ccw(counter-clockwise) is computed for polygon-based shapes to detect front/back faces- Shapes without
.draw()method (points3D,lines3D) can be rendered directly with SVG elements using theprojectedcoordinates
Input Data Details:
- points3D: Each point must have properties accessible via
.x(),.y(),.z()accessors (default:{x, y, z}) - lines3D: Each line is defined by exactly 2 points (start and end)
- lineStrips3D: Each strip connects consecutive points in the array
- triangles3D: Each triangle requires exactly 3 points in counter-clockwise order
- planes3D: Each plane requires exactly 4 points in counter-clockwise order
- polygons3D: Each polygon can have any number of points (≥3) in counter-clockwise order
- gridPlanes3D: Input is a flat array of points that forms a grid. Important: You must specify the number of points per row using .rows() so the library can correctly reconstruct the faces. All rows must have the same length.
- cubes3D: Each cube requires exactly 8 vertices ordered as shown below:
Transforms the input data by applying rotation, projection, and computing additional properties.
Available on: All shapes
Parameters:
data: Datum[][]- Array of shapes, where each shape is an array of data points
Returns: Triangle<Datum>[] (or Polygon<Datum>[], Plane<Datum>[], etc. depending on the shape)
Transformed shapes with the following properties:
- Original data preserved - All properties from your input data are preserved
rotated: Point3D- Rotated 3D coordinates for each pointprojected: Point2D- 2D screen coordinates for each pointcentroid: Point3D- Computed geometric center of the shapeccw: boolean- Whether the shape is counter-clockwise oriented (polygons only)
Example:
const renderer = triangles3D()
.scale(100)
.rotateY(Math.PI / 4);
const data = [
[
{ x: 0, y: 0, z: 0 },
{ x: 1, y: 0, z: 0 },
{ x: 0, y: 1, z: 0 }
]
];
const result = renderer.data(data);
// Access computed properties
console.log(result[0].centroid); // { x: 0.33, y: 0.33, z: 0 }
console.log(result[0].ccw); // true
console.log(result[0][0].rotated); // { x: ..., y: ..., z: ... }
console.log(result[0][0].projected); // { x: ..., y: ... }TypeScript with custom data:
interface CustomPoint {
x: number;
y: number;
z: number;
id: string;
value: number;
}
const renderer = triangles3D<CustomPoint>()
.x((d) => d.x)
.y((d) => d.y)
.z((d) => d.z);
const data: CustomPoint[][] = [...];
const result = renderer.data(data);
// Original properties are preserved
result[0][0].id; // ✓ string
result[0][0].value; // ✓ number
// Computed properties are added
result[0].centroid; // ✓ Point3D
result[0].ccw; // ✓ booleanConstructs an SVG <path> element string based on the transformed shape data.
Available on: lineStrips3D, triangles3D, planes3D, polygons3D, gridPlanes3D, cubes3D
Parameters:
shape: TransformedPoint<Datum>[]- A single transformed shape (from.data()result)
Returns: string - SVG path string (e.g., "M0,0L1,0L0.5,1Z")
Example:
const renderer = triangles3D();
const transformed = renderer.data(myData);
// Draw single shape
const pathString = renderer.draw(transformed[0]);
// Use with D3
svg.selectAll('path').data(transformed).join('path').attr('d', renderer.draw);If x is specified, sets the x accessor to the specified function or number and returns the shape instance for chaining. If x is not specified, returns the current x accessor, which defaults to:
Available on: All shapes
function x(p) {
return p.x;
}This function will be invoked for each point in the input data array.
If y is specified, sets the y accessor to the specified function or number and returns the shape instance for chaining. If y is not specified, returns the current y accessor, which defaults to:
Available on: All shapes
function y(p) {
return p.y;
}This function will be invoked for each point in the input data array.
If z is specified, sets the z accessor to the specified function or number and returns the shape instance for chaining. If z is not specified, returns the current z accessor, which defaults to:
Available on: All shapes
function z(p) {
return p.z;
}This function will be invoked for each point in the input data array.
If scale is specified, sets the scale to the specified number and returns the shape instance for chaining. If scale is not specified, returns the current scale.
Available on: All shapes
Default: 1
If angleX is specified, sets angleX to the specified number (in radians) and returns the shape instance for chaining. If angleX is not specified, returns the current angleX.
Available on: All shapes
Default: 0
angleX should be expressed in radians, for example: Math.PI / 4.
If angleY is specified, sets angleY to the specified number (in radians) and returns the shape instance for chaining. If angleY is not specified, returns the current angleY.
Available on: All shapes
Default: 0
angleY should be expressed in radians, for example: Math.PI / 4.
If angleZ is specified, sets angleZ to the specified number (in radians) and returns the shape instance for chaining. If angleZ is not specified, returns the current angleZ.
Available on: All shapes
Default: 0
angleZ should be expressed in radians, for example: Math.PI / 4.
Sets the center point around which rotations are performed. This is different from .origin() which controls the 2D rendering position on the screen.
Available on: All shapes
Parameters:
point?: Point3D- The 3D point to rotate around
Returns:
- If called without arguments: current
Point3Dvalue - If called with arguments:
this(for chaining)
Default: { x: 0, y: 0, z: 0 }
Example:
const renderer = triangles3D()
.rotationCenter({ x: 50, y: 50, z: 0 }) // Rotate around point (50,50,0)
.rotateY(Math.PI / 2);
// The rotation will pivot around (50,50,0) instead of (0,0,0)If origin is specified, sets the 2D rendering origin to the specified point and returns the shape instance for chaining. If origin is not specified, returns the current origin.
Available on: All shapes
Default: { x: 0, y: 0 }
Sets the number of points per row (columns) for gridPlanes3D. Since a grid is passed as a flat array, the library needs to know how many points constitute one horizontal line to correctly create the rectangular faces.
Available on: gridPlanes3D
Parameters:
rows?: number- Number of points per row
Returns:
- If called without arguments: current
number - If called with arguments:
this(for chaining)
Default: 1
Example:
const points = [
{ x: 0, y: 0, z: 0 },
{ x: 1, y: 0, z: 0 },
{ x: 2, y: 0, z: 0 }, // Row 0
{ x: 0, y: 0, z: 1 },
{ x: 1, y: 0, z: 1 },
{ x: 2, y: 0, z: 1 } // Row 1
];
const grid = gridPlanes3D()
.rows(3) // 3 points per row
.data(points);A comparator function for sorting 3D shapes by their centroid's z-coordinate. Use this with JavaScript's .sort() to render shapes in correct depth order (painter's algorithm).
Usage:
import { triangles3D, sort } from 'd3-3d';
const renderer = triangles3D();
const transformed = renderer.data(data);
// Sort back-to-front for correct rendering
const sorted = transformed.sort(sort);
svg.selectAll('path').data(sorted).join('path').attr('d', renderer.draw);Type Signature:
function sort<T extends HasCentroid>(a: T, b: T): number;
interface HasCentroid {
centroid: { z: number };
}All shape transformations automatically compute additional properties on the returned data:
The geometric center of the shape in 3D space (after rotation). Available on all shapes.
const data = triangles3D().data(myTriangles);
console.log(data[0].centroid); // { x: 1.5, y: 2.0, z: 0.5 }Use this for:
- Sorting shapes by depth
- Calculating bounding boxes
- Finding center points for labels or interaction
Boolean indicating whether the polygon is oriented counter-clockwise when viewed from the camera. Useful for backface culling. Available on: triangles3D, polygons3D, planes3D, cubes3D (per face).
const data = triangles3D().data(myTriangles);
if (data[0].ccw) {
// Front-facing triangle - render normally
} else {
// Back-facing triangle - optionally skip or render differently
}Algorithm: Uses the shoelace formula on the rotated 2D projection to determine orientation.
3D coordinates after rotation has been applied. Available on each individual point.
const data = points3D().data(myPoints);
console.log(data[0].rotated); // { x: number, y: number, z: number }2D screen coordinates after orthographic projection. Available on each individual point.
const data = points3D().data(myPoints);
console.log(data[0].projected); // { x: number, y: number }The API has been modernized with a new .data() method pattern for better TypeScript support and clarity.
Before (v0.x):
const triangles = triangles3D();
const result = triangles(data); // Called as functionAfter (v1.0+):
const renderer = triangles3D();
const result = renderer.data(data); // Explicit .data() methodBreaking Changes:
- ❌ Direct function invocation removed:
renderer(data)no longer works - ✅ Use
.data()method instead:renderer.data(data) - ✅ Full TypeScript generics support added
- ✅ Computed properties (
centroid,ccw) now included automatically
Benefits:
- Better IDE autocomplete and type inference
- Explicit API that's easier to understand
- No confusion between configuration and data transformation
- Full type safety with custom data structures
Migration Example:
- const triangles = triangles3D().scale(100);
- const result = triangles(data);
+ const renderer = triangles3D().scale(100);
+ const result = renderer.data(data);
svg.selectAll('path')
.data(result)
.join('path')
.attr('d', triangles.draw);