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

Tags:

Python

Math

Graph