110 lines
3.4 KiB
C#
110 lines
3.4 KiB
C#
|
|
using System.Drawing.Drawing2D;
|
||
|
|
|
||
|
|
namespace Substation;
|
||
|
|
|
||
|
|
public class HeatMap : Panel
|
||
|
|
{
|
||
|
|
record Tick(byte[] Freq, byte[] Intensity);
|
||
|
|
|
||
|
|
readonly List<Tick> _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<byte>(), Array.Empty<byte>()));
|
||
|
|
|
||
|
|
Invalidate();
|
||
|
|
}
|
||
|
|
|
||
|
|
protected override void OnPaint(PaintEventArgs e)
|
||
|
|
{
|
||
|
|
base.OnPaint(e);
|
||
|
|
var g = e.Graphics;
|
||
|
|
g.SmoothingMode = SmoothingMode.None;
|
||
|
|
|
||
|
|
int w = Width;
|
||
|
|
int h = Height;
|
||
|
|
int colW = Math.Max(1, w / _maxTicks);
|
||
|
|
|
||
|
|
// 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;
|
||
|
|
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));
|
||
|
|
}
|
||
|
|
}
|