/

3D Surface Chart

Render mathematical functions or grid data as a smooth 3D mesh with computed normals, height-based coloring, and optional wireframe mode.

Quick Start

import { Surface3D } from "@chartts/gl"
 
// Generate a sine-cosine surface
const size = 40
const grid: number[][] = []
for (let r = 0; r < size; r++) {
  const row: number[] = []
  for (let c = 0; c < size; c++) {
    const x = (c / size) * 4 * Math.PI - 2 * Math.PI
    const z = (r / size) * 4 * Math.PI - 2 * Math.PI
    row.push(Math.sin(x) * Math.cos(z) * 2)
  }
  grid.push(row)
}
 
const chart = Surface3D("#chart", {
  data: { series: [], grid },
  orbit: { autoRotate: true },
})

That renders a smooth 3D surface mesh with height-based coloring that transitions from deep blue through teal and emerald to gold and coral. The surface has per-vertex normals computed from the height grid for proper Phong lighting. Orbit controls let you rotate, zoom, and pan.

When to Use 3D Surface Charts

Surface charts visualize continuous functions of two variables or gridded data where each cell has a height value.

Use a 3D surface chart when:

  • Visualizing mathematical functions like f(x, z) = y
  • Displaying terrain, elevation maps, or heightfield data
  • Showing how an output varies continuously across two input dimensions
  • Exploring optimization landscapes (loss surfaces, fitness landscapes)

Don't use a 3D surface chart when:

  • Your data is sparse or irregularly sampled (use a 3D scatter chart)
  • You need to compare discrete categories (use a bar chart)
  • The surface is mostly flat with rare features (a heatmap is more readable)
  • Users cannot interact with the visualization (wireframe loses depth cues in print)

Props Reference

PropTypeDefaultDescription
dataGLChartDatarequiredChart data with grid: number[][] as the heightmap
wireframebooleanfalseRender as wireframe lines instead of solid triangles
cameraCameraOptionsauto-fitCamera position and target. Auto-calculated from grid bounds if omitted
orbitboolean | OrbitConfigtrueEnable orbit controls with optional auto-rotation
lightPartial<LightConfig>defaultPhong lighting configuration
theme'dark' | 'light' | GLTheme'dark'Color theme for background, text, and grid
animatebooleantrueEnable fade-in animation on mount
tooltipbooleantrueShow tooltip on hover with row, column, and height value

Wireframe Mode

Set wireframe to true to render the surface as a grid of lines instead of filled triangles. Wireframe mode is useful for seeing through the surface and understanding the underlying grid structure.

Surface3D("#chart", {
  data: { series: [], grid },
  wireframe: true,
})

In wireframe mode, each grid cell draws horizontal and vertical lines connecting adjacent vertices. The height-based coloring is still applied to each vertex, so the wireframe retains the elevation gradient.


Height-Based Color Mapping

The surface automatically maps height values to a five-stop color gradient:

  • Deep blue for the lowest values
  • Teal for low-mid values
  • Emerald for mid values
  • Gold for mid-high values
  • Coral for the highest values

This mapping is computed per-vertex from the global min and max of the grid, so the full color range is always used regardless of the data scale.

// A simple peak function
const grid: number[][] = []
for (let r = 0; r < 50; r++) {
  const row: number[] = []
  for (let c = 0; c < 50; c++) {
    const x = (c / 50) * 10 - 5
    const z = (r / 50) * 10 - 5
    row.push(5 * Math.exp(-(x * x + z * z) / 8))
  }
  grid.push(row)
}
 
Surface3D("#peak", {
  data: { series: [], grid },
  orbit: { autoRotate: true, autoRotateSpeed: 0.4 },
})

Smooth Normals

Surface normals are computed from finite differences of neighboring height values. This produces smooth Phong shading across the entire mesh without visible facets. The normal at each vertex accounts for the slope in both the x and z directions.

Surface3D("#chart", {
  data: { series: [], grid },
  light: {
    ambient: 0.2,
    diffuse: 0.8,
    specular: 0.5,
    shininess: 64,
    position: [10, 20, 10],
  },
})

Adjusting the light position changes how highlights sweep across peaks and valleys. Higher specular shininess produces tighter, brighter highlights on steep slopes.


Accessibility

  • Tooltip shows the grid row, column, and exact height value on hover
  • The height-to-color gradient provides a visual legend for value magnitude
  • Wireframe mode offers an alternative view that does not rely solely on color
  • Dark and light themes ensure text and grid lines remain readable against the surface

Real-World Examples

Mathematical function explorer

const size = 60
const grid: number[][] = []
for (let r = 0; r < size; r++) {
  const row: number[] = []
  for (let c = 0; c < size; c++) {
    const x = (c / size) * 6 - 3
    const z = (r / size) * 6 - 3
    const d = Math.sqrt(x * x + z * z)
    row.push(d === 0 ? 1 : Math.sin(d * Math.PI) / (d * Math.PI))
  }
  grid.push(row)
}
 
Surface3D("#sinc", {
  data: { series: [], grid },
  orbit: { autoRotate: true, autoRotateSpeed: 0.5 },
  theme: "dark",
})

Terrain elevation map

// Procedural terrain with multiple octaves
const size = 80
const grid: number[][] = []
for (let r = 0; r < size; r++) {
  const row: number[] = []
  for (let c = 0; c < size; c++) {
    const x = c / size, z = r / size
    const height =
      Math.sin(x * 8) * Math.cos(z * 6) * 2 +
      Math.sin(x * 16 + 1) * Math.cos(z * 12 + 2) * 0.5 +
      Math.sin(x * 4 - z * 3) * 1.5
    row.push(height)
  }
  grid.push(row)
}
 
Surface3D("#terrain", {
  data: { series: [], grid },
  camera: {
    position: [12, 8, 12],
    target: [0, 0, 0],
  },
})

Optimization loss landscape

const size = 50
const grid: number[][] = []
for (let r = 0; r < size; r++) {
  const row: number[] = []
  for (let c = 0; c < size; c++) {
    const x = (c / size) * 8 - 4
    const z = (r / size) * 8 - 4
    // Rosenbrock-like function with multiple local minima
    row.push(
      (1 - x) * (1 - x) + 10 * (z - x * x) * (z - x * x) * 0.01
    )
  }
  grid.push(row)
}
 
Surface3D("#loss", {
  data: { series: [], grid },
  wireframe: true,
  orbit: { autoRotate: true, autoRotateSpeed: 0.2 },
  theme: "light",
})

Other Charts