Intelligently calculating chart tick positions
The following is what I've used for years which is simple and works well enough. Forgive me for it being C but translating to Python shouldn't be difficult.
The following function is needed and is from Graphic Gems volume 1.
double NiceNumber (const double Value, const int Round) {
int Exponent;
double Fraction;
double NiceFraction;
Exponent = (int) floor(log10(Value));
Fraction = Value/pow(10, (double)Exponent);
if (Round) {
if (Fraction < 1.5)
NiceFraction = 1.0;
else if (Fraction < 3.0)
NiceFraction = 2.0;
else if (Fraction < 7.0)
NiceFraction = 5.0;
else
NiceFraction = 10.0;
}
else {
if (Fraction <= 1.0)
NiceFraction = 1.0;
else if (Fraction <= 2.0)
NiceFraction = 2.0;
else if (Fraction <= 5.0)
NiceFraction = 5.0;
else
NiceFraction = 10.0;
}
return NiceFraction*pow(10, (double)Exponent);
}
Use it like in the following example to choose a "nice" start/end of the axis based on the number of major ticks you wish displayed. If you don't care about ticks you can just set it to a constant value (ex: 10).
//Input parameters
double AxisStart = 26.5;
double AxisEnd = 28.3;
double NumTicks = 10;
double AxisWidth;
double NewAxisStart;
double NewAxisEnd;
double NiceRange;
double NiceTick;
/* Check for special cases */
AxisWidth = AxisEnd - AxisStart;
if (AxisWidth == 0.0) return (0.0);
/* Compute the new nice range and ticks */
NiceRange = NiceNumber(AxisEnd - AxisStart, 0);
NiceTick = NiceNumber(NiceRange/(NumTicks - 1), 1);
/* Compute the new nice start and end values */
NewAxisStart = floor(AxisStart/NiceTick)*NiceTick;
NewAxisEnd = ceil(AxisEnd/NiceTick)*NiceTick;
AxisStart = NewAxisStart; //26.4
AxisEnd = NewAxisEnd; //28.4
I report here my python version of above C code if it may be of any help for someone:
import math
def nice_number(value, round_=False):
'''nice_number(value, round_=False) -> float'''
exponent = math.floor(math.log(value, 10))
fraction = value / 10 ** exponent
if round_:
if fraction < 1.5:
nice_fraction = 1.
elif fraction < 3.:
nice_fraction = 2.
elif fraction < 7.:
nice_fraction = 5.
else:
nice_fraction = 10.
else:
if fraction <= 1:
nice_fraction = 1.
elif fraction <= 2:
nice_fraction = 2.
elif fraction <= 5:
nice_fraction = 5.
else:
nice_fraction = 10.
return nice_fraction * 10 ** exponent
def nice_bounds(axis_start, axis_end, num_ticks=10):
'''
nice_bounds(axis_start, axis_end, num_ticks=10) -> tuple
@return: tuple as (nice_axis_start, nice_axis_end, nice_tick_width)
'''
axis_width = axis_end - axis_start
if axis_width == 0:
nice_tick = 0
else:
nice_range = nice_number(axis_width)
nice_tick = nice_number(nice_range / (num_ticks - 1), round_=True)
axis_start = math.floor(axis_start / nice_tick) * nice_tick
axis_end = math.ceil(axis_end / nice_tick) * nice_tick
return axis_start, axis_end, nice_tick
use as:
>>> nice_bounds(26.5, 28.3)
(26.4, 28.4, 0.2)
Also add a javascript porting:
function nice_number(value, round_){
//default value for round_ is false
round_ = round_ || false;
// :latex: \log_y z = \frac{\log_x z}{\log_x y}
var exponent = Math.floor(Math.log(value) / Math.log(10));
var fraction = value / Math.pow(10, exponent);
if (round_)
if (fraction < 1.5)
nice_fraction = 1.
else if (fraction < 3.)
nice_fraction = 2.
else if (fraction < 7.)
nice_fraction = 5.
else
nice_fraction = 10.
else
if (fraction <= 1)
nice_fraction = 1.
else if (fraction <= 2)
nice_fraction = 2.
else if (fraction <= 5)
nice_fraction = 5.
else
nice_fraction = 10.
return nice_fraction * Math.pow(10, exponent)
}
function nice_bounds(axis_start, axis_end, num_ticks){
//default value is 10
num_ticks = num_ticks || 10;
var axis_width = axis_end - axis_start;
if (axis_width == 0){
axis_start -= .5
axis_end += .5
axis_width = axis_end - axis_start
}
var nice_range = nice_number(axis_width);
var nice_tick = nice_number(nice_range / (num_ticks -1), true);
var axis_start = Math.floor(axis_start / nice_tick) * nice_tick;
var axis_end = Math.ceil(axis_end / nice_tick) * nice_tick;
return {
"min": axis_start,
"max": axis_end,
"steps": nice_tick
}
}