Bar Graphs to Go

Creating a Custom Graph Control from Scratch

ControlFreak

LANGUAGES: VB.NET | HTML

ASP.NET VERSIONS: 1.0 | 1.1

 

Bar Graphs to Go

Creating a Custom Graph Control from Scratch

 

By Steve C. Orr

 

In the inaugural ControlFreak column you saw how to inherit from existing controls and extend them to meet your needs. This is a valuable technique, but sometimes custom functionality is needed that simply isn't addressed at all by the built-in .NET Web controls. Charts and graphs are a good example of this. A basic bar graph is a control that can be used and reused to spruce up most any Web site. And colorful charts not only please the eye, they are an effective way to communicate important information or fun facts.

 

Weighing Your Options

To create graphs so beautiful that they defy words you usually need to use the System.Drawing namespace, be an amazing artist, and have plenty of development time on your hands. But it doesn't have to be that complicated. Basic HTML provides everything that's needed to display simple, attractive bar graphs that will connect with users. With that in mind, the System.Drawing namespace will be left for a future article.

 

Now it's time to determine the best way to go about generating the HTML that's needed for the bar graph. As I mentioned, inheriting from another control isn't an option, because there's no single suitable control to extend. However, a set of label controls could be up to the task, with each configured to varying widths and background colors to represent the bars. Figure 1 contains the VB.NET code for a basic control that uses this technique. The data is hard-coded to keep things simple for now; by the end of the article the control will be much more practical.

 

Imports System.ComponentModel

Imports System.Web.UI

Imports System.Web.UI.WebControls

 

  "<{0}:BarGraphA " + "runat=server>")> _

Public Class BarGraphA Inherits System.Web.UI.Control

 

  Protected Overrides Sub Render( _

    ByVal output As HtmlTextWriter)

 

    Dim lbl As New WebControls.Label()

    Dim lf As New LiteralControl("
")

 

    ' Output graph title.

    lbl.Text = "Projected Output:"

    lbl.RenderControl(output)

    lf.RenderControl(output)

    ' Output first bar.

    lbl.BackColor = Drawing.Color.Gray

    lbl.ForeColor = Drawing.Color.Yellow

    lbl.Width = New Unit("10%")

    lbl.Text = "2004"

    lbl.RenderControl(output)

    lf.RenderControl(output)

    ' Output second bar.

    lbl = New WebControls.Label()

    lbl.BackColor = Drawing.Color.Black

    lbl.ForeColor = Drawing.Color.Aqua

    lbl.Width = New Unit("40%")

    lbl.Text = "2005"

    lbl.RenderControl(output)

    lf.RenderControl(output)

    ' Output third bar.

    lbl = New WebControls.Label()

     lbl.BackColor = Drawing.Color.Blue

    lbl.ForeColor = Drawing.Color.Black

    lbl.Width = New Unit("80%")

    lbl.Text = "2006"

    lbl.RenderControl(output)

    lf.RenderControl(output)

  End Sub

End Class

Figure 1: Although not the most efficient code ever written, this shows that a bar graph can be created with surprisingly little effort.

 

The example in Figure 1 starts by importing some namespaces that will be used frequently throughout the code. Then a couple of basic attributes are set so the control will display properly when placed in the toolbox. This class inherits from System.Web.UI.Control, which supplies the basic functionality needed for any simple control, including the Render method that is overridden to output the bar graph. When developing controls you'll be more likely to inherit from an existing control or from System.Web.UI.WebControls.WebControl (which, in turn, inherits from System.Web.UI.Control.) Although System.Web.UI.WebControls.WebControl provides more default functionality than System.Web.UI.Control, the majority of those properties aren't needed for this bar graph control, and therefore would only detract from the focus of this article.

 

The example code in Figure 1 then goes on to declare an object variable that will contain a label, which will be reused for the display of each bar. It also declares a LiteralControl that will output a line break between each bar. The label control is first used to propel the title of the graph into the output stream, with a line break following.

 

Following the "output graph title" code block, a block of code is repeated three times to display three bars. (This isn't the epitome of efficiency, but it will be cleaned up later.) The code block starts by putting a new label control into the lbl variable, and setting it up with some nice looking colors. The width of the control is then set to define how large the bar will be, and some text is written onto the bar. Finally, the control is rendered, thus generating the HTML for the bar. A line break then follows so two bars don't end up on the same horizontal line.

 

If you compile this code into a new Web control library, then add it to your toolbox and drop it onto a Web form in a Web application, you'll see that it displays the nice little bar graph shown in Figure 2.

 


Figure 2: A reasonably attractive bar graph can spruce up any Web site, and the code doesn't have to be complex.

 

The code in Figure 1 is a basic example of a composite control. Composite controls delegate work to multiple underlying Web controls. This is a practical way to create rich controls quickly, and requires no knowledge of HTML. However, any expert will tell you that composite controls tend to be rather bloated and inefficient compared to the alternatives. In this example, instantiating all those label controls comes with a performance penalty that could be significant in a high-volume Web site. If only there were a reasonably simple way to output the same HTML without having to create all those controls! Well, as it turns out, there is a better way.

 

Weight Loss Techniques

HTMLTextWriter is a valuable tool that any control developer should have in his or her arsenal. This is the most direct and efficient way to output HTML while still having a few basic niceties, such as support for down-level browsers. Figure 3 shows code that outputs HTML that is virtually identical to the example in Figure 1, but uses HTMLTextWriter instead of Web controls to keep things lubricated.

 

Imports System.ComponentModel

Imports System.Web.UI

 

  + "runat=server>")> _

Public Class BarGraphB Inherits Web.UI.Control

 

  Protected Overrides Sub Render ( _

    ByVal output As HtmlTextWriter)

 

    ' Output Graph Title.

    output.AddStyleAttribute("width", "80%")

    output.AddAttribute("align", "center")

    output.RenderBeginTag("div")

    output.Write("Projected Output:")

    output.RenderEndTag()

    ' Output first bar.

    output.AddStyleAttribute("color", "yellow")

    output.AddStyleAttribute("background-color", "gray")

    output.AddStyleAttribute("width", "10%")

    output.AddAttribute("title", "10%")

    output.RenderBeginTag("div")

    output.Write("2004")

    output.RenderEndTag()

    ' Output second bar.

    output.AddStyleAttribute("color", "aqua")

    output.AddStyleAttribute("background-color", "black")

    output.AddStyleAttribute("width", "40%")

    output.AddAttribute("title", "40%")

    output.RenderBeginTag("div")

    output.Write("2005")

    output.RenderEndTag()

    ' Output third bar.

    output.AddStyleAttribute("color", "black")

    output.AddStyleAttribute("background-color", "blue")

    output.AddStyleAttribute("width", "80%")

    output.AddAttribute("title", "80%")

    output.RenderBeginTag("div")

    output.Write("2006")

    output.RenderEndTag()

  End Sub

End Class

Figure 3: Using HTMLTextWriter takes some getting used to, but the efficiency makes it worth the learning curve.

 

Notice that the example code in Figure 3 isn't any longer than the code in Figure 1, nor is it very complex - as long as you're comfortable with HTML. This code starts the same way, overriding the Render method provided by the base Control class from which this control inherits. Then things start to deviate. This is where HTMLTextWriter (which is passed as a parameter to the Render method) starts to get used to its fullest extent.

 

As before, the title for the bar graph is output first. Instead of instantiating a label control, however, this time the HTML will be output directly in the form of a

tag. Some basic attributes are set for this tag, such as a centered alignment. Then the title text is output and rendered to the output stream.

 

The next three code blocks, which output the three bars in the graph, are nearly identical to each other. (Again, this isn't the most efficient way to handle this. And yes, the data is still hard coded. Hang in there; this will all be resolved.) Each bar is a

tag with foreground and background colors set. The width of each
tag determines the size of the bar. The title attribute provides a tooltip for each bar. The text in the bar is rendered after the opening
tag, and before the closing
tag. The resulting HTML is output to the browser:

 

  "color:yellow;background-color:gray;width:10%;">2004

 

Again, this results in a bar graph that looks like that shown in Figure 2.

 

Now that it's been established that heavy use of HTMLTextWriter tends to lead to better performance than composite controls, this will be the focus of the remainder of this article. This is not to say that composite controls are always bad; they have their merits and will be covered in more detail in upcoming articles.

 

Keeping It Real

It's time to get to work. The code in Figure 3 is fine for a prototype, but it needs a serious overhaul to become a valuable member of your toolbox. First of all, the data obviously can't be hard-coded in the control. It needs to accept data from external sources. The new version of the control will provide a Bar class that allows you to configure properties for each bar, such as color, text, value, and tooltip. This will allow a bar in the BarGraph control to be configured with a simple line of code such as this:

 

BG1.Bars.Add(New BarGraph.Bar("2004", "red", 10, "10K"))

 

The Bar class listed in Figure 4 is nothing remarkable. It's basically just a container that holds the properties you'll need for the display of each bar. The control will also contain a collection class named Bars to hold all the Bar objects for the current instance of the control. This class is standard collection code. In ASP.NET version 2 you'll be able to use Generics, which will eliminate the need for most of this boilerplate code.

 

Public Class cBar

  Public Sub New()

  End Sub

 

  Public Sub New(ByVal Text As String, _

    ByVal BarColor As String, ByVal Value As Double, _

    Optional ByVal tooltip As String = "")

    _text = Text

    _barColor = BarColor

    _Value = Value

    _toolTip = tooltip

  End Sub

 

  Dim _text As String = String.Empty

  Public Property Text() As String

    Get

      Return _text

    End Get

    Set(ByVal Value As String)

      _text = Value

    End Set

  End Property

 

  Dim _barColor As String = "gray"

  Public Property BarColor() As String

    Get

      Return _barColor

    End Get

    Set(ByVal Value As String)

      _barColor = Value

    End Set

  End Property

 

  Dim _Value As Double = 0

  Public Property Value() As Double

    Get

      Return _Value

    End Get

    Set(ByVal Val As Double)

      _Value = Val

     End Set

  End Property

 

  Dim _toolTip As String = String.Empty

  Public Property ToolTip() As String

    Get

      Return _toolTip

    End Get

    Set(ByVal Value As String)

      _toolTip = Value

    End Set

  End Property

End Class

Figure 4: The overloaded constructor of the Bar class allows a Bar object to be instantiated and filled with data in a single line of code.

 

The BarGraph control will itself have a number of public properties: Text, BorderWidth, CellPadding, and CellSpacing. These properties will allow adjustments to the overall look of the BarGraph. The code for all the properties looks pretty much the same. The Text property will contain the title displayed above the graph:

 

_

Property Text() As String

  Get

    Return _text

  End Get

  Set(ByVal Value As String)

    _text = Value

  End Set

End Property

 

Especially interesting are the attributes at the top of the code that permit data binding and specify the placement of this property within the Appearance section of the properties window at design time.

 

This new version of the control will be in more direct command of the layout of the bar graph by using an HTML table to precisely position each element of the control. One example is that the text for the bar cannot be inside the bar itself, because this will interfere with the size of small bars, causing them to elongate to accommodate the width of the text. The solution is to move the associated text into a separate table cell next to the bar. The Render event of the control should resemble Figure 5.

 

Protected Overrides Sub Render( _

  ByVal output As HtmlTextWriter)

  ' Output the table that wraps around the bar graph.

  If MyBase.Visible Then

    output.WriteBeginTag("table")

    output.WriteAttribute("width", "90%")

    output.WriteAttribute("align", "center")

    output.WriteAttribute("border", _border)

    output.WriteAttribute("cellPadding", _cellPadding)

    output.WriteAttribute("cellSpacing", _cellSpacing)

    output.Write(HtmlTextWriter.TagRightChar)

    output.WriteFullBeginTag("tr")

    output.WriteBeginTag("td")

    output.WriteAttribute("colspan", "2")

    output.WriteAttribute("align", "center")

    output.Write(HtmlTextWriter.TagRightChar)

    output.Write(_text)

    output.WriteEndTag("td")

    output.WriteEndTag("tr")

    output.WriteLine()  ' For cosmetic HTML source display.

    OutputBars(output)   ' Each bar is on 1 table row.

    output.WriteEndTag("table")

  End If

End Sub

Figure 5: Elements of the bar graph are now placed within an HTML table for more precise positioning.

 

The first thing the code in Figure 5 does is check to make sure the Visible property of the BarGraph control is true. Even though this property hasn't been explicitly defined in the code of the BarGraph control, it still exists implicitly in the base Control class from which the BarGraph class inherits, and can therefore be used.

 

The bulk of the remaining code in Figure 5 should look somewhat familiar to you at this point. It outputs the HTML necessary to create a nice-looking two-column HTML table. The BarGraph properties are used where appropriate to make sure the title of the graph is displayed at the top, and that border and cell attributes are set correctly as specified by the consumer of the control. The individual bars are output from the OutputBars subroutine defined in Figure 6.

 

Private Sub OutputBars(ByVal output As HtmlTextWriter)

  Dim bar As cBar

  For Each bar In Bars()

    ' Output the text column.

    output.WriteFullBeginTag("tr")

    output.WriteBeginTag("td")

    output.WriteAttribute("width", "1%")

    output.Write(HtmlTextWriter.TagRightChar)

    output.Write(bar.Text & " ")

    output.WriteEndTag("td")

    ' Output the bar column.

    output.WriteFullBeginTag("td")

    output.AddStyleAttribute("background-color", _

      bar.BarColor)

    output.AddStyleAttribute("width", _

      bar.Value / LargestBarValue() * 100 & "%")

    output.AddAttribute("title", bar.ToolTip)

    output.RenderBeginTag("div")

    output.RenderEndTag()

    output.WriteEndTag("td")

    output.WriteEndTag("tr")

    output.WriteLine()  ' Cosmetic only.

  Next

End Sub

Figure 6: Each row of the HTML table consists of a bar column and an associated text column that describes the bar.

 

This subroutine loops through each bar in the Bars collection, outputting one HTML table row for each. The first column is filled with a description of the bar that's placed in the second column. The first column should take up no more room than necessary, so its width is set to 1%. Of course, HTML dictates that this column will grow larger than 1% if necessary for display, or it may use word wrapping where applicable to help keep it small. The line:

 

output.WriteFullBeginTag("tr")

 

will output this full begin tag: . The WriteBeginTag line that follows it leaves off the ending bracket of the tag (i.e. ) will be displayed on the right side of it. Then the bar text is output with a hard space after it to make it look nicer. Finally, the ending tag is output before the bar column is rendered.

 

The bar column is rendered similarly to the text column. It begins with a full tag. Inside this table cell a

is created to represent the bar. Some attributes are added to it to specify the color and tooltip, and the required size is calculated and set as the width for the
. The end tag for the
is then output, followed by the end tags for the table cell and table row. The final WriteLine statement is there only to make the generated HTML source code look nicer.

 

The control is now quite functional. If you drop this compiled control onto a Web form, you can configure it from your code behind with a few lines of code, like this:

 

With BarGraphC1

  .Text = "Projected revenue from software sales:"

  .BorderWidth = 1

  .Bars.Add(New BarGraph.cBar("2004", "gray", 10, "10K"))

  .Bars.Add(New BarGraph.cBar("2005", "black", 25, "25K"))

  .Bars.Add(New BarGraph.cBar("2006", "blue", 50, "50K"))

  .Bars.Add(New BarGraph.cBar("2007", "red", 75, "75K"))

End With

 

This will display a control that looks like Figure 7. A number of BarGraph controls can be dropped onto a page, and they can all be configured differently, resulting in a page that might look something like that shown in Figure 8.

 


Figure 7: An HTML table is used to precisely place each element of the bar graph.

 


Figure 8: The final version of the BarGraph control is flexible enough to allow information to be attractively displayed in a variety of ways.

 

What about Design Time?

Although the BarGraph control is looking pretty nice at run time, there isn't really any visible display to speak of at design time. The main reason is that the control doesn't have any data to display until run time. This situation can be remedied by supplying some dummy data for the control at design time by associating the control with a designer class. The designer in Figure 9 is up to the task.

 

Friend Class ControlDesigner

  Inherits System.Web.UI.Design.ControlDesigner

  ' Creates random output for display at design time.

  Public Overrides Function GetDesignTimeHtml() As String

    ' Create a bar graph control.

    Dim BarGraphC1 As New BarGraphC()

    Dim Rand As Byte = 1

    ' Fill the bar graph with random data.

    With BarGraphC1

      .Text = "Graph Title"

      .Bars.Add(New BarGraph.cBar())

      ' Displays a random value between 1 and 12.

      Rand = CByte(Int((12 * Rnd()) + 1))

      ' Displays the first of 3 bars.

      .Bars.Add(New BarGraph.cBar("Row1", _

        "red", Rand, Rand.ToString))

      ' Same routine (different color) for bar 2.

      Rand = CByte(Int((12 * Rnd()) + 1))

      .Bars.Add(New BarGraph.cBar("Row2", _

        "black", Rand, Rand.ToString))

      ' Same routine (different color) for bar 3.

      Rand = CByte(Int((12 * Rnd()) + 1))

      .Bars.Add(New BarGraph.cBar("Row3", _

        "blue", Rand, Rand.ToString))

      .Bars.Add(New BarGraph.cBar())  ' Blank bar.

    End With

    ' Return the HTML that is output by the control.

    Dim sw As IO.StringWriter = New IO.StringWriter()

    Dim tw As HtmlTextWriter = New HtmlTextWriter(sw)

    BarGraphC1.RenderControl(tw)

    Return sw.ToString()

  End Function

End Class

Figure 9: This designer class instantiates a BarGraph control at design time, fills it with random data, and outputs the resulting HTML to Visual Studio.NET for a pleasant design-time experience.

 

For this to work, a reference needs to be added to system.design.dll. The following attribute also needs to be added to your BarGraph class to ensure the control is linked to the designer:

 

 

You should now have a pretty good idea of how to create custom controls from scratch, without the overhead involved with using existing Web controls. With some practice you can master such techniques and create entire control libraries filled with reusable gold. Good luck!

 

The sample code in this article is available for download.

 

Steve C. Orr is an MCSD and a Microsoft MVP in ASP.NET. He's an independent consultant who develops effective software solutions for many leading companies in the Seattle area. When he's not busy designing software systems or writing about it, he can often be found loitering at local user groups and habitually lurking in the ASP.NET newsgroup. Find out more about him at http://Steve.Orr.net or e-mail him at mailto:[email protected].

 

 

 

Hide comments

Comments

  • Allowed HTML tags: <em> <strong> <blockquote> <br> <p>

Plain text

  • No HTML tags allowed.
  • Web page addresses and e-mail addresses turn into links automatically.
  • Lines and paragraphs break automatically.
Publish