Title: Draw a normal distribution curve in C#
A normal distribution is a probability distribution that occurs arise when you analyze seemingly "random" occurrences. You can define a normal distribution in terms of its mean and standard deviation.
- Mean (μ)
- This is the basically the average of the values in the data set.
- Standard deviation (σ)
- This is a measurement of how tall and peaked or short and wide the curve it.
Its is also helpful to define the curve's variance, which is simply σ2. With those three values defined, you can graph a normal distribution by using the following equation.
The notation F(x|μ,σ) means you're calculating a function of x given fixed values for μ and σ.
The following code shows the function used by the example.
// The normal distribution function.
private float F(float x, float one_over_2pi, float mean,
float stddev, float var)
{
return (float)(one_over_2pi *
Math.Exp(-(x - mean) * (x - mean) / (2 * var)));
}
This is simply a matter of plugging in the values for x, the mean, the standard deviation, and the variance.
That's all there is to the example as far as calculating the distribution function is concerned. Most of the example's code deals with drawing it. The code is fairly long so it's presented in pieces below.
The following code shows how the program starts drawing the curve.
private void btnDraw_Click(object sender, EventArgs e)
{
float mean = float.Parse(txtMean.Text);
float stddev = float.Parse(txtStdDev.Text);
float var = stddev * stddev;
// Make a bitmap.
Bitmap bm = new Bitmap(
picGraph.ClientSize.Width,
picGraph.ClientSize.Height);
using (Graphics gr = Graphics.FromImage(bm))
{
gr.SmoothingMode = SmoothingMode.AntiAlias;
// Define the mapping from world
// coordinates onto the PictureBox.
const float wxmin = -5.1f;
const float wymin = -0.2f;
const float wxmax = -wxmin;
const float wymax = 1.1f;
const float wwid = wxmax - wxmin;
const float whgt = wymax - wymin;
RectangleF world = new RectangleF(wxmin, wymin, wwid, whgt);
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 gets the mean and standard deviation entered by the user. It calculates the variance.
Next the code creates a Bitmap to hold the drawing. It makes an associated Graphics object to do the drawing.
To make drawing easier, the program will draw in a coordinate system that's natural for it, so the code defines constants to represent the world coordinates where it will draw. For this example, I used -5.1 ≤ x ≤ 5.1, -0.2 ≤ y ≤ 1.1. Those values work well for distributions with means near 0 and relatively small standard deviations.
Next the code creates a RectangleF defining the world coordinates and an array if PointF that indicates where the upper left, upper right, and lower left corners of the world coordinates should be mapped to fit on the Graphics object that will be used for drawing. It uses the RectangleF and array if PointF to make a Matrix representing a transformation from world coordinates onto a Graphics object. You'll see how that's used shortly.
The following code shows the next few steps.
// Make a thin Pen to use.
using (Pen pen = new Pen(Color.Red, 0))
{
using (Font font = new Font("Arial", 8))
{
// Draw the X axis.
gr.Transform = transform;
pen.Color = Color.Black;
gr.DrawLine(pen, wxmin, 0, wxmax, 0);
for (int x = (int)wxmin; x <= wxmax; x++)
{
gr.DrawLine(pen, x, -0.05f, x, 0.05f);
gr.DrawLine(pen, x + 0.25f, -0.025f,
x + 0.25f, 0.025f);
gr.DrawLine(pen, x + 0.50f, -0.025f,
x + 0.50f, 0.025f);
gr.DrawLine(pen, x + 0.75f, -0.025f,
x + 0.75f, 0.025f);
}
This code defines a Pen with thickness 0. When you use transformations, any lines you draw are also transformed. For this example, I don't want the lines to be transformed because they would be stretched out of shape. In this case the world coordinates are 10.2 units wide so if I drew a line with thickness 1, it would be scaled so it was about 10% of the width of the drawing area (horizontally). For this example, the line would be almost 50 pixels wide.
If you use a Pen with thickness 0, the program draws it one pixel wide no matter how the Graphics object is transformed.
After defining the thin pen, the code creates a Font to use in labeling the axes. It then sets the Graphics object's Transform property to the Matrix that maps from world coordinates to the Bitmap. After this, any drawing commands will be transformed appropriately.
The code then loops over some x values to draw the X axis.
The following code shows how the program labels the axis.
// Label the X axis.
gr.Transform = new Matrix();
gr.TextRenderingHint =
TextRenderingHint.AntiAliasGridFit;
List<PointF> ints = new List<PointF>();
for (int x = (int)wxmin; x <= wxmax; x++)
ints.Add(new PointF(x, -0.07f));
PointF[] ints_array = ints.ToArray();
transform.TransformPoints(ints_array);
using (StringFormat sf = new StringFormat())
{
sf.Alignment = StringAlignment.Center;
sf.LineAlignment = StringAlignment.Near;
int index = 0;
for (int x = (int)wxmin; x <= wxmax; x++)
{
gr.DrawString(x.ToString(), font,
Brushes.Black,
ints_array[index++], sf);
}
}
If the program simply drew text at this point, it would be transformed by the Graphics object's current transformation so the text would be stretched weirdly. To avoid that, the code resets the Graphics object's Transform property to a new identity transformation. Any drawing after this point, including drawing text, will not be transformed.
Of course that means the program can't use world coordinates to draw the text because they wouldn't be mapped to the correct locations. To solve that problem the code creates an array of PointF holding the locations where it would like to draw the text. It then uses the transformation Matrix object's TransformPoints method to transform the points into Graphics object coordinates.
Next the program creates a StringFormat object to align the text properly and loops through the points drawing X values at the appropriate positions.
The code that draws the Y axis is similar so it isn't shown here.
The following code shows the rest of the Draw button's event handler, which draws the distribution curve.
// Draw the curve.
gr.Transform = transform;
List<PointF> points = new List<PointF>();
float one_over_2pi = (float)(1.0 /
(stddev * Math.Sqrt(2 * Math.PI)));
float dx = (wxmax - wxmin) /
picGraph.ClientSize.Width;
for (float x = wxmin; x <= wxmax; x += dx)
{
float y = F(x, one_over_2pi, mean, stddev, var);
points.Add(new PointF(x, y));
}
pen.Color = Color.Red;
gr.DrawLines(pen, points.ToArray());
} // Font
} // Pen
picGraph.Image = bm;
}
}
This code makes a List<PointF>. It then divides the width of the world coordinates by the width of the output bitmap so it can plot a value with x equal to each pixel in the bitmap. It loops through those x values and uses the function F to adds points to the list. It finishes by converting the list into an array and drawing lines to connect the points.
Download the example to experiment with it and to see additional details.
|