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 spacethe "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_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;
In the preceding code, the arguments from
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()
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_dst = new SizeF();
sz_dst.Width = (from.Width / to.Width) *
sz_dst.Height = (from.Height / to.Height) *
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.|