From ddec09f2309745aad32e9db06767c097c0ac57f1 Mon Sep 17 00:00:00 2001 From: chtenb Date: Mon, 2 Jun 2025 14:46:45 +0200 Subject: [PATCH 1/9] update readme --- README-src.md | 4 ++-- README.md | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/README-src.md b/README-src.md index e207b9a..98cf984 100644 --- a/README-src.md +++ b/README-src.md @@ -12,8 +12,8 @@ This library is compatible with .NET Standard 2.0, but the nuget package only su The unit tests run on windows and linux against .NET Framework .NET 8.0. ### Linux -In the same vein as our windows build, we ship precompiled binaries to make sure that this library is deployed with the same binaries as we've tested it. -However, we do not ship graphviz dependencies, you will have to installed those yourself, if you need them. +In the same vein as our windows build, we ship Graphviz binaries to make sure that this library is deployed with the same binaries as we've tested it. +However, we do not ship all the dependencies of Graphviz, you will have to make sure these are available on your system, if you need them. [Here is a list of all the graphviz dependencies.](https://packages.fedoraproject.org/pkgs/graphviz/graphviz/fedora-rawhide.html#dependencies) In practice you may not need all of those. In particular, if you only want to read graphs and e.g. run the DOT algorithm, libc and libz are enough. diff --git a/README.md b/README.md index 8e2e37b..9ebeadf 100644 --- a/README.md +++ b/README.md @@ -12,8 +12,8 @@ This library is compatible with .NET Standard 2.0, but the nuget package only su The unit tests run on windows and linux against .NET Framework .NET 8.0. ### Linux -In the same vein as our windows build, we ship precompiled binaries to make sure that this library is deployed with the same binaries as we've tested it. -However, we do not ship graphviz dependencies, you will have to installed those yourself, if you need them. +In the same vein as our windows build, we ship Graphviz binaries to make sure that this library is deployed with the same binaries as we've tested it. +However, we do not ship all the dependencies of Graphviz, you will have to make sure these are available on your system, if you need them. [Here is a list of all the graphviz dependencies.](https://packages.fedoraproject.org/pkgs/graphviz/graphviz/fedora-rawhide.html#dependencies) In practice you may not need all of those. In particular, if you only want to read graphs and e.g. run the DOT algorithm, libc and libz are enough. From d14748d04fcbf415c1317da502c8e5fb8048852e Mon Sep 17 00:00:00 2001 From: chtenb Date: Mon, 2 Jun 2025 14:49:09 +0200 Subject: [PATCH 2/9] update readme --- README-src.md | 11 ++++++----- README.md | 11 ++++++----- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/README-src.md b/README-src.md index 98cf984..0536138 100644 --- a/README-src.md +++ b/README-src.md @@ -5,18 +5,19 @@ Graphviz.NetWrapper `Rubjerg.Graphviz` ships with precompiled Graphviz binaries to ensure the exact graphviz version is used that we have tested and to make deployment more predictable. The current version we ship is Graphviz 11.0.0. - -### Windows -`Rubjerg.Graphviz` ships with a bunch of precompiled Graphviz dlls built for 64 bit Windows. This library is compatible with .NET Standard 2.0, but the nuget package only supports .NET5.0 and higher. The unit tests run on windows and linux against .NET Framework .NET 8.0. +### Windows +`Rubjerg.Graphviz` ships with the necessary Graphviz binaries and dependencies built for 64 bit Windows. + ### Linux In the same vein as our windows build, we ship Graphviz binaries to make sure that this library is deployed with the same binaries as we've tested it. -However, we do not ship all the dependencies of Graphviz, you will have to make sure these are available on your system, if you need them. +However, we do not ship all the dependencies of Graphviz. +You will have to make sure these are available on your system, if you need them. [Here is a list of all the graphviz dependencies.](https://packages.fedoraproject.org/pkgs/graphviz/graphviz/fedora-rawhide.html#dependencies) In practice you may not need all of those. -In particular, if you only want to read graphs and e.g. run the DOT algorithm, libc and libz are enough. +In particular, if you only want to read graphs and run the DOT layout algorithm, libc and libz are enough. To run our tests successfully you will also need libgts and libpcre2 (for the neato algorithm). For more details, check the dependencies of any graphviz binaries with `ldd`. diff --git a/README.md b/README.md index 9ebeadf..21a6d0f 100644 --- a/README.md +++ b/README.md @@ -5,18 +5,19 @@ Graphviz.NetWrapper `Rubjerg.Graphviz` ships with precompiled Graphviz binaries to ensure the exact graphviz version is used that we have tested and to make deployment more predictable. The current version we ship is Graphviz 11.0.0. - -### Windows -`Rubjerg.Graphviz` ships with a bunch of precompiled Graphviz dlls built for 64 bit Windows. This library is compatible with .NET Standard 2.0, but the nuget package only supports .NET5.0 and higher. The unit tests run on windows and linux against .NET Framework .NET 8.0. +### Windows +`Rubjerg.Graphviz` ships with the necessary Graphviz binaries and dependencies built for 64 bit Windows. + ### Linux In the same vein as our windows build, we ship Graphviz binaries to make sure that this library is deployed with the same binaries as we've tested it. -However, we do not ship all the dependencies of Graphviz, you will have to make sure these are available on your system, if you need them. +However, we do not ship all the dependencies of Graphviz. +You will have to make sure these are available on your system, if you need them. [Here is a list of all the graphviz dependencies.](https://packages.fedoraproject.org/pkgs/graphviz/graphviz/fedora-rawhide.html#dependencies) In practice you may not need all of those. -In particular, if you only want to read graphs and e.g. run the DOT algorithm, libc and libz are enough. +In particular, if you only want to read graphs and run the DOT layout algorithm, libc and libz are enough. To run our tests successfully you will also need libgts and libpcre2 (for the neato algorithm). For more details, check the dependencies of any graphviz binaries with `ldd`. From 6e28de18d3a5cfad5e5e6652e4b164d592599752 Mon Sep 17 00:00:00 2001 From: chtenb Date: Mon, 2 Jun 2025 14:55:58 +0200 Subject: [PATCH 3/9] update readme --- .editorconfig | 4 ++ README.md | 65 +++++++++++++++++-------------- Rubjerg.Graphviz.Test/Tutorial.cs | 65 +++++++++++++++++-------------- 3 files changed, 74 insertions(+), 60 deletions(-) diff --git a/.editorconfig b/.editorconfig index 6b22895..128c864 100644 --- a/.editorconfig +++ b/.editorconfig @@ -209,3 +209,7 @@ dotnet_naming_symbols.private_internal_fields.applicable_kinds = field dotnet_naming_symbols.private_internal_fields.applicable_accessibilities = private, internal dotnet_naming_style.camel_case_underscore_style.required_prefix = _ dotnet_naming_style.camel_case_underscore_style.capitalization = camel_case + +# To avoid the scrollbar on the github homepage +[Tutorial.cs] +max_line_length = 100 diff --git a/README.md b/README.md index 21a6d0f..8f9c888 100644 --- a/README.md +++ b/README.md @@ -64,17 +64,17 @@ public class Tutorial [Test, Order(1)] public void GraphConstruction() { - // You can programmatically construct graphs as follows + // You can programmatically construct graphs as follows. RootGraph root = RootGraph.CreateNew(GraphType.Directed, "Some Unique Identifier"); // The graph name is optional, and can be omitted. The name is not interpreted by Graphviz, // except it is recorded and preserved when the graph is written as a file. - // The node names are unique identifiers within a graph in Graphviz + // The node names are unique identifiers within a graph in Graphviz. Node nodeA = root.GetOrAddNode("A"); Node nodeB = root.GetOrAddNode("B"); Node nodeC = root.GetOrAddNode("C"); - // The edge name is only unique between two nodes + // The edge name is only unique between two nodes. Edge edgeAB = root.GetOrAddEdge(nodeA, nodeB, "Some edge name"); Edge edgeBC = root.GetOrAddEdge(nodeB, nodeC, "Some edge name"); Edge anotherEdgeBC = root.GetOrAddEdge(nodeB, nodeC, "Another edge name"); @@ -87,8 +87,8 @@ public class Tutorial // We can attach attributes to nodes, edges and graphs to store information and instruct // Graphviz by specifying layout parameters. At the moment we only support string - // attributes. Cgraph assumes that all objects of a given kind (graphs/subgraphs, nodes, - // or edges) have the same attributes. An attribute has to be introduced with a default value + // attributes. Cgraph assumes that all objects of a given kind (graphs/subgraphs, nodes, or + // edges) have the same attributes. An attribute has to be introduced with a default value // first for a certain kind, before we can use it. Node.IntroduceAttribute(root, "my attribute", "defaultvalue"); nodeA.SetAttribute("my attribute", "othervalue"); @@ -98,17 +98,18 @@ public class Tutorial Edge.IntroduceAttribute(root, "my attribute", "defaultvalue"); edgeAB.SetAttribute("my attribute", "othervalue"); - // To introduce and set an attribute at the same time, there are convenience wrappers + // To introduce and set an attribute at the same time, there are convenience wrappers. edgeBC.SafeSetAttribute("arrowsize", "2.0", "1.0"); - // If we set an unintroduced attribute, the attribute will be introduced with an empty default value. + // If we set an unintroduced attribute, the attribute will be introduced with an empty + // default value. edgeBC.SetAttribute("new attr", "value"); - // Some attributes - like "label" - accept HTML strings as value - // To tell Graphviz that a string should be interpreted as HTML use the designated methods + // Some attributes - like "label" - accept HTML strings as value. + // To tell Graphviz that a string should be interpreted as HTML use the designated methods. Node.IntroduceAttribute(root, "label", "defaultlabel"); nodeB.SetAttributeHtml("label", "Some HTML string"); - // We can simply export this graph to a text file in dot format + // We can simply export this graph to a text file in dot format. root.ToDotFile(TestContext.CurrentContext.TestDirectory + "/out.dot"); // A word of advice, Graphviz doesn't play very well with empty strings. @@ -118,20 +119,21 @@ public class Tutorial [Test, Order(2)] public void Layouting() { - // If we have a given dot file (in this case the one we generated above), we can also read it back in + // If we have a given dot file (in this case the one we generated above), we can also read + // it back in. RootGraph root = RootGraph.FromDotFile(TestContext.CurrentContext.TestDirectory + "/out.dot"); - // We can ask Graphviz to compute a layout and render it to svg + // We can ask Graphviz to compute a layout and render it to svg. root.ToSvgFile(TestContext.CurrentContext.TestDirectory + "/dot_out.svg"); - // We can use layout engines other than dot by explicitly passing the engine we want + // We can use layout engines other than dot by explicitly passing the engine we want. root.ToSvgFile(TestContext.CurrentContext.TestDirectory + "/neato_out.svg", LayoutEngines.Neato); - // 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. 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. + // 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. 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 @@ -143,7 +145,7 @@ public class Tutorial RectangleD nodeboundingbox = nodeA.GetBoundingBox(); Utils.AssertPattern(RectPattern, nodeboundingbox.ToString()); - // Or splines between nodes + // Or splines between nodes. Node nodeB = layout.GetNode("B")!; Edge edge = layout.GetEdge(nodeA, nodeB, "Some edge name")!; PointD[] spline = edge.GetFirstSpline(); @@ -202,10 +204,10 @@ public class Tutorial // COMPOUND EDGES // Graphviz does not really support edges from and to clusters. However, by adding an - // invisible dummynode and setting the ltail or lhead attributes of an edge this - // behavior can be faked. Graphviz will then draw an edge to the dummy node but clip it - // at the border of the cluster. We provide convenience methods for this. - // To enable this feature, Graphviz requires us to set the "compound" attribute to "true". + // invisible dummynode and setting the ltail or lhead attributes of an edge this behavior + // can be faked. Graphviz will then draw an edge to the dummy node but clip it at the border + // of the cluster. We provide convenience methods for this. To enable this feature, Graphviz + // requires us to set the "compound" attribute to "true". Graph.IntroduceAttribute(root, "compound", "true"); // Allow lhead/ltail // The boolean indicates whether the dummy node should take up any space. When you pass // false and you have a lot of edges, the edges may start to overlap a lot. @@ -228,12 +230,13 @@ public class Tutorial RootGraph root = RootGraph.CreateNew(GraphType.Directed, "Graph with records"); Node nodeA = root.GetOrAddNode("A"); nodeA.SetAttribute("shape", "record"); - // New line characters are not supported by record labels, and will be ignored by Graphviz + // 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. + // 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); @@ -248,19 +251,21 @@ public class Tutorial Node nodeA = root.GetOrAddNode("A"); // Several characters and character sequences can have special meanings in labels, like \N. - // When you want to have a literal string in a label, we provide a convenience function for you to do just that. + // When you want to have a literal string in a label, we provide a convenience function for + // you to do just that. nodeA.SetAttribute("label", CGraphThing.EscapeLabel("Some string literal \\N \\n |}>")); - // When defining portnames, some characters, like ':' and '|', are not allowed and they can't be escaped either. - // This can be troubling if you have an externally defined ID for such a port. - // We provide a function that maps strings to valid portnames. + // When defining portnames, some characters, like ':' and '|', are not allowed and they + // can't be escaped either. This can be troubling if you have an externally defined ID for + // such a port. We provide a function that maps strings to valid portnames. var somePortId = "port id with :| special characters"; var validPortName = Edge.ConvertUidToPortName(somePortId); Node nodeB = root.GetOrAddNode("B"); 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 + // The conversion function makes sure different strings don't accidentally map onto the same + // portname. Assert.AreNotEqual(Edge.ConvertUidToPortName(":"), Edge.ConvertUidToPortName("|")); } } diff --git a/Rubjerg.Graphviz.Test/Tutorial.cs b/Rubjerg.Graphviz.Test/Tutorial.cs index a411ce3..8545982 100644 --- a/Rubjerg.Graphviz.Test/Tutorial.cs +++ b/Rubjerg.Graphviz.Test/Tutorial.cs @@ -16,17 +16,17 @@ public class Tutorial [Test, Order(1)] public void GraphConstruction() { - // You can programmatically construct graphs as follows + // You can programmatically construct graphs as follows. RootGraph root = RootGraph.CreateNew(GraphType.Directed, "Some Unique Identifier"); // The graph name is optional, and can be omitted. The name is not interpreted by Graphviz, // except it is recorded and preserved when the graph is written as a file. - // The node names are unique identifiers within a graph in Graphviz + // The node names are unique identifiers within a graph in Graphviz. Node nodeA = root.GetOrAddNode("A"); Node nodeB = root.GetOrAddNode("B"); Node nodeC = root.GetOrAddNode("C"); - // The edge name is only unique between two nodes + // The edge name is only unique between two nodes. Edge edgeAB = root.GetOrAddEdge(nodeA, nodeB, "Some edge name"); Edge edgeBC = root.GetOrAddEdge(nodeB, nodeC, "Some edge name"); Edge anotherEdgeBC = root.GetOrAddEdge(nodeB, nodeC, "Another edge name"); @@ -39,8 +39,8 @@ public void GraphConstruction() // We can attach attributes to nodes, edges and graphs to store information and instruct // Graphviz by specifying layout parameters. At the moment we only support string - // attributes. Cgraph assumes that all objects of a given kind (graphs/subgraphs, nodes, - // or edges) have the same attributes. An attribute has to be introduced with a default value + // attributes. Cgraph assumes that all objects of a given kind (graphs/subgraphs, nodes, or + // edges) have the same attributes. An attribute has to be introduced with a default value // first for a certain kind, before we can use it. Node.IntroduceAttribute(root, "my attribute", "defaultvalue"); nodeA.SetAttribute("my attribute", "othervalue"); @@ -50,17 +50,18 @@ public void GraphConstruction() Edge.IntroduceAttribute(root, "my attribute", "defaultvalue"); edgeAB.SetAttribute("my attribute", "othervalue"); - // To introduce and set an attribute at the same time, there are convenience wrappers + // To introduce and set an attribute at the same time, there are convenience wrappers. edgeBC.SafeSetAttribute("arrowsize", "2.0", "1.0"); - // If we set an unintroduced attribute, the attribute will be introduced with an empty default value. + // If we set an unintroduced attribute, the attribute will be introduced with an empty + // default value. edgeBC.SetAttribute("new attr", "value"); - // Some attributes - like "label" - accept HTML strings as value - // To tell Graphviz that a string should be interpreted as HTML use the designated methods + // Some attributes - like "label" - accept HTML strings as value. + // To tell Graphviz that a string should be interpreted as HTML use the designated methods. Node.IntroduceAttribute(root, "label", "defaultlabel"); nodeB.SetAttributeHtml("label", "Some HTML string"); - // We can simply export this graph to a text file in dot format + // We can simply export this graph to a text file in dot format. root.ToDotFile(TestContext.CurrentContext.TestDirectory + "/out.dot"); // A word of advice, Graphviz doesn't play very well with empty strings. @@ -70,20 +71,21 @@ public void GraphConstruction() [Test, Order(2)] public void Layouting() { - // If we have a given dot file (in this case the one we generated above), we can also read it back in + // If we have a given dot file (in this case the one we generated above), we can also read + // it back in. RootGraph root = RootGraph.FromDotFile(TestContext.CurrentContext.TestDirectory + "/out.dot"); - // We can ask Graphviz to compute a layout and render it to svg + // We can ask Graphviz to compute a layout and render it to svg. root.ToSvgFile(TestContext.CurrentContext.TestDirectory + "/dot_out.svg"); - // We can use layout engines other than dot by explicitly passing the engine we want + // We can use layout engines other than dot by explicitly passing the engine we want. root.ToSvgFile(TestContext.CurrentContext.TestDirectory + "/neato_out.svg", LayoutEngines.Neato); - // 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. 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. + // 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. 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 @@ -95,7 +97,7 @@ public void Layouting() RectangleD nodeboundingbox = nodeA.GetBoundingBox(); Utils.AssertPattern(RectPattern, nodeboundingbox.ToString()); - // Or splines between nodes + // Or splines between nodes. Node nodeB = layout.GetNode("B")!; Edge edge = layout.GetEdge(nodeA, nodeB, "Some edge name")!; PointD[] spline = edge.GetFirstSpline(); @@ -154,10 +156,10 @@ public void Clusters() // COMPOUND EDGES // Graphviz does not really support edges from and to clusters. However, by adding an - // invisible dummynode and setting the ltail or lhead attributes of an edge this - // behavior can be faked. Graphviz will then draw an edge to the dummy node but clip it - // at the border of the cluster. We provide convenience methods for this. - // To enable this feature, Graphviz requires us to set the "compound" attribute to "true". + // invisible dummynode and setting the ltail or lhead attributes of an edge this behavior + // can be faked. Graphviz will then draw an edge to the dummy node but clip it at the border + // of the cluster. We provide convenience methods for this. To enable this feature, Graphviz + // requires us to set the "compound" attribute to "true". Graph.IntroduceAttribute(root, "compound", "true"); // Allow lhead/ltail // The boolean indicates whether the dummy node should take up any space. When you pass // false and you have a lot of edges, the edges may start to overlap a lot. @@ -180,12 +182,13 @@ public void Records() RootGraph root = RootGraph.CreateNew(GraphType.Directed, "Graph with records"); Node nodeA = root.GetOrAddNode("A"); nodeA.SetAttribute("shape", "record"); - // New line characters are not supported by record labels, and will be ignored by Graphviz + // 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. + // 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); @@ -200,19 +203,21 @@ public void StringEscaping() Node nodeA = root.GetOrAddNode("A"); // Several characters and character sequences can have special meanings in labels, like \N. - // When you want to have a literal string in a label, we provide a convenience function for you to do just that. + // When you want to have a literal string in a label, we provide a convenience function for + // you to do just that. nodeA.SetAttribute("label", CGraphThing.EscapeLabel("Some string literal \\N \\n |}>")); - // When defining portnames, some characters, like ':' and '|', are not allowed and they can't be escaped either. - // This can be troubling if you have an externally defined ID for such a port. - // We provide a function that maps strings to valid portnames. + // When defining portnames, some characters, like ':' and '|', are not allowed and they + // can't be escaped either. This can be troubling if you have an externally defined ID for + // such a port. We provide a function that maps strings to valid portnames. var somePortId = "port id with :| special characters"; var validPortName = Edge.ConvertUidToPortName(somePortId); Node nodeB = root.GetOrAddNode("B"); 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 + // The conversion function makes sure different strings don't accidentally map onto the same + // portname. Assert.AreNotEqual(Edge.ConvertUidToPortName(":"), Edge.ConvertUidToPortName("|")); } } From 6d03746fcc4b07644476c2dac424ec467071a1cc Mon Sep 17 00:00:00 2001 From: chtenb Date: Mon, 2 Jun 2025 15:22:50 +0200 Subject: [PATCH 4/9] pin .net sdk in workflow --- .github/workflows/linux-build.yml | 6 +++--- .github/workflows/nuget.org-linux.yml | 6 +++--- .github/workflows/nuget.org-windows.yml | 5 +++++ .github/workflows/win-build.yml | 7 +++++-- 4 files changed, 16 insertions(+), 8 deletions(-) diff --git a/.github/workflows/linux-build.yml b/.github/workflows/linux-build.yml index e896750..8664f0a 100644 --- a/.github/workflows/linux-build.yml +++ b/.github/workflows/linux-build.yml @@ -21,10 +21,10 @@ jobs: - name: Install libgts run: sudo apt-get update && sudo apt-get install -y libgts-0.7-5 - - name: Setup .NET - uses: actions/setup-dotnet@v3 + - name: Setup .NET SDK + uses: actions/setup-dotnet@v4 with: - dotnet-version: 8.0.x + dotnet-version: '8.0.x' - name: Setup Just uses: extractions/setup-just@v3 diff --git a/.github/workflows/nuget.org-linux.yml b/.github/workflows/nuget.org-linux.yml index cd3844b..bb64fce 100644 --- a/.github/workflows/nuget.org-linux.yml +++ b/.github/workflows/nuget.org-linux.yml @@ -22,10 +22,10 @@ jobs: - name: Install libgts run: sudo apt-get update && sudo apt-get install -y libgts-0.7-5 - - name: Setup .NET - uses: actions/setup-dotnet@v3 + - name: Setup .NET SDK + uses: actions/setup-dotnet@v4 with: - dotnet-version: 8.0.x + dotnet-version: '8.0.x' - name: Setup Just uses: extractions/setup-just@v3 diff --git a/.github/workflows/nuget.org-windows.yml b/.github/workflows/nuget.org-windows.yml index 6b9c71c..15b6ee5 100644 --- a/.github/workflows/nuget.org-windows.yml +++ b/.github/workflows/nuget.org-windows.yml @@ -17,6 +17,11 @@ jobs: with: fetch-depth: 0 + - name: Setup .NET SDK + uses: actions/setup-dotnet@v4 + with: + dotnet-version: '8.0.x' + - name: Add msbuild to PATH uses: microsoft/setup-msbuild@v1.0.2 diff --git a/.github/workflows/win-build.yml b/.github/workflows/win-build.yml index 5ef066b..2c2f014 100644 --- a/.github/workflows/win-build.yml +++ b/.github/workflows/win-build.yml @@ -1,5 +1,3 @@ -# This is a basic workflow to help you get started with Actions - name: win-build # Controls when the action will run. Triggers the workflow on push or pull request @@ -21,6 +19,11 @@ jobs: with: fetch-depth: 0 + - name: Setup .NET SDK + uses: actions/setup-dotnet@v4 + with: + dotnet-version: '8.0.x' + - name: Add msbuild to PATH uses: microsoft/setup-msbuild@v1.0.2 From cbf2916e239c3f423593a9e40233bc55ceeba76e Mon Sep 17 00:00:00 2001 From: chtenb Date: Mon, 2 Jun 2025 15:33:41 +0200 Subject: [PATCH 5/9] Use global.json --- global.json | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 global.json diff --git a/global.json b/global.json new file mode 100644 index 0000000..f47a8c1 --- /dev/null +++ b/global.json @@ -0,0 +1,6 @@ +{ + "sdk": { + "version": "8.0.102", + "rollForward": "latestPatch" + } +} From 2dc7b9ae231fe582154970e0e7d78054d86bdf7f Mon Sep 17 00:00:00 2001 From: chtenb Date: Mon, 2 Jun 2025 15:36:57 +0200 Subject: [PATCH 6/9] change verson --- global.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/global.json b/global.json index f47a8c1..1e9e160 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@ { "sdk": { - "version": "8.0.102", + "version": "8.0.0", "rollForward": "latestPatch" } } From b5b7c9fa1c72bcfd33f7853883ed401f86ee424a Mon Sep 17 00:00:00 2001 From: chtenb Date: Mon, 2 Jun 2025 15:40:33 +0200 Subject: [PATCH 7/9] change verson --- global.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/global.json b/global.json index 1e9e160..226a520 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@ { "sdk": { "version": "8.0.0", - "rollForward": "latestPatch" + "rollForward": "latestFeature" } } From c66113875fc3f908e3ccb38711a4f164168991aa Mon Sep 17 00:00:00 2001 From: chtenb Date: Mon, 2 Jun 2025 15:52:00 +0200 Subject: [PATCH 8/9] Enfore linewidth in tutorial --- .editorconfig | 1 + Rubjerg.Graphviz.Test/Tutorial.cs | 412 +++++++++++++++--------------- justfile | 3 + 3 files changed, 210 insertions(+), 206 deletions(-) diff --git a/.editorconfig b/.editorconfig index 128c864..a76bdc2 100644 --- a/.editorconfig +++ b/.editorconfig @@ -213,3 +213,4 @@ dotnet_naming_style.camel_case_underscore_style.capitalization = camel_case # To avoid the scrollbar on the github homepage [Tutorial.cs] max_line_length = 100 +indent_size = 2 diff --git a/Rubjerg.Graphviz.Test/Tutorial.cs b/Rubjerg.Graphviz.Test/Tutorial.cs index 8545982..11978d1 100644 --- a/Rubjerg.Graphviz.Test/Tutorial.cs +++ b/Rubjerg.Graphviz.Test/Tutorial.cs @@ -8,216 +8,216 @@ namespace Rubjerg.Graphviz.Test; [TestFixture()] 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 SplinePattern = - @"{X=[\d.]+, Y=[\d.]+}, {X=[\d.]+, Y=[\d.]+}, {X=[\d.]+, Y=[\d.]+}, {X=[\d.]+, Y=[\d.]+}"; - - [Test, Order(1)] - public void GraphConstruction() + public const string PointPattern = @"{X=[\d.]+, Y=[\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.]+}"; + + [Test, Order(1)] + public void GraphConstruction() + { + // You can programmatically construct graphs as follows. + RootGraph root = RootGraph.CreateNew(GraphType.Directed, "Some Unique Identifier"); + // The graph name is optional, and can be omitted. The name is not interpreted by Graphviz, + // except it is recorded and preserved when the graph is written as a file. + + // The node names are unique identifiers within a graph in Graphviz. + Node nodeA = root.GetOrAddNode("A"); + Node nodeB = root.GetOrAddNode("B"); + Node nodeC = root.GetOrAddNode("C"); + + // The edge name is only unique between two nodes. + Edge edgeAB = root.GetOrAddEdge(nodeA, nodeB, "Some edge name"); + Edge edgeBC = root.GetOrAddEdge(nodeB, nodeC, "Some edge name"); + Edge anotherEdgeBC = root.GetOrAddEdge(nodeB, nodeC, "Another edge name"); + + // An edge name is optional and omitting it will result in a new nameless edge. + // There can be multiple nameless edges between any two nodes. + Edge edgeAB1 = root.GetOrAddEdge(nodeA, nodeB); + Edge edgeAB2 = root.GetOrAddEdge(nodeA, nodeB); + Assert.AreNotEqual(edgeAB1, edgeAB2); + + // We can attach attributes to nodes, edges and graphs to store information and instruct + // Graphviz by specifying layout parameters. At the moment we only support string + // attributes. Cgraph assumes that all objects of a given kind (graphs/subgraphs, nodes, or + // edges) have the same attributes. An attribute has to be introduced with a default value + // first for a certain kind, before we can use it. + Node.IntroduceAttribute(root, "my attribute", "defaultvalue"); + nodeA.SetAttribute("my attribute", "othervalue"); + + // Attributes are introduced per kind (Node, Edge, Graph) per root graph. + // So to be able to use "my attribute" on edges, we first have to introduce it as well. + Edge.IntroduceAttribute(root, "my attribute", "defaultvalue"); + edgeAB.SetAttribute("my attribute", "othervalue"); + + // To introduce and set an attribute at the same time, there are convenience wrappers. + edgeBC.SafeSetAttribute("arrowsize", "2.0", "1.0"); + // If we set an unintroduced attribute, the attribute will be introduced with an empty + // default value. + edgeBC.SetAttribute("new attr", "value"); + + // Some attributes - like "label" - accept HTML strings as value. + // To tell Graphviz that a string should be interpreted as HTML use the designated methods. + Node.IntroduceAttribute(root, "label", "defaultlabel"); + nodeB.SetAttributeHtml("label", "Some HTML string"); + + // We can simply export this graph to a text file in dot format. + root.ToDotFile(TestContext.CurrentContext.TestDirectory + "/out.dot"); + + // A word of advice, Graphviz doesn't play very well with empty strings. + // Try to avoid them when possible. (https://gitlab.com/graphviz/graphviz/-/issues/1887) + } + + [Test, Order(2)] + public void Layouting() + { + // If we have a given dot file (in this case the one we generated above), we can also read + // it back in. + RootGraph root = RootGraph.FromDotFile(TestContext.CurrentContext.TestDirectory + "/out.dot"); + + // We can ask Graphviz to compute a layout and render it to svg. + root.ToSvgFile(TestContext.CurrentContext.TestDirectory + "/dot.svg"); + + // We can use layout engines other than dot by explicitly passing the engine we want. + root.ToSvgFile(TestContext.CurrentContext.TestDirectory + "/neato.svg", LayoutEngines.Neato); + + // 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. 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")!; + PointD position = nodeA.GetPosition(); + Utils.AssertPattern(PointPattern, position.ToString()); + + 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")!; + 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 activeFillColor = System.Drawing.Color.Black; + foreach (var op in nodeA.GetDrawing()) { - // You can programmatically construct graphs as follows. - RootGraph root = RootGraph.CreateNew(GraphType.Directed, "Some Unique Identifier"); - // The graph name is optional, and can be omitted. The name is not interpreted by Graphviz, - // except it is recorded and preserved when the graph is written as a file. - - // The node names are unique identifiers within a graph in Graphviz. - Node nodeA = root.GetOrAddNode("A"); - Node nodeB = root.GetOrAddNode("B"); - Node nodeC = root.GetOrAddNode("C"); - - // The edge name is only unique between two nodes. - Edge edgeAB = root.GetOrAddEdge(nodeA, nodeB, "Some edge name"); - Edge edgeBC = root.GetOrAddEdge(nodeB, nodeC, "Some edge name"); - Edge anotherEdgeBC = root.GetOrAddEdge(nodeB, nodeC, "Another edge name"); - - // An edge name is optional and omitting it will result in a new nameless edge. - // There can be multiple nameless edges between any two nodes. - Edge edgeAB1 = root.GetOrAddEdge(nodeA, nodeB); - Edge edgeAB2 = root.GetOrAddEdge(nodeA, nodeB); - Assert.AreNotEqual(edgeAB1, edgeAB2); - - // We can attach attributes to nodes, edges and graphs to store information and instruct - // Graphviz by specifying layout parameters. At the moment we only support string - // attributes. Cgraph assumes that all objects of a given kind (graphs/subgraphs, nodes, or - // edges) have the same attributes. An attribute has to be introduced with a default value - // first for a certain kind, before we can use it. - Node.IntroduceAttribute(root, "my attribute", "defaultvalue"); - nodeA.SetAttribute("my attribute", "othervalue"); - - // Attributes are introduced per kind (Node, Edge, Graph) per root graph. - // So to be able to use "my attribute" on edges, we first have to introduce it as well. - Edge.IntroduceAttribute(root, "my attribute", "defaultvalue"); - edgeAB.SetAttribute("my attribute", "othervalue"); - - // To introduce and set an attribute at the same time, there are convenience wrappers. - edgeBC.SafeSetAttribute("arrowsize", "2.0", "1.0"); - // If we set an unintroduced attribute, the attribute will be introduced with an empty - // default value. - edgeBC.SetAttribute("new attr", "value"); - - // Some attributes - like "label" - accept HTML strings as value. - // To tell Graphviz that a string should be interpreted as HTML use the designated methods. - Node.IntroduceAttribute(root, "label", "defaultlabel"); - nodeB.SetAttributeHtml("label", "Some HTML string"); - - // We can simply export this graph to a text file in dot format. - root.ToDotFile(TestContext.CurrentContext.TestDirectory + "/out.dot"); - - // A word of advice, Graphviz doesn't play very well with empty strings. - // Try to avoid them when possible. (https://gitlab.com/graphviz/graphviz/-/issues/1887) + if (op is XDotOp.FillColor { Value: Color.Uniform { HtmlColor: var htmlColor } }) + { + activeFillColor = System.Drawing.ColorTranslator.FromHtml(htmlColor); + } + else if (op is XDotOp.FilledEllipse { Value: var boundingBox }) + { + Utils.AssertPattern(RectPattern, boundingBox.ToString()); + } + // Handle any xdot operation you require } - [Test, Order(2)] - public void Layouting() + foreach (var op in nodeA.GetLabelDrawing()) { - // If we have a given dot file (in this case the one we generated above), we can also read - // it back in. - RootGraph root = RootGraph.FromDotFile(TestContext.CurrentContext.TestDirectory + "/out.dot"); - - // We can ask Graphviz to compute a layout and render it to svg. - root.ToSvgFile(TestContext.CurrentContext.TestDirectory + "/dot_out.svg"); - - // We can use layout engines other than dot by explicitly passing the engine we want. - root.ToSvgFile(TestContext.CurrentContext.TestDirectory + "/neato_out.svg", LayoutEngines.Neato); - - // 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. 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")!; - PointD position = nodeA.GetPosition(); - Utils.AssertPattern(PointPattern, position.ToString()); - - 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")!; - 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 activeFillColor = System.Drawing.Color.Black; - foreach (var op in nodeA.GetDrawing()) - { - if (op is XDotOp.FillColor { Value: Color.Uniform { HtmlColor: var htmlColor } }) - { - activeFillColor = System.Drawing.ColorTranslator.FromHtml(htmlColor); - } - else if (op is XDotOp.FilledEllipse { Value: var boundingBox }) - { - Utils.AssertPattern(RectPattern, boundingBox.ToString()); - } - // Handle any xdot operation you require - } - - foreach (var op in nodeA.GetLabelDrawing()) - { - if (op is XDotOp.Text { Value: var text }) - { - 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 - } - - // These are just simple examples to showcase the structure of xdot operations. - // In reality the information can be much richer and more complex. + if (op is XDotOp.Text { Value: var text }) + { + 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 } - [Test, Order(3)] - public void Clusters() - { - RootGraph root = RootGraph.CreateNew(GraphType.Directed, "Graph with clusters"); - Node nodeA = root.GetOrAddNode("A"); - Node nodeB = root.GetOrAddNode("B"); - Node nodeC = root.GetOrAddNode("C"); - Node nodeD = root.GetOrAddNode("D"); - - // When a subgraph name is prefixed with cluster, - // the dot layout engine will render it as a box around the containing nodes. - SubGraph cluster1 = root.GetOrAddSubgraph("cluster_1"); - cluster1.AddExisting(nodeB); - cluster1.AddExisting(nodeC); - SubGraph cluster2 = root.GetOrAddSubgraph("cluster_2"); - cluster2.AddExisting(nodeD); - - // COMPOUND EDGES - // Graphviz does not really support edges from and to clusters. However, by adding an - // invisible dummynode and setting the ltail or lhead attributes of an edge this behavior - // can be faked. Graphviz will then draw an edge to the dummy node but clip it at the border - // of the cluster. We provide convenience methods for this. To enable this feature, Graphviz - // requires us to set the "compound" attribute to "true". - Graph.IntroduceAttribute(root, "compound", "true"); // Allow lhead/ltail - // The boolean indicates whether the dummy node should take up any space. When you pass - // false and you have a lot of edges, the edges may start to overlap a lot. - _ = root.GetOrAddEdge(nodeA, cluster1, false, "edge to a cluster"); - _ = root.GetOrAddEdge(cluster1, nodeD, false, "edge from a cluster"); - _ = root.GetOrAddEdge(cluster1, cluster1, false, "edge between clusters"); - - var layout = root.CreateLayout(); - - SubGraph cluster = layout.GetSubgraph("cluster_1")!; - RectangleD clusterbox = cluster.GetBoundingBox(); - RectangleD rootgraphbox = layout.GetBoundingBox(); - Utils.AssertPattern(RectPattern, clusterbox.ToString()); - Utils.AssertPattern(RectPattern, rootgraphbox.ToString()); - } - - [Test, Order(4)] - public void Records() - { - RootGraph root = RootGraph.CreateNew(GraphType.Directed, "Graph with records"); - Node nodeA = root.GetOrAddNode("A"); - 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)] - public void StringEscaping() - { - RootGraph root = RootGraph.CreateNew(GraphType.Directed, "Graph with escaped strings"); - Node.IntroduceAttribute(root, "label", "\\N"); - Node nodeA = root.GetOrAddNode("A"); - - // Several characters and character sequences can have special meanings in labels, like \N. - // When you want to have a literal string in a label, we provide a convenience function for - // you to do just that. - nodeA.SetAttribute("label", CGraphThing.EscapeLabel("Some string literal \\N \\n |}>")); - - // When defining portnames, some characters, like ':' and '|', are not allowed and they - // can't be escaped either. This can be troubling if you have an externally defined ID for - // such a port. We provide a function that maps strings to valid portnames. - var somePortId = "port id with :| special characters"; - var validPortName = Edge.ConvertUidToPortName(somePortId); - Node nodeB = root.GetOrAddNode("B"); - 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("|")); - } + // These are just simple examples to showcase the structure of xdot operations. + // In reality the information can be much richer and more complex. + } + + [Test, Order(3)] + public void Clusters() + { + RootGraph root = RootGraph.CreateNew(GraphType.Directed, "Graph with clusters"); + Node nodeA = root.GetOrAddNode("A"); + Node nodeB = root.GetOrAddNode("B"); + Node nodeC = root.GetOrAddNode("C"); + Node nodeD = root.GetOrAddNode("D"); + + // When a subgraph name is prefixed with cluster, + // the dot layout engine will render it as a box around the containing nodes. + SubGraph cluster1 = root.GetOrAddSubgraph("cluster_1"); + cluster1.AddExisting(nodeB); + cluster1.AddExisting(nodeC); + SubGraph cluster2 = root.GetOrAddSubgraph("cluster_2"); + cluster2.AddExisting(nodeD); + + // COMPOUND EDGES + // Graphviz does not really support edges from and to clusters. However, by adding an + // invisible dummynode and setting the ltail or lhead attributes of an edge this behavior + // can be faked. Graphviz will then draw an edge to the dummy node but clip it at the border + // of the cluster. We provide convenience methods for this. To enable this feature, Graphviz + // requires us to set the "compound" attribute to "true". + Graph.IntroduceAttribute(root, "compound", "true"); // Allow lhead/ltail + // The boolean indicates whether the dummy node should take up any space. When you pass + // false and you have a lot of edges, the edges may start to overlap a lot. + _ = root.GetOrAddEdge(nodeA, cluster1, false, "edge to a cluster"); + _ = root.GetOrAddEdge(cluster1, nodeD, false, "edge from a cluster"); + _ = root.GetOrAddEdge(cluster1, cluster1, false, "edge between clusters"); + + var layout = root.CreateLayout(); + + SubGraph cluster = layout.GetSubgraph("cluster_1")!; + RectangleD clusterbox = cluster.GetBoundingBox(); + RectangleD rootgraphbox = layout.GetBoundingBox(); + Utils.AssertPattern(RectPattern, clusterbox.ToString()); + Utils.AssertPattern(RectPattern, rootgraphbox.ToString()); + } + + [Test, Order(4)] + public void Records() + { + RootGraph root = RootGraph.CreateNew(GraphType.Directed, "Graph with records"); + Node nodeA = root.GetOrAddNode("A"); + 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)] + public void StringEscaping() + { + RootGraph root = RootGraph.CreateNew(GraphType.Directed, "Graph with escaped strings"); + Node.IntroduceAttribute(root, "label", "\\N"); + Node nodeA = root.GetOrAddNode("A"); + + // Several characters and character sequences can have special meanings in labels, like \N. + // When you want to have a literal string in a label, we provide a convenience function for + // you to do just that. + nodeA.SetAttribute("label", CGraphThing.EscapeLabel("Some string literal \\N \\n |}>")); + + // When defining portnames, some characters, like ':' and '|', are not allowed and they + // can't be escaped either. This can be troubling if you have an externally defined ID for + // such a port. We provide a function that maps strings to valid portnames. + var somePortId = "port id with :| special characters"; + var validPortName = Edge.ConvertUidToPortName(somePortId); + Node nodeB = root.GetOrAddNode("B"); + 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("|")); + } } diff --git a/justfile b/justfile index 5819696..b6bc371 100644 --- a/justfile +++ b/justfile @@ -82,5 +82,8 @@ check-diff: check-fixme: bash -c "! git grep 'FIX''NOW'" +check-tutorial-width: + awk 'length($0) > 100 { print FILENAME ":" NR ": " $0; found=1 } END { exit found }' Rubjerg.Graphviz.Test/Tutorial.cs + check-all: generate-readme normalize format check-diff check-fixme From c65cfb9062371ea953af1d7c4ad06444f94b3428 Mon Sep 17 00:00:00 2001 From: chtenb Date: Mon, 2 Jun 2025 15:52:50 +0200 Subject: [PATCH 9/9] update --- README.md | 412 +++++++++++++++++++++++++++--------------------------- justfile | 2 +- 2 files changed, 207 insertions(+), 207 deletions(-) diff --git a/README.md b/README.md index 8f9c888..35319ed 100644 --- a/README.md +++ b/README.md @@ -56,217 +56,217 @@ namespace Rubjerg.Graphviz.Test; [TestFixture()] 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 SplinePattern = - @"{X=[\d.]+, Y=[\d.]+}, {X=[\d.]+, Y=[\d.]+}, {X=[\d.]+, Y=[\d.]+}, {X=[\d.]+, Y=[\d.]+}"; - - [Test, Order(1)] - public void GraphConstruction() + public const string PointPattern = @"{X=[\d.]+, Y=[\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.]+}"; + + [Test, Order(1)] + public void GraphConstruction() + { + // You can programmatically construct graphs as follows. + RootGraph root = RootGraph.CreateNew(GraphType.Directed, "Some Unique Identifier"); + // The graph name is optional, and can be omitted. The name is not interpreted by Graphviz, + // except it is recorded and preserved when the graph is written as a file. + + // The node names are unique identifiers within a graph in Graphviz. + Node nodeA = root.GetOrAddNode("A"); + Node nodeB = root.GetOrAddNode("B"); + Node nodeC = root.GetOrAddNode("C"); + + // The edge name is only unique between two nodes. + Edge edgeAB = root.GetOrAddEdge(nodeA, nodeB, "Some edge name"); + Edge edgeBC = root.GetOrAddEdge(nodeB, nodeC, "Some edge name"); + Edge anotherEdgeBC = root.GetOrAddEdge(nodeB, nodeC, "Another edge name"); + + // An edge name is optional and omitting it will result in a new nameless edge. + // There can be multiple nameless edges between any two nodes. + Edge edgeAB1 = root.GetOrAddEdge(nodeA, nodeB); + Edge edgeAB2 = root.GetOrAddEdge(nodeA, nodeB); + Assert.AreNotEqual(edgeAB1, edgeAB2); + + // We can attach attributes to nodes, edges and graphs to store information and instruct + // Graphviz by specifying layout parameters. At the moment we only support string + // attributes. Cgraph assumes that all objects of a given kind (graphs/subgraphs, nodes, or + // edges) have the same attributes. An attribute has to be introduced with a default value + // first for a certain kind, before we can use it. + Node.IntroduceAttribute(root, "my attribute", "defaultvalue"); + nodeA.SetAttribute("my attribute", "othervalue"); + + // Attributes are introduced per kind (Node, Edge, Graph) per root graph. + // So to be able to use "my attribute" on edges, we first have to introduce it as well. + Edge.IntroduceAttribute(root, "my attribute", "defaultvalue"); + edgeAB.SetAttribute("my attribute", "othervalue"); + + // To introduce and set an attribute at the same time, there are convenience wrappers. + edgeBC.SafeSetAttribute("arrowsize", "2.0", "1.0"); + // If we set an unintroduced attribute, the attribute will be introduced with an empty + // default value. + edgeBC.SetAttribute("new attr", "value"); + + // Some attributes - like "label" - accept HTML strings as value. + // To tell Graphviz that a string should be interpreted as HTML use the designated methods. + Node.IntroduceAttribute(root, "label", "defaultlabel"); + nodeB.SetAttributeHtml("label", "Some HTML string"); + + // We can simply export this graph to a text file in dot format. + root.ToDotFile(TestContext.CurrentContext.TestDirectory + "/out.dot"); + + // A word of advice, Graphviz doesn't play very well with empty strings. + // Try to avoid them when possible. (https://gitlab.com/graphviz/graphviz/-/issues/1887) + } + + [Test, Order(2)] + public void Layouting() + { + // If we have a given dot file (in this case the one we generated above), we can also read + // it back in. + RootGraph root = RootGraph.FromDotFile(TestContext.CurrentContext.TestDirectory + "/out.dot"); + + // We can ask Graphviz to compute a layout and render it to svg. + root.ToSvgFile(TestContext.CurrentContext.TestDirectory + "/dot.svg"); + + // We can use layout engines other than dot by explicitly passing the engine we want. + root.ToSvgFile(TestContext.CurrentContext.TestDirectory + "/neato.svg", LayoutEngines.Neato); + + // 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. 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")!; + PointD position = nodeA.GetPosition(); + Utils.AssertPattern(PointPattern, position.ToString()); + + 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")!; + 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 activeFillColor = System.Drawing.Color.Black; + foreach (var op in nodeA.GetDrawing()) { - // You can programmatically construct graphs as follows. - RootGraph root = RootGraph.CreateNew(GraphType.Directed, "Some Unique Identifier"); - // The graph name is optional, and can be omitted. The name is not interpreted by Graphviz, - // except it is recorded and preserved when the graph is written as a file. - - // The node names are unique identifiers within a graph in Graphviz. - Node nodeA = root.GetOrAddNode("A"); - Node nodeB = root.GetOrAddNode("B"); - Node nodeC = root.GetOrAddNode("C"); - - // The edge name is only unique between two nodes. - Edge edgeAB = root.GetOrAddEdge(nodeA, nodeB, "Some edge name"); - Edge edgeBC = root.GetOrAddEdge(nodeB, nodeC, "Some edge name"); - Edge anotherEdgeBC = root.GetOrAddEdge(nodeB, nodeC, "Another edge name"); - - // An edge name is optional and omitting it will result in a new nameless edge. - // There can be multiple nameless edges between any two nodes. - Edge edgeAB1 = root.GetOrAddEdge(nodeA, nodeB); - Edge edgeAB2 = root.GetOrAddEdge(nodeA, nodeB); - Assert.AreNotEqual(edgeAB1, edgeAB2); - - // We can attach attributes to nodes, edges and graphs to store information and instruct - // Graphviz by specifying layout parameters. At the moment we only support string - // attributes. Cgraph assumes that all objects of a given kind (graphs/subgraphs, nodes, or - // edges) have the same attributes. An attribute has to be introduced with a default value - // first for a certain kind, before we can use it. - Node.IntroduceAttribute(root, "my attribute", "defaultvalue"); - nodeA.SetAttribute("my attribute", "othervalue"); - - // Attributes are introduced per kind (Node, Edge, Graph) per root graph. - // So to be able to use "my attribute" on edges, we first have to introduce it as well. - Edge.IntroduceAttribute(root, "my attribute", "defaultvalue"); - edgeAB.SetAttribute("my attribute", "othervalue"); - - // To introduce and set an attribute at the same time, there are convenience wrappers. - edgeBC.SafeSetAttribute("arrowsize", "2.0", "1.0"); - // If we set an unintroduced attribute, the attribute will be introduced with an empty - // default value. - edgeBC.SetAttribute("new attr", "value"); - - // Some attributes - like "label" - accept HTML strings as value. - // To tell Graphviz that a string should be interpreted as HTML use the designated methods. - Node.IntroduceAttribute(root, "label", "defaultlabel"); - nodeB.SetAttributeHtml("label", "Some HTML string"); - - // We can simply export this graph to a text file in dot format. - root.ToDotFile(TestContext.CurrentContext.TestDirectory + "/out.dot"); - - // A word of advice, Graphviz doesn't play very well with empty strings. - // Try to avoid them when possible. (https://gitlab.com/graphviz/graphviz/-/issues/1887) + if (op is XDotOp.FillColor { Value: Color.Uniform { HtmlColor: var htmlColor } }) + { + activeFillColor = System.Drawing.ColorTranslator.FromHtml(htmlColor); + } + else if (op is XDotOp.FilledEllipse { Value: var boundingBox }) + { + Utils.AssertPattern(RectPattern, boundingBox.ToString()); + } + // Handle any xdot operation you require } - [Test, Order(2)] - public void Layouting() + foreach (var op in nodeA.GetLabelDrawing()) { - // If we have a given dot file (in this case the one we generated above), we can also read - // it back in. - RootGraph root = RootGraph.FromDotFile(TestContext.CurrentContext.TestDirectory + "/out.dot"); - - // We can ask Graphviz to compute a layout and render it to svg. - root.ToSvgFile(TestContext.CurrentContext.TestDirectory + "/dot_out.svg"); - - // We can use layout engines other than dot by explicitly passing the engine we want. - root.ToSvgFile(TestContext.CurrentContext.TestDirectory + "/neato_out.svg", LayoutEngines.Neato); - - // 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. 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")!; - PointD position = nodeA.GetPosition(); - Utils.AssertPattern(PointPattern, position.ToString()); - - 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")!; - 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 activeFillColor = System.Drawing.Color.Black; - foreach (var op in nodeA.GetDrawing()) - { - if (op is XDotOp.FillColor { Value: Color.Uniform { HtmlColor: var htmlColor } }) - { - activeFillColor = System.Drawing.ColorTranslator.FromHtml(htmlColor); - } - else if (op is XDotOp.FilledEllipse { Value: var boundingBox }) - { - Utils.AssertPattern(RectPattern, boundingBox.ToString()); - } - // Handle any xdot operation you require - } - - foreach (var op in nodeA.GetLabelDrawing()) - { - if (op is XDotOp.Text { Value: var text }) - { - 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 - } - - // These are just simple examples to showcase the structure of xdot operations. - // In reality the information can be much richer and more complex. + if (op is XDotOp.Text { Value: var text }) + { + 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 } - [Test, Order(3)] - public void Clusters() - { - RootGraph root = RootGraph.CreateNew(GraphType.Directed, "Graph with clusters"); - Node nodeA = root.GetOrAddNode("A"); - Node nodeB = root.GetOrAddNode("B"); - Node nodeC = root.GetOrAddNode("C"); - Node nodeD = root.GetOrAddNode("D"); - - // When a subgraph name is prefixed with cluster, - // the dot layout engine will render it as a box around the containing nodes. - SubGraph cluster1 = root.GetOrAddSubgraph("cluster_1"); - cluster1.AddExisting(nodeB); - cluster1.AddExisting(nodeC); - SubGraph cluster2 = root.GetOrAddSubgraph("cluster_2"); - cluster2.AddExisting(nodeD); - - // COMPOUND EDGES - // Graphviz does not really support edges from and to clusters. However, by adding an - // invisible dummynode and setting the ltail or lhead attributes of an edge this behavior - // can be faked. Graphviz will then draw an edge to the dummy node but clip it at the border - // of the cluster. We provide convenience methods for this. To enable this feature, Graphviz - // requires us to set the "compound" attribute to "true". - Graph.IntroduceAttribute(root, "compound", "true"); // Allow lhead/ltail - // The boolean indicates whether the dummy node should take up any space. When you pass - // false and you have a lot of edges, the edges may start to overlap a lot. - _ = root.GetOrAddEdge(nodeA, cluster1, false, "edge to a cluster"); - _ = root.GetOrAddEdge(cluster1, nodeD, false, "edge from a cluster"); - _ = root.GetOrAddEdge(cluster1, cluster1, false, "edge between clusters"); - - var layout = root.CreateLayout(); - - SubGraph cluster = layout.GetSubgraph("cluster_1")!; - RectangleD clusterbox = cluster.GetBoundingBox(); - RectangleD rootgraphbox = layout.GetBoundingBox(); - Utils.AssertPattern(RectPattern, clusterbox.ToString()); - Utils.AssertPattern(RectPattern, rootgraphbox.ToString()); - } - - [Test, Order(4)] - public void Records() - { - RootGraph root = RootGraph.CreateNew(GraphType.Directed, "Graph with records"); - Node nodeA = root.GetOrAddNode("A"); - 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)] - public void StringEscaping() - { - RootGraph root = RootGraph.CreateNew(GraphType.Directed, "Graph with escaped strings"); - Node.IntroduceAttribute(root, "label", "\\N"); - Node nodeA = root.GetOrAddNode("A"); - - // Several characters and character sequences can have special meanings in labels, like \N. - // When you want to have a literal string in a label, we provide a convenience function for - // you to do just that. - nodeA.SetAttribute("label", CGraphThing.EscapeLabel("Some string literal \\N \\n |}>")); - - // When defining portnames, some characters, like ':' and '|', are not allowed and they - // can't be escaped either. This can be troubling if you have an externally defined ID for - // such a port. We provide a function that maps strings to valid portnames. - var somePortId = "port id with :| special characters"; - var validPortName = Edge.ConvertUidToPortName(somePortId); - Node nodeB = root.GetOrAddNode("B"); - 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("|")); - } + // These are just simple examples to showcase the structure of xdot operations. + // In reality the information can be much richer and more complex. + } + + [Test, Order(3)] + public void Clusters() + { + RootGraph root = RootGraph.CreateNew(GraphType.Directed, "Graph with clusters"); + Node nodeA = root.GetOrAddNode("A"); + Node nodeB = root.GetOrAddNode("B"); + Node nodeC = root.GetOrAddNode("C"); + Node nodeD = root.GetOrAddNode("D"); + + // When a subgraph name is prefixed with cluster, + // the dot layout engine will render it as a box around the containing nodes. + SubGraph cluster1 = root.GetOrAddSubgraph("cluster_1"); + cluster1.AddExisting(nodeB); + cluster1.AddExisting(nodeC); + SubGraph cluster2 = root.GetOrAddSubgraph("cluster_2"); + cluster2.AddExisting(nodeD); + + // COMPOUND EDGES + // Graphviz does not really support edges from and to clusters. However, by adding an + // invisible dummynode and setting the ltail or lhead attributes of an edge this behavior + // can be faked. Graphviz will then draw an edge to the dummy node but clip it at the border + // of the cluster. We provide convenience methods for this. To enable this feature, Graphviz + // requires us to set the "compound" attribute to "true". + Graph.IntroduceAttribute(root, "compound", "true"); // Allow lhead/ltail + // The boolean indicates whether the dummy node should take up any space. When you pass + // false and you have a lot of edges, the edges may start to overlap a lot. + _ = root.GetOrAddEdge(nodeA, cluster1, false, "edge to a cluster"); + _ = root.GetOrAddEdge(cluster1, nodeD, false, "edge from a cluster"); + _ = root.GetOrAddEdge(cluster1, cluster1, false, "edge between clusters"); + + var layout = root.CreateLayout(); + + SubGraph cluster = layout.GetSubgraph("cluster_1")!; + RectangleD clusterbox = cluster.GetBoundingBox(); + RectangleD rootgraphbox = layout.GetBoundingBox(); + Utils.AssertPattern(RectPattern, clusterbox.ToString()); + Utils.AssertPattern(RectPattern, rootgraphbox.ToString()); + } + + [Test, Order(4)] + public void Records() + { + RootGraph root = RootGraph.CreateNew(GraphType.Directed, "Graph with records"); + Node nodeA = root.GetOrAddNode("A"); + 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)] + public void StringEscaping() + { + RootGraph root = RootGraph.CreateNew(GraphType.Directed, "Graph with escaped strings"); + Node.IntroduceAttribute(root, "label", "\\N"); + Node nodeA = root.GetOrAddNode("A"); + + // Several characters and character sequences can have special meanings in labels, like \N. + // When you want to have a literal string in a label, we provide a convenience function for + // you to do just that. + nodeA.SetAttribute("label", CGraphThing.EscapeLabel("Some string literal \\N \\n |}>")); + + // When defining portnames, some characters, like ':' and '|', are not allowed and they + // can't be escaped either. This can be troubling if you have an externally defined ID for + // such a port. We provide a function that maps strings to valid portnames. + var somePortId = "port id with :| special characters"; + var validPortName = Edge.ConvertUidToPortName(somePortId); + Node nodeB = root.GetOrAddNode("B"); + 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("|")); + } } ``` diff --git a/justfile b/justfile index b6bc371..0f7be33 100644 --- a/justfile +++ b/justfile @@ -85,5 +85,5 @@ check-fixme: check-tutorial-width: awk 'length($0) > 100 { print FILENAME ":" NR ": " $0; found=1 } END { exit found }' Rubjerg.Graphviz.Test/Tutorial.cs -check-all: generate-readme normalize format check-diff check-fixme +check-all: generate-readme normalize format check-diff check-fixme check-tutorial-width