phoria2: Interactive 3D Charts and Scenes in JavaScript

Category: Chart & Graph , Javascript , Recommended | November 2, 2026
Authorkevinroast
Last UpdateNovember 2, 2026
LicenseMIT
Tags
Views0 views
phoria2: Interactive 3D Charts and Scenes in JavaScript

phoria2 is a vanilla JavaScript WebGL 2 library for generating interactive 3D charts, diagrams, floor plans, and product viewers.

It creates charts from data specifications and renders custom 3D scenes through a compact scene graph.

Each view uses a regular canvas. Dashboard views share WebGL contexts after the first four and redraw as their contents change.

Features

  • Physically based materials, shadows, and HDR environment lighting.
  • 6 chart marks for 3D data visualization.
  • HTML labels with overlap and occlusion controls.
  • Mouse picking, hover highlights, tooltips, and object dragging.
  • Instanced geometry for repeated objects and data points.
  • glTF 2.0 models and animations.
  • Particle emitters, motion timelines, and scene editing tools.
  • Keyboard navigation, screen reader descriptions, and reduced-motion controls.
  • JSON scene descriptions with schema validation.
  • Animated data updates and configurable chart legends.

How to Use It

Installation

Install phoria2 in your ES module project:

npm install phoria2

Import the core and whichever modules your application uses.

import { View, Mesh, box, DirectionalLight } from 'phoria2';
import { chart } from 'phoria2/charts';
import { loadGLTF } from 'phoria2/gltf';

For browser use, use an import map.

<script type="importmap">
{
  "imports": {
    "phoria2": "https://cdn.jsdelivr.net/npm/[email protected]/dist/phoria2.js",
    "phoria2/charts": "https://cdn.jsdelivr.net/npm/[email protected]/dist/charts.js",
    "phoria2/particles": "https://cdn.jsdelivr.net/npm/[email protected]/dist/particles.js",
    "phoria2/gltf": "https://cdn.jsdelivr.net/npm/[email protected]/dist/gltf.js",
    "phoria2/motion": "https://cdn.jsdelivr.net/npm/[email protected]/dist/motion.js",
    "phoria2/edit": "https://cdn.jsdelivr.net/npm/[email protected]/dist/edit.js"
  }
}
</script>

Basic Usage

The following example uses the import map above. Paste the canvas and module script into your HTML page.

<canvas id="phoria-demo" style="display:block;width:100%;height:360px"></canvas>
<script type="module">
  import { View, Mesh, box, DirectionalLight } from 'phoria2';
  const view = new View({
    canvas: '#phoria-demo',
    background: '#191d25',
    orbit: true
  });
  const cube = view.scene.add(new Mesh({
    geometry: box(2),
    material: { color: '#e69b43', roughness: 0.35 }
  }));
  view.scene.add(new DirectionalLight({
    direction: [-1, -2, -1]
  }));
  view.start((dt) => cube.rotateY(dt * 0.5));
</script>

Create a 3D Bar Chart

The charts module accepts an array of objects. The x and z channels group categories, and y sets each value.

<canvas id="chart-canvas" style="display:block;width:100%;height:360px"></canvas>
import { View } from 'phoria2';
import { chart } from 'phoria2/charts';
const rows = [
  { quarter: 'Q1', team: 'North', revenue: 32 },
  { quarter: 'Q1', team: 'South', revenue: 24 },
  { quarter: 'Q2', team: 'North', revenue: 41 },
  { quarter: 'Q2', team: 'South', revenue: 29 }
];
const view = new View({ canvas: '#chart-canvas', orbit: true });
const sales = view.scene.add(chart({
  mark: 'bar',
  data: rows,
  x: 'quarter',
  z: 'team',
  y: 'revenue',
  color: 'team',
  tooltip: true,
  title: 'Quarterly revenue'
}));
view.frame(sales);

Handle Clicks and Dragging

A picking result contains the object and its associated data row. Click handlers receive null for empty canvas positions.

<canvas id="scene-canvas" style="display:block;width:100%;height:320px"></canvas>
import { View, Mesh, box } from 'phoria2';
const view = new View({
  canvas: '#scene-canvas',
  orbit: true,
  highlight: '#ffcc00',
  tooltip: (hit) => hit.datum?.name ? { title: hit.datum.name } : null
});
const chair = view.scene.add(new Mesh({
  geometry: box(1),
  material: { color: '#46a6a0' },
  data: { name: 'Movable object' }
}));
view.on('click', (hit) => {
  if (hit) console.log(hit.node, hit.instance, hit.datum);
});
let initialPosition;
chair.onDrag = (drag) => {
  if (drag.phase === 'start') initialPosition = chair.position.clone();
  else if (drag.point) chair.position.copy(initialPosition).add(drag.delta);
};

Load a glTF Model

loadGLTF() loads .glb and .gltf models. Host the file on your web server and use a View already initialized as in Basic Usage. Your models can include meshes, cameras, and animations.

import { loadGLTF } from 'phoria2/gltf';
const model = await loadGLTF('./models/chair.glb');
view.scene.add(model);
view.frame(model);
if (model.animations.length) model.play();

API Reference

View Initialization and Options

OptionDescription
canvasCanvas element or selector. Defaults to a newly created full-window canvas.
sceneExisting Scene instance. Defaults to a new scene. To load a JSON description, first call create().
camera'perspective' (default), 'isometric', 'top', 'front', 'side', an existing camera, or camera options { position, target, fov }.
orbitfalse by default. Enable with true or an object containing autoRotate, inertia, minDistance, maxDistance, minPitch, maxPitch, pan, zoom, and mode.
backgroundBackground color, panorama texture, texture options, or null. Transparent by default.
highlightColor applied to the object beneath the pointer. Disabled by default.
tooltip(hit) => content callback for a tooltip. Disabled by default.
framing'height' (default), 'width', 'both', or { keep, aspect } for resize framing.
renderMode'auto' (default), 'always', or 'manual'.
toneMapping'neutral' (default), 'aces', or 'none'.
exposureTone mapping exposure. Default 1.
bloomfalse (default), true, or { strength, radius, threshold }.
autoLightAutomatically light scenes with no lights. Default true.
pixelRatioCanvas pixel ratio. Default Math.min(devicePixelRatio, 2).
context'auto' (default), 'own', or 'shared'. Automatic mode assigns independent contexts to the first four views, then uses a shared context.
antialiasAntialiasing, default true.
maxDeltaMaximum frame delta in seconds, default 0.1, to limit jumps after backgrounding.
keyboardfalse (default), true, or { label: (hit) => text } for keyboard exploration.
descriptionAccessible canvas description. Derived from the scene when keyboard navigation is active.
reducedMotion'auto' (default), true, or false.
outlineAntialiasAntialias outline edges, defaulting to the antialias setting. false uses hard pixel edges.

The orbit object’s mode is 'orbit' (default) or 'map' for pan-only controls. autoRotate accepts true or a speed in radians per second. Distance limits apply to the camera target or orthographic view height.

View members include canvas, scene, camera, controls, renderer, width, height, time, renderMode, maxDelta, autoLight, description, reducedMotion, and bloom. When orbit is enabled, controls.spherical exposes yaw, pitch, and distance, and controls.enabled, controls.mode, and controls.autoRotate are writable.

View Methods and Events

view.start((dt, time) => {});            // Start a frame callback; dt and time are seconds.
view.frame(node, { padding: 0.2, direction: 'front', duration: 0.6 });
                                        // Frame a node or the whole scene.
const unsubscribe = view.on('click', (hit, event) => {});
const unsubscribeHover = view.on('hover', (hit, event) => {});
                                        // Listen for picking changes or clicks.
view.tooltip((hit) => hit?.datum?.name ?? null); // Set the tooltip callback.
view.pick(x, y);                        // Find the nearest hit at canvas coordinates.
view.pick(event);                       // Pick using a pointer event.
view.pickAll(event);                    // Return all hits, nearest first.
view.rayAt(x, y);                       // Return a world-space Ray through a canvas point.
view.rayAt(event);                      // Return a Ray for a pointer event.
view.framing(node, { padding: 0.2 });   // Calculate a framing position without moving the camera.
await view.toImage();                   // Return a PNG Blob, including HTML labels.
await view.explain();                   // Return scene diagnostics as text.
view.render();                         // Draw a frame.
view.invalidate();                     // Request a redraw after an untracked data mutation.
view.controls.cancel();                // Cancel the current orbit gesture.
view.dispose();                        // Release the view and its resources.

Scene Graph and Node Options

OptionDescription
nameIdentifier for scene lookup.
position[x, y, z] or an equivalent vector object.
rotationEuler XYZ rotation in radians.
quaternion[x, y, z, w] or a matching object.
scaleUniform number or [x, y, z].
visibleControls node visibility.
pickableControls pointer picking. Default true.
matrixAutoUpdateRecompose the local matrix from position, rotation, and scale every frame. Default true.
userDataApplication metadata.
childrenChild nodes.
showFrom(camera) => boolean callback that determines which cameras display this subtree.
onUpdateFrame update callback.
clip{ normal, offset, cap } section plane for the subtree.
tooltip(hit) => content callback for this node and its subtree.
highlightNode-level pointer highlight or false.
highlightWholeHighlight the entire subtree as one item, default false.
onHover, onClickPointer interaction callbacks.
onDragDrag callback receiving a drag object.
outlineColor or { color, width } for a silhouette outline.

Scene also accepts background, environment ('studio', 'none', or an HDR URL), environmentIntensity, backgroundBlur, and fog. Fog is { color, density } or { color, near, far }. A clip applies one cutting plane to meshes in its subtree; cap fills closed cut faces.

Use the #scene-canvas element from the interaction example. Vector properties accept .set(), .copy(), and per-axis assignments; bounds expose .min, .max, .getCenter(), and .getSize().

import { View, Group, Mesh, box } from 'phoria2';
const view = new View({ canvas: '#scene-canvas', orbit: true });
const group = view.scene.add(new Group({ name: 'display' }));
const cube = group.add(new Mesh({ geometry: box(1) }));
const copy = cube.clone();             // Clone a node.
group.add(copy);                       // Insert the clone.
group.remove(copy);                    // Remove the clone.
cube.translate(1, 0, 0);              // Change local position.
cube.rotate([0, 1, 0], Math.PI / 4);   // Rotate around an arbitrary axis.
cube.rotateX(Math.PI / 4);            // Rotate around X (also rotateY, rotateZ).
cube.lookAt([0, 0, 0]);               // Face a point.
cube.onUpdate((node, dt, t) => {});    // Per-frame update callback.
const off = cube.on('click', (hit) => console.log(hit));
view.scene.find('display');           // Find by name.
view.scene.findAll((node) => node.visible);
view.scene.traverse((node) => {});
cube.updateWorld();                   // Update world transforms before inspection.
cube.updateWorldTree();               // Update descendants when the parent is current.
cube.bounds;                          // Current world-space Box3.
cube.boundsFrom(view.camera);         // Geometry drawn by this camera.
cube.worldPosition;                   // Read-only world position.
cube.worldQuaternion;                 // Read-only world rotation.
cube.worldScale;                      // Read-only world scale.
cube.matrix;                          // Local transform matrix.
cube.worldMatrix;                     // World transform matrix.
cube.parent;                          // Parent node, or null.
cube.root;                            // Root node.
cube.children;                        // Child nodes.
cube.toJSON();                        // Serialize the node hierarchy.
cube.removeFromParent();              // Detach the node.
cube.dispose();                       // Release node resources.
off();                                // Unsubscribe from the click.

Drawables, Instancing, and Labels

  • Mesh renders solid geometry.
  • Points renders points, with material size and sizeAttenuation.
  • Lines renders pairs of vertices as segments, with material width in pixels.
  • InstancedMesh draws repeated geometry with per-instance placement and colors. Its constructor accepts count, instances, instanceMatrices, instanceColors, and labels; count defaults to instances.length or data.length when available.

instanceMatrices holds 16 column-major numbers per instance, and instanceColors holds three linear RGB values each. Call needsUpdate() after direct array edits.

The example uses the earlier #scene-canvas element.

import { View, InstancedMesh, box } from 'phoria2';
const view = new View({ canvas: '#scene-canvas', orbit: true });
const instances = view.scene.add(new InstancedMesh({
  geometry: box(1),
  material: { color: 'white' },
  data: [{ name: 'A' }, { name: 'B' }],
  labels: { text: (row) => row.name, place: 'above' }
}));
instances.setInstance(0, { position: [-1, 0, 0], scale: [1, 2, 1], color: '#ed6a5a' });
instances.setInstance(1, { position: [1, 0, 0], quaternion: [0, 0, 0, 1], color: '#51b8a4' });
instances.setColor(0, '#f0bb77');
instances.instanceMatrices[0] = 0.8;
instances.needsUpdate(); // Required after changing a matrix array directly.

For per-instance or per-point labels, labels accepts one configuration or an array. text(datum, index) is required, priority may be a number or function, and at: 'top' | 'center' controls the instance anchor (default 'top').

Label optionDescription
textRequired string or number on Label; required (datum, index) => text callback for labels on instances and points.
position3D anchor position.
classNameCSS class attached to the generated HTML label.
atFor instance labels: 'top' (default) or 'center'.
place'center' (default), 'above', 'below', 'left', or 'right'.
offsetPixel offset [x, y].
priorityOverlap priority (default 0); higher values win.
overlap'hide' (default), 'dim', or 'show'.
occludeConsider geometry occlusion; default false.
leaderDraw a leader line and point marker; default false. Style its line via .phoria-leader.
edge'left' or 'right' canvas-edge callout placement.
minScaleMinimum on-screen pixels per world unit before showing a label. Default 0.
onClick, onHoverInteractive label callbacks.

Geometry and Outlines

GeneratorArguments
box()Size number or { width, height, depth }.
sphere(){ radius, segments, rings }.
plane()Size number or { width, depth }; lies in the XZ plane.
cylinder(){ radius, radiusTop, height, segments, axis }.
torus(){ radius, tube }.
icosphere(){ radius, detail }.
lathe(){ points, segments, axis } for a rotated profile.
extrude(){ shape, holes, depth, axis } for a 2D outline.
grid(){ size, divisions }.
edges()Geometry and { angle }; default angle is 1 degree.

The axis option on cylinders, lathes, and extrusions accepts 'x', 'y', or 'z'. Public outline generators are outline.circle(radius | { radius, segments, center }), outline.rect({ width, height, radius, center }), outline.polygon({ radius, sides, angle }), outline.arc({ radius, from, to, center }), and outline.gear({ radius, teeth, depth }).

Custom geometry uses new Geometry({ positions, normals, uvs, uvs2, colors, indices }). Update vertex arrays and call geometry.needsUpdate() for geometry animation. Colors are RGB values from 0 to 1, and uvs2 supplies the second UV set.

Materials and Textures

A material is accepted as an options object or a Material instance.

PropertyDescription
nameMaterial identifier.
colorBase surface color, default white.
opacityTransparency; 1 is opaque (default).
transparentForce blending at any opacity. By default enabled for opacity below 1.
roughnessMicrosurface roughness, default 0.5.
metalnessMetallic response, default 0.
emissive, emissiveIntensityEmission color and brightness.
unlitDraw base color independently of lighting.
flatShadingFlat-shaded normals.
doubleSidedRender both sides of a surface.
vertexColorsMultiply material color by geometry vertex colors.
fogApply scene fog, default true.
depthWrite, depthTestEnable writing and testing the depth buffer, both true by default.
blending'normal', 'additive', or 'multiply'.
alphaTestDiscard low-alpha texture pixels.
mapBase color texture.
normalMap, normalScaleNormal texture and strength.
bumpMap, bumpScaleHeight texture and bump strength.
roughnessMap, metalnessMapSurface data textures.
aoMapAmbient occlusion texture.
aoMapIntensityAmbient occlusion strength, default 1.
emissiveMapEmission texture.
lightMap, lightMapIntensityBaked lighting map; intensity defaults to 1.
environmentIntensityPer-material environment strength, default 1.
transmissionLight-through-surface amount from 0 to 1; no refraction.
clearcoat, clearcoatRoughnessClear glossy surface layer and its roughness.
sheen, sheenColor, sheenRoughnessCloth-like grazing-angle reflection; sheen roughness defaults to 0.5.
iorIndex of refraction for surface reflectance; default 1.5.
specularIntensity, specularColorNonmetallic reflection strength and tint; intensity defaults to 1.
anisotropy, anisotropyRotationBrushed-metal reflection strength and direction.
translucency, translucencyColorThin-surface backlighting strength and tint.
matcapMatcap texture for baked view-relative shading.
toonNumber of discrete tone steps; 0 is smooth.
triplanarTexture projection without mesh UVs; default 0.
size, sizeAttenuationPoint diameter and distance scaling.
widthLine width in pixels.

new Texture(options) accepts { image } or { url }; a URL string also works. Images may be an HTMLImageElement, canvas, video, ImageBitmap, OffscreenCanvas, or raw pixel data. Call texture.needsUpdate() after drawing onto a canvas texture. texture.ready resolves after loading, and texture.dispose() releases GPU resources. Browser autoplay policies can keep a video texture paused until a click calls texture.image.play().

Texture optionDescription
imageImage, canvas, video, bitmap, or raw pixel data.
urlURL for an image, .hdr panorama, or video.
colorSpace'srgb' (default) or 'linear'.
wrap'repeat' (default), 'clamp', or 'mirror'.
repeat, offsetUV pairs, default [1, 1] and [0, 0].
rotation, centerAngle in radians and center UV; defaults 0 and [0, 0].
uvSet0 (default) or 1 for the second UV set.
filter'linear' (default) or 'nearest'.
mipmapsGenerate mipmaps (default true; default false for video).
anisotropyTexture filtering level, default 8, subject to the GPU limit. 1 disables it.
flipYFlip pixels on upload, default false.

Lights, Shadows, and Cameras

ClassPublic options
AmbientLightcolor, intensity.
HemisphereLightsky, ground, intensity.
DirectionalLightcolor, intensity, direction, castShadow, shadowBounds.
PointLightcolor, intensity, position, range, castShadow.
SpotLightPoint light options plus direction, angle, and penumbra.
PerspectiveCamerafov, near, far, position, target.
OrthographicCameraheight, near, far, position, target.

The renderer uses at most two directional lights and four point/spot lights together. Shadow support includes one directional light, up to four spot lights, and one point light. Set castShadow on the light and participating meshes; receivers use receiveShadow.

Picking, Events, and Drag Callbacks

view.on('click' | 'hover', handler) and node.on('click' | 'hover', handler) are public event subscriptions. Node options onClick, onHover, and onDrag register callbacks; .on() registers additional hover and click listeners.

A hit has { node, instance, datum, point, distance, normal, face, uv }. The onDrag argument has { phase, hit, node, ray, plane, start, point, delta, event }, with phase set to 'start', 'move', or 'end'.

new Raycaster() exposes ray, camera, near (default 0), far (default Infinity), and threshold (default 0.1 world units). setFromCamera([x, y], camera) aims the ray using normalized device coordinates, and intersect(nodeOrNodes) returns ordered hits. A Ray provides origin, direction, intersectPlane(normal, point), and at(distance).

Tooltips accept plain text, an HTML element, or { title, rows }; return null to hide. HTML tooltip elements use the .phoria-tooltip CSS class. Labels accept your className for custom styles.

Charts API (`phoria2/charts`)

Create a chart with chart(spec) and insert the returned node into a scene. Channel names accept a field string, a constant, { value }, or { field, type, title, format, scale, axis, priority, legend }.

Chart propertyDescription
mark'bar', 'point', 'line', 'area', 'surface', or 'network'; also accepts Vega-Lite-style { type, tooltip }.
dataArray of rows or { nodes, links } for networks. Vega-Lite-style { values } is also accepted.
$schemaOptional JSON Schema identifier included in the specification.
x, y, zSpatial channels with fields, constants, or channel definitions.
color, size, opacity, labelMark encoding and text channels.
keyData row identity for animation, default id.
detailAdditional data grouping channel.
tooltiptrue, false, a field, a list of fields or { field, title, format } entries, or (row, hit) => content.
box[width, height, depth] in world units; approximately [10, 6, 6] by default.
stackStack compatible bar or area series.
widthFractional bar width from 0 to 1.
legendfalse or { position, container, ticks }.
theme'dark' or 'light'.
title, descriptionChart title and accessible description.
transitionfalse or { duration, ease } for animated updates.
overlap'dim', 'hide', or 'show' for chart labels.
highlightHighlight color, false, 'node', or 'neighbours' for networks.
linkNetwork link encodings including width, color, opacity, flow.
layoutNetwork layout 'force' (default), 'given', or { tiers, order }.
steps, seedPositive integer force-layout iterations and numeric random seed for network placement.
flattrue constrains network node positions to the XZ plane.
source, targetCustom data field names for network link endpoints.
encodingVega-Lite-style channel object as an alternative to top-level channels.

Channel type accepts 'quantitative', 'nominal', 'ordinal', or 'temporal'. A channel scale accepts domain, range, scheme, zero, nice, and type: 'linear' | 'log'. Color schemes are 'category', 'viridis', 'blues', 'heat', 'cool', and 'redblue'. Axis configuration is false or { title, format, ticks }. Format accepts Intl.NumberFormat or Intl.DateTimeFormat options or a function. Chart specs require preloaded data; data: { url } does not load remote files.

legend.position accepts 'top-right', 'top-left', 'bottom-right', and 'bottom-left'; container places the HTML legend in a chosen element. Chart specs also accept Node options such as position. These methods continue the sales example above.

sales.update(rows);                      // Replace data and animate changes.
sales.update({ color: 'team' });          // Update a specification property.
sales.filter((row) => row.revenue > 0); // Restrict displayed rows.
sales.filter(null);                    // Remove the filter.
sales.summary();                       // Chart description for assistive technology.
sales.explain();                       // Report plotted rows and scale information.
sales.scales.y(50);                    // Convert a data value to a chart coordinate.
sales.marks;                           // Nodes rendering the chart.
sales.data;                            // Original chart data.
sales.spec;                            // Normalized chart specification.

For network charts, give each node an id and each link source/target identifiers. Surface charts need one row per grid point with x, z, and height in y. The schema is available as phoria2/charts/schema.json.

glTF API (`phoria2/gltf`)

loadGLTF(url) returns a model node with meshes, cameras, and animations. Use model.play(name?) to play the first or a specified animation. Each animation exposes play(), pause(), stop(), loop, and speed.

model.toJSON() serializes the geometry and node hierarchy. Embedded binary textures and animation tracks are not retained in that JSON representation.

Curves and Motion API (`phoria2/motion`)

The core module exports curve(points, { closed, tension }) and ellipse({ plane, radius, radiusX, radiusY, radiusZ, center, tilt }). Curves expose length, at(t), tangentAt(t), points(count), line({ segments, dash }), and tube({ radius, segments, sides }). Their normalized t values follow distance along the curve.

The optional motion module exports Timeline, follow(), face(), and reveal().

APIDescription
new Timeline({ loop, speed })Create a sequence of timed steps.
timeline.add(seconds, run, { label, ease })Add a step whose callback receives progress from 0 to 1.
timeline.play()Play the sequence.
timeline.pause()Pause playback.
timeline.seek(seconds)Move to a time in seconds.
timeline.seekStep(indexOrLabel)Move to a step.
timeline.onStep(fn)Register a step-change callback.
timeline.update(dt)Advance the timeline by elapsed seconds.
follow(node, curve, t, { orient, forward, up })Move an object along a curve and optionally orient it.
face(node, direction, { forward, up })Turn an object toward a direction.
reveal(lines, t)Show the first fraction of a curve line.

ease accepts 'linear' (default), 'in', 'out', 'inOut', or a function. Timeline members include duration, time, step, label, steps, and playing. The forward option identifies the model’s forward axis, default '-z'.

Particles API (`phoria2/particles`)

The Emitter class extends Points and runs its particle updates as part of the scene.

Emitter optionDescription
maxMaximum living particles, default 1000.
rateEmission per second, default 100.
origin, originSpreadSpawn point and radius of variation.
velocity, velocitySpreadStarting velocity and scalar or per-axis random variation.
lifetimeLifetime in seconds or a [min, max] range.
colorSingle color or sequence of colors over a particle’s life.
gravity, dragAcceleration and velocity loss per second.
followNode used as the spawn position.
onParticlePer-particle callback; return false to remove a particle.

Emitters also accept Node and material options. Public methods are burst(count, spawnOptions), clear(), and set(options); count reports living particles.

new Body({ velocity, gravity, drag }) updates node motion. Attach body.update with node.onUpdate(); body.impulse([x, y, z]) applies a velocity change.

Editing API (`phoria2/edit`)

TransformHandle renders a drag-and-rotation control for a selected node. History manages undo and redo.

TransformHandle optionDescription
viewRequired View instance.
targetSelected node; assign handle.target = null to hide the handle.
move'xz' (default), 'xyz', or false.
turnEnable the rotation ring. Default true.
snap{ move, turn } snap increments, in world units and radians.
sizeRing diameter in pixels, default 120.
dragTargetAllow dragging the node itself, default true.
historyHistory instance for tracking edits.
constrain`(placement, target) => placement
onChange(target, phase) callback for editor updates.

Use handle.dispose() to remove the editing overlay.

new History({ limit, onChange }) accepts a maximum record count (100 by default) and a (history) => {} callback. The public reference is:

history.push({ label, undo, redo }); // Record an already completed change.
history.undo();                     // Undo; returns false when unavailable.
history.redo();                     // Redo; returns false when unavailable.
history.clear();                    // Clear the undo and redo stacks.
const done = history.transform(nodes, label); // Capture node transforms.
done(label);                        // Commit the change after moving nodes.
history.canUndo;
history.canRedo;
history.undoCount;
history.redoCount;
history.undoLabel;
history.redoLabel;

JSON Scenes and Headless Rendering

create(description) constructs a scene node from a JSON description. Its type can be 'scene', 'group', 'mesh', 'instanced-mesh', 'points', 'lines', 'label', a camera or light type, and 'chart' when the charts module is loaded. node.toJSON() produces a serializable description; handlers and function-based labels do not serialize. The scene schema is exported as phoria2/schema.json.

Use registerType(CustomNodeClass) to register custom scene node types. A custom class supplies static type, extends the parent’s static OPTIONS, and can override toJSON() for extra serializable fields. scene.ready resolves after URL-based texture assets have loaded.

For scene calculations in Node.js, use Scene, PerspectiveCamera, Raycaster, and NullRenderer. renderer.render(scene, camera) processes a frame; renderer.frameCount counts rendered frames. scene.update(dt, time) runs registered update callbacks. NullRenderer.lastFrame is a debugging detail outside the stable public contract.

Styling, Accessibility, and Browser Support

Labels are HTML elements. Use className for label styling, .phoria-tooltip for tooltips, and .phoria-leader for label callout lines:

.rack-label {
  color: #fff;
  font: 600 12px/1.4 system-ui;
  text-shadow: 0 1px 3px #000;
}
.phoria-tooltip {
  font: 13px/1.5 system-ui;
}

Set keyboard: true to place the canvas in the tab order. Arrow keys, Home, End, PageUp, and PageDown navigate interactive marks; Enter or Space invokes a click, and Escape leaves the selection. Use keyboard: { label: (hit) => text } and description for screen reader announcements. reducedMotion: 'auto' follows the system preference for camera glides, chart transitions, and auto-rotation.

phoria2 requires WebGL 2 and supports current Chrome, Edge, Firefox, and Safari, including Safari on iOS 15 and newer.

Alternatives and Related Resources

FAQs

Q: Why is my phoria2 canvas blank?
A: Give the canvas a nonzero CSS height and confirm WebGL 2 is available. Call console.log(await view.explain()) to check for an empty view, camera framing, and missing lights.

Q: Can a chart load its data directly from a URL?
A: Fetch or transform the rows in your application first, then pass them to chart({ data: rows, ... }). The chart spec does not fetch data: { url } for you.

Q: How do I keep dashboard charts from redrawing continuously?
A: Leave renderMode set to 'auto'. A View schedules a new frame when its scene changes; use view.invalidate() after modifying raw arrays the View cannot track.

Q: Does phoria2 support custom shaders or refractive glass?
A: No. The renderer provides predefined materials and transmission, but it has no custom shader hook or refraction effect. Choose a more extensible rendering engine for those requirements.

Q: How should I dispose of a View in React or another component framework?
A: Create the View once the canvas has mounted, update its scene when props change, and call view.dispose() during unmount cleanup.

You Might Be Interested In:


Leave a Reply