using System.Drawing.Drawing2D; namespace Substation; public class HeatMap : Panel { record Tick(byte[] Freq, byte[] Intensity); readonly List _history = new(); readonly int _maxTicks; public HeatMap(int maxTicks = 60) { _maxTicks = maxTicks; DoubleBuffered = true; BackColor = Color.FromArgb(20, 20, 22); } public void AddTick(byte[] freq, byte[] intensity, bool active) { if (_history.Count >= _maxTicks) _history.RemoveAt(0); if (active) _history.Add(new Tick(freq, intensity)); else _history.Add(new Tick(Array.Empty(), Array.Empty())); Invalidate(); } protected override void OnPaint(PaintEventArgs e) { base.OnPaint(e); var g = e.Graphics; g.SmoothingMode = SmoothingMode.None; int w = Width; int h = Height; // Draw axis labels using var font = new Font(FontFamily.GenericMonospace, 7); using var textBrush = new SolidBrush(Color.FromArgb(100, 100, 100)); g.DrawString("100Hz", font, textBrush, 2, 2); g.DrawString("1Hz", font, textBrush, 2, h - 14); int plotX = 36; int plotW = w - plotX - 4; int plotH = h - 4; int colW = Math.Max(1, plotW / _maxTicks); for (int i = 0; i < _history.Count; i++) { var tick = _history[i]; int xPos = plotX + i * colW; if (tick.Freq.Length == 0) { // Inactive — dark cell using var dark = new SolidBrush(Color.FromArgb(25, 25, 28)); g.FillRectangle(dark, xPos, 2, colW, plotH); continue; } // 4 sub-ticks side by side within the column int subW = Math.Max(1, colW / 4); for (int s = 0; s < 4; s++) { int freq = tick.Freq[s]; // 10-240 int inten = tick.Intensity[s]; // 0-100 if (inten > 100) inten = 0; // 101 sentinel = off // Map freq byte (10-240) to Y position (top=high freq, bottom=low freq) float normFreq = (freq - 10f) / (240f - 10f); int y = (int)(2 + plotH * (1f - normFreq)); if (y < 2) y = 2; if (y > plotH) y = plotH; var color = HeatColor(inten); using var brush = new SolidBrush(color); g.FillRectangle(brush, xPos + s * subW, y, subW, 3); } } } static Color HeatColor(int intensity) { // 0=black, 30=blue, 60=green, 80=gold, 100=red if (intensity <= 0) return Color.FromArgb(20, 20, 22); if (intensity < 30) { float t = intensity / 30f; return Color.FromArgb(0, (int)(50 * t), (int)(80 + 80 * t)); } if (intensity < 60) { float t = (intensity - 30) / 30f; return Color.FromArgb((int)(60 * t), (int)(130 + 80 * t), (int)(160 - 100 * t)); } if (intensity < 80) { float t = (intensity - 60) / 20f; return Color.FromArgb((int)(60 + 180 * t), 210, (int)(60 - 30 * t)); } float t2 = (intensity - 80) / 20f; return Color.FromArgb(255, (int)(240 - 140 * t2), (int)(30 - 10 * t2)); } }