Node Graph
Topology graph with force-directed auto-layout and selectable nodes.
Overview
The NodeGraph control visualizes a topology or network: each
GraphNode is drawn as a small labeled box and each GraphEdge as a
braille line connecting the boxes it references by Id. Positions are computed
automatically with a deterministic force-directed layout
(Fruchterman–Reingold) — you supply only nodes and edges, never coordinates, and the
same data always renders the same picture. Nodes are set as content; edges via the
NodeGraph.GraphEdges property element.
Properties
| Property | Type | Description |
|---|---|---|
GraphNodes | IList<GraphNode> | Inline node collection (content property). |
GraphEdges | IList<GraphEdge> | Inline edge collection, set via the NodeGraph.GraphEdges property element. |
GraphNodesSource | IEnumerable | Optional bound node collection; overrides GraphNodes when set. |
GraphEdgesSource | IEnumerable | Optional bound edge collection; overrides GraphEdges when set. |
SelectedIndex | int | Index of the selected node (-1 = none). Two-way bindable. |
SelectedNode | object | The selected node, kept in sync with SelectedIndex. Two-way bindable. |
LayoutIterations | int | Force-simulation budget, clamped to 1–200 (default 60). The layout stops earlier once it converges. |
ShowEdgeArrows | bool | Draw a direction marker (▶ ◀ ▲ ▼) at each edge’s target end, in the edge’s own color. Default True. |
Zoom | double | Magnification of the fitted layout, clamped to 1–10 (default 1 = fit-all). Two-way bindable. |
PanX | double | Horizontal offset of the viewport centre from the graph centre, in whole-graph widths (default 0). Clamped to ±(0.5 − 0.5/Zoom). Two-way bindable. |
PanY | double | Vertical pan; same units and clamp as PanX. |
Title | string | Optional heading drawn along the top edge. |
GraphNode fields: Id, Name (the box label; also
the text content), Value, Color. GraphEdge
fields: From, To (matching a node Id — unknown ids
are ignored) and Color (defaults to the axis color). GraphNode.Tag
carries an arbitrary payload so a selection leads straight back to the domain object.
ShowAxes, ShowGrid and ShowLegend have no effect on
this chart.
Data Binding
Bind GraphNodesSource and GraphEdgesSource to
ObservableCollections on your ViewModel — adding or removing items re-renders
and re-layouts automatically. Data classes and ViewModels must be public or
internal so the AOT source generator can bind them.
public sealed class TopologyViewModel : ViewModelBase
{
public ObservableCollection<GraphNode> Nodes { get; } =
[
new GraphNode { Id = "api", Name = "api" },
new GraphNode { Id = "db", Name = "postgres" },
];
public ObservableCollection<GraphEdge> Edges { get; } =
[
new GraphEdge { From = "api", To = "db" },
];
}
<NodeGraph GraphNodesSource="{Binding Nodes}"
GraphEdgesSource="{Binding Edges}" />
Interactivity
The graph is focusable. Give it focus (Tab, or click) and use the arrow keys to move the
selection through the nodes in declaration order — Home/End jump to the
first and last node — or click any box. The selected box is highlighted (brighter while
focused) and SelectedIndex/SelectedNode update two-way, ready to
drive a details pane.
<NodeGraph GraphNodesSource="{Binding Nodes}"
SelectedNode="{Binding Selected, Mode=TwoWay}" />
Zoom & Pan
By default the whole graph is fitted to the control, which turns a large topology into a
wall of touching boxes. Zoom magnifies the layout and PanX/PanY
choose which part of it the control shows, so a big graph can be explored in place instead
of being pre-filtered by the application. A terminal cannot draw a bigger glyph, so zoom
does not enlarge the boxes — it spreads the node positions apart and shows fewer of
them, which is what makes a crowded graph readable.
Zoom is anchored on the viewport centre: the pan is stored as that centre,
so changing the zoom is a pure change of scale and whatever the user is looking at stays
put. Pan is clamped so the viewport can never leave the graph, and at Zoom="1"
that clamp collapses to zero — fit-all renders exactly as it did before zoom existed, no
matter what is assigned to the pan properties.
<NodeGraph GraphNodesSource="{Binding Nodes}"
Zoom="{Binding Zoom, Mode=TwoWay}"
PanX="{Binding PanX, Mode=TwoWay}"
PanY="{Binding PanY, Mode=TwoWay}" />
ResetView() puts all three back to fit-all from code.
Keyboard Shortcuts
| Key | Action |
|---|---|
↑ / ← | Select the previous node. |
↓ / → | Select the next node. |
Home / End | Select the first / last node. |
| Left click | Select the clicked node box. |
+ / = | Zoom in one step (×1.25), about the viewport centre. |
- / _ | Zoom out one step, down to fit-all. |
0 | Reset the view to fit-all. |
Shift+↑ ↓ ← → | Pan by a tenth of the viewport. The bare arrows keep moving the selection. |
| Mouse wheel | Zoom in / out, about the viewport centre. |
Key Concepts
- Layout is deterministic: positions are seeded on a circle by node index and refined by a force simulation, so identical data renders identically.
- Edges are drawn with braille sub-cell lines (2×4 dots per cell) and pass beneath the node boxes.
- Node border colors come from the chart palette, overridable per node with
Color; labels are truncated with an ellipsis. - Edges reference nodes by string
Id; edges with unknown endpoints are silently ignored. - Layout is cached and recomputed only when the node/edge set changes; graphs beyond 500 nodes show a "… N more" notice instead of hanging on the O(n²) simulation.
- Zoom and pan are applied when that cached layout is projected onto the plot, alongside the box packing. They never enter the layout signature, so changing the view re-projects the picture without re-running the simulation and without reshuffling it.
- Box packing, edge labels, arrowheads and the mouse hit-test all operate on the projected rects, so they follow the zoomed view; boxes pushed past the viewport are clipped to the plot rather than drawn over the title row.