TODAY'S HEADLINES  |   ARTICLE ARCHIVE  |   FORUMS  |   TIP BANK
 Specialized Dev Zones Research Center eBook Library .NET Java C++ Web Dev Architecture Database Security Open Source Enterprise Mobile Special Reports 10-Minute Solutions DevXtra Blogs Slideshow

Use Transformations to Draw Your Own Great Graphs : Page 2

Use .NET to build your own graphing control that displays bar, line, and point data either on its own surface, in a printout, or in an image file.

 by Rod Stephens
 May 11, 2007
 Page 2 of 7

WEBINAR:On-Demand

Building the Right Environment to Support AI, Machine Learning and Deep Learning

Tricky Transformations
One of the most confusing parts of drawing a graph is making the data fit nicely on the graph's surface. Only rarely does raw data in world coordinates map naturally to a control's surface measured in device coordinates (pixels).

For example, suppose you want to graph sales over time, where the X axis measures time in months between 1 and 12, and the Y axis measures book sales in units between 0 and 20,000. To make the data fit nicely in a 300 x 300 pixel graph, you need to somehow scale the world coordinate units in the X (months) and Y (units) directions into the device coordinates (pixels).

To make fitting the data even more complex, the control's device coordinates start with (0, 0) in the upper left corner, with X and Y coordinates increasing to the right and down. This is counterintuitive, because most people think of a graph's coordinates as starting with (0, 0) in the lower left corner and increasing to the right and upward.

With a lot of patience and some tricky mathematics, you can probably figure out how to scale, translate, and flip the graph's X and Y coordinates around to map your data in world coordinates onto the control's surface in device coordinates. Fortunately you don't need to go to all of that bother because the Graphics object provides methods that make these sorts of transformations easy. All drawing in .NET takes place on a Graphics object so mapping world coordinates to device coordinates is simply a matter of properly setting up the Graphics object's transformations before you start drawing.

The Graphics object's transformation methods are pretty powerful and let you arbitrarily translate, scale, and rotate any drawing operation. You can see my previous article Terrific Transformations for details about these general transformations, but the GreatGraph control doesn't need that much flexibility. It only needs to map the world coordinates onto the control's device coordinates in a reasonably straightforward way, so in this article I'll stick to the simplest possible transformations.

You can build a complex series of transformations by using the Graphics objects methods but in this case there's a shortcut. The Graphics object's Transform property is a Matrix that determines how the Graphics object transforms everything that it draws. The Matrix class provides a constructor that builds an object representing a transformation from some rectangle to an arbitrary parallelogram. We can use that constructor to build a transformation from the world coordinate rectangle to the control's device coordinate parallelogram, which is also a rectangle.

The following code shows how the GreatGraph control's DrawToGraphics method draws the data it contains:

``````   ' Draw the graph on the Graphics object.
Public Sub DrawToGraphics(ByVal gr As Graphics, _
ByVal dxmin As Single, ByVal dxmax As Single, _
ByVal dymin As Single, ByVal dymax As Single)
' Save the graphics state.
Dim original_state As GraphicsState = gr.Save()

' Map the world coordinates onto the control's surface.
Dim world_rect As New RectangleF(Wxmin, Wymax, _
Wxmax - Wxmin, Wymin - Wymax)
Dim client_points() As PointF = { _
New PointF(dxmin, dymin), _
New PointF(dxmax, dymin), _
New PointF(dxmin, dymax) _
}
gr.Transform = New Matrix(world_rect, client_points)

' Clip to the world coordinates.
gr.SetClip(world_rect)

' Clear and draw the graph objects.
For Each obj As DataSeries In m_GraphObjects
obj.Draw(gr, Wxmin, Wxmax)
Next obj

' Restore the original graphics state.
gr.Restore(original_state)
End Sub
``````
The preceding code starts by saving the Graphics object's state so it can restore it when it is done drawing.

Next the code builds a rectangle representing the world coordinates. It also creates an array of points to represent the upper left, upper right, and lower left corners of the parallelogram to which the Matrix should map the rectangle. The code then uses the rectangle and array of points to build the Matrix and saves it in the Graphics object's Transform property.

The final step before drawing is to set the control's clipping area to the world coordinate rectangle. That makes the Graphics object clip off any drawing commands that fall outside of this rectangle. The control's drawing surface automatically clips to its edges but the control also uses this code to draw on printouts where the Graphics object is not clipped by the control's boundaries. Setting the clipping region ensures that graphics won't stray outside of the area where you want them on the printout.

At this point, the control is ready to draw. The code loops through the control's DataSeries objects, calling each object's Draw method. The next section of this article covers the DataSeries class in more detail.

After it has finished drawing, the code calls the Graphics object's Restore method to restore the graphics state it saved earlier. That resets the Graphics object's transformation and clipping region to whatever they were before the DrawToGraphics method started.

The following code shows how the control redraws itself when it receives a Paint event. The code clears its drawing surface and calls its own DrawToGraphics method, passing in the Graphics object on which to draw and its device coordinates. The DrawToGraphics method does all of the work:

``````   ' Draw the graph objects.
Private Sub GreatGraph_Paint(ByVal sender As Object, _
ByVal e As System.Windows.Forms.PaintEventArgs) Handles Me.Paint
e.Graphics.Clear(Me.BackColor)

DrawToGraphics(e.Graphics, _
Me.ClientRectangle.Left, _
Me.ClientRectangle.Right, _
Me.ClientRectangle.Top, _
Me.ClientRectangle.Bottom)
End Sub
``````
This code may seem a little indirect, calling the DrawToGraphics subroutine to do all of its work when you could just include the DrawToGraphics code right here in the Paint event handler. However, as the following section shows, putting the drawing code in a separate routine makes it easier to draw to print previews and image files.