Multi-Line-Chart streamed direct to positioned image tag
page 1 of 1
Published: 13 Oct 2003
Unedited - Community Contributed
Abstract
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.
by Terry Voss
Feedback
Average Rating: 
Views (Total / Last 10 Days): 16131/ 23


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.


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
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: October 09, 2003



 





User Comments

No comments posted yet.

Product Spotlight
Product Spotlight 





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


©Copyright 1998-2017 ASPAlliance.com  |  Page Processed at 2017-04-28 2:31:42 AM  AspAlliance Recent Articles RSS Feed
About ASPAlliance | Newsgroups | Advertise | Authors | Email Lists | Feedback | Link To Us | Privacy | Search