VBAF.Visualization.GraphRenderer.ps1
|
#Requires -Version 5.1 <# .SYNOPSIS Graph rendering utilities for learning visualization .DESCRIPTION Provides methods to draw learning curves, heatmaps, and other visualizations using System.Drawing. .NOTES Part of VBAF (Visual Business Automation Framework) FIXED: Array subtraction errors + Assembly loading order #> class GraphRenderer { # Draw line graph - FIXED: Accept ArrayList directly instead of array static [void] DrawLineGraph([System.Drawing.Graphics]$g, [System.Drawing.Rectangle]$bounds, [System.Collections.ArrayList]$dataList, [string]$title, [System.Drawing.Color]$lineColor) { # Convert to array INSIDE this method to avoid scoping issues $data = @() foreach ($item in $dataList) { $data += [double]$item } if ($data.Count -eq 0) { # No data yet - show placeholder $font = New-Object Drawing.Font("Segoe UI", 10) $brush = New-Object Drawing.SolidBrush([Drawing.Color]::Gray) $text = "$title (No data yet)" $g.DrawString($text, $font, $brush, $bounds.X + 10, $bounds.Y + 10) $font.Dispose() $brush.Dispose() return } # Background $bgBrush = New-Object Drawing.SolidBrush([Drawing.Color]::FromArgb(30, 30, 40)) $g.FillRectangle($bgBrush, $bounds) $bgBrush.Dispose() # Border $borderPen = New-Object Drawing.Pen([Drawing.Color]::FromArgb(60, 60, 80), 1) $g.DrawRectangle($borderPen, $bounds) $borderPen.Dispose() # Title $font = New-Object Drawing.Font("Segoe UI", 10, [Drawing.FontStyle]::Bold) $titleBrush = New-Object Drawing.SolidBrush([Drawing.Color]::White) $g.DrawString($title, $font, $titleBrush, $bounds.X + 10, $bounds.Y + 5) $font.Dispose() $titleBrush.Dispose() # Calculate scale $padding = 40 $graphX = $bounds.X + $padding $graphY = $bounds.Y + 30 $graphWidth = $bounds.Width - ($padding * 2) $graphHeight = $bounds.Height - 50 # Find min/max using Measure-Object which is more reliable $stats = $data | Measure-Object -Minimum -Maximum $minVal = [double]$stats.Minimum $maxVal = [double]$stats.Maximum # Add padding to range $range = $maxVal - $minVal if ($range -eq 0) { $range = 1.0 } $padding10Percent = $range * 0.1 $minVal = $minVal - $padding10Percent $maxVal = $maxVal + $padding10Percent $range = $maxVal - $minVal # Draw grid lines $gridPen = New-Object Drawing.Pen([Drawing.Color]::FromArgb(50, 50, 60), 1) $gridPen.DashStyle = [Drawing.Drawing2D.DashStyle]::Dot for ($i = 0; $i -le 4; $i++) { $yPos = $graphY + ($graphHeight * $i / 4) $g.DrawLine($gridPen, $graphX, $yPos, $graphX + $graphWidth, $yPos) } $gridPen.Dispose() # Draw axis labels $labelFont = New-Object Drawing.Font("Consolas", 8) $labelBrush = New-Object Drawing.SolidBrush([Drawing.Color]::LightGray) for ($i = 0; $i -le 4; $i++) { $fraction = $i / 4.0 $value = $maxVal - ($range * $fraction) $yPos = $graphY + ($graphHeight * $i / 4) $g.DrawString($value.ToString("F2"), $labelFont, $labelBrush, $bounds.X + 5, $yPos - 7) } $labelFont.Dispose() $labelBrush.Dispose() # Draw line if ($data.Count -gt 1) { $points = New-Object System.Collections.ArrayList for ($i = 0; $i -lt $data.Count; $i++) { $xPos = $graphX + ($i * $graphWidth / ($data.Count - 1)) # Safe normalization $dataValue = [double]$data[$i] $normalized = 0.5 # default if ($range -ne 0) { $normalized = ($dataValue - $minVal) / $range } $yPos = $graphY + $graphHeight - ($normalized * $graphHeight) $point = New-Object Drawing.PointF($xPos, $yPos) [void]$points.Add($point) } if ($points.Count -gt 1) { $linePen = New-Object Drawing.Pen($lineColor, 2) $g.DrawLines($linePen, $points.ToArray([Drawing.PointF])) $linePen.Dispose() } } # Draw current value if ($data.Count -gt 0) { $currentVal = [double]$data[$data.Count - 1] $valueFont = New-Object Drawing.Font("Consolas", 9, [Drawing.FontStyle]::Bold) $valueBrush = New-Object Drawing.SolidBrush($lineColor) $valueText = "Current: $($currentVal.ToString('F4'))" $g.DrawString($valueText, $valueFont, $valueBrush, $bounds.X + $bounds.Width - 150, $bounds.Y + 5) $valueFont.Dispose() $valueBrush.Dispose() } } # Draw heatmap (for Q-values) static [void] DrawHeatmap([System.Drawing.Graphics]$g, [System.Drawing.Rectangle]$bounds, [hashtable]$data, [string]$title) { # Background $bgBrush = New-Object Drawing.SolidBrush([Drawing.Color]::FromArgb(30, 30, 40)) $g.FillRectangle($bgBrush, $bounds) $bgBrush.Dispose() # Border $borderPen = New-Object Drawing.Pen([Drawing.Color]::FromArgb(60, 60, 80), 1) $g.DrawRectangle($borderPen, $bounds) $borderPen.Dispose() # Title $font = New-Object Drawing.Font("Segoe UI", 10, [Drawing.FontStyle]::Bold) $titleBrush = New-Object Drawing.SolidBrush([Drawing.Color]::White) $g.DrawString($title, $font, $titleBrush, $bounds.X + 10, $bounds.Y + 5) $font.Dispose() $titleBrush.Dispose() if ($data.Count -eq 0) { $font2 = New-Object Drawing.Font("Segoe UI", 9) $brush2 = New-Object Drawing.SolidBrush([Drawing.Color]::Gray) $g.DrawString("No Q-values yet", $font2, $brush2, $bounds.X + 10, $bounds.Y + 30) $font2.Dispose() $brush2.Dispose() return } # Sort data by value and get min/max using Measure-Object $sorted = $data.GetEnumerator() | Sort-Object Value -Descending $values = @($sorted | ForEach-Object { [double]$_.Value }) $stats = $values | Measure-Object -Minimum -Maximum $minVal = [double]$stats.Minimum $maxVal = [double]$stats.Maximum $range = $maxVal - $minVal if ($range -eq 0) { $range = 1.0 } # Draw bars $barHeight = 25 $startY = $bounds.Y + 30 $barWidth = $bounds.Width - 200 $labelFont = New-Object Drawing.Font("Consolas", 9) $index = 0 foreach ($item in $sorted) { $yPos = $startY + ($index * $barHeight) if ($yPos + $barHeight -gt $bounds.Y + $bounds.Height) { break } # Color based on value $itemValue = [double]$item.Value $normalized = ($itemValue - $minVal) / $range $red = [int](255 * (1.0 - $normalized)) $green = [int](255 * $normalized) $barColor = [Drawing.Color]::FromArgb(150, $red, $green, 0) # Draw bar $barActualWidth = [int]($barWidth * $normalized) $barBrush = New-Object Drawing.SolidBrush($barColor) $g.FillRectangle($barBrush, $bounds.X + 120, $yPos, $barActualWidth, $barHeight - 2) $barBrush.Dispose() # Label $labelBrush = New-Object Drawing.SolidBrush([Drawing.Color]::White) $g.DrawString($item.Key, $labelFont, $labelBrush, $bounds.X + 10, $yPos + 5) # Value $valueText = $itemValue.ToString("F4") $g.DrawString($valueText, $labelFont, $labelBrush, $bounds.X + $bounds.Width - 80, $yPos + 5) $labelBrush.Dispose() $index++ } $labelFont.Dispose() } # Draw progress bar static [void] DrawProgressBar([System.Drawing.Graphics]$g, [System.Drawing.Rectangle]$bounds, [double]$value, [double]$max, [string]$label, [System.Drawing.Color]$color) { # Background $bgBrush = New-Object Drawing.SolidBrush([Drawing.Color]::FromArgb(50, 50, 60)) $g.FillRectangle($bgBrush, $bounds) $bgBrush.Dispose() # Progress $progress = if ($max -gt 0) { $value / $max } else { 0 } $progressWidth = [int]($bounds.Width * $progress) $progressBrush = New-Object Drawing.SolidBrush($color) $g.FillRectangle($progressBrush, $bounds.X, $bounds.Y, $progressWidth, $bounds.Height) $progressBrush.Dispose() # Label $font = New-Object Drawing.Font("Consolas", 10, [Drawing.FontStyle]::Bold) $textBrush = New-Object Drawing.SolidBrush([Drawing.Color]::White) $text = "$label : $($value.ToString('F2')) / $($max.ToString('F2'))" $textSize = $g.MeasureString($text, $font) $textX = $bounds.X + ($bounds.Width - $textSize.Width) / 2 $textY = $bounds.Y + ($bounds.Height - $textSize.Height) / 2 $g.DrawString($text, $font, $textBrush, $textX, $textY) $font.Dispose() $textBrush.Dispose() } } |