D3 Scales make creating chart axis simple by passing the extent (min / max) values to the scale.domain. Unfortuanly our values mean axis extremes aren’t always “nice”, they are absolute and not rounding. Not always a problem but can lead to an ugly ugly chart.
For example lets say our min / max values are 14 and 72. Our D3 axis would compute as follows.

If we were to add the .nice() parameter when we construct the scale we get something a little better.

Its still the same date, just rounded of for the purpose of the scales. This is great but you’re still at the mercy of D3.scale.nice function…
This happened to me recently, I wanted more control over how numbers were rounded up or down. What multiple did they scale too.. Here is a table which shows how I wanted positive numbers should be rounded. This has to work for any positive number and fraction.
Max <= | Multiple ( r ) |
5 | 2.5 |
50 | 5 |
100 | 10 |
250 | 25 |
500 | 50 |
1,000 | 100 |
10,000 | 1,000 |
100,000 | 10,000 |
1,000,000 | 100,000 |
10,000,000 | 1,000,000 |
100,000,000 | 10,000,000 |
JavaScript has a round function and if used in conjunction with a multiplier (r) it could round any value to the nearest (r):
let result = Math.round( num / r ) * r
Num | r | Result |
77.84 | 25 | 75 |
So far so easy, when we round up we need to a ( R / 2) to the original number, this will mean the above example will result in 100
Max Num | Num + ( r / 2 ) | r | Result |
240 | 252.5 | 25 | 250 |
242 | 254.5 | 25 | 250 |
244 | 256.5 | 25 | 250 |
246 | 258.5 | 25 | 250 |
248 | 260.5 | 25 | 250 |
250 | 262.5 | 25 | 275 |
252 | 277 | 50 | 300 |
254 | 279 | 50 | 300 |
256 | 281 | 50 | 300 |
258 | 283 | 50 | 300 |
Again, not to complex. The real challenge came when dealing with fractions less than 1. For that I had to expend the table to convert any fractions or Floating number into Integers. For this I had to count the number of decimal places so I knew how many tenths (i think that the right word) to use.
A | B | C | D | E | F | G | H |
“1” + B x “0” | A x C | D + ( F / 2) | Round(E / F) X F | G /C | |||
Float | Number of DP | tenths | fraction to whole | Num + ( r / 2 ) | r | Result | Result to Float |
0.01 | 2 | 100 | 1 | 2.25 | 2.5 | 2.5 | 0.025 |
0.003 | 3 | 1000 | 3 | 4.25 | 2.5 | 5 | 0.005 |
0.00356 | 5 | 100000 | 356 | 381 | 50 | 400 | 0.004 |
0.7 | 1 | 10 | 7 | 9.5 | 5 | 10 | 1 |
0.1345 | 4 | 10000 | 1345 | 1395 | 100 | 1400 | 0.14 |
0.8649 | 4 | 10000 | 8649 | 9149 | 1000 | 9000 | 0.9 |
1.4 | 1 | 10 | 14 | 16.5 | 5 | 15 | 1.5 |
58 | 0 | 1 | 58 | 63 | 10 | 60 | 60 |
Doing this analysis ensured the code used to round numbers to the multiple of my choosing worked well. If you know of any other code out there that does the same job better please post in the comments.
Thanks
const countDecimals = (num) => {
if (Math.floor(num.valueOf()) === num.valueOf()) return 0;
var str = num.toString();
if (str.indexOf(".") !== -1 && str.indexOf("-") !== -1) {
return str.split("-")[1] || 0;
} else if (str.indexOf(".") !== -1) {
return str.split(".")[1].length || 0;
}
return str.split("-")[1] || 0;
}
const axisRound = (numFloat,direction) =>{
let tenths = parseInt("1" + "0".repeat( countDecimals(numFloat)))
let integer = numFloat * tenths
// define r
if(integer <= 5) { r = 2.5 }
else if (integer <= 50) { r = 5 }
else if (integer <= 100) { r = 10 }
else if (integer <= 250) { r = 25 }
else if (integer <= 500) { r = 50 }
else if (integer <= 1000) { r = 100 }
else if (integer <= 10000) { r = 1000 }
else if (integer <= 100000) { r = 10000 }
else if (integer <= 1000000) { r = 100000 } // 1 Million
else if (integer <= 10000000) { r = 1000000 }
else if (integer <= 100000000) { r = 10000000 }
else { r = 100000000 };
if( direction==='up'){ integerOffsett = integer + (r / 2)}
else if (direction==='down'){ integerOffsett = integer - (r / 2)}
else { integerOffsett = integer};
let integerOffsettResult = Math.round(integerOffsett / r ) * r
let result = integerOffsettResult / tenths
return result;
};
Here’s a link to a google sheet with all my working outs:
https://docs.google.com/spreadsheets/d/1B2Fg2vfiEbOGQF2cABvFUD5OnBY_3ola0IZQ6UgwmIA/edit?usp=sharing