Calculating heat map colours
Asked Answered
C

3

9

I'm working on a heat map made up of an HTML table. This table contains n cells and has a lowest value and a highest value (highest is always higher than lowest). Each cell has a cell value. All these values are ints.

Cells with the lowest value are meant to be a light blue, scaling across to the point where the cells with the highest value are a deep red. See gradient below for an ideal range:

enter image description here

To calculate the hex colour value of each individual cell, I look at the lowest and highest values from the table and the cell's total value, passing them into a method that returns the RGB hex, ready to be used with HTML's background-color style.

Here is the method so far:

public string ConvertTotalToRgb(int low, int high, int cell)
{
    int range = high - low;

    int main = 255 * cell/ range;
    string hexR = main.ToString("X2");
    int flip = 255 * (1 - (cell/ range));
    string hexB = flip.ToString("X2");

    return hexR + "00" + hexB;
}

With a lowest value of 0 and a highest value of 235, this method returns the following table (cell values are in the cells).

enter image description here

Example case: If lowest was 20, highest was 400 and cell was 60, I would want the method returning the RGB hex of the colour about 15.8% of the way along the gradient.

400 - 20 = 380
380 / 60 = 6.33
100 / 6.33 = 15.8

I'm aware that this formula isn't quite accurate but that's partially why I'm asking for help here.

I've made it this far but I'm really not sure how to proceed. Any help is hugely appreciated!

Catlee answered 23/7, 2013 at 22:4 Comment(6)
Your question is really confusing.. what does the HTML table look like?Babby
I literally put a picture of the table in the question, right at the bottom :/Catlee
I misunderstood then.. where are the values coming from?Babby
Where the values are coming from should be irrelevant to this question; I've purposefully narrowed the scope to a single method. This single method accepts the lowest possible value (which would render as the far left colour in the gradient), the highest possible value (which would render as the far right colour in the gradient) and the cell value which would render as a colour somewhere in the gradient depending on the value.Catlee
For example, if lowest was 20, highest was 400 and cell was 60, I would want the method returning the RGB hex of the colour about 15% of the way along the gradient.Catlee
Ok in that case then you know that 0% is 255 0 0.... 50% is 0 255 0 and 100% is 0 0 255 right? therefore 15% would fall between 255 0 0 and 0 255 0 so in my mind thats (255 * 0.15) (255 * 0.15) 0.... sorry if ive completely misunderstood (also the 0.15's seem slightly off to me in my own logic)Babby
W
4

What you really want is an HSV color, because the Hue (H value) is cyclical. if the hue is between 0 and 1, then it indicates how far along your color gradient you want to be. The saturation and value components are always 1 in this case.

Follow the HSV to RGB conversion code here: HSV to RGB Stops at yellow C#

public string ConvertTotalToRgb(int low, int high, int cell)
{
    int range = high - low;
    float h= cell/ (float)range;
    rgb = HSVtoRGB(h,1.0f,1.0f);
    return "#" + rgb.R.ToString("X2") + rgb.G.ToString("X2") + rgb.B.ToString("X2");
}

If you know you can target browsers that support it (CSS3), you can just render the hsv value directly.

Wallen answered 24/7, 2013 at 0:12 Comment(2)
That doesn't appear to have worked - it's rendering everything in the table above as red.Catlee
Ok, I figured out the issue. The hue was a tiny decimal, meaning it barely moved the colour along the spectrum. When it was multiplied by 240 (the position of pure blue on the HSV spectrum), it worked as expected. Thanks for the help!Catlee
M
14

Today I've googoled to find some help about his matter, but no answer I found answer precisely the question.

The "Heat Map" is not a raw Value% to Hue conversion, it can be build with 7, 5 or less colors (eg: red to yellow), can be linear or logarithmic, etc.

enter image description here

I wrote, and I sharing, a C# .Net 4.6.1 code that can be a solid base to build your ValueToColorOnHeatMap converter: (Note: this was debuged and tested)

using System.Windows.Media;// for WPF
// for WindowsForms using System.Drawing
using System;
using System.Collections.Generic;
public class ColorHeatMap
{
    public ColorHeatMap()
    {
        initColorsBlocks();
    }
    public ColorHeatMap(byte alpha)
    {
        this.Alpha = alpha;
        initColorsBlocks();
    }
    private void initColorsBlocks()
    {
        ColorsOfMap.AddRange(new Color[]{
            Color.FromArgb(Alpha, 0, 0, 0) ,//Black
            Color.FromArgb(Alpha, 0, 0, 0xFF) ,//Blue
            Color.FromArgb(Alpha, 0, 0xFF, 0xFF) ,//Cyan
            Color.FromArgb(Alpha, 0, 0xFF, 0) ,//Green
            Color.FromArgb(Alpha, 0xFF, 0xFF, 0) ,//Yellow
            Color.FromArgb(Alpha, 0xFF, 0, 0) ,//Red
            Color.FromArgb(Alpha, 0xFF, 0xFF, 0xFF) // White
        });
    }
    public Color GetColorForValue(double val, double maxVal)
    {
        double valPerc = val / maxVal;// value%
        double colorPerc = 1d / (ColorsOfMap.Count-1);// % of each block of color. the last is the "100% Color"
        double blockOfColor = valPerc / colorPerc;// the integer part repersents how many block to skip
        int blockIdx = (int)Math.Truncate(blockOfColor);// Idx of 
        double valPercResidual = valPerc - (blockIdx*colorPerc);//remove the part represented of block 
        double percOfColor = valPercResidual / colorPerc;// % of color of this block that will be filled

        Color cTarget = ColorsOfMap[blockIdx];
        Color cNext = cNext = ColorsOfMap[blockIdx + 1]; 

        var deltaR =cNext.R - cTarget.R;
        var deltaG =cNext.G - cTarget.G;
        var deltaB =cNext.B - cTarget.B;

        var R = cTarget.R + (deltaR * percOfColor);
        var G = cTarget.G + (deltaG * percOfColor);
        var B = cTarget.B + (deltaB * percOfColor);

        Color c = ColorsOfMap[0];
        try
        {
            c = Color.FromArgb(Alpha, (byte)R, (byte)G, (byte)B);
        }
        catch (Exception ex)
        {
        }
        return c;
    }
    public byte Alpha = 0xff;
    public List<Color> ColorsOfMap = new List<Color>();
}

To use less, or personalized colors, work on ColorsOfMap List. The class use a proportional, linear, reperesentation, work on blocOfColor to change the linearity.

I hope this will help some people to save time :)

Thanks to all people that share their answers/solutions with the comunity.

To use less, or personalized colors, work on ColorsOfMap List. The class use a proportional, linear, reperesentation, work on blocOfColor to change the linearity.

I hope this will help some people to save time :)

Thanks to all people that share their answers/solutions with the comunity.

Meade answered 19/6, 2016 at 20:46 Comment(6)
As written, this code crashes with an index out of bounds exception when GetColorForValue is called with val == maxval. A simple fix would be double valPerc = val / (maxval + 1)Affettuoso
BTW, this was the best color-mapping code example I found after several hours of searching. Using (maxval + 1) to keep valPerc below 1.0 worked great in my environment, but you might need a different approach depending on the range of your values.Affettuoso
I have also used this implementation, it is nice. You can configure your own colors in gradient, etc. But I have changed some lines. Your implementation is assuming that minVal is 0 but what if it is not? So I used double valPerc = (val-minVal) / (maxVal-minVal); Also changed @Affettuoso treatment of case val==maxVal this way Color cNext = val == maxVal ? ColorGradient[blockIdx] : ColorGradient[blockIdx + 1]; Because when you use double valPerc = val / (maxval + 1) you will never reach max color which is in this case red.Nanci
I fixed the index error by adding the last color (white) twice to the ColorsOfMap list and then used ColorsOfMap.Count-2 instead of ColorsOfMap.Count-1 in the calculation of colorPerc.Westberg
How can I use that ColorGradient ?Fissi
@SimonGiesen, by example, in WPF, using it inside your Converter to convert a value to a Color with this logic and use it in the UIMeade
W
4

What you really want is an HSV color, because the Hue (H value) is cyclical. if the hue is between 0 and 1, then it indicates how far along your color gradient you want to be. The saturation and value components are always 1 in this case.

Follow the HSV to RGB conversion code here: HSV to RGB Stops at yellow C#

public string ConvertTotalToRgb(int low, int high, int cell)
{
    int range = high - low;
    float h= cell/ (float)range;
    rgb = HSVtoRGB(h,1.0f,1.0f);
    return "#" + rgb.R.ToString("X2") + rgb.G.ToString("X2") + rgb.B.ToString("X2");
}

If you know you can target browsers that support it (CSS3), you can just render the hsv value directly.

Wallen answered 24/7, 2013 at 0:12 Comment(2)
That doesn't appear to have worked - it's rendering everything in the table above as red.Catlee
Ok, I figured out the issue. The hue was a tiny decimal, meaning it barely moved the colour along the spectrum. When it was multiplied by 240 (the position of pure blue on the HSV spectrum), it worked as expected. Thanks for the help!Catlee
G
0

I use the following c# class to scale between 3 colours (typically red for negative, yellow for the mid point and green for positive numbers:

using System.Drawing;

namespace Utils.Maths;

public class ColourScaler
{
    public static (int r, int g, int b) HighestColor = (99, 190, 123);
    public static (int r, int g, int b) MidColor = (255, 235, 132);
    public static (int r, int g, int b) LowestColor = (248, 105, 107);

    public double MidValue { get; set; } = 0;
    public double HighestValue { get; set; } = 0;
    public double LowestValue { get; set; } = 0;
    
    public IEnumerable<double> Values 
    { 
        set
        {
            var items = value.OrderBy(o => o).Distinct().ToArray();
            if (items.Length > 0) 
            {
                LowestValue= items[0];
                HighestValue = items[items.Length - 1];
            }
        }
    }

    public ColourScaler(IEnumerable<double>? values = null, double midValue = 0)
    {
        Values = values ?? Enumerable.Empty<double>();
        MidValue = midValue;
    }

    public (int r, int g, int b) GetRgb(double value)
    {
        if (value < LowestValue || value > HighestValue)
            throw new ArgumentOutOfRangeException().AddData("value", value);

        if (value >= 0)
        {
            if (HighestValue - MidValue == 0)
                return HighestColor;

            var ratio = (value - MidValue) / (HighestValue - MidValue);
            var r = (int)(MidColor.r - (MidColor.r - HighestColor.r) * ratio);
            var g = (int)(MidColor.g - (MidColor.g - HighestColor.g) * ratio);
            var b = (int)(MidColor.b - (MidColor.b - HighestColor.b) * ratio);
            return (r, g, b);
        }
        else
        {
            if (MidValue - LowestValue == 0)
                return LowestColor;

            var ratio = (MidValue - value) / (MidValue - LowestValue);
            var r = (int)(MidColor.r - (MidColor.r - LowestColor.r) * ratio);
            var g = (int)(MidColor.g - (MidColor.g - LowestColor.g) * ratio);
            var b = (int)(MidColor.b - (MidColor.b - LowestColor.b) * ratio);
            return (r, g, b);
        }
    }

    public Color GetColour(double value)
    {
        var rgb = GetRgb(value);
        return Color.FromArgb(rgb.r, rgb.g, rgb.b);
    }
}

And here are some tests:

namespace Tests.ColourScaler;

[TestClass]
public class ColourScalerTests
{
    [TestMethod]
    public void ColourScaler_Values()
    {
        var item = new ColourScaler { Values = new double[] { 13, -2, -3, 4, 3, -3, 4, 1, 0 } };
        Assert.AreEqual(-3, item.LowestValue);
        Assert.AreEqual(0, item.MidValue);
        Assert.AreEqual(13, item.HighestValue);

        item = new ColourScaler { Values = new double[] { } };
        Assert.AreEqual(0, item.LowestValue);
        Assert.AreEqual(0, item.MidValue);
        Assert.AreEqual(0, item.HighestValue);
    }

    [TestMethod]
    public void ColourScaler_New()
    {
        var item = new ColourScaler(new double[] { 13, -2, -3, 4, 3, -3, 4, 1, 0 }, 1);
        Assert.AreEqual(-3, item.LowestValue);
        Assert.AreEqual(1, item.MidValue);
        Assert.AreEqual(13, item.HighestValue);

        item = new ColourScaler(new double[] { 13, -2, -3, 4, 3, -3, 4, 1, 0 });
        Assert.AreEqual(-3, item.LowestValue);
        Assert.AreEqual(0, item.MidValue);
        Assert.AreEqual(13, item.HighestValue);

        item = new ColourScaler(new double[] { });
        Assert.AreEqual(0, item.LowestValue);
        Assert.AreEqual(0, item.MidValue);
        Assert.AreEqual(0, item.HighestValue);
    }

    [TestMethod]
    public void ColourScaler_GetRgb()
    {
        var item = new ColourScaler { Values = new double[] { -7, -5, -3, -1, 0, 2, 4, 6 } };

        var rgb = item.GetRgb(-7);
        Assert.AreEqual(248, rgb.r);
        Assert.AreEqual(105, rgb.g);
        Assert.AreEqual(107, rgb.b);

        rgb = item.GetRgb(-6);
        Assert.AreEqual(249, rgb.r);
        Assert.AreEqual(123, rgb.g);
        Assert.AreEqual(110, rgb.b);

        rgb = item.GetRgb(-5);
        Assert.AreEqual(250, rgb.r);
        Assert.AreEqual(142, rgb.g);
        Assert.AreEqual(114, rgb.b);

        rgb = item.GetRgb(-2);
        Assert.AreEqual(253, rgb.r);
        Assert.AreEqual(197, rgb.g);
        Assert.AreEqual(124, rgb.b);

        rgb = item.GetRgb(-1);
        Assert.AreEqual(254, rgb.r);
        Assert.AreEqual(216, rgb.g);
        Assert.AreEqual(128, rgb.b);

        rgb = item.GetRgb(0);
        Assert.AreEqual(255, rgb.r);
        Assert.AreEqual(235, rgb.g);
        Assert.AreEqual(132, rgb.b);

        rgb = item.GetRgb(1);
        Assert.AreEqual(229, rgb.r);
        Assert.AreEqual(227, rgb.g);
        Assert.AreEqual(130, rgb.b);

        rgb = item.GetRgb(2);
        Assert.AreEqual(203, rgb.r);
        Assert.AreEqual(220, rgb.g);
        Assert.AreEqual(129, rgb.b);

        rgb = item.GetRgb(4);
        Assert.AreEqual(151, rgb.r);
        Assert.AreEqual(205, rgb.g);
        Assert.AreEqual(126, rgb.b);

        rgb = item.GetRgb(5);
        Assert.AreEqual(125, rgb.r);
        Assert.AreEqual(197, rgb.g);
        Assert.AreEqual(124, rgb.b);

        rgb = item.GetRgb(6);
        Assert.AreEqual(99, rgb.r);
        Assert.AreEqual(190, rgb.g);
        Assert.AreEqual(123, rgb.b);
    }
}
Gummosis answered 24/11, 2022 at 8:20 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.