Skip to content

Commit

Permalink
Implement Mermaid charting and graph functionalities
Browse files Browse the repository at this point in the history
This commit introduces the ability to generate Mermaid diagrams using C# language. It incorporates new classes such as XyChart, Timeline, and Flowchart. Furthermore, it implements tests for these features to guarantee their correctness. Noticeably, it supports customizing different properties of graphs, such as the shape of nodes in a flowchart, and the direction of links. Useful additions include an 'IMermaidable' interface and numerous helper classes like Node, Link, Subgraph, and Series.
  • Loading branch information
frankhaugen committed Apr 28, 2024
1 parent 8d7ebbf commit b4396f2
Show file tree
Hide file tree
Showing 24 changed files with 769 additions and 14 deletions.
104 changes: 104 additions & 0 deletions Frank.Mermaid.Tests/FlowchartTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
using Frank.Mermaid.Flowchart;
using Frank.Mermaid.Timeline;
using Xunit.Abstractions;

namespace Frank.Mermaid.Tests;

public class FlowchartTests
{
private readonly ITestOutputHelper _outputHelper;

public FlowchartTests(ITestOutputHelper outputHelper)
{
_outputHelper = outputHelper;
}

[Fact]
public void Test1()
{
var flowchart = new Flowchart.Flowchart();
var node1 = new Node("Node 1");
var node2 = new Node("Node 2");
flowchart.AddNode(node1);
flowchart.AddNode(node2);

var link = new Link(node1, node2);
flowchart.AddLink(link);

var subgraph = new Subgraph("Subgraph 1", Direction.TopToBottom);
var subgraphNode1 = new Node("Subgraph Node 1");
var subgraphNode2 = new Node("Subgraph Node 2");
subgraph.AddNode(subgraphNode1);
subgraph.AddNode(subgraphNode2);

var subgraphLink = new Link(subgraphNode1, subgraphNode2, "Subgraph Link 1");
subgraph.AddLink(subgraphLink);

var link2 = new Link(subgraph, node2, "Subgraph Link to Node 2");
flowchart.AddLink(link2);

flowchart.AddSubgraph(subgraph);

var writer = flowchart.ToMermaidSyntax();
var result = writer.ToString();

_outputHelper.WriteLine(result);
}

[Fact]
public void Test2()
{
/*
Testcase:
flowchart LR
subgraph subgraph1
direction TB
top1[top] --> bottom1[bottom]
end
subgraph subgraph2
direction TB
top2[top] --> bottom2[bottom]
end
%% ^ These subgraphs are identical, except for the links to them:
%% Link *to* subgraph1: subgraph1 direction is maintained
outside --> subgraph1
%% Link *within* subgraph2:
%% subgraph2 inherits the direction of the top-level graph (LR)
outside ---> top2
*/

var flowchart = new Flowchart.Flowchart();

var subgraph1 = new Subgraph("subgraph1", Direction.TopToBottom);
var top1 = new Node("top");
var bottom1 = new Node("bottom");
subgraph1.AddNode(top1);
subgraph1.AddNode(bottom1);
var link1 = new Link(top1, bottom1);
subgraph1.AddLink(link1);
flowchart.AddSubgraph(subgraph1);

var subgraph2 = new Subgraph("subgraph2", Direction.TopToBottom);
var top2 = new Node("top");
var bottom2 = new Node("bottom");
subgraph2.AddNode(top2);
subgraph2.AddNode(bottom2);
var link2 = new Link(top2, bottom2);
subgraph2.AddLink(link2);
flowchart.AddSubgraph(subgraph2);

var outside = new Node("outside");
flowchart.AddNode(outside);
var link3 = new Link(outside, subgraph1);
flowchart.AddLink(link3);

var link4 = new Link(outside, top2);
flowchart.AddLink(link4);

var writer = flowchart.ToMermaidSyntax();
var result = writer.ToString();

_outputHelper.WriteLine(result);
}
}
39 changes: 39 additions & 0 deletions Frank.Mermaid.Tests/TimelineTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
using Frank.Mermaid.Timeline;
using Xunit.Abstractions;

namespace Frank.Mermaid.Tests;

public class TimelineTests
{
private readonly ITestOutputHelper _outputHelper;

public TimelineTests(ITestOutputHelper outputHelper)
{
_outputHelper = outputHelper;
}

[Fact]
public void Test1()
{
var timeline = new Timeline.Timeline("My Timeline");
var section1 = new Section("Section 1");
section1.AddEvent(new Event("Event 1", new DateTime(2022, 1, 1)));
section1.AddEvent(new Event("Event 2", new DateTime(2022, 1, 2)));
timeline.AddSection(section1);

var section2 = new Section("Section 2");
section2.AddEvent(new Event("Event 3", new DateTime(2022, 1, 3)));
section2.AddEvent(new Event("Event 4", new DateTime(2022, 1, 4)));
timeline.AddSection(section2);

var section3 = new Section("Section 3");
section3.AddEvent(new Event("Event 5", new DateTime(2022, 1, 5)));
section3.AddEvent(new Event("Event 6", new DateTime(2022, 1, 6)));
timeline.AddSection(section3);

var writer = timeline.ToMermaidSyntax();
var result = writer.ToString();

_outputHelper.WriteLine(result);
}
}
9 changes: 0 additions & 9 deletions Frank.Mermaid.Tests/UnitTest1.cs

This file was deleted.

5 changes: 0 additions & 5 deletions Frank.Mermaid/Class1.cs

This file was deleted.

10 changes: 10 additions & 0 deletions Frank.Mermaid/Flowchart/Direction.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
namespace Frank.Mermaid.Flowchart;

public enum Direction
{
TopToBottom,
TopDown,
BottomToTop,
RightToLeft,
LeftToRight
}
17 changes: 17 additions & 0 deletions Frank.Mermaid/Flowchart/DirectionExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
namespace Frank.Mermaid.Flowchart;

public static class DirectionExtensions
{
public static string ToMermaidSyntax(this Direction direction)
{
return direction switch
{
Direction.TopToBottom => "TB",
Direction.TopDown => "TD",
Direction.BottomToTop => "BT",
Direction.RightToLeft => "RL",
Direction.LeftToRight => "LR",
_ => throw new ArgumentOutOfRangeException(nameof(direction), direction, null)
};
}
}
55 changes: 55 additions & 0 deletions Frank.Mermaid/Flowchart/Flowchart.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
using CodegenCS;

namespace Frank.Mermaid.Flowchart;

public class Flowchart : IMermaidable
{
private readonly List<Node> _nodes = new();
private readonly List<Link> _links = new();
private readonly List<Subgraph> _subgraphs = new();
private readonly Direction _direction;

public Flowchart(Direction direction = Direction.TopToBottom)
{
_direction = direction;
}

public void AddSubgraph(Subgraph subgraph) => _subgraphs.Add(subgraph);
public void AddSubgraphs(IEnumerable<Subgraph> subgraphs) => _subgraphs.AddRange(subgraphs);

public void AddNode(Node node) => _nodes.Add(node);
public void AddNodes(IEnumerable<Node> nodes) => _nodes.AddRange(nodes);

public void AddLink(Link link) => _links.Add(link);
public void AddLinks(IEnumerable<Link> links) => _links.AddRange(links);

/// <inheritdoc />
public Guid Id { get; } = Guid.NewGuid();

/// <inheritdoc />
public ICodegenTextWriter ToMermaidSyntax()
{
var writer = new CodegenTextWriter();
writer.WriteLine("flowchart {0}", _direction.ToMermaidSyntax());
writer.IncreaseIndent();

foreach (var node in _nodes)
{
writer.Write(node.ToMermaidSyntax());
}

foreach (var link in _links)
{
writer.Write(link.ToMermaidSyntax());
}

foreach (var subgraph in _subgraphs)
{
writer.Write(subgraph.ToMermaidSyntax());
}

writer.DecreaseIndent();

return writer;
}
}
50 changes: 50 additions & 0 deletions Frank.Mermaid/Flowchart/Line.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
using CodegenCS;

namespace Frank.Mermaid.Flowchart;

public class Line : IMermaidable
{
private readonly LineStyle _lineStyle;
private readonly int _lineWidth;

public Line(LineStyle lineStyle, int lineWidth = 1)
{
_lineStyle = lineStyle;
_lineWidth = lineWidth;
}

/// <inheritdoc />
public Guid Id { get; } = Guid.NewGuid();

/// <inheritdoc />
public ICodegenTextWriter ToMermaidSyntax()
{
var writer = new CodegenTextWriter();
var arrow = _lineStyle.ToString().Contains("Arrow") ? ">" : "";

switch (_lineStyle)
{
case LineStyle.Normal:
case LineStyle.NormalWithArrow:
writer.Write(new string('-', _lineWidth));
break;
case LineStyle.Thick:
case LineStyle.ThickWithArrow:
writer.Write(new string('=', _lineWidth));
break;
case LineStyle.Dotted:
case LineStyle.DottedWithArrow:
for (var i = 0; i < _lineWidth; i++)
{
writer.Write(i == _lineWidth - 1 ? "." : ".-");
}
break;
default:
throw new ArgumentOutOfRangeException();
}

writer.Write(arrow);

return writer;
}
}
11 changes: 11 additions & 0 deletions Frank.Mermaid/Flowchart/LineStyle.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
namespace Frank.Mermaid.Flowchart;

public enum LineStyle
{
Normal,
NormalWithArrow,
Thick,
ThickWithArrow,
Dotted,
DottedWithArrow
}
69 changes: 69 additions & 0 deletions Frank.Mermaid/Flowchart/Link.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
using CodegenCS;

namespace Frank.Mermaid.Flowchart;

public class Link : IMermaidable
{
public Link(string source, string target, string? label = null)
{
Source = source;
Target = target;
Label = label;
}

public string? Label { get; }

public string Target { get; }

public string Source { get; }

public Line Line { get; private set; } = new Line(LineStyle.Normal, 3);

public Link(IMermaidable source, IMermaidable target, string? label = null)
{
Source = GetIdentifier(source);
Target = GetIdentifier(target);
Label = label;
}

private string? GetIdentifier(IMermaidable source)
{
if (source is Subgraph subgraph)
{
return subgraph.Label;
}

return source.Id.ToString("N");
}

public void SetLineStyle(Line line)
{
Line = line;
}

/// <inheritdoc />
public Guid Id { get; } = Guid.NewGuid();

/// <inheritdoc />
public ICodegenTextWriter ToMermaidSyntax()
{
var writer = new CodegenTextWriter();

writer.Write("{0} {1}{2} {3}", Source, Line.ToMermaidSyntax(), GetLabel(), Target);

writer.WriteLine();
return writer;
}

private string GetLabel() => !string.IsNullOrWhiteSpace(Label) ? $"|{Label}|" : string.Empty;

/*
Length 1 2 3
Normal --- ---- -----
Normal with arrow --> ---> ---->
Thick === ==== =====
Thick with arrow ==> ===> ====>
Dotted -.- -..- -...-
Dotted with arrow -.-> -..-> -...->
*/
}
9 changes: 9 additions & 0 deletions Frank.Mermaid/Flowchart/LinkType.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
namespace Frank.Mermaid.Flowchart;

public enum LinkType
{
Default,
Dotted,
Thick,
DottedThick
}
Loading

0 comments on commit b4396f2

Please sign in to comment.