Skip to content

Sidebar Options ​

The sidebar can be collapsed by default depending on screen size or user preference.

Determines whether the sidebar is collapsed by default.

  • 'auto' default: Keeps the sidebar open unless there isn't enough screen space, in which case it collapses automatically.
  • true Sidebar starts collapsed.
  • false Sidebar starts expanded.

The sidebar displays contextual information for graph elements. It has three customizable components:

UI.sidebar.enabled: false drops the whole column and hands its width back to the canvas. The properties and neighbours panels have a switch each — UI.propertiesPanel.enabled, UI.neighborsPanel.enabled — which removes that panel and its separator while the rest of the sidebar stays. See Turning features off.

Click to see the code
js
UI: {
    mode: 'full',
    sidebar: {
        collapsed: false,
    },
    mainHeader: {
        render: (element) => {
            const div = document.createElement('div')
            div.textContent = 'Main Header'
            div.style.fontWeight = 'bold'
            div.style.color = 'darkred'
            div.style.background = 'var(--pvt-bg-color-8)'
            div.style.padding = '4px 8px'
            div.style.borderRadius = '4px'
            return div
        }
    },
    propertiesPanel: {
        render: (element) => {
            const div = document.createElement('div')
            div.textContent = 'Properties Panel'
            div.style.fontWeight = 'bold'
            div.style.color = 'darkred'
            div.style.background = 'var(--pvt-bg-color-8)'
            div.style.padding = '4px 8px'
            div.style.borderRadius = '4px'
            return div
        }
    },
    extraPanels: [
        {
            title: 'Extra panel #1',
            render: (node) => {
                const div = document.createElement('div')
                div.textContent = 'Extra Panel #1'
                div.style.fontWeight = 'bold'
                div.style.color = 'darkred'
                div.style.background = 'var(--pvt-bg-color-8)'
                div.style.padding = '4px 8px'
                div.style.borderRadius = '4px'
                return div
            },
        },
        {
            title: 'Extra panel #2',
            render: (node) => {
                const div = document.createElement('div')
                div.textContent = 'Extra Panel #2'
                div.style.fontWeight = 'bold'
                div.style.color = 'darkred'
                div.style.background = 'var(--pvt-bg-color-8)'
                div.style.padding = '4px 8px'
                div.style.borderRadius = '4px'
                return div
            },
        },
    ],
    tooltip: {
        enabled: false,
    },
    contextMenu: {
        enabled: false,
    },
},

Main Header interface ​

Shows a concise summary of the selected node or edge, such as title and subtitle. It helps users quickly identify the current selected element.

The main header panel can be customized through mapping functions. The default mapping for a node is

ts
{
    title:    node => node.data.label ?? "Could not resolve title"
    subtitle: node => node.data.description ?? ""
}

You can change this mapping by overriding the parts you need:

ts
const options = {
    UI: {
        mainHeader: {
            nodeHeaderMap: {
                title: node => `Node ${node.id}`,
                subtitle: node => node.data.type ?? "",
            }
        }
    }
}
ts
const options = {
    UI: {
        mainHeader: {
            edgeHeaderMap: {
                title: node => `Node ${node.id}`,
                subtitle: node => node.data.type ?? "",
            }
        }
    }
}
ts
const options = {
    UI: {
        mainHeader: {
            render: (element) => {
                const div = document.createElement('div')
                div.textContent = 'Main Header'
                return div 
            }
        }
    }
}

WARNING

When render() is provided, Pivotick skips all default mapping logic.

Properties Panel interface ​

Displays detailed properties of the selected node or edge in the sidebar. Each property has a name and value that can be static or computed dynamically.

The default behavior is to show all key/value pairs from the node or edge's getData().

You can customize which properties are displayed and how they are rendered using mapping functions (nodePropertiesMap and edgePropertiesMap) or a full custom renderer.

ts
const options = {
    UI: {
        propertiesPanel: {
            nodePropertiesMap: (node: Node) => {
                return [
                    {
                        name: 'Node ID',
                        value: node.id,
                    },
                    {
                        name: (node) => `Type of node`,
                        value: (node) => el ? el.type : 'Unknown'
                    },
                    {
                        name: 'Custom HTML',
                        value: document.createElement('div')
                    }
                ]
            }
        }
    }
}
ts
const options = {
    UI: {
        propertiesPanel: {
            edgePropertiesMap: (node: Node) => {
                return [
                    {
                        name: 'Edge ID',
                        value: node.id,
                    },
                    {
                        name: (node) => `Type of edge`,
                        value: (node) => el ? el.type : 'Unknown'
                    },
                    {
                        name: 'Custom HTML',
                        value: document.createElement('div')
                    }
                ]
            }
        }
    }
}
ts
const options = {
    UI: {
        propertiesPanel: {
            render: (element: Node | Edge | Node[] | Edge[] | null) => {
                const div = document.createElement('div')
                div.textContent = `Element ID: ${element?.id}`
                div.style.fontWeight = 'bold'
                return div
            }
        }
    }
}
ts
const options = {
    UI: {
        propertiesPanel: {
            nodePropertiesMap: (node: Node) => {
                return Object.entries(node.getData())
                    .filter(([key, value]) => key && value)
                    .map(([key, value]) => ({ name: key, value }))
            }
        }
    }
}

Extra Panels interface ​

Allows adding fully custom panels with dynamic or static content. These panels are ideal for showing additional contextual information, custom controls, or interactive widgets related to the selected element.

Each extra panel has an optional title and a render(), both of which can be static (string/HTMLElement) or a function of the current selection. A returned string renders as text; return an HTMLElement to render your own markup.

ts
const options = {
    UI: {
        extraPanels: [
            {
                title: "Info",
                render: "This is a static extra panel content"
            }
        ]
    }
}
ts
// The selection is a Node, an Edge, an array of either, or null — this example
// only cares about a single element:
const single = (element) => (element && !Array.isArray(element)) ? element : null

const options = {
    UI: {
        extraPanels: [
            {
                // Both are called with the live selection, on every selection change.
                title: (element) => single(element) ? `Node #${single(element).id}` : 'Nothing selected',
                render: (element) => {
                    const div = document.createElement('div')
                    div.textContent = single(element)?.getData().description ?? 'No description'
                    return div
                }
            }
        ]
    }
}

The selection a panel is rendered with ​

title and render are re-invoked on every selection change, with what is currently selected:

SelectionArgument
One node / one edgethe Node or Edge
Several nodes / edgesa Node[] / Edge[]
Nothing selectednull

A panel is only shown while something is selected, unless it sets alwaysVisible: true — but a reactive panel is re-rendered either way, so it never holds content describing a stale selection.

Set reactive: false for a panel that doesn't describe the selection and is expensive to build: it then renders once, and only an explicit refreshPanel() rebuilds it.

Registering panels at runtime ​

UI.extraPanels is the declarative form of the same registry. Anything you can declare there you can also register — and remove — at any point in the graph's life, through the UIManager:

ts
const graph = new Pivotick(container, data, options)

// Register a panel that depends on the graph itself. Returns a disposer.
const dispose = graph.UIManager.addPanel({
    id: 'unlinked-items',
    title: 'Unlinked items',
    alwaysVisible: true,
    order: -1,                              // sorts above the option-declared panels
    render: (selection) => tray.render(selection),
})

// Your own data changed (a save landed) rather than the selection —
// ask for a re-render instead of hand-patching the panel's DOM.
graph.UIManager.refreshPanel('unlinked-items')

// Gone for good (equivalent to calling `dispose()`):
graph.UIManager.removePanel('unlinked-items')
Method
addPanel(panel)Register a panel; returns a disposer. id is auto-generated when omitted. Works before and after the graph is ready, and from a plugin's install (as ctx.addPanel).
removePanel(id)Remove the panel and its DOM.
refreshPanel(id?)Re-render one panel, or all of them. Refreshes reactive: false panels too.
getPanels()The registered panels, in display order.

Panels sort by order (ascending, default 0) and keep registration order within the same value, so UI.extraPanels reads top-to-bottom and runtime panels append after them.

A panel's own title / render receives a second argument — a handle on itself — so it can drive its own updates without reaching for the graph:

ts
render: (selection, panel) => {
    const button = document.createElement('button')
    button.textContent = 'Reload'
    button.onclick = () => panel.refresh()   // …or panel.remove()
    return button
}

Sidebar-only

Panels are shown by the sidebar, which exists in full mode. Registration succeeds in any mode — the panel simply has nowhere to render until a sidebar does.

Filling a panel asynchronously ​

title and render may both be async, as may the properties panel's render / nodePropertiesMap / edgePropertiesMap and the main header's render. The panel shows a placeholder while the promise is pending:

ts
graph.UIManager.addPanel({
    id: 'sightings',
    title: 'Sightings',
    render: async (selection, panel, { signal }) => {
        if (!(selection instanceof Node)) return 'Select a node'
        const res = await fetch(`/sightings/${selection.id}`, { signal })
        return renderSightings(await res.json())
    },
})

Note the argument order: the panel handle stays second, and the RenderContext is always last.

Because a panel re-renders on every selection change, a fetch is routinely superseded before it resolves. The library handles that for you: the old render's signal is aborted, and a result arriving after the selection moved on is dropped rather than painted into a panel describing a different node. The same applies to refreshPanel() and to removing the panel. See asynchronous content for the full contract.