Reputation: 131
I have a chart on which I want to plot a heat map; the only data I have is humidity and temperature, which represent a point in the chart.
How do I get the rectangular type of heat map on the chart in c#?
What I want is similar to picture below :
What I really want is a rectangular region in the chart which is plotted in different color based on the point that i get from the list of points and form the colorful section in the chart.
Upvotes: 7
Views: 11975
Reputation: 640
Alternatively, you can manually draw such a heat map to the background of the Chart control.
Here is an example:
First, handle the event PrePaint.
chart.PrePaint += ChartPrePaint;
The method is then executed for deferent elements of the chart, such as Title, Legend, etc. Use the property ChartElement to filter the element of type ChartArea.
private void ChartPrePaint(object sender, ChartPaintEventArgs e)
{
var area = e.ChartElement as ChartArea;
if (area == null)
{
return;
}
}
Next step is to split the chart area into rectangular segments and fill each segment with its own color depending on the settings.
private void ChartPrePaint(object sender, ChartPaintEventArgs e)
{
var chart = sender as Chart;
if (chart == null)
{
return;
}
var width = chart.Width;
var height = chart.Height;
var area = e.ChartElement as ChartArea;
if (area == null)
{
return;
}
/*
* In this example we plot the average temperature of each period of a day (Y-axis) for each month (X-axis).
*/
foreach (var month in Observations)
{
for (var period = 0; period < month.Value.Length; period++)
{
/*
* Firstly, find where each segment begins and ends.
* In this example we split the X-axis proportionally to the number of months and find the edges of each month.
*/
var x1 = e.ChartGraphics.GetPositionFromAxis(area.Name, area.AxisX.AxisName, month.Key);
var x2 = e.ChartGraphics.GetPositionFromAxis(area.Name, area.AxisX.AxisName, month.Key + 1);
/*
* Similarly, split the Y-axis into 4 segments for each period of a day.
*/
var y1 = e.ChartGraphics.GetPositionFromAxis(area.Name, area.AxisY.AxisName, period);
var y2 = e.ChartGraphics.GetPositionFromAxis(area.Name, area.AxisY.AxisName, period + 1);
/*
* Convert the relative coordinates to absolute ones.
*/
var x1abs = (float)(x1 / 100 * width);
var x2abs = (float)((x2 - x1) / 100 * width);
var y1abs = (float)(y2 / 100 * height);
var y2abs = (float)((y1 - y2) / 100 * height);
/*
* Finally, pick the color of the segment.
*/
var temperature = month.Value[period];
var color = GetColor(temperature);
using (var brush = new SolidBrush(color))
{
/*
* Fill the segment with its color.
*/
e.ChartGraphics.Graphics.FillRectangle(brush, x1abs, y1abs, x2abs, y2abs);
}
}
}
}
Here is the full version:
using System.Collections.Generic;
using System.Drawing;
using System.Globalization;
using System.Windows.Forms;
using System.Windows.Forms.DataVisualization.Charting;
namespace WindowsFormsAppHeatMap
{
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
var chart = new Chart { Dock = DockStyle.Fill };
chart.PrePaint += ChartPrePaint;
Controls.Add(chart);
var area = new ChartArea();
chart.ChartAreas.Add(area);
/* Add data to the chart. */
var series = new Series() { ChartType = SeriesChartType.Point };
series.MarkerSize = 0;
foreach (var month in Observations)
{
for (var i = 0; i < month.Value.Length; i++)
{
var index = series.Points.AddXY(month.Key + 0.5, i + 0.5);
series.Points[index].Label = month.Value[i].ToString();
}
}
chart.Series.Add(series);
/* Custom labels for the X-axis. */
area.AxisX.Minimum = 0;
area.AxisX.Maximum = 12;
area.AxisX.Interval = 1;
var ci = CultureInfo.InvariantCulture;
for (var i = 0; i < 12; i++)
{
var month = ci.DateTimeFormat.GetMonthName(i + 1).Substring(0, 3);
area.AxisX.CustomLabels.Add(i, i + 1, month);
}
/* Custom labels for the Y-axis. */
area.AxisY.Minimum = 0;
area.AxisY.Maximum = 4;
area.AxisY.Interval = 1;
area.AxisY.CustomLabels.Add(0, 1, "Morning");
area.AxisY.CustomLabels.Add(1, 2, "Afternoon");
area.AxisY.CustomLabels.Add(2, 3, "Evening");
area.AxisY.CustomLabels.Add(3, 4, "Night");
}
/// <summary>
/// Input data for each month and each period of a day (Morning, Afternoon, Evening, Night).
/// </summary>
private readonly Dictionary<int, int[]> Observations = new Dictionary<int, int[]>
{
{ 0, new [] { -15, -10, -10, -18 } },
{ 1, new [] { -18, -12, -13, -20 } },
{ 2, new [] { -14, -9, -8, -15 } },
{ 3, new [] { -5, -2, -4, -7 } },
{ 4, new [] { -1, 5, 2, -2 } },
{ 5, new [] { 14, 22, 20, 13 } },
{ 6, new [] { 18, 30, 24, 19 } },
{ 7, new [] { 17, 28, 22, 17 } },
{ 8 ,new [] { 10, 13, 11, 8 } },
{ 9, new [] { 5, 10, 6, 3 } },
{ 10, new [] { -2, 3, -1, -5 } },
{ 11, new [] { -7, -5, -8, -10 } }
};
/// <summary>
/// Colors map that states the color for each temperature range.
/// </summary>
private readonly Dictionary<int, Color> Colors = new Dictionary<int, Color>
{
{ -35, Color.FromArgb( 0, 0, 255) },
{ -30, Color.FromArgb(40, 67, 255) },
{ -25, Color.FromArgb(53, 115, 255) },
{ -20, Color.FromArgb(53, 168, 255) },
{ -15, Color.FromArgb(40, 220, 254) },
{ -10, Color.FromArgb(64, 255, 240) },
{ -5, Color.FromArgb(144, 244, 194) },
{ 0, Color.FromArgb(183, 254, 140) },
{ 5, Color.FromArgb(219, 254, 92) },
{ 10, Color.FromArgb(249, 255, 16) },
{ 15, Color.FromArgb(255, 225, 0) },
{ 20, Color.FromArgb(255, 178, 0) },
{ 25, Color.FromArgb(255, 133, 0) },
{ 30, Color.FromArgb(255, 80, 0) },
{ 99, Color.FromArgb(179, 0, 0) }
};
/// <summary>
/// Returns the color for the specified temperature.
/// </summary>
/// <param name="temperature">A temperature.</param>
/// <returns>A color.</returns>
private Color GetColor(int temperature)
{
foreach (var color in Colors)
{
if (temperature < color.Key)
{
return color.Value;
}
}
return Color.Transparent;
}
/// <summary>
/// Draws the colorful segments in the chart area.
/// </summary>
private void ChartPrePaint(object sender, ChartPaintEventArgs e)
{
var chart = sender as Chart;
if (chart == null)
{
return;
}
var width = chart.Width;
var height = chart.Height;
var area = e.ChartElement as ChartArea;
if (area == null)
{
return;
}
/*
* In this example we plot the average temperature of each period of a day (Y-axis) for each month (X-axis).
*/
foreach (var month in Observations)
{
for (var period = 0; period < month.Value.Length; period++)
{
/*
* Firstly, find where each segment begins and ends.
* In this example we split the X-axis proportionally to the number of months and find the edges of each month.
*/
var x1 = e.ChartGraphics.GetPositionFromAxis(area.Name, area.AxisX.AxisName, month.Key);
var x2 = e.ChartGraphics.GetPositionFromAxis(area.Name, area.AxisX.AxisName, month.Key + 1);
/*
* Similarly, split the Y-axis into 4 segments for each period of a day.
*/
var y1 = e.ChartGraphics.GetPositionFromAxis(area.Name, area.AxisY.AxisName, period);
var y2 = e.ChartGraphics.GetPositionFromAxis(area.Name, area.AxisY.AxisName, period + 1);
/*
* Convert the relative coordinates to absolute ones.
*/
var x1abs = (float)(x1 / 100 * width);
var x2abs = (float)((x2 - x1) / 100 * width);
var y1abs = (float)(y2 / 100 * height);
var y2abs = (float)((y1 - y2) / 100 * height);
/*
* Finally, pick the color of the segment.
*/
var temperature = month.Value[period];
var color = GetColor(temperature);
using (var brush = new SolidBrush(color))
{
/*
* Fill the segment with its color.
*/
e.ChartGraphics.Graphics.FillRectangle(brush, x1abs, y1abs, x2abs, y2abs);
}
}
}
}
}
}
Upvotes: 1
Reputation: 54433
You have a choice of at least three ways to create a chart with colored rectangles that make up a heat map.
Here is one example
that uses/abuses a DataGridView
. While I would not suggest this, the post contains a useful function that creates nice color lists to use in your task.
Then there is the option to draw the chart using GDI+ methods, namely Graphics.FillRectangle
. This not hard at all but once you want to get those nice extras a Chart control offers, like scaling, axes, tooltips etc the work adds up.. See below!
So let's have a look at option three: Using the Chart
control from the DataVisualization
namespace.
Let's first assume that you have created a list of colors:
List<Color> colorList = new List<Color>();
And that you have managed to project your data onto a 2D array of int indices that point into the color list:
int[,] coloredData = null;
Next you have to pick a ChartType
for your Series S1
There really is only one I can think of that will help:
S1.ChartType = SeriesChartType.Point;
Points are displayed by Markers
. We want the DataPoints
not really displayed as one of the standard MarkerTypes.
Square
would be ok, if we wanted to display squares; but for rectangles it will not work well: Even if we let them overlap there will still be points at the borders that have a different size because they don't fully overlap..
So we use a custom marker by setting the MarkerImage
of each point to a bitmap of a suitable size and color.
Here is a loop that adds the DataPoints
to our Series
and sets each to have a MarkerImage
:
for (int x = 1; x < coloredData.GetLength(0); x++)
for (int y = 1; y < coloredData.GetLength(1); y++)
{
int pt = S1.Points.AddXY(x, y);
S1.Points[pt].MarkerImage = "NI" + coloredData[x,y];
}
This takes some explaining: To set a MarkerImage
that is not at a path on the disk, it has to reside in the Chart's Images
collection. This means is needs to be of type NamedImage
. Any image will do, but it has to have a unique name string added to identify it in the NamedImagesCollection
. I chose the names to be 'NI1', 'NI2'..
Obviously we need to create all those images; here is a function to do that:
void createMarkers(Chart chart, int count)
{
// rough calculation:
int sw = chart.ClientSize.Width / coloredData.GetLength(0);
int sh = chart.ClientSize.Height / coloredData.GetLength(1);
// clean up previous images:
foreach(NamedImage ni in chart1.Images) ni.Dispose();
chart.Images.Clear();
// now create count images:
for (int i = 0; i < count; i++)
{
Bitmap bmp = new Bitmap(sw, sh);
using (Graphics G = Graphics.FromImage(bmp))
G.Clear(colorList[i]);
chart.Images.Add(new NamedImage("NI" + i, bmp));
}
}
We want all markers to have at least roughly the right size; so whenever that size changes we set it again:
void setMarkerSize(Chart chart)
{
int sx = chart1.ClientSize.Width / coloredData.GetLength(0);
int sy = chart1.ClientSize.Height / coloredData.GetLength(1);
chart1.Series["S1"].MarkerSize = (int)Math.Max(sx, sy);
}
This doesn't care much about details like the InnerPlotPosition
, i.e. the actual area to draw to; so here is some room for refinement..!
We call this when we set up the chart but also upon resizing:
private void chart1_Resize(object sender, EventArgs e)
{
setMarkerSize(chart1);
createMarkers(chart1, 100);
}
Let's have a look at the result using some cheap testdata:
As you can see resizing works ok..
Here is the full code that set up my example:
private void button6_Click(object sender, EventArgs e)
{
List<Color> stopColors = new List<Color>()
{ Color.Blue, Color.Cyan, Color.YellowGreen, Color.Orange, Color.Red };
colorList = interpolateColors(stopColors, 100);
coloredData = getCData(32, 24);
// basic setup..
chart1.ChartAreas.Clear();
ChartArea CA = chart1.ChartAreas.Add("CA");
chart1.Series.Clear();
Series S1 = chart1.Series.Add("S1");
chart1.Legends.Clear();
// we choose a charttype that lets us add points freely:
S1.ChartType = SeriesChartType.Point;
Size sz = chart1.ClientSize;
// we need to make the markers large enough to fill the area completely:
setMarkerSize(chart1);
createMarkers(chart1, 100);
// now we fill in the datapoints
for (int x = 1; x < coloredData.GetLength(0); x++)
for (int y = 1; y < coloredData.GetLength(1); y++)
{
int pt = S1.Points.AddXY(x, y);
// S1.Points[pt].Color = coloredData[x, y];
S1.Points[pt].MarkerImage = "NI" + coloredData[x,y];
}
}
A few notes on limitations:
The point will always sit on top of any gridlines. If you really needs those you will have to draw them on top in one of the the Paint
events.
The labels as shown are referring to the integers indices of the data array. If you want to show the original data, one way would be to add CustomLabels
to the axes.. See here for an example!
This should give you an idea of what you can do with a Chart
control; to complete your confusion here is how to draw those rectangles in GDI+ using the same colors and data:
Bitmap getChartImg(float[,] data, Size sz, Padding pad)
{
Bitmap bmp = new Bitmap(sz.Width , sz.Height);
using (Graphics G = Graphics.FromImage(bmp))
{
float w = 1f * (sz.Width - pad.Left - pad.Right) / coloredData.GetLength(0);
float h = 1f * (sz.Height - pad.Top - pad.Bottom) / coloredData.GetLength(1);
for (int x = 0; x < coloredData.GetLength(0); x++)
for (int y = 0; y < coloredData.GetLength(1); y++)
{
using (SolidBrush brush = new SolidBrush(colorList[coloredData[x,y]]))
G.FillRectangle(brush, pad.Left + x * w, y * h - pad.Bottom, w, h);
}
}
return bmp;
}
The resulting Bitmap looks familiar:
That was simple; but to add all the extras into the space reserved by the padding will not be so easy..
Upvotes: 14