Network Graph
Nodes and edges in 2D — for correlation networks, lead-lag flows, Louvain community
visualisation, and minimum spanning trees. Three deterministic layouts ship in v1.10
PR 1: Manual (pass-through coords), Circular (unit-circle), Hierarchical (BFS top-down).
ForceDirected (Fruchterman–Reingold) is reserved at enum ordinal 1 and lands in PR 2.
Basic graph
GraphNode[] nodes =
[
new("AAPL"),
new("MSFT"),
new("GOOG"),
];
GraphEdge[] edges =
[
new("AAPL", "MSFT"),
new("AAPL", "GOOG"),
new("MSFT", "GOOG"),
];
Plt.Create()
.AddSubPlot(1, 1, 1, ax => ax.NetworkGraph(nodes, edges))
.ToSvg();
The default Circular layout places nodes evenly on the unit circle; no need to supply
coordinates yourself.
Directed edges with weights
GraphEdge[] edges =
[
new("AAPL", "MSFT", Weight: 0.85, IsDirected: true),
new("AAPL", "GOOG", Weight: 0.42, IsDirected: true),
new("MSFT", "GOOG", Weight: 0.55, IsDirected: false),
];
Plt.Create()
.AddSubPlot(1, 1, 1, ax => ax.NetworkGraph(nodes, edges, s =>
{
s.ShowEdgeWeights = true;
s.EdgeThicknessScale = 4.0; // multiplier on per-edge Weight → stroke width
}))
.ToSvg();
IsDirected edges get an arrowhead at the target end, reusing
ArrowHeadBuilder.FancyArrow for visual consistency with annotation arrows.
Per-node colour and size
GraphNode[] nodes =
[
new("AAPL", ColorScalar: 0.2, SizeScalar: 1.5), // small, dark colour-map sample
new("MSFT", ColorScalar: 0.5, SizeScalar: 2.5),
new("GOOG", ColorScalar: 0.8, SizeScalar: 3.0), // large, bright sample
];
Plt.Create()
.AddSubPlot(1, 1, 1, ax => ax.NetworkGraph(nodes, edges, s =>
{
s.ColorMap = ColorMaps.Viridis;
s.NodeRadiusScale = 8.0; // multiplier on per-node SizeScalar
}))
.ToSvg();
ColorScalar is mapped through ColorMap (defaults to Viridis); the renderer clamps to
[0, 1]. SizeScalar × NodeRadiusScale = pixel radius.
Layouts
Circular (default)
All nodes on the unit circle at evenly-spaced angles. Ignores edges, ignores any
pre-set X/Y. O(N), deterministic. Good first pick for any small graph.
Hierarchical
BFS top-down layering from node 0. Depth becomes Y; within-depth order becomes X (centred around 0, single-node layer at X=0). Cycles are tolerated via the visited set; disconnected components stay at depth 0.
s.Layout = GraphLayout.Hierarchical;
Manual
Pass-through: each node's pre-set X / Y is used verbatim. Use this when you have
a custom layout algorithm of your own (e.g. from a t-SNE embedding) and want
NetworkGraph to render-only.
GraphNode[] nodes =
[
new("a", X: 0, Y: 0),
new("b", X: 1, Y: 1),
new("c", X: 2, Y: 0),
];
Plt.Create()
.AddSubPlot(1, 1, 1, ax => ax.NetworkGraph(nodes, edges, s =>
{
s.Layout = GraphLayout.Manual;
}))
.ToSvg();
ForceDirected (Fruchterman–Reingold spring-embedder)
Repulsive force k²/d between every pair of nodes; attractive spring force d²/k on
each edge; k = √(area/N). Random initial positions seeded by LayoutSeed (default 0)
for bit-identical reproducibility across runs. Step size cools linearly across iterations.
Final positions are normalised to fit [-1, 1]².
Plt.Create()
.AddSubPlot(1, 1, 1, ax => ax.NetworkGraph(nodes, edges, s =>
{
s.Layout = GraphLayout.ForceDirected;
s.LayoutSeed = 42; // any int — same seed → identical layout
s.LayoutIterations = 100; // default 50; higher = better quality at quadratic cost
s.ConvergenceThreshold = 0.5; // optional: early-stop when energy delta drops below
}))
.ToSvg();
⚠️ Performance cliff. ForceDirected is O(N² × iterations) for the repulsive pass. At default 50 iterations the practical limit is roughly N ≤ 500. Beyond that, render time grows quadratically — N=1000 takes seconds; N=5000 takes minutes. For larger graphs switch to
GraphLayout.Hierarchical(O(N + E)) orGraphLayout.Circular(O(N)). Edge density also matters: dense graphs (E ≈ N²/2) add measurable spring-force overhead on top of the always-quadratic repulsion. SeeBenchmarks/MatPlotLibNet.Benchmarks/NetworkGraphBenchmarks.csfor the full N × edge-density matrix.
Seeded determinism. Two figures rendered with the same LayoutSeed, LayoutIterations,
and ConvergenceThreshold produce byte-identical SVG. This is what makes ForceDirected
testable and what unblocks "snapshot" workflows where the layout must be reproducible
across builds.
Convergence-mode early-stop. Set ConvergenceThreshold to a positive value and the
loop exits as soon as the per-iteration total displacement-energy drops below that
threshold. Sparse / well-separated topologies (e.g. clear cluster structure) converge in
10–20 iterations and benefit; dense uniform graphs typically don't converge below any
reasonable threshold and run the full LayoutIterations count.
DataFrame extension
For edge-list DataFrames, df.NetworkGraph(...) derives nodes from the union of
distinct values in the source/target columns (first-seen order):
using MatPlotLibNet;
using Microsoft.Data.Analysis;
DataFrame df = ...; // columns: source, target, weight, directed
string svg = df.NetworkGraph(
edgeFromCol: "source",
edgeToCol: "target",
weightCol: "weight",
directedCol: "directed")
.WithTitle("Asset correlation network")
.ToSvg();
weightCol and directedCol are optional. Missing nodes (edges that reference an ID
not in the union) are silently skipped at render time — defensive fallback rather
than throwing.
Configuration reference
| Property | Type | Default | Effect |
|---|---|---|---|
Nodes |
IReadOnlyList<GraphNode> |
(constructor arg) | The graph's nodes. |
Edges |
IReadOnlyList<GraphEdge> |
(constructor arg) | The graph's edges. |
Layout |
GraphLayout |
Circular |
Manual / Circular / Hierarchical / ForceDirected. |
ColorMap |
IColorMap? |
null → Viridis |
Per-node colour from ColorScalar. |
ShowNodeLabels |
bool |
true |
Render Label (or Id) next to each node. |
ShowEdgeWeights |
bool |
false |
Render numeric weight on top of each edge. |
EdgeThicknessScale |
double |
1.0 |
Multiplier on per-edge Weight → stroke width. |
NodeRadiusScale |
double |
5.0 |
Multiplier on per-node SizeScalar → circle radius. |
LayoutSeed |
int |
0 |
Seed for ForceDirected RNG. Ignored by deterministic layouts. |
LayoutIterations |
int |
50 |
Max iterations for ForceDirected (O(N²) per iteration). Ignored by deterministic layouts. |
ConvergenceThreshold |
double? |
null |
Optional early-stop for ForceDirected when per-iter energy drops below this. Null = run full LayoutIterations. |
See also
- Sankey Diagrams — flow visualisation when edges have explicit thicknesses
- Treemaps — hierarchical containers when the relationship is strictly nesting
- Dendrograms — hierarchical clustering trees with merge distances