Skip to content

Commit a6f13e9

Browse files
committed
Add scriptable Graphviz export
1 parent 0e4c1eb commit a6f13e9

7 files changed

Lines changed: 431 additions & 34 deletions

File tree

PSGraph.Tests/ExportGraphViewCmdletTests.cs

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,132 @@ public void ExportGraph_GraphvizFormat_RespectsGraphRenderProperties()
118118
dot.Should().Contain("splines=line");
119119
}
120120

121+
[Fact]
122+
public void ExportGraph_GraphvizFormat_AppliesGraphScriptAttributes()
123+
{
124+
var graph = CreateSampleGraph();
125+
var filePath = Path.Combine(_tempDirectory, "graph-script.dot");
126+
127+
_powershell.AddCommand("Export-Graph")
128+
.AddParameter("Graph", graph)
129+
.AddParameter("Format", GraphExportTypes.Graphviz)
130+
.AddParameter("GraphScript", ScriptBlock.Create("@{ rankdir = 'LR'; label = 'Micrograd' }"))
131+
.AddParameter("Path", filePath);
132+
133+
_powershell.Invoke();
134+
_powershell.Commands.Clear();
135+
136+
_powershell.HadErrors.Should().BeFalse();
137+
var dot = File.ReadAllText(filePath);
138+
dot.Should().Contain("rankdir=LR");
139+
dot.Should().Contain("label=\"Micrograd\"");
140+
}
141+
142+
[Fact]
143+
public void ExportGraph_GraphvizFormat_AppliesVertexScriptToOriginalObject()
144+
{
145+
var filePath = Path.Combine(_tempDirectory, "vertex-script.dot");
146+
147+
_powershell.Runspace.SessionStateProxy.SetVariable("path", filePath);
148+
_powershell.AddScript(
149+
"""
150+
$g = New-Graph
151+
$a = [pscustomobject]@{ type = 'value'; Name = 'a'; data = 2.0; grad = 6.0 }
152+
$b = [pscustomobject]@{ type = 'operation'; Name = '*' }
153+
Add-Edge -Graph $g -From $a -To $b
154+
Export-Graph -Graph $g -Format Graphviz -Path $path -VertexScript {
155+
$item = $_
156+
switch ($item.type) {
157+
'value' {
158+
@{
159+
label = "{ $($item.Name) | data $('{0:F4}' -f $item.data) | grad $('{0:F4}' -f $item.grad) }"
160+
shape = 'record'
161+
}
162+
}
163+
'operation' {
164+
@{
165+
label = $item.Name
166+
shape = 'ellipse'
167+
}
168+
}
169+
}
170+
}
171+
""");
172+
173+
_powershell.Invoke();
174+
_powershell.Commands.Clear();
175+
176+
_powershell.HadErrors.Should().BeFalse();
177+
var dot = File.ReadAllText(filePath);
178+
dot.Should().Contain("shape=record");
179+
dot.Should().Contain("\"{ a | data 2.0000 | grad 6.0000 }\"");
180+
dot.Should().Contain("shape=ellipse");
181+
dot.Should().Contain("label=\"*\"");
182+
}
183+
184+
[Fact]
185+
public void ExportGraph_GraphvizFormat_AppliesEdgeScriptAttributes()
186+
{
187+
var graph = CreateSampleGraph();
188+
var filePath = Path.Combine(_tempDirectory, "edge-script.dot");
189+
190+
_powershell.AddCommand("Export-Graph")
191+
.AddParameter("Graph", graph)
192+
.AddParameter("Format", GraphExportTypes.Graphviz)
193+
.AddParameter("EdgeScript", ScriptBlock.Create("@{ label = \"$($Source.Label)-to-$($Target.Label)\"; style = 'dashed' }"))
194+
.AddParameter("Path", filePath);
195+
196+
_powershell.Invoke();
197+
_powershell.Commands.Clear();
198+
199+
_powershell.HadErrors.Should().BeFalse();
200+
var dot = File.ReadAllText(filePath);
201+
dot.Should().Contain("style=dashed");
202+
dot.Should().Contain("label=\"A-to-B\"");
203+
}
204+
205+
[Fact]
206+
public void ExportGraph_GraphvizFormat_AcceptsPSCustomObjectScriptOutput()
207+
{
208+
var graph = CreateSampleGraph();
209+
var filePath = Path.Combine(_tempDirectory, "pscustomobject-script.dot");
210+
211+
_powershell.AddCommand("Export-Graph")
212+
.AddParameter("Graph", graph)
213+
.AddParameter("Format", GraphExportTypes.Graphviz)
214+
.AddParameter("GraphScript", ScriptBlock.Create("[pscustomobject]@{ rankdir = 'LR'; label = 'ObjectOutput' }"))
215+
.AddParameter("Path", filePath);
216+
217+
_powershell.Invoke();
218+
_powershell.Commands.Clear();
219+
220+
_powershell.HadErrors.Should().BeFalse();
221+
var dot = File.ReadAllText(filePath);
222+
dot.Should().Contain("rankdir=LR");
223+
dot.Should().Contain("label=\"ObjectOutput\"");
224+
}
225+
226+
[Fact]
227+
public void ExportGraph_GraphvizFormat_IgnoresUnknownScriptAttributes()
228+
{
229+
var graph = CreateSampleGraph();
230+
var filePath = Path.Combine(_tempDirectory, "unknown-script-attribute.dot");
231+
232+
_powershell.AddCommand("Export-Graph")
233+
.AddParameter("Graph", graph)
234+
.AddParameter("Format", GraphExportTypes.Graphviz)
235+
.AddParameter("GraphScript", ScriptBlock.Create("@{ notARealGraphvizProperty = 'ignored'; rankdir = 'LR' }"))
236+
.AddParameter("Path", filePath);
237+
238+
_powershell.Invoke();
239+
_powershell.Commands.Clear();
240+
241+
_powershell.HadErrors.Should().BeFalse();
242+
var dot = File.ReadAllText(filePath);
243+
dot.Should().Contain("rankdir=LR");
244+
dot.Should().NotContain("notARealGraphvizProperty");
245+
}
246+
121247
[Fact]
122248
public void ExportGraph_GraphMLFormat_WritesGraphMlFile()
123249
{
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
$ErrorActionPreference = 'Stop'
2+
3+
$g = New-Graph
4+
5+
$a = [pscustomobject]@{ type = 'value'; Name = 'a'; data = 2.0; grad = 6.0 }
6+
$b = [pscustomobject]@{ type = 'value'; Name = 'b'; data = -3.0; grad = -4.0 }
7+
$mul = [pscustomobject]@{ type = 'operation'; Name = '*' }
8+
$e = [pscustomobject]@{ type = 'value'; Name = 'e'; data = -6.0; grad = -2.0 }
9+
10+
Add-Edge -Graph $g -From $a -To $mul | Out-Null
11+
Add-Edge -Graph $g -From $b -To $mul | Out-Null
12+
Add-Edge -Graph $g -From $mul -To $e | Out-Null
13+
14+
$dot = Export-Graph -Graph $g -Format Graphviz `
15+
-GraphScript {
16+
@{
17+
rankdir = 'LR'
18+
label = 'Micrograd'
19+
}
20+
} `
21+
-VertexScript {
22+
$item = $_
23+
24+
switch ($item.type) {
25+
'value' {
26+
@{
27+
label = "{ $($item.Name) | data $('{0:F4}' -f $item.data) | grad $('{0:F4}' -f $item.grad) }"
28+
shape = 'record'
29+
}
30+
}
31+
'operation' {
32+
@{
33+
label = $item.Name
34+
shape = 'ellipse'
35+
}
36+
}
37+
}
38+
}
39+
40+
$dot
41+
42+
# If PSGraphView is available:
43+
# $svg = Export-GraphvizView -InputObject $dot -Renderer Dot -As Svg
44+
# Display $svg 'image/svg+xml'

PSGraph/cmdlets/graph/AddEdgeCmdLet.cs

Lines changed: 2 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -33,27 +33,8 @@ protected override void ProcessRecord()
3333

3434
void ProcessRecordDefault()
3535
{
36-
PSVertex? newFrom = null;
37-
PSVertex? newTo = null;
38-
39-
if (From.ImmediateBaseObject is PSVertex)
40-
{
41-
newFrom = (PSVertex)From.ImmediateBaseObject;
42-
}
43-
else
44-
{
45-
newFrom = new PSVertex(From.ImmediateBaseObject.ToString(), From.ImmediateBaseObject);
46-
}
47-
48-
if (To.ImmediateBaseObject is PSVertex)
49-
{
50-
newTo = (PSVertex)To.ImmediateBaseObject;
51-
}
52-
else
53-
{
54-
newTo = new PSVertex(To.ImmediateBaseObject.ToString(), To.ImmediateBaseObject);
55-
}
56-
36+
var newFrom = PSVertexFactory.FromPSObject(From);
37+
var newTo = PSVertexFactory.FromPSObject(To);
5738

5839
var edge = new PSEdge(newFrom, newTo, new PSEdgeTag(Tag?.ToString()));
5940
var result = Graph.AddVerticesAndEdge(edge);

PSGraph/cmdlets/graph/AddVertexCmdlet.cs

Lines changed: 1 addition & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -20,18 +20,7 @@ public class AddVertexCmdlet : PSCmdlet
2020
protected override void ProcessRecord()
2121
{
2222

23-
//var v = ((PSObject) Vertex).ImmediateBaseObject;
24-
25-
PSVertex? newPSVertex = null;
26-
27-
if (Vertex.ImmediateBaseObject is PSVertex)
28-
{
29-
newPSVertex = (PSVertex)Vertex.ImmediateBaseObject;
30-
}
31-
else
32-
{
33-
newPSVertex = new PSVertex(Vertex.ImmediateBaseObject.ToString(), Vertex.ImmediateBaseObject);
34-
}
23+
var newPSVertex = PSVertexFactory.FromPSObject(Vertex);
3524

3625
var result = Graph.AddVertex(newPSVertex);
3726
WriteVerbose(result.ToString());

0 commit comments

Comments
 (0)