Title: Plot data with standard deviation ranges in C#
The examples Draw a normal distribution curve in C# and Draw a scaled normal distribution in C# show one approach for visualizing how data is distributed. This example shows a method that lets you visualize where a particular data value lies within a sequence of data sets.
The example uses a different data set for each of the ages 6 through 13. (You could use other kinds of data such as sales over several years, traffic crashes over several years, test scores versus hours of studying, and so forth.) The black line that runs through the graph shows the means for the different data sets. The colored bands show 0.5, 1.5, and 2.5 standard deviations from the means.
The following code shows how the example initializes its data.
// The mean and standard deviation data.
private const int MinAge = 6;
private float[] Means =
{ 62, 56, 43, 42, 39, 36, 32, 31, };
private float[] StdDevs =
{ 15, 9, 8, 8, 7, 5, 5, 6, };
// Some test points.
private PointF[] TestPoints =
{
new PointF(6, 58),
new PointF(7, 63),
new PointF(9, 55),
new PointF(11, 39),
new PointF(13, 29),
};
This code simply creates arrays to hold mean and standard deviations for each of the ages. It then creates an array of test points to plot on the graph.
The example's DrawGraph method draws the graph. It's pretty long, so it's shown here in parts. Here's the first part:
// Draw the graph.
private void DrawGraph(int min_age, float[] means, float[] stddevs)
{
int max_age = min_age + means.Length - 1;
// Get the minimum and maximum values.
const float max_dev = 2.5f;
float min_value = means[0] - max_dev * stddevs[0];
float max_value = means[0] + max_dev * stddevs[0];
for (int i = 0; i < means.Length; i++)
{
if (min_value > means[i] - max_dev * stddevs[i])
min_value = means[i] - max_dev * stddevs[i];
if (max_value < means[i] + max_dev * stddevs[i])
max_value = means[i] + max_dev * stddevs[i];
}
if (min_value > 0) min_value = 0;
float hgt = 1.2f * (max_value - min_value);
float middle = (max_value + min_value) / 2f;
min_value = middle - hgt / 2f;
max_value = middle + hgt / 2f;
// Make a transformation for drawing.
RectangleF world = new RectangleF(
min_age - 1f, min_value,
max_age - min_age + 1.5f, max_value - min_value);
PointF[] device_points =
{
new PointF(0, picGraph.ClientSize.Height),
new PointF(
picGraph.ClientSize.Width,
picGraph.ClientSize.Height),
new PointF(0, 0),
};
Matrix transform = new Matrix(world, device_points);
This code calculates the minimum and maximum values that the program needs to display. To do that, it loops through the means and adds and subtracts 2.5 times the standard deviation to each.
After finding the largest and smallest values, the code creates a RectangleF representing the world coordinates that it will plot. (The minimum and maximum X and Y coordinates it will need to draw.) It creates an array of PointF to determine where the upper left, upper right, and lower left corners of the world coordinates should be mapped to appear correctly on the PictureBox. It then uses the RectangleF and the array of PointF to create a transformation to perform that mapping.
The following piece of code draws the underlying graph.
Bitmap bm = new Bitmap(
picGraph.ClientSize.Width,
picGraph.ClientSize.Height);
using (Graphics gr = Graphics.FromImage(bm))
{
using (Pen pen = new Pen(Color.Red, 0))
{
gr.SmoothingMode = SmoothingMode.AntiAlias;
gr.Transform = transform;
// Draw the standard deviation envelopes.
using (SolidBrush brush =
new SolidBrush(Color.FromArgb(255, 128, 128)))
{
pen.Color = brush.Color;
DrawEnvelope(gr, min_age, means, stddevs,
2.5f, brush, pen);
}
using (SolidBrush brush =
new SolidBrush(Color.FromArgb(255, 255, 128)))
{
pen.Color = brush.Color;
DrawEnvelope(gr, min_age, means, stddevs,
1.5f, brush, pen);
}
using (SolidBrush brush =
new SolidBrush(Color.FromArgb(128, 255, 128)))
{
pen.Color = brush.Color;
DrawEnvelope(gr, min_age, means, stddevs,
0.5f, brush, pen);
}
// Draw the curve.
List<PointF> points = new List<PointF>();
for (int i = 0; i < means.Length; i++)
points.Add(new PointF(i + min_age, means[i]));
pen.Color = Color.Black;
gr.DrawLines(pen, points.ToArray());
This code creates a Bitmap to fit the PictureBox and then makes an associated Graphics object.
Next it creates a Pen with thickness 0. When you draw with a Pen that has thickness 0, the GDI+ drawing library draws the line 1 pixel wide even if the line's points are transformed. If you use a line with some other thickness, the transformation also applies to the line's width so you get distorted lines. In this example, the whole graph is covered by the lines. (Set the thickness to 1 and see for yourself.)
The code sets the Graphics object's SmoothingMode property to produce smooth lines. It then sets the Transform property so lines are mapped from world coordinates to the Bitmap coordinate system.
Next the code creates a series of brushes and passes them and appropriately colored Pen into the DrawEnvelope method to draw the bands showing multiples of the standard deviations. (That method is described later.)
This piece of code finished by drawing the curve connecting the means.
The next chunk of code draws the X axis.
// Draw and label the axes.
pen.Color = Color.Black;
using (Font font = new Font("Arial", 8))
{
gr.TextRenderingHint =
TextRenderingHint.AntiAliasGridFit;
using (StringFormat sf = new StringFormat())
{
sf.Alignment = StringAlignment.Center;
sf.LineAlignment = StringAlignment.Near;
// Draw the X axis.
// Draw the axis.
gr.DrawLine(pen, min_age, 0, max_age, 0);
// Draw the tick marks.
for (int x = min_age; x <= max_age; x++)
gr.DrawLine(pen, x, 0, x, max_value);
// Label the ages.
List<PointF> tick_points = new List<PointF>();
List<string> tick_labels = new List<string>();
PointF[] label_points_array;
for (int x = min_age; x <= max_age; x++)
{
tick_points.Add(new PointF(x, 0));
tick_labels.Add(x.ToString());
}
label_points_array = tick_points.ToArray();
transform.TransformPoints(label_points_array);
gr.Transform = new Matrix();
for (int i = 0; i <
label_points_array.Length; i++)
{
gr.DrawString(tick_labels[i], font,
Brushes.Black, label_points_array[i],
sf);
}
This code creates Font and StringFormat objects to use when drawing text. It then draws the X axis's line and tick marks.
To label the tick marks, the code creates two lists, one holding the location where each label should be placed and the label's text. It converts the list of points into an array and uses the transform object's TransformPoints method to map the points from world coordinates to the Bitmap coordinate system.
Next the code resets the Graphics object's Transform property to a new Matrix, which represents the identity transformation. That prevents the transformation from applying to the label text and distorting the characters.
The code finishes by looping through the labels, drawing each label at its transformed location.
The code that draws the Y axis is similar so it isn't shown here.
The following code shows the last part of the DrawGraph method.
} // StringFormat
} // Font
// Plot test points.
transform.TransformPoints(TestPoints);
gr.Transform = new Matrix();
foreach (PointF point in TestPoints)
{
gr.FillRectangle(Brushes.Red,
point.X - 3, point.Y - 3, 6, 6);
}
} // Pen
} // Graphics
picGraph.Image = bm;
}
This code simply plots the test data points.
The following code shows the DrawEnvelope method, which draws one of the shaded areas on the graph.
// Draw an envelope for dev_mult times the standard deviations.
private void DrawEnvelope(Graphics gr, int min_age,
float[] means, float[] stddevs,
float dev_mult, Brush brush, Pen pen)
{
List<PointF> points = new List<PointF>();
for (int i = 0; i < means.Length; i++)
points.Add(new PointF(
i + min_age,
means[i] + dev_mult * stddevs[i]));
for (int i = means.Length - 1; i >= 0; i--)
points.Add(new PointF(
i + min_age,
means[i] - dev_mult * stddevs[i]));
gr.FillPolygon(brush, points.ToArray());
gr.DrawPolygon(pen, points.ToArray());
}
This method creates a list of PointF objects. It then loops through the data adding points to the list. First it goes from left-to-right, adding the mean values plus a multiple of their standard deviations to get the Y coordinates along the top of the area it is drawing. Next the method goes from left-to-right, subtracting a multiple of their standard deviations from the mean values to get the Y coordinates along the bottom of the area.
After it has generated the points representing the area, the code fills it and then outlines it using the Brush and Pen passed in as parameters. (In this example the Brush and Pen have the same color, so you don't see the effect of the Pen. I had originally planned to outline each area with a slightly darker color, but decided this version looked better.)
That's all there is to this example. It's fairly long, but not too complicated if you have some experience with GDI+ graphics.
Download the example to experiment with it and to see additional details.
|