The code for drawing the lines will be contained in a separate procedure called DrawTheLine. The first task is to calculate the length of each segment of the overall graph line. By this I mean that each month's sales figure is to be represented by a line segment; we need to calculate how much of the available width of the PictureBox is to be given to each of those monthly segments.
It's fairly simple math:
' Calculate length of baseline drawn by the previous procedure
BaseLineLength = PBLineChart.Width - (LeftMargin + RightMargin)
' Calculate the width of each line segment
LineWidth = (BaseLineLength / Sales.Length)
Sales.Length is of course the number of elements in the array that holds the Sales figures. We work out the total available width of the baseline and divide it by the number of segments that we want to fit into it.
Calculate Vertical Scale
The line segments that we draw are going to rise or fall to represent the rise or fall in sales figures from month to month. These values are fixed. However, the length of the vertical axis can be changed (as we demonstrated previously by resizing the form and seeing that the PictureBox size also changed). In order to ensure that the height rise or fall is drawn proportionately to the current size of the PictureBox we must calculate the scale, i.e. how many Sales are represented by a single pixel up that vertical line. This measurement will be used to ensure that the line segments are drawn proportionately within the overall height available.
This may sound complicated. It really isn’t and the calculation itself is very simple. We divide the total number of pixels in that vertical line by the maximum value we set for the axis:
Dim VertScale As Double
VertScale = VertLineLength / 1000
and that provides us with a scaling value that will be valid no matter how many times we resize the form.
Create and Draw the First Line Segment
The first line segment will of course represent the sales figure for Jan (which will be start of the line segment) and the sales figure for Feb (the end of the line segment). We know that the start and end points of lines each have an X and a Y value. The best way to draw the series of line segments is to create four variables, one each for the two X values and one each for the two Y values.
Here's the code that does that:
Dim XPosStart As Integer = CInt(LeftMargin + 30)
Dim XPosEnd As Integer = CInt(XPosStart + LineWidth)
Dim YPosStart As Integer = CInt(Sales(0) * VertScale)
Dim YPosEnd As Integer = CInt(Sales(1) * VertScale)
The names of the four variables are self-explanatory. The arithmetic is straightforward:
XPosStart is the X value of the start of the line segment and is placed 30 pixels in from the left margin.
XPosEnd is the X value of the end of the line segment and is calculated by adding the line segment width (or length, if you prefer to think of it that way) to the start point value.
YPosStart is the Y value of the start of the line segment. It is equal to the value of the first Sales figure (Jan) multiplied by that vertical scaling factor that we created a moment ago.
YPosEnd is the Y value of the end of the line segment. It is equal to the value of next Sales figure, which in the code above happens to be Feb's figure, the second element in the array.
The Inverted and Disappearing Lines Problem
You would expect that we could now simply draw a set of lines one after the other using the standard DrawLine method. We could, but you wouldn't get the results you expected. The reason is that when we view graphs we expect the start point of the graph (that is, Point 0,0) to be in the bottom left hand corner. This is standard and you will be familiar with this expectation, I'm sure.
The problem is that the PictureBox control has its 0,0 point in the Top Left corner. So if we were to draw a line, say from 0,0 to 100,100 this line would angle downwards from the top left hand corner. This is the opposite of what we would expect - we actually want a line that extends from the bottom left corner and goes upwards.
There are several ways of solving this problem. The fix we will use in this article is to create the series of line segments in memory which we will store in a Drawing class object called the GraphicsPath. We will then rotate and adjust the starting position of the "upside down" lines in this GraphicsPath so that they appear in the way we want them.
The GraphicsPath object is quite versatile and is not difficult to use. The transformations and rotations, however, are not quite so easy to understand. In fact, they can be quite mind boggling unless you have the mind of a mathematician (which I don't). For the purposes of this graph creation article, therefore, I am going to show you the code needed but will sidestep the mechanics of just how it works. In a later article I will come back to this thorny subject and try and explain a step at a time in non-mathematician terms just what is involved. If you have tried transformations yourself, you will probably have already come up against the problem of the invisible line - that is, you rotate a line and it simply disappears!
The code that we use will resolve all the above difficulties. Create a new GraphicsPath object:
Dim MyPath As New GraphicsPath
The First Line Segment
As you will see, we will iterate through each of the elements in the Sales array - that's to say, each of the sales figures - in turn. As we come upon each new figure, we will use it to change the end point for the next line segment.
The first segment, however, we have to set up individually in order to get the ball rolling:
MyPath.AddLine(XPosStart, YPosStart, XPosEnd, YPosEnd)
Note that the AddLine method takes just the four parameters - two X values and two Y values. We don't need to specify a pen to draw the line with; this will be taken care of when we finally come to draw the complete Path. AddLine does what the name suggests; it adds the details of this line segment to the GraphicsPath.
The Remaining Line Segments
Now that the first segment is created and stored, we can iterate through the Sales array and add the remaining line segments to the GraphicsPath:
For i As Integer = 1 To UBound(Sales) - 1
' Update the X and Y positions for the next value:
' Move start point one line width to the right
XPosStart = XPosEnd
' Move end point one line width to the right
XPosEnd = CInt(XPosStart + LineWidth)
' Assign YPosStart the 'old' value of Y
YPosStart = YPosEnd
' Assign YPosEnd the next the next scaled Sales figure
YPosEnd = CInt(Sales(i + 1) * VertScale)
' Add this line segment to the GraphicsPath
MyPath.AddLine(XPosStart, YPosStart, XPosEnd, YPosEnd)
That's all the line segments created and stored in the GraphicsPath. Now for that tricky Transformation bit.
Here is the code that rotates and flips the complete line and also moves its start location to the correct position. I have included some commenting to give an indication of what each line does.
' We want the line to go in opposite direction, so rotate by 180 degrees
' Because the rotation also moves the line out of view (to the left), so
' we need to scale the x so that it is on the (plus) side of the
' vertical axis.
' The Y value remains unchanged, so a "scale" of 1 is used.
' (i.e. no change made)
' Move the start point down to the bottom left corner
' The X value remains the same
' Y is shifted down to the end of the vertical axis, adjusted by
' a fudge factor of 10 to compensate for the vertical scaling.
g.TranslateTransform(0, VertLineLength + 10, MatrixOrder.Append)
Draw the Transformed Line
Drawing the complete line that we have just created and transformed is a simple matter of drawing the GraphicsPath on to the Graphics Object. The GraphicsPath, of course, contains all the data we entered for the line segments.
Dim MyPen As Pen = New Pen(Color.Blue, 3)
One final, but very important task remains. We have to reset or roll back the Transformations that have been applied to the Graphics Object. Failure to do so will mean that any future drawing task using this Graphics Object will have the same Transformations applied to them. The results would then almost certainly not be what you wanted.
Check It Out
Once again, we can check that everything is going to plan. In the click event of the button code, enter this:
Private Sub btnDraw_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btnDraw.Click
Run the project and click the button. You should now see the chart line that represents the Sales figures.