Scatter3D
Overview
A scatter3d trace draws markers, lines and text at x, y, z positions in a 3D scene. Use it for point clouds and clusters in three variables, trajectories and parametric curves, and labeled positions in space. The camera turns, zooms and pans with the mouse, wheel or touch, and hovering a point shows its three coordinates, with spikes to the walls.
Holochart draws each part of the trace (markers, line, text, error bars, each projection, the surface) as one GPU draw call, whatever the number of points. Positions are uploaded once: orbiting only moves the camera, so a million points stay interactive.
From a table of rows, hx.scatter3d and hx.line3d (Express's px.scatter_3d and px.line_3d) build one trace per group, with a legend, a colorscale for a numeric color, marker sizes and animation frames.
Pick a different chart when:
- two variables are enough: a 2D scatter plot (colored or sized by the third) is easier to read exactly than any projection of 3D;
- you have one
zvalue per cell of anx–ygrid: use a heatmap or a contour plot in 2D; - you want to compare many dimensions at once: a scatter plot matrix or parallel coordinates show every pair or axis.
scatter3d is part of the full @mk7s/holochart bundle. With a script tag, load the 3D add-on holochart-3d.iife.min.js after holochart.iife.min.js.
Minimal example
import { createChart } from '@mk7s/holochart';
createChart(document.getElementById('chart')!, {
data: [
{
type: 'scatter3d',
mode: 'markers',
x: [1, 2, 3, 4, 5],
y: [2, 1, 4, 3, 5],
z: [3, 5, 1, 4, 2],
},
],
});The trace creates the default scene (layout.scene) and places each point at its x, y, z. Without mode, points are drawn as 'lines+markers'. The live example draws 600 points in three clusters, colored by z through a colorscale with a colorbar; drag to turn it and hover a point:
scatter3d/basicData format
x,y,z: one value per point, as arrays or typed arrays (Float64Array, …). Only the firstmin(x.length, y.length, z.length)points are drawn. A point with a missing or invalid coordinate (null,NaN) is a gap: it draws no marker and breaks the line, unlessconnectgaps: truejoins its neighbors.- Axis types. Numbers, dates and categories all work, per axis. The scene axis types are detected from the data, as on cartesian axes, or set with
scene.xaxis.type(andyaxis,zaxis):linear,log,dateorcategory. Positions are kept in float64 relative to the data's center, so dates keep their precision. - Per-point attributes.
text,hovertext,customdata,marker.size,marker.color,marker.symbolandline.colortake one value per point (or a single value for all). - Scene.
scene: 'scene2'puts the trace in another scene (default'scene'); see several scenes.
import { createChart } from '@mk7s/holochart';
// Dates on x, categories on y, a log z axis.
createChart(document.getElementById('chart')!, {
data: [
{
type: 'scatter3d',
x: ['2026-01-05', '2026-01-12', '2026-01-19', '2026-01-26'],
y: ['north', 'north', 'south', 'south'],
z: [12, 140, 35, 900],
},
],
layout: { scene: { zaxis: { type: 'log' } } },
});Variations
Lines colored along their length
mode: 'lines' joins the points with a screen-space line: line.width is in CSS px at every depth. Numbers in line.color go through line.colorscale and are interpolated along each segment. Here a helix is colored by its parameter, with diamond markers on every tenth point:
scatter3d/helixTubes
line.render: 'tube' (a Holochart extension) draws the line as a lit tube instead of a px-wide band. line.radius is a fraction of the longest side of the scene's axis box (default 0.01), so tubes stay round whatever the axis scales and grow and shrink with the view like the data. Here a helix tube is colored along its length through a colorscale, and a thinner tube with a metallic line.material winds around it:
scatter3d/tubeRibbons
line.render: 'ribbon' sweeps the line along one axis into a lit strip: line.ribbon.axis ('x', 'y' or 'z', default 'y') and line.ribbon.width in that axis's units (default a twentieth of the axis range). Here twelve spectra, one trace per moment on the time (y) axis, form a spectrogram-style waterfall, each ribbon 0.6 s wide and colored by amplitude:
scatter3d/ribbonsMarker symbols
marker.symbol takes Plotly's 3D set: circle, circle-open, cross, diamond, diamond-open, square, square-open and x, one for the whole trace or one per point. Each trace shows its glyph in the legend; click an entry to hide the trace:
scatter3d/symbolsLit sphere markers
marker.render: 'sphere' (a Holochart extension) draws each marker as a shaded sphere, ray-cast on the GPU, with exact silhouettes and intersections. Spheres keep the px size of sprite markers, so bubble sizing (sizeref, sizemin, sizemode) works the same:
scatter3d/spheresText labels
mode: 'markers+text' writes text (or texttemplate) beside each point, at textposition (default 'top center', one per point allowed). Labels face the camera, keep their px size and are hidden behind nearer data (depth-tested):
scatter3d/textError bars
error_x, error_y and error_z take the 2D error bar attributes: type (percent, constant, sqrt, data), value / valueminus, array / arrayminus, symmetric, color and thickness. Each bar is a segment through its point along its axis, without caps, as in Plotly. copy_zstyle on error_x / error_y copies the style of error_z. The scene's autorange includes the bars' ends:
scatter3d/error-barsProjections on the walls
projection.x.show (and y, z) draws the markers' shadows on the wall perpendicular to that axis, at projection.x.scale of the marker size (default 2/3) and opacity (default 1). The shadows go on the far walls, the ones behind the data, and move when the camera turns:
scatter3d/projectionsA surface through the points
surfaceaxis: 0, 1 or 2 fills a surface through the points, triangulated (Delaunay) in the plane perpendicular to the x, y or z axis; -1 (the default) draws none. surfacecolor defaults to the line or marker color. Here two closed loops are filled with surfaceaxis: 2:
scatter3d/surfaceaxisSeveral traces
Traces share the scene and its axes, each with its own style and legend entry. Here three clusters with different symbols and a dashed trajectory (line.dash: 'dash'):
scatter3d/multiple-tracesDate, category and log axes
One trace per region, with weekly dates on x, the region names on a category y axis, and a log z axis:
scatter3d/axis-typesStyling
- Mode.
modecombineslines,markersandtext(default'lines+markers'). - Markers.
marker.size(px; per point for bubbles, withsizeref,sizeminandsizemode),marker.symbol,marker.opacity(one value for the trace) andmarker.line.color/widthfor the rim. - Marker colors.
marker.coloras CSS colors, or numbers mapped throughcolorscale,cmin,cmax,cmidandreversescale, withshowscaleandcolorbarfor a colorbar, orcoloraxisto share a scale between traces. See colors and colorscales. - Lines.
line.color(CSS, or numbers throughline.colorscaleand its range attributes, withline.showscale/line.colorbar),line.width(CSS px) andline.dash(solid,dot,dash,longdash,dashdot,longdashdot, in CSS px). - Tubes and ribbons.
line.render('screen', the default,'tube'or'ribbon'; a Holochart extension) withline.radiusfor tubes andline.ribbon.axis/line.ribbon.widthfor ribbons. They takeline.colorlike screen lines (one color, or numbers throughline.colorscale, interpolated along the line), and are lit withline.lighting(defaults ambient 0.5, diffuse 0.7, specular 0.15, roughness 0.4, fresnel 0.2),line.lightpositionandline.material.line.widthandline.dashdon't apply to them. - Text.
text,texttemplate(%{x},%{y},%{z},%{text},%{customdata}),textpositionandtextfont. - Default look. In the default
holocharttemplate, markers are 5 px with rims in the background color, and colorbars are slim.template: 'plotly-classic'(or'none') keeps Plotly's defaults: 8 px markers and 2 px lines (themes and templates).
import { createChart } from '@mk7s/holochart';
createChart(document.getElementById('chart')!, {
data: [
{
type: 'scatter3d',
mode: 'lines+markers+text',
x: [0, 1, 2, 3],
y: [0, 1, 0, 1],
z: [0, 1, 2, 3],
text: ['start', 'b', 'c', 'end'],
texttemplate: '%{text} (%{z})',
textposition: 'middle right',
marker: { size: 6, symbol: 'square-open', color: [0, 1, 2, 3], colorscale: 'Viridis' },
line: { width: 3, dash: 'dot', color: '#8a8fa3' },
},
],
});Interactivity
Camera. Drag to turn the scene, scroll or pinch to zoom, double-click to go back to the first view; the scene controls list every gesture.
Hover. Markers and lines are picked on the GPU, so the point you hover is the one you see (not one hidden behind it). The label sits at the point and reads
x: …,y: …,z: …(plustext), each value formatted like its scene axis.xhoverformat,yhoverformatandzhoverformat(orscene.xaxis.hoverformat, …) set the formats,hoverinfopicks amongx+y+z+text+name, andhovertemplatetakes%{x},%{y},%{z},%{text}and%{customdata}:tsimport { createChart } from '@mk7s/holochart'; createChart(document.getElementById('chart')!, { data: [ { type: 'scatter3d', mode: 'markers', x: [1.2, 2.5, 3.1], y: [0.4, 1.8, 2.2], z: [10, 25, 18], customdata: ['A-17', 'B-02', 'C-44'], hovertemplate: 'sample %{customdata}<br>depth %{z:.1f} m<extra></extra>', }, ], });Tubes and ribbons are picked too: hovering one hovers the data point its part of the mesh was built around.
Hover is always
closestin a scene, as in Plotly.scene.hovermode: false(orlayout.hovermode: false) turns it off. While the camera moves the label is hidden; it comes back at the point's new place once the camera rests. See hover and picking.Spikes. Hovering a point draws lines from it to the axis walls (
scene.xaxis.showspikes, default on), and withspikesidesalong the other walls to the box edges;spikecolorandspikethicknessstyle them. See spikes.Events.
hoverandclickpoints carryx,y,z,curveNumber,pointNumberandcustomdata. A click (a press and release without dragging) emitsclickwith the hovered point:tschart.on('click', (e) => { const p = e.points[0]; if (p) console.log(p.curveNumber, p.pointNumber, p.x, p.y, p.z); });Legend. Each trace shows its marker or line glyph; clicking an entry toggles the trace.
Annotations.
scene.annotationsanchor labels and arrows at 3D points; they follow the camera. See annotations.
3D-native options
- Sphere markers.
marker.render: 'sphere'draws lit, ray-cast spheres instead of the default flat'sprite'symbols (which face the camera, as in Plotly). Spheres keep the marker's px size and color, and read better where depth and overlaps matter; symbols don't apply to them. - Tubes and ribbons.
line.render: 'tube'or'ribbon'draws lines as lit meshes that scale with the view, instead of Plotly's px-wide screen lines. Tube frames are carried along the curve by rotation-minimizing parallel transport (the double reflection method), so tubes don't twist; each end has a flat cap. Ribbons are lit on both sides.line.materialtakes the material types of meshes. - Camera tours and auto-rotation.
chart.animateCameraflies the camera to a new view, andscene.autorotateturns the scene like a turntable.
Performance notes
- One draw call per part (markers, line, text, error bars, each projection, the surface), whatever the number of points. Orbiting, zooming and panning only move the camera: nothing is re-uploaded.
- Opaque markers and lines write depth and draw in any order. Translucent markers (
marker.opacity < 1) are blended and depth-sorted as the camera moves, for traces of up to 200,000 points; larger translucent traces are not sorted. Keepopacity: 1for the biggest clouds. - A million markers orbit interactively. The
scatter3d/perf-1msandbox example draws them, andpnpm bench:gpu --only scatter3d/perf-1mmeasures the frame rate. - Tubes and ribbons are meshes built on the CPU: a tube has 12 vertices per point plus 13 per cap (two caps per unbroken run), a ribbon 2 vertices per point. They are built in scene units, so they are rebuilt when the scene's layout changes (axis ranges, aspect ratio), not when the camera moves. Screen lines cost less for very long lines.
- Pass
x,y,zas typed arrays to keep large inputs compact. Text labels cost more than markers per point; keep them for a few hundred points. See the performance guide.
Accessibility notes
- Screen readers: the chart is a
<canvas>; the hidden description (see the accessibility guide) names each 3D scatter trace, its number of points and the range of each ofx,yandz, formatted like the axes. - Keyboard: Tab moves into the plot area; the arrow keys then step through the points in data order, each showing its hover label. Shift + arrow keys orbit the camera,
+/-move it in and out and0resets it. See the keys by chart family. - Color and depth: a 3D view hides depth on a flat screen. Give each trace a distinct symbol as well as a color, color by a value with a lightness-monotonic scale (the default, Viridis) and keep the colorbar, and consider sphere markers or projections so position reads without turning the scene. Set a useful initial
scene.camera; not everyone can drag to explore.
Attribute reference
See the scatter3d attribute reference for every attribute, its type, and its default. Scene attributes (camera, axes, spikes, annotations) are under scene in the layout reference.
Related charts
- 3D scenes: the camera, axes, controls, hover, spikes and annotations every 3D trace shares
- Express 3D charts:
scatter3d/line3dfigures from tabular data, grouped by color, symbol and line - Scatter: the same markers, lines and text in 2D
- Bubble chart: encode a third variable as marker size in 2D
- Scatter plot matrix: every pair of many variables at once
Plotly migration notes
- Attribute names and defaults match Plotly's
scatter3d:x,y,z,mode,marker(symbols, sizes, colorscales,line),line(colors, colorscales,width,dash),connectgaps,text,texttemplate,textposition,textfont,error_x/error_y/error_z,projection,surfaceaxis,surfacecolor, the hover formats,hoverinfo,hovertemplate,hovertext,customdataandscene. Hover labels and their formats match Plotly's gl3d. marker.render('sprite'or'sphere') is a Holochart extension;'mesh'is planned.line.render('screen','tube'or'ribbon') and itsline.radius,line.ribbon,line.lighting,line.lightpositionandline.materialare Holochart extensions. The default,'screen', is Plotly's line; Plotly ignores the others and draws screen lines.- Error bars have no caps (as in Plotly);
widthis accepted but ignored. - Projections are camera-facing sprites drawn on the walls (Plotly flattens them onto the wall).
- The
surfaceaxissurface is flat-shaded (unlit). - Line dashes follow the 2D dash patterns, in CSS px.
- The
crossandxsymbols are the 2D (filled) glyphs. - Not supported yet: programmatic hover of 3D points (
Fx.hover/chart.hover), hover on text-only traces (no markers or lines), and drawing thehovertextlabel of annotations (as in 2D).