Hierarchical charts

edit

Build a tree

The hierarchical marks draw a tree of HierarchyNode<T>. Hierarchy builds one from nested objects, from rows that give the identifier of their parent, or from paths. Pass a value to compute the value of every node: its own value plus the values of its descendants. NaN, infinite and negative values count as zero, and a sum larger than double.MaxValue is double.MaxValue. SortByValue sorts the children by decreasing value, which gives better treemaps and packings.

public sealed record Department(string Name, double People, IReadOnlyList<Department> Teams);
var tree = new Department("Company", 0,
[
    new("Engineering", 0, [new("Platform", 12, []), new("Product", 18, []), new("Data", 7, [])]),
    new("Sales", 0, [new("Europe", 9, []), new("Americas", 11, [])]),
    new("Support", 6, []),
]);

var root = Hierarchy.FromTree(tree, children: department => department.Teams, name: department => department.Name, value: department => department.People);
Assert.Equal(63, root.Value);

var plot = new Plot { Title = "People by team", Width = 400, Height = 440 };
plot.Add(new PackMark<Department>(root) { Padding = 4 });
var rows = new[]
{
    (Id: "root", Parent: (string?)null, Size: 0.0),
    (Id: "a", Parent: "root", Size: 0.0),
    (Id: "a1", Parent: "a", Size: 3.0),
    (Id: "a2", Parent: "a", Size: 5.0),
    (Id: "b", Parent: "root", Size: 4.0),
};
var root = Hierarchy.Stratify(rows, row => row.Id, row => row.Parent, row => row.Size);

The examples below build the tree from file paths:

public sealed record FileEntry(string Path, double Size);

The nodes are colored by their top-level group, unless the Fill channel of the mark says otherwise.

Packed circles

A PackMark<T> packs the leaves as circles whose areas are proportional to their values. The layout is deterministic.

FileEntry[] files =
[
    new("src/app/main.cs", 120), new("src/app/view.cs", 80), new("src/app/model.cs", 60),
    new("src/core/parser.cs", 140), new("src/core/lexer.cs", 90), new("src/core/ast.cs", 45),
    new("tests/app.tests.cs", 70), new("tests/core.tests.cs", 85),
    new("docs/readme.md", 40), new("docs/guide.md", 55),
];

var root = Hierarchy.FromPaths(files, file => file.Path, value: file => file.Size).SortByValue();
var plot = new Plot { Title = "Size of the files", Width = 480, Height = 520 };
plot.Add(new PackMark<FileEntry?>(root));

Circles of files packed in the circles of their folders

Treemap

A TreemapMark<T> divides the area into rectangles. PaddingTop leaves room for the names of the groups. The default tiling is squarified; TreemapTiling has the others.

var root = Hierarchy.FromPaths(SampleData.Files, file => file.Path, value: file => file.Size).SortByValue();
var plot = new Plot { Title = "Size of the files" };
plot.Add(new TreemapMark<FileEntry?>(root) { PaddingTop = 18, PaddingOuter = 2, PaddingInner = 2 });
plot.Legend.Visible = false;

Rectangles of files nested in the rectangles of their folders

Sunburst

A PartitionMark<T> in polar coordinates draws a ring per depth.

var root = Hierarchy.FromPaths(SampleData.Files, file => file.Path, value: file => file.Size).SortByValue();
var plot = new Plot
{
    Title = "Size of the files",
    Width = 480,
    Height = 480,
    Coordinates = new PolarCoordinates { InnerRadius = 0.2 },
};
plot.Add(new PartitionMark<FileEntry?>(root));

Rings of folders and files around a hole

Icicle

The same mark in Cartesian coordinates draws a layer per depth: from the top down, or from the left when transposed.

var root = Hierarchy.FromPaths(SampleData.Files, file => file.Path, value: file => file.Size).SortByValue();
var plot = new Plot { Title = "Size of the files", Coordinates = new CartesianCoordinates { Transposed = true } };
plot.Add(new PartitionMark<FileEntry?>(root) { ShowRoot = true, Label = node => node.Depth is 0 ? "all" : node.Name });
plot.Legend.Visible = false;

Columns of folders and files from left to right

Tree diagram

A TreeMark<T> draws a node-link diagram with the tidy tree layout. The root is at the top in Cartesian coordinates, and on the left when they are transposed.

var root = Hierarchy.FromPaths(SampleData.Files, file => file.Path);
var plot = new Plot { Title = "Files of the repository", Height = 420, Coordinates = new CartesianCoordinates { Transposed = true } };
plot.Add(new TreeMark<FileEntry?>(root) { Label = node => node.Depth is 0 ? "repository" : node.Name });
plot.Legend.Visible = false;

A tree of folders and files from left to right

Radial dendrogram

With TreeLayoutKind.Cluster, the leaves are all at the deepest level: a dendrogram. In polar coordinates, the tree is radial.

var root = Hierarchy.FromPaths(SampleData.Files, file => file.Path);
var plot = new Plot { Title = "Files of the repository", Width = 520, Height = 520, Coordinates = new PolarCoordinates() };
plot.Add(new TreeMark<FileEntry?>(root) { Layout = TreeLayoutKind.Cluster });
plot.Legend.Visible = false;

A radial tree of files whose leaves are on a circle

Layouts

The layouts are public, to draw trees yourself: PackLayout, TreemapLayout and PartitionLayout return the circle or the rectangle of every node, TreeLayout and ClusterLayout its point.

var root = Hierarchy.FromPaths(["a/x", "a/y", "b"], path => path, value: path => path.Length).SortByValue();
var layout = new TreemapLayout { Width = 300, Height = 200, Tiling = tiling };
var rectangles = layout.Layout(root);
var root = Hierarchy.FromPaths(["a/1", "a/2", "a/3", "b", "c/1/x", "c/1/y", "c/2"], path => path);
var points = new TreeLayout { Width = 600, Height = 300 }.Layout(root);