Rounding D3 Scale Values in JavaScript

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 )
52.5
505
10010
25025
50050
1,000100
10,0001,000
100,00010,000
1,000,000100,000
10,000,0001,000,000
100,000,00010,000,000
Rounding Multiplier Matrix

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
NumrResult
77.842575
Rounding Example

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 NumNum + ( r / 2 )r Result
240252.525250
242254.525250
244256.525250
246258.525250
248260.525250
250262.525275
25227750300
25427950300
25628150300
25828350300

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.

ABCDEFGH
“1” + B x “0”A x CD + ( F / 2)Round(E / F) X FG /C
FloatNumber of DPtenthsfraction to wholeNum + ( r / 2 )rResultResult to Float
0.01210012.252.52.50.025
0.0033100034.252.550.005
0.003565100000356381504000.004
0.711079.55101
0.13454100001345139510014000.14
0.864941000086499149100090000.9
1.41101416.55151.5
58015863106060

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

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s