Demo the LineChart Download the zip with 3 files: usage.aspx, usage.aspx.vb, linechart.vb (extract 3 files into new web project, set usage.aspx as startup, F5)
Earlier I read a couple of articles on charting, but not being familiar with GDI, or GDI+ it was easy to get the code to chart, but hard to understand the code well enough to see how to create a totallly different type of graph. The goal here is simple. A chart class for line charts that creates one line graph on the chart for each record in the datatable that you input to the class's single method, render, but that method must output the stream into an asp:image server control in a way that works in design time as well as run time so you can easily position the chart and see it as you work on the rest of the page without using other pages or controls to make this happen. I want to describe each non-trivial line of code. This is vb .NET, but easily converted to c# as most of the code is object manipulation, not language manipulation.
The motivation for this kind of a chart is for a graph that compares many things on one graph to help people make decisions about things like if one compared average utility usage for all customers with one's own usage it might motivate one to reduce usage of the utility somewhat. First let's put some code in a page_load event to build some test data for us that we can use with the graph.
Public
Protected WithEvents Image1 As System.Web.UI.WebControls.Image
Private Sub Page_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
If Request.QueryString("image") = 1 Then
Dim jan As New DataColumn("Jan", GetType(Double)) ' here the column gets defined, with jan as the column object name and Jan as the table field or column name
dt.Columns.Add(jan) ' here the column object is added to the column collection of the table dt
Dim feb As New DataColumn("Feb", GetType(Double))
dt.Columns.Add(feb)
Dim mar As New DataColumn("Mar", GetType(Double))
dt.Columns.Add(mar)
Dim apr As New DataColumn("Apr", GetType(Double))
dt.Columns.Add(apr)
Dim may As New DataColumn("May", GetType(Double))
dt.Columns.Add(may)
Dim jun As New DataColumn("Jun", GetType(Double))
dt.Columns.Add(jun)
Dim jul As New DataColumn("Jul", GetType(Double))
dt.Columns.Add(jul)
Dim aug As New DataColumn("Aug", GetType(Double))
dt.Columns.Add(aug)
Dim sep As New DataColumn("Sep", GetType(Double))
dt.Columns.Add(sep)
Dim oct As New DataColumn("Oct", GetType(Double))
dt.Columns.Add(oct)
Dim nov As New DataColumn("Nov", GetType(Double))
dt.Columns.Add(nov)
Dim dec As New DataColumn("Dec", GetType(Double))
dt.Columns.Add(dec)
Dim dr As DataRow = dt.NewRow ' with all of our columns added, lets add 2 rows to our defined table structure
dr("jan") = 44 ' a row can except a value for each column if the value is the proper type
dr("feb") = 55
dr("mar") = 66
dr("apr") = 77
dr("may") = 65
dr("jun") = 64
dr("jul") = 63
dr("aug") = 67
dr("sep") = 68
dr("oct") = 70
dr("nov") = 77
dr("dec") = 67
dt.Rows.Add(dr)
Dim dr1 As DataRow = dt.NewRow
dr1("jan") = 33
dr1("feb") = 44
dr1("mar") = 55
dr1("apr") = 77
dr1("may") = 87
dr1("jun") = 92
dr1("jul") = 44
dr1("aug") = 55
dr1("sep") = 44
dr1("oct") = 33
dr1("nov") = 22
dr1("dec") = 44
dt.Rows.Add(dr1)
Dim ourChart As New LineChart() ' here we instantiate an object from our custom class
ourChart.Render("Usage Graph", "(versus average)", 1000, 600, dt, Response.OutputStream) ' render is the only method of our class that we will need to call, and this method returns a stream of Gif type
End If
End Class
Note that this page_load event code only runs when request.querystring("image")=1. Why is this? The first time this page loads we simple call the name of the page, usage.aspx, right? Well there are no parameters at the end of that url, correct? So that is why upon loading of this aspx page the code does not run. So what does run the code? An image tag url. I put this code next from the page usage.aspx.
<asp:Image id="Image1" ImageUrl="usage.aspx?image=1" ' this code is from usage.aspx style="Z-INDEX: 101; LEFT: 117px; POSITION: absolute; TOP: 102px" runat="server" Width="1000px" Height="600px"> </asp:Image>
Note the ImageUrl tag and its value. It is pointing back to the page that the control resides within, but this time with a parameter image with value of 1. When this control, even in design time, calls the page the page_load event processes again and creates the data and then chart, and then stream and then the stream is routed off to the target that has been input to the linechart method render. Now Response.OutputStream is not the page, but the image tag control. You may have to think about this part for a while, but if you don't understand it it might be worth the thinking time and energy. So now the stream, even in design time, shows up in the image tag control so that you can see it while you design the rest of your page around the asp:Image server control.
Next we want to focus in on the linechart class definition to a degree that would allow you to control it and customize it to any degree that you want, even for 3d charts, for whatever purpose you might be faced with. So far with .NET I have not had to use Crystal Reports or any other graph engine, or Excel or anything but variations of this code to do all my graphing needs. This way you are totally in control and you are not likely to have problems with speed as I have had with other options in the past. Once I was asked to use Crystal Reports to solve a reporting requirement with data and graph and Crystal Reports generated 20 pages of html for the report. After using this code, the one page report was one page of html and loaded very quickly in relation to the other option and allowed me to customize much more also. This is partially my motivation for this article. I see many people using awkward tools where .NET has a very good solution available. The whole class is only a page in length. First the overview.
All the rest of the code comes from linechart.vb
Imports
Public Sub Render(ByVal title As String, ByVal subTitle As String, ByVal width As Integer, ByVal height As Integer, ByVal dt As DataTable, ByVal target As Stream)
End Sub ' title, and subtitle should be clear, width and height is the size you want the output stream to be, dt holds the records for the lines
' the target is where you want the formatted stream to go. In our case it is going to be Response.OutputStream
Public Shared Function GetColor(ByVal row As Integer) As Color ' this simple method just returns a color object for each row to color each line chart differently
Dim currentColor As Color ' add more colors if you want more line charts
Select Case row
Case 0
currentColor = Color.Blue
Case 1
currentColor = Color.Red
Case 2
currentColor = Color.Green
Case 3
currentColor = Color.Purple
Case 4
currentColor = Color.Orange
Case Else
currentColor = Color.Navy
End Select
End Function
End
This is the first part of the code inside the render method:
Const CANVAS As Integer = 1000 ' I could draw directly on the 1000 by 600 size graphic, but this will show how to draw on 1000x1000 and then scale to 1000x600
Const CHART_TOP As Integer = 90 ' Start the charting area 90 pixels below the canvas top to leave room for the title and subtitle
Const CHART_HEIGHT As Integer = 800 ' The chart height is most of the 1000 height canvas
Const CHART_LEFT As Integer = 0 ' Let's start with 0 because there are some adjustments we will have to make
Const CHART_WIDTH As Integer = 1000 ' Go the whole width, but with adjustments
Dim myBitMap As Bitmap = New Bitmap(width, height) ' A bitmap is like a stream, a memory representation of a picture format that can convert to many formats
Dim graph As Graphics = Graphics.FromImage(myBitMap) ' A graphics object has many methods for drawing on the abstract bitmap object
Dim highPoint As Single = 0 ' determine highpoint of all your data so we can relate that amount to the CHART_HEIGHT
Dim row As DataRow
Dim col As DataColumn
For Each row In dt.Rows ' each row has a set of columns to consider for highpoint
For Each col In dt.Columns
highPoint=iif(highPoint<row.Item(col),row.Item(col),highPoint) ' check each column, we started with 0 and if current column value is greater replace with it
Next col
Next row ' ScaleTransform affects all drawing after it has taken place
graph.ScaleTransform((Convert.ToSingle(width)) / CANVAS, (Convert.ToSingle(height)) / CANVAS) ' this is the line that squeezes the 1000x1000 into the 1000x600
graph.SmoothingMode = SmoothingMode.Default ' here we apply the default smoothingmode to the drawing using a value of an enumerator
graph.TextRenderingHint = TextRenderingHint.AntiAlias ' this enumerator value causes some antialiasing to happen on the drawing
graph.Clear(Color.LemonChiffon) 'draw canvas white
graph.DrawRectangle(Pens.Black, 0, 0, CANVAS - 1, CANVAS - 1)
Once you get this section digested some go on to the actual start of the interesting drawing.
Titles first and then and then the two-level loop for the main linecharts:
graph.DrawString(title,
graph.DrawString(subTitle,
Dim lineWidth As Single = (width / (dt.Columns.Count)) ' this is the width on the chart that each line segment can take up
Dim lineEnd As PointF = New PointF(0, 0) ' this is a trivial line end initialization
Dim firstColumn As Boolean = True ' at the first column we only have the info for one point & so don't have enough to draw a segment
lineEnd.X = lineOrigin.X + lineWidth ' each column requires x-coordinate move to right one lineWidth, therefore define lineend appropriately
lineEnd.Y = (highPoint - row.Item(col) + BASE) * CHART_HEIGHT / highPoint ' since y moves down from top, subtract value from highPoint, then adj for highpoint relation to CHART_HEIGHT
If Not firstColumn Then ' the BASE is another height adjustment that was obviously needed, but I am not sure why
graph.DrawLine(
Else
firstColumn =
lineOrigin.X = lineOrigin.X + lineWidth ' updating x is just lineWidth
lineOrigin.Y = lineEnd.Y ' new lineOrigin.Y is the old lineEnd.Y to keep the line segments continuous
currentRow += 1 ' upgrade currentRow so that each linechart has a distinct color
Next row
Now we can finish off the chart with some legends, and horizontal and vertical ticks
Dim tickheight As Single = CHART_TOP ' a horizontal line separating individual values, just a single
Dim tickvalorigin As PointF = New PointF(21, CHART_TOP) ' this is a point object with two single coordinates
Dim m As Integer
graph.DrawString(Int(highPoint * ((7 - m) / 6)),
tickvalorigin.Y += CHART_HEIGHT / 6 ' update for next
tickheight += CHART_HEIGHT / 6
Next
graph.DrawString("Average",
graph.DrawString("Usage",
graph.FillRectangle(
Dim markOrigin As PointF = New PointF(CHART_LEFT + lineWidth / 2, 890) 'draw legend items
graph.DrawString(dt.Columns(col.ToString).ToString(), New Font("Tahoma", 10), Brushes.Black, textOrigin) ' fieldnames are printed here which happen to be month names
graph.DrawLine(New Pen(Color.LightGray, 1), New Point(markOrigin.X, CHART_TOP), New Point(markOrigin.X, CHART_TOP + CHART_HEIGHT)) ' vertical tick line
markOrigin.X += lineWidth
textOrigin.X += lineWidth
myBitMap.Save(target, ImageFormat.Gif) 'stream the bar chart image to the browser via the Response.OutputStream object
Now how would this line chart be made to be three dimensional? The graphing programs I've looked at have no switch that is thrown to get a 3D version except in the case of applying a gradient between two colors across a shape. Generally the 2D version is drawn first and then it is enhanced into a 3D version by adding drawing over the existing shape. In the case of a bar, another bar is drawn next to it and then connected with proper fillins. I would add this enhancement by a method so your code stays simple at this point. Without going 3D looks can be varied quite a bit here by varying line widths and colors.