Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Autoscaling Graph Control

0.00/5 (No votes)
10 May 2006 1  
Learn how to create a graph that can automatically scale itself!

WDIGraph Sample Image

Introduction

I write a lot of programs that take data in from outside sources, like lab equipments and other junk. I've always wanted a simple graph control that will display the data I'm reading, so that I can tell, while the code is running, whether or not something has gone wrong. One part of what makes this tough is making the data fit into the graph window. So, I figured out how to use the Matrix class to do the auto-scaling for me!

This article is more of a tutorial on using Matrix transformations, and not a complete control. If you want to use this as a control in your application, you can either add what you need to the code, or go to my website and download the more complete version.

Background

This article (c-sharpcorner.com) is a pretty good introduction on using Matrix transforms with GDI+.

Using Matrices to Transform Graphics Objects

So, the basic idea is that we can use a Matrix to change the origin and scale of our graph window. To actually perform the transform, you use some code like this:

// in my Paint even handler...


// transform the graphics context to get

// the origin near the bottom left of the window

Matrix trans = new Matrix(1, 0, 0, -1, 30, this.Height - 30);
trans.Scale(scale_x, scale_y);
e.Graphics.Transform = trans;

In this code, I'm actually using the Matrix object to do three things.

First, I want the origin of the graph to be 30 pixels to the right, and 30 pixels up from the bottom-left corner of the window. This is what the last two arguments of the Matrix constructor do. The first one of these, 30, is the x-axis translation factor. So this argument means, move the origin of the graph 30 pixels to the right. this.Height - 30 is similar, and it means move the origin down 30 pixels less that the height of the graph window.

The second thing I want to do is change how the coordinates are treated by the graph. GDI, and every other programming language's coordinate system, starts in the upper-right hand corner, and increasing the y-axis coordinates move downwards on the screen. This is the opposite of what humans expect a graph to do. We like the graph to move upwards when the y value increases. So, that's what the first four numbers of the Matrix constructor do. Actually, it's the fourth number in the sequence that does this. If you look at the c-sharpcorner.com article mentioned above, you can figure out why you need that -1 in the matrix. Essentially, it means, y-axis coordinates are the opposite of what the computer normally thinks they are.

The final thing I'm doing with this matrix is scaling the graph. I do this with the trans.Scale(scale_x, scale_y); line. Fortunately, this line is a little less complicated than the Matrix constructor. All you really need to do is feed it some scale factors, and you're all set.

Finally, you just need to set the current Graphics Transform member to the Matrix object you've just created. I'll explain the scale factors in the next section. Now, your coordinate system will be set up just like it was when you learned how to graph numbers in grade school...

If you're into math, you can look at the end of the article to see exactly what's going on with this Matrix mess.

Determining Auto-scale Factors

So, how do we get the scale factors mentioned in the section above? It's actually pretty simple. In this implementation, I've used an ArrayList of PointFs to store the data I want on the graph. So, I've created the following function to find the scale factors:

public void Autoscale()
{
  foreach (PointF p in data)
  {
    if (p.X > xmax)
      xmax = p.X;
    if (p.Y > ymax)
      ymax = p.Y;
    }
    scale_x = (float)((this.Width - 30f) / xmax);
    scale_y = (float)((this.Height - 30f) / ymax);            
}

All this function does is, look through each value in the data and find which one of them is the largest in each dimension. Then, to create the scale factor, it divides the width (in pixels) of the graph window by this number. That's it! (Note that I subtract 30 from the Width and Height because I moved my origin 30 pixels in.)

An Alternate Method

If you really don't like creating the Matrix object, there's another, possibly simpler way to do this. If you look at my code, you can see I do some translation and rotation to draw my "Y-Axis" string. The code looks like this:

e.Graphics.TranslateTransform(15, 130);
e.Graphics.RotateTransform(-90);
e.Graphics.DrawString(y_text, 
  new Font(FontFamily.GenericMonospace, 8f), 
  new SolidBrush(Color.LawnGreen), new PointF(0f, 0f));

This code sets a TranslateTransform to move the origin to (15, 130), and then rotates the coordinate system by -90 degrees. This way, I can use Graphics.DrawString to draw a vertical string next to my y-axis line. I usually use this method when I'm only going to make one or two method calls that need an altered coordinate system. Like in this case. I feel that using the actual Matrix object is better when doing multiple draw calls. I'm not really sure why though. I guess I just like it better.

What is all this Matrix Garbage?

OK! I'll try to explain exactly what's going on with the Matrix. If you know even a little matrix algebra, this shouldn't be too hard. If you don't really care, it's not essential to know this, but some people might be curious.

So, what is actually being created if we write a line like the following?

Matrix trans = new Matrix(1, 0, 0, -1, 0, -10);

Well, I'll tell you. You're actually getting two matrices with one constructor! That's pretty great! Here is what's actually created:

Of course, x and y are the input points, and x' and y' are the output. The left-most matrix is similar to the identity matrix, except for the -1. So, if you multiply this by the input points, you can see that you're just multiplying y by -1. If you think about the input as points that GDI deals with, you can see that instead of moving down the screen with increasing y values, we start moving up, or vice versa. That's pretty nice! The next matrix in the equation is the translation matrix ([0 -10]'). If you think about performing this operation, you can see that it ends up moving every y value 10 units down. So, if you use this exact Matrix in your code, you'll be moving your graph 10 pixels down, and inverting the direction points, move in the y-axis. That's exactly what we want to happen for this graph!

Just to prove this, I wrote a quick MATLAB script, and plotted the results:

Points of Interest

One interesting and frustrating thing I found when writing this code is that the pixel width of a Pen is changed when using scaling transforms. If you read my code, you'll see in the paint handler that I set the width of each pen to 1 / scale_x. Otherwise, your graph line will get really fat if you have a large scale factor. It's really not a perfect solution, but it works OK.

There's not really much documentation on the Matrix class in the MSDN docs, so it's sort of hard to figure out exactly what kind of a matrix you need to do what you want. This seemed like a pretty common transform people would want to do, so I thought I'd share it. Now, how about some comments?

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here