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.

### 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:

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`

.

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; } } |

Categories: Programming, Use Case