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


Build a Reusable Graphical Charting Engine with C# : Page 2

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.

Limitations of GDI+
If you're familiar with the System.Drawing namespace, you might well be wondering: Why would anyone write custom code to transform between data space and screen space when there are perfectly good GDI+ methods that seemingly handle the same tasks?

Good question. GDI+ provides considerable support for transforming between different coordinate systems. When you draw onto a Graphics object, it uses the current Graphics.Transform matrix to apply any transformations you may have specified. By default, GDI+ uses the identity matrix, but you can explicitly Translate(), Scale() and Rotate() that matrix. You can even set its members yourself (for example, to achieve a shear effect). Furthermore, because it's a matrix, there's nothing to stop you from creating a pipeline of transformations and applying them successively.

Listing 2 draws a graph of arbitrary point data entirely in data space, using the GDI to scale and translate it so that it fits within prescribed margins (you don't want the graph to fill the entire available area because you still need to draw the axes, axis titles and division markers). GDI+ certainly makes things simpler when you're doing all your drawing in data units rather than screen units—and, if you're sure this is what you want, GDI+ transformations are the way to go.

However, if you need to mix and match your drawing primitives and the spaces in which they are rendered, GDI+ transformations aren't the way to go. The following code fragment gives a clue as to why:

// Create the pen with a 1 pixel width Pen pen = new Pen(control.ForeColor, m_data_rect.Width / main_rect.Width);

By default, a pen has a width of one pixel. But when you transform the drawing space, it affects the coordinate system of all the drawing primitives, not just the way that some particular point gets transformed into screen space. It's as if you physically stuck a magnifying glass in front of the window. So you must selectively rescale Pens, Fonts and other GDI objects if you want to combine a scaled data space with unscaled legends. As you'll see, a useful chart should be able to draw (in addition to its data) a pair of axes with some kind of scale along them. Typically, each major division along the x-axis scale should be annotated with a value, and that value should be centred horizontally and displayed a few pixels below the division in question. If you transform everything into data space, you would need (at a minimum) to:

  1. Create a font to draw the division value, and scale it back into screen space (so the font is a sensible size)
  2. Calculate the width of the division annotation string in screen space
  3. Calculate where to draw the division marker in screen space, and offset it by half the width of the string
  4. Translate this back into data space
  5. Calculate the y offset for the string ("a few pixels") in data space
  6. Draw the string at the appropriate data space coordinates
Such needless complexity is a fairly compelling argument against drawing everything in data space using the GDI's ready-rolled transformations, but there are two even better reasons not to use the GDI transformations. First, you won't get what you want. Second, it's hit and miss whether they'll work at all. Here's why.

Suppose your data's x-axis scaling differs from its y-axis scaling (that's very common in charts). If you draw in data space, any fonts you create will be similarly scaled. When you create a font, you can specify its point size, but you can't control its aspect ratio. True, the Font object has thirteen constructors, and, I must confess, I should have thought at least one of them might provide a workaround, but I've yet to find it. You can expect worse than badly proportioned fonts, though. You may not get a font at all:

Graphics g = CreateGraphics(); Brush brush = new SolidBrush(Color.Red); try { // With equal scaling of 750,000:1, // the GDI doesn't complain g.ScaleTransform((float)750000.0, (float)750000.0); Font font = new Font("Arial", (float)0.000093); g.DrawString("Hi there", font, brush, new PointF(0,0)); // But with unequal scaling, the GDI can't cope g.ResetTransform(); g.ScaleTransform((float)750000.0, (float)1); font = new Font("Arial", (float)0.0000093); g.DrawString("Hi there", font, brush, new PointF(0,0)); g.ResetTransform(); g.ScaleTransform((float)1, (float)750000.0); font = new Font("Arial", (float)0.0000093); g.DrawString("Hi there", font, brush, new PointF(0,0)); } catch(System.Runtime.InteropServices. ExternalException ec) { // That was one unhappy object MessageBox.Show(ec.Message); }

If you run the preceding code, you'll find that the first string draws without problems, but you get an exception when the GDI tries to draw either the second or third strings. The code illustrates that scaling the data space doesn't in itself create a problem. Even when scaling by a factor of 750,000:1, the GDI still correctly renders a string with a specified font size. But the code also illustrates that when the data space is scaled unevenly, there's no simple way to determine how the scaling affects the chosen font, or what point setting might compensate for it. The moral is that GDI transforms have their uses, but think carefully before you do use them. Sometimes doing things yourself may be the only way to get the job done.

Comment and Contribute






(Maximum characters: 1200). You have 1200 characters left.



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