diff --git a/CHANGELOG.md b/CHANGELOG.md index 8e787e6..76cfe16 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,39 @@ All notable changes to PSGraph are documented here. This file starts at (2.1.38, August 2019), the point at which `themodulecollective/PSGraph` forked from the then-dormant `KevinMarquette/PSGraph`. +## 3.2.0 - 2026-08-30 + +### Added + +- `Graph -Strict` emits `strict digraph`/`strict graph`, telling GraphViz + to merge duplicate edges instead of drawing them separately. +- `New-GraphAttributeSet` (alias `GraphAttributes`): a graph/cluster-level + counterpart to `New-NodeAttributeSet`/`New-EdgeAttributeSet`, exposing + `-RankDir`, `-Splines`, `-BgColor`/`-GradientAngle`/`-Style radial` + (gradient fills), `-NodeSep`/`-RankSep`, `-Concentrate`, `-Compound`, + `-ColorScheme`, `-Ratio`, `-Size`, and graph-level font/label + attributes, with the same tab completion as the existing two builders. + `rankdir` previously had no dedicated parameter anywhere in the module. +- `New-NodeAttributeSet`: added `-Peripheries`, `-GradientAngle`, + `-Tooltip`, `-URL` (alias `-Href`), `-XLabel`, `-ColorScheme`; added + `'radial'` to `-Style` (gradient fills) and `'record'`/`'Mrecord'` to + `-Shape`. +- `New-EdgeAttributeSet`: added `-Weight`, `-MinLen`, `-Tooltip`, `-URL` + (alias `-Href`), `-XLabel`. +- `Export-PSGraph -OutputFormat`: widened the `ValidateSet` from 10 to 26 + formats (adds `svgz`, `svg_inline`, `eps`, `ps`, `ps2`, `xdot`, + `dot_json`, `xdot_json`, `json0`, `canon`, `gv`, `fig`, `bmp`, + `tif`/`tiff`, `wbmp`, `pic`, `plain-ext`) — the previous list rejected + several valid `dot -T` values outright. Formats needing extra native + libraries not universally present across platforms (`webp`, `gd`, + `gd2`, `pov`, `gtk`, `xlib`, `exr`, `psd`) are deliberately excluded; + actual availability always depends on the local build (`dot -T?`). Two + formats from the original plan (`vml`, `vmlz`) turned out to no longer + be present in current Graphviz output-format lists at all and were + dropped rather than added speculatively. +- `Export-PSGraph -LayoutEngine`: added `osage` and `patchwork`, the two + Graphviz layout engines that were previously unreachable. + ## 3.1.0 - 2026-08-29 ### Added diff --git a/PSGraph/PSGraph.psd1 b/PSGraph/PSGraph.psd1 index 138211f..f2a39db 100644 --- a/PSGraph/PSGraph.psd1 +++ b/PSGraph/PSGraph.psd1 @@ -12,7 +12,7 @@ RootModule = 'PSGraph.psm1' # Version number of this module. - ModuleVersion = '3.1.0' + ModuleVersion = '3.2.0' # Supported PSEditions CompatiblePSEditions = @('Desktop', 'Core') @@ -74,8 +74,8 @@ # delete the entry, use an empty array if there are no functions to export. FunctionsToExport = @( 'Cells', 'Edge', 'Entity', 'Export-PSGraph', 'Graph', 'Inline', 'Install-GraphViz', - 'New-EdgeAttributeSet', 'New-NodeAttributeSet', 'Node', 'Rank', 'Record', 'Row', - 'Set-NodeFormatScript', 'Show-PSGraph', 'SubGraph' + 'New-EdgeAttributeSet', 'New-GraphAttributeSet', 'New-NodeAttributeSet', 'Node', 'Rank', + 'Record', 'Row', 'Set-NodeFormatScript', 'Show-PSGraph', 'SubGraph' ) # Cmdlets to export from this module, for best performance, do not use wildcards and do not @@ -88,7 +88,7 @@ # Aliases to export from this module, for best performance, do not use wildcards and do not # delete the entry, use an empty array if there are no aliases to export. AliasesToExport = @( - 'digraph', 'NodeAttributes', 'EdgeAttributes', + 'digraph', 'NodeAttributes', 'EdgeAttributes', 'GraphAttributes', 'jpgGraph', 'pngGraph', 'gifGraph', 'imapGraph', 'cmapxGraph', 'jp2Graph', 'jsonGraph', 'pdfGraph', 'plainGraph', 'dotGraph', 'svgGraph' ) @@ -122,6 +122,26 @@ # ReleaseNotes of this module ReleaseNotes = @' +3.2.0 20260830 +* Export-PSGraph: -OutputFormat now accepts a much wider set of GraphViz + output formats (svgz, svg_inline, eps, ps, ps2, xdot, dot_json, + xdot_json, json0, canon, gv, fig, bmp, tif/tiff, wbmp, pic, plain-ext) + in addition to the original 10 - actual availability still depends on + how the local GraphViz build was compiled (see `dot -T?`) +* Export-PSGraph: -LayoutEngine now accepts osage and patchwork, the two + packing-layout engines that were previously unreachable +* Graph: added -Strict switch, emitting 'strict digraph'/'strict graph' + so GraphViz merges duplicate edges +* New-GraphAttributeSet (alias GraphAttributes): new command for + building case-correct GraphViz graph/cluster attribute hashtables + (RankDir, Splines, BgColor, gradients, spacing, ...), with the same + tab completion as the Node/Edge attribute-set builders +* New-NodeAttributeSet: added -Peripheries, -GradientAngle, -Tooltip, + -URL/-Href, -XLabel, -ColorScheme; added 'radial' to -Style and + 'record'/'Mrecord' to -Shape +* New-EdgeAttributeSet: added -Weight, -MinLen, -Tooltip, -URL/-Href, + -XLabel + 3.1.0 20260829 * Format-Value: fixed HTML-like label detection so any valid GraphViz HTML-like label round-trips, not just ones starting with b edge into one. + .Notes The output is a string so it can be saved to a variable or piped to other commands #> @@ -97,7 +106,12 @@ support; it isn't an oversight." # Keyword that initiates the graph [string] - $Type = 'digraph' + $Type = 'digraph', + + # Emits 'strict digraph'/'strict graph', which tells GraphViz to merge duplicate edges + # instead of drawing them separately. Only meaningful on the top-level graph. + [switch] + $Strict ) begin @@ -115,7 +129,8 @@ support; it isn't an oversight." $script:SubGraphList = @{} } - "{0}{1} {2} {{" -f (Get-Indent), $Type, $name + $typeKeyword = if ( $Strict ) { "strict $Type" } else { $Type } + "{0}{1} {2} {{" -f (Get-Indent), $typeKeyword, $name $script:indent++ if ($null -ne $Attributes) diff --git a/PSGraph/Public/New-EdgeAttributeSet.ps1 b/PSGraph/Public/New-EdgeAttributeSet.ps1 index 47a358b..4528b9f 100644 --- a/PSGraph/Public/New-EdgeAttributeSet.ps1 +++ b/PSGraph/Public/New-EdgeAttributeSet.ps1 @@ -99,6 +99,10 @@ doesn't change any external/persistent state, so ShouldProcess doesn't apply." [double] $Length, + # Minimum rank difference (in ranks) enforced between the edge's two nodes + [int16] + $MinLen, + # Width of the pen, in points, used to draw lines and curves [double] $PenWidth, @@ -110,7 +114,24 @@ doesn't change any external/persistent state, so ShouldProcess doesn't apply." # Text label placed near the tail of the edge [string] - $TailLabel + $TailLabel, + + # Tooltip text shown on hover in SVG/interactive output formats + [string] + $Tooltip, + + # Hyperlink attached to the edge in SVG/PostScript/map output formats + [Alias('Href')] + [string] + $URL, + + # How strongly GraphViz's layout should try to keep the edge close to its preferred length + [double] + $Weight, + + # External label placed near the edge without affecting layout + [string] + $XLabel ) $values = @{} @@ -137,7 +158,8 @@ doesn't change any external/persistent state, so ShouldProcess doesn't apply." # Passed through unchanged - numeric, boolean, or free-form text where case is meaningful $passthroughParams = @( 'ArrowSize', 'Constraint', 'FontName', 'FontSize', 'HeadLabel', 'Label', - 'LabelFontName', 'LabelFontSize', 'PenWidth', 'TailLabel' + 'LabelFontName', 'LabelFontSize', 'MinLen', 'PenWidth', 'TailLabel', 'Tooltip', 'URL', + 'Weight', 'XLabel' ) foreach ($param in $passthroughParams) { diff --git a/PSGraph/Public/New-GraphAttributeSet.ps1 b/PSGraph/Public/New-GraphAttributeSet.ps1 new file mode 100644 index 0000000..3ec62fb --- /dev/null +++ b/PSGraph/Public/New-GraphAttributeSet.ps1 @@ -0,0 +1,151 @@ +function New-GraphAttributeSet +{ + <# + .SYNOPSIS + Builds a GraphViz attribute hashtable for the Graph/SubGraph commands. + + .DESCRIPTION + Graph and SubGraph take a hashtable of attributes, but GraphViz attribute names and + values are case-sensitive and easy to get wrong ('blue' works, 'Blue' does not). This + command exposes the common graph-level attributes (rankdir, splines, background, + spacing, ...) as PowerShell parameters - with tab completion for color/font values - and + normalizes casing for the ones GraphViz requires lowercase. + + .EXAMPLE + $graphAttributeSetSplat = @{ + RankDir = 'LR' + BgColor = 'lightyellow' + FontName = 'Calibri' + } + $attrs = New-GraphAttributeSet @graphAttributeSetSplat + graph g -Attributes $attrs { edge a b } + + .NOTES + Follows the same lowercase/passthrough split as New-NodeAttributeSet and + New-EdgeAttributeSet. RankDir's TB/LR/BT/RL values are left in their required + uppercase form - unlike most other GraphViz enum values, rankdir is not lowercase. + #> + [Diagnostics.CodeAnalysis.SuppressMessageAttribute( + "PSUseShouldProcessForStateChangingFunctions", "", + Justification = "Despite the New- verb, this builds and returns a hashtable in memory - it +doesn't change any external/persistent state, so ShouldProcess doesn't apply." + )] + [CmdletBinding()] + [Alias('GraphAttributes')] + [OutputType([hashtable])] + param( + # Background color for the graph/cluster; supports a two-color 'c1:c2' gradient + [string] + $BgColor, + + # Namespace GraphViz resolves BgColor/FontColor small-integer values against, + # e.g. 'blues9' (a Brewer palette) or 'x11' (the default). No tab completion is provided - + # see https://graphviz.org/doc/info/colors.html#brewer for the full scheme list. + [string] + $ColorScheme, + + # If false, forces edges between clusters to attach to the actual node instead of the + # cluster boundary, even when Edge would otherwise rewrite them with lhead/ltail + [bool] + $Compound, + + # Merges edges sharing an endpoint into a single line where the layout allows it + [switch] + $Concentrate, + + # Font color used for the graph's Label + [string] + $FontColor, + + # Font used for the graph's Label + [string] + $FontName, + + # Font size, in points, used for the graph's Label + [double] + $FontSize, + + # Angle, in degrees, controlling the direction of a BgColor gradient fill + [double] + $GradientAngle, + + # Text label for the graph/cluster + [string] + $Label, + + # Placement of Label: 't' (top) or 'b' (bottom) + [ValidateSet('t', 'b')] + [string] + $LabelLoc, + + # Minimum space, in inches, between adjacent nodes on the same rank + [double] + $NodeSep, + + # Direction of graph layout: top-to-bottom, left-to-right, bottom-to-top, right-to-left + [ValidateSet('TB', 'LR', 'BT', 'RL')] + [string] + $RankDir, + + # Minimum space, in inches, between adjacent ranks + [double] + $RankSep, + + # Aspect-ratio hint for the final layout - a number (e.g. 0.5) or a keyword such as + # 'fill'/'compress'/'expand'/'auto' + [string] + $Ratio, + + # Maximum drawing size, e.g. '8,8' or '8,8!' (the trailing '!' forces scaling up too) + [string] + $Size, + + # Splines/edge-routing mode, e.g. curved, ortho, polyline, none + [ValidateSet('line', 'polyline', 'curved', 'ortho', 'spline', 'none', 'true', 'false')] + [string] + $Splines, + + # Style for the graph/cluster background, e.g. filled, rounded, radial (gradient fill) + [ValidateSet('filled', 'striped', 'rounded', 'radial')] + [string] + $Style + ) + + $values = @{} + + # GraphViz requires these lowercase; user input may not be + $lowercaseParams = @('BgColor', 'ColorScheme', 'FontColor', 'Splines', 'Style') + foreach ($param in $lowercaseParams) + { + if ($PSBoundParameters.ContainsKey($param)) + { + $values[$param.ToLower()] = $PSBoundParameters[$param].ToLower() + } + } + + # Passed through unchanged - numeric, boolean, free-form text, or an enum GraphViz requires + # in a case other than lowercase (RankDir's TB/LR/BT/RL) + $passthroughParams = @( + 'FontName', 'FontSize', 'GradientAngle', 'Label', 'LabelLoc', 'NodeSep', 'RankDir', + 'RankSep', 'Ratio', 'Size' + ) + foreach ($param in $passthroughParams) + { + if ($PSBoundParameters.ContainsKey($param)) + { + $values[$param.ToLower()] = $PSBoundParameters[$param] + } + } + + if ($PSBoundParameters.ContainsKey('Compound')) + { + $values['compound'] = $Compound + } + + if ($Concentrate) + { + $values['concentrate'] = $true + } + + $values +} diff --git a/PSGraph/Public/New-NodeAttributeSet.ps1 b/PSGraph/Public/New-NodeAttributeSet.ps1 index 61aae59..d6019ec 100644 --- a/PSGraph/Public/New-NodeAttributeSet.ps1 +++ b/PSGraph/Public/New-NodeAttributeSet.ps1 @@ -38,6 +38,12 @@ doesn't change any external/persistent state, so ShouldProcess doesn't apply." [string] $Color, + # Namespace GraphViz resolves Color/FillColor/FontColor small-integer values against, + # e.g. 'blues9' (a Brewer palette) or 'x11' (the default). No tab completion is provided - + # see https://graphviz.org/doc/info/colors.html#brewer for the full scheme list. + [string] + $ColorScheme, + # Distortion factor for shape=polygon. Positive values enlarge the top; negative the bottom. [double] $Distortion, @@ -63,6 +69,11 @@ doesn't change any external/persistent state, so ShouldProcess doesn't apply." [double] $FontSize, + # Angle, in degrees, controlling the direction of a gradient fill (used with a two-color + # FillColor and Style 'radial', or Style 'filled' for a linear gradient) + [double] + $GradientAngle, + # Height of node, in inches - the initial, minimum height [double] $Height, @@ -79,6 +90,10 @@ doesn't change any external/persistent state, so ShouldProcess doesn't apply." [double] $PenWidth, + # Number of node boundaries drawn, one inside the other + [int16] + $Peripheries, + # Forces a polygon shape to be regular (vertices lie on a circle centered on the node) [switch] $Regular, @@ -94,7 +109,7 @@ doesn't change any external/persistent state, so ShouldProcess doesn't apply." 'folder', 'box3d', 'component', 'promoter', 'cds', 'terminator', 'utr', 'primersite', 'restrictionsite', 'fivepoverhang', 'threepoverhang', 'noverhang', 'assembly', 'signature', 'insulator', 'ribosite', 'rnastab', 'proteasesite', - 'proteinstab', 'rpromoter', 'rarrow', 'larrow', 'lpromoter' + 'proteinstab', 'rpromoter', 'rarrow', 'larrow', 'lpromoter', 'record', 'Mrecord' )] [string] $Shape, @@ -107,22 +122,36 @@ doesn't change any external/persistent state, so ShouldProcess doesn't apply." [double] $Skew, - # Style for the node, e.g. filled, dashed, rounded + # Style for the node, e.g. filled, dashed, rounded, radial (gradient fill) [ValidateSet( - 'dashed', 'dotted', 'solid', 'invis', 'bold', 'filled', 'striped', 'wedged', 'diagonals', 'rounded' + 'dashed', 'dotted', 'solid', 'invis', 'bold', 'filled', 'striped', 'wedged', 'diagonals', + 'rounded', 'radial' )] [string] $Style, + # Tooltip text shown on hover in SVG/interactive output formats + [string] + $Tooltip, + + # Hyperlink attached to the node in SVG/PostScript/map output formats + [Alias('Href')] + [string] + $URL, + # Width of node, in inches - the initial, minimum width [double] - $Width + $Width, + + # External label placed near the node without affecting its size or layout + [string] + $XLabel ) $values = @{} # GraphViz requires these lowercase; user input may not be - $lowercaseParams = @('Color', 'FillColor', 'FixedSize', 'FontColor', 'Shape', 'Style') + $lowercaseParams = @('Color', 'ColorScheme', 'FillColor', 'FixedSize', 'FontColor', 'Shape', 'Style') foreach ($param in $lowercaseParams) { if ($PSBoundParameters.ContainsKey($param)) @@ -133,7 +162,8 @@ doesn't change any external/persistent state, so ShouldProcess doesn't apply." # Passed through unchanged - numeric, or free-form text where case is meaningful $passthroughParams = @( - 'Distortion', 'FontName', 'FontSize', 'Height', 'Image', 'Label', 'PenWidth', 'Sides', 'Skew', 'Width' + 'Distortion', 'FontName', 'FontSize', 'GradientAngle', 'Height', 'Image', + 'Label', 'PenWidth', 'Peripheries', 'Sides', 'Skew', 'Tooltip', 'URL', 'Width', 'XLabel' ) foreach ($param in $passthroughParams) { @@ -221,6 +251,8 @@ if (Get-Command -Name Register-ArgumentCompleter -ErrorAction SilentlyContinue) @{ CommandName = 'New-EdgeAttributeSet'; ParameterName = 'Color' } @{ CommandName = 'New-EdgeAttributeSet'; ParameterName = 'FontColor' } @{ CommandName = 'New-EdgeAttributeSet'; ParameterName = 'LabelFontColor' } + @{ CommandName = 'New-GraphAttributeSet'; ParameterName = 'BgColor' } + @{ CommandName = 'New-GraphAttributeSet'; ParameterName = 'FontColor' } ) foreach ($target in $colorCompletionTargets) { @@ -231,6 +263,7 @@ if (Get-Command -Name Register-ArgumentCompleter -ErrorAction SilentlyContinue) @{ CommandName = 'New-NodeAttributeSet'; ParameterName = 'FontName' } @{ CommandName = 'New-EdgeAttributeSet'; ParameterName = 'FontName' } @{ CommandName = 'New-EdgeAttributeSet'; ParameterName = 'LabelFontName' } + @{ CommandName = 'New-GraphAttributeSet'; ParameterName = 'FontName' } ) foreach ($target in $fontCompletionTargets) { diff --git a/PSGraph/en-US/about_PSGraph.help.txt b/PSGraph/en-US/about_PSGraph.help.txt index 4667ed7..36606ad 100644 --- a/PSGraph/en-US/about_PSGraph.help.txt +++ b/PSGraph/en-US/about_PSGraph.help.txt @@ -59,6 +59,7 @@ LONG DESCRIPTION New-NodeAttributeSet (alias NodeAttributes) New-EdgeAttributeSet (alias EdgeAttributes) + New-GraphAttributeSet (alias GraphAttributes) Build a case-correct GraphViz attribute hashtable from parameters, with tab completion for shape, color, and font values - GraphViz attribute names and values are case-sensitive, diff --git a/Tests/Graph.Tests.ps1 b/Tests/Graph.Tests.ps1 index a0436bb..74eb13a 100644 --- a/Tests/Graph.Tests.ps1 +++ b/Tests/Graph.Tests.ps1 @@ -66,6 +66,20 @@ Describe 'Function Graph' -Tag Build { } } + Context "-Strict switch" { + + It "Emits 'strict digraph' when specified" { + $result = (graph g -Strict {}) -join '' + $result | Should -Match '^strict digraph' + } + + It "Emits plain 'digraph' when not specified" { + $result = (graph g {}) -join '' + $result | Should -Match '^digraph' + $result | Should -Not -Match 'strict' + } + } + Context "Indentation" { It "Has no indention for first graph element" { diff --git a/Tests/New-EdgeAttributeSet.Tests.ps1 b/Tests/New-EdgeAttributeSet.Tests.ps1 index 6f72c61..39aef33 100644 --- a/Tests/New-EdgeAttributeSet.Tests.ps1 +++ b/Tests/New-EdgeAttributeSet.Tests.ps1 @@ -65,6 +65,32 @@ Describe 'Function New-EdgeAttributeSet' -Tag Build { } } + Context "Phase 8 additions" { + + It "Accepts -Weight and -MinLen without throwing (numeric)" { + { New-EdgeAttributeSet -Weight 2.5 -MinLen 2 } | Should -Not -Throw + (New-EdgeAttributeSet -Weight 2.5).weight | Should -Be 2.5 + (New-EdgeAttributeSet -MinLen 2).minlen | Should -Be 2 + } + + It "Preserves case for -Tooltip and -XLabel" { + $result = New-EdgeAttributeSet -Tooltip 'MyTip' -XLabel 'MyXLabel' + $result.tooltip | Should -Be 'MyTip' + $result.xlabel | Should -Be 'MyXLabel' + } + + It "-URL and its -Href alias both bind to the same 'url' key" { + (New-EdgeAttributeSet -URL 'https://example.com').url | Should -Be 'https://example.com' + (New-EdgeAttributeSet -Href 'https://example.com').url | Should -Be 'https://example.com' + } + + It "Renders -Weight and -URL in DOT output via Edge" { + $dot = (graph g { edge one two (New-EdgeAttributeSet -Weight 3 -URL 'https://example.com') }) -join "`n" + $dot | Should -Match 'weight="3"' + $dot | Should -Match 'URL="https://example.com"' + } + } + Context "Integration with Edge" { It "Merges cleanly into Edge's -Attributes and renders in the DOT output" { diff --git a/Tests/New-GraphAttributeSet.Tests.ps1 b/Tests/New-GraphAttributeSet.Tests.ps1 new file mode 100644 index 0000000..4d1fbdf --- /dev/null +++ b/Tests/New-GraphAttributeSet.Tests.ps1 @@ -0,0 +1,95 @@ +Describe 'Function New-GraphAttributeSet' -Tag Build { + + Context "Unit Tests" { + + It "Does not throw an error" { + { New-GraphAttributeSet } | Should -Not -Throw + } + + It "Returns an empty hashtable when nothing is specified" { + (New-GraphAttributeSet).Count | Should -Be 0 + } + + It "GraphAttributes alias resolves to the same command" { + (Get-Alias GraphAttributes).ResolvedCommand.Name | Should -Be 'New-GraphAttributeSet' + } + } + + Context "-RankDir keeps its required uppercase form" { + + It "Passes 'LR' through unchanged (rankdir is not a lowercase attribute)" { + (New-GraphAttributeSet -RankDir LR).rankdir | Should -Be 'LR' + } + + It "Accepts TB, LR, BT and RL" { + foreach ($direction in 'TB', 'LR', 'BT', 'RL') + { + { New-GraphAttributeSet -RankDir $direction } | Should -Not -Throw + } + } + } + + Context "Lowercased string attributes" { + + It "Lowercases -BgColor, -FontColor, -Splines and -Style" { + $result = New-GraphAttributeSet -BgColor LightYellow -FontColor Green -Splines Curved -Style Radial + + $result.bgcolor | Should -Be 'lightyellow' + $result.fontcolor | Should -Be 'green' + $result.splines | Should -Be 'curved' + $result.style | Should -Be 'radial' + } + + It "Lowercases -ColorScheme" { + (New-GraphAttributeSet -ColorScheme 'Blues9').colorscheme | Should -Be 'blues9' + } + } + + Context "Passed-through numeric/free-form attributes" { + + It "Accepts -NodeSep, -RankSep, -FontSize and -GradientAngle without throwing (double)" { + { New-GraphAttributeSet -NodeSep 0.5 -RankSep 1 -FontSize 12 -GradientAngle 45 } | Should -Not -Throw + } + + It "Preserves case for -Label, -Ratio and -Size" { + $result = New-GraphAttributeSet -Label 'MyGraph' -Ratio 'fill' -Size '8,8!' + $result.label | Should -Be 'MyGraph' + $result.ratio | Should -Be 'fill' + $result.size | Should -Be '8,8!' + } + } + + Context "-Concentrate switch and -Compound bool" { + + It "Sets concentrate = true when specified" { + (New-GraphAttributeSet -Concentrate).concentrate | Should -Be $true + } + + It "Omits 'concentrate' when not specified" { + (New-GraphAttributeSet).ContainsKey('concentrate') | Should -Be $false + } + + It "Sets compound explicitly, including an explicit false" { + (New-GraphAttributeSet -Compound $true).compound | Should -Be $true + (New-GraphAttributeSet -Compound $false).compound | Should -Be $false + } + } + + Context "Integration with Graph" { + + It "Merges cleanly into Graph's -Attributes and renders in the DOT output" { + $attrs = New-GraphAttributeSet -RankDir LR -BgColor lightyellow + $dot = (graph g -Attributes $attrs {}) -join "`n" + + $dot | Should -Match 'rankdir="LR"' + $dot | Should -Match 'bgcolor="lightyellow"' + } + + It "An explicit -Compound `$false is respected, not silently overridden back to true (#98)" { + $attrs = New-GraphAttributeSet -Compound $false + $dot = (graph g -Attributes $attrs {}) -join "`n" + + $dot | Should -Match 'compound="False"' + } + } +} diff --git a/Tests/New-NodeAttributeSet.Tests.ps1 b/Tests/New-NodeAttributeSet.Tests.ps1 index b6407d8..5fa87d3 100644 --- a/Tests/New-NodeAttributeSet.Tests.ps1 +++ b/Tests/New-NodeAttributeSet.Tests.ps1 @@ -61,6 +61,45 @@ Describe 'Function New-NodeAttributeSet' -Tag Build { } } + Context "Phase 8 additions" { + + It "Accepts -Peripheries and -GradientAngle without throwing" { + { New-NodeAttributeSet -Peripheries 2 -GradientAngle 45 } | Should -Not -Throw + (New-NodeAttributeSet -Peripheries 2).peripheries | Should -Be 2 + (New-NodeAttributeSet -GradientAngle 45).gradientangle | Should -Be 45 + } + + It "Preserves case for -Tooltip and -XLabel" { + $result = New-NodeAttributeSet -Tooltip 'MyTip' -XLabel 'MyXLabel' + $result.tooltip | Should -Be 'MyTip' + $result.xlabel | Should -Be 'MyXLabel' + } + + It "-URL and its -Href alias both bind to the same 'url' key" { + (New-NodeAttributeSet -URL 'https://example.com').url | Should -Be 'https://example.com' + (New-NodeAttributeSet -Href 'https://example.com').url | Should -Be 'https://example.com' + } + + It "Lowercases -ColorScheme" { + (New-NodeAttributeSet -ColorScheme 'Blues9').colorscheme | Should -Be 'blues9' + } + + It "Accepts 'radial' for -Style" { + (New-NodeAttributeSet -Style radial).style | Should -Be 'radial' + } + + It "Accepts 'record' and 'Mrecord' for -Shape" { + (New-NodeAttributeSet -Shape record).shape | Should -Be 'record' + (New-NodeAttributeSet -Shape Mrecord).shape | Should -Be 'mrecord' + } + + It "Renders -URL and -Tooltip in DOT output via Node" { + $dot = (graph g { node MyNode (New-NodeAttributeSet -URL 'https://example.com' -Tooltip 'hi') }) -join "`n" + $dot | Should -Match 'URL="https://example.com"' + $dot | Should -Match 'tooltip="hi"' + } + } + Context "-Regular switch" { It "Sets regular = true when specified" { diff --git a/Tests/PrivateFunctions.Tests.ps1 b/Tests/PrivateFunctions.Tests.ps1 index e9a492e..da0cd14 100644 --- a/Tests/PrivateFunctions.Tests.ps1 +++ b/Tests/PrivateFunctions.Tests.ps1 @@ -24,6 +24,11 @@ InModuleScope -ModuleName PSGraph { Get-LayoutEngine -Name $layout.name | Should -be $layout.value } } + + it "resolves osage and patchwork (previously unreachable)" { + Get-LayoutEngine -Name 'osage' | Should -Be 'osage' + Get-LayoutEngine -Name 'patchwork' | Should -Be 'patchwork' + } } Context "Get-ArgumentLookUpTable" { diff --git a/docs/Command-Export-PSGraph.md b/docs/Command-Export-PSGraph.md index 4255d7e..0785295 100644 --- a/docs/Command-Export-PSGraph.md +++ b/docs/Command-Export-PSGraph.md @@ -46,15 +46,37 @@ These are the valid options for output formats: * pdf * plain * dot +* svg +* svgz +* svg_inline +* eps +* ps +* ps2 +* xdot +* dot_json +* xdot_json +* json0 +* canon +* gv +* fig +* bmp +* tif / tiff +* wbmp +* pic +* plain-ext + +Whether a given format actually renders depends on how your local GraphViz build was compiled — run `dot -T?` to see what your install supports. Formats needing extra native libraries not universally present across platforms (e.g. `webp`, `gd`, `gd2`, `pov`) aren't in this list; use `-GraphVizPath` to point at a build that has them and pass the format value directly to `dot` yourself if you need one. ## -LayoutEngine [enum] GraphViz supports multiple layout engines. Each work better on different types of datasets. These are the available engines: -* Hierarchical (Default) -* Radial -* Circular -* SpringModelSmall -* SpringModelMedium -* SpringModelLarge +* Hierarchical (Default) / dot +* Radial / twopi +* Circular / circo +* SpringModelSmall / neato +* SpringModelMedium / fdp +* SpringModelLarge / sfdp +* osage +* patchwork diff --git a/docs/Command-Graph.md b/docs/Command-Graph.md index de13903..309b07e 100644 --- a/docs/Command-Graph.md +++ b/docs/Command-Graph.md @@ -64,3 +64,12 @@ Positional attributes also work. graph g @{label='my graph'} { edge a,b,c,d,aS } + +## Graph -Strict + +`-Strict` emits `strict digraph`/`strict graph`, which tells GraphViz to merge duplicate edges into one instead of drawing them separately. + + graph g -Strict { + edge a b + edge a b # collapsed into the same edge as above + } diff --git a/docs/Command-New-EdgeAttributeSet.md b/docs/Command-New-EdgeAttributeSet.md index 3f3fba8..1d089de 100644 --- a/docs/Command-New-EdgeAttributeSet.md +++ b/docs/Command-New-EdgeAttributeSet.md @@ -13,7 +13,7 @@ It has an alias, `EdgeAttributes`, for shorter call sites. ## Supported attributes -`-ArrowHead`, `-ArrowTail`, `-ArrowSize`, `-Color`, `-Constraint`, `-Direction`, `-FontColor`, `-FontName`, `-FontSize`, `-HeadLabel`, `-Label`, `-LabelFontColor`, `-LabelFontName`, `-LabelFontSize`, `-Length`, `-PenWidth`, `-Style`, `-TailLabel`. +`-ArrowHead`, `-ArrowTail`, `-ArrowSize`, `-Color`, `-Constraint`, `-Direction`, `-FontColor`, `-FontName`, `-FontSize`, `-HeadLabel`, `-Label`, `-LabelFontColor`, `-LabelFontName`, `-LabelFontSize`, `-Length`, `-MinLen`, `-PenWidth`, `-Style`, `-TailLabel`, `-Tooltip`, `-URL` (alias `-Href`), `-Weight`, `-XLabel`. A couple of these map to shortened GraphViz keys: `-Direction` becomes `dir`, `-Length` becomes `len`. `-Direction` and `-Style` have `[ValidateSet(...)]` on them for tab completion and up-front validation. diff --git a/docs/Command-New-GraphAttributeSet.md b/docs/Command-New-GraphAttributeSet.md new file mode 100644 index 0000000..44ba878 --- /dev/null +++ b/docs/Command-New-GraphAttributeSet.md @@ -0,0 +1,38 @@ +# New-GraphAttributeSet + +The graph-level counterpart to [`New-NodeAttributeSet`](Command-New-NodeAttributeSet.md) and [`New-EdgeAttributeSet`](Command-New-EdgeAttributeSet.md). `Graph`/`SubGraph` take a plain hashtable of GraphViz attributes, but GraphViz attribute names and values are case-sensitive — `New-GraphAttributeSet` builds that hashtable from PowerShell parameters instead, with tab completion for color and font values, and normalizes casing where GraphViz requires it. + + $attrs = New-GraphAttributeSet -RankDir LR -BgColor lightyellow -FontName 'Calibri' + graph g -Attributes $attrs { + edge left right + } + +It has an alias, `GraphAttributes`, for shorter call sites. + + graph g -Attributes (GraphAttributes -RankDir LR) { edge a b } + +## Supported attributes + +`-BgColor`, `-ColorScheme`, `-Compound`, `-Concentrate`, `-FontColor`, `-FontName`, `-FontSize`, `-GradientAngle`, `-Label`, `-LabelLoc`, `-NodeSep`, `-RankDir`, `-RankSep`, `-Ratio`, `-Size`, `-Splines`, `-Style`. + +`-RankDir`, `-LabelLoc`, `-Splines`, and `-Style` have `[ValidateSet(...)]` on them for tab completion and up-front validation. Unlike most GraphViz enum values, `rankdir`'s `TB`/`LR`/`BT`/`RL` values are required in uppercase — `-RankDir` is passed through as typed, not lowercased. + +## Gradients and colorschemes + +`-Style radial` (or `filled`) plus a two-color `-BgColor` (e.g. `'yellow:red'`) and `-GradientAngle` produce a gradient background on the graph or cluster. `-ColorScheme` names a palette (most commonly one of the [Brewer color schemes](https://graphviz.org/doc/info/colors.html#brewer)) that small-integer color values resolve against; there's no tab completion for scheme names since GraphViz ships dozens of them with varying color counts. + +## Tab completion + +`-BgColor` and `-FontColor` tab-complete against the system's known colors; `-FontName` tab-completes against installed fonts, the same completers `New-NodeAttributeSet`/`New-EdgeAttributeSet` use. Completion silently produces no suggestions on platforms where `System.Drawing` isn't available — it doesn't block you from typing a value by hand. + +## An explicit -Compound is respected + +`Graph` defaults `compound` to `true` unless the caller's attribute hashtable already contains a `compound` key — so `-Compound $false` here is not silently overridden back to `true` (see the [`compound=$false` fix](Command-Graph.md) for background). + + $attrs = New-GraphAttributeSet -Compound $false + graph g -Attributes $attrs {} + +## Only the attributes you set are included + + $attrs = New-GraphAttributeSet -RankDir LR + # $attrs is @{ rankdir = 'LR' } - nothing else diff --git a/docs/Command-New-NodeAttributeSet.md b/docs/Command-New-NodeAttributeSet.md index 1c018b3..63d302a 100644 --- a/docs/Command-New-NodeAttributeSet.md +++ b/docs/Command-New-NodeAttributeSet.md @@ -11,9 +11,9 @@ It has an alias, `NodeAttributes`, if you want something shorter at the call sit ## Supported attributes -`-Color`, `-FillColor`, `-FixedSize`, `-FontColor`, `-FontName`, `-FontSize`, `-Height`, `-Image`, `-Label`, `-PenWidth`, `-Regular`, `-Shape`, `-Sides`, `-Skew`, `-Style`, `-Width`, `-Distortion`. Each maps to the matching GraphViz node attribute. +`-Color`, `-ColorScheme`, `-Distortion`, `-FillColor`, `-FixedSize`, `-FontColor`, `-FontName`, `-FontSize`, `-GradientAngle`, `-Height`, `-Image`, `-Label`, `-PenWidth`, `-Peripheries`, `-Regular`, `-Shape`, `-Sides`, `-Skew`, `-Style`, `-Tooltip`, `-URL` (alias `-Href`), `-Width`, `-XLabel`. Each maps to the matching GraphViz node attribute. -`-Shape` and `-Style` have `[ValidateSet(...)]` on them, so tab completion and parameter validation catch typos before you ever hand the graph to GraphViz. +`-Shape` and `-Style` have `[ValidateSet(...)]` on them, so tab completion and parameter validation catch typos before you ever hand the graph to GraphViz. `-Style` includes `radial`, for a gradient fill (pair it with a two-color `-FillColor` and `-GradientAngle`); `-Shape` includes `record`/`Mrecord` for callers who want to hand-build a record label rather than use `Record`. ## Tab completion for color and font diff --git a/docs/graphviz.md b/docs/graphviz.md index f3fd1cd..7bcaf91 100644 --- a/docs/graphviz.md +++ b/docs/graphviz.md @@ -8,7 +8,8 @@ PSGraph is a thin PowerShell layer over [GraphViz](http://graphviz.org/) — eve * [DOT language reference](https://graphviz.org/doc/info/lang.html) — the text format PSGraph generates. * [Node, Edge, and Graph attributes](https://graphviz.org/doc/info/attrs.html) — every attribute name/value `Node`, `Edge`, `Graph`, and `SubGraph` accept in their `-Attributes` hashtables. * [Node shapes](https://graphviz.org/doc/info/shapes.html) — including the HTML-like table labels `Record`/`Row`/`Cells`/`Entity` build for you. -* [Layout engines](https://graphviz.org/docs/layouts/) — `Export-PSGraph -LayoutEngine` accepts `dot`, `neato`, `circo`, `fdp`, `sfdp`, `twopi`, and a few legacy aliases (`SpringModelSmall`, etc.) kept for backward compatibility. +* [Layout engines](https://graphviz.org/docs/layouts/) — `Export-PSGraph -LayoutEngine` accepts `dot`, `neato`, `circo`, `fdp`, `sfdp`, `twopi`, `osage`, `patchwork`, and a few legacy aliases (`SpringModelSmall`, etc.) kept for backward compatibility. +* [Output formats](https://graphviz.org/docs/outputs/) — `Export-PSGraph -OutputFormat` accepts a broad set of GraphViz's `dot -T` values (`png`, `svg`, `pdf`, `eps`, `xdot`, `dot_json`, ...). Which of these actually work depends on how your local GraphViz build was compiled — run `dot -T?` to see what your install supports. * [Command line / graph gallery](https://graphviz.org/gallery/) — the source for the recreated examples in `Example-Gallery.md`. ## Falling back to raw DOT @@ -25,6 +26,40 @@ For syntax PSGraph's DSL doesn't model at all (a construct with no hashtable-att inline 'rankdir=LR' } +## Graph-level attributes + +`New-GraphAttributeSet` (alias `GraphAttributes`) builds a case-correct attribute hashtable for `Graph`/`SubGraph`, the same way `New-NodeAttributeSet`/`New-EdgeAttributeSet` do for `Node`/`Edge`. It covers the graph attributes callers reach for most often — `rankdir`, `splines`, background/gradient fills, and rank/node spacing: + + $attrs = New-GraphAttributeSet -RankDir LR -BgColor lightyellow + graph g -Attributes $attrs { + edge left right + } + +## Strict graphs + +`Graph -Strict` emits `strict digraph`/`strict graph`, which tells GraphViz to merge duplicate edges into one instead of drawing them separately: + + graph g -Strict { + edge a b + edge a b # collapsed into the same edge as above + } + +## Gradients + +Any color attribute accepts a two-color `"c1:c2"` value plus `gradientangle`, and `style` needs `filled` or `radial`: + + graph g { + node A (New-NodeAttributeSet -FillColor 'yellow:red' -GradientAngle 45 -Style radial) + } + +## Colorschemes + +GraphViz can resolve small-integer color values against a named scheme instead of X11 color names — most commonly one of the [Brewer color schemes](https://graphviz.org/doc/info/colors.html#brewer). `New-NodeAttributeSet -ColorScheme`/`New-EdgeAttributeSet`/`New-GraphAttributeSet` all expose the attribute as a plain string (no tab completion, since Graphviz ships dozens of Brewer palettes with varying color counts): + + graph g { + node A @{ colorscheme = 'blues9'; fillcolor = '7'; style = 'filled' } + } + ## Installing the GraphViz binaries PSGraph's `Install-GraphViz` command installs the native `dot` binary GraphViz itself ships (Chocolatey on Windows with a nuget.org fallback for non-admin installs via `-Scope CurrentUser`, Homebrew on macOS). See [Command-Install-GraphViz.md](Command-Install-GraphViz.md) for details, or install GraphViz yourself through your platform's package manager (e.g. `apt-get install graphviz` on Debian/Ubuntu) if you'd rather not use PSGraph's installer. diff --git a/mkdocs.yml b/mkdocs.yml index cb0cbcc..9d97a4c 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -19,6 +19,7 @@ pages: - Entity: Command-Entity.md - New-NodeAttributeSet: Command-New-NodeAttributeSet.md - New-EdgeAttributeSet: Command-New-EdgeAttributeSet.md + - New-GraphAttributeSet: Command-New-GraphAttributeSet.md - Export-PSGraph: Command-Export-PSGraph.md - Show-PSGraph: Command-Show-PSGraph.md - Set-NodeFormatScript: Command-Set-NodeFormatScript.md diff --git a/readme.md b/readme.md index 78536a4..20edf5d 100644 --- a/readme.md +++ b/readme.md @@ -25,6 +25,7 @@ PSGraph is a helper module implemented as a DSL (Domain Specific Language) for g * [New-NodeAttributeSet](http://psgraph.readthedocs.io/en/latest/Command-New-NodeAttributeSet/) (alias `NodeAttributes`) * [New-EdgeAttributeSet](http://psgraph.readthedocs.io/en/latest/Command-New-EdgeAttributeSet/) (alias `EdgeAttributes`) +* [New-GraphAttributeSet](http://psgraph.readthedocs.io/en/latest/Command-New-GraphAttributeSet/) (alias `GraphAttributes`) * [Set-NodeFormatScript](http://psgraph.readthedocs.io/en/latest/Command-Set-NodeFormatScript/) **Rendering**