Skip to content

Navigator

A navigator is an interactive pan / zoom / brush controller — the flat-scene analogue of the 3D Camera. It owns a single affine view transform ({ k, x, y } — a uniform scale plus a translation) and an optional rectangular brush, and it emits events as that transform changes. The navigator never repaints anything itself; you react to its events and re-render your scene under the new transform.

The controller is split in two, mirroring the Context / DOMContext split:

  • Navigator (in @ripl/core) is context-agnostic. It owns the view model and the imperative commands that mutate it (panBy, zoomBy, fitBounds, …) but attaches no input listeners. Non-DOM environments can drive it programmatically.
  • DOMNavigator (in @ripl/dom, via createNavigator) extends it to translate real wheel / pointer / touch gestures into those commands.

NOTE

For the full API, see the Core API Reference.

Demo

Drag to pan, use the wheel (or pinch) to zoom toward the pointer, and shift-drag to zoom into a region. Translation is unbounded, so you can roam past the framed content — use the buttons to jump back.

Creating a navigator

createNavigator binds a DOMNavigator to a context's element and (optionally) wires up gestures. Pass interactions: true to enable everything, or an object to enable them individually — each interaction also accepts a sensitivity multiplier.

ts
import {
    createNavigator,
} from '@ripl/web';

const navigator = createNavigator(context, {
    // Clamp zoom between 0.5× and 20×.
    scaleExtent: [0.5, 20],
    interactions: {
        zoom: {
            sensitivity: 1.5,
        },
        pan: true,
        brush: true,
    },
});

The gesture model is intentionally Figma-like: click-and-hold (left or middle button, with or without ⌘/Ctrl) pans, the wheel and two-finger pinch zoom toward the pointer, and shift-drag brushes a rectangle. Call navigator.destroy() to detach every listener.

The view transform

The transform is a plain { k, x, y } object: a content-space point p maps to the screen as k · p + [x, y]. Two helpers convert between spaces:

ts
navigator.applyPoint([100, 50]); // content → screen
navigator.invertPoint([320, 240]); // screen → content

transform returns a copy, so mutating it never affects the navigator — always go through a command.

Commands

Every command clamps k to the scale extent and emits an event when it changes the view.

CommandDescription
panBy(dx, dy)Pan by a pixel delta
panTo(x, y)Pan to an absolute translation
zoomBy(factor, center?)Multiply the zoom, keeping center (screen px) fixed
zoomTo(k, center?)Set an absolute zoom factor
centerOn(point, viewport?)Centre a content point in the viewport, keeping zoom
fitBounds(bounds, options?)Zoom and pan so a content box fills the viewport
reset()Return to the identity transform
setTransform(transform)Replace the transform outright

Because translation is unbounded, fitBounds and centerOn can jump the view to content that currently lies entirely outside the viewport — the "navigate anywhere" behaviour the demo's Frame all button relies on.

ts
// Zoom to 2× toward the top-left corner of the viewport.
navigator.zoomTo(2, [0, 0]);

// Frame a bounding box with 24px of padding.
navigator.fitBounds({ x0: 40, y0: 40, x1: 520, y1: 300 }, { padding: 24 });

Events

EventPayloadFires when
zoomNavigatorTransformThe zoom factor changes
panNavigatorTransformThe translation changes
changeNavigatorTransformAny transform change (zoom or pan)
brushNavigatorBrushThe brush selection updates
brushendNavigatorBrush | nullA brush gesture finishes (or is cleared)

Subscribe to change for the common "repaint on any view change" case; use zoom / pan when you need to react to just one.

ts
navigator.on('change', event => scene.update(event.data));
navigator.on('brushend', event => event.data && zoomToSelection(event.data));

Rescaling chart axes

A cartesian chart doesn't transform its rendered geometry — it rescales its axis domains and redraws. rescaleDomain does exactly that: it inverts the transformed pixel range back through a scale to produce the visible data domain.

ts
import {
    rescaleDomain,
} from '@ripl/web';

navigator.on('change', transform => {
    const [x0, x1] = rescaleDomain(xScale, transform, [0, width]);
    chart.setDomain([x0, x1]);
});

This keeps geometry crisp at any zoom level (no scaled stroke widths or blurred text) and is how the opt-in navigator support on the cartesian charts works under the hood.

Programmatic use

Because the base Navigator in @ripl/core owns the view model without any input wiring, you can drive it directly — for animations, tests, or non-DOM contexts — and consume the same events:

ts
import {
    Navigator,
} from '@ripl/web';

const navigator = new Navigator({
    viewport: {
        width: 800,
        height: 600,
    },
});

navigator.on('change', ({ k, x, y }) => render(k, x, y));
navigator.zoomTo(4, [400, 300]);