### WEBINAR:

On-Demand

Building the Right Environment to Support AI, Machine Learning and Deep Learning

**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))
+1);
```

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. |