Installation
import { WrGraph, WrGraphNodeTemplate } from 'ngwr/graph';
// WrGraphNodeTemplate is the <ng-template wrGraphNode> directive in the custom
// node examples below — the selector is wrGraphNode, and imports: [] takes the
// class. A graph that draws its default cards needs WrGraph alone.
@Component({ imports: [WrGraph, WrGraphNodeTemplate] })
export class MyComponent {}@use 'ngwr/graph';Nodes and edges
nodes are the blocks and edges the links, each { from, to } with from the parent and to the child. Without a template every node is a card showing its label. Parents sit above their children, with one exception: no order can put every parent in a cycle above its child, so an edge that closes a cycle climbs back up instead. Every line, that one included, ends in an arrow at the child. The same arrays always give the same picture — on the server, in the browser, run after run — and under dir="rtl" the horizontal order mirrors. An edge that names a node which does not exist is skipped, with a warning in dev mode, and neither array is ever written to.
<wr-graph [nodes]="nodes" [edges]="edges" ariaLabel="Writing workflow" />Custom nodes
ng-template wrGraphNode replaces the default card, and the node arrives as let-node with whatever you put in data. The box stays the graph's: its size is what the layout reserved — nodeWidth / nodeHeight for every node, or a node's own width / height, as the three teams use here — so a template fills that box rather than sizing it. Render node.label somewhere in it, because every neighbour's relation text names this node by its label. Below, Platform has two heads and Dev Sandoval reports to both, while Noor Haddad belongs to Design and Research at once. At most widths this chart is wider than the column, and it scrolls inside its own viewport rather than the page.
<wr-graph [nodes]="nodes" [edges]="edges" [nodeWidth]="176" [nodeHeight]="56" ariaLabel="Team structure">
<ng-template wrGraphNode let-node>
@if (node.data.kind === 'unit') {
<div class="unit">{{ node.label }}</div>
} @else {
<div class="person">
<wr-avatar class="person__avatar" [size]="32" shape="circle">
<span aria-hidden="true">{{ node.data.initials }}</span>
</wr-avatar>
<div class="person__text">
<span class="person__name">{{ node.label }}</span>
<span class="person__role">{{ node.data.role }}</span>
</div>
</div>
}
</ng-template>
</wr-graph>Shared dependencies
A package graph reads the same way: the parent is the package that depends, the child is its dependency. @example/signals is shared by four packages, one of them two layers up, and that longer line passes between the nodes of the layer it crosses rather than through one. The data does not have to be a clean hierarchy either: several edges between one pair of nodes, or an edge that closes a cycle, are laid out without breaking the picture.
<wr-graph [nodes]="nodes" [edges]="edges" [nodeWidth]="184" [nodeHeight]="52" ariaLabel="Dependencies">
<ng-template wrGraphNode let-node>
<div class="package">
<code class="package__name">{{ node.label }}</code>
<span class="package__version">{{ node.data.version }}</span>
</div>
</ng-template>
</wr-graph>Accessibility
What the lines show is also written out. Every node is a list item, and after its own content comes a visually hidden sentence naming its parents and then its children, in reading order — so a screen reader hears the structure without the drawing. A node with no edges gets no sentence. The lines and arrowheads are decorative and hidden from assistive technology. The viewport is the one tab stop: a role="group" named by ariaLabel (falling back to the graph.label catalog entry), and focusable so a keyboard can scroll a graph that is larger than its box. Give each graph its own ariaLabel when a page holds more than one.
<!-- What the team structure above exposes. The SVG holding the lines is aria-hidden,
and each <span> is visually hidden. -->
<div role="group" tabindex="0" aria-label="Team structure">
<div role="list">
<div role="listitem">Platform <span>Children: Mira Okafor and Tomas Lindqvist.</span></div>
<!-- … -->
<div role="listitem">Noor Haddad Product designer <span>Parents: Design and Research.</span></div>
<!-- … -->
<div role="listitem">Dev Sandoval Engineer <span>Parents: Mira Okafor and Tomas Lindqvist.</span></div>
</div>
</div>What it does not do
The graph is a picture with its relations written out, and nothing more. There is no selection or highlighting, no keyboard movement from one node to the next, and no zoom, fit or panning beyond the viewport's own scrolling. Nothing is editable: nodes cannot be dragged, added, collapsed or grouped. The layout runs top to bottom only, with no left-to-right direction, and lines carry no labels. Node sizes are never measured, so nothing is fitted to its box: the default card cuts off a label that does not fit, and a custom template whose content outgrows its box spills over its neighbours — size the box with nodeWidth / nodeHeight or a node's own width / height instead.
API
| Name | Description | Type | Default |
|---|---|---|---|
nodes | The nodes, in any order. Input order breaks layout ties, so the same arrays always produce the same picture. Never mutated. | readonly WrGraphNode<TData>[] | [] |
edges | Parent-child links, from the parent and to the child. Never mutated. | readonly WrGraphEdge[] | [] |
nodeWidth | Width in pixels of a node that sets none of its own. | number | 160 |
nodeHeight | Height in pixels of a node that sets none of its own. | number | 48 |
ariaLabel | Accessible name of the scrolling viewport. Falls back to graph.label, then 'Graph'. | string | null | null |
Types
The data shapes the inputs and the node template take.
| Name | Description | Type | Default |
|---|---|---|---|
WrGraphNode | One block. TData is whatever data carries. | interface | — |
idrequired | Unique within the graph. A repeated id is skipped after its first occurrence, with a warning in dev mode. | string | — |
labelrequired | The node’s name. The default card shows it, and every neighbour’s relation text reads it out — so a custom template should render it too. | string | — |
width | Width in pixels. Falls back to nodeWidth when absent, non-finite or not positive. | number | — |
height | Height in pixels. Falls back to nodeHeight when absent, non-finite or not positive. | number | — |
data | Anything the node template needs. Never read by the graph or its layout. | TData | — |
WrGraphEdge | A parent-child link, drawn with its arrow at the child. Several edges between one pair, and edges that close a cycle, are both legal. | interface | — |
fromrequired | Id of the parent. An edge naming a node that does not exist is skipped, with a warning in dev mode. | string | — |
torequired | Id of the child. | string | — |
WrGraphNodeContext | What an ng-template wrGraphNode receives. | interface | — |
$implicitrequired | The node, bound with let-node. | WrGraphNode<TData> | — |
CSS variables
Custom properties ngwr/graph publishes. Each default below is declared on the component's own selector, so a :root override is shadowed by it — set them on that selector, on a wrapper you scope yourself, or inline on the element. Unlike the BEM class names, these are the supported way to restyle the component.
| Variable | Default | Declared on |
|---|---|---|
--wr-graph-background | var(--wr-color-surface) | .wr-graph |
--wr-graph-card-background | var(--wr-color-surface) | .wr-graph |
--wr-graph-card-border | var(--wr-color-outline) | .wr-graph |
--wr-graph-card-color | var(--wr-color-on-surface) | .wr-graph |
--wr-graph-card-font-size | var(--wr-text-sm) | .wr-graph |
--wr-graph-card-padding | 0.5rem 0.75rem | .wr-graph |
--wr-graph-card-radius | var(--wr-border-radius-base) | .wr-graph |
--wr-graph-edge-color | color-mix(in srgb, var(--wr-color-on-surface-muted) 60%, var(--wr-color-surface)) | .wr-graph |
--wr-graph-edge-width | 1px | .wr-graph |
--wr-graph-padding | 1rem | .wr-graph |