Types/Turtle/PieGraph.ps1

<#
.SYNOPSIS
    Draws a pie graph using turtle graphics.
.DESCRIPTION
    This script uses turtle graphics to draw a pie graph based on the provided data.
.EXAMPLE
    turtle PieGraph 400 80 20 save ./80-20.svg
.EXAMPLE
    turtle PieGraph 400 5 10 15 20 15 10 5 | Save-Turtle ./PieGraph.svg
.EXAMPLE
    turtle PieGraph 400 @{value=20;fill='red'} @{value=40;fill='blue'} save ./PieGraphColor.svg
.EXAMPLE
    turtle PieGraph 400 @(
        5,10,15,20,15,10,5 | Sort-Object -Descending
    ) | Save-Turtle ./PieGraphDescending.svg
.EXAMPLE
    turtle rotate (Get-Random -Max 360) PieGraph 400 @(
        5,10,15,20,15,10,5 | Sort-Object -Descending
    ) | Save-Turtle ./PieGraphDescendingRotated.svg
.EXAMPLE
    turtle PieGraph 200 (
        @(1..50) |
            Get-Random -Count (Get-Random -Minimum 5 -Maximum 20)
    ) save ./RandomPieGraph.svg
.EXAMPLE
    turtle rotate -90 piegraph 100 @(
        $allTokens = Get-Module Turtle |
            Split-Path |
            Get-ChildItem -Filter *.ps1 |
            Foreach-Object {
                [Management.Automation.PSParser]::Tokenize(
                    (Get-Content -Path $_.FullName -Raw), [ref]$null
                )
            }
        $allTokens |
            Group-Object Type -NoElement |
            Sort-Object Count -Descending |
            Add-Member ScriptProperty Fill {
                "#{0:x6}" -f (Get-Random -Maximum 0xffffff)
            } -Force -PassThru |
             Add-Member ScriptProperty Link {
                "https://learn.microsoft.com/en-us/dotnet/api/system.management.automation.pstokentype?view=powershellsdk-7.4.0#system-management-automation-pstokentype-$($this.Name.ToLower())"
            } -Force -PassThru
        
    ) save ./TurtlePSTokenCountPieGraph.svg
.EXAMPLE
    turtle rotate -90 piegraph 100 @(
        $allTokens = Get-Module Turtle |
            Split-Path |
            Get-ChildItem -Filter *.ps1 |
            Foreach-Object {
                [Management.Automation.PSParser]::Tokenize(
                    (Get-Content -Path $_.FullName -Raw), [ref]$null
                )
            }
        $allTokens |
            Group-Object Type |
            Select-Object Name, @{
                Name='TotalLength'
                Expression = {
                    $total = 0
                    foreach ($item in $_.Group) {
                        $total+=$item.Length
                    }
                    $total
                }
            } |
            Sort-Object TotalLength -Descending |
            Add-Member ScriptProperty Fill {
                "#{0:x6}" -f (Get-Random -Maximum 0xffffff)
            } -Force -PassThru |
             Add-Member ScriptProperty Link {
                "https://learn.microsoft.com/en-us/dotnet/api/system.management.automation.pstokentype?view=powershellsdk-7.4.0#system-management-automation-pstokentype-$($this.Name.ToLower())"
            } -Force -PassThru
        
    ) save ./TurtlePSTokenLengthPieGraph.svg
.EXAMPLE
    turtle rotate -90 piegraph 100 @(
        $allTypes = Get-Module Turtle |
            Split-Path |
            Get-ChildItem -Filter *.ps1 |
            Get-Command -Name { $_.FullName } |
            Foreach-Object {
                $_.ScriptBlock.Ast.FindAll({
                    param($ast)
                    $ast -is [Management.Automation.Language.TypeExpressionAst]
                }, $true)
            } |
            Foreach-Object {
                $_.Extent -replace '^\[' -replace '\]$' -as [type]
            }
        $allTypes |
            Group-Object FullName |
            Sort-Object Count -Descending |
            Add-Member ScriptProperty Fill {
                "#{0:x6}" -f (Get-Random -Maximum 0xffffff)
            } -Force -PassThru |
            Add-Member ScriptProperty Title {
                $this.Name
            } -Force -PassThru |
            Add-Member ScriptProperty Link {
                "https://learn.microsoft.com/en-us/dotnet/api/$($this.Name.ToLower())?wt.mc_id=MVP_321542"
            } -Force -PassThru
    ) save ./TurtleDotNetTypesPieGraph.svg
.EXAMPLE
    $n = Get-Random -Min 5 -Max 10
    turtle width 200 height 200 morph @(
        turtle PieGraph 200 200 @(1..50 | Get-Random -Count $n)
        turtle PieGraph 200 200 @(1..50 | Get-Random -Count $n)
        turtle PieGraph 200 200 @(1..50 | Get-Random -Count $n)
    ) save ./RandomPieGraphMorph.svg
.EXAMPLE
    turtle PieGraph 200 (
        @(1..50;-1..-50) |
            Get-Random -Count (Get-Random -Minimum 5 -Maximum 20)
    ) save ./RandomPieGraphWithNegative.svg
.EXAMPLE
    $randomNegativePie = turtle PieGraph 200 (
            @(1..50;-1..-50) |
                Get-Random -Count 10
        )
    turtle viewbox 200 morph @(
        $randomNegativePie
        turtle PieGraph 200 200 (
            @(1..50;-1..-50) |
                Get-Random -Count 10
        )
        $randomNegativePie
    ) save ./RandomPieGraphWithNegativeMorph.svg
.EXAMPLE
    # Multiple pie graphs
    turtle PieGraph 400 (1,2,4,8,4,2,1) jump 800 rotate 180 PieGraph 400 (1,2,4,8,4,2,1) save ./pg.svg
#>

param(
# The radius of the bar graph
[double]$Radius,

# The points in the bar graph.
# Each point will be turned into a relative number and turned into an equal-width bar.
[Parameter(ValueFromRemainingArguments)]
[PSObject[]]
$GraphData
)


# If there were no points, we are drawing nothing, so return ourself.
if (-not $GraphData) { return $this}

filter IsPrimitive {$_.GetType -and $_.GetType().IsPrimitive}

# To make a pie graph we need to know the total, and thus we need to make a couple of passes
[double]$Total = 0.0

$sliceObjects = [Ordered]@{}
$richSlices = $false
$Slices = @(
    $dataPointIndex = 0
    foreach ($dataPoint in $GraphData)
    {        
        # If the data point is a number (or other primitive data)
        if ($dataPoint | IsPrimitive)
        {
            $Total += $dataPoint # add it to the total
            $dataPoint -as [double] # and output it
            $sliceObjects["slice$($sliceObjects.Count)"] = $dataPoint
        }
        # Otherwise, if the data point has a value that is a number
        elseif ($dataPoint.value | IsPrimitive)
        {            
            $Total += $dataPoint.value # add it to the total
            $dataPoint.value -as [double] # and output that
            $richSlices = $true
            $sliceObjects["slice$($sliceObjects.Count)"] = $dataPoint
        }
        elseif ($dataPoint -is [Collections.IDictionary]) {
            foreach ($key in $dataPoint.Keys) {
                if ($dataPoint[$key] | IsPrimitive) {
                    $Total += $dataPoint[$key] # add it to the total
                    $dataPoint[$key] -as [double] # and output that
                    $sliceObjects["slice$($sliceObjects.Count)"] = $dataPoint[$key]
                }
            }
            
            $richSlices = $true
        }
        elseif ($DataPoint -is 'Microsoft.PowerShell.Commands.GroupInfo') {
            $total += $dataPoint.Count
            $dataPoint.Count
            $sliceObjects["slice$($sliceObjects.Count)"] = $dataPoint
            $richSlices = $true
        }
        elseif ($dataPoint -isnot [string]) {
            foreach ($prop in $dataPoint.psobject.properties) {
                if ($dataPoint.($prop.Name) | IsPrimitive) {
                    $Total += $dataPoint.($prop.Name) # add it to the total
                    $dataPoint.($prop.Name) -as [double] # and output that
                    $sliceObjects["slice$($sliceObjects.Count)"] = $dataPoint
                }
            }
            $richSlices = $true
        }
    }
)

if ($Slices.Length -eq 1 -and -not $richSlices) {

    # If we provide a single number, we will auto-slice the pie
    # If the number is between 0 and 1, we want to show a fraction
    if ($slices[0] -ge 0 -and $slices[0] -le 1) {        
        # Set the total to one
        $total = 1
        # and make two pie slices.
        $slices = $slices[0], (1- $slices[0])
    } else {
        # Otherwise, we want mostly equal pie slices
        # (mostly is in case of a decimal value)
        # Get the floor of our slice,
        $floor = [Math]::Floor($slices[0])
        # and determine the remainder.
        $remainder = $slices[0] - $floor
        # Then create N equal slices.
        $Slices = @(,1 * $floor)
        # If there was a remainder
        if ($remainder) {
            # create a small slice.
            $slices += $remainder
        }
        # Retotal our pie
        $total = 0.0
        foreach ($slice in $slices) {
            $total += $slice
        }        
    }    
}

# Turn each numeric slice into a ratio
$relativeSlices =
    foreach ($slice in $Slices) { $slice/ $total }

# If we have no ratios, we have nothing to graph, and we are done here.
if (-not $relativeSlices) { return $this }

# Next let's figure out the maximum delta x and delta y
$dx = $this.X + $Radius
$dy = $this.Y + $Radius
# and resize our viewbox with respect to our radius
$null = $this.ResizeViewBox($Radius)

# If we are not rendering "rich" slices, we can draw the arcs as one path
if (-not $richSlices) {
    # and we do not need to teleport
    for ($sliceNumber =0 ; $sliceNumber -lt $Slices.Length; $sliceNumber++) {
        # Turn each ratio into an angle
        $Angle = $relativeSlices[$sliceNumber] * 360        
        $this = $this. 
                # Draw an arc of that angle,
                CircleArc($Radius, $Angle).
                # then rotate by the angle.
                Rotate($angle)
    }
}
else {
    # Otherwise, we are making multiple turtles
    $nestedTurtles = [Ordered]@{}
    # The idea is the same, but the implementation is more complicated
    $heading = $this.Heading
    if (-not $heading) { $heading = 0.0 }    
    # Calulate the midpoint of the circle
    for ($sliceNumber =0 ; $sliceNumber -lt $Slices.Length; $sliceNumber++) {
        $Angle = $relativeSlices[$sliceNumber] * 360
        $sliceName = "slice$sliceNumber"
        # created a nested turtle at the midpoint
        $nestedTurtles["slice$sliceNumber"] = turtle start $dx $dy
        # with the current heading
        $nestedTurtles["slice$sliceNumber"].Heading = $this.Heading
        # and arc by the angle
        $null = $nestedTurtles["slice$sliceNumber"].CircleArc($Radius, $Angle)

        # If the slice was of a dictionary
        if ($sliceObjects[$sliceName] -is [Collections.IDictionary])
        {
            # set any settable properties on the turtle
            foreach ($key in $sliceObjects[$sliceName].Keys) {
                # that exist in both the turtle and the dictionary
                if ($nestedTurtles[$sliceName].psobject.properties[$key].SetterScript) {
                    $nestedTurtles[$sliceName].$key = $sliceObjects[$sliceName][$key]
                }
            }
        }
        # If the slice was not a string
        elseif ($sliceObjects[$sliceName] -isnot [string])
        {
            # Set any settable properties on the turlte
            foreach ($key in $sliceObjects[$sliceName].psobject.properties.Name) {
                # that exist in both the turtle and the slice object.
                if ($nestedTurtles[$sliceName].psobject.properties[$key].SetterScript) {
                    $nestedTurtles[$sliceName].$key = $sliceObjects[$sliceName].$key
                }
            }
        }
        
        # Now rotate our own heading, even though we are not drawing anything.
        $null = $this.Rotate($angle)
    }
    # and set our nested turtles.
    $this.Turtles = $nestedTurtles
    # $null = $this.ResizeViewBox($Radius)
}
 
return $this