Skip to content

Camera & Coordinates

Nodus draws an unbounded graph space through a movable camera onto a fixed screen. Understanding the two coordinate spaces — and the camera that relates them — is the key to positioning elements, reading pointer input, and animating the view.

Everything here lives under nodus.view.*. For task-focused recipes see Controlling the Camera; for full signatures see the View API.

Two coordinate spaces

SpaceUnitOriginUsed for
Graph / worldgraph unitsthe layoutnode positions, layout, geometry
Screen / pixelsCSS pixelsthe containerpointer events, DOM overlays
  • A node’s getPosition() is in graph coordinates — stable regardless of where the camera is looking.
  • A mouse event’s clientX/clientY is in screen coordinates — relative to the container.

The camera is what converts between them.

The camera

The camera has three properties:

  • center — the graph point currently at the middle of the screen.
  • zoom — the scale factor (larger = more magnified).
  • angle — the rotation of the view.

You read and set them through nodus.view:

// Zoom
nodus.view.getZoom();
nodus.view.setZoom(2, { duration: 300 });
nodus.view.zoomIn();
nodus.view.zoomOut();
// Pan / center
nodus.view.getCenter(); // { x, y } in graph coords
nodus.view.setCenter({ x: 0, y: 0 });
nodus.view.move({ x: 50, y: 0 }); // pan by a delta
nodus.view.moveTo(target); // pan a target to center
nodus.view.moveToBounds(bounds); // frame a bounding box
// Rotate
nodus.view.getAngle();
nodus.view.setAngle(Math.PI / 8);
nodus.view.rotate(0.1); // rotate by a delta
// The whole view at once
const state = nodus.view.get();
nodus.view.set(state, { duration: 400 });

Converting between spaces

When you need to place a DOM overlay at a node, or turn a click into a graph point, convert explicitly:

// Graph -> screen: where does this node land on screen right now?
const screenPt = nodus.view.graphToScreenCoordinates(node.getPosition());
tooltip.style.left = `${screenPt.x}px`;
tooltip.style.top = `${screenPt.y}px`;
// Screen -> graph: what graph point did the user click?
const graphPt = nodus.view.screenToGraphCoordinates({ x: event.clientX, y: event.clientY });

These conversions account for the camera’s center, zoom and angle, so they stay correct even when the view is panned, zoomed or rotated.

Try it live — move the camera and watch screen and graph coordinates track each other:

Screen ↔ graph coordinates Open in new tab ↗

Fitting the graph

To frame the whole graph in the viewport — the thing you almost always want after a layout — use locateGraph, which returns a Promise<void>:

await nodus.layouts.force({ duration: 0 });
await nodus.view.locateGraph();

You can also fit a graph you haven’t loaded yet (e.g. to preview framing) with locateRawGraph(graph), or pan/zoom to a single element with element.locate().

Try it live — fit the whole graph, then zoom and focus a single node:

Fit & focus the camera Open in new tab ↗

Hit-testing and queries

The view answers spatial questions about what’s under or within a region:

// What element is at this screen point?
const hit = nodus.view.getElementAt({ x: event.clientX, y: event.clientY });
// What's currently visible?
const visible = nodus.view.getElementsInView();
// What's inside a rectangle? (screen coords by default)
const inside = nodus.view.getElementsInside(x1, y1, x2, y2);
// pass inGraphCoords = true to query in graph space:
const inGraph = nodus.view.getElementsInside(gx1, gy1, gx2, gy2, true);

Related helpers include getBounds(), getGraphBoundingBox(), getTextBoundingBox(el) and getNodeBadgeAt(node, position). These power custom selection, context menus and overlays — see Handling Events.

Sizing

The drawing surface tracks the container, but you can read and control it:

nodus.view.getSize(); // { width, height } in pixels
await nodus.view.setSize({ width: 800, height: 600 });
nodus.view.forceResize(); // re-measure after a layout change

forceResize() is the call to make after the container changes size in a way Nodus can’t observe automatically (for example, a sidebar opening). The renderer matches the device pixel ratio so the result stays crisp on high-DPI displays — see The Rendering Pipeline.

You can also go fullscreen: nodus.view.setFullScreen(true) / isFullScreen().

Animating and frame synchronization

Most camera moves accept an options object whose { duration } (ms) animates the transition — the same convention as Layouts:

await nodus.view.setZoom(3, { duration: 500 }); // smooth zoom
nodus.view.moveTo(node.getPosition(), { duration: 400 });

To coordinate work with the render loop, await a frame boundary:

await nodus.view.beforeNextFrame(); // resolves just before the next frame
// ... measure / set up overlays in sync with rendering ...
await nodus.view.afterNextFrame(); // resolves just after it has drawn

nodus.view.animationInProgress() tells you whether a camera animation is currently running — handy for not interrupting one. For a raster snapshot of the current frame, use nodus.view.getImageData().

Where to go next