Title: Graph the sine, cosine, and tangent functions in C#
This example is mostly an exercise in drawing but it does demonstrate a few useful graphics techniques. When you enter minimum and maximum X and Y values and click the Graph button, the following code graphs the sine, cosine, and tangent functions.
// The image used for the graph.
private Bitmap GraphImage;
// Graph.
private void btnGraph_Click(object sender, EventArgs e)
{
GraphImage = new Bitmap(
picGraph.ClientSize.Width,
picGraph.ClientSize.Height);
using (Graphics gr = Graphics.FromImage(GraphImage))
{
gr.Clear(Color.White);
gr.SmoothingMode = SmoothingMode.AntiAlias;
using (Pen thin_pen = new Pen(Color.Purple, 0))
{
// Get the bounds.
double xmin = double.Parse(txtXmin.Text) * Math.PI;
double xmax = double.Parse(txtXmax.Text) * Math.PI;
double ymin = double.Parse(txtYmin.Text);
double ymax = double.Parse(txtYmax.Text);
// Scale to make the area fit the PictureBox.
RectangleF world_coords = new RectangleF(
(float)xmin, (float)ymax,
(float)(xmax - xmin),
(float)(ymin - ymax));
PointF[] device_coords =
{
new PointF(0, 0),
new PointF(picGraph.ClientSize.Width, 0),
new PointF(0, picGraph.ClientSize.Height),
};
gr.Transform = new Matrix(world_coords, device_coords);
// Draw the X-axis.
// Start at the multiple of Pi < xmin.
double start_x = Math.PI * ((int)(xmin - 1));
gr.DrawLine(thin_pen, (float)xmin, 0, (float)xmax, 0);
float dy = (float)((ymax - ymin) / 30.0);
for (double x = start_x; x <= xmax; x += Math.PI)
{
gr.DrawLine(thin_pen, (float)x, -2 * dy,
(float)x, 2 * dy);
}
for (double x = start_x + Math.PI / 2.0; x <= xmax;
x += Math.PI)
{
gr.DrawLine(thin_pen, (float)x, -dy, (float)x, dy);
}
// Draw the Y-axis.
// Start at the multiple of 1 < ymin.
double start_y = (int)ymin - 1;
gr.DrawLine(thin_pen, 0, (float)ymin, 0, (float)ymax);
float dx = (float)((xmax - xmin) / 60.0);
for (double y = start_y; y <= ymax; y += 1.0)
{
gr.DrawLine(thin_pen, -2 * dx, (float)y,
2 * dx, (float)y);
}
for (double y = start_y + 0.5; y <= ymax; y += 1.0)
{
gr.DrawLine(thin_pen, -dx, (float)y, dx, (float)y);
}
// Draw vertical asymptotes.
thin_pen.DashPattern = new float[] { 5, 5 };
for (double x = start_x + Math.PI / 2.0; x <= xmax;
x += Math.PI)
{
gr.DrawLine(thin_pen, (float)x, (float)ymin,
(float)x, (float)ymax);
}
// Draw horizontal limits for sine and cosine.
gr.DrawLine(thin_pen, (float)xmin, 1, (float)xmax, 1);
gr.DrawLine(thin_pen, (float)xmin, -1, (float)xmax, -1);
thin_pen.DashStyle = DashStyle.Solid;
// See how big a pixel is before scaling.
Matrix inverse = gr.Transform;
inverse.Invert();
PointF[] pixel_pts =
{
new PointF(0, 0),
new PointF(1, 0),
};
inverse.TransformPoints(pixel_pts);
dx = pixel_pts[1].X - pixel_pts[0].X;
// Sine.
List<PointF> sine_points = new List<PointF>();
for (float x = (float)xmin; x <= xmax; x += dx)
{
sine_points.Add(new PointF(x, (float)Math.Sin(x)));
}
thin_pen.Color = Color.Red;
gr.DrawLines(thin_pen, sine_points.ToArray());
// Cosine.
List<PointF> cosine_points = new List<PointF>();
for (float x = (float)xmin; x <= xmax; x += dx)
{
cosine_points.Add(new PointF(x, (float)Math.Cos(x)));
}
thin_pen.Color = Color.Green;
gr.DrawLines(thin_pen, cosine_points.ToArray());
// Tangent.
List<PointF> tangent_points = new List<PointF>();
double old_value = Math.Tan(xmin);
thin_pen.Color = Color.Blue;
for (float x = (float)xmin; x <= xmax; x += dx)
{
// See if we're at a discontinuity.
double new_value = Math.Tan(x);
if ((Math.Abs(new_value - old_value) > 10) &&
(Math.Sign(new_value) != Math.Sign(old_value)))
{
if (tangent_points.Count > 1)
gr.DrawLines(thin_pen,
tangent_points.ToArray());
tangent_points = new List<PointF>();
}
else
{
tangent_points.Add(new PointF(x,
(float)Math.Tan(x)));
}
}
if (tangent_points.Count > 1)
gr.DrawLines(thin_pen, tangent_points.ToArray());
}
}
// Display the result.
picGraph.Image = GraphImage;
}
The code starts by creating a bitmap to fit the PictureBox named picGraph. It creates a Graphics object to draw on the bitmap, sets the SmoothingMode to produce a smooth image, and makes a pen.
Notice that the pen has thickness 0. That makes it draw the thinnest possible line no matter how the drawing is scaled. If you use a ScaleTransform to scale a Graphics object by a factor of 10 in the X and Y directions, then a pen with thickness 1 is drawn 10 pixels wide. This code sets the pen's thickness to 0 so the line's width isn't scaled.
Next the code creates a transformation to map the desired world coordinates (the X and Y coordinates in the TextBoxes) to the PictureBox's client area. I make this kind of transformation a lot, but I always need to stare at it for a while to figure it out.
The Graphics object's Transform property is a Matrix that can scale, rotate, skew, and translate whatever you draw. The Matrix class has a constructor that takes as parameters a RectangleF giving the world coordinates where you will draw and an array of three points that indicate where the Rectangle's upper left, upper right, and lower left corners should be mapped. It uses those values to figure out where the fourth corner should go. This constructor lets you handle scaling, rotation, skewing, and translation all at once.
In this example, the program maps the world coordinates so the upper left corner maps to the upper left corner of the PictureBox at (0, 0), the upper right corner maps to the PictureBox's upper right corner at (picGraph.ClientSize.Width, 0), and the lower left corner maps to the lower left corner of the PictureBox at (0, picGraph.ClientSize.Height).
One particularly weird aspect of the transformation is the fact that the world coordinate rectangle's height is negative. This is because rectangles in .NET are specified by giving the upper left corner and the width and height moving downward and to the right. However, in a normal mathematical coordinate system, the Y coordinates increase upward not downward. To get the rectangle's "height," you need to subtract the bottom Y coordinate ymin from the top Y coordinate ymax. (If it makes more sense, you can just think of this as correcting for the fact that the coordinates of a PictureBox start with (0, 0) in the upper left corner and then increase downward and to the right.)
The next several chunks of code draw the X and Y axes. The only trick here is how the code draws the axes' tick marks. For the X axis, the code finds the first multiple of π less than the minimum X value. It then draws a large tick mark for that value and other X values starting there up to the maximum X value skipping π between each. It then goes back and adds small tick marks for the values midway between the previous values.
The code similarly draws large tick marks at multiples of 1 along the Y axis and draws smaller tick marks for the values in between: -0.5, 0.5, 1.5, and so forth.
Next the code draws some asymptotes, the places where the tangent function approaches a vertical line, and horizontal lines at y = ±1 to show the minimum and maximum values attained by the sine and cosine functions. The code uses a dash pattern of 5, 5 (draw 5 then skip 5 and repeat) because I think the dashes in the standard dash pattern are too small.
Now it's finally time to draw the functions. To get the best possible result, the graph should plot 1 point for every X coordinate on the graphed area. However, the program is drawing in world coordinates and the Graphics object's transformation is converting the result so it fits in the PictureBox. In that case, how can the program tell what X values to plot?
To solve this problem, the program gets the Graphics object's transformation matrix and inverts it. This new matrix does the opposite of the original one: it maps points on the PictureBox back to points in world coordinate space. The program makes an array holding two points 1 pixel apart in PictureBox coordinates and then uses the inverted transformation to map those points back to the world coordinate space. The difference in those points' X coordinates tells how far apart dx in coordinate space points must be to be 1 pixel apart on the PictureBox in the X direction.
Now the program loops variable x from xmin to xmax, increasing x by the value dx each time to produce 1 point for each pixel in the X direction. The code calculates Math.Sine and plots the result. It then repeats those steps to plot Math.Cos.
A complication arises with the tangent function because tangent(x) is undefined if x is π/2 plus any integer multiple of π. However, Math.Pi returns numbers for those X values so it's not obvious how to tell when the program shouldn't plot a value. In fact, if you blindly plot all of the values, you'll get a graph that looks mostly reasonable except it connects very large values before the discontinuities with very small values after the discontinuities making a vertical line.
To handle this problem, the code keeps track of the previous tangent value and compares it to the next one in the loop. If the values are far apart and they have a different sign, then the function is jumping over a discontinuity. In that case the program draws whatever points it has in its tangent_points list and starts a new list for future points. After it finishes traversing all of the X values, the code draws any remaining points in the list.
To summarize the useful tricks and techniques demonstrated by this example:
- Use a pen of thickness 0 to get a thin line even if the drawing is scaled.
- Use transformations to map from world coordinates to bitmap coordinates.
- Set a Graphics object's Transform property equal to a new Matrix object to handle scaling, rotation, skewing, and translation all at once.
- In defining the transformation matrix, use a rectangle with negative height if you want to map a mathematical coordinate system (Y increases upward) to a bitmap coordinate system (Y increases downward).
- Use code similar to the code that draws tick marks to find multiples of π.
- Use a pen's DashPattern property to draw custom dashes if, like me, you think the standard dashes are too small.
- Invert a Graphics object's Transform to get a matrix that maps from the drawing coordinates back to world coordinates. You can use this to draw things in world coordinates and have them end up where you want them in drawing coordinates.
- Follow me on Twitter so learn about new examples posted 4 or 5 times a week.
(Okay that last one isn't really a technique demonstrated by this example, but you might want to do it anyway ;-))
Download the example to experiment with it and to see additional details.
|