3D Scatter Plot
The ScatterPlot3D component provides efficient 3D scatter plot visualization using Three.js (via Threlte) with instanced rendering for optimal performance with thousands of points. It supports colored surfaces, multiple data series, and interactive camera controls.
Basic 3D Scatter Plot
A simple 3D scatter plot with multiple data series. Use mouse to rotate, scroll to zoom, and right-click drag to pan:
<script lang="ts">
import { ScatterPlot3D } from 'matterviz'
// Generate helical data
const n_points = 100
const helix_1 = {
x: Array.from({ length: n_points }, (_, idx) => Math.cos(idx * 0.2)),
y: Array.from({ length: n_points }, (_, idx) => idx * 0.1),
z: Array.from({ length: n_points }, (_, idx) => Math.sin(idx * 0.2)),
point_style: { fill: `steelblue` },
label: `Helix 1`,
}
const helix_2 = {
x: Array.from({ length: n_points }, (_, idx) => Math.cos(idx * 0.2 + Math.PI)),
y: Array.from({ length: n_points }, (_, idx) => idx * 0.1),
z: Array.from({ length: n_points }, (_, idx) => Math.sin(idx * 0.2 + Math.PI)),
point_style: { fill: `orangered` },
label: `Helix 2`,
}
</script>
<ScatterPlot3D
series={[helix_1, helix_2]}
x_axis={{ label: `X` }}
y_axis={{ label: `Height` }}
z_axis={{ label: `Z` }}
style="height: 450px"
/> Color-Coded Points
Points can be colored based on data values using a continuous color scale. The color bar automatically displays the value range:
<script lang="ts">
import { ScatterPlot3D } from 'matterviz'
// Generate spherical shell of points with color based on z-coordinate
const n_points = 500
const sphere_data = {
x: [],
y: [],
z: [],
color_values: [],
}
for (let idx = 0; idx < n_points; idx++) {
// Random point on unit sphere
const theta = Math.random() * Math.PI * 2
const phi = Math.acos(2 * Math.random() - 1)
const radius = 0.8 + Math.random() * 0.4 // Slight thickness
const x_val = radius * Math.sin(phi) * Math.cos(theta)
const y_val = radius * Math.sin(phi) * Math.sin(theta)
const z_val = radius * Math.cos(phi)
sphere_data.x.push(x_val)
sphere_data.y.push(y_val)
sphere_data.z.push(z_val)
sphere_data.color_values.push(z_val) // Color by z-coordinate
}
</script>
<ScatterPlot3D
series={[{ ...sphere_data, label: `Sphere` }]}
x_axis={{ label: `X` }}
y_axis={{ label: `Y` }}
z_axis={{ label: `Z` }}
color_scale={{ scheme: `interpolateViridis` }}
color_bar={{ title: `Z-coordinate` }}
style="height: 450px"
/> Grid Surface
Add a surface defined by a z = f(x, y) function. The surface is colored by the z-value by default:
<script lang="ts">
import { ScatterPlot3D } from 'matterviz'
// Define a saddle surface: z = x^2 - y^2
const saddle_surface = {
type: `grid`,
x_range: [-2, 2],
y_range: [-2, 2],
resolution: 30,
z_fn: (x, y) => x * x - y * y,
opacity: 0.8,
wireframe: true,
wireframe_color: `#444`,
}
// Scatter points on the surface
const n_points = 50
const points_on_surface = {
x: Array.from({ length: n_points }, () => Math.random() * 4 - 2),
y: Array.from({ length: n_points }, () => Math.random() * 4 - 2),
z: [],
color_values: [],
point_style: { fill: `white`, radius: 6 },
label: `Sample Points`,
}
// Calculate z values using the surface function
for (let idx = 0; idx < n_points; idx++) {
const x = points_on_surface.x[idx]
const y = points_on_surface.y[idx]
const z = x * x - y * y
points_on_surface.z.push(z)
points_on_surface.color_values.push(z)
}
</script>
<ScatterPlot3D
series={[points_on_surface]}
surfaces={[saddle_surface]}
x_axis={{ label: `X` }}
y_axis={{ label: `Y` }}
z_axis={{ label: `Z = X² - Y²` }}
color_scale={{ scheme: `interpolateCool` }}
color_bar={{ title: `Height` }}
style="height: 500px"
/> Parametric Surface
Create surfaces using parametric equations. This example shows a torus:
<script lang="ts">
import { ScatterPlot3D } from 'matterviz'
const [major_radius, minor_radius] = [0.4, 0.15]
const torus_surface = { // Parametric torus surface
type: `parametric`,
u_range: [0, Math.PI * 2],
v_range: [0, Math.PI * 2],
resolution: [40, 20],
parametric_fn: (u, v) => ({
x: (major_radius + minor_radius * Math.cos(v)) * Math.cos(u),
y: (major_radius + minor_radius * Math.cos(v)) * Math.sin(u),
z: minor_radius * Math.sin(v),
}),
color_fn: (x, y, z) => { // Color by angle around the tube
const hue = (Math.atan2(z, Math.sqrt(x * x + y * y) - major_radius) + Math.PI) /
(2 * Math.PI)
return `hsl(${hue * 360}, 70%, 50%)`
},
opacity: 0.85,
}
</script>
<ScatterPlot3D
surfaces={[torus_surface]}
x_axis={{ label: `X` }}
y_axis={{ label: `Y` }}
z_axis={{ label: `Z` }}
style="height: 500px"
legend={null}
/> Lines with Markers
Display 3D trajectories as connected lines with markers at each data point. Each series can have its own color and style:
<script lang="ts">
import { ScatterPlot3D } from 'matterviz'
// Generate Lissajous curves - parametric 3D curves
const n_points = 60
const curve_1 = {
x: Array.from({ length: n_points }, (_, idx) => {
const t = (idx / n_points) * Math.PI * 2
return Math.sin(3 * t)
}),
y: Array.from({ length: n_points }, (_, idx) => {
const t = (idx / n_points) * Math.PI * 2
return Math.sin(4 * t)
}),
z: Array.from({ length: n_points }, (_, idx) => {
const t = (idx / n_points) * Math.PI * 2
return Math.sin(5 * t)
}),
point_style: { fill: `#e74c3c`, radius: 4 },
line_style: { stroke: `#e74c3c`, stroke_width: 3 },
label: `Lissajous (3:4:5)`,
}
const curve_2 = {
x: Array.from({ length: n_points }, (_, idx) => {
const t = (idx / n_points) * Math.PI * 2
return Math.sin(2 * t + Math.PI / 4)
}),
y: Array.from({ length: n_points }, (_, idx) => {
const t = (idx / n_points) * Math.PI * 2
return Math.sin(3 * t)
}),
z: Array.from({ length: n_points }, (_, idx) => {
const t = (idx / n_points) * Math.PI * 2
return Math.cos(2 * t)
}),
point_style: { fill: `#3498db`, radius: 4 },
line_style: { stroke: `#3498db`, stroke_width: 3 },
label: `Lissajous (2:3:2)`,
}
// Spring/helix trajectory
const curve_3 = {
x: Array.from({ length: n_points }, (_, idx) => {
const t = (idx / n_points) * Math.PI * 4
return 0.7 * Math.cos(t)
}),
y: Array.from({ length: n_points }, (_, idx) => {
const t = (idx / n_points) * Math.PI * 4
return 0.7 * Math.sin(t)
}),
z: Array.from({ length: n_points }, (_, idx) => {
return (idx / n_points) * 2 - 1
}),
point_style: { fill: `#2ecc71`, radius: 3 },
line_style: { stroke: `#2ecc71`, stroke_width: 3, line_dash: `3 2` },
label: `Helix`,
}
</script>
<ScatterPlot3D
series={[curve_1, curve_2, curve_3]}
x_axis={{ label: `X` }}
y_axis={{ label: `Y` }}
z_axis={{ label: `Z` }}
style="height: 500px"
/> Size-Scaled Points
Points can be sized based on data values using the size_scale prop:
<script lang="ts">
import { ScatterPlot3D } from 'matterviz'
// Generate random 3D data with varying sizes
const n_points = 200
const random_data = {
x: Array.from({ length: n_points }, () => Math.random() * 10 - 5),
y: Array.from({ length: n_points }, () => Math.random() * 10 - 5),
z: Array.from({ length: n_points }, () => Math.random() * 10 - 5),
// Size based on distance from origin
size_values: [],
color_values: [],
}
for (let idx = 0; idx < n_points; idx++) {
const x = random_data.x[idx]
const y = random_data.y[idx]
const z = random_data.z[idx]
const distance = Math.sqrt(x * x + y * y + z * z)
random_data.size_values.push(distance)
random_data.color_values.push(distance)
}
</script>
<ScatterPlot3D
series={[{ ...random_data, label: `Random Points` }]}
x_axis={{ label: `X` }}
y_axis={{ label: `Y` }}
z_axis={{ label: `Z` }}
size_scale={{ radius_range: [0.05, 0.25] }}
color_scale={{ scheme: `interpolatePlasma` }}
color_bar={{ title: `Distance from Origin` }}
style="height: 500px"
/> Auto-Rotating View
Enable automatic rotation with the auto_rotate prop. Use the controls pane (gear icon) to adjust rotation speed:
<script lang="ts">
import { ScatterPlot3D } from 'matterviz'
// Generate a spiral galaxy-like structure
const n_arms = 3
const points_per_arm = 200
const galaxy_data = { x: [], y: [], z: [], color_values: [] }
for (let arm_idx = 0; arm_idx < n_arms; arm_idx++) {
const arm_offset = (arm_idx / n_arms) * Math.PI * 2
for (let point_idx = 0; point_idx < points_per_arm; point_idx++) {
const t = point_idx / points_per_arm
const radius = t * 4 + 0.5
const angle = t * 4 + arm_offset
const spread = (1 - t) * 0.5 // More spread at center
const x = radius * Math.cos(angle) + (Math.random() - 0.5) * spread
const y = radius * Math.sin(angle) + (Math.random() - 0.5) * spread
const z = (Math.random() - 0.5) * spread * 0.5 // Thin disk
galaxy_data.x.push(x)
galaxy_data.y.push(y)
galaxy_data.z.push(z)
galaxy_data.color_values.push(t) // Color by distance from center
}
}
let auto_rotate = $state(1)
</script>
<label style="display: block; margin-bottom: 1em">
Rotation Speed: {auto_rotate.toFixed(1)}
<input type="range" min="0" max="5" step="0.1" bind:value={auto_rotate} />
</label>
<ScatterPlot3D
series={[{ ...galaxy_data, label: `Galaxy` }]}
x_axis={{ label: `X` }}
y_axis={{ label: `Y` }}
z_axis={{ label: `Z` }}
color_scale={{ scheme: `interpolateYlOrRd` }}
{auto_rotate}
camera_position={[0, 8, 4]}
style="height: 500px"
legend={null}
/> Multiple Surfaces
Combine multiple surfaces in the same plot:
<script lang="ts">
import { ScatterPlot3D } from 'matterviz'
// Paraboloid surface
const paraboloid = {
type: `grid`,
x_range: [-1, 1],
y_range: [-1, 1],
resolution: 25,
z_fn: (x, y) => (x * x + y * y) - 0.5,
color: `#3498db`,
opacity: 0.6,
}
// Plane cutting through
const plane = {
type: `grid`,
x_range: [-1, 1],
y_range: [-1, 1],
resolution: 5,
z_fn: () => 0.25,
color: `#e74c3c`,
opacity: 0.5,
wireframe: true,
wireframe_color: `#c0392b`,
}
</script>
<ScatterPlot3D
surfaces={[paraboloid, plane]}
x_axis={{ label: `X` }}
y_axis={{ label: `Y` }}
z_axis={{ label: `Z` }}
camera_position={[4, 3, 3]}
style="height: 500px"
legend={null}
/> Performance with Many Points
The component uses instanced rendering with per-instance colors for efficient handling of large datasets:
Rendering 3,000 points with per-instance colors
<script lang="ts">
import { ScatterPlot3D } from 'matterviz'
// Generate many random points
const n_points = 3_000
const large_dataset = {
x: Array.from({ length: n_points }, () => (Math.random() - 0.5) * 10),
y: Array.from({ length: n_points }, () => (Math.random() - 0.5) * 10),
z: Array.from({ length: n_points }, () => (Math.random() - 0.5) * 10),
color_values: Array.from({ length: n_points }, () => Math.random()),
point_style: { radius: 3 },
label: `${n_points.toLocaleString()} Points`,
}
</script>
<p style="margin-bottom: 0.5em">
Rendering {n_points.toLocaleString()} points with per-instance colors
</p>
<ScatterPlot3D
series={[large_dataset]}
x_axis={{ label: `X` }}
y_axis={{ label: `Y` }}
z_axis={{ label: `Z` }}
color_scale={{ scheme: `interpolateRainbow` }}
sphere_segments={12}
style="height: 500px"
/> 3D Reference Lines
Add reference lines in 3D space to highlight axes, thresholds, or specific values. Lines can be parallel to any axis or defined as segments between points:
<script lang="ts">
import { ScatterPlot3D } from 'matterviz'
// Generate random 3D data
const n_points = 100
const scatter_data = {
x: Array.from({ length: n_points }, () => Math.random() * 4 - 2),
y: Array.from({ length: n_points }, () => Math.random() * 4 - 2),
z: Array.from({ length: n_points }, () => Math.random() * 4 - 2),
color_values: Array.from({ length: n_points }, (_, idx) => idx / n_points),
point_style: { radius: 4 },
label: `Data Points`,
}
// Reference lines parallel to axes
const ref_lines = [
// Line parallel to X-axis at y=0, z=0 (the X-axis itself)
{
type: `x-axis`,
y: 0,
z: 0,
label: `X-axis`,
style: { color: `#e74c3c`, width: 3 },
},
// Line parallel to Y-axis at x=0, z=0 (the Y-axis itself)
{
type: `y-axis`,
x: 0,
z: 0,
label: `Y-axis`,
style: { color: `#2ecc71`, width: 3 },
},
// Line parallel to Z-axis at x=0, y=0 (the Z-axis itself)
{
type: `z-axis`,
x: 0,
y: 0,
label: `Z-axis`,
style: { color: `#3498db`, width: 3 },
},
// Threshold line parallel to X-axis
{
type: `x-axis`,
y: 1.5,
z: 1.5,
label: `Threshold`,
style: { color: `#f39c12`, width: 2, dash: `4 2` },
},
// Segment between two points
{
type: `segment`,
p1: [-2, -2, -2],
p2: [2, 2, 2],
label: `Diagonal`,
style: { color: `#9b59b6`, width: 2 },
},
]
</script>
<ScatterPlot3D
series={[scatter_data]}
{ref_lines}
x_axis={{ label: `X`, range: [-2.5, 2.5] }}
y_axis={{ label: `Y`, range: [-2.5, 2.5] }}
z_axis={{ label: `Z`, range: [-2.5, 2.5] }}
color_scale={{ scheme: `interpolateViridis` }}
style="height: 500px"
/> 3D Reference Planes
Add reference planes in 3D space. Planes can be aligned to axis pairs (XY, XZ, YZ), defined by a normal vector and point, or through three points:
<script lang="ts">
import { ScatterPlot3D } from 'matterviz'
// Generate data clustered above and below a plane
const n_points = 80
const above_plane = {
x: Array.from({ length: n_points / 2 }, () => Math.random() * 3 - 1.5),
y: Array.from({ length: n_points / 2 }, () => Math.random() * 3 - 1.5),
z: Array.from({ length: n_points / 2 }, () => 0.3 + Math.random() * 1.5),
point_style: { fill: `#2ecc71`, radius: 5 },
label: `Class A (above)`,
}
const below_plane = {
x: Array.from({ length: n_points / 2 }, () => Math.random() * 3 - 1.5),
y: Array.from({ length: n_points / 2 }, () => Math.random() * 3 - 1.5),
z: Array.from({ length: n_points / 2 }, () => -0.3 - Math.random() * 1.5),
point_style: { fill: `#e74c3c`, radius: 5 },
label: `Class B (below)`,
}
// Reference planes
const ref_planes = [
// XY plane at z=0 (decision boundary)
{
type: `xy`,
z: 0,
label: `Decision Boundary`,
style: {
color: `#3498db`,
opacity: 0.3,
wireframe: true,
wireframe_color: `#2980b9`,
},
},
// YZ plane at x=0 (vertical slice)
{
type: `yz`,
x: 0,
label: `YZ Slice`,
style: {
color: `#f39c12`,
opacity: 0.2,
},
},
]
</script>
<ScatterPlot3D
series={[above_plane, below_plane]}
{ref_planes}
x_axis={{ label: `Feature 1`, range: [-2, 2] }}
y_axis={{ label: `Feature 2`, range: [-2, 2] }}
z_axis={{ label: `Feature 3`, range: [-2, 2] }}
camera_position={[5, 4, 3]}
style="height: 500px"
/> Combining Lines, Planes, and Surfaces
Create complex 3D visualizations by combining reference lines, planes, and surfaces:
<script lang="ts">
import { ScatterPlot3D } from 'matterviz'
// Generate points on a paraboloid z = x² + y²
const n_points = 60
const paraboloid_points = {
x: [],
y: [],
z: [],
color_values: [],
point_style: { radius: 4 },
label: `z = x² + y²`,
}
for (let idx = 0; idx < n_points; idx++) {
const theta = Math.random() * 2 * Math.PI
const r = Math.sqrt(Math.random()) * 1.5
const x = r * Math.cos(theta)
const y = r * Math.sin(theta)
const z = x * x + y * y
paraboloid_points.x.push(x)
paraboloid_points.y.push(y)
paraboloid_points.z.push(z)
paraboloid_points.color_values.push(z)
}
// Surface definition
const paraboloid_surface = {
type: `grid`,
x_range: [-1.5, 1.5],
y_range: [-1.5, 1.5],
resolution: 25,
z_fn: (x, y) => x * x + y * y,
opacity: 0.5,
wireframe: true,
wireframe_color: `#666`,
}
// Reference lines showing axis intercepts and key values
const ref_lines = [
// Vertical line at origin
{
type: `z-axis`,
x: 0,
y: 0,
label: `Z-axis`,
style: { color: `#e74c3c`, width: 3 },
},
// Circle at z = 1 (projected down)
{
type: `segment`,
p1: [1, 0, 1],
p2: [0, 1, 1],
style: { color: `#f39c12`, width: 2, dash: `4 2` },
},
{
type: `segment`,
p1: [0, 1, 1],
p2: [-1, 0, 1],
style: { color: `#f39c12`, width: 2, dash: `4 2` },
},
{
type: `segment`,
p1: [-1, 0, 1],
p2: [0, -1, 1],
style: { color: `#f39c12`, width: 2, dash: `4 2` },
},
{
type: `segment`,
p1: [0, -1, 1],
p2: [1, 0, 1],
style: { color: `#f39c12`, width: 2, dash: `4 2` },
},
]
// Reference plane at z = 1
const ref_planes = [
{
type: `xy`,
z: 1,
label: `z = 1`,
style: { color: `#2ecc71`, opacity: 0.15 },
},
]
</script>
<ScatterPlot3D
series={[paraboloid_points]}
surfaces={[paraboloid_surface]}
{ref_lines}
{ref_planes}
x_axis={{ label: `X` }}
y_axis={{ label: `Y` }}
z_axis={{ label: `Z = X² + Y²` }}
color_scale={{ scheme: `interpolatePlasma` }}
color_bar={{ title: `Height` }}
camera_position={[4, 4, 3]}
style="height: 550px"
/> Plane Through Three Points
Define a plane by specifying three non-collinear points:
<script lang="ts">
import { ScatterPlot3D } from 'matterviz'
// Three points defining a plane
const p1 = [1, 0, 0]
const p2 = [0, 1, 0]
const p3 = [0, 0, 1]
// Generate random points near the plane
const n_points = 50
const plane_normal = { x: 1, y: 1, z: 1 }
const plane_points = {
x: [],
y: [],
z: [],
point_style: { fill: `#3498db`, radius: 5 },
label: `Near Plane`,
}
for (let idx = 0; idx < n_points; idx++) {
const t1 = Math.random()
const t2 = Math.random() * (1 - t1)
const t3 = 1 - t1 - t2
const noise = (Math.random() - 0.5) * 0.3
plane_points.x.push(p1[0] * t1 + p2[0] * t2 + p3[0] * t3 + noise)
plane_points.y.push(p1[1] * t1 + p2[1] * t2 + p3[1] * t3 + noise)
plane_points.z.push(p1[2] * t1 + p2[2] * t2 + p3[2] * t3 + noise)
}
// Reference plane through the three points
const ref_planes = [
{
type: `points`,
p1,
p2,
p3,
label: `Fitted Plane`,
style: {
color: `#9b59b6`,
opacity: 0.4,
wireframe: true,
wireframe_color: `#7d3c98`,
double_sided: true,
},
},
]
// Reference lines from origin to corner points
const ref_lines = [
{
type: `segment`,
p1: [0, 0, 0],
p2: p1,
style: { color: `#e74c3c`, width: 2 },
label: `To P1`,
},
{
type: `segment`,
p1: [0, 0, 0],
p2: p2,
style: { color: `#2ecc71`, width: 2 },
label: `To P2`,
},
{
type: `segment`,
p1: [0, 0, 0],
p2: p3,
style: { color: `#f39c12`, width: 2 },
label: `To P3`,
},
// Triangle edges
{ type: `segment`, p1, p2, style: { color: `#3498db`, width: 3 } },
{ type: `segment`, p1: p2, p2: p3, style: { color: `#3498db`, width: 3 } },
{ type: `segment`, p1: p3, p2: p1, style: { color: `#3498db`, width: 3 } },
]
</script>
<ScatterPlot3D
series={[plane_points]}
{ref_planes}
{ref_lines}
x_axis={{ label: `X`, range: [-0.5, 1.5] }}
y_axis={{ label: `Y`, range: [-0.5, 1.5] }}
z_axis={{ label: `Z`, range: [-0.5, 1.5] }}
camera_position={[3, 3, 3]}
style="height: 500px"
/> Custom Surface Colors
Surfaces can be colored using a custom color function that receives x, y, z coordinates:
<script lang="ts">
import { ScatterPlot3D } from 'matterviz'
// Ripple surface with custom coloring
const ripple_surface = {
type: `grid`,
x_range: [-1, 1],
y_range: [-1, 1],
resolution: 40,
z_fn: (x, y) => {
const r = Math.sqrt(x * x + y * y)
return Math.sin(r * 4) * Math.exp(-r * 0.8) * 0.5
},
color_fn: (x, y, z) => {
// Color based on angle and height
const angle = (Math.atan2(y, x) + Math.PI) / (2 * Math.PI)
const height = (z + 0.5) / 1
return `hsl(${angle * 360}, ${50 + height * 50}%, ${40 + height * 30}%)`
},
opacity: 0.7,
double_sided: true,
}
</script>
<ScatterPlot3D
surfaces={[ripple_surface]}
x_axis={{ label: `X` }}
y_axis={{ label: `Y` }}
z_axis={{ label: `Z` }}
camera_position={[4, 3, 3]}
style="height: 500px"
legend={null}
/>