RSS Feed
Download our iPhone app
Browse DevX
Sign up for e-mail newsletters from DevX


Build a Reusable Graphical Charting Engine with C#

The .NET framework contains everything you need to build this customizable line-graphing application that supports multiple overlaid data sets, each with its own color and line style. Unless you need extremely sophisticated charts, just draw your own.

t's not every day you need to need to draw a chart, but it happens just often enough for you to wish there were some standard System.Drawing object to help you. By "chart" I mean nothing more sophisticated than a graphical rendering of a set of points with a scale and a pair of labelled axes.

I have written chart-like objects to fulfill particular needs before, but had never ended up reusing them, primarily because either the drawing logic was too specialized to be of general use, or because the data source it visualized was too specialized to describe other, more generic kinds of data. My dilemma, therefore, had a fairly obvious solution: Develop a Chart class that decouples its drawing logic from the data it visualizes; use as simple a data format as possible, and expose useful properties, so other classes can perform custom rendering if required.

Designing the Class
A chart needn't be complicated. At heart, it should simply encapsulate the transformation between two kinds of coordinate space—the "data space" of a set of data, and the "screen space" where you decide to render that data. Although screen coordinates are usually expressed as integers, that's simply a convention; the Chart class should assume that both data and screen space can contain fractional intervals, which it can then transform easily from one to the other like this:

   PointF Transform(RectangleF from, RectangleF to, 
      PointF pt_src)
      PointF pt_dst = new PointF();
      pt_dst.X = (((pt_src.X - from.Left) / from.Width) 
         * to.Width) + to.Left;
      pt_dst.Y = (((pt_src.Y - from.Top) / from.Height) 
         * to.Height) + to.Top;
      return pt_dst;

In the preceding code, the arguments from and to refer to the source and destination spaces respectively, and pt_src identifies a point in the source space to be transformed. This is more elegant than writing paired DataToScreen() and ScreenToData() methods whose transformations are simply inversions of one another. Points are not the only things you need to transform, though. You also need to be able to transform extents:

   SizeF Transform(RectangleF from, RectangleF to, 
      SizeF sz_src)
      SizeF sz_dst = new SizeF();
      sz_dst.Width = (from.Width / to.Width) * 
      sz_dst.Height = (from.Height / to.Height) * 
      return sz_dst;

The code in Listing 1 starts putting these ideas together. It generates fifty random points, using different scales for the X and Y axes, and scales and translates the data to fill a predefined screen rectangle with a 20-pixel border (see Figure 1). The code is straightforward, but one of its data structures has a few idiosyncrasies. Sidebar 1 discusses some points worth bearing in mind when using the System.Drawing primitives.

Figure 1. Random Points: This chart shows a starting point for graphing fifty random points, but doesn't have any axes or labels.

Close Icon
Thanks for your registration, follow us on our social networks to keep up-to-date