PSparklines.psm1
using namespace System.Collections; using namespace System.Collections.Generic; <# ____ ____ _ _ _ | _ \/ ___| _ __ __ _ _ __| | _| (_)_ __ ___ ___ | |_) \___ \| '_ \ / _` | '__| |/ / | | '_ \ / _ \/ __| | __/ ___) | |_) | (_| | | | <| | | | | | __/\__ \ |_| |____/| .__/ \__,_|_| |_|\_\_|_|_| |_|\___||___/ |_| #> <# .Synopsis This module is a very simple way to show text sparklines in the console. .Description This module is a very simple way to show text sparklines in the console. It was ported to PowerShell from Python. The original package is sparklines.py. It is hosted on github.com at github.com/deeplook/sparklines. This module does implement emphasis in a manner similar to the original. However, instead of a simple string pattern, it uses Emphasis objects. Objects are added to a dictionary with functions that support auto-completion. This module does not implement the batching (array splitting) that sparklines used. Use the `Ansi` switch parameter with `Show-Sparkline` to take advantage of Ansi colors. This module also outputs Sparklines as objects and uses two different functions to write them. `Show-Sparkline` will write the sparkline to the host and STDINFO (6) and colorize based on an emphasis table. `Write-Sparkline` will write the sparkline to STDOUT (1) as a string for further parsing or use. Because `Get-Sparkline` will write objects, the user can write a custom function to write the sparkline how they need if the default functions are inadequate. Cmdlets/Functions for Sparklines: Get-Sparkline Write-Sparkline Show-Sparkline Cmdlets/Functions for Emphasis: New-Emphasis .Example PS> Get-Sparkline 25, 50, 75, 100, 25 -Emphasis @( New-Emphasis -Color 'Red' -Predicate { param($x) $x -gt 50 } ) | Show-Sparkline Display a sparkline in the host of line height 1 with every bar representing a number greater than 50 as ConsoleColor.Red #> param() # Idea: PowerShell 7 is so much easier to work with more 'programmatic' PS -- consider requiring it <# ___ _ / __| ___| |_ _ _ _ __ \__ \/ -_) _| || | '_ \ |___/\___|\__|\_,_| .__/ |_| #> #region Setup ------------------------------------------------------------------ # Module variables go here Set-Variable PSparklines -Option ReadOnly -Value @{ DefaultForegroundColor = [Console]::ForegroundColor ModuleName = 'PSparklines' Esc = [char] 0x1b } $ErrorActionPreference = 'Stop' $ResourceFile = @{ BindingVariable = 'Resources' BaseDirectory = $PSScriptRoot FileName = $PSparklines.ModuleName + '.Resources.psd1' } $ConfigFile = @{ BindingVariable = 'Config' BaseDirectory = $PSScriptRoot FileName = $PSparklines.ModuleName + '.Config.psd1' } # Try to import the resource file try { Import-LocalizedData @ResourceFile } catch { # Uh-oh. The module is likely broken if this file cannot be found. Import-LocalizedData @ResourceFile -UICulture en-US } # Try to import the config file. try { Import-LocalizedData @ConfigFile } catch { # The config file is missing. Not a big deal! Here's a default Config. $Config = @{ Blocks = @' ▁▂▃▄▅▆▇█ '@ } } #endregion <# ___ _ / __| |__ _ ______ ___ ___ | (__| / _` (_-<_-</ -_|_-< \___|_\__,_/__/__/\___/__/ #> #region Module Classes --------------------------------------------------------- class Color { [byte] $R [byte] $G [byte] $B [byte] $Value [ConsoleColor] $ConsoleColor Color($n) { $x = $n -as [int] if ($x -is [int]) { $this.Value = $n } else { $this.Value = switch ($n) { Black { 0 } DarkRed { 1 } DarkGreen { 2 } DarkYellow { 3 } DarkBlue { 4 } DarkMagenta { 5 } DarkCyan { 6 } Gray { 7 } DarkGray { 8 } Red { 9 } Green { 10 } Yellow { 11 } Blue { 12 } Magenta { 13 } Cyan { 14 } White { 15 } default { 0 } # Todo: Drawing.Color -> rgb -> ansi } } $this.R, $this.G, $this.B = [Color]::RgbFromAnsi256($this.Value) $this.ConsoleColor = [Color]::ConsoleColorFromAnsi($this.Value) } static [int] CubeValue($n) { return @( 0 95 135 175 215 255 )[$n] } static [int[]] RgbFromAnsi256($n) { $x = switch ($n) { { $n -lt 232 } { $idx = $n - 16 [Color]::CubeValue($idx / 36), [Color]::CubeValue($idx / 6 % 6), [Color]::CubeValue($idx % 6) } default { $gr = ($n - 232) * 10 + 8 $gr, $gr, $gr } } return $x } static [ConsoleColor] ClosestConsoleColorFromRgb($r, $g, $b) { $color = if ($r -eq $g -and $g -eq $b) { switch ($r) { { $r -gt 192 } { 0xf } # 0b1111 { $r -gt 128 } { 7 } # 0b0111 { $r -gt 64 } { 8 } # 0b1000 default { 0 } } } else { $br = if ($r -gt 128 -or $g -gt 128 -or $b -gt 128) { 7 } else { 0 } $rb = if ($r -gt 64) { 4 } else { 0 } # 0b0100 $gb = if ($g -gt 64) { 2 } else { 0 } # 0b0010 $bb = if ($b -gt 64) { 1 } else { 0 } # 0b0001 $br -bor $rb -bor $gb -bor $bb } return $color } static [ConsoleColor] ConsoleColorFromAnsi($n) { $colorMap = @( [ConsoleColor]::Black [ConsoleColor]::DarkRed [ConsoleColor]::DarkGreen [ConsoleColor]::DarkYellow [ConsoleColor]::DarkBlue [ConsoleColor]::DarkMagenta [ConsoleColor]::DarkCyan [ConsoleColor]::Gray [ConsoleColor]::DarkGray [ConsoleColor]::Red [ConsoleColor]::Green [ConsoleColor]::Yellow [ConsoleColor]::Blue [ConsoleColor]::Magenta [ConsoleColor]::Cyan [ConsoleColor]::White ) $color = switch ($n) { { $n -lt 16 } { $colorMap[$n] } default { $x, $y, $z = [Color]::RgbFromAnsi256($n) [Color]::ClosestConsoleColorFromRgb($x, $y, $z) } } return $color } [string] ToString() { return $this.ConsoleColor.ToString() } } class Emphasis { [Color] $Color [scriptblock] $Predicate } class Spark { [int] $Row [int] $Col [int] $Val [string] $Block [AllowNull()] [Color] $Color } #endregion <# _ _ _ | || |___| |_ __ ___ _ _ ___ | __ / -_) | '_ \/ -_) '_(_-< |_||_\___|_| .__/\___|_| /__/ |_| #> #region Class Helpers ---------------------------------------------------------- function Get-Max ($a, $b) { [Math]::Max($a, $b) } function Get-Min ($a, $b) { [Math]::Min($a, $b) } function Get-RoundUp ($n) { [Math]::Round($n) } function Get-ScaledValues { # .Synopsis # Scale input numbers to appropriate range. # double[] -> int -> double? -> double? -> double[] # .Notes # Replaces scale_values(numbers, num_lines=1, minium=None, maximum=None) param( [double[]] $Numbers , [int] $NumLines = 1 , [double] $Minimum , [double] $Maximum ) $Numbers | Measure-Object -Minimum -Maximum | Set-Variable mo $min = ($Minimum, $mo.Minimum)[!$Minimum] $max = ($Maximum, $mo.Maximum)[!$Maximum] $dv = $max - $min $nums = $Numbers.ForEach{ Max (Min $_ $max) $min } $getValue = { $maxIndex = $NumLines * ($Config.Blocks.Length - 1) (($maxIndex - 1) * ($_ - $min)) / $dv + 1 } $roundValue = { $v = RoundUp $_ (1, $v)[$v -gt 0] } switch ($dv) { { $dv -eq 0 } { $nums.ForEach{ 4 * $NumLines } } { $dv -gt 0 } { $nums.ForEach($getValue).ForEach($roundValue) } default { } } } function Get-ColorArray ($ns, $xs) { # .Synopsis # Get the colors mapped from a double array when passed through an array of predicates # double[] -> Emphasis[]? -> Color[] foreach ($n in $ns) { $color = [Console]::ForegroundColor -as [Color] foreach ($x in $xs) { if ($x.Predicate.Invoke($n)) { $color = $x.Color break; } } $color } } #endregion <# ___ _ _ _ | _ \_ _| |__| (_)__ | _/ || | '_ \ | / _| |_| \_,_|_.__/_|_\__| #> #region Public Commands -------------------------------------------------------- function New-Emphasis ($Color, $Predicate) { <# .Synopsis Creates a new Emphasis object. .Description A public helper function that creates a new Emphasis object. Emphasis objects allow for the colorization of sparks in a sparkline. The predicate parameter .Parameter Color The color parameter capitalizes on PowerShell's powerful casting. Pass it either a ConsoleColor name, e.g. 'Red' or an Ansi 256 color. If an Ansi 256 color is passed above 16, the Color class will smartly select the closest ConsoleColor. .Parameter Predicate The predicate parameter must be a scriptblock. The scriptblock should accept 1 parameter and evaluate to a boolean. .Example New-Emphasis -Color 'Red' -Predicate { param($x) $x -gt 50 } .Example New-Emphasis -Color 55 -Predicate { $x, $rest = $args; $x -in (6..13) } .Example New-Emphasis -Color 231 -Predicate { $args[0] -like '6*' } .Link Get-Sparkline .Notes Replaces the emph pattern used in sparklines.py #> [Emphasis] @{ Color = $Color Predicate = $Predicate } } function Get-Sparkline { <# .Synopsis Return an array of sparkline objects for a given list of input numbers. .Description Return an array of sparkline objects for a given list of input numbers. .Example PS> Get-Sparkline -Numbers 20, 80, 60, 100 Returns sparkline objects representing the numbers 20, 80, 60, 100. .Example PS> Get-Sparkline -Numbers 20, 80, 60, 100 | Write-Sparkline ▁▆▄█ .Example PS> Get-Sparkline -Numbers 20, 80, 60, 100 -NumLines 3 | Write-Sparkline ▂ █ █▄█ ▁███ .Example PS> Get-Sparkline -Numbers 20, 80, 60, 100 -Emphasis (New-Emphasis -Color Red -Predicate { param($x) $x -gt 70 } | Show-Sparkline This will display a sparkline in the host with the second and fourth bar colored red, if the host is capable. .Example PS> -join (Get-Sparkline 1,2,3,4 | Show-Sparkline 6>&1) One possible way to capture the output of `Show-Sparkline`. .Link New-Emphasis .Link Write-Sparkline .Link Show-Sparkline .Inputs double[] .Outputs Sparks[] .Notes Replaces sparklines(numbers=[], num_lines=1, emph=None, verboe=False, minimum=None, maximum=None, wrap=None). Wrap is not #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '')] param( # An array of numbers to turn into a sparkline. [Parameter(ValueFromPipeline)] [double[]] $Numbers , # The number of lines to write or show the sparkline on. Must be positive. [ValidateScript({ Assert-Positive $_ })] [int] $NumLines = 1 , # An array of Emphasis objects that will color certain sparks based on simple logical tests. $Emphasis # For some reason cannot be [Emphasis[]]? , # The lowest number to display on the sparkline--a high-pass filter. [double] $Minimum , # The highest number to display on the sparkline--a low-pass filter. [double] $Maximum ) begin { $ls = [System.Collections.ArrayList] @() } process { [void] $Numbers.ForEach{ $ls.Add($_) } } end { $PSBoundParameters.Numbers = $ls.ToArray() $xs = $Emphasis # Remove params from the hashtable to allow for easy-reuse [void] $PSBoundParameters.Remove('Emphasis') Test-NegativeNumber $ls.ToArray() $x = Get-ColorArray $ls.ToArray() $xs Get-ScaledValues @PSBoundParameters | ForEach-Object { $c = 0 } { $v = $_ 1..$NumLines | ForEach-Object { $r = 0 } { $vs = Min $v 8 $v = Max 0 ($v - 8) [Spark] @{ Row = $r Col = $c Val = $vs Block = $Config.Blocks[$vs] Color = $x[$c] } $r++ } $c++ } } } function Show-Sparkline { <# .Synopsis Format the pipelined Sparkline and send it to the information stream and write it to the host. .Description Format the pipelined Sparkline and send it to the information stream and write it to the host. Allows for in host formatting and colorization if the Sparkline array was defined with an Emphasis. If the console host support virtual terminal codes and 24 bit color, use the ansi switch to get enhanced colors defined by Emphasis objects. .Example PS> Get-Sparkline 1,2,3,4 | Show-Sparkline .Example PS> Get-Sparkline 1,2,3,4 -Emphasis (New-Emphasis -Color 55 -Predicate { param($x) $x -eq 2 }) | Show-Sparkline -Ansi #> param( # Do not terminate the sparkline with a newline. [switch] $NoNewline , # Use the 256 Ansi Colors [switch] $Ansi ) $input | Sort-Object @{ Expression = 'Row'; Descending = $true }, Col -OutVariable sparks | Measure-Object -Property Row -Maximum | Set-Variable mo $r = $mo.Maximum foreach ($x in $sparks) { if ($x.Row -ne $r) { Write-Host $r-- } if ($Ansi.IsPresent) { Write-Host ('{2}[38;5;{0}m{1}{2}[0m' -f $x.Color.Value, $x.Block, $PSparklines.Esc) -NoNewline } else { Write-Host $x.Block -ForegroundColor $x.Color.ConsoleColor -NoNewline } } Write-Host -NoNewline:$NoNewline.IsPresent } function Write-Sparkline { <# .Synopsis Format the pipelines Sparkline and send it the standard output stream and write it as a string. .Example PS> Get-Sparkline 1,2,3,4 | Write-Sparkline #> $input | Group-Object Row | Sort-Object Name -Descending | ForEach-Object { -join $_.Group.Block } } #endregion <# __ , ,-| ~ , ,, ('||/__, _ || || ' _ (( ||| | < \, =||= _-_ ||/\ _-_ _-_ -_-_ \\ \\/\\ / \\ (( |||==| /-|| || || \\ ||_< || \\ || \\ || \\ || || || || || ( / | , (( || || ||/ || | ||/ ||/ || || || || || || || -____/ \/\\ \\, \\,/ \\,\ \\,/ \\,/ ||-' \\ \\ \\ \\_-| |/ / \ ' '----` #> #region Gatekeeping ------------------------------------------------------------ function Assert-Positive ($n) { # .Synopsis # Returns true if the number is greater than zero. # Otherwise, throws an exception. # double -> bool $isNumeric = [double]::TryParse($n, [ref] $null) if (!$isNumeric) { throw $Resources.InvalidNumeric -f $n } if ($n -le 0) { throw $Resources.InvalidNegative -f $n } $true } filter Get-NegativeNumbers { # .Synopsis # Passes numbers less than zero. # double[] -> double if ($_ -lt 0) { $_ } } function Write-Scolding { # .Synopsis # Writes a warning for every negative number caught. # Writes a general warning if any negative number is caught. # double[] -> () process { Write-Warning ($Resources.FoundNegativeNum -f $_) $b = $null -ne $_ } end { if ($b) { Write-Warning $Resources.UnexpectedOutput } } } function Test-NegativeNumber ($a) { # .Synopsis # Raise warning for negative numbers. # double[] -> () $a | Get-NegativeNumbers | Write-Scolding } #endregion |