Diagram
DiagramView draws a graph of shapes and connections: an org chart, a decision tree, a
flowchart, a mindmap. It lays the graph out for you, routes the lines so they meet the real outline of
each shape, and gives you a surface to pan, zoom, select on, drag nodes around and author connections
in.
MAUI
| Decision tree | Flowchart with a retry loop |
|---|---|
![]() |
![]() |
Blazor
| Mind map | Org chart |
|---|---|
![]() |
![]() |
Not the Mermaid control
Section titled “Not the Mermaid control”Mermaid Diagrams renders a picture from mermaid text. This renders a graph you bind to and edit. If your source of truth is markup, use Mermaid; if it is a collection of nodes — rows from a database, a view model — use this one.
One engine, two hosts
Section titled “One engine, two hosts”A diagram is mostly arithmetic. Packing a tidy tree so a parent sits centred over its children and no
two subtrees collide, assigning layers to a graph that loops back on itself, working out where a line
leaving a diamond actually crosses its edge — none of it wants to know whether it is drawing into a
MAUI canvas or an SVG viewBox.
So it doesn’t. Shiny.Controls.Diagram.Shared owns the model, all five layouts, the connector
routing and the shape geometry — down to the exact box every node occupies and the exact polyline
every connection follows. Both controls are thin: they turn gestures into a DiagramEditPlan and
paint what the engine tells them to.
A layout that resolved differently on the two hosts would be a bug neither host’s own tests would notice, and this removes the possibility rather than testing for it. The package is dependency-free, trimmable and AOT-clean, and is perfectly usable with no UI at all.
Getting started
Section titled “Getting started”dotnet add package Shiny.Maui.Controls.Diagram# ordotnet add package Shiny.Blazor.Controls.Diagramusing Shiny.Controls.Diagramming;
var nodes = new ObservableCollection<DiagramNode>{ new("start", "Ticket raised") { Shape = DiagramNodeShape.Stadium }, new("paid", "Paid plan?") { Shape = DiagramNodeShape.Diamond }, new("queue", "Support queue"), new("forum", "Community forum")};
var connections = new ObservableCollection<DiagramConnection>{ new("start", "paid"), new("paid", "queue", "Yes"), new("paid", "forum", "No")};<shiny:DiagramView Nodes="{Binding Nodes}" Connections="{Binding Connections}" LayoutKind="Layered" AllowNodeDrag="True" AllowConnectionEdit="True" /><div style="height:520px"> <DiagramView Nodes="nodes" Connections="connections" LayoutKind="DiagramLayoutKind.Layered" AllowNodeDrag="true" AllowConnectionEdit="true" /></div>The Blazor component fills its parent and needs a bounded height, the same as GanttView.
The model
Section titled “The model”Nodes are DiagramNode, connections are DiagramConnection — concrete types rather than a generic
TItem, because the engine writes layout results back onto them. Put your own object in
DiagramNode.Item and bind a template to it.
Both collections are watched. An ObservableCollection redraws on add and remove, and every node and
connection is watched for property changes, so editing the source redraws without calling anything.
Hierarchy, either way round
Section titled “Hierarchy, either way round”Nest nodes in Children and the parent link maintains itself; or hand over a flat list where each
node carries a ParentId and the model reassembles the tree. Mixing the two is fine. Charts arrive
from a database as rows and from a view model as a tree, and forcing a conversion on the consumer is
how a control ends up with two half-supported paths.
A hierarchy with no connections of its own still draws its links. An org chart handed over as nested nodes has parents and children and no edges; without this it would draw as a tidy grid of boxes joined by nothing, which looks like the control failed rather than like a deliberate absence. Declaring a connection between the same two nodes replaces the implicit one — that is how you add a label or a different arrowhead.
Shapes, and why the outline matters
Section titled “Shapes, and why the outline matters”DiagramNodeShape is the flowchart vocabulary rather than a general shape library: Rectangle,
RoundedRectangle, Stadium, Circle, Ellipse, Diamond, Parallelogram, Hexagon, Cylinder,
Document, Triangle. Each one means something to a reader of a flowchart.
Anchoring and hit testing both work against the real outline, not the bounding box. That single detail is most of what makes a decision tree look drawn rather than approximated: with a box anchor, every arrow into a diamond stops short in mid-air at the corner of an invisible rectangle, and a click in that same empty corner selects the node.
Validation reports, it does not throw
Section titled “Validation reports, it does not throw”Real data routinely contains a dangling connection, a duplicate id or a parent chain that loops. A
control that threw on one would be unusable against it, and one that silently dropped it would be
worse — so the diagram still draws and DiagramModel.Issues says what was wrong. Read it from the
Built event (MAUI) or OnBuilt (Blazor), because nothing else will tell you.
Saving
Section titled “Saving”DiagramJson.Save and DiagramJson.Load round-trip the graph, the shapes and hand-placed positions
through a flat pair of lists. Routes, depths and selection are not saved — they are derived on the
next rebuild, so saving them would only create the possibility of a file disagreeing with itself.
Serialization is source-generated, so it survives a trimmed WebAssembly publish.
Load into LayoutKind = None to keep the saved positions.
- Layouts — the five arrangements and when each is the right one
- Editing & Undo — plans, cancellation and the undo stack






