Legend
Built-in canvas legend mapping node types to colors and shapes. Manual or auto-generated.
Manual Legend
const app = create(container, {
nodes: [...],
edges: [...],
layout: 'force',
theme: 'dark',
legend: {
title: 'Node Types',
position: 'bottom-left',
items: [
{ label: 'Server', color: '#3b82f6', shape: 'rect' },
{ label: 'Database', color: '#8b5cf6', shape: 'circle' },
{ label: 'Client', color: '#10b981', shape: 'diamond' },
{ label: 'Gateway', color: '#f59e0b', shape: 'hexagon' },
],
},
}); Auto-Generated Legend
const app = create(container, {
nodes: [...],
edges: [...],
layout: 'force',
legend: {
groupBy: 'data.type',
position: 'bottom-left',
title: 'Categories',
},
}); Automatically lists every unique value from the specified field with its assigned color.
Options
| Option | Type | Default | Description |
|---|---|---|---|
title | string | 'Legend' | Title text. |
position | string | 'top-left' | 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right' |
items | LegendItem[] | [] | Manual legend entries. |
groupBy | string | undefined | Dot-path or callback for auto-grouping nodes by category. |
groupEdgesBy | string | undefined | Dot-path or callback for auto-grouping edges. See Edge-type filter. |
maxItems | number | 10 | Max entries (extra collapsed as "+ N more"). |
collapsed | boolean | false | Start collapsed. |
onItemClick | (item, groupKey) => void | undefined | Click handler for legend items. Always wins over clickToToggle. |
clickToToggle | boolean | true | When true and onItemClick is unset, clicking a row toggles visibility of all nodes in that group. |
fadedNodeOpacity | number | 0.25 | Opacity applied to nodes whose visible incident edge count drops to zero from edge-type filtering. |
LegendItem
| Property | Type | Description |
|---|---|---|
label | string | Display text. |
color | string | Color swatch. |
shape | string | Shape indicator. |
count | number | Optional count beside label. |
Interactive Legend
const app = create(container, {
nodes: [...],
edges: [...],
legend: {
groupBy: 'data.type',
position: 'bottom-left',
onItemClick: (item) => {
app.graph.highlightByPredicate(
(node) => node.data.type === item.label,
{ dimOpacity: 0.1 }
);
setTimeout(() => app.graph.resetHighlight(), 3000);
},
},
}); Clickable Legend & Pinned Focus
By default (clickToToggle: true), clicking a legend row
toggles visibility of every node in that group. Toggling a group off
also hides every edge with either endpoint in that group — even
cross-category edges. A second click restores the group; the legend
tracks an internal legend-hidden set that is separate from any
explicit visible: false the host code may have set, so
toggling off-then-on never accidentally un-hides user-hidden nodes.
An explicit onItemClick handler always wins over the
default toggle. Use it for spotlights or to drive your own focus
behavior.
// Toggle a category programmatically
app.legend.toggleCategory('database');
// Pinned-focus events fire on click (pinned) and on second click / outside-click (released)
app.graph.on('focus:pin', ({ nodeId, hops }) => {
console.log('Pinned focus on', nodeId, 'with', hops, 'hops of context');
});
app.graph.on('focus:release', () => {
console.log('Focus released');
}); Edge-Type Filter
Set groupEdgesBy (or rely on auto-detect from
type → category → role → kind → class)
to grow a second legend section with one row per edge category.
Clicking an edge category toggles all edges of that group between
visible and faded — edges don't disappear, they fade to
fadedNodeOpacity (default 0.25) so the graph
keeps its spatial structure.
app.legend.toggleEdgeCategory('auth');
app.graph.on('focus:edge-filter', ({ hiddenTypes }) => {
console.log('Hidden edge categories:', hiddenTypes);
});
See Filtering →
Edge-type filter for the full behavior, including how it interacts
with hideOrphans.
Minimap Clearance
When a Minimap is mounted on the
same vertical edge as the legend (e.g. both bottom-left),
the legend body's max-height self-shrinks to keep them
from overlapping. The cap is floored at 80 px, so very
short containers produce a scrollable legend rather than a clipped
minimap. On opposite edges, the legend uses its CSS default
(240 px, or 180 px in compact mode).
The sizing helper is also exported as a pure function for testing:
import { computeLegendBodyMaxHeight } from '@topokit/renderer-canvas';
const cap = computeLegendBodyMaxHeight({
legendPosition: 'bottom-left',
minimapPosition: 'bottom-left',
containerHeight: 600,
minimapHeight: 120,
headerHeight: 28,
});
// → number (px) | null. Null when CSS default already prevents overlap. Runtime Updates
// Update legend
app.setLegend({
title: 'Updated Legend',
items: [
{ label: 'Active', color: '#10b981' },
{ label: 'Inactive', color: '#6b7280' },
],
});
// Remove legend
app.setLegend(null); Next Steps
- Node Badges -- status indicators on nodes
- Minimap & UI Components -- navigation and tooling
- Data Mapping -- auto-generate from mapped data