Code - heatmaps and color gradients
About
A heatmap is a colored representation of data where different values are represented as different colors. In some case a heat-map can be in monochrome, with values ranging from black to white - and is very easy to code. More commonly however, a special gradient of colors is used and these colors are typically as follows (from lowest/coldest to highest/warmest):
- (1) black, (2) blue, (3) cyan, (4) green, (5) yellow, (6) red, (7) white
In many cases the black and white are dropped off, so it's just 5 color points instead of 7. On this page I'll show you can program a heat-map - or in fact any color gradient you want - in a few lines of C++ code.
A minimalist heatmap function
In the following code we'll assume that you have already "normalized" your range of values such that your minimum value maps to 0 and maximum value maps to 1, thus giving you a final "value" between 0 and 1. If you're using a monochrome heatmap, you can probably code it in a single line of code.
float value = (valueOfYourDataPoint - minDataVal / (maxDataValue - minDataVal)); // First you should normalize to a number between 0 and 1.
Color colorOfYourDataPoint = Color(value*255, value*255, value*255); // Single line of code (the one above we had to do anyway).
To program a colored gradient requires more than one line of code, but the advantage is that it's easier to identify values when spread over different colors, rather than try to match to different "grey values". If you just want your gradient to have any two arbitrary colors at the start and end - lets say blue and red - the code for that is quite short:
bool getValueBetweenTwoFixedColors(float value, int &red, int &green, int &blue)
{
int aR = 0; int aG = 0; int aB=255; // RGB for our 1st color (blue in this case).
int bR = 255; int bG = 0; int bB=0; // RGB for our 2nd color (red in this case).
red = (float)(bR - aR) * value + aR; // Evaluated as -255*value + 255.
green = (float)(bG - aG) * value + aG; // Evaluates as 0.
blue = (float)(bB - aB) * value + aB; // Evaluates as 255*value + 0.
}
The gradient for this is shown in panel D of the figure... and you'll notice that it naturally passes through a "fairly purple" value in the middle (r,g,b = 127, 0, 127). For a "real" heat-map however, we probably want several distinct colors spaced out over our gradient. Following is a function which includes four evenly spaced values: (1) blue, (2) green, (3) yellow, (4) red, but could easily be modified to include 5 or 7 values.
NOTE: Some GUI libraries will expect their red, green, blue (rgb) color values as integers between 0 and 255 (as per above), but many others instead want floating point values between 0 and 1. In all my following examples I assume output is expected as floats between between 0 and 1. Either way it's easy to change by replacing all the "1" values by "255", or just multiplying the return values by 255 at the end.
bool getHeatMapColor(float value, float *red, float *green, float *blue)
{
const int NUM_COLORS = 4;
static float color[NUM_COLORS][3] = { {0,0,1}, {0,1,0}, {1,1,0}, {1,0,0} };
// A static array of 4 colors: (blue, green, yellow, red) using {r,g,b} for each.
int idx1; // |-- Our desired color will be between these two indexes in "color".
int idx2; // |
float fractBetween = 0; // Fraction between "idx1" and "idx2" where our value is.
if(value <= 0) { idx1 = idx2 = 0; } // accounts for an input <=0
else if(value >= 1) { idx1 = idx2 = NUM_COLORS-1; } // accounts for an input >=0
else
{
value = value * (NUM_COLORS-1); // Will multiply value by 3.
idx1 = floor(value); // Our desired color will be after this index.
idx2 = idx1+1; // ... and before this index (inclusive).
fractBetween = value - float(idx1); // Distance between the two indexes (0-1).
}
*red = (color[idx2][0] - color[idx1][0])*fractBetween + color[idx1][0];
*green = (color[idx2][1] - color[idx1][1])*fractBetween + color[idx1][1];
*blue = (color[idx2][2] - color[idx1][2])*fractBetween + color[idx1][2];
}
And to test this code:
void main() // to test the code
{
float r,g,b;
for(float value=-0.1f; value<1.1f; value+=0.1f)
{
getHeatMapColor(value, &r, &g, &b);
cout << value << "={" << r << "," << g << "," << b << "} " << endl;
}
}
This "minimal function" requires no library includes (except "#include <iostream>" for the cout statements) and the use of the "static" statement means that the array of colors is produced only one. Its limitation however, is that we haven't allowed it to be dynamically changed, and that the spacing between all our colors is constant.
A color gradient class
In this example we create use a more object oriented approach and create a class called "ColorGradient" filled with a a vector of "ColorPoint" values, where each color point has a "value" representing its position within the gradient (plus of course r,g,b values too). This is more along the lines of the gradient modifies you see in painting programs (eg: Adobe Photoshop). The "createDefaultHeatMapGradient()" method populates a nice 5-color heatmap, but you can easily change it to generate any color gradients you like.
class ColorGradient
{
private:
struct ColorPoint // Internal class used to store colors at different points in the gradient.
{
float r,g,b; // Red, green and blue values of our color.
float val; // Position of our color along the gradient (between 0 and 1).
ColorPoint(float red, float green, float blue, float value)
: r(red), g(green), b(blue), val(value) {}
};
vector<ColorPoint> color; // An array of color points in ascending value.
public:
//-- Default constructor:
ColorGradient() { createDefaultHeatMapGradient(); }
//-- Inserts a new color point into its correct position:
void addColorPoint(float red, float green, float blue, float value)
{
for(int i=0; i<color.size(); i++) {
if(value < color[i].val) {
color.insert(color.begin()+i, ColorPoint(red,green,blue, value));
return; }}
color.push_back(ColorPoint(red,green,blue, value));
}
//-- Inserts a new color point into its correct position:
void clearGradient() { color.clear(); }
//-- Places a 5 color heapmap gradient into the "color" vector:
void createDefaultHeatMapGradient()
{
color.clear();
color.push_back(ColorPoint(0, 0, 1, 0.0f)); // Blue.
color.push_back(ColorPoint(0, 1, 1, 0.25f)); // Cyan.
color.push_back(ColorPoint(0, 1, 0, 0.5f)); // Green.
color.push_back(ColorPoint(1, 1, 0, 0.75f)); // Yellow.
color.push_back(ColorPoint(1, 0, 0, 1.0f)); // Red.
}
//-- Inputs a (value) between 0 and 1 and outputs the (red), (green) and (blue)
//-- values representing that position in the gradient.
void getColorAtValue(const float value, float &red, float &green, float &blue)
{
if(color.size()==0)
return;
for(int i=0; i<color.size(); i++)
{
ColorPoint &currC = color[i];
if(value < currC.val)
{
ColorPoint &prevC = color[ max(0,i-1) ];
float valueDiff = (prevC.val - currC.val);
float fractBetween = (valueDiff==0) ? 0 : (value - currC.val) / valueDiff;
red = (prevC.r - currC.r)*fractBetween + currC.r;
green = (prevC.g - currC.g)*fractBetween + currC.g;
blue = (prevC.b - currC.b)*fractBetween + currC.b;
return;
}
}
red = color.back().r;
green = color.back().g;
blue = color.back().b;
return;
}
};
To use this from main code you would then type:
ColorGradient heatMapGradient; // Used to create a nice array of different colors.
heatMapGradient.createDefaultHeatMapGradient();
float r,g,b;
heatMapGradient.getColorAtValue(yourGradientValue, r,g,b);
I've also added a "addColorPoint" and "clearGradient" functions so you could the gradient, but possibly you'd add extra function to let users access and delete existing ColorPoint values. As you can see, the object oriented approach has almost tripled the lines of code from the minimal (function) example, but it does have a bit more versatility and would allow you to create multiple gradient instances at once. Depends what you need it for. Hope this code helps!
Links:
Code license | For all of the code on my site... if there are specific instruction or licence comments please leave them in. If you copy my code with minimum modifications to another webpage, or into any code other people will see I would love an acknowledgment to my site.... otherwise, the license for this code is more-or-less WTFPL (do what you want)! If only copying <20 lines, then don't bother. That said - if you'd like to add a web-link to my site www.andrewnoske.com or (better yet) the specific page with code, that's a really sweet gestures! Links to the page may be useful to yourself or your users and helps increase traffic to my site. Hope my code is useful! :)
|