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 3

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.

Adding Axis Scales
Because charts tend to depend on transformations between different coordinate spaces, it's imperative that they include information to help explain what they're visualizing. At a minimum, each axis should display at least two reference values and a divided scale, so readers can make a visual estimate of the data range. Initially, I thought it would be enough simply to divide each axis into ten intervals and label each accordingly, but I soon realized that such a naive approach was unacceptable for a variety of reasons

First, having a nice round number of intervals isn't always helpful or even sensible. For example, if you divide the range 1.72 to 7.27 into ten intervals, each interval ends up as 0.555, which is neither a sensible nor intuitive interval for users (see Figure 2).

Figure 2. Unintuitive Axes: If you determine your major intervals by simply dividing up the data range into a fixed number of segments, you are likely to end up with strange intervals.
Secondly, strings representing axis reference values may run to many decimal places, and accordingly take up large amounts of screen space. Unfortunately, rounding them to an appropriate number of significant digits is non trivial, because the level of significance required depends completely on the range of data being visualized. For example, you can easily round data between 1.72 to 7.27 to two decimal places, but if you apply the same rounding to data between 0.00172 and 0.00727, your reference values would be meaningless (see Figure 3).

Figure 3. Round Axis Divisions: If you don't explicitly specify a rounding scheme, you end up with axes like these. In this case, the values along the x-axis should clearly be rounded to a single decimal place.
Instead, charts typically use nice round intervals like 1000 or 0.1 or 0.01, both because they're easy to interpolate mentally, and because they occupy minimal screen space for their particular precision. But what if the data range doesn't cross a neat "power of 10" boundary? For example, given a data range from 57 to 63, you'd expect to see not ten, but seven numbered intervals—from 57 to 63 inclusive. Because of these complications, the sample code contains the method CalcDivIntervals(), which performs common work for the two methods that actually draw the axes, DrawXAxis() and DrawYAxis() (see Listing 3). I must confess that CalcDivIntervals() isn't the most intuitive piece of code, but it seems to do the job. I've marked it "virtual", so you can override it to draw axis scales to specific requirements. At its heart is the line:

   // Calculate a suitable exponent for the 
   // division marker based on 
   // the magnitude of the data range
   int mag = Math.Abs(((int) Math.Log10(1 / ax_size)) 
This code fragment determines what the major scale interval will be, based on the magnitude of the data range being displayed. For example, if the total range is 70,000, mag would receive a value of 4, meaning that major intervals should be displayed at 10 to the power of 4—or every 10,000 units. It would be nice if that were the end of the matter, and you could just draw in the major intervals right away, but you must also take into account the data range's offset. While the range might be 70,000, that doesn't necessarily mean the range is actually located between 0 and 70,000; for example, it might be between 30,000 and 100,000 (see Figure 4).

Figure 4. Invalid Data Range Offset: This figure illustrates calculating the axes using the correct range but the wrong offset. The charting engine has chosen appropriate major divisions for the two axes, but unhelpfully calculated the range relative to the graph's origin.

Accordingly, you need to round the low end of the range (let's call it min) to the nearest major interval, and then work your way back toward min by successive tenths of a major interval. This way, the chart engine draws divisions along the full length of the axis, and you can use the minor divisions as a guide when the axis contains only a few major divisions. Before doing that, though, you should check to see how many major divisions would be displayed, and adjust major_div accordingly, increasing its size if there are more than ten divisions, and reducing it if there are fewer than three, or you'll end up with something like Figure 5. These bounds are largely subjective, so feel free to adjust them accordingly. I chose them simply because they seemed to work well with all the test data I could throw at them.

Figure 5. Too Many Divisions: Having ten major divisions isn't always the correct solution, as this chart shows. Instead, you should adjust divisions based on the number of values that will actually display.

The actual axis rendering code merits little comment. Aside from a bit of color information, it really needs to know only two things—the data rectangle it should visualize (m_data_rect), and the screen rectangle in which to render it (m_screen_rect). It looks better if you center the scale's reference values around the axis divisions they annotate, so, after converting the relevant values into strings, measure them and offset them accordingly. Because you're calculating the string's dimensions anyway, you may as well check if the string you are about to draw intersects the last string you drew. That way, you can draw a reference value only when there's enough surrounding space to set it apart from its neighbouring values, which avoids situations like Figure 6. This simple idea works particularly well when you're rendering the chart in a resizable window (try the sample project to see it in action).

Figure 6. Overwritten Axis Values: Even with a sensible major division and offset, and a suitable number of major divisions, things can still get untidy when you try to shrink a chart too far.

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