21
votes

We have a heatmap we want to display. The numbers that will make up the values being displayed are unknown (except that they will be positive integers). The range of numbers is also unknown (again, except that they will be posiitive integars). The range could be between 0 and 200 or 578 and 1M or whatever. It depends on the data, which is unknown.

We want to take an unknown range of positive integers and turn it into a scaled (compressed) range to be displayed with RGB values in a heatmap. I hope this makes sense. Thanks!

I want to clarify that the min/max values need to be "plugged" into the forumla.

10

10 Answers

21
votes

You need to first find the range of those values to get the min and max. Then you need to create a colour scale like the bar below this image. You can experiment with different functions to map an integer to an RGB. You need 3 functions R(X), G(X), B(X). Looking at the image below it loks like B(X) peaks in the middle, R(X) peaks at the end and green is somewhere else. As long as you make sure that you never get two (RGBs) for some value of X then you've got your conversion.

alt text
(source: globalwarmingart.com)

EDIT: Come to think of it you could sample some unit circle around YUV space. alt text http://www.biocrawler.com/w/images/e/ec/Yuv.png

Or even just download a high-res colour bar and sample that.

EDIT 2: I was just faced with color bar generation and remembered the MATLAB/Octave colorbar code. I plotted their data and got the following image. alt text

12
votes

You want to convert your data values to a frequency of light:

  • lower wavelength = cooler colors = blueish
  • higher wavelength = warmer colors = redder

The frequencies of visible light go from about 350nm (violet) to 650nm (red):

alt text
(source: gamonline.com)

The following function converts numbers in your specified range to the the range of visible light, then gets the rgb:

function DataPointToColor(Value, MinValue, MaxValue: Real): TColor;
var
   r, g, b: Byte;
   WaveLength: Real;
begin
   WaveLength := GetWaveLengthFromDataPoint(Value, MinValue, MaxValue);
   WavelengthToRGB(Wavelength, r, g, b);
   Result := RGB(r, g, b);
end;

With the function i wrote off the top of my head:

function GetWaveLengthFromDataPoint(Value: Real; MinValues, MaxValues: Real): Real;
const
   MinVisibleWaveLength = 350.0;
   MaxVisibleWaveLength = 650.0;
begin
   //Convert data value in the range of MinValues..MaxValues to the 
   //range 350..650

   Result := (Value - MinValue) / (MaxValues-MinValues) *
         (MaxVisibleWavelength - MinVisibleWavelength) +
         MinVisibleWaveLength;
end;

And a function i found on the internets, that converts a wavelength into RGB:

PROCEDURE WavelengthToRGB(CONST Wavelength:  Nanometers;
                          VAR R,G,B:  BYTE);
  CONST
    Gamma        =   0.80;
    IntensityMax = 255;
  VAR
    Blue   :  DOUBLE;
    factor :  DOUBLE;
    Green  :  DOUBLE;
    Red    :  DOUBLE;
  FUNCTION Adjust(CONST Color, Factor:  DOUBLE):  INTEGER;
  BEGIN
    IF   Color = 0.0
    THEN RESULT := 0     // Don't want 0^x = 1 for x <> 0
    ELSE RESULT := ROUND(IntensityMax * Power(Color * Factor, Gamma))
  END {Adjust};
BEGIN
  CASE TRUNC(Wavelength) OF
    380..439:
      BEGIN
        Red   := -(Wavelength - 440) / (440 - 380);
        Green := 0.0;
        Blue  := 1.0
      END;
    440..489:
      BEGIN
        Red   := 0.0;
        Green := (Wavelength - 440) / (490 - 440);
        Blue  := 1.0
      END;
    490..509:
      BEGIN
        Red   := 0.0;
        Green := 1.0;
        Blue  := -(Wavelength - 510) / (510 - 490)
      END;
    510..579:
      BEGIN
        Red   := (Wavelength - 510) / (580 - 510);
        Green := 1.0;
        Blue  := 0.0
      END;
    580..644:
      BEGIN
        Red   := 1.0;
        Green := -(Wavelength - 645) / (645 - 580);
        Blue  := 0.0
      END;
    645..780:
      BEGIN
        Red   := 1.0;
        Green := 0.0;
        Blue  := 0.0
      END;
    ELSE
      Red   := 0.0;
      Green := 0.0;
      Blue  := 0.0
  END;
  // Let the intensity fall off near the vision limits
  CASE TRUNC(Wavelength) OF
    380..419:  factor := 0.3 + 0.7*(Wavelength - 380) / (420 - 380);
    420..700:  factor := 1.0;
    701..780:  factor := 0.3 + 0.7*(780 - Wavelength) / (780 - 700)
    ELSE       factor := 0.0
  END;
  R := Adjust(Red,   Factor);
  G := Adjust(Green, Factor);
  B := Adjust(Blue,  Factor)
END {WavelengthToRGB}; 

Sample use:

Data set in the range of 10..65,000,000. And this particular data point has a value of 638,328:

color = DataPointToColor(638328, 10, 65000000);
7
votes

Function for colorbar

// value between 0 and 1 (percent)   
function color(value) {
    var RGB = {R:0,G:0,B:0};

    // y = mx + b
    // m = 4
    // x = value
    // y = RGB._
    if (0 <= value && value <= 1/8) {
        RGB.R = 0;
        RGB.G = 0;
        RGB.B = 4*value + .5; // .5 - 1 // b = 1/2
    } else if (1/8 < value && value <= 3/8) {
        RGB.R = 0;
        RGB.G = 4*value - .5; // 0 - 1 // b = - 1/2
        RGB.B = 1; // small fix
    } else if (3/8 < value && value <= 5/8) {
        RGB.R = 4*value - 1.5; // 0 - 1 // b = - 3/2
        RGB.G = 1;
        RGB.B = -4*value + 2.5; // 1 - 0 // b = 5/2
    } else if (5/8 < value && value <= 7/8) {
        RGB.R = 1;
        RGB.G = -4*value + 3.5; // 1 - 0 // b = 7/2
        RGB.B = 0;
    } else if (7/8 < value && value <= 1) {
        RGB.R = -4*value + 4.5; // 1 - .5 // b = 9/2
        RGB.G = 0;
        RGB.B = 0;
    } else {    // should never happen - value > 1
        RGB.R = .5;
        RGB.G = 0;
        RGB.B = 0;
    }

    // scale for hex conversion
    RGB.R *= 15;
    RGB.G *= 15;
    RGB.B *= 15;

    return Math.round(RGB.R).toString(16)+''+Math.round(RGB.G).toString(16)+''+Math.round(RGB.B).toString(16);
}
4
votes

Going off of the picture provided by Chris H, you can model the rgb values as:

r = min(max(0, 1.5-abs(1-4*(val-0.5))),1);
g = min(max(0, 1.5-abs(1-4*(val-0.25))),1);
b = min(max(0, 1.5-abs(1-4*val)),1);
2
votes

Without knowing the range of values, there isn't much you can do to come up with a meaningful function mapping an arbitrary range of positive integers to a heat-map type range of colors.

I think you're going to have to run through your data at least once to get the min/max or know them ahead of time. Once you have that you can normalize appropriately and use any number of color schemes. The simplest solution would be to specify something like "hue" and convert from HSV to RGB.

2
votes

Continuing from Ian Boyd's excellent answer, I needed a distinguishable set of colours to build a heatmap. The trick was to find a way to differentiate close colours and I found a solution by converting to HSV and varying the V according to the value, with a little emphasis in the middle of the colour range to bring out the yellows and oranges.

Here's the code:

Imports System.Drawing
Imports RGBHSV

Module HeatToColour_

    ' Thanks to Ian Boyd's excellent post here:
    ' http://stackoverflow.com/questions/2374959/algorithm-to-convert-any-positive-integer-to-an-rgb-value

    Private Const MinVisibleWaveLength As Double = 450.0
    Private Const MaxVisibleWaveLength As Double = 700.0
    Private Const Gamma As Double = 0.8
    Private Const IntensityMax As Integer = 255

    Function HeatToColour(ByVal value As Double, ByVal MinValue As Double, ByVal MaxValues As Double) As System.Drawing.Color

        Dim wavelength As Double
        Dim Red As Double
        Dim Green As Double
        Dim Blue As Double
        Dim Factor As Double
        Dim scaled As Double

        scaled = (value - MinValue) / (MaxValues - MinValue)

        wavelength = scaled * (MaxVisibleWaveLength - MinVisibleWaveLength) + MinVisibleWaveLength

        Select Case Math.Floor(wavelength)

            Case 380 To 439
                Red = -(wavelength - 440) / (440 - 380)
                Green = 0.0
                Blue = 1.0

            Case 440 To 489
                Red = 0.0
                Green = (wavelength - 440) / (490 - 440)
                Blue = 1.0

            Case 490 To 509
                Red = 0.0
                Green = 1.0
                Blue = -(wavelength - 510) / (510 - 490)

            Case 510 To 579
                Red = (wavelength - 510) / (580 - 510)
                Green = 1.0
                Blue = 0.0

            Case 580 To 644
                Red = 1.0
                Green = -(wavelength - 645) / (645 - 580)
                Blue = 0.0

            Case 645 To 780
                Red = 1.0
                Green = 0.0
                Blue = 0.0

            Case Else
                Red = 0.0
                Green = 0.0
                Blue = 0.0

        End Select

        ' Let the intensity fall off near the vision limits
        Select Case Math.Floor(wavelength)
            Case 380 To 419
                Factor = 0.3 + 0.7 * (wavelength - 380) / (420 - 380)
            Case 420 To 700
                Factor = 1.0
            Case 701 To 780
                Factor = 0.3 + 0.7 * (780 - wavelength) / (780 - 700)
            Case Else
                Factor = 0.0
        End Select

        Dim R As Integer = Adjust(Red, Factor)
        Dim G As Integer = Adjust(Green, Factor)
        Dim B As Integer = Adjust(Blue, Factor)

        Dim result As Color = System.Drawing.Color.FromArgb(255, R, G, B)
        Dim resulthsv As New HSV
        resulthsv = ColorToHSV(result)
        resulthsv.Value = 0.7 + 0.1 * scaled + 0.2 * Math.Sin(scaled * Math.PI)

        result = HSVToColour(resulthsv)

        Return result

    End Function
    Private Function Adjust(ByVal Colour As Double, ByVal Factor As Double) As Integer
        If Colour = 0 Then
            Return 0
        Else
            Return Math.Round(IntensityMax * Math.Pow(Colour * Factor, Gamma))
        End If
    End Function

End Module

Imports System.Drawing
Public Module RGBHSV

    Public Class HSV
        Sub New()
            Hue = 0
            Saturation = 0
            Value = 0
        End Sub
        Public Sub New(ByVal H As Double, ByVal S As Double, ByVal V As Double)
            Hue = H
            Saturation = S
            Value = V
        End Sub
        Public Hue As Double
        Public Saturation As Double
        Public Value As Double
    End Class

    Public Function ColorToHSV(ByVal color As Color) As HSV
        Dim max As Integer = Math.Max(color.R, Math.Max(color.G, color.B))
        Dim min As Integer = Math.Min(color.R, Math.Min(color.G, color.B))
        Dim result As New HSV
        With result
            .Hue = color.GetHue()
            .Saturation = If((max = 0), 0, 1.0 - (1.0 * min / max))
            .Value = max / 255.0
        End With
        Return result
    End Function

    Public Function HSVToColour(ByVal hsv As HSV) As Color
        Dim hi As Integer
        Dim f As Double

        With hsv
            hi = Convert.ToInt32(Math.Floor(.Hue / 60)) Mod 6
            f = .Hue / 60 - Math.Floor(.Hue / 60)
            .Value = .Value * 255
            Dim v As Integer = Convert.ToInt32(.Value)
            Dim p As Integer = Convert.ToInt32(.Value * (1 - .Saturation))
            Dim q As Integer = Convert.ToInt32(.Value * (1 - f * .Saturation))
            Dim t As Integer = Convert.ToInt32(.Value * (1 - (1 - f) * .Saturation))

            If hi = 0 Then
                Return Color.FromArgb(255, v, t, p)
            ElseIf hi = 1 Then
                Return Color.FromArgb(255, q, v, p)
            ElseIf hi = 2 Then
                Return Color.FromArgb(255, p, v, t)
            ElseIf hi = 3 Then
                Return Color.FromArgb(255, p, q, v)
            ElseIf hi = 4 Then
                Return Color.FromArgb(255, t, p, v)
            Else
                Return Color.FromArgb(255, v, p, q)
            End If
        End With
    End Function

End Module

and a resulting heatmap, showing GDP per capita for the countries in the EEC: GDP/Capita, EEC

2
votes

This answer is probably a little late to the party. I'm displaying some environmental data, and need to colour the resulting bars from green to red relative to the max and min of the data set (or which ever values are passed as max and min to the function. Anyway, the below accomplishes that. Can be changed for blue to red fairly easily enough, I would think.

// scale colour temp relatively

function getColourTemp(maxVal, minVal, actual) {
    var midVal = (maxVal - minVal)/2;
    var intR;
    var intG;
    var intB = Math.round(0);

    if (actual >= midVal){
         intR = 255;
         intG = Math.round(255 * ((maxVal - actual) / (maxVal - midVal)));
    }
    else{
        intG = 255;
        intR = Math.round(255 * ((actual - minVal) / (midVal - minVal)));
    }

    return to_rgb(intR, intG, intB);
}
1
votes

man, you could probably use YUV color space and only for demonstration purposes convert it to RGB.

0
votes

A little late, but I was trying to do the same and found I can modify HSV to RGB to get a similar result. It's similar to the wavelength approach but by passes the need to convert to wavelength first. Just substitute H with your value (assuming a value between 0 and 1), and fix S and V to 1. I found the HSVtoRGB example here to be very helpful:

http://www.cs.rit.edu/~ncs/color/t_convert.html

However, I had to change the lines

h /= 60;
i = floor ( h );

to

h *= 5;
i = (int) h;

to get an output that goes through the whole spectrum.

Additional resource: http://www.easyrgb.com/index.php?X=MATH&H=21#text21

-1
votes

Simple algorithm

// given a max and min value
float red,green,blue;
float range=max-min;
float mid=(max+min)/2.0;

//foreach value
    red = (value[ii]-mid)/range;            
    if (red>0.0) {
        //above mid = red-green
        blue=0.0;
        green = 1.0-red;
    } else {
        // lower half green-blue
        blue=-red;
        green = 1.0-blue;
        red=0.0;
    }

}

More complex:
If your range is a few million but most are around 0 you want to scale it so that 'red' in the above example is the log of the distance from the midpoint. The cod eis a little more complicated if the values are +/-

// assume equally distributed around 0 so max is the largest (or most negative number)
float range = log(fabs(max));
float mid=0.0

// foreach value
if (value[ii] > 0.0 ) {
    // above mid = red-green
    red = log(value[ii])/range;
    blue=0.0;
    green = 1.0 - red;
} else {
    // below mid = green-blue
    blue=-log(value[ii])/range;
    green = 1.0 - blue;
   red = 0.0;
}

note - I haven't tested this code, just spinning ideas!