Nice numbers part 1

Rendering “nice numbers” has been the subject of a few debates at Factmint over the last few weeks, as we expand our collection of data visualizations, so I thought I’d put some thoughts down…

What’s a “nice number”?

Well, that’s a difficult question; 1729 is a nice number but I am specifically talking about aesthetically pleasing numbers. For example, the series 10, 20, 30 is more aesthetically pleasing than 9, 18, 27 – certainly, if you were using those numbers of the ticks on a bar chart’s axis.

If you want five ticks on the side of bar chart, and the range is 0 – 8, you could do 0, 2, 4, 6, 8. But what about 0 – 5? That would be 0, 1.25, 2.5, 3.75, 5. How about if the chart is displayed on a small screen and you can only fit in 4 ticks, then 0 – 8 is 0, 2.666, 5.333, 8. Those are not nice numbers.

When you look at very big and very small numbers the problem is similar. 1452519892 is not nice; 1500000000 is better; 1.5bn is better still.

Rounding

So, JavaScript (as with many other languages) provides a simple solution to this problem: Number.prototype.toPrecision.

(1452519892).toPrecision(2); // "1.5e+9"

Doing better

Rounding to two significant figures is definitely an improvement but we can do a lot better, especially if you are not happy with standard form. First off, let’s parse a float so we can work with numbers again:

parseFloat((1452519892).toPrecision(2)); // 1500000000

Now, we need to know the power of 10 from the standard form bit. You could parse that from the "1.5e+9" but it’s easier to get as a log-base-10.

var number = parseFloat((1452519892).toPrecision(2)); // 1500000000
var order = Math.floor(Math.log10(number)); // 9

That is saying that the smallest power of 10 that is less than 1500000000 is 9.

Now we need some more fiddly code to deal with all of the cases. Let’s start with the big numbers:

function niceNumber(uglyNumber) {
var niceNumber = parseFloat((uglyNumber).toPrecision(2));
var order = Math.floor(Math.log10(niceNumber));

var suffix = '';
if (order >= 12) {
niceNumber = niceNumber / Math.pow(10, 12);
suffix = ' trillion';
} else if (order >= 9) {
niceNumber = niceNumber / Math.pow(10, 9);
suffix = 'bn';
} else if (order >= 6) {
niceNumber = niceNumber / Math.pow(10, 6);
suffix = 'm';
} else if (order >= 3) {
niceNumber = niceNumber / Math.pow(10, 3);
suffix = 'k';
}

return niceNumber + suffix;
}

niceNumber(1452519892); // "1.5bn"

That’s pretty cool and does what we want – now for the little ones…

There are a few ways you can deal with very small numbers. One option, is to switch back to standard form but to display it a little more elegantly. This example uses HTML in the output (which may not be an option for all cases) but you could use the “e” syntax or even a standard fraction.

function niceNumber(uglyNumber) {
var niceNumber = parseFloat((uglyNumber).toPrecision(2));
var order = Math.floor(Math.log10(niceNumber));

var suffix = '';
if (order >= 12) {
niceNumber = niceNumber / Math.pow(10, 12);
suffix = ' trillion';
} else if (order >= 9) {
niceNumber = niceNumber / Math.pow(10, 9);
suffix = 'bn';
} else if (order >= 6) {
niceNumber = niceNumber / Math.pow(10, 6);
suffix = 'm';
} else if (order >= 3) {
niceNumber = niceNumber / Math.pow(10, 3);
suffix = 'k';
} else if (order <= -3) {
niceNumber = niceNumber / Math.pow(10, order);
suffix = ' × 10<sup>' + order + '<\sup>';
}

return niceNumber + suffix;
}

console.log(niceNumber(0.000326343));

That returns:

3.3 × 10-4

Ranges

So, that sorts out rendering single values nicely but doesn’t fix the problems, outlined at the beginning of this post, when dealing with picking numbers in a range. Out issue of the picking 4 numbers from 0 – 8 would still be there, just to two decimal places. We have sorted that out in our visualizations and I’ll write about choosing sensible numbers in another post soon.

One Response to “Nice numbers part 1”

1. […] part 1 I talked about a problem when developing scales built algorithmically. For example choosing numbers […]