Use Transformations to Draw Your Own Great Graphs : Page 4

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 4 of 7

DataSeries Drawing
The DataSeries class represents a set of data points to be drawn. While the GreatGraph control ties everything together, the DataSeries class does most of the control's heavy lifting. After the control's code sets up the Graphics object's transformation, the DataSeries object does all of the drawing.

Depending on a DataSeries object's properties, the object can draw its data as a bar graph, as points connected by lines in various styles, or as a series of points represented by filled boxes. It can draw labels or tick marks next to the points, and it can draw lines showing the points' minimum, maximum, and average values.

In fact, a DataSeries object can do all of these things at the same time. For example, you can make it draw a bar graph, a line graph, and a series of points on top of each other with tick marks, labels, and minimum, maximum, and average values. The result would be cluttered to the point of uselessness, but allowing you all of this flexibility actually makes the code simpler.

The following code shows the properties that determine how the DataSeries draws its data. The Points array holds the data values. The Parent property stores a reference to the GreatGraph control that contains the DataSeries. The Name property holds the DataSeries object's name.

The control uses additional properties for specific drawing tasks. For example, the BarPen and BarBrush properties hold the pen and brush that the object should use when drawing its data as a bar graph. The LinePen property holds the pen that the object should use when drawing its data as a series of lines.

The AllowUserChangeX and AllowUserChangeY properties determine whether the user can click and drag to change data values' X and Y coordinates at run time. ShowDataTips tells the object whether it should display a point's value in a tooltip when the mouse moves over the point. Finally HitDx and HitDy are values you can set to determine how close to a point the mouse should be for the control to consider that the user has moused over the point.

The rest of the properties are relatively self-explanatory. They include values that determine how the DataSeries draws point boxes, tick marks, point labels, and average, minimum, and maximum value lines:

' The data points.
Public Points() As PointF = {}
' The GreatGraph containing this object.
Public Parent As GreatGraph = Nothing
' The data series name.
Public Name As String = ""
' Bar drawing.
Public BarPen As Pen = Nothing
Public BarBrush As Brush = Nothing
' Line drawing.
Public LinePen As Pen = Nothing
' Point drawing.
Public PointWidth As Single = 0.5
Public PointPen As Pen = Nothing
Public PointBrush As Brush = Nothing
' Tick mark drawing.
Public TickPen As Pen = Nothing
Public TickMarkWidth As Single = 1
' Label drawing.
Public Labels() As String = Nothing
Public LabelFont As Font = Nothing
Public LabelsOnLeft As Boolean = False
Public LabelBrush As Brush = Nothing
' Aggregate function drawing.
Public AveragePen As Pen = Nothing
Public MinimumPen As Pen = Nothing
Public MaximumPen As Pen = Nothing
' Determines whether the user can change data.
Public AllowUserChangeX As Boolean = False
Public AllowUserChangeY As Boolean = False
' Determines whether we display a data value tooltip.
Public ShowDataTips As Boolean = False
' The X and Y distances from the mouse
' to the cursor to indicate a data hit.
Public HitDx As Single = 0.25
Public HitDy As Single = 0.25

The following code shows the DataSeries class's main Draw subroutine that coordinates all of the drawing. This routine takes as parameters the Graphics object on which it should draw and the smallest and largest X world coordinate values. It simply calls other subroutines to do the actual drawing:

Each of the DataSeries object's drawing methods performs a single simple task, so their code is quite straightforward. The following code shows the DrawBar subroutine that draws the data as a bar graph:

DrawBar checks whether BarPen is Nothing. If so, the data should not be drawn in a bar graph, so the subroutine exits.

Next the routine calculates the width of the bars (it assumes they are all the same width). It loops through the Points array, making a RectangleF for each data point either above or below the X axis. Finally the code calls the Graphics object's FillRectangles and DrawRectangles methods to fill and outline the bars. (Note that the program assumes that if BarPen is present then BarBrush is also present.)

The DrawLine subroutine is even simpler. It checks the object's LinePen property and exits if LinePen is Nothing. It then just uses the Graphics object's DrawLines method to draw lines between the data points:

For each data point, the code calls GetTickVector to get a vector <tx, ty> pointing in the right direction for the point's tick mark. It then uses the Graphics object's DrawLine method to draw the tick mark along this vector.

The GetTickVector method in Listing 2 finds a tick mark vector for a point. It starts by finding the direction vectors for the segments before and after the point. The code handles the first and last points specially, because they don't have a previous or next segment respectively.