The Life of a Programmer

Calculating pleasant stepping values for a chart

I needed pleasant range values for the charting API in Fuse. These are the values written by the ticks on the plot, typically on the Y-axis. In the interest of simplicity it should just work by default; provide the plot with raw data and let it work out good values. It’s a small but important detail in producing an aesthetically appealing chart. The algorithm I came up with is rather simple, but seems to work well.

pleasant

Range and steps

Let’s look at what we’re contending with first. The plot will be given a set of data, from which we calculate a maximum and minimum value. We need to divide this into a number of ticks. If we simply divided the range by a step count with no further processing we end up with ugly numbers like this:

unpleasant

A quick test to root out robotic colleagues is ask them whether they find these range values pleasant.

It doesn’t look as nice as the earlier chart. It’s missing a zero-line, and it requires more than a glance to understand the tick ranges.

The algorithm needs to adjust those values, the maximum, minimum, and step count, to produce something a human might choose.

The algorithm

I created a GetStepping function that modifies the values. For the example chart the input steps is always 8, and the min and max are based on random data in the range of roughly -150...150. In my test app I use a lot of constrained random ranges to help identify corner cases that may come up with actual data.

1
static public void GetStepping( ref int steps, ref float min, ref float max )

Hard-coding a step value, like 8, is not very practical for responsive displays. The API also provides for providing a size value instead, in which case the step count will be calculate based on the available space.

The full code is at the bottom of the article. I’ll go over bits of it here, adjusting slightly for clarity.

Powers of 10

We like nice clean numbers, such as multiples of 10. The first part of the code quantizes the range (max - min).

1
2
3
var step = range / steps;
var mag10 = Math.Ceil( Math.Log(step) / Math.Log(10) );
var stepSize = Math.Pow( 10, mag10 );

The stepSize gives us a decent step range for the ticks. We can adjust the input values with this and see what the chart might look like.

1
2
3
4
//oMin/oMax are copies of the input min and max values, so we always have the original
min = Math.Floor( oMin / stepSize )  * stepSize;
max = Math.Ceil( oMax / stepSize ) * stepSize;
steps = (int)Math.Round( (max-min) / stepSize );

This converts the min/max into multiples of the stepSize and calculates the required number of steps based on these new values. The Round is required to deal with floating point imprecision, otherwise the integer conversion would truncate a value like 3.99976 to 3, not the desired 4.

big_range

The numbers are clear at a quick glance, but there aren’t very many of them; the stepSize is quite large. There’s also a bit too much empty space at the top/bottom.

Other multiples

What other values do we find appealing in a chart? I used my powers of intuition, otherwise known as searching the internet, and found a pattern of pleasing values. The values are 2, 2.5, and 5 multiplied by various powers of ten, like 20, 250, and 0.5.

We seem to have a strong cultural liking for these values. We’ve even hard-coded this liking into our currency. The common denominations, both coins and bills, fit the pattern: 1¢, 2¢, 5¢, 10¢, 20¢, 25¢, 50¢, 5€, 10€, 20€, 50€, 100€.

I just needed to find a way to use these various values. Look back to the chart with the big 100 step increments, notice there aren’t many steps, far less than the desired number provided on input. If we reduce the step size we’ll get more steps.

Reducing to desired multiples of 2, 2.5 and 5 is done by dividing our first step size by 5, 4 and 2. The algorithm does this in a loop:

1
2
3
4
var trySteps = new[]{ 5, 4, 2, 1 };
for (int i=0; i < trySteps.Length; ++i )
{
    var stepSize = baseStepSize / trySteps[i];

1 is included to consider the original multiple of 10. For each trySteps we calculate a min, max and steps. Once steps is below the desired number of steps we stop.

1
2
if (steps <= desiredSteps)
    break;

Source Code

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
static public void GetStepping( ref int steps, ref float min, ref float max )
{
    var oMin = min;
    var oMax = max;
    var desiredSteps = steps;
    var range = max - min;

    //find magnitude and steps in powers of 10
    var step = range / steps;
    var mag10 = Math.Ceil( Math.Log(step) / Math.Log(10) );
    var baseStepSize = Math.Pow( 10, mag10 );

    //find common divisions to get closer to desiredSteps
    var trySteps = new[]{ 5, 4, 2, 1 };
    for (int i=0; i < trySteps.Length; ++i )
    {
        var stepSize = baseStepSize / trySteps[i];
        var ns = Math.Round( range / stepSize );
        //bail if anything didn't work, We can't check float.ZeroTolernace anywhere since we should
        //work on arbitrary range values
        if (Float.IsNaN(baseStepSize) || Float.IsNaN(ns) || (ns < 1))
            return;

        min = Math.Floor( oMin / stepSize )  * stepSize;
        max = Math.Ceil( oMax / stepSize ) * stepSize;
        steps = (int)Math.Round( (max-min) / stepSize );

        if (steps <= desiredSteps)
            break;
    }
}

Please join me on Discord to discuss, or ping me on Mastadon.

Calculating pleasant stepping values for a chart

A Harmony of People. Code That Runs the World. And the Individual Behind the Keyboard.

Mailing List

Signup to my mailing list to get notified of each article I publish.

Recent Posts