CoreCoder
LANGUAGES: C#
TECHNOLOGIES: Imaging | Streams | GDI+
Build Dynamic Web Charts
Create charts and manipulate images in ASP.NET pages.
By Dino Esposito
Many Web sites - financial sites, for example - offer a particularly helpful feature: rather than return data in tables, they present data visually, in the form of a graphical chart. In most cases, Web-deployed charts are generated dynamically on the server and sent to the browser as a stream of bytes, traveling over the classic Response channel. What really marks the difference between a normal HTML-based page and an image is the content type.
The return of graphics from a Web server is not a recent technological innovation, and in fact it can be accomplished with almost all languages and over almost all platforms. The real issue is how easily you can create and manipulate server-side images. In this column, I first will discuss the relatively standardized code necessary to send images back to the browser, and then I'll examine how to create and manipulate images - in particular, graphic charts.
Create Graphic Web Pages
Each and every Web page has a content type that
determines, for the most part, how the browser will treat the actual content.
Thus, pages with a MIME type of text/html are rendered as HTML, and pages
marked with text/xml are parsed and displayed as raw text or perhaps as an
embedded stylesheet document. An image, regardless of whether it is generated
dynamically or is statically resident on the server, is returned as a separate
page with a particular content type. For example, you use the image/gif MIME
type for a GIF image and image/jpeg for a JPEG image. If the image is available
on the server as is, you link it to the page by inserting an tag
that points to the GIF or JPEG file. Otherwise, the
tag needs to
point to a page that creates and returns the image. In fact, the procedure to
return the image is set in stone. If the image is stored physically on the Web,
Internet Information Server (IIS) does the job for you, reading the bytes from
disk, packing them into a response payload of the corresponding type, and
transmitting the data to the client. If the image is not available on the
server directly, such as with graphic charts that must be generated on the fly,
you must write an ad-hoc page (see Figure 1).
Figure 1. This page sets the content type, generates
the image using GDI+ classes, then saves the output to the Response's output
stream. Save this page with an .aspx extension and link it to a Web page
through the tag.
Assuming you name the page in Figure 1 barchart.aspx, here is the necessary ASP.NET code to insert the dynamic image on a page:
You also can set the source URL for the tag
programmatically, resulting in a more flexible approach.
Discover GDI+
The previous solution can just as reasonably be defined as cross-platform as the HTTP protocol itself; in no way is it a solution exclusive to ASP.NET. The real added value of dynamically generated images results from the image manipulation features supported by the surrounding environment. For ASP.NET pages, this back-end system is GDI+.
GDI+ is the latest incarnation of the classic Windows Graphics Device Interface (GDI). GDI+ is a graphic subsystem that lets users write device-independent applications. It's native to Windows XP, but it also is available for most 32-bit and 64-bit Windows platforms. For operating systems such as Windows 2000 (that is, pre-Windows XP), the GDI+ engine is packed in a redistributable DLL (gdiplus.dll) available for download at http://www.microsoft.com/msdownload/platformsdk/sdkupdate/psdkredist.htm. The .NET Framework encapsulates the key GDI+ functionalities in a handful of namespaces, making those functions available to both Web and Windows Forms applications.
Most of the GDI+ services belong to two categories: 2-D vector graphics and imaging. Two-D vector graphics are concerned with drawing primitive figures such as lines, curves, and polygons specified by a set of points taken on a coordinate system. Maintained under the imaging umbrella are those functions that display, manipulate, save, and convert pictures as bitmaps. Typography - having to do with the display of text in a variety of fonts, sizes, and styles - represents a third category of functions I won't cover in this article as I am focusing on imaging and graphic primitives only.
The Win32 GDI programming model has a central element in the device context - namely a data structure - that stores information concerned with the capabilities of a particular display device. A device context such as a window, printer, or block of memory is associated with a drawing surface. Win32 programs obtain a handle to a device context and pass it to all GDI functions they call. A device context also maintains a list of the graphic attributes to be used for the actual drawing. This device context is the key interface between the high-level front-end API and the underlying drivers.
GDI+ replaces the concept of a device context and its subsequent handle-based programming style with a truly object-oriented approach. In GDI+, the Graphics object plays the role of the central console for drawing; graphic surfaces (such as the window or printer) and graphic objects are decoupled and treated as independent entities. GDI+ drawing methods, then, take graphic objects as arguments. This represents a major step forward because in Win32, you are required to select objects in their prior context so they may affect code output.
Chart With GDI+ Classes
Now let's see how to create a sample dynamic bar chart embedded in an ASP.NET page. The first step is to create the Graphics object and associate it with a particular canvas. For a server-side application, memory is the only viable option. Instantiate a Bitmap object of the desired size and create a drawing context from it:
Bitmap bmp = new Bitmap(500, 300);
Graphics g = Graphics.FromImage(bmp);
Any graphic primitives called to operate on the just-created Graphics object will affect the underlying Bitmap object. Notice that the Bitmap object is not necessarily a BMP image. The Bitmap class is a general-purpose GDI+ class that represents an in-memory image. The image is stored in an intermediate binary format that can then be converted into any of the most commonly used image formats, such as GIF or JPEG. To create a bar chart, you draw rectangles and text using the Graphics object as the canvas, then you need to decide which image format you want the bitmap to use.
You need data to draw a chart. In particular, you need values and labels. The sample code shown in Figure 2 assumes you pick up this data from an ADO.NET DataTable containing two columns.
// Initialize some position-related vars
...
// Initialize global graphic objects
Font fnt = new Font("Tahoma", 8);
SolidBrush textColor = new SolidBrush(Color.Blue);
// Create the chart
for(int i=0; i
{
// Calculate the roof of the chart
float f = Convert.ToSingle(dt.Rows[i]["Sales"])/
fMax*m_MaxBarHeight;
// Set some internal vars
int barBottom = m_MinYPos + m_MaxBarHeight;
int barHeight = (int) f;
int yBarTop = barBottom-barHeight;
int yCaption = barBottom-barHeight-m_TextHeight;
// Write value on top of the bar
int x = i * barWidth;
g.DrawString(dt.Rows[i]["Sales"].ToString(), fnt,
textColor, x, yCaption);
// Draw the bar using a gradient brush
Rectangle bar = new Rectangle(x, yBarTop, barWidth-10,
barHeight);
LinearGradientBrush brInterior;
brInterior = new LinearGradientBrush(
bar, Color.SkyBlue, Color.AntiqueWhite,
LinearGradientMode.Horizontal);
g.FillRectangle(brInterior, bar);
// Write the label at the bottom of the bar
g.DrawString(dt.Rows[i]["Employee"].ToString(), fnt,
textColor, x, barBottom+m_TextHeight);
}
Figure 2. This is the main loop that draws the bars of the chart, which represent the sales per year realized by all employees of the company. Each bar has the value drawn on top, and the label - figured here as the employee name - is drawn at the bottom.
In the sample DataTable object, the column named Sales represents the values to use for the chart, whereas the column named Employee represents the name of the employee and the label of the graphic. The actual drawing is crafted in three steps: The value is drawn first, then the bar, and last is the label. The bitmap's width is partitioned in as many columns as there are rows in the DataTable. Each column hosts a bar representing the sales of an employee. The height of each bar is calculated considering the height of the bitmap, the maximum height of the text for the label, and the value. The actual value is translated into a number of vertical pixels proportional to the maximum height for the bar chart. Each bar is rendered as a rectangular object - a GDI+ graphic primitive that you can draw, fill, and outline.
To fill a closed figure, you can use any valid GDI+ brush, including textured brushes, solid brushes, and gradients. In this case, I used a horizontal linear gradient. A linear gradient is fully identified, with two colors and two points. The interior of the figure varies from the starting color to the final color. By default, you are not required to specify the start and end points of the gradient, only a direction. Given the direction and the shape of the figure, the GDI+ engine determines the two most extreme points, and the LinearGradientBrush class implements the functionalities of a linear gradient. (As an aside, you can override this behavior and create gradients with more than two colors and over a multipoint path.)
To write text, call the DrawString method, and pass a font object, a brush object for the color of the text (this can be a gradient brush too), and the write-to position. Once you're done drawing the chart, you must save it. The Save method for the Bitmap class allows you to choose a number of storage media and image formats. The Response.OutputStream channel then sends the bits out to the client using the default Web server console. (Note that writing to the OutputStream property is roughly equivalent to using the ASP Response object's BinaryWrite method.) Figure 3 shows the output for a particular year (the data is taken from the Northwind database generic to SQL Server 2000).
Figure 3. Here is an ASP.NET page that performs a SQL query and creates
a bar chart. The image is then associated with an tag and displayed
as a GIF.
Cache Page Output Using ASP.NET
The solution outlined in this article works effectively and generates a new image per each user request. But is this solution really optimal in all cases? That depends on the data you have and its volatility. In some situations, caching created images for further use is an interesting and effective form of optimization. You might also think of saving the image to a disk file. Although practical, this solution has a couple drawbacks. First, by default, ASP.NET applications cannot create disk files unless you set special privileges. Second, you must determine an effective naming system to map user parameters to image files - and the price of this mapping is all yours.
A more effective approach - possible only in ASP.NET - is the utilization of the cache system to save copies of pages based on arguments. In this case, you could configure the caching subsystem to cache the barchart.aspx page (the page that actually produces the chart) based on the values of the Year attribute. Here, all the burden of the caching is taken care of by the ASP.NET runtime, thereby letting you escape without writing a single line of code. Caching page output is, however, a significant topic - one that deserves an article of its own. Stay tuned!
The files referenced in this article are available for download.
Dino Esposito is a trainer and consultant for Wintellect (http://www.wintellect.com), where he manages the ADO.NET class. Author of Building Web Solutions with ASP.NET and ADO.NET and Applied XML Programming for Microsoft .NET, both from Microsoft Press, Dino also is a co-founder of http://www.VB2TheMax.com. E-mail him at mailto:[email protected].
Tell us what you think! Please send any comments about this article to [email protected]. Please include the article title and author.