Skip to content
Pivotick
Main Navigation HomeGetting StartedConfigurationGalleryTry it out!Generated API docs

Appearance

Sidebar Navigation

Getting Started

Configuration

Callbacks

Edge layers

Layout

Render

Simulation

Customizing UI

Sidebar

Tooltip

Context Menu

Filters

Legend

Data table

Styling UI

Plugins

Pivotick API

Pivots & enrichment

Saving pivot results

Undo & history

Security

API docs

Gallery

Basics

Node styling

Edge styling

Layouts

Events & callbacks

UI customization

Editing & authoring

Notes & annotations

Filtering, search & hierarchy

Programmatic control

Theming & performance

Pivots & enrichment

Showpieces

Add a rail mode ​

The left rail holds modes, not buttons. Select, Create, View and Physics are built in, and each shows only its own tools — which is why the rail stays four slots tall however much the graph can do. addRailMode() adds a fifth.

This card adds an Explore mode for walking a service graph: pick a node, select its neighbours, narrow the canvas to them, then step back out. Press E, or click the compass below the divider.

Where your mode lands ​

Registered modes sit below a divider, after the built-ins, ordered among themselves by order. A plugin can't reorder or remove the built-in four, so everything above the line is the same in every Pivotick graph, and everything below it is yours.

Declare tools, not markup ​

A mode's tools become rows in the contextual panel, drawn just like Select's and Create's. You supply the verbs; each row's kind says how it behaves:

kindBehaviour
'action'runs once and leaves the mode alone — all three tools here
'toggle'arms a tool: the panel collapses and the rail button becomes that tool, the way Select's slot becomes Lasso
'default'the tool the mode rests on, and what a toggle goes back to

enabled() is re-checked whenever the selection changes — that's why Select neighbours and Isolate stay greyed out until you pick a node.

Nothing here is privileged ​

Every tool above calls public API you could call yourself: getEdges() to walk relations, selectNodes() to select them, setVisibleNodes() to narrow the canvas. The mode just gives them somewhere to live — a rail slot, a shortcut, an armed state and a panel.

Which is the point: an Explore mode that knows what "neighbour" means for your data is something you can build and we can't.

Modes exclude each other

Opening Explore closes View and Physics, and vice-versa — one store, one active mode. Undo whatever your mode armed in onExit; it also runs if the mode is removed while it's active, and the rail falls back to Select.

When a flyout fits better ​

A mode that configures rather than acts wants the wide overlay, not the narrow tool strip. Set kind: 'flyout', subclass the exported Flyout, and pass addRailMode a factory — see Contributing a rail mode.

js
// The rail's four modes — Select, Create, View, Physics — are built in. `addRailMode`
// is how you add a fifth. It renders below a divider, after them, and drives the same
// contextual tool panel the built-ins do: declare `tools`, and the arming, the
// enabled/disabled states and the collapse are handled for you.
//
// This card adds an **Explore** mode for walking the graph: reach the neighbours of what
// is selected, isolate a subgraph, and step back out. Nothing here is privileged — every
// tool is public graph API a consumer could call themselves.

const compass = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="20" height="20"><path fill="none" stroke="currentColor" stroke-width="2" stroke-linejoin="round" d="M12 21a9 9 0 1 0 0-18a9 9 0 0 0 0 18Z"/><path fill="currentColor" d="M15.5 8.5l-2 5l-5 2l2-5z"/></svg>'
const expandIcon = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="18" height="18"><path fill="currentColor" d="M11 13v3a1 1 0 0 0 2 0v-3h3a1 1 0 0 0 0-2h-3V8a1 1 0 0 0-2 0v3H8a1 1 0 0 0 0 2zm1 9a10 10 0 1 1 0-20a10 10 0 0 1 0 20m0-2a8 8 0 1 0 0-16a8 8 0 0 0 0 16"/></svg>'
const isolateIcon = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="18" height="18" fill="none" stroke="currentColor" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"><path d="M16 6H8a2 2 0 0 0-2 2v8m10 26H8a2 2 0 0 1-2-2v-8m26 10h8a2 2 0 0 0 2-2v-8M32 6h8a2 2 0 0 1 2 2v8"/><circle cx="24" cy="24" r="6"/></svg>'
const resetIcon = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="18" height="18"><path fill="currentColor" d="M12 5V2L8 6l4 4V7a5 5 0 1 1-5 5H5a7 7 0 1 0 7-7"/></svg>'

function exploreMode() {
    return {
        name: 'explore-mode',
        install(ctx) {
            const interaction = () => ctx.graph.renderer.getGraphInteraction()
            const selected = () => interaction().getSelectedNode()?.node

            /** Every node one hop from `id`, itself included. */
            const withNeighbours = (id) => {
                const keep = new Set([id])
                for (const edge of ctx.graph.getEdges()) {
                    if (edge.from.id === id) keep.add(edge.to.id)
                    if (edge.to.id === id) keep.add(edge.from.id)
                }
                return keep
            }

            ctx.addRailMode({
                id: 'explore',
                label: 'Explore',
                icon: compass,
                // Pressing E switches to the mode, or toggles its panel when it is
                // already active — exactly what V and C do for Select and Create.
                shortcut: 'E',
                tools: [
                    {
                        // A one-shot action: it runs and leaves the mode as it was.
                        id: 'expand',
                        label: 'Select neighbours',
                        icon: expandIcon,
                        kind: 'action',
                        // Re-checked on every selection change, so the row greys out
                        // when it would do nothing.
                        enabled: () => !!selected(),
                        run: () => {
                            const node = selected()
                            if (!node) return
                            const keep = withNeighbours(node.id)
                            interaction().selectNodes(
                                ctx.graph.getMutableNodes()
                                    .filter(n => keep.has(n.id))
                                    .map(n => ({ node: n, element: n.getGraphElement() }))
                            )
                        },
                    },
                    {
                        id: 'isolate',
                        label: 'Isolate neighbourhood',
                        icon: isolateIcon,
                        kind: 'action',
                        enabled: () => !!selected(),
                        run: () => {
                            const node = selected()
                            if (!node) return
                            const keep = withNeighbours(node.id)
                            ctx.graph.setVisibleNodes(
                                ctx.graph.getMutableNodes().filter(n => keep.has(n.id))
                            )
                        },
                    },
                    {
                        id: 'reset',
                        label: 'Show everything',
                        icon: resetIcon,
                        kind: 'action',
                        run: () => ctx.graph.setVisibleNodes(ctx.graph.getMutableNodes()),
                    },
                ],
            })
        },
    }
}

const options = {
    UI: {
        mode: 'full',
        // Neither is what this card is about, and both crowd a small canvas.
        minimap: false,
        table: false,
    },
    plugins: [exploreMode()],
}
js
const data = {
    nodes: [
        { id: 'api', data: { label: 'api-gateway', kind: 'service' } },
        { id: 'auth', data: { label: 'auth-service', kind: 'service' } },
        { id: 'billing', data: { label: 'billing-service', kind: 'service' } },
        { id: 'search', data: { label: 'search-service', kind: 'service' } },
        { id: 'users-db', data: { label: 'users-db', kind: 'datastore' } },
        { id: 'orders-db', data: { label: 'orders-db', kind: 'datastore' } },
        { id: 'index', data: { label: 'search-index', kind: 'datastore' } },
        { id: 'cache', data: { label: 'edge-cache', kind: 'datastore' } },
        { id: 'mailer', data: { label: 'mailer', kind: 'worker' } },
        { id: 'reindexer', data: { label: 'reindexer', kind: 'worker' } },
    ],
    edges: [
        { from: 'api', to: 'auth' },
        { from: 'api', to: 'search' },
        { from: 'api', to: 'billing' },
        { from: 'api', to: 'cache' },
        { from: 'auth', to: 'users-db' },
        { from: 'billing', to: 'orders-db' },
        { from: 'billing', to: 'mailer' },
        { from: 'search', to: 'index' },
        { from: 'reindexer', to: 'index' },
        { from: 'reindexer', to: 'orders-db' },
    ],
}

Last updated:

Pager
Next pageGetting Started