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**