Skip to content

Navigating

A large chart is mostly off screen. What matters is how you get to a specific node and how you keep your bearings once you are there.

Go to a node

ts
chart.api.focus('lead-42')

focus opens every collapsed ancestor on the way, then centres the node. It works from a fully collapsed chart, which is the case it exists for: it waits for the layout that expanding produced rather than reading a position that does not exist yet. When nothing needed expanding, the move happens immediately.

Add a confirmation flash on arrival:

ts
chart.api.focus('lead-42', { ring: true })

The ring fires when the camera gets there, not when it sets off, so the whole flash happens where you are looking. It is off by default — stepping through search results in a loop should move the camera, not strobe.

Showing the way

pathTo returns the chain of ids from the root down to a node, inclusive:

ts
chart.api.pathTo('lead-42') // ['ceo', 'cto', 'eng', 'lead-42']

Which is exactly what highlight wants:

ts
chart.api.highlight(chart.api.pathTo('lead-42'))
chart.api.focus('lead-42', { ring: true })

A connector is drawn in the highlight colour when both of its endpoints are highlighted. For a root-to-node chain that is precisely the route and nothing else — a highlighted node's other children are not themselves highlighted, so their connectors stay quiet. Scattered highlights (a search result) light the nodes without inventing a path between them.

highlight(null) clears it — and so does Esc.

Showing one branch

ts
chart.api.fitSubtree('eng') // point the camera at it
chart.api.isolate('eng') // make it the chart
chart.api.isolate(null) // and back

The difference matters once a chart is large. fitSubtree moves the camera and leaves everything else where it was, just off screen. isolate re-roots the tree: the layout has one branch to arrange, the minimap shows that branch rather than a speck inside a company, Tab walks it instead of the org, and an export is a picture of it.

Where the viewer is, is yours to say — pathTo(id) returns the chain from the real root, which is a breadcrumb:

ts
const trail = chart.api.pathTo('eng') // ['ceo', 'cto', 'eng']

Saving a view

ts
const view = chart.api.getView() // { camera, open, highlighted }
chart.api.setView(view) // arrive there
chart.api.setView(view, { animate: true }) // fly there

Where the viewer is, as one plain object: the camera, which branches are open, and what is lit. It is serialisable and names nodes by id, so it goes in a URL or a saved report and still works after the data is refetched or grown. Ids it names that have since left the tree are ignored rather than throwing.

ts
const results = chart.api.search('chen')
// [{ id, item, path }, ...]

Substring match on the node's label by default, or pass your own predicate:

ts
chart.api.search((item) => item.department === 'Design' && item.level > 3)

Each result carries its own path, so a result list can show where a match sits without a second call.

Camera

CallWhat it does
fit()Zooms out far enough to show the whole visible tree.
fitSubtree(id)Frames one branch instead. On a large chart this is the useful one — fitting everything means a zoom level where nothing can be read.
isolate(id)Shows that branch as the chart; isolate(null) puts the rest back.
reset()Back to the opening view.
zoomIn() / zoomOut()One step, about the centre.
zoomTo(k)An exact scale.
focus(id, opts?)Centres a node, opening the way to it.

Every one of them eases rather than jumping, and every one is interrupted the instant a user's hand touches the canvas — dragging always wins immediately.

Keyboard

Click the chart, or Tab to it, and the camera answers to the keyboard:

Key
ArrowsPan. Hold Shift for a stride rather than a step.
+ / -Zoom about the middle of the view.
fFit the whole chart.
0Back to the opening view.
HomeCentre the root.
EscClear the highlight.

The chart is a tab stop, and the first one inside itself — so "Tab, then arrows" works without walking past every card's own buttons to get there. Keys are left alone when the focus is inside something using them already: an input or <select> on one of your cards, or a row of the accessibility tree, which has its own arrow keys for moving between NODES rather than moving the view.

Set keyboard: false if the surrounding app binds these itself, or if the host must not take focus.

Gestures

Gesture
Drag with the primary button, or one fingerPan. A release with speed coasts to a stop.
Wheel or trackpad scrollZoom about the pointer.
Two fingersPinch to zoom about the midpoint.
Right or middle buttonNothing. Yours — a right-click reaches your own contextmenu handler with the chart holding still under it.

The host element is given touch-action: none while a chart is mounted, and it is handed back on destroy(). That is what makes a one-finger drag pan the chart instead of scrolling the page, and a pinch zoom the camera instead of the whole document. Text selection is suppressed on the host for the same reason — a pan that starts on a card would otherwise drag-select its label — while buttons, links and form controls inside a card keep working normally.

Expanding and collapsing

ts
chart.api.expand('cto') // just this node
chart.api.expand('cto', true) // and everything below it
chart.api.collapse('cto', true)
chart.api.expandAll()
chart.api.collapseAll()
chart.api.expandTo('lead-42') // open the ancestors, without moving the camera

A single-node toggle keeps that node pinned exactly where it was on screen while the rest of the layout moves around it — to the pixel, on both the worker and main-thread paths. Turn it off with autoPanOnToggle: false if you would rather the camera stayed still and the tree moved under it.

The minimap

ts
minimap: true
minimap: { position: 'top-left', width: 200, height: 140 }
minimap: { silhouetteColour: '#94a3b8' } // for a dark host

It draws a silhouette of the occupied area rather than a shrunken chart — at that scale individual boxes fall below a pixel — with the current viewport as a rectangle over it. Click or drag inside it to pan.

The plate, its border and the viewport rectangle are ordinary DOM — style them from your own CSS via .klad-minimap. The silhouette is not: it is written pixel by pixel into a canvas, so it is the one part that needs an option, silhouetteColour. The default slate reads well on a light plate and disappears on a dark one, so a dark theme should set it. Only the colour's RGB is used; each pixel's alpha is the silhouette's own coverage.

Its frame is held steady across an expand or collapse rather than refitting to whatever is currently open: a minimap whose scale lurched on every toggle would be a zoom rather than a map, and nothing would stay where you last saw it.

AGPL-3.0-or-later, with a commercial licence available.