DevCity.NET -
Chart Success Part 5 - Line Graph
Ged Mead

Ged Mead (XTab) is a Microsoft Visual Basic MVP who has been working on computer software and design for more than 25 years. His journey has taken him through many different facets of IT. These include training as a Systems Analyst, working in a mainframe software development environment, creating financial management systems and a short time spent on military laptop systems in the days when it took two strong men to carry a 'mobile' system.

Based in an idyllic lochside location in the West of Scotland, he is currently involved in an ever-widening range of VB.NET, WPF and Silverlight development projects. Now working in a consultancy environment, his passion however still remains helping students and professional developers to take advantage of the ever increasing range of sophisticated tools available to them.

Ged is a regular contributor to forums on vbCity and authors articles for DevCity. He is a moderator on VBCity and the MSDN Tech Forums and spends a lot of time answering technical questions there and in several other VB forum sites. Senior Editor for DevCity.NET, vbCity Developer Community Leader and Admin, and DevCity.NET Newsletter Editor. He has written and continues to tutor a number of free online courses for VB.NET developers.

by Ged Mead
Published on 6/6/2006

   In previous articles, we have looked ways of creating various Pie Charts and Bar Charts.  In this Part of the series, we are going to take a first look at another common type of chart, the Line Graph.


   As is often the case in VB.NET, there are several ways of achieving the end result.   Although I haven't chosen the easiest route this time, the journey takes us into some interesting GDI+ territory.



   As in the previous articles, the chart will be displayed in a PictureBox control.   Although it would be easy - easier in some ways - to simply plaster the chart onto the surface of the form itself, I think it is more realistic to house it inside some kind of container control because a chart is most often just one part of a range of information displayed to the user.   It also lends itself more easily to the situation where you want to take user input on screen and to use that data directly to draw or redraw the chart.

    For convenience, we will create some sample data at the start of the project.  In a real world scenario, you will probably gather this data from a data source of some kind, either one that has been previously saved to file or directly from the user on screen.    If you want to adjust the code in this article to take in data at runtime, you will find an example of how this can be done in Part 3 of this series.

Getting Started

   Add a form to a project and name it LineChartDemo.  Make the form size something in the region of 640 by 480.   Drag  a PictureBox on to the form from the ToolBox and drag its left, top and right borders so that they are very close to the edge of the form.   Drag the bottom of the PictureBox so that the PictureBox height is approx 80% of the form's height.   Set the PictureBox's Anchor property to include all four sides, Top, Bottom, Left and Right.  Set its BorderStyle property to the selection your prefer; I have used Fixed3D in the screenshot examples.  Name it "PBLineChart".

  Now, add a button to the form and place it down at the bottom right hand corner.   Change its Anchor property from the default setting to Bottom and Right.   These changes to the Anchor properties of the two controls mean that the PictureBox will resize usefully if the size of the form is changed and the button will remain anchored down in the left hand corner where you want it to be. 

   At the very top of the form, insert the following:

    Option Strict On
   Imports System.Drawing.Drawing2D

  The first line, as you probably know, is good practice (although it makes some of the code more unwieldy) and helps to avoid tricky conversion errors between the various Types .  The second line allows us access to all the methods in the Drawing2D class, some of which we will use to create the various lines and shapes in the chart.

 The Sample Data  

    Our sample data is very basic - 12 months of the year to be displayed on the horizontal axis and some figures to represent sales per month to be set against the vertical axis.   The two sets of data can conveniently be stored in two separate arrays;  Array "Months" to store the names, Array "Sales" to hold the figures.   As mentioned above, this is simply a convenience to enable us to get on with the business of drawing without getting too bogged down with data handling.

     Inside the "Public Class LineChartDemo" class code, enter the following code which creates the two arrays and populates them with the demo data in one step:

    Dim Months() As String = {"Jan", "Feb", "Mar", "Apr", "May", "Jun", _
    "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"}
Dim Sales() As Integer = {835, 314, 672, 429, 715, 642, _
    153, 699, 622, 901, 345, 655}


    Much of the fiddly setting up of a chart involves deciding or calculating margins and lengths of lines, etc.   Some of these can be hard coded at design time - items such as the number of pixels to inset the start of the graph lines.   Others are better calculated dynamically -  things like the length of the baseline which can grow or shrink as the containing form's size is changed.

   The following code either sets up the hard coded values of variables or creates variables ready to be assigned appropriate values later when the application is run.   Place it below the code that instantiated the arrays.  

    '  ~~  Variables for margins and outlines, etc ~~
    '  # of pixels Y-Axis is inset from PicBox Left
    Dim LeftMargin As Integer = 35
    '  # of Pixels left unused at right side of PicBox
    Dim RightMargin As Integer = 15
    '  # of pixels above base of picturebox the X-Axis is placed
    Dim BaseMargin As Integer = 35
    '  Margin at Top
    Dim TopMargin As Integer = 10
    '  Variable to hold length of vertical axis
    Dim VertLineLength As Integer
    '  Variable to hold length of horizontal axis
    Dim BaseLineLength As Integer
    '  Variable to store length of each line segment
    Dim LineWidth As Double

    As in earlier examples, we are going to use the double buffering technique to create the chart in the background and "paste" it into the PictureBox once it is all ready and good to go.    Of course, we don't actually paste it in the traditional sense; what we do is to assign the completed chart drawing as the image for the PictureBox.  If you want to review how this works, you can find further explanation on Page 3 of Part 2 of this series. 

   We will create the Graphics object and Bitmap object in the next lines of code:

    '  ~~  Bitmap and Graphics Objects  ~~
    '  A variable to hold a Graphics object

    Dim g As Graphics
    '    Next, create a Bitmap object which is
    '    the same size and resolution as the PictureBox

    Dim bmap As Bitmap

    With that code all in place, we are ready to move on to the business of actually drawing the chart.   We will break the tasks into a series of procedures.   If you decide to recycle the sample code for your own purposes later, you will find it quite easy to make the necessary changes and can improve the procedures by passing parameters in place of some of the hard coded values I have used for demonstration purposes.



The Chart Outline

The Chart Outline

   We begin by drawing the X and Y Axis of the chart, including tick marks and numerals on the vertical axis.  This code is all contained in a procedure named DrawOutline.

Graphics Object

  If you have read the previous articles you will recall that we can think of a Graphics Object as a kind of transparent canvas on which we are able to draw directly.     The code below allows us to draw on the Graphics object of the Bitmap.   Later, we will take the completed Bitmap and assign it to the PictureBox control; the end result being that the user will see the drawn chart inside the PictureBox. 

 Private Sub DrawOutline()

   ' Instantiate bmap and set its width and height to that of the PictureBox.
   ' Also set the bitmap's resolution to that of the PictureBox

   bmap = New Bitmap(PBLineChart.Width, _ PBLineChart.Height, PBLineChart.CreateGraphics)
    '    Assign the Bitmap to the Graphics object.  

    g = Graphics.FromImage(bmap)

Vertical Axis

   The code for the vertical axis is the same as that we have used in some of the previous articles.  We draw a simple vertical line and then insert 10 small horizontal tick marks  evenly spaced up its length.  For simplicity, we again use an arbitrary minimum-maximum range of 0 to 1000.   If you need to incorporate a different specific range of values (and in many cases you will) then you can see a description of how to do this in the Part 4 article here.

   First thing is to calculate the start and end points for the vertical line.   You will see that we do this by making use of those variables we  set up earlier for the margins. 

  '   Draw a line for the Vertical Axis.
        Dim StartPoint As New Point(LeftMargin, PBLineChart.Height - BaseMargin)
        Dim EndPoint As New Point(LeftMargin, TopMargin)

        '  Basic Pen to draw outline lines
        Dim LinePen As New Pen(Color.Black, 2)
        '  Draw the vertical line
        g.DrawLine(LinePen, StartPoint, EndPoint)

    To add the tickmarks and the values (100, 200, etc to 1000), use the following code. 

     Dim StartPoint As New Point(LeftMargin, PBBarChart.Height - BaseMargin)
   Dim EndPoint As New Point(LeftMargin, TopMargin)

   The names of the variables StartPoint and EndPoint need no explanation but the concept of a Point might be worth a mention.  We are all familiar with the idea of a point – that is, a specific location which can be identified in some way.  In .NET graphics, a Point is identified by its X value (how far it is from the left hand side) and its Y value (how far down it is from the top).  

  In the current project we measure these two values in pixels, because that is the unit used to track positions on the screen.   It is important to understand that the Point isn’t just a position in space, it is in fact an Object, complete with its own properties and methods.

   You see that the StartPoint Point has its x position set in line with the left margin we set earlier;  its y position is calculated by moving up from the bottom of the picture box by the distance equal to the BaseMargin.

   The EndPoint of the axis has an x value the same as the StartPoint, as you would expect (it is a vertical line, after all).  Its y value is set to a point which is shy of the top of the PictureBox by the value of the TopMargin.

   Armed with our start and end points, we take a pen and draw the line: 

    Dim LinePen As New Pen(Color.Black, 2)
    g.DrawLine(LinePen, StartPoint, EndPoint)

   If you are entering the code as you read the article, (and you haven't read the previous articles) you may be wondering why you haven't seen anything appear in the PictureBox.   The reason is that we have drawn the line on an object that isn’t visible yet to the user.   You will recall that we have created a bitmap object the same size as the picturebox and behind the scenes we are drawing on this bitmap.  (Actually,  to be technically correct - as explained earlier - we are drawing on the GraphicsObject of the Bitmap, but the net effect is the same).   At this stage though we haven't assigned this bitmap back into the PictureBox as its image, so the PictureBox remains empty for the time being.

Tick Marks

  To space out the 10 Tick marks correctly we need to know the length of the vertical axis:

    Dim VertLineLength As Integer = PBBarChart.Height - (BaseMargin + TopMargin)

 This will enable us to calculate the correct gap between each of the Ticks.  The layout is from  0 to 1000 in intervals of 100, remember:

   Dim VertGap As Integer = CInt(VertLineLength / 10)
    I have set the horizontal length of each Tick mark as 5 pixels.   It follows then that the start point of each Tick must be 5 pixels to the left of the vertical axis.   The end point of each Tick will be on the vertical axis itself.  Those are the X positions needed and are the same for each Tick.

   The Y positions (which you will recall are the number of pixels counting down from the top) must change for each Tick mark, because we are drawing ten of them equally spaced all the way up the vertical axis.   The spacing between them therefore must be the value we have just calculated, i.e. the value of VertGap.

  Dim TickSP As New Point(LeftMargin - 5, StartPoint.Y - VertGap)
  Dim TickEP As New Point(LeftMargin, StartPoint.Y - VertGap)

 We have all the information needed to create the Tick marks and the 100, 200, 300, etc text.   Pick a Font for the text:

  Dim ValueFont As New Font("Arial", 8, FontStyle.Regular)
 And now we can loop 10 times through a code block which will draw out Tick marks from bottom to top and add the 100s as text at the same time:

  For i As Integer = 1 To 10
   '  Tick mark
   g.DrawLine(New Pen(Color.Black), TickSP, TickEP)
   '  Tick Values as text
   g.DrawString(CStr(i * 100), ValueFont, Brushes.Black, 2, TickSP.Y - 5)
   '  Reset y positions, moving 10% up vertical line
   TickSP.Y -= VertGap
   TickEP.Y -= VertGap

  The only code which might need additional explanation is the one which uses DrawString to draw the text next to the Tick marks.   You may have noticed that the y-position of the start of the text has been shifted 5 pixels higher than the start of the Tick mark itself (TickSP.Y – 5).   This is simply because the X,Y position (the point of origin) of the drawn string is the very top left point of the imaginary rectangle in which the text is drawn.  So, to bring the middle of the text approximately in line with the tick mark, that point of origin has been nudged five pixels further up the page.

Horizontal Axis (a.k.a. The Base Line)

     The code for the Horizontal (X) Axis looks awkward but is actually quite straightforward.  Using the Pen we created earlier (LinePen) we draw a straight line, offsetting the start and end points of the line by values calculated from the the margins we set earlier. 

  g.DrawLine(LinePen, LeftMargin, PBBarChart.Height - BaseMargin, _
     PBBarChart.Width - RightMargin, PBBarChart.Height - BaseMargin)

What Do You Think Of The Show So Far?

   If you are new to all this, you may want some reassurance that all this code is actually going to work.   As a temporary test, try the following.

  •  Ensure that the above line of code is followed by the "End Sub" line of the DrawOutline procedure.
  •  Create a new procedure as shown below:

    Private Sub TempDisplay()
        PBLineChart.Image = bmap
    End Sub

   The above code takes our chart drawing (or what there is of it so far) and assigns it as the image of the PictureBox.   This means that the chart outline will then be visible to you when you run this small procedure.    To run it, put the following code in the Click event of the button you placed on the form earlier:

    Private Sub btnDraw_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btnDraw.Click
End Sub 

  Save the project and then run it.   If you have entered the code correctly, you will be rewarded with something like this:

   While running the project, try resizing the form and then immediately click the button again.   You will see that the chart outline resizes correctly to fill the available space in the resized PictureBox.  



Drawing The Line(s)

Calculate Lengths

   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)

        g.ScaleTransform(-1, 1)

        ' 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)
        g.DrawPath(MyPen, MyPath)

   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
    End Sub

   Run the project and click the button.   You should now see the chart line that represents the Sales figures.


Month Names

Display The Months

    To be useful and usable we need to show the names of the months that the sales figures represent.   To do this we employ the DrawString method in the same way that we have done in previous articles.   That is, we "write" the text by drawing it with a brush.  It's an odd way of going about the task of writing to the human mind, but perfectly logical to a computer;  letters of the alphabet are after all just shapes - drawings that we happen to be able to translate in our heads into something meaningful.  

   First, we fix the start point for the first letter of the first word.   This is 18 pixels in from the left margin.   Then we create a Brush to draw with and also set out the details of the Font that we want to be used.

   '  Set the start point of the first string
        Dim TextStartX As Integer = CInt(LeftMargin + 18)

        '  Create a Brush to draw the text
        Dim TextBrsh As Brush = New SolidBrush(Color.Black)
        '  Create a Font object instance for text display
        Dim TextFont As New Font("Arial", 10, FontStyle.Regular)

   We're all set now and can go ahead and draw the month names.  Those names were stored in the Months array right at the start of this project, you may remember:

        For i As Integer = 0 To Months.Length - 1
            '  Draw the name of the month
            g.DrawString(Months(i), TextFont, TextBrsh, TextStartX, _
                CInt(PBLineChart.Height - (BaseMargin - 4)))
            '  Move start point for next name along to the right
            TextStartX += CInt(LineWidth)

   Now that we have finished with the Brush and the Font objects we should dispose of them as this is good housekeeping practice.   It's not critical in a tiny project like this one, but is a good habit to get into.


Display The Result

    As we did at the end of the previous pages, we can now display the results of our efforts.   However, as we are no longer working with a temporary display, we will create a small procedure that

  •  Displays the finished chart
  •  Disposes of the Graphics Object

   Here is the procedure:

       Private Sub FinalDisplay()
           PBLineChart.Image = bmap
       End Sub

   And finally we can list the procedures we need to call in turn from the button click event:

    Private Sub btnDraw_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btnDraw.Click
    End Sub



   All done .... or are we?  

   The chart above is OK, but not much more than that.   How much better it would be if the joins at the line segments were more clearly marked.   And if there were grid lines in the body of the chart to make it easier to see the values on the vertical axis.   Shall we add these improvements to the chart?

   You bet!





Line Joins and Grid Lines

Line Joins

   The power of the GraphicsPath now becomes more obvious.   In order to add a small circle - or rectangle, square or even text - in between each line segment we only need to make three tiny tweaks to the code.  The first tweak adds the initial circle at the very start of the chart line; the second tweak is inserted into the loop so that a small circle is added to the start of each subsequent line segment;  the third tweak pops a circle on the end of the final line segment.

   Check out the following code snippet, which forms part of the original DrawTheLine procedure.   I have highlighted in red the three new lines.   All other code in that procedure remains unchanged.

 '  Create a GraphicsPath to hold the line info
        Dim MyPath As New GraphicsPath

        ' Manually add the first circle to the path
        MyPath.AddEllipse(XPosStart - 2, YPosStart - 2, 4, 4)

        '  Manually add the first line to the Path
        MyPath.AddLine(XPosStart, YPosStart, XPosEnd, YPosEnd)
            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 next circle
  MyPath.AddEllipse(XPosStart - 2, YPosStart - 2, 4, 4)
                '  Add the next line segment to the GraphicsPath
                MyPath.AddLine(XPosStart, YPosStart, XPosEnd, YPosEnd)

            '  Finally, manually add the last circle
  MyPath.AddEllipse(XPosEnd - 2, YPosEnd - 2, 4, 4)

   The AddEllipse method adds a circle with a diameter of 4 pixels to the GraphicsPath.   The X and Y positions of the circle are offset by a value of 2 pixels so that it is placed midway between the end of one line segment and the start of the next.

  You can change the circles into squares by replacing each AddEllipse line with this:

          MyPath.AddRectangle(New Rectangle(XPosEnd - 2, YPosEnd - 2, 4, 4))

Grid Lines

   Although by definition a grid comprises both horizontal and vertical lines, I have chosen to create two separate procedures, one for the horizontal grid lines and the other for the verticals.   You can then decide if you want both sets of gridlines to be used.  In a very small PictureBox, having both sets of lines can be distracting and you might want to give the user the choice of horizontal only, vertical only, both or none.  

Horizontal Grid Lines

   Based on what we have covered so far in this article, I hope you will find the following procedure, which draws a set of horizontal grid lines, fairly easy to follow:

   Private Sub DrawHorizontalLines()
        '  Calculate vertically equal gaps
        Dim VertGap As Integer = CInt(VertLineLength / 10)

        '  Set the Start and End points
        '  = Left and right margins on the baseline
        Dim StartPoint As New Point(LeftMargin + 3, PBLineChart.Height - BaseMargin)
        Dim EndPoint As New Point(PBLineChart.Width, PBLineChart.Height - BaseMargin)

        '  Initial settings
        Dim LineStart As New Point(StartPoint.X, StartPoint.Y - VertGap)
        Dim LineEnd As New Point(EndPoint.X, StartPoint.Y - VertGap)

        Dim ThinPen As New Pen(Color.LightGray, 1)

        For i As Integer = 1 To 10
            '  Draw a line
            g.DrawLine(ThinPen, LineStart, LineEnd)

            '  Reset Start and End Y positions, moving up the vertical line
            LineStart.Y -= VertGap
            LineEnd.Y -= VertGap


    End Sub

   The procedure is very similar to that which we used to create the tickmarks on the vertical axis.  In fact, as you can probably tell, I've simply recycled that code, changing the start and end points, omitting the text and using a thinner pen in a light color.

Vertical Grid Lines

   In the same way, I have used the code that draws the Month names and tweaked it so that it draws vertical lines all the way across the chart:

 Private Sub DrawVerticalGridLines()
        Dim ThinPen As New Pen(Color.Bisque, 1)

        '  Calculate length of baseline drawn by the code above
        BaseLineLength = PBLineChart.Width - (LeftMargin + RightMargin)
        '  Calculate the width of each line segment
        LineWidth = (BaseLineLength / Sales.Length)

        '  Set the start point of the first string
        Dim LineStartX As Integer = CInt(LeftMargin + 30)

        For i As Integer = 0 To Months.Length - 1
            g.DrawLine(ThinPen, LineStartX, TopMargin, LineStartX, PBLineChart.Height - (BaseMargin + 4))

            '  Move start point along to the right
            LineStartX += CInt(LineWidth)


    End Sub

   The only substantial difference being the replacement of DrawString with DrawLine, assigned with appropriate parameter values.

Final Display

   Add a call to these two procedures to the button click event.

    Private Sub btnDraw_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btnDraw.Click
    End Sub

   Note that the gridlines are drawn before the actual Sales figures line is drawn.   This is important, otherwise the gridlines will tend to erase the sales line as it passes over it and makes it look unfinished.

   But finished we are!   A fairly basic but usable Line Chart, which you can alter or enhance in many ways as needed.



   You may have been surprised by how much code you have to write to create this rather basic looking chart.   As with so many things when it comes to transferring ideas from paper to screen, things are not always as straightforward as they seem.  That good idea that takes 30 seconds to plan out in your mind, ends up taking 30 hours of hard work to bring to life - ah, the joys of programming!


 In this article we covered or used the following :

  • Bitmap
  • Brush
  • CreateGraphics
  • Double Buffering
  • DrawLine
  • DrawString
  • Font
  • GraphicsPath
  • Graphics Object
  • Pen
  • Point
  • Scaling
  • Transformations
  • X position
  • Y position