Skip to content

Commit

Permalink
Support top-left oriented coordinate system (#65)
Browse files Browse the repository at this point in the history
  • Loading branch information
chtenb authored Aug 11, 2023
1 parent 4946925 commit f41e135
Show file tree
Hide file tree
Showing 18 changed files with 611 additions and 672 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,11 +49,11 @@ jobs:

- name: Run Unittests With Coverage Calculation (.NET 4.8)

run: packages\opencover\4.7.1221\tools\OpenCover.Console.exe -skipautoprops -register '-target:bash.exe' -targetargs:'nunit-console.sh Rubjerg.Graphviz.Test\bin\x64\Release\net48\Rubjerg.Graphviz.Test.dll' '-filter:+[Rubjerg*]* -[Rubjerg.Graphviz.Test*]*'
run: packages\opencover\4.7.1221\tools\OpenCover.Console.exe -skipautoprops -returntargetcode -register '-target:bash.exe' -targetargs:'nunit-console.sh Rubjerg.Graphviz.Test\bin\x64\Release\net48\Rubjerg.Graphviz.Test.dll' '-filter:+[Rubjerg*]* -[Rubjerg.Graphviz.Test*]*'

- name: Run Unittests With Coverage Calculation (.NET 6)

run: packages\opencover\4.7.1221\tools\OpenCover.Console.exe -skipautoprops -register '-target:bash.exe' -targetargs:'nunit-console-netcore.sh Rubjerg.Graphviz.Test\bin\x64\Release\net6.0\Rubjerg.Graphviz.Test.dll' '-filter:+[Rubjerg*]* -[Rubjerg.Graphviz.Test*]*'
run: packages\opencover\4.7.1221\tools\OpenCover.Console.exe -skipautoprops -returntargetcode -register '-target:bash.exe' -targetargs:'nunit-console-netcore.sh Rubjerg.Graphviz.Test\bin\x64\Release\net6.0\Rubjerg.Graphviz.Test.dll' '-filter:+[Rubjerg*]* -[Rubjerg.Graphviz.Test*]*'

- name: Upload Coverage data
run: |
Expand Down
55 changes: 26 additions & 29 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,6 @@ documents presented at the [Graphviz documentation page](https://graphviz.org/do

```cs
using NUnit.Framework;
using System.Drawing;
using System.Linq;

namespace Rubjerg.Graphviz.Test;
Expand All @@ -59,7 +58,7 @@ namespace Rubjerg.Graphviz.Test;
public class Tutorial
{
public const string PointPattern = @"{X=[\d.]+, Y=[\d.]+}";
public const string RectPattern = @"{X=[\d.]+,Y=[\d.]+,Width=[\d.]+,Height=[\d.]+}";
public const string RectPattern = @"{X=[\d.]+, Y=[\d.]+, Width=[\d.]+, Height=[\d.]+}";
public const string SplinePattern =
@"{X=[\d.]+, Y=[\d.]+}, {X=[\d.]+, Y=[\d.]+}, {X=[\d.]+, Y=[\d.]+}, {X=[\d.]+, Y=[\d.]+}";

Expand Down Expand Up @@ -131,57 +130,52 @@ public class Tutorial

// Or we can ask Graphviz to compute the layout and programatically read out the layout attributes
// This will create a copy of our original graph with layout information attached to it in the form
// of attributes.
RootGraph layout = root.CreateLayout();
// of attributes. Graphviz outputs coordinates in a bottom-left originated coordinate system.
// But since many applications require rendering in a top-left originated coordinate system,
// we provide a way to translate the coordinates.
RootGraph layout = root.CreateLayout(coordinateSystem: CoordinateSystem.TopLeft);

// There are convenience methods available that parse these attributes for us and give
// back the layout information in an accessible form.
Node nodeA = layout.GetNode("A");
PointF position = nodeA.GetPosition();
PointD position = nodeA.GetPosition();
Utils.AssertPattern(PointPattern, position.ToString());

RectangleF nodeboundingbox = nodeA.GetBoundingBox();
RectangleD nodeboundingbox = nodeA.GetBoundingBox();
Utils.AssertPattern(RectPattern, nodeboundingbox.ToString());

// Or splines between nodes
Node nodeB = layout.GetNode("B");
Edge edge = layout.GetEdge(nodeA, nodeB, "Some edge name");
PointF[] spline = edge.GetFirstSpline();
PointD[] spline = edge.GetFirstSpline();
string splineString = string.Join(", ", spline.Select(p => p.ToString()));
Utils.AssertPattern(SplinePattern, splineString);

// If we require detailed drawing information for any object, we can retrieve the so called "xdot"
// operations. See https://graphviz.org/docs/outputs/canon/#xdot for a specification.
var activeColor = Color.Black;
var activeFillColor = System.Drawing.Color.Black;
foreach (var op in nodeA.GetDrawing())
{
if (op is XDotOp.FillColor { Value: string htmlColor })
if (op is XDotOp.FillColor { Value: Color.Uniform { HtmlColor: var htmlColor } })
{
activeColor = ColorTranslator.FromHtml(htmlColor);
activeFillColor = System.Drawing.ColorTranslator.FromHtml(htmlColor);
}
else if (op is XDotOp.FilledEllipse { Value: var filledEllipse })
else if (op is XDotOp.FilledEllipse { Value: var boundingBox })
{
var boundingBox = filledEllipse.ToRectangleF();
Utils.AssertPattern(RectPattern, boundingBox.ToString());
}
// Handle any xdot operation you require
}

var activeFont = XDotFont.Default;
foreach (var op in nodeA.GetDrawing())
foreach (var op in nodeA.GetLabelDrawing())
{
if (op is XDotOp.Font { Value: var font })
{
activeFont = font;
Utils.AssertPattern(@"Times-Roman", font.Name);
}
else if (op is XDotOp.Text { Value: var text })
if (op is XDotOp.Text { Value: var text })
{
var anchor = text.Anchor();
Utils.AssertPattern(PointPattern, anchor.ToString());
var boundingBox = text.TextBoundingBox(activeFont);
Utils.AssertPattern(PointPattern, text.Anchor.ToString());
var boundingBox = text.TextBoundingBoxEstimate();
Utils.AssertPattern(RectPattern, boundingBox.ToString());
Assert.AreEqual(text.Text, "A");
Assert.AreEqual(text.Font.Name, "Times-Roman");
}
// Handle any xdot operation you require
}
Expand Down Expand Up @@ -223,8 +217,8 @@ public class Tutorial
var layout = root.CreateLayout();

SubGraph cluster = layout.GetSubgraph("cluster_1");
RectangleF clusterbox = cluster.GetBoundingBox();
RectangleF rootgraphbox = layout.GetBoundingBox();
RectangleD clusterbox = cluster.GetBoundingBox();
RectangleD rootgraphbox = layout.GetBoundingBox();
Utils.AssertPattern(RectPattern, clusterbox.ToString());
Utils.AssertPattern(RectPattern, rootgraphbox.ToString());
}
Expand All @@ -234,14 +228,17 @@ public class Tutorial
{
RootGraph root = RootGraph.CreateNew(GraphType.Directed, "Graph with records");
Node nodeA = root.GetOrAddNode("A");
nodeA.SafeSetAttribute("shape", "record", "");
nodeA.SafeSetAttribute("label", "1|2|3|{4|5}|6|{7|8|9}", "\\N");
nodeA.SetAttribute("shape", "record");
// New line characters are not supported by record labels, and will be ignored by Graphviz
nodeA.SetAttribute("label", "1|2|3|{4|5}|6|{7|8|9}");

var layout = root.CreateLayout();

// The order of the list matches the order in which the labels occur in the label string above.
var rects = layout.GetNode("A").GetRecordRectangles().ToList();
var rectLabels = layout.GetNode("A").GetRecordRectangleLabels().Select(l => l.Text).ToList();
Assert.AreEqual(9, rects.Count);
Assert.AreEqual(new[] { "1", "2", "3", "4", "5", "6", "7", "8", "9" }, rectLabels);
}

[Test, Order(5)]
Expand All @@ -261,8 +258,8 @@ public class Tutorial
var somePortId = "port id with :| special characters";
var validPortName = Edge.ConvertUidToPortName(somePortId);
Node nodeB = root.GetOrAddNode("B");
nodeB.SafeSetAttribute("shape", "record", "");
nodeB.SafeSetAttribute("label", $"<{validPortName}>1|2", "\\N");
nodeB.SetAttribute("shape", "record");
nodeB.SetAttribute("label", $"<{validPortName}>1|2");

// The conversion function makes sure different strings don't accidentally map onto the same portname
Assert.AreNotEqual(Edge.ConvertUidToPortName(":"), Edge.ConvertUidToPortName("|"));
Expand Down
4 changes: 2 additions & 2 deletions Rubjerg.Graphviz.Test/CGraphEdgeCases.cs
Original file line number Diff line number Diff line change
Expand Up @@ -372,8 +372,8 @@ public void DotOutputConsistency()
RootGraph root = Utils.CreateUniqueTestGraph();
Node nodeA = root.GetOrAddNode("A");

nodeA.SafeSetAttribute("shape", "record", "");
nodeA.SafeSetAttribute("label", "1|2|3|{4|5}|6|{7|8|9}", "\\N");
nodeA.SetAttribute("shape", "record");
nodeA.SetAttribute("label", "1|2|3|{4|5}|6|{7|8|9}");

root.ComputeLayout();
var dotstr = root.ToDotString();
Expand Down
22 changes: 8 additions & 14 deletions Rubjerg.Graphviz.Test/OldTutorial.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
using NUnit.Framework;
using System.Drawing;
using System.Linq;

#pragma warning disable CS0618 // Type or member is obsolete
Expand Down Expand Up @@ -69,28 +68,23 @@ public void Layouting()

// Or programatically read out the layout attributes
Node nodeA = root.GetNode("A");
PointF position = nodeA.GetPosition();
PointD position = nodeA.GetPosition();
Utils.AssertPattern(@"{X=[\d.]+, Y=[\d.]+}", position.ToString());

// Like a bounding box of an object
RectangleF nodeboundingbox = nodeA.GetBoundingBox();
Utils.AssertPattern(@"{X=[\d.]+,Y=[\d.]+,Width=[\d.]+,Height=[\d.]+}", nodeboundingbox.ToString());
RectangleD nodeboundingbox = nodeA.GetBoundingBox();
Utils.AssertPattern(@"{X=[\d.]+, Y=[\d.]+, Width=[\d.]+, Height=[\d.]+}", nodeboundingbox.ToString());

// Or splines between nodes
Node nodeB = root.GetNode("B");
Edge edge = root.GetEdge(nodeA, nodeB, "Some edge name");
PointF[] spline = edge.GetFirstSpline();
PointD[] spline = edge.GetFirstSpline();
string splineString = string.Join(", ", spline.Select(p => p.ToString()));
string expectedSplinePattern =
@"{X=[\d.]+, Y=[\d.]+}, {X=[\d.]+, Y=[\d.]+},"
+ @" {X=[\d.]+, Y=[\d.]+}, {X=[\d.]+, Y=[\d.]+}";
Utils.AssertPattern(expectedSplinePattern, splineString);

GraphvizLabel nodeLabel = nodeA.GetLabel();
Utils.AssertPattern(@"{X=[\d.]+,Y=[\d.]+,Width=[\d.]+,Height=[\d.]+}",
nodeLabel.BoundingBox().ToString());
Utils.AssertPattern(@"Times-Roman", nodeLabel.FontName().ToString());

// Once all layout information is obtained from the graph, the resources should be
// reclaimed. To do this, the application should call the cleanup routine associated
// with the layout algorithm used to draw the graph. This is done by a call to
Expand Down Expand Up @@ -137,10 +131,10 @@ public void Clusters()
root.ComputeLayout();

SubGraph cluster = root.GetSubgraph("cluster_1");
RectangleF clusterbox = cluster.GetBoundingBox();
RectangleF rootgraphbox = root.GetBoundingBox();
Utils.AssertPattern(@"{X=[\d.]+,Y=[\d.]+,Width=[\d.]+,Height=[\d.]+}", clusterbox.ToString());
Utils.AssertPattern(@"{X=[\d.]+,Y=[\d.]+,Width=[\d.]+,Height=[\d.]+}", rootgraphbox.ToString());
RectangleD clusterbox = cluster.GetBoundingBox();
RectangleD rootgraphbox = root.GetBoundingBox();
Utils.AssertPattern(@"{X=[\d.]+, Y=[\d.]+, Width=[\d.]+, Height=[\d.]+}", clusterbox.ToString());
Utils.AssertPattern(@"{X=[\d.]+, Y=[\d.]+, Width=[\d.]+, Height=[\d.]+}", rootgraphbox.ToString());
}

[Test, Order(4)]
Expand Down
17 changes: 9 additions & 8 deletions Rubjerg.Graphviz.Test/Reproductions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,15 +33,16 @@ public void TestRecordShapeAlignment(string fontname, double fontsize, double ma

Node nodeA = root.GetOrAddNode("A");

nodeA.SafeSetAttribute("shape", "record", "");
nodeA.SafeSetAttribute("label", "{20 VH|{1|2}}", "");
nodeA.SetAttribute("shape", "record");
nodeA.SetAttribute("label", "{20 VH|{1|2}}");

//TestContext.Write(root.ToDotString());
root.ComputeLayout();
//TestContext.Write(root.ToDotString());

var rects = nodeA.GetRecordRectangles().ToList();
Assert.That(rects[0].Right, Is.EqualTo(rects[2].Right));
// This test is fixed by passing snapOntoDrawingCoordinates: true
var rects = nodeA.GetRecordRectangles(snapOntoDrawingCoordinates: true).ToList();
Assert.That(rects[0].FarPoint().X, Is.EqualTo(rects[2].FarPoint().X));
}

// This test only failed when running in isolation
Expand Down Expand Up @@ -74,8 +75,8 @@ public void TestDotNewlines()
RootGraph root = Utils.CreateUniqueTestGraph();
Node nodeA = root.GetOrAddNode("A");

nodeA.SafeSetAttribute("shape", "record", "");
nodeA.SafeSetAttribute("label", "1|2|3|{4|5}|6|{7|8|9}", "\\N");
nodeA.SetAttribute("shape", "record");
nodeA.SetAttribute("label", "1|2|3|{4|5}|6|{7|8|9}");

var dotString = root.ToDotString();
Assert.IsFalse(dotString.Contains("\r"));
Expand All @@ -88,8 +89,8 @@ public void TestDotNewlines2()
RootGraph root = Utils.CreateUniqueTestGraph();
Node nodeA = root.GetOrAddNode("A");

nodeA.SafeSetAttribute("shape", "record", "");
nodeA.SafeSetAttribute("label", "1|2|3|{4|5}|6|{7|8|9}", "\\N");
nodeA.SetAttribute("shape", "record");
nodeA.SetAttribute("label", "1|2|3|{4|5}|6|{7|8|9}");

var xdotGraph = root.CreateLayout();
var xNodeA = xdotGraph.GetNode("A");
Expand Down
40 changes: 17 additions & 23 deletions Rubjerg.Graphviz.Test/TestDotLayout.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,14 +34,13 @@ public void TestLayoutMethodsWithoutLayout()
{
CreateSimpleTestGraph(out RootGraph root, out Node nodeA, out Edge edge);

Assert.AreEqual(root.GetBoundingBox(), default(RectangleF));
Assert.AreEqual(root.GetColor(), Color.Black);
Assert.AreEqual(root.GetBoundingBox(), default(RectangleD));
Assert.AreEqual(root.GetDrawing().Count, 0);
Assert.AreEqual(root.GetLabelDrawing().Count, 0);

Assert.AreEqual(nodeA.GetPosition(), default(PointF));
Assert.AreEqual(nodeA.GetBoundingBox(), default(RectangleF));
Assert.AreEqual(nodeA.GetSize(), default(SizeF));
Assert.AreEqual(nodeA.GetPosition(), default(PointD));
Assert.AreEqual(nodeA.GetBoundingBox(), default(RectangleD));
Assert.AreEqual(nodeA.GetSize(), default(SizeD));
Assert.AreEqual(nodeA.GetRecordRectangles().Count(), 0);
Assert.AreEqual(nodeA.GetDrawing().Count, 0);
Assert.AreEqual(nodeA.GetLabelDrawing().Count, 0);
Expand All @@ -65,12 +64,10 @@ public void TestLayoutMethodsWithInProcessLayout()

root.ComputeLayout();

Assert.AreEqual(root.GetColor(), Color.Black);
Assert.AreNotEqual(root.GetBoundingBox(), default(RectangleF));
Assert.AreNotEqual(root.GetDrawing().Count, 0);
Assert.AreNotEqual(root.GetLabelDrawing().Count, 0);

Assert.AreEqual(nodeA.GetColor(), Color.Red);
Assert.AreEqual(nodeA.GetRecordRectangles().Count(), 2);
Assert.AreNotEqual(nodeA.GetPosition(), default(PointF));
Assert.AreNotEqual(nodeA.GetBoundingBox(), default(RectangleF));
Expand Down Expand Up @@ -98,12 +95,10 @@ public void TestLayoutMethodsWithLayout()
var xnodeB = xroot.GetNode("B");
Edge xedge = xroot.GetEdge(xnodeA, xnodeB, "");

Assert.AreEqual(xroot.GetColor(), Color.Black);
Assert.AreNotEqual(xroot.GetBoundingBox(), default(RectangleF));
Assert.AreNotEqual(xroot.GetDrawing().Count, 0);
Assert.AreNotEqual(xroot.GetLabelDrawing().Count, 0);

Assert.AreEqual(xnodeA.GetColor(), Color.Red);
Assert.AreEqual(xnodeA.GetRecordRectangles().Count(), 2);
Assert.AreNotEqual(xnodeA.GetPosition(), default(PointF));
Assert.AreNotEqual(xnodeA.GetBoundingBox(), default(RectangleF));
Expand Down Expand Up @@ -179,15 +174,14 @@ public void TestRecordShapeOrder()
RootGraph root = CreateUniqueTestGraph();
Node nodeA = root.GetOrAddNode("A");

nodeA.SafeSetAttribute("shape", "record", "");
nodeA.SafeSetAttribute("label", "1|2|3|{4|5}|6|{7|8|9}", "\\N");
nodeA.SetAttribute("shape", "record");
nodeA.SetAttribute("label", "1|2|3|{4|5}|6|{7|8|9}");

root.ComputeLayout();

var rects = nodeA.GetRecordRectangles().ToList();

// Because Graphviz uses a lower-left originated coordinate system, we need to flip the y coordinates
Utils.AssertOrder(rects, r => (r.Left, -r.Top));
Utils.AssertOrder(rects, r => (r.Origin.X, -r.Origin.Y));
Assert.That(rects.Count, Is.EqualTo(9));
}

Expand All @@ -196,8 +190,8 @@ public void TestEmptyRecordShapes()
{
RootGraph root = CreateUniqueTestGraph();
Node nodeA = root.GetOrAddNode("A");
nodeA.SafeSetAttribute("shape", "record", "");
nodeA.SafeSetAttribute("label", "||||", "");
nodeA.SetAttribute("shape", "record");
nodeA.SetAttribute("label", "||||");

root.ComputeLayout();

Expand All @@ -223,11 +217,11 @@ public void TestPortNameConversion(bool escape)
{
RootGraph root = CreateUniqueTestGraph();
Node node = root.GetOrAddNode("N");
node.SafeSetAttribute("shape", "record", "");
node.SafeSetAttribute("label", label, "");
node.SetAttribute("shape", "record");
node.SetAttribute("label", label);
Edge edge = root.GetOrAddEdge(node, node, "");
edge.SafeSetAttribute("tailport", port1 + ":n", "");
edge.SafeSetAttribute("headport", port2 + ":s", "");
edge.SetAttribute("tailport", port1 + ":n");
edge.SetAttribute("headport", port2 + ":s");
root.ToDotFile(GetTestFilePath("out.gv"));
}

Expand Down Expand Up @@ -269,12 +263,12 @@ public void TestLabelEscaping(bool escape)
{
RootGraph root = CreateUniqueTestGraph();
Node node1 = root.GetOrAddNode("1");
node1.SafeSetAttribute("shape", "record", "");
node1.SafeSetAttribute("label", label1, "");
node1.SetAttribute("shape", "record");
node1.SetAttribute("label", label1);
Node node2 = root.GetOrAddNode("2");
node2.SafeSetAttribute("label", label2, "");
node2.SetAttribute("label", label2);
Node node3 = root.GetOrAddNode("3");
node3.SafeSetAttribute("label", label3, "");
node3.SetAttribute("label", label3);
root.ToDotFile(GetTestFilePath("out.gv"));
}

Expand Down
Loading

0 comments on commit f41e135

Please sign in to comment.