Public/Add-DhHeatmap.ps1
|
function Add-DhHeatmap { <# .SYNOPSIS Add a heatmap block — 2D grid with a sequential or diverging colour scale. .DESCRIPTION Renders a rows × columns matrix where each cell is coloured by its numeric value via a continuous colour scale. This is the canonical "Heatmap" widget from the IT-infrastructure KPI dashboard specification (§2): time-of-day patterns, replication-lag matrices, latency by endpoint × hour, request volume by region × day. **Distinct from Add-DhStatusGrid**: StatusGrid is CATEGORICAL (cells carry a state like ok / warn / danger). Heatmap is CONTINUOUS (cells carry a numeric value mapped to a colour gradient). Two colour scales: 'sequential' (default) — single-hue gradient from light to dark. Use for "more is more": request counts, latency, error counts. 'diverging' — three-stop gradient red → neutral → green. Use for "deviation from a centre": actual-vs-target deltas, drift, A/B lift. Cells you don't declare render as a faint "no data" state. .PARAMETER Report Dashboard object from New-DhDashboard. .PARAMETER Id Unique identifier (alphanumeric, dash, underscore). .PARAMETER Title Block heading shown above the matrix. .PARAMETER Rows Array of row labels (Y-axis). .PARAMETER Columns Array of column labels (X-axis). .PARAMETER Cells Array of cell hashtables: @{ Row = 'DC01' # REQUIRED — must appear in -Rows Column = '14:00' # REQUIRED — must appear in -Columns Value = 42 # REQUIRED — numeric (the heat) Tooltip = 'avg over 5m' # optional — extra text in hover tooltip } .PARAMETER RowLabel Optional caption for the Y-axis. .PARAMETER ColumnLabel Optional caption for the X-axis. .PARAMETER ColorScale 'sequential' (default) | 'diverging'. .PARAMETER Min Lower bound of the colour scale. Auto-fits from data when omitted. .PARAMETER Max Upper bound of the colour scale. Auto-fits from data when omitted. .PARAMETER Unit Optional suffix appended to values in hover tooltips ('ms', '%', etc). .PARAMETER CellSize Cell side length preset: 'small' (24 px) | 'normal' (36 px, default) | 'large' (48 px). .PARAMETER ShowValues When set, the numeric value is printed inside each cell. Useful for small grids where the colour alone isn't precise enough. .PARAMETER MaxCells Hard ceiling on Rows × Columns. Default 5000 (heatmaps can be larger than StatusGrids since cells are smaller). Throws above this. .PARAMETER NavGroup Primary nav group label (enables two-tier nav). .PARAMETER NavSubGroup Optional second-level group under NavGroup. .EXAMPLE # Replication latency per DC pair × hour (24h) $hours = 0..23 | ForEach-Object { '{0:00}:00' -f $_ } Add-DhHeatmap -Report $report -Id 'repl-latency-heat' ` -Title 'Replication latency (ms) — DC pair × hour' ` -RowLabel 'DC pair' -ColumnLabel 'Hour' ` -Rows @('DC01→DC02','DC01→DC03','DC02→DC03') ` -Columns $hours ` -Cells $latencyCells ` -ColorScale 'sequential' -Unit ' ms' -Min 0 -Max 100 #> [CmdletBinding()] param( [Parameter(Mandatory)] [System.Collections.Specialized.OrderedDictionary] $Report, [Parameter(Mandatory)] [ValidatePattern('^[A-Za-z0-9_-]+$')] [string] $Id, [Parameter(Mandatory)] [string] $Title, [Parameter(Mandatory)] [object[]] $Rows, [Parameter(Mandatory)] [object[]] $Columns, [Parameter(Mandatory)] [object[]] $Cells, [string] $RowLabel = '', [string] $ColumnLabel = '', [ValidateSet('sequential','diverging')] [string] $ColorScale = 'sequential', [object] $Min = $null, [object] $Max = $null, [string] $Unit = '', [ValidateSet('small','normal','large')] [string] $CellSize = 'normal', [switch] $ShowValues, [ValidateRange(1, 200000)] [int] $MaxCells = 5000, [string] $NavGroup = '', [string] $NavSubGroup = '' ) if (-not $Report.Contains('Blocks')) { $Report['Blocks'] = [System.Collections.Generic.List[hashtable]]::new() } foreach ($existing in $Report.Blocks) { if ($existing.Id -eq $Id) { throw "Add-DhHeatmap: A block with Id '$Id' already exists in this report." } } if ($Rows.Count -lt 1) { throw "Add-DhHeatmap: -Rows must contain at least one label." } if ($Columns.Count -lt 1) { throw "Add-DhHeatmap: -Columns must contain at least one label." } $totalCells = $Rows.Count * $Columns.Count if ($totalCells -gt $MaxCells) { throw "Add-DhHeatmap: Rows × Columns = $totalCells exceeds -MaxCells ($MaxCells). Split the matrix or raise -MaxCells deliberately." } if ($totalCells -gt 1000) { Write-Warning "Add-DhHeatmap: $totalCells cells is dense ($($Rows.Count) × $($Columns.Count)) — consider splitting into smaller grouped heatmaps for legibility." } # Normalise axes and lookup sets $rowList = @($Rows | ForEach-Object { [string]$_ }) $colList = @($Columns | ForEach-Object { [string]$_ }) $rowSet = @{} $colSet = @{} foreach ($r in $rowList) { $rowSet[$r] = $true } foreach ($c in $colList) { $colSet[$c] = $true } # Local InvariantCulture parser function _tryNum { param([object] $v, [string] $label, [string] $ctx) $n = 0.0 $ok = [double]::TryParse( [string]$v, [System.Globalization.NumberStyles]::Float -bor [System.Globalization.NumberStyles]::AllowThousands, [System.Globalization.CultureInfo]::InvariantCulture, [ref]$n) if (-not $ok) { throw "Add-DhHeatmap: $ctx — $label must be a number, got: $v" } return $n } $normCells = foreach ($cell in $Cells) { if ($cell -isnot [hashtable] -and $cell -isnot [System.Collections.Specialized.OrderedDictionary]) { throw "Add-DhHeatmap: each cell must be a hashtable. Got: $($cell.GetType().Name)" } foreach ($req in 'Row','Column','Value') { if (-not $cell.Contains($req) -or $null -eq $cell[$req]) { throw "Add-DhHeatmap: each cell must have a non-empty '$req' key." } } $r = [string]$cell['Row'] $c = [string]$cell['Column'] if (-not $rowSet.ContainsKey($r)) { throw "Add-DhHeatmap: cell Row '$r' is not in -Rows." } if (-not $colSet.ContainsKey($c)) { throw "Add-DhHeatmap: cell Column '$c' is not in -Columns." } $v = _tryNum $cell['Value'] 'Value' "cell ($r, $c)" @{ Row = $r Column = $c Value = $v Tooltip = if ($cell.Contains('Tooltip') -and $null -ne $cell['Tooltip']) { [string]$cell['Tooltip'] } else { '' } } } # Optional Min/Max bounds $minNum = if ($null -ne $Min) { _tryNum $Min 'Min' 'Add-DhHeatmap' } else { $null } $maxNum = if ($null -ne $Max) { _tryNum $Max 'Max' 'Add-DhHeatmap' } else { $null } if (($null -ne $minNum) -and ($null -ne $maxNum) -and ($maxNum -le $minNum)) { throw "Add-DhHeatmap: -Max ($maxNum) must be greater than -Min ($minNum)." } $Report.Blocks.Add([ordered]@{ BlockType = 'heatmap' Id = $Id Title = $Title Rows = $rowList Columns = $colList Cells = @($normCells) RowLabel = $RowLabel ColumnLabel = $ColumnLabel ColorScale = $ColorScale Min = $minNum Max = $maxNum Unit = $Unit CellSize = $CellSize ShowValues = [bool]$ShowValues NavGroup = $NavGroup NavSubGroup = $NavSubGroup }) Write-Verbose "Add-DhHeatmap: '$Id' ($($rowList.Count) × $($colList.Count), $(@($normCells).Count) cells, scale=$ColorScale)." } |