• Ryan Elkins
  • Jul 21,2008
  • In: Code

Heatmaps in WPF using C#: Part 1 The Mask

I found myself with a little extra time at work and decided to look into a side project I had been thinking of for a while. It’s not really anything that helps the business but it’s a neat visualization of our data - a heatmap. Heatmaps are one way to visualize 3 dimensional data - in my case, position (two pieces of data) and rate of occurrence. I wasn’t sure if I wanted to do it in GDI+ or WPF but I figured there would be plenty of examples either way. Boy, was I wrong.

I found a pretty good tutorial that most of my approach was based on. It was done in GDI+. (Creating Heat Maps with .NET 2.0 (C#))

Update: There are now two other parts in this series:

Heatmaps in WPF using C#: Part 2 Adding Color

Heatmaps in WPF using C#: Part 3 Mapping

The main problem with this approach is that GDI+ does not have a radial gradient brush. This is an important part of what we are doing as you will see in a bit. The author of the above post gets around it by using math and loops to progressively draw the gradient. It works, but as you can imagine, it starts to cause performance issues as you start to work with more and more points. As I knew I would be needing to work with 5,000-10,000 points at a time, I decided to look and see if WPF had a radial gradient brush.

Fortunately, WPF does have such a brush. Unfortunately, WPF is a real pain in the ass to work with, especially when you are dynamically creating the elements. Any error during creation tends to throw cryptic messages (”An error has been thrown by a target of the invocation” or “Error in the application” GREAT!) that you can’t even break point through.

A quick tip around this - if you put your code in a really generic try catch block (where you are simply catching “Exception”) THEN you can at least step into the exception block. It still won’t let you step into the try block, but at least that gives you a chance to see just what the exception was, if not how exactly it occurred.

OK, back to heatmaps. WPF allows you to paint objects with a radial gradient brush, called, appropriately enough, RadialGradientBrush. Why is this important? Well, to generate our heatmap wee need to plot out all the points. Unless you are working with an astronomical number of data points, the map will come out looking very pixelated unless you use some sort of gradient. The idea is that each point of data represents an occurrence - and as more occurrences stack up on top of each other, that location gets “hotter”.

We will first represent this in black and white and then convert that black and white image to a color image. We do it this way because if you try drawing the colored gradients on top of each other, the colors get muddled and don’t really represent anything. Instead its all done using shades of white and later those shades (once the image is fully drawn) will be replaced by their appropriate colored counterpart.

Left: "Mask" created by plotting points with a RadialGradientBrush. Right: Same mask colored using a custom gradient.

Left: "Mask" created by plotting points with a RadialGradientBrush. Right: Same mask colored using a custom gradient.

Let’s understand the basic concept here. It’s a little different than in the article I linked to above. In my version, each point has the same level of intensity. Intensity really gets defined as points stack up on or near each other. The whiter an element in the image, the “hotter” it becomes. Alot of how this will look will depend on how you set up your RadialGradientBrush. The one used in the example above is set up as follows:

private System.Windows.Media.Brush createRadialBrush()
{
    RadialGradientBrush radialBrush = new RadialGradientBrush();
    radialBrush.GradientOrigin = new Point(0.5, 0.5);
    radialBrush.Center = new Point(0.5, 0.5);
    radialBrush.RadiusX = 1.0;
    radialBrush.RadiusY = 1.0;
    radialBrush.GradientStops.Add(new GradientStop(
        Color.FromArgb(255, 255, 255, 255), 0.0));
    radialBrush.GradientStops.Add(new GradientStop(
        Color.FromArgb(0, 255, 255, 255), 0.5));
    radialBrush.Freeze();
    return radialBrush;
}

Let’s take a look st some of the points here - Gradient Origin is where the Gradient will start. (As a quick side note, you’ll notice almost everything is relative to the range of 0.0 to 1.0 - stay in these bounds) Center is the center of the outermost circle - just set both of these to 0.5 as I have done and that will put it right in the middle. The two radius properties determine how big the brush stroke will be in each direction.

The GradientStops are how you define the actual gradient (this is still the black and white one, remember). It takes two properties, a color and an offset. The offset is where it starts - in this example I have the first one starting at the origin (0.0) and the second starting halfway out (0.5). I noticed that going to the edge (1.0) tends to produce gradients with hard edges at the ends instead of more fuzzy blobs that I was going for. The color in both instances is white, but the first is white with no transparency and the second is fully transparent. This means the gradient starts out opaque white in the center and fades to a more and more transparent version as it get to the edges. This will allow our points to stack up on each other and define hotter areas as they overlap. without using transparency they would simply overwrite each other as they stacked up and not provide a cumulative effect.

OK, so you have your brush - let’s use it to paint our black and white mask:

private RenderTargetBitmap CreateBWBitmapSource()
{
    DrawingVisual drawingVisual = new DrawingVisual();
    DrawingContext drawingContext = drawingVisual.RenderOpen();
    Brush brush = createRadialBrush();
    Brush fillBrush = Brushes.Black;
    drawingContext.DrawRectangle(fillBrush,
        null, new Rect(0.0, 0.0, size.X, size.Y));
    foreach (Point p in heatPoints)
    {
        drawingContext.DrawEllipse(brush, null, p, 25, 25);
    }
    drawingContext.Close();
    RenderTargetBitmap renderer =
        new RenderTargetBitmap(size.X, size.Y, 96, 96,
            PixelFormats.Default);
    renderer.Render(drawingVisual);
    return renderer;
}

We first create a new DrawingVisual - this is what will hold he drawing. From there we create a new Darwingcontext - what will hold our actual drawing. Next we get our radial brush. the next step is important. Fill the entire image with the color black. Otherwise we will just have shades of white. There needs to be a black backdrop for this to work properly.

Next we are drawing our points. This method assumes that we have our data in a list of points called heatPoints. This is simply a List
object populated however you wish. Then close the DrawingContext and render the whole thing as an image by creating a RenderTargetBitmap object (size.X and size.Y are simply predefined to constrain the size of the image) and then passing the DrawingVisual we have created to its Render method.

If you display this RenderTargetBitmap on your window you should be able to get something similar to the black and white image posted above.

In the next day or two I will post the next part - Coloring the Mask.

If you like this blog please take a second and subscribe to my rss feed

Tags: , , , , , , , ,

Comments: 3 comments

All the fields that are marked with REQ must be filled

  • Ryan Abreu
    July 22nd, 2008 at 12:22 am

    I finally had a chance to read through this article and I have to say that I am really glad that someone is finally going through the process of generating a custom heat map in WPF. I know it can be a real pain in the ass sometimes to work with, but once you get used to it, you’ll find that there are elements of WPF that are so simple to implement that you can’t imagine the time it would take to do the same sort of thing in GDI+ or WinForms.

    The only problem that I can see with generating the heat map this way is that you are limited only to the granularity that you can get with the number of shades of gray. It’s a bit harder to implement this with some sort of threshold but it’s well worth it, especially if you are dealing with a large amount of data.

    In short, I can’t wait for Part 2.

  • Jason
    July 22nd, 2008 at 7:34 am

    WPF sounds like such a pain. Is part 3 going to be putting this on a map? :)

  • ryan
    July 22nd, 2008 at 8:11 am

    Part 3 will be start on that topic. It will most likely be converting latitude/longitude to pixel coordinates for Virtual Earth and Google Maps while Part 4 will go into overlaying the image onto a map. I haven’t quite got the whole thing down to import it as an overlay for either of those map systems yet but I understand the basic concept so we’ll see if I get to a part that handles that or not.

    WPF is a pain… I’m slowly getting the hang of it - at least in a very basic sense. My main issue is that the designer is not very intuitive. I keep hearing that I need a separate design tool like Expression Blend, but it bugs me that it just isn’t done well in Visual Studio.

Leave a reply

Name (Req)

E-mail (Req)

URI

Message