Creating Heat Maps with .NET 2.0 (C#)

Heat Map Example

It seems now a days that heat maps are everywhere, from visitor mouse tracking on your website, to geographic density mapping. Heat maps are earning this well deserved deployment primarily do to the fact that they are a powerful visualization tool for three dimensional data (two for plotting, one for density) and nearly everyone has seen them depicted in movies. In this post I would like to show you how easy it is to create your very own heat map using the power of the GDI+ in the .NET framework.

I'd first like to point out that for this first post we'll only be discussing plotting points in screen space, that being the X and Y coordinates of your screen. To do something like geographic plotting you would need an additional step to translate the longitude and latitude coordinates to screen coordinates. Ok, now lets move onto the fun stuff.

Creating the Intensity Mask

Creating a heat map in its simplest form is a two step process. The first step is to create an intensity mask. An intensity mask is basically a grayscale (256 shades of gray) image that we use to mark the intensity of every pixel in our final image. Each shade in the white to black spectrum will map directly to an actual color in our gradient palette. Take a look at the following image to see what I mean.

Gradient Palette Sample

As you can see from the image above the darker the pixel in the intensity mask the hotter the color is that gets mapped to it. Ok, so now that you know what an intensity mask is, let me show you how to create one. The first thing you need to do is create a new Windows Application project in Visual Studio. On the default form, create a new picture box and a button.

Heat Map Sample Form

Before we put any code against this this form we first need to define a struct which will hold our most basic building block for a heat map; the heat point. A heat point is very similar to a regular point in the .NET GDI accept for the fact that it will contain an additional property for storing the density. It can be argued that this additional property is not necessary since heat points are additive by nature, meaning that when stacked on top of each other the area they occupy will already appear denser. However, the reason I believe this property is necessary is for performance reasons. It's quite a bit less overhead to generate a "denser" heat point than it is to generate a bunch of less dense heat points, plus this means you won't have to store as many points since you'll be feeding the heat mapping class data that is pre-aggregated.

 

public struct HeatPoint
{
    public int X;
    public int Y;
    public byte Intensity;
    public HeatPoint(int iX, int iY, byte bIntensity)
    {
        X = iX;
        Y = iY;
        Intensity = bIntensity;
    }
}
 

Now that we have a struct to hold heat point data. Lets create a generic list to hold a set of heat points which will use to plot on our surface as our intensity mask. Add this private variable to the form class.

 

private List<HeatPoint> HeatPoints = new List<HeatPoint>();
 

Next we'll create a method to generate the actual intensity mask. This method will accept a memory bitmap object and a generic list of heat points and return a new memory bitmap. Here is what it looks like.

 

private Bitmap CreateIntensityMask(Bitmap bSurface, List<HeatPoint> aHeatPoints)
{
    // Create new graphics surface from memory bitmap
    Graphics DrawSurface = Graphics.FromImage(bSurface);
    // Set background color to white so that pixels can be correctly colorized
    DrawSurface.Clear(Color.White);

    // Traverse heat point data and draw masks for each heat point
    foreach (HeatPoint DataPoint in aHeatPoints)
    {
        // Render current heat point on draw surface
        DrawHeatPoint(DrawSurface, DataPoint, 25);
    }

    return bSurface;
}
 

I'd like you to focus your attentions on the method DrawHeatPoint. This method is used to draw an actual radial gradient "spot" on the drawing surface. It's perhaps the most important method in this entire project as it handles drawing spots of varying size and density. Please read through the comments to get an idea for how this crucial method works.

 

private void DrawHeatPoint(Graphics Canvas, HeatPoint HeatPoint, int Radius)
{
    // Create points generic list of points to hold circumference points
    List<Point> CircumferencePointsList = new List<Point>();

    // Create an empty point to predefine the point struct used in the circumference loop
    Point CircumferencePoint;

    // Create an empty array that will be populated with points from the generic list
    Point[] CircumferencePointsArray;

    // Calculate ratio to scale byte intensity range from 0-255 to 0-1
    float fRatio = 1F / Byte.MaxValue;
    // Precalulate half of byte max value
    byte bHalf = Byte.MaxValue / 2;
    // Flip intensity on it's center value from low-high to high-low
    int iIntensity = (byte)(HeatPoint.Intensity - ((HeatPoint.Intensity - bHalf) * 2));
    // Store scaled and flipped intensity value for use with gradient center location
    float fIntensity = iIntensity * fRatio;

    // Loop through all angles of a circle
    // Define loop variable as a double to prevent casting in each iteration
    // Iterate through loop on 10 degree deltas, this can change to improve performance
    for (double i = 0; i <= 360; i += 10)
    {
        // Replace last iteration point with new empty point struct
        CircumferencePoint = new Point();

        // Plot new point on the circumference of a circle of the defined radius
        // Using the point coordinates, radius, and angle
        // Calculate the position of this iterations point on the circle
        CircumferencePoint.X = Convert.ToInt32(HeatPoint.X + Radius * Math.Cos(ConvertDegreesToRadians(i)));
        CircumferencePoint.Y = Convert.ToInt32(HeatPoint.Y + Radius * Math.Sin(ConvertDegreesToRadians(i)));

        // Add newly plotted circumference point to generic point list
        CircumferencePointsList.Add(CircumferencePoint);
    }

    // Populate empty points system array from generic points array list
    // Do this to satisfy the datatype of the PathGradientBrush and FillPolygon methods
    CircumferencePointsArray = CircumferencePointsList.ToArray();

    // Create new PathGradientBrush to create a radial gradient using the circumference points
    PathGradientBrush GradientShaper = new PathGradientBrush(CircumferencePointsArray);
    // Create new color blend to tell the PathGradientBrush what colors to use and where to put them
    ColorBlend GradientSpecifications = new ColorBlend(3);

    // Define positions of gradient colors, use intesity to adjust the middle color to
    // show more mask or less mask
    GradientSpecifications.Positions = new float[3] { 0, fIntensity, 1 };
    // Define gradient colors and their alpha values, adjust alpha of gradient colors to match intensity
    GradientSpecifications.Colors = new Color[3]
    {
        Color.FromArgb(0, Color.White),
        Color.FromArgb(HeatPoint.Intensity, Color.Black),
        Color.FromArgb(HeatPoint.Intensity, Color.Black)
    };

    // Pass off color blend to PathGradientBrush to instruct it how to generate the gradient
    GradientShaper.InterpolationColors = GradientSpecifications;
    // Draw polygon (circle) using our point array and gradient brush
    Canvas.FillPolygon(GradientShaper, CircumferencePointsArray);
}
 

One thing I would like to talk about in this method is the FOR loop. The reason for it's existence is to allow the GDI to produce a radial gradient, because unfortunately there is no easy way to produce a variable radial gradient using the GDI in .NET 2.0. The way my method works is by creating a new polygon object and adding points to it that fall along the circumference of heat point. The circumference is calculated using the radius passed to the DrawHeatPoint method. The rest of the code in the method is used to adjust the ratio of white and black in the gradient brush that we use to fill the polygon generated in the FOR loop.

Radial

A much simpler method of drawing a heat point would be to simply have a semi-transparent png image the mimics the black radial gradient that you see above. However, the problem with doing it like that is that you lose the ability to dynamically adjust the size and density of the heat point without a lose in quality and flexibility.

Also, don't forget this little guy because the DrawHeatPoint method is dependent on him.

 
private double ConvertDegreesToRadians(double degrees)
{
    double radians = (Math.PI / 180) * degrees;
    return (radians);
}
 

Ok, so the last thing we need to do to create our intensity mask is to create a button click event handler and sprinkle enough code in it to force it to generate an intensity mask. Here is what the handler should look like.

 

private void button1_Click(object sender, EventArgs e)
{
    // Create new memory bitmap the same size as the picture box
    Bitmap bMap = new Bitmap(pictureBox1.Width, pictureBox1.Height);

    // Initialize random number generator
    Random rRand = new Random();

    // Loop variables
    int iX;
    int iY;
    byte iIntense;

    // Lets loop 500 times and create a random point each iteration
    for (int i = 0; i < 500; i++)
    {
        // Pick random locations and intensity
        iX = rRand.Next(0, 200);
        iY = rRand.Next(0, 200);
        iIntense = (byte)rRand.Next(0, 120);

        // Add heat point to heat points list
        HeatPoints.Add(new HeatPoint(iX, iY, iIntense));
    }

    // Call CreateIntensityMask, give it the memory bitmap, and use it's output to set the picture box image
    pictureBox1.Image = CreateIntensityMask(bMap, HeatPoints);
}
 

As you can see by the comments we are generating 500 random heat points as test data so that you will actually have something to look at when the heat map is generated. This is what your code file should look like for your form.

 

using System;
using System.Collections.Generic;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Windows.Forms;

public partial class Form1 : Form
{
    private List<HeatPoint> HeatPoints = new List<HeatPoint>();

    public Form1()
    {
        InitializeComponent();
    }

    private void button1_Click(object sender, EventArgs e)
    {
        // Create new memory bitmap the same size as the picture box
        Bitmap bMap = new Bitmap(pictureBox1.Width, pictureBox1.Height);

        // Initialize random number generator
        Random rRand = new Random();

        // Loop variables
        int iX;
        int iY;
        byte iIntense;

        // Lets loop 500 times and create a random point each iteration
        for (int i = 0; i < 500; i++)
        {
            // Pick random locations and intensity
            iX = rRand.Next(0, 200);
            iY = rRand.Next(0, 200);
            iIntense = (byte)rRand.Next(0, 120);
            // Add heat point to heat points list
            HeatPoints.Add(new HeatPoint(iX, iY, iIntense));
        }

        // Call CreateIntensityMask, give it the memory bitmap, and use it's output to set the picture box image
        pictureBox1.Image = CreateIntensityMask(bMap, HeatPoints);
    }

    private Bitmap CreateIntensityMask(Bitmap bSurface, List<HeatPoint> aHeatPoints)
    {
        // Create new graphics surface from memory bitmap
        Graphics DrawSurface = Graphics.FromImage(bSurface);

        // Set background color to white so that pixels can be correctly colorized
        DrawSurface.Clear(Color.White);

        // Traverse heat point data and draw masks for each heat point
        foreach (HeatPoint DataPoint in aHeatPoints)
        {
            // Render current heat point on draw surface
            DrawHeatPoint(DrawSurface, DataPoint, 15);
        }

        return bSurface;
    }

    private void DrawHeatPoint(Graphics Canvas, HeatPoint HeatPoint, int Radius)
    {
        // Create points generic list of points to hold circumference points
        List<Point> CircumferencePointsList = new List<Point>();

        // Create an empty point to predefine the point struct used in the circumference loop
        Point CircumferencePoint;

        // Create an empty array that will be populated with points from the generic list
        Point[] CircumferencePointsArray;

        // Calculate ratio to scale byte intensity range from 0-255 to 0-1
        float fRatio = 1F / Byte.MaxValue;
        // Precalulate half of byte max value
        byte bHalf = Byte.MaxValue / 2;
        // Flip intensity on it's center value from low-high to high-low
        int iIntensity = (byte)(HeatPoint.Intensity - ((HeatPoint.Intensity - bHalf) * 2));
        // Store scaled and flipped intensity value for use with gradient center location
        float fIntensity = iIntensity * fRatio;

        // Loop through all angles of a circle
        // Define loop variable as a double to prevent casting in each iteration
        // Iterate through loop on 10 degree deltas, this can change to improve performance
        for (double i = 0; i <= 360; i += 10)
        {
            // Replace last iteration point with new empty point struct
            CircumferencePoint = new Point();
            // Plot new point on the circumference of a circle of the defined radius
            // Using the point coordinates, radius, and angle
            // Calculate the position of this iterations point on the circle
            CircumferencePoint.X = Convert.ToInt32(HeatPoint.X + Radius * Math.Cos(ConvertDegreesToRadians(i)));
            CircumferencePoint.Y = Convert.ToInt32(HeatPoint.Y + Radius * Math.Sin(ConvertDegreesToRadians(i)));
            // Add newly plotted circumference point to generic point list
            CircumferencePointsList.Add(CircumferencePoint);
        }

        // Populate empty points system array from generic points array list
        // Do this to satisfy the datatype of the PathGradientBrush and FillPolygon methods
        CircumferencePointsArray = CircumferencePointsList.ToArray();

        // Create new PathGradientBrush to create a radial gradient using the circumference points
        PathGradientBrush GradientShaper = new PathGradientBrush(CircumferencePointsArray);
        // Create new color blend to tell the PathGradientBrush what colors to use and where to put them
        ColorBlend GradientSpecifications = new ColorBlend(3);

        // Define positions of gradient colors, use intesity to adjust the middle color to
        // show more mask or less mask
        GradientSpecifications.Positions = new float[3] { 0, fIntensity, 1 };
        // Define gradient colors and their alpha values, adjust alpha of gradient colors to match intensity
        GradientSpecifications.Colors = new Color[3]
        {
            Color.FromArgb(0, Color.White),
            Color.FromArgb(HeatPoint.Intensity, Color.Black),
            Color.FromArgb(HeatPoint.Intensity, Color.Black)
        };

        // Pass off color blend to PathGradientBrush to instruct it how to generate the gradient
        GradientShaper.InterpolationColors = GradientSpecifications;
        // Draw polygon (circle) using our point array and gradient brush
        Canvas.FillPolygon(GradientShaper, CircumferencePointsArray);
    }

    private double ConvertDegreesToRadians(double degrees)
    {
        double radians = (Math.PI / 180) * degrees;
        return (radians);
    }
}

public struct HeatPoint
{
    public int X;
    public int Y;
    public byte Intensity;
    public HeatPoint(int iX, int iY, byte bIntensity)
    {
        X = iX;
        Y = iY;
        Intensity = bIntensity;
    }
}
 

Ok, now fire this thing up and click the button on your form. You should get an output that looks similar to this.

Sample Intensity Mask

You've done it, you've created an intensity mask which is the biggest hurdle in generating a heat map. In this sample all we did was generate a bunch of nonsense test data. But at this point you should be able to feed it your own meaningful data and have it visualize the numbers. You didn't come here to end up with a bunch of black blobs though. You came for a majestically full color visualization masterpiece, and believe it or not, that's the easy part!

Colorizing the Intensity Mask

Ok, so this step is cake, because all we have to do now is create a color map that tells the GDI rendering method how it should draw the colors. What we're actually doing here is creating a table that specifies a new color for each shade of gray (all 256) of them. The easiest way to do this is to create a palette image that's 256 pixels wide by 1 pixel high. That means that we can directly map each pixel in the palette image to a shade of gray. I'll let you have my palette image since it can be sort of a pain to create one that looks nice.

palette

So this brings me to the last two methods we'll need to generate our final image. One to do the colorization, and one to create a color remap table.

 

public static Bitmap Colorize(Bitmap Mask, byte Alpha)
{
    // Create new bitmap to act as a work surface for the colorization process
    Bitmap Output = new Bitmap(Mask.Width, Mask.Height, PixelFormat.Format32bppArgb);

    // Create a graphics object from our memory bitmap so we can draw on it and clear it's drawing surface
    Graphics Surface = Graphics.FromImage(Output);
    Surface.Clear(Color.Transparent);

    // Build an array of color mappings to remap our greyscale mask to full color
    // Accept an alpha byte to specify the transparancy of the output image
    ColorMap[] Colors = CreatePaletteIndex(Alpha);

    // Create new image attributes class to handle the color remappings
    // Inject our color map array to instruct the image attributes class how to do the colorization
    ImageAttributes Remapper = new ImageAttributes();
    Remapper.SetRemapTable(Colors);

    // Draw our mask onto our memory bitmap work surface using the new color mapping scheme
    Surface.DrawImage(Mask, new Rectangle(0, 0, Mask.Width, Mask.Height), 0, 0, Mask.Width, Mask.Height, GraphicsUnit.Pixel, Remapper);

    // Send back newly colorized memory bitmap
    return Output;
}
private static ColorMap[] CreatePaletteIndex(byte Alpha) { ColorMap[] OutputMap = new ColorMap[256]; // Change this path to wherever you saved the palette image. Bitmap Palette = (Bitmap)Bitmap.FromFile(@"C:\Users\Dylan\Documents\Visual Studio 2005\Projects\HeatMapTest\palette.bmp"); // Loop through each pixel and create a new color mapping for (int X = 0; X <= 255; X++) { OutputMap[X] = new ColorMap(); OutputMap[X].OldColor = Color.FromArgb(X, X, X); OutputMap[X].NewColor = Color.FromArgb(Alpha, Palette.GetPixel(X, 0)); } return OutputMap; }
 

Now with just a slight modification to the button click event handler we'll include the ability to colorize the image and we should be golden. Here is what the new button click event handler will look like.

 

private void button1_Click(object sender, EventArgs e)
{
    // Create new memory bitmap the same size as the picture box
    Bitmap bMap = new Bitmap(pictureBox1.Width, pictureBox1.Height);

    // Initialize random number generator
    Random rRand = new Random();

    // Loop variables
    int iX;
    int iY;
    byte iIntense;

    // Lets loop 500 times and create a random point each iteration
    for (int i = 0; i < 500; i++)
    {
        // Pick random locations and intensity
        iX = rRand.Next(0, 200);
        iY = rRand.Next(0, 200);
        iIntense = (byte)rRand.Next(0, 120);

        // Add heat point to heat points list
        HeatPoints.Add(new HeatPoint(iX, iY, iIntense));
    }

    // Call CreateIntensityMask, give it the memory bitmap, and store the result back in the memory bitmap
    bMap = CreateIntensityMask(bMap, HeatPoints);

    // Colorize the memory bitmap and assign it as the picture boxes image
    pictureBox1.Image = Colorize(bMap, 255);
}
 

Go ahead and run the project and click the button on the form and you should get a sexy little heat map.

Final Heat Map

Congratulations on generating your first heat map with .NET 2.0! Please feel free to post any comments about my code or my writing below. Thanks, and finally here is what the whole code file looks like when completed.

 

using System;
using System.Collections.Generic;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Drawing.Imaging;
using System.Windows.Forms;

public partial class Form1 : Form
{
    private List<HeatPoint> HeatPoints = new List<HeatPoint>();

    public Form1()
    {
        InitializeComponent();
    }

    private void button1_Click(object sender, EventArgs e)
    {
        // Create new memory bitmap the same size as the picture box
        Bitmap bMap = new Bitmap(pictureBox1.Width, pictureBox1.Height);

        // Initialize random number generator
        Random rRand = new Random();

        // Loop variables
        int iX;
        int iY;
        byte iIntense;

        // Lets loop 500 times and create a random point each iteration
        for (int i = 0; i < 500; i++)
        {
            // Pick random locations and intensity
            iX = rRand.Next(0, 200);
            iY = rRand.Next(0, 200);
            iIntense = (byte)rRand.Next(0, 120);

            // Add heat point to heat points list
            HeatPoints.Add(new HeatPoint(iX, iY, iIntense));
        }

        // Call CreateIntensityMask, give it the memory bitmap, and store the result back in the memory bitmap
        bMap = CreateIntensityMask(bMap, HeatPoints);

        // Colorize the memory bitmap and assign it as the picture boxes image
        pictureBox1.Image = Colorize(bMap, 255);
    }

    private Bitmap CreateIntensityMask(Bitmap bSurface, List<HeatPoint> aHeatPoints)
    {
        // Create new graphics surface from memory bitmap
        Graphics DrawSurface = Graphics.FromImage(bSurface);

        // Set background color to white so that pixels can be correctly colorized
        DrawSurface.Clear(Color.White);

        // Traverse heat point data and draw masks for each heat point
        foreach (HeatPoint DataPoint in aHeatPoints)
        {
            // Render current heat point on draw surface
            DrawHeatPoint(DrawSurface, DataPoint, 15);
        }

        return bSurface;
    }

    private void DrawHeatPoint(Graphics Canvas, HeatPoint HeatPoint, int Radius)
    {
        // Create points generic list of points to hold circumference points
        List<Point> CircumferencePointsList = new List<Point>();

        // Create an empty point to predefine the point struct used in the circumference loop
        Point CircumferencePoint;

        // Create an empty array that will be populated with points from the generic list
        Point[] CircumferencePointsArray;

        // Calculate ratio to scale byte intensity range from 0-255 to 0-1
        float fRatio = 1F / Byte.MaxValue;
        // Precalulate half of byte max value
        byte bHalf = Byte.MaxValue / 2;
        // Flip intensity on it's center value from low-high to high-low
        int iIntensity = (byte)(HeatPoint.Intensity - ((HeatPoint.Intensity - bHalf) * 2));
        // Store scaled and flipped intensity value for use with gradient center location
        float fIntensity = iIntensity * fRatio;

        // Loop through all angles of a circle
        // Define loop variable as a double to prevent casting in each iteration
        // Iterate through loop on 10 degree deltas, this can change to improve performance
        for (double i = 0; i <= 360; i += 10)
        {
            // Replace last iteration point with new empty point struct
            CircumferencePoint = new Point();

            // Plot new point on the circumference of a circle of the defined radius
            // Using the point coordinates, radius, and angle
            // Calculate the position of this iterations point on the circle
            CircumferencePoint.X = Convert.ToInt32(HeatPoint.X + Radius * Math.Cos(ConvertDegreesToRadians(i)));
            CircumferencePoint.Y = Convert.ToInt32(HeatPoint.Y + Radius * Math.Sin(ConvertDegreesToRadians(i)));

            // Add newly plotted circumference point to generic point list
            CircumferencePointsList.Add(CircumferencePoint);
        }

        // Populate empty points system array from generic points array list
        // Do this to satisfy the datatype of the PathGradientBrush and FillPolygon methods
        CircumferencePointsArray = CircumferencePointsList.ToArray();

        // Create new PathGradientBrush to create a radial gradient using the circumference points
        PathGradientBrush GradientShaper = new PathGradientBrush(CircumferencePointsArray);

        // Create new color blend to tell the PathGradientBrush what colors to use and where to put them
        ColorBlend GradientSpecifications = new ColorBlend(3);

        // Define positions of gradient colors, use intesity to adjust the middle color to
        // show more mask or less mask
        GradientSpecifications.Positions = new float[3] { 0, fIntensity, 1 };
        // Define gradient colors and their alpha values, adjust alpha of gradient colors to match intensity
        GradientSpecifications.Colors = new Color[3]
        {
            Color.FromArgb(0, Color.White),
            Color.FromArgb(HeatPoint.Intensity, Color.Black),
            Color.FromArgb(HeatPoint.Intensity, Color.Black)
        };

        // Pass off color blend to PathGradientBrush to instruct it how to generate the gradient
        GradientShaper.InterpolationColors = GradientSpecifications;

        // Draw polygon (circle) using our point array and gradient brush
        Canvas.FillPolygon(GradientShaper, CircumferencePointsArray);
    }

    private double ConvertDegreesToRadians(double degrees)
    {
        double radians = (Math.PI / 180) * degrees;
        return (radians);
    }

    public static Bitmap Colorize(Bitmap Mask, byte Alpha)
    {
        // Create new bitmap to act as a work surface for the colorization process
        Bitmap Output = new Bitmap(Mask.Width, Mask.Height, PixelFormat.Format32bppArgb);

        // Create a graphics object from our memory bitmap so we can draw on it and clear it's drawing surface
        Graphics Surface = Graphics.FromImage(Output);
        Surface.Clear(Color.Transparent);

        // Build an array of color mappings to remap our greyscale mask to full color
        // Accept an alpha byte to specify the transparancy of the output image
        ColorMap[] Colors = CreatePaletteIndex(Alpha);

        // Create new image attributes class to handle the color remappings
        // Inject our color map array to instruct the image attributes class how to do the colorization
        ImageAttributes Remapper = new ImageAttributes();
        Remapper.SetRemapTable(Colors);

        // Draw our mask onto our memory bitmap work surface using the new color mapping scheme
        Surface.DrawImage(Mask, new Rectangle(0, 0, Mask.Width, Mask.Height), 0, 0, Mask.Width, Mask.Height, GraphicsUnit.Pixel, Remapper);

        // Send back newly colorized memory bitmap
        return Output;
    }

    private static ColorMap[] CreatePaletteIndex(byte Alpha)
    {
        ColorMap[] OutputMap = new ColorMap[256];

        // Change this path to wherever you saved the palette image.
        Bitmap Palette = (Bitmap)Bitmap.FromFile(@"C:\Users\Dylan\Documents\Visual Studio 2005\Projects\HeatMapTest\palette.bmp");

        // Loop through each pixel and create a new color mapping
        for (int X = 0; X <= 255; X++)
        {
            OutputMap[X] = new ColorMap();
            OutputMap[X].OldColor = Color.FromArgb(X, X, X);
            OutputMap[X].NewColor = Color.FromArgb(Alpha, Palette.GetPixel(X, 0));
        }

        return OutputMap;
    }
}

public struct HeatPoint
{
    public int X;
    public int Y;
    public byte Intensity;
    public HeatPoint(int iX, int iY, byte bIntensity)
    {
        X = iX;
        Y = iY;
        Intensity = bIntensity;
    }
}

Currently rated 4.7 by 16 people

  • Currently 4.6875/5 Stars.
  • 1
  • 2
  • 3
  • 4
  • 5

Comments

April 8. 2008 16:30

Excellent article Dylan, I'll be definitely using a couple of these concepts on a demographics mapping project I am working on.

A couple of comments on the code...

Are you really gaining anything by adding 360 points to your path? I am having a lot of success using path.AddEllipse(). I would imagine that it is also faster to use this approach.

Also, while I understand why you are using the ColorBlend to allow for variance in the intensity of your points, it should be noted that using the CenterColor and SurroundColor properties of PathGradientBrush can provide a very visually appealing gradient for data which varies slowly - such as demographic information. For things like a click heat map or visitor map (Nice visitor map BTW) I like your approach better.

All in all an excellent resource.

Oh and what are the odds that you're the number one hit for "GDI+ Heat Map" when I'm looking for ideas for one?

- Nathan



Nathaniel Reinhart

April 8. 2008 22:35

Nathan,

Wow, didn't expect to see you around these parts! Nice to hear from you guy.

Thanks for the kind words, I definitely see what you mean about the Path.AddEllipse() method. That would of saved a little time, the first time and every time Smile. I'll refactor and check the results. In my defense I'm only adding 36 points to the path because it's iterating on 10 degree deltas.

As the visitor widget goes, I'm glad you like it. I wanted to present a real world application for this heat mapping stuff (even though there are tons). My next item to tackle is geo data plotting. I can't seem to figure out how to plot the points on a mercator map projection. As you can see by the visitor widget, I have rough approximation working, but I'm looking for higher accuracy. Let me know if you have any input on that.

I'm really glad you like the article, because I'm a programmer and not a writer, so it's good to see that it can be understood by someone in my target audience. Smile

I guess the odds of you finding this site are pretty good being that when I was in your shoes looking for this stuff, I couldn't find any GDI+ stuff. I felt compelled to write this article! Crazy that you found it still though!

Good to hear from you Nathan!

Dylan

April 9. 2008 16:47

//Clear list of heat points; so we can start again!
HeatPoints.Clear();


Can you guess where that goes? Wink

Was interesting article. Well written and thorough.
Came here from dzone.

Will play with this more to peak interest in other such things.

Skylan Hill

April 9. 2008 20:52

I must have read the code to fast, I totally missed that you were incrementing by 10.

As far as plotting to Mercator goes, thats something that I have just recently started to think about. I have run across several very good resources about it - but I have yet to implement them. Mostly because my project really isn't much more then a proof of concept right now and it's dealing with localized data where Mercator doesn't really come into play.

Yes, your article certainly can be understood by someone in your target audience. Keep up the good work!

Nathaniel Reinhart

April 9. 2008 21:58

Skylan,

Nice catch! That little piece of code really increases reusability.

As you probably have guessed, I only ran this baby once ;)

Dylan

June 7. 2008 10:19

Any idea how to make the background of the heat map transparent? I want to draw the heat map image on top of another image but the way this works the background is 100% opaque.

george

July 29. 2008 04:42

Can you please provide a link to download the palette.bmp file that is used in this example?
Regards,

John D

July 29. 2008 05:57

Hi Dynal,

Thanks. The article was really helpful and yes though you mentioned that u r not a writer your explanation was really cool.

Thanks mate
Regards

Ruptapas

July 29. 2008 08:08

John D,

Haven't been around my blog for a while. I'm in the process of redesigning it. However, you can download the palette.bmp file at:

http://dylanvester.com/image.axd?picture=WindowsLiveWriter/CreatingHeatMapswith.NET2.0C_136B6/palette_2.jpg

Dylan

March 11. 2009 03:09

Hi,

this is an excellent article. Can u also elaborate on how we can extend this to show heat map for website pages?

sidd

March 11. 2009 09:12

sidd,

Thank you!

I'm assuming you're trying to accomplish something like click tracking on a website, and then visualizing that data as an overlay to the actual website?

In that case you would need a sprinkle of javascript and ajax to capture the points where the user is clicking and send that data to a database on the server. You would then need an .NET HTTP Handler for returning an semi-transparent overlay image that was generated using the code above and user click data from the database. Then you would just need javascript/CSS to fit the overlay over the webpage.

There are a bunch of software packages that do this exact thing. But, if you wanted to do it, really the most important thing is to get the HTTP Handler working using the code above.

Dylan

April 9. 2009 15:51

First of all I'd like to thank you (Dylan) for a very educating lesson, I've learned a lot GDI stuff I haven't been familiar with before.
As for George's question, I had the need to show the data on top of another image as well; what I ended up doing was to add:

Mask.MakeTransparent(Color.Transparent);

to the Colorize function, that did the trick.

Nir

May 30. 2009 17:43

thank you.

sunny

June 5. 2009 00:05

Hi Dylan:

Thank you very much for the tutorial!

There is one line of code that I don't quite understand:

// Flip intensity on it's center value from low-high to high-low
int iIntensity = (byte)(HeatPoint.Intensity - ((HeatPoint.Intensity - bHalf) * 2));

Could you explain a bit more on this?

Thank you!

Cheng

June 8. 2009 00:39

Cheng,

This line of code is designed to flip the upper bounds and lower bounds of the intensity to accommodate the needs of further calculations. The intensity value coming in is between 0 and 255 (a byte), and the meaning of that range is 0 being the lowest intensity and 255 being the highest intensity. However, for the calculations that happen below that line of code. I need the range of 0 and 255 to mean the opposite. So 0 would become the highest intensity and 255 would become the lowest intensity. To accomplish this I have to flip what ever value that comes in for intensity on the center of the byte range (127). So if a value comes in that's 200, after the line of code in question the value would become 55 because I need 0 to be the most intense number, and before 255 was the most intense so I "flipped" the value on the center of the byte range.

Does that make sense?

Dylan

Dylan

June 9. 2009 12:31

Dylan,

Just wanted to say THANK YOU for such a great article! It was very very very helpful to me! Thanks a lot!

Anton

July 13. 2009 20:18

Love the article! I stumbled upon the technique a while back from some pieced togethor BBS posts. My technique is quite similar to yours, guess we are both victims of a kind of convergent evolution... I was wondering if you had any input on a problem I have come across. The splatter technique (or gaussian splatter as I believe it is formally known) is useful for what I have come to call incidence based data. Where the probablity of finding something in a certain area is proportional to distance from a 2 dimensional point. However, as you probably know the sum of 2 gaussian curves, when they are insufficiently displaced exceeds the height of either of those being summed. So when I'm plotting something like population, 100 people in one place when measured in close proximity to 100 people in another place will equal ~150 people between the points. This is incorrect, the result should be an average. I am under pressure to relate the heat of a point to a meaningful figure, not possible as far as I can see using the current technique. I've been thinking a while and all I can think of doing is creating a mesh from the points and evaluating height to colour. But this, as far as I can see is not decomposable to graphics operations, which leaves me the problem of writting from scratch or cobbling together the code to do this myself, I've only got 4 days to the first deliverable, I'm in a quandry. Maybe if I could do a Dulaney triangulation and then do a gradient on the triangles based on height this would work? Do you have any ideas?

Marcus

September 4. 2009 05:59

I am using above code for generating Heat Map. But I am not getting above picture after pressing the Genearate button , i am only getting dark black picture even i am using palatte.bmp picture.

ruchi

September 15. 2009 08:24

Hi Dylan, Great post.

Got any suggestions when creating this in a C# web form for rendering in a web browser? Then I can use this to overlay properly onto a website Smile

Thanks,

Ben

Ben Sidebottom

September 16. 2009 04:55

Nir, how did you get that to work? I tried placing your code in the Colorize function but it either overrides my loaded image or sits behind it. I cannot get the coloured circles to sit ontop of my image? Any ideas?

Thanks,

Ben

Ben Sidebottom

Add comment

  Country flag

[b][/b] - [i][/i] - [u][/u]- [quote][/quote]


Live preview

February 8. 2010 17:34