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 Double, ByVal 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 Double, ByVal 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.