Skip to content

PointCloud3D · Surface3D ​

renderPointCloud3D draws independent XYZ samples as screen-facing points, and renderSurface3D draws a Y-up height grid as a lit, colormapped surface. Use them for measured 3D data (scans, trajectories, simulation output) when you need real X/Y/Z axes, picking and streaming rather than a force-directed graph.

Live example

Basic usage ​

ts
import { Charturbo } from 'charturbo';

const canvas = document.querySelector('canvas')!;
// nodeCount / edgeCount 0: skip the default seeded graph
const sdk = await Charturbo.create(canvas, { nodeCount: 0, edgeCount: 0 });

sdk.renderPointCloud3D([
  { name: 'Scan', data: new Float32Array([0, 1, 2, 3, 4, 5]), size: 5 },
  { name: 'Reference', data: [{ x: 1, y: 2, z: 3 }], color: '#ff9933', size: 10 },
], { title: 'XYZ cloud', legend: true });

// Orbit / pan / zoom, hover tooltip, keyboard exploration
sdk.enableNodeInteractions(canvas, { autoRender: true });
sdk.render();

A surface uses the same pattern:

ts
sdk.renderSurface3D([{
  name: 'Height',
  data: { columns: 3, rows: 2, xStart: -1, xStep: 1, zStart: 0, zStep: 2,
    y: [0, 1, 0, 1, 2, 1] },
  colormap: 'viridis', lighting: 0.65,
  contours: { show: true, levels: 8, color: '#f8fafc' },
}], { title: 'Y-up surface', showGrid: true });
sdk.render();

Both methods return { pointCount, seriesCount }. Coordinates are Y-up. The original XYZ values are kept separate from render coordinates: tooltips, keyboard focus and CSV export always report the original values. Internal render coordinates use Float32 precision.

Data format ​

Point cloud (PointCloud3DSeries.data) ​

Any of these shapes:

ShapeExample
Interleaved Float32Array or Float64Arraynew Float32Array([x0, y0, z0, x1, y1, z1])
Array of tuples[[x, y, z], ...]
Array of objects[{ x, y, z }, ...]
Columns of equal length{ x: ArrayLike<number>, y: ArrayLike<number>, z: ArrayLike<number> }

A null entry or a non-finite coordinate is a gap: it is not drawn, but the remaining samples keep their original IDs.

Surface (Surface3DSeries.data) ​

ts
interface Surface3DGridData {
  columns: number; // >= 2
  rows: number;    // >= 2
  xStart: number; xStep: number; // finite start, non-zero step (negative allowed)
  zStart: number; zStep: number;
  y: ArrayLike<number>;           // row-major: y[row * columns + column]
}

A cell with at least one non-finite vertex becomes a hole.

A chart holds at most 4,000,000 samples in total.

Options ​

Series options ​

Common to both series types:

OptionTypeDefaultDescription
namestringLegend and tooltip name
colorColorInputpaletteSeries color
visiblebooleantrueInitial visibility
opacitynumberSeries opacity

PointCloud3DSeries:

OptionTypeDefaultDescription
dataPointCloud3DDataXYZ samples (see above)
sizenumber3Point diameter in CSS pixels
colorBy{ values, colormap?, min?, max? }One scalar per point, mapped through 'viridis' (default) or 'magma'. min / max fix the color range

Surface3DSeries:

OptionTypeDefaultDescription
dataSurface3DGridDataHeight grid
colormap'viridis' | 'magma''viridis'Height colormap. With only an explicit color (no colormap) the surface is a solid color
yMin / yMaxnumberdata rangeColor range of the colormap
wireframebooleanfalseDraw grid lines instead of filled cells
lightingnumber0.65Flat diffuse lighting, 0..1
contoursSurface3DContourOptionsHeight isolines, see Contours

Chart options (Scientific3DChartOptions) ​

OptionTypeDefaultDescription
titlestringChart title
descriptionstringAccessible description
legendboolean | ChartLegendOptionsSeries legend
gridChartGridStyleGrid line style
showGridbooleanShow the grid
hoverChartHoverStyleHover marker style
axes3d{ x?, y?, z? }Per-axis range, name, ticks, visibility
aspect'cube' | 'data' | [x, y, z]'cube'Box proportions
cameraScientific3DCameraOptionsProjection and initial pose

The camera is a Y-up orbit in normalized render space. It keeps rotation, pan, distance and projection size across resizes.

Axis ranges, aspect and orthographic projection ​

ts
sdk.renderPointCloud3D([{ data: measuredXYZ, size: 4 }], {
  axes3d: {
    x: { name: 'X', min: 0, max: 120, tickCount: 4 },
    y: { min: 0, max: 40, tickCount: 3 },
    z: { min: 0, max: 20, tickCount: 3 },
  },
  aspect: 'data', // 'cube' (default) or relative XYZ box lengths such as [2, 1, 0.5]
  camera: { type: 'orthographic' }, // perspective by default; orthoSize omitted = auto framing
});

axes3d.x / y / z (Scientific3DAxisOptions):

OptionTypeDefaultDescription
namestringAxis label
min / maxnumberdata rangeFixed frame range in data units
tickCountnumber5Integer 2..50, evenly spaced inside the range
visiblebooleantruefalse hides this axis's ticks and its grid direction
  • An omitted endpoint follows the range of all finite samples, including hidden series. When both endpoints are set, min < max is required. If only one end is fixed and the automatic end would cross it, the automatic end is widened by 5 % of the fixed value (at least 1 unit).
  • A fixed range is an axis frame: geometry outside it is not clipped.
  • Axis labels are DOM elements. The whole grid is controlled by showGrid.
  • aspect: 'cube' normalizes each axis independently to [-60, 60]. aspect: 'data' uses the ratio of the axis range lengths, so equal data units have equal length. A numeric tuple gives positive relative box lengths. The longest side is 120 render units. Tooltips, CSV and accessibility output keep the original XYZ.
  • While streaming, fixed endpoints stay and only automatic endpoints are recomputed.

camera (Scientific3DCameraOptions):

OptionTypeDefaultDescription
type'perspective' | 'orthographic''perspective'Projection
eye[x, y, z]fits the data boxEye position in render space
target[x, y, z][0, 0, 0]Orbit target in render space; requires eye
orthoSizenumberautoInitial vertical half-extent (orthographic only, positive). Changes with wheel zoom

Orthographic picking casts a parallel ray per screen position. getCameraState() reports projection and orthoSize. Switching to another chart restores perspective.

Contours ​

contours: { show?, levels?, color?, width?, opacity? } on a surface series draws height isolines.

OptionTypeDefaultDescription
showbooleanfalseDraw contours
levelsnumber | number[]12Integer 1..64: evenly spaced levels strictly inside yMin..yMax (or the finite height range), end points excluded; a single-value range gives that one value. An array of finite numbers: explicit raw Y heights
colorColorInput'#e2e8f0'Line color
widthnumber1.5Visual weight, not a CSS pixel width: lines are 1 px hairlines with alpha min(1, opacity × colorAlpha × (0.75 + 0.15 × min(width, 4)))
opacitynumber0.850..1

Invalid values are rejected before rendering. Contours are traced with marching triangles on the rendered triangles and use the same coordinate mapping as the surface, so they stay on it with fixed axes3d, any aspect and orthographic projection. They skip hole cells, follow series visibility and emphasis, and are depth tested, but they are not pickable. updateSurface3D recomputes count-based levels from the updated heights and yMin / yMax.

Streaming updates ​

ts
// Append to cloud series 0; returns the retained length of that series
sdk.appendPointCloud3D(0, new Float32Array([1, 2, 3, 4, 5, 6]), { maxPoints: 2400 });
// A colorBy series needs one colorValues entry per appended point:
// sdk.appendPointCloud3D(0, xyz, { maxPoints: 2400, colorValues: scalars });
sdk.render();

// On a rendered surface (a separate chart from the cloud above)
sdk.updateSurface3D(0, { mode: 'replaceY', y: heights, yMin: -1, yMax: 1 });
sdk.updateSurface3D(0, { mode: 'appendColumns', columns: 1, y: nextColumn });
sdk.updateSurface3D(0, { mode: 'appendRows', rows: 1, y: nextRow, scrollZ: false });
sdk.render();

appendPointCloud3D(seriesIndex, data, options?) ​

Accepts the same XYZ shapes as renderPointCloud3D and returns the retained sample count of the series.

OptionTypeDefaultDescription
maxPointsnumber0 (unbounded)Keep only the newest N samples
colorValuesArrayLike<number>Required when the series uses colorBy (one per appended point); not allowed otherwise

The colormap and fixed min / max are kept; an automatic color range is recomputed from the retained scalars.

updateSurface3D(seriesIndex, update) ​

Returns { columns, rows, pointCount } for the series.

modeFieldsDescription
'replaceY'y, yMin?, yMax?Replaces all heights (row-major, rows * columns). yMin / yMax persist for later updates; omitted keeps the current setting
'appendColumns'columns, y, scrollX?y is a column-major strip: y[newColumn * rows + row]
'appendRows'rows, y, scrollZ?y is a row-major block: y[newRow * columns + column]

With scrollX / scrollZ true (default) the grid keeps its size: the oldest columns / rows are dropped and the origin moves by appended count × step. A chunk larger than the window keeps only the last window. With false the origin stays and the grid grows. Negative steps work.

Update rules ​

  • Updates are atomic. A wrong length, range, kind, series index or an unsupported backend is rejected without changing the current data.
  • Inputs are copied: changing your array afterwards does not affect the chart. Each chunk holds at most 4,000,000 samples, and so does the whole chart. An empty append is allowed (it still applies a smaller cloud maxPoints).
  • Camera, styles, visibility, emphasis and fixed axis endpoints are kept. Automatic ranges are recomputed from all retained data. IDs are renumbered from the current series order and sample position. A pinned tooltip or keyboard focus is released; CSV, accessibility and tooltips use the updated XYZ.
  • The current implementation copies SDK metadata, sends the changed series to WebAssembly again and lets Rust rebuild ranges and geometry. It does not guarantee partial GPU uploads or O(appended) cost.

Interaction and export ​

  • Call enableNodeInteractions(canvas, { autoRender: true }) for orbit (left drag), pan (Shift / right / middle drag) and zoom (wheel); see 3D Network Graph for the camera controls.
  • Point cloud picking returns the front-most sample within the tolerance radius. Surface picking intersects the triangles and returns point3d: { x, y, z, column, row }. Wireframe surfaces also pick inside cells.
  • Keyboard navigation and CSV report the raw (not interpolated) vertex XYZ.
  • SVG export falls back to a raster image; PNG and PDF capture the current 3D view.

Framework usage ​

React and Vue accept type="pointCloud3d" and type="surface3d", with the same options. Updating means passing the full data to the render API again. For streaming, call appendPointCloud3D / updateSurface3D and render() on the SDK from onReady(sdk) or the component ref's whenReady(), and keep the data / options prop references stable: a new reference re-runs the full render API.

Notes ​

  • Hover currently tests every point / cell.
  • Semi-transparent geometry writes depth but is not depth sorted; exact multi-layer transparency is not supported.
  • Not available yet: 3D brush and sync, contour labels, FOV / near / far / up camera settings, nice ticks, GPU glyph labels, an independent box toggle.

See also ​

MIT License