Public/Add-DhLineChart.ps1
|
function Add-DhLineChart { <# .SYNOPSIS Add a line or area chart with axes, multi-series support, and an optional target / threshold annotation. .DESCRIPTION Renders a time-series style chart as a top-level block. Pure SVG, no external library. Single or multiple series share a common X-axis of category labels (timestamps, day names, etc.). Optional `-Filled` switches each series from a polyline to a filled area. Per the IT-infrastructure KPI dashboard specification (§7.4): "Always pair a time-series chart with a threshold annotation line." Use `-TargetLine` to draw a dashed horizontal reference at a Y value (SLA, alert threshold, capacity ceiling) plus an optional `-TargetLabel`. .PARAMETER Report Dashboard object from New-DhDashboard. .PARAMETER Id Unique identifier (alphanumeric, dash, underscore). .PARAMETER Title Chart heading shown above the plot. .PARAMETER XAxis Array of category labels for the X-axis (e.g. timestamps). Each series's Values must have the same length. .PARAMETER Series Array of series-definition hashtables: @{ Label = 'Avg CPU' # REQUIRED Values = @(42,38,55,67,71,68,52) # REQUIRED — numeric, len == XAxis Class = 'cell-ok' # optional — RAG class for line colour Color = '#1B6B35' # optional — explicit colour override } .PARAMETER Filled Render each series as a filled area chart instead of a line. .PARAMETER TargetLine Optional numeric Y value — a dashed horizontal annotation line is drawn at this position. Use it for SLA / warning thresholds, capacity ceilings, alert lines. .PARAMETER TargetLabel Optional label text shown next to the target line. .PARAMETER YAxisMin / -YAxisMax Explicit Y-axis bounds. When omitted, the chart auto-fits from the series data (with a small padding above the max). .PARAMETER YAxisUnit Optional suffix appended to Y-axis tick labels (e.g. '%', 'ms', 'GB'). .PARAMETER ShowLegend Show a legend strip below the chart with one swatch per series. Defaults to $true. .PARAMETER ShowGrid Show horizontal gridlines at each Y-axis tick. Defaults to $true. .PARAMETER Height Pixel height of the SVG. Defaults to 240. .EXAMPLE # Single series with a warning-threshold line Add-DhLineChart -Report $report -Id 'cpu-trend' -Title 'Cluster CPU (24h)' ` -XAxis @('00:00','04:00','08:00','12:00','16:00','20:00','24:00') ` -Series @( @{ Label='Avg'; Values=@(42,38,55,67,71,68,52); Class='cell-ok' } ) ` -TargetLine 70 -TargetLabel 'Warning threshold' ` -YAxisMax 100 -YAxisUnit '%' .EXAMPLE # Multi-series filled area chart Add-DhLineChart -Report $report -Id 'net-bw' -Title 'Network throughput' ` -Filled ` -XAxis $hours ` -Series @( @{ Label='Ingress'; Values=$ingress; Class='cell-ok' } @{ Label='Egress'; Values=$egress; Class='cell-warn' } ) #> [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[]] $XAxis, [Parameter(Mandatory)] [object[]] $Series, [switch] $Filled, [object] $TargetLine = $null, [string] $TargetLabel = '', [object] $YAxisMin = $null, [object] $YAxisMax = $null, [string] $YAxisUnit = '', [bool] $ShowLegend = $true, [bool] $ShowGrid = $true, [ValidateRange(80, 1000)] [int] $Height = 240, [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-DhLineChart: A block with Id '$Id' already exists in this report. Use a unique Id." } } if ($XAxis.Count -lt 2) { throw "Add-DhLineChart: -XAxis must contain at least 2 labels (got $($XAxis.Count))." } if (-not $Series -or $Series.Count -lt 1) { throw "Add-DhLineChart: -Series must contain at least one series." } # Local InvariantCulture number 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-DhLineChart: $ctx — $label must be a number, got: $v" } return $n } $normSeries = foreach ($s in $Series) { if ($s -isnot [hashtable] -and $s -isnot [System.Collections.Specialized.OrderedDictionary]) { throw "Add-DhLineChart: each series entry must be a hashtable. Got: $($s.GetType().Name)" } if (-not $s.Contains('Label') -or [string]::IsNullOrWhiteSpace([string]$s['Label'])) { throw "Add-DhLineChart: each series must have a non-empty 'Label' key." } if (-not $s.Contains('Values') -or $null -eq $s['Values']) { throw "Add-DhLineChart: series '$($s.Label)' must have a 'Values' key." } $vals = @($s['Values']) if ($vals.Count -ne $XAxis.Count) { throw "Add-DhLineChart: series '$($s.Label)' has $($vals.Count) Values but XAxis has $($XAxis.Count) labels. Lengths must match." } $numVals = foreach ($v in $vals) { if ($null -eq $v -or [string]::IsNullOrWhiteSpace([string]$v)) { # Null entries are allowed — JS plots them as line breaks (NaN) [double]::NaN } else { _tryNum $v "series '$($s.Label)' value" "Add-DhLineChart" } } @{ Label = [string]$s['Label'] Values = @($numVals) Class = if ($s.Contains('Class') -and $s['Class']) { [string]$s['Class'] } else { '' } Color = if ($s.Contains('Color') -and $s['Color']) { [string]$s['Color'] } else { '' } } } # Optional numerics $tlNum = if ($null -ne $TargetLine) { _tryNum $TargetLine 'TargetLine' 'Add-DhLineChart' } else { $null } $yMinNum = if ($null -ne $YAxisMin) { _tryNum $YAxisMin 'YAxisMin' 'Add-DhLineChart' } else { $null } $yMaxNum = if ($null -ne $YAxisMax) { _tryNum $YAxisMax 'YAxisMax' 'Add-DhLineChart' } else { $null } if (($null -ne $yMinNum) -and ($null -ne $yMaxNum) -and ($yMaxNum -le $yMinNum)) { throw "Add-DhLineChart: -YAxisMax ($yMaxNum) must be greater than -YAxisMin ($yMinNum)." } # Normalise XAxis to strings $xAxisOut = @($XAxis | ForEach-Object { [string]$_ }) $Report.Blocks.Add([ordered]@{ BlockType = 'linechart' Id = $Id Title = $Title XAxis = $xAxisOut Series = @($normSeries) Filled = [bool]$Filled TargetLine = $tlNum TargetLabel = $TargetLabel YAxisMin = $yMinNum YAxisMax = $yMaxNum YAxisUnit = $YAxisUnit ShowLegend = $ShowLegend ShowGrid = $ShowGrid Height = $Height NavGroup = $NavGroup NavSubGroup = $NavSubGroup }) Write-Verbose "Add-DhLineChart: '$Id' ($($normSeries.Count) series, $($xAxisOut.Count) X-points$(if ($Filled) { ', filled' })$(if ($tlNum) { ', target=$tlNum' }))." } |