iNET Interactive - Online Advertising Agency
          
Go Back  Xtreme Visual Basic Talk > Legacy Visual Basic (VB 4/5/6) > Knowledge Base > Tutors' Corner > How to Create a Line Graph


Reply
 
Thread Tools Display Modes
  #1  
Old 12-10-2003, 05:32 PM
BillSoo's Avatar
BillSoo BillSoo is offline
Code Meister
Retired Moderator
* Guru *
 
Join Date: Aug 2000
Location: Vancouver, BC, Canada
Posts: 10,441
Default How to Create a Line Graph

When it comes to graphing data, there are a lot of controls out there that can do a good job. Unfortunately, most of these controls come with a pretty steep price. Cheaper ones, like the MSChart control, can work as well but have limited utility.

The problem is that there is no "one way" to draw a graph. There are line graphs, pie charts, 3D charts, scatter plots and hundreds more. With so many options, you would most likely have to buy one of the more expensive controls to get the features you want since these can be customized to greatest. In the process, you also buy all the graph types you will never use as well. In addition, learning to access the various features can be a trial....the features you want are lost against the noise of all the features you don't need.

Ideally, you could buy a graph control that had only the features you want, and no more. Good luck....

This tutorial covers the process of designing your OWN graphing routines from scratch. In it you will see that graphing isn't that difficult as long as you are aware of basic principles.

Since there are so many options and since this is supposed to be a SIMPLE tutorial, let's lay the limits of this application.

1) Draws a line graph
2) Supports multiple lines
3) Supports multiple Axes
4) Uses a single X axis based on Time
5) The time interval between data points is constant (not a scatter graph)
6) Supports Zooming
7) Can draw to any Device Context so it can support printing as well as screen display. With a little extra code, you can save to file as well by using a WMF format.
8) All data points are stored in memory (if you have LOTS of data points, you will need to develop routines to read from disk)

The layout of the graph is as follows:
Attached Images
File Type: jpg GraphAreas.jpg (11.3 KB, 944 views)
__________________
"I have a plan so cunning you could put a tail on it and call it a weasel!" - Edmund Blackadder
Reply With Quote
  #2  
Old 12-10-2003, 05:33 PM
BillSoo's Avatar
BillSoo BillSoo is offline
Code Meister
Retired Moderator
* Guru *
 
Join Date: Aug 2000
Location: Vancouver, BC, Canada
Posts: 10,441
Default

The graph starts within a specified rectangle R. The first thing we do is draw the title. After we draw the title, we SUBTRACT this area from R. Now we draw the X axis time scale. Again, we subtract this area from R. Now we draw the Axes. For each Axis, we draw it on the leftmost edge of R, then subtract the area from R. The remaining area is now used for drawing the lines.

If we were planning on 2 Y axes only, we could put one on the leftmost edge and one on the rightmost edge.

The trick to drawing the lines lies in differentiating between LOGICAL coordinates and PHYSICAL coordinates.

The logical coordinates are the coordinates of a point in logical units like time and temperature. The physical coordinates are the coordinates of the same point in pixels. We need functions to convert logical coordinates to physical coordinates and vice versa. Consider the following:
Attached Images
File Type: jpg Conversion.jpg (6.9 KB, 593 views)
__________________
"I have a plan so cunning you could put a tail on it and call it a weasel!" - Edmund Blackadder
Reply With Quote
  #3  
Old 12-10-2003, 05:35 PM
BillSoo's Avatar
BillSoo BillSoo is offline
Code Meister
Retired Moderator
* Guru *
 
Join Date: Aug 2000
Location: Vancouver, BC, Canada
Posts: 10,441
Default

In this diagram, the LOGICAL rectangle specified by (xMin,yMax)-(xMax,yMin) is mapped to the PHYSICAL coordinates specifed by r.left, r.right, r.top and r.bottom.

We can convert the logical coordinates x,y to physical coordinates X,Y as follows:

X = (r.right - r.left) * (x - xMin) / (xMax - xMin) + r.left
Y = (r.top - r.bottom) * (y - yMin) / (yMax - yMin) + r.bottom

similarily, to convert from physical coordinates X,Y to logical coordinates x,y:

x = (xMax - xMin) * (X - r.left) / (r.right - r.left) + xMin
y = (yMax - yMin) * (Y - r.bottom) / (r.top - r.bottom) + yMin

Note that these equations work even if r.top < r.bottom (as in most screen coordinate systems).

Now that we have a basic understanding of graphing theory, let's examine some code:

I've written the code as a BAS module that contains all the graphing functions. It could easily be rewritten as an ActiveX DLL or OCX.

The first step is to declare your API functions and type declarations. These are used instead of their VB equivalents because they can be used with ANY device context whereas VB methods are tied to a specific object type. So instead of having to have separate routines using picturebox.Line vs printer.Line, we can have a standard LineTo function and simply pass either the picturebox.hDC or the printer.hDC to this function.

In addition to the standard types like the RECT structure and POINTAPI structure, we add our own structures:
Code:
Private Type LineType StartTime As Date EndTime As Date nPoints As Integer AxisID As Integer Pen As LOGPEN Data() As Double End Type Private Type AxisType max As Double min As Double name As String units As String End Type
These structures are used to store information about the lines and axes in our graph.

Each line has a start time and finish time and are linked to a specif axis. This gives us the complete LOGICAL coordinate system. The data is stored in an array where each value is a Y value (since we know when we start and when we end, plus how many points we have, we can calculate the corresponding X coordinate). Finally we have a LOGPEN structure to contain the information about the pen we will use to actually draw the graph.

Each axis has a maximum value and a minimum value. We also add supplementary information like name and units.

Next we add our module level variables:
Code:
Private sTitle As String 'graph title Private nLines As Integer 'number of lines (sets of data) to draw Private aLineData() As LineType '2D array of data Private nAxes As Integer 'number of axes to use Private aAxisData() As AxisType 'array of info on each axis Private MaxTime As Date 'largest timestamp in all datasets Private MinTime As Date 'smallest timestamp in all datasets Private GraphRect As RECT 'the rectangle containing the actual graph lines Private ZoomMin As Date 'current minimum x value Private ZoomMax As Date 'current maximum x value
Since these are declared as PRIVATE, they are local to the module rather than being global. Limiting access in this way makes it easier to convert the module to an ActiveX DLL or OCX.

If you need to access these variables, you can use public functions to do so:
Code:
Public Function GetMinTime() As Date GetMinTime = MinTime End Function Public Function GetMaxTime() As Date GetMaxTime = MaxTime End Function

We now add functions to put data into our axis and line arrays:
Code:
Public Sub AddLine(d() As Double, ByVal n As Integer, ByVal st As Date, ByVal et As Date, Optional ByVal AxisID As Integer = 0, _ Optional ByVal Color As Long = 0, Optional ByVal Style As Long = 0, Optional ByVal Width = 1) 'add a line to be drawn to the array of lines Dim i As Long ReDim Preserve aLineData(nLines) With aLineData(nLines) .AxisID = AxisID .StartTime = st .EndTime = et .nPoints = n .Pen.lopnColor = Color .Pen.lopnStyle = Style .Pen.lopnWidth.x = Width .Pen.lopnWidth.y = Width ReDim .Data(n - 1) For i = 0 To n - 1 .Data(i) = d(i) Next i End With nLines = nLines + 1 If et > MaxTime Then MaxTime = et If st < MinTime Then MinTime = st End Sub Public Function AddAxis(ByVal max As Double, ByVal min As Double, ByVal name As String, ByVal units As String) As Integer 'add new axis to be drawn. Returns the ID of this axis ReDim Preserve aAxisData(nAxes) With aAxisData(nAxes) .max = max .min = min .name = name .units = units End With AddAxis = nAxes nAxes = nAxes + 1 End Function
We also add a function to initialize all the variables and structures.
Code:
Public Sub NewGraph(ByVal Title As String) 'clears any existing data. Sets the title nAxes = 0 nLines = 0 Erase aLineData Erase aAxisData MaxTime = 0 'set maxtime and mintime to impossible values MinTime = Now() + 1 sTitle = Title End Sub

Now that we have the data loaded, we can draw the graph:
Code:
Public Sub DrawGraph(ByVal dc As Long, r As RECT, ByVal st As Date, ByVal et As Date) 'draws the current graph to the device context dc 'within the rectangle specified by r 'the graph starts at st and ends at et Dim te As Size GetTextExtentPoint dc, sTitle, Len(sTitle), te 'get the length/height of the title string TextOut dc, (r.Right - r.Left - te.cx) / 2, 0, sTitle, Len(sTitle) 'print the title centred at the top of the rect r.Top = r.Top + te.cy 'reduce the height of the rect to put the title outsize the graphed area 'draw axes here. Reduce the size of the r rectangle to put the axes outside of the graphed area '....not yet implemented 'save the current rectangle that we draw with GraphRect.Right = r.Right GraphRect.Left = r.Left GraphRect.Top = r.Top GraphRect.Bottom = r.Bottom 'save the current zoom times ZoomMin = st ZoomMax = et 'draw all the lines DrawLines dc, r, st, et End Sub
Basically, the function simply draws each section of the graph, then reduces the rectangle by the amount used for that section. At this time, I haven't written the axis labelling functions. The problem is that these functions are probably the trickiest part of graphing. It isn't easy to automatically determine a set of intervals that look good. Anyway, what is left is passed to the DrawLines function:
Code:
Public Function GetX(ByVal x As Long) As Double 'converts the x physical coordinate to logical coordinate (time) GetX = (x - GraphRect.Left) * (ZoomMax - ZoomMin) / (GraphRect.Right - GraphRect.Left) + ZoomMin End Function Public Function GetY(ByVal y As Long, ByVal axis As Integer) As Double 'converts the y physical coordinate to logical coordinate for that axis GetY = (y - GraphRect.Bottom) * (aAxisData(axis).max - aAxisData(axis).min) / (GraphRect.Top - GraphRect.Bottom) + aAxisData(axis).min End Function Public Sub DrawLines(ByVal dc As Long, r As RECT, ByVal st As Date, ByVal et As Date) 'draws all the lines Dim i As Integer Dim mx As Double Dim mn As Double Dim pt As Long Dim sp As Integer 'start point Dim ep As Integer 'end point Dim dx As Double Dim x As Long Dim y As Long Dim oldPt As POINTAPI Dim xt As Double Dim yt As Double Dim cx As Double Dim cy As Double Dim newpen As Long Dim oldpen As Long Rectangle dc, r.Left, r.Top, r.Right, r.Bottom 'draw border For i = 0 To nLines - 1 'for each line With aLineData(i) 'get the maximum and minimum values for the y axis mx = aAxisData(.AxisID).max mn = aAxisData(.AxisID).min 'create the pen we want for the line newpen = CreatePenIndirect(.Pen) oldpen = SelectObject(dc, newpen) dx = (.EndTime - .StartTime) / .nPoints 'dx is the amount of time between each point sp = (st - .StartTime) / dx - 1 'sp is the starting point If sp < 0 Then sp = 0 'sp can't be less than 0 ep = (et - .StartTime) / dx + 1 'ep is the ending point If ep >= .nPoints Then ep = .nPoints - 1 'ep can't be more than the largest point 'determine the logical coordinates (xt,yt) of the first point (time,units) xt = (sp * dx) + .StartTime yt = .Data(sp) 'cx and cy are conversion factors to convert logical units to physical units (pixels) cx = (r.Right - r.Left) / (et - st) cy = (r.Top - r.Bottom) / (mx - mn) 'calculate the pixel coordinates (x,y) x = (xt - st) * cx + r.Left y = (yt - mn) * cy + r.Bottom MoveToEx dc, x, y, oldPt 'moveto this first point For pt = sp + 1 To ep xt = xt + dx 'increment time by dx yt = .Data(pt) 'get the next yt value 'convert to pixels x = (xt - st) * cx + r.Left y = (yt - mn) * cy + r.Bottom LineTo dc, x, y 'draw the line Next pt 'reset the pen SelectObject dc, oldpen DeleteObject newpen End With Next i End Sub
The drawlines function will draw outside the graphing rectangle if you let it because it has no clipping function. On the screen, it isn't too bad because the picturebox automatically clips the line but if you add axis labels, it could be a problem. There are two general ways to clip a line, you could use a clipping rectangle or you could end the lines at the axis and use interpolation to find the y value. In general, interpolation might be better because not all devices contexts will support clipping rectangles.

That's pretty much it. You use the AddLine, AddAxis and NewGraph functions to setup the graph, and the DrawGraph function to draw it. You can easily zoom simply be adjusting what values you pass to the DrawGraph function.

Here is the complete project that draws to a picturebox and supports zooming.
Attached Files
File Type: zip VBGraph.zip (4.1 KB, 1404 views)
__________________
"I have a plan so cunning you could put a tail on it and call it a weasel!" - Edmund Blackadder

Last edited by Deadalus : 12-11-2003 at 07:38 AM. Reason: Added line break
Reply With Quote
Reply


Currently Active Users Viewing This Thread: 1 (0 members and 1 guests)
 
Thread Tools
Display Modes

Posting Rules
You may not post new threads
You may not post replies
You may not post attachments
You may not edit your posts

vB code is On
Smilies are On
[IMG] code is On
HTML code is Off
Forum Jump

Similar Threads
Thread Thread Starter Forum Replies Last Post
Creating a Line Graph liaaam API 2 12-20-2003 09:43 PM
Drawing inclined squares XVB Interface and Graphics 7 11-14-2003 03:46 AM
Drawing joined parallel line segments bart111 Game Programming 0 03-15-2003 06:32 PM
Create String to be line of code? kkonkle General 8 12-10-2002 02:42 PM
Transfer private controls to another project.. Help! Jazler General 8 07-01-2002 09:59 AM

Advertisement: