Multi-line-chart streamed direct to image control: (explained, or made plain)
Author: Terry Voss
credit to:
Jonathan Goodyear's article:                           Dino Esposito's article:
Chart a Course With ASP.NET Graphics           Build Dynamic Web Charts
http://www.angrycoder.com                             http://www.aspnetpro.com

        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 Class usage1 : Inherits System.Web.UI.Page               ' this code is from usage.aspx.vb

 

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 dt As New DataTable("usage")                                     ' note that we must have a table object to add the columns to

    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 Sub

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 System.IO                                    ' this is only needed so that we can conveniently use a stream as an input parameter for the render method

Imports System.Drawing.Text                    ' this is only needed so that we can conveniently use the TextRenderingHint enumeration, note that system.drawing is already imported by default by the project

Imports System.Drawing.Drawing2D           ' this is only needed so that we can conveniently use the SmoothingMode enumeration

Imports System.Drawing.Imaging               ' this is only needed so that we can conveniently use the ImageFormat.Gif class and shared property

 

Public Class LineChart                              ' the class only has one main member, the render method which returns nothing, and receives 6 input parameters

 

  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

    Return currentColor

  End Function

 

End Class

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 base As Double = 1 + highPoint / 10                                    ' This is an adjustment factor that we will use in a few places

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)  'draw border to the size of 999x999 so that the line is inside the viewing area, this will scale to 1000x600

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, New Font("Arial", 24), Brushes.Red, New PointF(5, 5))                  ' draw title needs just 4 params, text, font, brushcolor, and a point for the location, x goes right, y goes down

graph.DrawString(subTitle, New Font("Arial", 14), Brushes.Blue, New PointF(10, 46))         ' draw subtitle just below and to right a bit, note: coordinate system is in upper-left

Dim currentRow As Integer = 0                                                                                        ' this is needed to grab a new color each row loop for the line chart color differentiation

Dim lineWidth As Single = (width / (dt.Columns.Count))                                                     ' this is the width on the chart that each line segment can take up

For Each row In dt.Rows                                                                                                 ' draw one line chart per row of data

  Dim lineOrigin As PointF = New PointF(CHART_LEFT - lineWidth / 2, 0)                           ' the x coordinate is made negative because of what addition we need to do each time

  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

  For Each col In dt.Columns                                                                                            ' each column after the first has enough info to draw one segment of the line chart

    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(New Pen(GetColor(currentRow), 1), lineOrigin.X, lineOrigin.Y, lineEnd.X, lineEnd.Y)  ' draw the line segment using lineOrigin and lineEnd, notice how we get the color of the line

    Else

      firstColumn = False

    End If

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

Next col

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

graph.DrawLine(New Pen(Color.Black, 1), New Point(CHART_LEFT, CHART_TOP + CHART_HEIGHT), New Point(CHART_LEFT + CHART_WIDTH, CHART_TOP + CHART_HEIGHT)) ' at chart bottom

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

For m = 1 To 6     ' We are going to divide the highpoint range into 6 areas

  graph.DrawString(Int(highPoint * ((7 - m) / 6)), New Font("Tahoma", 10), Brushes.Black, tickvalorigin) ' param1=text that will be drawn, param2=font, param3=brush, param4=coordinate point, draw value

  graph.DrawLine(New Pen(Color.Black, 1), New Point(CHART_LEFT, tickheight), New Point(CHART_WIDTH + CHART_LEFT, tickheight)) ' draw line horizontal

  tickvalorigin.Y += CHART_HEIGHT / 6  ' update for next

  tickheight += CHART_HEIGHT / 6

Next

graph.DrawString("Average", New Font("Tahoma", 12, FontStyle.Bold), Brushes.Black, New PointF(350, 950))  'draw legend box and label to go inside

graph.DrawString("Usage", New Font("Tahoma", 12, FontStyle.Bold), Brushes.Black, New PointF(550, 950))    ' I made this part manual as clients often want this customized, you could automate this

graph.FillRectangle(New SolidBrush(Color.Blue), 430, 957, 20, 10)                                                                 ' You could loop here for each row

graph.FillRectangle(New SolidBrush(Color.Red), 610, 957, 20, 10)

Dim markOrigin As PointF = New PointF(CHART_LEFT + lineWidth / 2, 890) 'draw legend items

Dim textOrigin As PointF = New PointF(CHART_LEFT + (lineWidth / 2) - BASE, 895)

For Each col In dt.Columns

  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

Next col

myBitMap.Save(target, ImageFormat.Gif) 'stream the bar chart image to the browser via the Response.OutputStream object

End Sub

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.

Send mail to Computer Consulting with questions or comments about this web site.
Last modified: January 04, 2006