Tutorial2026-03-0111 min read

3D Charts in JavaScript with Chart.ts WebGL

Create 3D scatter plots, surface charts, globe visualizations, and more with @chartts/gl. GPU-accelerated WebGL rendering.

3D data visualization on the web has historically required either D3 with custom WebGL code or heavyweight libraries like Three.js wrapped around a charting abstraction. Neither approach is simple. You end up writing shader code or managing a 3D scene graph when all you wanted was a scatter plot with a z-axis.

@chartts/gl is the Chart.ts package for 3D visualization. It provides six GPU-accelerated chart types that use WebGL under the hood but expose the same declarative API as every other Chart.ts chart. No shader code. No scene management. Just data and props.

Installation

npm install @chartts/gl

For React:

npm install @chartts/gl @chartts/react

@chartts/gl has a single dependency: @chartts/core. It adds approximately 12kb gzipped to your bundle.

Scatter3D: multivariate data exploration

The most common 3D chart type. Each point has x, y, and z coordinates. You can also map color and size to additional data dimensions, giving you five variables in a single view.

import { Scatter3D } from "@chartts/gl/react"
 
const clusterData = [
  { x: 2.3, y: 4.1, z: 1.8, cluster: "A", weight: 0.9 },
  { x: 5.1, y: 2.8, z: 3.4, cluster: "B", weight: 0.4 },
  { x: 1.7, y: 6.2, z: 5.1, cluster: "A", weight: 0.7 },
  // ... thousands of points
]
 
export function ClusterAnalysis() {
  return (
    <Scatter3D
      data={clusterData}
      x="x"
      y="y"
      z="z"
      color="cluster"
      size="weight"
      className="h-[500px]"
      orbit
      grid
      axisLabels={{ x: "Feature 1", y: "Feature 2", z: "Feature 3" }}
      colorScale={["#3b82f6", "#ef4444", "#22c55e"]}
    />
  )
}

The orbit prop enables click-and-drag rotation, scroll-to-zoom, and right-click panning. The grid prop draws reference lines on the floor, back wall, and side wall of the 3D space.

Scatter3D handles up to 200,000 points at 60fps using instanced rendering. Each point is drawn as a sphere with a single GPU draw call for the entire dataset.

Surface3D: continuous surface visualization

Surface charts render a continuous mesh from a grid of z-values. They are used for topographic data, mathematical functions, heatmap elevation, and response surfaces in optimization.

import { Surface3D } from "@chartts/gl/react"
 
// Generate a surface: z = sin(x) * cos(y)
const surfaceData = []
for (let xi = 0; xi < 100; xi++) {
  for (let yi = 0; yi < 100; yi++) {
    const x = (xi - 50) / 10
    const y = (yi - 50) / 10
    surfaceData.push({
      x,
      y,
      z: Math.sin(x) * Math.cos(y),
    })
  }
}
 
export function MathSurface() {
  return (
    <Surface3D
      data={surfaceData}
      x="x"
      y="y"
      z="z"
      className="h-[500px]"
      orbit
      colorScale={["#1e40af", "#3b82f6", "#93c5fd", "#fca5a5", "#ef4444", "#991b1b"]}
      wireframe={false}
      gridSize={100}
    />
  )
}

The gridSize prop tells Chart.ts the resolution of the grid (100x100 in this case). It uses this to build the triangle mesh efficiently. The colorScale maps z-values to a gradient, so peaks and valleys are visually distinct.

Surface3D handles grids up to 1000x1000 (1 million vertices) at interactive frame rates.

Globe3D: geographic data on a sphere

Globe3D renders data on a 3D sphere with country boundaries. It is designed for geographic datasets where a flat map projection introduces too much distortion, or where the spherical context matters for understanding the data.

import { Globe3D } from "@chartts/gl/react"
 
const populationData = [
  { country: "US", value: 331_000_000 },
  { country: "CN", value: 1_412_000_000 },
  { country: "IN", value: 1_408_000_000 },
  { country: "BR", value: 214_000_000 },
  { country: "NG", value: 218_000_000 },
  // ... all countries
]
 
export function WorldPopulation() {
  return (
    <Globe3D
      data={populationData}
      country="country"
      value="value"
      className="h-[500px]"
      orbit
      autoRotate
      autoRotateSpeed={0.5}
      colorScale={["#dbeafe", "#1e40af"]}
      oceanColor="#0f172a"
      borderColor="#334155"
      tooltip={(d) => `${d.country}: ${(d.value / 1_000_000).toFixed(0)}M`}
    />
  )
}

The autoRotate prop spins the globe slowly, which is useful for presentations and dashboards. Country codes use ISO 3166-1 alpha-2 format. The built-in geometry includes boundaries for all 195 countries.

Globe3D also supports point markers for city-level data:

<Globe3D
  className="h-[500px]"
  orbit
  oceanColor="#0f172a"
>
  <Globe3D.Points
    data={cities}
    lat="latitude"
    lng="longitude"
    size="population"
    color="#ef4444"
    tooltip={(d) => d.name}
  />
  <Globe3D.Arcs
    data={flights}
    fromLat="originLat"
    fromLng="originLng"
    toLat="destLat"
    toLng="destLng"
    stroke="#3b82f6"
    strokeWidth={1}
  />
</Globe3D>

The Globe3D.Arcs component draws curved lines between pairs of coordinates, perfect for flight routes, trade flows, or network connections.

Bar3D: categorical data in three dimensions

Bar3D extends the standard bar chart into 3D space. Each bar has x, y (category axes) and z (value axis) positions.

import { Bar3D } from "@chartts/gl/react"
 
const salesData = [
  { region: "North", quarter: "Q1", revenue: 42_000 },
  { region: "North", quarter: "Q2", revenue: 51_000 },
  { region: "South", quarter: "Q1", revenue: 38_000 },
  { region: "South", quarter: "Q2", revenue: 45_000 },
  { region: "East", quarter: "Q1", revenue: 29_000 },
  { region: "East", quarter: "Q2", revenue: 34_000 },
  { region: "West", quarter: "Q1", revenue: 55_000 },
  { region: "West", quarter: "Q2", revenue: 62_000 },
]
 
export function RegionalSales() {
  return (
    <Bar3D
      data={salesData}
      x="region"
      y="quarter"
      z="revenue"
      className="h-[500px]"
      orbit
      colorScale={["#3b82f6", "#8b5cf6"]}
      barWidth={0.6}
      tooltip
    />
  )
}

Bar3D is effective for two-category comparisons where you want to see the full grid at once. The orbit controls let you rotate to compare bars from different angles.

Map3D: extruded choropleth

Map3D renders a flat map with extruded regions. The height of each region represents a data value, creating a physical sense of magnitude that flat choropleths lack.

import { Map3D } from "@chartts/gl/react"
 
const gdpData = [
  { country: "US", gdp: 25_460 },
  { country: "CN", gdp: 17_960 },
  { country: "JP", gdp: 4_230 },
  { country: "DE", gdp: 4_070 },
  { country: "GB", gdp: 3_070 },
  // ...
]
 
export function WorldGDP() {
  return (
    <Map3D
      data={gdpData}
      country="country"
      value="gdp"
      className="h-[500px]"
      orbit
      extrudeScale={0.001}
      colorScale={["#dbeafe", "#1e40af"]}
      tooltip={(d) => `${d.country}: $${d.gdp}B`}
    />
  )
}

The extrudeScale prop controls how much height per unit of value. Adjust this based on your data range to get visually balanced extrusions.

Orbit controls and interaction

All 3D chart types share the same interaction model when orbit is enabled:

  • Left click + drag: Rotate around the center
  • Scroll wheel: Zoom in/out
  • Right click + drag: Pan the camera
  • Double click: Reset to default camera position

You can also set the initial camera position programmatically:

<Scatter3D
  data={data}
  x="x"
  y="y"
  z="z"
  orbit
  camera={{
    position: [5, 3, 5],   // x, y, z position
    target: [0, 0, 0],      // Look-at point
    fov: 45,                 // Field of view in degrees
  }}
/>

For animated camera transitions (useful for guided storytelling), use the imperative API:

import { useChartRef } from "@chartts/react"
 
function AnimatedView() {
  const chartRef = useChartRef()
 
  const focusCluster = () => {
    chartRef.current?.animateCamera({
      position: [2, 1, 2],
      target: [2.3, 4.1, 1.8],
      duration: 800,
      easing: "easeInOut",
    })
  }
 
  return (
    <>
      <button onClick={focusCluster}>Focus Cluster A</button>
      <Scatter3D ref={chartRef} data={data} x="x" y="y" z="z" orbit />
    </>
  )
}

Combining 2D and 3D

A common pattern is using 3D for the main visualization and 2D charts for supporting panels. Since @chartts/gl and the standard Chart.ts components share the same theme system, they look consistent side by side:

import { Scatter3D } from "@chartts/gl/react"
import { BarChart, Histogram } from "@chartts/react"
 
export function DataExplorer({ data }) {
  return (
    <div className="grid grid-cols-3 gap-4">
      <div className="col-span-2">
        <Scatter3D data={data} x="x" y="y" z="z" color="cluster" orbit className="h-96" />
      </div>
      <div className="flex flex-col gap-4">
        <BarChart data={clusterCounts} x="cluster" y="count" className="h-44" />
        <Histogram data={data} value="z" bins={20} className="h-44" />
      </div>
    </div>
  )
}

Performance characteristics

All @chartts/gl chart types use WebGL exclusively. Here are the performance characteristics on a 2024 MacBook Pro (M3 Pro), Chrome 125:

Chart typeMax data pointsRender timeInteraction FPS
Scatter3D200,00045ms60fps
Surface3D1,000,000 vertices120ms55fps
Globe3D195 countries + 10,000 points60ms60fps
Bar3D10,000 bars25ms60fps
Map3D195 countries35ms60fps

These numbers include full orbit interaction (rotation, zoom, pan). The GPU does the heavy work, leaving the main thread free for your application logic.

When to use 3D

3D charts are not always the right choice. They add visual complexity and can make exact value comparison harder than 2D alternatives. Use 3D when:

  • Your data has three or more continuous dimensions
  • Spatial relationships matter (geographic data, molecular structures)
  • You need to show the shape of a surface or distribution
  • The presentation context benefits from visual impact

For most business dashboards, 2D charts are clearer. For scientific analysis, engineering, and geographic applications, 3D can reveal patterns that flat projections hide. Chart.ts gives you both options with a consistent API.