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

Filterable legend ​

A colour-coded graph needs a key — and once it has one, the key is the fastest filter in the UI. UI.legend docks one in a canvas corner: a swatch, a label and a node count per category, and clicking a row hides that category. Pointing at a row first lights its nodes on the canvas and dims the rest, so a category can be found before it is switched off.

One line configures it: legend: { key: 'type' }. The library collects the distinct values of data.type, reads each swatch from the colour the renderer already resolved, and counts the nodes behind it. Often you need no line at all — with a render.nodeTypeAccessor declared, a legend appears by itself once that dimension is shown to explain the colours (see Legend); this card declares the block to show the knobs. The legend is purely descriptive — the colours here come from a ColorPaletteMapper in the options, exactly as in Color by category, and the legend never assigns one. Change the palette and the legend follows.

Clicking a row filters through graph.queryEngine, so it is the same filtering the header's Graph Filters panel does. This card declares a type facet under the legend's own key, which makes the two one control: switch off api in the legend, open the panel (funnel, or Shift+K) and its Service type multiselect has already dropped it. resetFilters() — or the panel's Reset — re-lights every row.

Nodes with no type at all get no row, and the legend never hides them: it can only act on the categories it lists.

One key is rarely the whole story. Two keys at once stacks a titled section per dimension in the same docked card — press type + zone below: type keeps driving the panel's facet, zone gets a section of its own, and the two filters and together, so hiding api and dmz leaves what is neither. zone declares its own swatch colours, because the canvas encodes type in its colours and a sampled swatch for zone would only be a coincidence.

A section can key edges instead of nodes, too — scope: 'edge' lists the graph's relation kinds with a line swatch and hides a whole layer when you click one. That needs edges that come in kinds, so it lives on the Edge layers card.

In a section header: show all, invert, and a chevron that folds that section to its title — alt-click the chevron to fold every section. Hover a row to light its category on the canvas; alt-click one to show only that category. Toggles are logged to the console through legendToggle, which names its section.

js
// import { ColorPaletteMapper } from 'pivotick'

// The consumer owns the colours — here a palette mapper over `type`, exactly as in
// the "Color by category" card. The legend never assigns a colour.
const palette = new ColorPaletteMapper('okabe-ito')

// `key` is all a legend needs: it collects the distinct values of `data.type`,
// reads each swatch out of the colour the renderer resolved, counts the nodes
// behind it, and makes every row a filter toggle.
//
// You can also leave `UI.legend` out entirely: with a `render.nodeTypeAccessor`
// declared, a legend appears by itself as soon as that dimension is shown to
// explain the colours. `UI.legend: false` opts out.
const legend = {
    key: 'type',
    title: 'Service type',
    // Every one of these is a default — spelled out here to show the knobs.
    position: 'bottom-left',
    showCounts: true,
    collapsible: true,
    filterable: true,
    maxVisibleEntries: 12
}
js
const options = {
    UI: {
        // The legend is chrome: `full` and `light` modes have it, `viewer` and
        // `static` don't. Full mode also ships the Graph Filters panel, which the
        // legend below shares a filter with.
        mode: 'full',
        // Off: `full` mode brings a minimap and the data dock, and this card is about
        // neither (see the Minimap and Data table cards for those).
        minimap: false,
        table: false,
        legend,
        // Declaring a facet under the *same key* the legend uses makes the two one
        // control: toggle a swatch and the panel's multiselect follows, and the
        // other way round. (The trade-off: an empty multiselect means "no
        // constraint" to the panel, so the legend won't let you switch off the last
        // remaining category.)
        filter: {
            facets: [
                {
                    key: 'type', label: 'Service type', type: 'multiselect',
                    options: (graph) => [...new Set(graph.getNodes().map((node) => node.getData().type))]
                        .sort().map((value) => ({ label: value, value }))
                }
            ]
        }
    },
    render: {
        defaultNodeStyle: {
            size: 15,
            strokeColor: '#ffffff',
            textColor: '#334155',
            text: (node) => node.getData()?.name,
            textVerticalShift: -1.8,
            color: (node) => palette.getColor(node.getData()?.type)
        }
    }
}
js
// The legend announces every toggle on the data bus, so the choice can be
// persisted, mirrored elsewhere, or logged.
function watchLegend(graph) {
    graph.on('legendToggle', ({ section, hidden, visible }) => {
        console.log(`legend [${section}]: showing ${visible.join(', ') || '(none)'} — hiding ${hidden.join(', ') || '(none)'}`)
    })
}

// Swap the legend at runtime — a graph that started without one gets it built on
// the spot, and `undefined` removes it (clearing its filter with it).
function legendByType(graph) {
    graph.setLegend({ key: 'type', title: 'Service type' })
}

function removeLegend(graph) {
    graph.setLegend(false)
}

// Declared entries, for when the categories aren't a plain data key: supply the
// label, the colour and the predicate yourself. An array works too; a function is
// re-resolved whenever the data changes.
function legendByTier(graph) {
    graph.setLegend({
        title: 'Tier',
        entries: [
            {
                id: 'edge', label: 'Edge', color: '#0072B2',
                predicate: (node) => node.getData().type === 'web'
            },
            {
                id: 'service', label: 'Services', color: '#E69F00',
                predicate: (node) => node.getData().type === 'api'
            },
            {
                id: 'storage', label: 'Storage', color: '#009E73',
                predicate: (node) => ['database', 'cache'].includes(node.getData().type)
            }
        ]
    })
}

// Two keys at once: `sections` stacks one titled block per dimension in a single
// docked card. Each section filters on its own, and the filters *and* together —
// hide `api` above and `dmz` below and what is left is neither.
function legendByTypeAndZone(graph) {
    graph.setLegend({
        // `position` belongs to the card, not to a section. Top-left, because a
        // two-section card is tall enough to reach the mode rail in the corner the
        // single-key legend uses.
        position: 'top-left',
        sections: [
            // Still adopts the declared `type` facet, so this section and the filter
            // panel remain one control.
            { key: 'type', title: 'Service type' },
            {
                key: 'zone',
                title: 'Network zone',
                // The colours encode `type`, not `zone`, and the legend can only
                // sample colours — so this section declares swatches of its own
                // rather than showing one that would be a coincidence. `key` still
                // supplies the predicate.
                entries: [
                    { id: 'dmz', label: 'DMZ', color: '#CC79A7' },
                    { id: 'internal', label: 'Internal', color: '#56B4E9' }
                ]
            }
        ]
    })
}
js
// A service graph whose `type` drives both the colours and the legend. The counts
// differ per type on purpose — the legend reports them. `zone` is a second
// dimension the colours say nothing about — see the stacked legend below.
const data = {
    nodes: [
        { id: 'web-1', data: { name: 'Web app', type: 'web', zone: 'dmz' } },
        { id: 'web-2', data: { name: 'Mobile web', type: 'web', zone: 'dmz' } },
        { id: 'api-1', data: { name: 'Auth API', type: 'api', zone: 'internal' } },
        { id: 'api-2', data: { name: 'Orders API', type: 'api', zone: 'internal' } },
        { id: 'api-3', data: { name: 'Billing API', type: 'api', zone: 'internal' } },
        { id: 'db-1', data: { name: 'Postgres', type: 'database', zone: 'internal' } },
        { id: 'db-2', data: { name: 'Replica', type: 'database', zone: 'internal' } },
        { id: 'cache-1', data: { name: 'Redis', type: 'cache', zone: 'internal' } }
    ],
    edges: [
        { from: 'web-1', to: 'api-1' },
        { from: 'web-1', to: 'api-2' },
        { from: 'web-2', to: 'api-1' },
        { from: 'api-2', to: 'api-3' },
        { from: 'api-1', to: 'cache-1' },
        { from: 'api-2', to: 'db-1' },
        { from: 'api-3', to: 'db-1' },
        { from: 'db-1', to: 'db-2' }
    ]
}

Last updated:

Pager
Next pageGetting Started