DevCity.NET - http://devcity.net
Chart Success : GDI+ Graphics At Work Part 4
http://devcity.net/Articles/154/1/article.aspx
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 5/10/2005
 

 Back in Part 2 of this series we created a basic Bar Chart.  It did the job, but - let’s face it - it isn’t likely to win any prizes in the “GUI of the Year “ competition.

  So, in this article we will use the same basic approach, but will build a better  3D bar chart to give the display more depth, color and hopefully, as a result, more impact.

 


Setting Up

Data Source 

 First, let’s generate some data to display via the chart.   We will create it at design time  to allow us to get on with the core Graphics Class parts of the project.  If you prefer to have the data input by the user at run time then you can adapt the techniques covered in Part 3.

 

Create Data

  Shown below is what has become  our standard code  to create initial variables and to generate  and store some data.   I won’t trundle through the details as they are fully covered in earlier articles:

 


Option Strict On


Imports System.Drawing.Drawing2D
Imports System.Collections

 

Structure GraphData
        Dim Country As String
        Dim Sales As Integer
        Dim BarColor As Color
        Sub New(ByVal country As String, ByVal sales As Short, ByVal barcol As Color)
            Me.Country = country
            Me.Sales = sales
            Me.BarColor = barcol
        End Sub
    End Structure

 

'  Create Variables

‘ # of pixels vertical Axis is inset from PictureBox Left
    Dim LeftMargin As Integer = 35
    '  # of Pixels left unused at right side of PictureBox
    Dim RightMargin As Integer = 15
    '  The number of pixels above bottom of baseline
    Dim BaseMargin As Integer = 35
    '  Margin at Top
    Dim TopMargin As Integer = 10
    '   Set size of gap between each bar
    Dim BarGap As Integer = 12

    Dim SalesData As New ArrayList 
    Dim HighSale As Double  ' Maximum sales figure
    Dim VertScale As Double  ' Scaling used for bar heights

  '   Minor changes for this version:
  '   Declare a variable to hold a Graphics object
        Dim g As Graphics
  '  Variable for Bitmap to be displayed in (PictureBox) control
    Dim bmap As Bitmap
  '  Length of vertical axis
    Dim VertLineLength As Integer
Dim BarWidth As Integer  ' width of bars
Dim BaseLineLength As Integer  ' X Axis length

Private Sub GetData()
        SalesData.Clear() ' Avoid data duplication
        '  Generate some data and store it in the arraylist
        SalesData.Add(New GraphData("Belgium", 934, Color.Blue))
        SalesData.Add(New GraphData("Greece", 385, Color.DarkOrange))
        SalesData.Add(New GraphData("Portugal", 1029, Color.Green))
        SalesData.Add(New GraphData("Spain", 729, Color.IndianRed))
        SalesData.Add(New GraphData("Turkey", 1472, Color.Tomato))
        SalesData.Add(New GraphData("UK", 1142, Color.Aquamarine))
End Sub


 

Controls

   Add a PictureBox named PBBarChart and a Button named btnDraw to the Form. 

 

  Place the button somewhere down in the bottom corner of the Form.

 

   Stretch the PictureBox so that it is close to the top, right and left edges of the form.   Set its Anchor property so that it is anchored to all four sides.  Doing this will allow the user to click the button for a resized version of the chart to be displayed whenever the form size is altered .   (As we will see in later articles, there are more dynamic ways of achieving this, but we’ll stick with this method for now.)

 

Draw Vertical Axis

Graphics Object and Bitmap
Next, we set up Graphics and Bitmap objects.   Put this initialising code in a separate procedure named GetGraphics:


Private Function GetGraphics() As Graphics
        '  Make bmap the same size and resolution as the PictureBox
        bmap = New Bitmap(PBBarChart.Width, PBBarChart.Height, PBBarChart.CreateGraphics)
        '   Assign the Bitmap object to the Graphics object
        '   and return it
        Return Graphics.FromImage(bmap)
    End Function


Vertical Axis
  If you have read the previous articles, you may notice that the various stages are now being broken up into smaller code chunks and put into separate procedures.

   The code for the vertical axis is now placed in its own procedure and remains largely unchanged from previous versions.  The first part looks like this:


Private Sub DrawVerticalAxis(ByVal g As Graphics)
'   Draw a line for the Vertical Axis.
        Dim StartPoint As New Point(LeftMargin, PBBarChart.Height - BaseMargin)
        Dim EndPoint As New Point(LeftMargin, TopMargin)
        '  Basic Pen
        Dim LinePen As New Pen(Color.Black, 2)
        '  Draw the vertical line (without tick marks)
        g.DrawLine(LinePen, StartPoint, EndPoint)

        '  Draw the Tickmarks and Display Numbers
        '   Calculate length of the vertical axis
        VertLineLength = PBBarChart.Height - (BaseMargin + TopMargin)

'  Identify the highest sales figure
        For Each gd As GraphData In SalesData
            If gd.Sales > HighSale Then HighSale = gd.Sales
        Next
' :  Scaled Tick Marks code follows


Scaled Tick Marks
   Here’s a change from the earlier article.    In Part 2,  we fixed the maximum number of sales at a figure of 1000.  Not very realistic, but helped to keep the code less complicated.    This time round we will adjust the maximum sales figure (and therefore the number and the spacing of tick marks) in accordance with whatever value is held in the HighSale variable above.

  This means that you can change the sales data to  allow a total sales value of more than 1000 at any time in the future and your graph won’t become distorted.

   To do this, we identify the highest sales figure, then round up until we get to the next round value of 100.  For example,  if the HighSale figure is 1675 then the maximum value on the scale will be set to 1700.

  Here is the code that does this:

' DrawVerticalAxis procedure continued:
     ' Round up to next hundred above highest sales figure
        Dim NextCent As Integer = CInt(HighSale)
        Do While NextCent Mod 100 <> 0
            NextCent += 1
        Loop

   '  Identify how many TickMarks required (one per hundred):
        Dim TotalTicks As Integer = CInt(NextCent / 100)

  Now we know how many hundreds we have to allow for, we can divide the vertical axis proportionately into 100s, draw the tick marks and the values as text, just as we did in Part 2:

' Calc gaps between vertical tick marks
        Dim YPos As Integer = CInt(VertLineLength / TotalTicks)
        '  Variables for Start and End Points of Tick Marks
        Dim TickSP As New Point(LeftMargin - 5, StartPoint.Y - YPos)
        Dim TickEP As New Point(LeftMargin, StartPoint.Y - YPos)
        '  Font for values  - declared here for readability
        Dim ValueFont As New Font("Arial", 8, FontStyle.Regular)

        For i As Integer = 1 To TotalTicks
            g.DrawLine(New Pen(Color.Black), TickSP, TickEP)   '  Tick mark
            '  Tick Values as text :
            g.DrawString(CStr(i * 100), ValueFont, Brushes.Black, 2, TickSP.Y - 5)
            '  Resetx, y positions, proportionately up vertical line
            TickSP.Y = CInt(TickSP.Y - (VertLineLength / TotalTicks))
            TickEP.Y -= CInt(VertLineLength / TotalTicks)
        Next
End Sub

 

 

Testing 1, 2, 3

   Well, to be strictly accurate, it’s more a case of “Testing 1, 2, ... 1500”.    If at this stage you want to see if the code so far is working, put the next code snippet in the btnDraw’s Click event and then click the button.  

   Private Sub btnDraw_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btnDraw.Click
        '  The various actions are now split into separate procedures to make
        '  the project more modular

        '  1.  Get the data
        GetData()
        '  2.  Get a Graphics object to use for the drawing methods
        g = GetGraphics()
        '  3.   Draw the Vertical Axis
        DrawVerticalAxis(g)

        '  :  More code to come ....


        ' n.   Assign the bitmap with the vertical axis drawn to the PictureBox
        PBBarChart.Image = bmap
    End Sub

  As you will see, the Tick Marks now range from 100 to 1500, reflecting the current highest sales figure of 1472 (Turkey).
You can tweak these values, run the project again and you will see that the vertical axis adjusts accordingly.

 

 

Draw The Horizontals

  Baseline (The X Axis)
   You can include a horizontal axis if you like the look of it.    Create a new procedure named Draw3DBars.


Private Sub Draw3DBars()
        g.DrawLine(New Pen(Color.Black), LeftMargin, PBBarChart.Height - BaseMargin, _
             PBBarChart.Width - RightMargin, PBBarChart.Height - BaseMargin)
        '  Calculate length of baseline drawn by the code above
BaseLineLength = pbBarChart.Width - (LeftMargin + RightMargin)

'  More code to follow

3D Bars
  
    I came across a C# method for building up bars with a 3D effect on codeproject.com.  The author,  Michael Damron, had used the simple but effective idea of building a series of  polygons one above the other. 

 Topping the bar off with a lighter coloured polygon completes the effect.     So I’ve re-jigged his original basic idea for this project.

 Here’s the way I’ve gone about it:   The first few code items deal with some of the core calculations required.   For example, we can calculate the optimum width of each bar by using the total width available and dividing it by the number of bars; not forgetting to take into account the required gaps between each bar.

   Most of these are the same as were used in Part 2:

' Calculate width of each bar making full use of space available
BarWidth = CInt((BaseLineLength / SalesData.Count) - BarGap)            '  Set the start point for the first bar
  Dim BarStartX As Integer = LeftMargin + BarGap
            '  Set the baseline of the graph
            Dim BaseLine As Integer = PBBarChart.Height - BaseMargin
 '  Calc scaling  (new in this project)
            VertScale = VertLineLength / HighSale

   '  : More code to follow

 - the only new item being the VertScale variable, which is used to ensure that the bars are drawn to the correct height, allowing for the scaling we applied in the DrawVerticalAxis procedure earlier.
  This procedure continues with the following block of code which draws each of the bars in turn.   I have added quite a lot of commenting and (especially if you have read any of the previous articles) it should all make sense to you.


  For Each gd As GraphData In SalesData
 '  Set the positions of the four points of the bottom-most
 '  parallelogram and store in an array
    Dim Corners(3) As Point
     Corners(0) = New Point(BarStartX, BaseLine - 10)
     Corners(1) = New Point(BarStartX + BarWidth - 5, BaseLine - 10)
     Corners(2) = New Point(BarStartX + BarWidth, BaseLine)
     Corners(3) = New Point(BarStartX + 5, BaseLine)

'  Calculate the height of this bar, taking scale into account:
      Dim BarHeight As Integer = CInt(gd.Sales * VertScale)

'  Create brushes to draw the bars
'  Colors will change according to settings in GraphData
    Dim barmainBrush As New HatchBrush(HatchStyle.Percent50, gd.BarColor)
    Dim bartopBrush As New SolidBrush(gd.BarColor)

 '  Draw one complete bar
     For i As Integer = 0 To BarHeight - 11
‘  (“BarHeight - 11” might be confusing.   This makes allowance for
‘  a.   the 10 pixels which are added to create the 3D depth effect
‘  plus
‘  b.  the final rhombus is to be drawn in a lighter color, so we
‘  need to stop drawing these hatched ones 1 pixel below the
‘  total bar height.

 '  Fill next polygon using the hatchbrush
      g.FillPolygon(barmainBrush, Corners)
 '  Move  all the Y positions up the picture box by 1 pixel
      Corners(0).Y -= 1
      Corners(1).Y -= 1
      Corners(2).Y -= 1
      Corners(3).Y -= 1
     Next
  '  Finally, top it off with a lighter rhombus
      g.FillPolygon(bartopBrush, Corners)

  '  Move the startpoint for the next bar
       BarStartX += CInt(BarWidth + BarGap)

‘   Dispose of brushes
       barmainBrush.Dispose()
       bartopBrush.Dispose()
  Next
End Sub


  (If you want to test the above procedure, don't forget to add a call to it in the btnDraw_Click event. )

 

Finishing Touches

  Just to finish it off, the names of the countries are written below each bar.

  This is very similar to what we did in Part 2, with the addition of a code line that adjusts the font size according to the width available.

   Private Sub WriteTheNames()
        '  X position for start of country name(s).  It is placed
        '  under the left edge of the bar, plus 5 pixels for better look
        Dim TextStartX As Integer = LeftMargin + BarGap + 5

        '  Create a Brush to draw the text
        Dim TextBrsh As Brush = New SolidBrush(Color.Black)
        '  Create a Font object instance for text display
        '  dynamically adjusted font size would be useful:
        Dim fntSize As Integer = CInt(BarWidth / 7)
        Dim TextFont As New Font("Verdana", fntSize, FontStyle.Bold)
        '   Write them:
        For Each gd As GraphData In SalesData
            g.DrawString(gd.Country, TextFont, TextBrsh, TextStartX, CInt(PBBarChart.Height - (BaseMargin - 4)))
            TextStartX += CInt(BarWidth + BarGap)
        Next
    End Sub

Again, you will need to add a call to this procedure in the Draw Button’s click event.  The complete code for that event would therefore look like this:

  Private Sub btnDraw_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btnDraw.Click
        '  The various actions are now split into separate procedures to make
        '  the project more modular

        '  1.  Get the data
        GetData()
        '  2.  Get a Graphics object
        g = GetGraphics()
        '  3.   Draw the Vertical Axis
        DrawVerticalAxis(g)
        '  4.  Draw bars
        Draw3DBars()
        '  5.  Write the titles
        WriteTheNames()
        ' All Done!   Assign the bitmap with the vertical axis drawn to the picturebox
        pbBarChart.Image = bmap
  g.Dispose()
    End Sub

 

Summary

  This project creates a more versatile and better looking bar chart.   It splits the main drawing tasks into separate procedures.  However, the code is still rather unwieldy and this makes a strong case for  creating a  3DBar class with settable properties for width, depth, height, color, etc.   It would then be possible to include  properties well beyond the UI choices we have used here, and of course it conforms more closely to the OOP approach to developing in VB.NET.

In this article I explained or used the following:

  •  Bitmap object
  •  Brushes
  •  Dispose
  •  Do While Loop
  •  DrawLine
  •  DrawString
  •  FillPolygon
  •  Font object
  •  Graphics Object
  •  Pen
  •  Structure

   In the next article in this series, we will move on to another, sometimes more dynamic, type of chart - the line chart.