Logarithmic Linecharts Component with GDI+/VB.NET
page 1 of 1
Published: 24 May 2006
Unedited - Community Contributed
Abstract
In this article Terry demonstrates how to create a Logarithmic Linecharts Component with Visual Basic .NET. The application will work with Web, Win, Console, Service, or Web Service with only minor or no changes.
by Terry Voss
Feedback
Average Rating: This article has not yet been rated.
Views (Total / Last 10 Days): 10399/ 15

Introduction

I needed a logarithmic plot to work with a web page that allowed me to output .JPG allowing compression if needed, allowed attachment of graph photos to email along with other photos and allowed printing of the jpg inside of various reports.  Using a Log function from System.Math was easy, but understanding how to make the graph paper look good and match the curve exactly was a problem.  Then I saw the following article.

DataPlotter - linear or logarithmic display of 2D data

It had that basic problem solved, but it did not create a .JPG image on disk and was a WinForms control, not a component that could be used in other environments.  I also did not find the documentation that I wanted, thus this article.  Thanks to the author of the above article.

Imports required by the LogPlotter Class

Imports System.Drawing                         holds graphics, bitmap, brush, etc

Imports System.Drawing.drawing2d holds smoothingmode and pentype enumerations

Imports System.Drawing.Imaging              holds imageformat enumeration

Imports System.Math                    holds log function

Imports System.IO                                 holds file class for deleting existing files

Overview

There are two constructors to handle 2 different range requirements that I have.  You could easily add as many as you want here.  My two are: 0-90 w/spacing of ten between markers, versus 0-260 w/spacing of twenty on the linear y-axis.  The properties will be discussed later, if they are not already somewhat apparent from the naming documentation.  Note that the main Method, Render, has 3 inputs.  2 are arrays of doubles and the other is the fullpath to the disk file you want created.  The arrays must have the same length for the code to work.  If these parameters are dynamically or human generated, you may want to place some error code around this.

Listing 1

Public Class LogPlotter
  Sub New()
    _yRangeEnd = 90
    _yGrid = 10
  End Sub
 
  Sub New(ByVal alternateRange As Boolean)
    _yRangeEnd = 260
    _yGrid = 20
  End Sub
 
Private Declarations
 
Public Properties
 
  Public Sub Render(ByVal xData() As Double, ByValyData() As DoubleByVal filename As String)
 
  Private Sub DrawVerticalLines(ByVal g As Graphics)
 
  Private Function LargeFormat(ByVal value AsString) As String
 
  Private Sub DrawHorizontalLines(ByVal g AsGraphics)
 
  Private Sub DrawData(ByVal g As Graphics, ByValxdata() As DoubleByVal ydata() As Double)
 
End Class

The LargeFormat Method above is so that 10000000 prints as 10M = 10 Million, 10000 prints as 10K = 10 thousand, etc.  Since my main challenge has been getting the grid paper that a chart prints to exactly match the data plotting with GDI+ charting, you see that I have focused on those procedures for easier debugging.

Let's discuss the Render Method first

First, I will explain the class and then at the end I will give an example regarding how I have used the class. The Render Method is the main method of the LogPlotter Class.  Create a bitmap first in any physical size of jpg, gif or bmp that you might want to display or print.  I chose 300 height by 600 width because the x axis goes from .01 to 10 million and the 300 fits my form for displaying and for printing.  

Create a graphics object from the bitmap because that is what we can draw on. Set SmoothingMode Property to enumeration and SmoothingMode to value AntiAlias, to smooth the looks.  Create a rectangle on the graphic so we can color the background with the FillRectangle Method.  Create x and y axis start end variables using border Properties that are set large enough to hold the descriptive labels that you want to put on the chart.  Drawing the Vertical lines will be handled separately because with a log the increment, as you go right on the x axis it will not be linear as we are familiar with, but will actually increment faster and faster as we go right.  We need vertical lines that can help us actually test that our data will hit the proper lines on the graph.

The variable w0 is the ClientRectangle width adjusted by the right and left borders.  This width must be adjusted to w1, which can be a little different than w0, due to rounding errors in dividing the width by an odd number of divisions into an odd distance between lines.  It is the same with h1 and h0.  If you do not understand this, run the code with odd demarcations like 27 and with the distance between the lines being 17 versus 10.  Trace the code to see how h1 becomes a little different than h0.  If we did not adjust for this, the plotted data could, for example, plot outside the grid.  The variable "d" is the distance between lines and "n" is the number of demarcations on the axis in both cases of x and y.  We will study the horizontal lines which in this code are linear, not logarithmic.  You can put something besides base 10 logging, which is the most commonly used logging.  If you need that for a particular graph, for example if you wanted to graph photon travel upon explosions, you might need a more extreme compression of dimensions.  

Now we draw the rectangle around the graph lines.  DrawData Method is the last major routine and will be discussed later.  We save as .JPEG next.  Disposes are more important here than in most coding, since we are working with physical resources that might block another operation done later.

Listing 2

Public Sub Render(ByVal xData() As Double, ByValyData() As Double, _
                  ByVal filename As String)
  Dim outputBitmap As New Bitmap(600, 300)
  Dim g As Graphics = Graphics.FromImage(outputBitmap)
  g.SmoothingMode = SmoothingMode.AntiAlias
  Dim clientRectangle As New RectangleF(0, 0, 600,300)
  x0 = clientRectangle.Left + BorderLeft
  y0 = clientRectangle.Top + BorderTop
  w0 = clientRectangle.Width - BorderLeft -BorderRight
  h0 = clientRectangle.Height - BorderTop -BorderBottom
  x1 = clientRectangle.Right - BorderRight
  y1 = clientRectangle.Bottom - BorderBottom
  g.FillRectangle(New SolidBrush(ColorBg),clientRectangle)
  Me.DrawVerticalLines(g)
  w1 = d * n
  Me.DrawHorizontalLines(g)
  Dim penAxis As New Pen(ColorAxis, 1)
  h1 = d * n
  g.DrawRectangle(penAxis, x0, y0, w0, h0) ' drawaxis
  h0 = h1 'must correct internal width & heightsince equidistant
  w0 = w1 'gridlines may not fit in axis rectanglew/o rounding errors
  Me.DrawData(g, xData, yData)
  If File.Exists(filename) Then
    File.Delete(filename)
  End If
  outputBitmap.Save(filename, ImageFormat.Jpeg)
  outputBitmap.Dispose()
  g.Dispose()
End Sub

Let's discuss drawing the vertical lines of the chart

The DrawVerticalLines Method shows how to draw the lines for logging which are not linear, but look like the image below.  The horizontal lines are drawn equidistant from each other.  The log or vertical lines must show that 45 is not half way between 10 and 100 even though half of 90 is 45.   Note that the lowest part of the data line is at the x-axis value of 40,000.  This is why the log paper must have the funny looking vertical lines.

Figure 1

The object "g" is the only input to the Method.  We need a pen for the grid lines and we need a brush for the labels for the axis lines. These are defined with the ColorGrid and ColorAxis properties of the Class LogPlotter that the current Method being discussed is packaged inside.

The variable "n" is the number of divisions or vertical line sections, although there will be sub lines within a section. This explains why there is a loop on "j" inside the loop on "i."  Note that in a log axis versus a linear axis, "n" ignores the property XGrid and uses about 1 as the XGrid property since it is using the calculation shown below.  Log(10000000) is about 16 and Log(.01) is about -4, so we get an "n" of about 20.  So "d" is the distance in each section as the available graphing width w0 divided by how many sections.  The position of a vertical line will be at x + d1. "X" is the same for each section, while d1 varies within the section.  "X" is x0, the starting point on the x-axis plus "i" times "d."  The d1 is the log of "j" times "d."  Now draw the line from y0 to y1.  Whew!  Why does that do it?  Because "j" is varying from 1 to XLogBase -1.  Our XLogBase is 10 so we want to show the 10 values between each section.  The ten are not evenly divided, but log spaced.  The Log(j) is the fraction of "d" where the line should be drawn within the section.  This is the hardest part to understand if you feel you need to.  Stare at it, play with it, run your variation and see what happens if you must.  Now print the label formatted like you want it.

Listing 3

Private Sub DrawVerticalLines(ByVal g As Graphics)
  Dim penGrid As New Pen(ColorGrid, 1)
  Dim brushAxis As New SolidBrush(ColorAxis)
  n = Convert.ToInt32(Math.Log(XRangeEnd, XLogBase)- _
      Math.Log(XRangeStart, XLogBase))
  If n = 0 Then n = 1 ' we know we don't want todivide by zero
  d = w0 / n
  For i As Integer = 0 To n
    x = x0 + i * d
    If i < n Then 'do not draw the detailedgradations after the border
      For j As Integer = 1 To XLogBase - 1
        d1 = Convert.ToInt32(Math.Log(j, XLogBase) *d)
        g.DrawLine(penGrid, x + d1, y0, x + d1, y1)
      Next
    End If
    s =Me.LargeFormat(Convert.ToString(Math.Pow(XLogBase, _
        Math.Log(XRangeStart, XLogBase) + i)))
    Dim sf As SizeF = g.MeasureString(s, FontAxis)
    g.DrawString(s, FontAxis, brushAxis, x -sf.Width / 2, y1 + sf.Height / 2)
  Next
End Sub

Let's discuss drawing the horizontal lines of the chart

The horizontal lines are linear and so are much simpler in this case, but remember the y axis could be logged instead or as well.  Here, "n" is defined with both the range of values and the YGrid property.  This is just a really simple version of the vertical lines.

Listing 4

Private Sub DrawHorizontalLines(ByVal g As Graphics)
  Dim penGrid As New Pen(ColorGrid, 1)
  Dim brushAxis As New SolidBrush(ColorAxis)
  n = Convert.ToInt32((YRangeEnd - YRangeStart) /YGrid)
  If n = 0 Then n = 1
  d = h0 / n
  For i As Integer = 0 To n
    y = y1 - i * d
    g.DrawLine(penGrid, x0, y, x1, y)
    Dim s As String = Convert.ToString(YRangeStart +_
                      (YRangeEnd - YRangeStart) * i/ n)
    Dim sf As SizeF = g.MeasureString(s, FontAxis)
    g.DrawString(s, FontAxis, brushAxis, x0 -sf.Width - sf.Height / 4, _
                 y - sf.Height / 2)
  Next
End Sub

Let's discuss drawing the Data on the chart

The hardest part here is converting the data for the points into log versions so they fit on the new log scale grid that we just drew.  DrawData Method needs the graphic "g" and the 2 arrays of doubles.  We need the pen to draw the line.  We need an array of point objects called "pts" that expects the right number of dots to graph.  The lastValidPt is used just in case the log of some number is not graphable, so we do not crash.  Remember when dimensioning arrays to use 5 for an array of six elements, since it is zero based.  A point object has an X property and Y property.  The point must be at the axis starting point x0 or y0, plus some amount from the data values.  Y is fairly simple, so let us understand that first.  Y1 is the extreme bottom that any data can be graphed.  Let us make this example even simpler by looking at the case where we picked the YRangeStart as 0 and the YRangeEnd as 1.  Also for simplicity, assume ydata(i) is .5, then the conversion is y1, which is 1 minus .5 divided by the height which is 1, so .5.  You need to see how, when the values are different, this formula still works.  Once you get that, then the log version is the same except for the log and that the axis origin in GDI+ by default is at the Northwest corner of our grid.  In other words, "x" increases to the right while "y" increases going down.  So x0 adds the formula on while y1 subtracts the formula.

Listing 5

Private Sub DrawData(ByVal g As Graphics, ByValxdata() As Double, _
                     ByVal ydata() As Double)
  Dim penDraw As New Pen(ColorDraw, PenWidth)
  Dim pts(xdata.Length - 1) As Point
  Dim lastValidPt As New Point(x0, y1)
  For i As Integer = 0 To pts.Length - 1 'convertpoints to fit log grid
    Try
    pts(i).X = Convert.ToInt32(x0 + (Math.Log(xdata(i),XLogBase) - _
        Math.Log(XRangeStart, XLogBase)) /(Math.Log(XRangeEnd, XLogBase) - _
        Math.Log(XRangeStart, XLogBase)) * w0)
    pts(i).Y = Convert.ToInt32(y1 - (ydata(i) -YRangeStart) / _
        (YRangeEnd - YRangeStart) * h0)
    lastValidPt = pts(i)
    Catch ex As Exception
    pts(i) = lastValidPt 'redraw last valid point onerror
    End Try
  Next
  For i As Integer = 0 To pts.Length - 1 'now drawthe points
    If i > 0 Then g.DrawLine(penDraw, pts(i - 1),pts(i))
  Next
End Sub

Let's discuss using the LogPlotter Class from a web page

After what we just went through, using the class is calling the Render Method with 3 input parameters.

Listing 6

Protected Sub createGraph_Click(ByVal sender AsObject, _
ByVal e As System.EventArgs) HandlescreateGraph.Click
If Not samples.SelectedIndex = -1 Then
  If Me.DataValid() Then
    Dim logplot As New LogPlotter
    Dim xData(4) As Double
    xData(0) = pc21.Text
    xData(1) = pc22.Text
    xData(2) = pc23.Text
    xData(3) = pc24.Text
    xData(4) = pc25.Text
    Dim yData(4) As Double
    yData(0) = 5
    yData(1) = 10
    yData(2) = 15
    yData(3) = 25
    yData(4) = 50
    Dim filename As String ="c:\graph.jpg"
    logplot.Render(xData, yData, filename)
    lblError.Text = "Graphic created"
  Else
    lblError.Text = "Data not plottable"
  End If
Else
  lblError.Text = "Select a sample to graphfirst"
End If
End Sub

Downloads

[Download Sample]

Conclusion

One could add drawing modes like dash, dot versus solid line or other colored lines overlapping each other easily.  You could add titles or legend's type labeling to the graph drawing.  If you want to stream your output to a web page replace my bitmap.save line with:

Response.ContentType = "image/jpeg"
ouputBitmap.Save(Response.OutputStream, ImageFormat.Jpeg)

With this class it is easy to plot:
log x-axis versus log y-axis
log x-axis versus linear y-axis
linear x-axis versus log y-axis
linear x-axis versus linear y-axis

It is easy to change ranges, have many ranges supported by the class, change spacing of demarcated lines, etc.  Always check that your data hits the graph lines properly.  If they do not, just trace the code relating to the out of synch value to see how it is getting plotted incorrectly.

In the .NET Help/Index type System.Math.  You will see the huge list of member functions with Log and Pow in there in the middle, alphabetically.  With GDI+ we have a lot of functions that can be used to plot sophisticated charts, if one can only understand how to make the graph paper line up with the data.  I hope I have given an example here with a sophisticated relationship and helped you understand it.  If you can understand this one, you can do almost any one.  Good luck out there.



User Comments

Title: Logging Power v.s time   
Name: Fernando hood
Date: 2007-01-02 3:16:44 PM
Comment:
I have project which calcualtes power from acceleration,speed and rpm... Its a microcontroller sending data via the serial port using mscomm. I would love to be able to graph the data in realtime. Your assistance would be greatly appreciated.
Title: students   
Name: Dr Gabr
Date: 2006-09-13 8:58:24 AM
Comment:
Dear sir
i need to creat small project in plotting nearly like ur project. but i try to us ur code and i failed if u want u can send me the form and complet solution so i can use it
thanks
Gabr
E-mail gabr_101@yahoo.com
Title: students   
Name: Sittichoke
Date: 2006-06-06 9:09:25 PM
Comment:
Dear sir
Can I have full source code with windows form for test run this program ?.
I do not know, How to run your source code ?
I have a little experience only , Please teach me ?
Please send detail to me at
koy_aun@yahoo.com

Best Regard

Sittichoke Sae_fung

07-06-2006

Product Spotlight
Product Spotlight 





Community Advice: ASP | SQL | XML | Regular Expressions | Windows


©Copyright 1998-2024 ASPAlliance.com  |  Page Processed at 2024-03-19 3:05:21 AM  AspAlliance Recent Articles RSS Feed
About ASPAlliance | Newsgroups | Advertise | Authors | Email Lists | Feedback | Link To Us | Privacy | Search