Nice Label Algorithm for Charts with minimum ticks

19,553

Solution 1

You should be able to use the Java implementation with minor corrections.

Change maxticks to 5.

Change the calculate mehod to this:

private void calculate() {
        this.range = niceNum(maxPoint - minPoint, false);
        this.tickSpacing = niceNum(range / (maxTicks - 1), true);
        this.niceMin =
            Math.floor(minPoint / tickSpacing) * tickSpacing;
        this.niceMax = this.niceMin + tickSpacing * (maxticks - 1); // Always display maxticks
    }

Disclaimer: Note that I haven't tested this, so you may have to tweak it to make it look good. My suggested solution adds extra space at the top of the chart to always make room for 5 ticks. This may look ugly in some cases.

Solution 2

I am the author of "Algorithm for Optimal Scaling on a Chart Axis". It used to be hosted on trollop.org, but I have recently moved domains/blogging engines. Anyhow, I'll post the contents here for easier access.

I've been working on an Android charting application for an assignment and ran into a bit of an issue when it came to presenting the chart in a nicely scaled format. I spent a some time trying to create this algorithm on my own and came awfully close, but in the end I found a pseudo-code example in a book called "Graphics Gems, Volume 1" by Andrew S. Glassner. An excellent description of the problem is given in the chapter on "Nice Numbers for Graph Labels":

When creating a graph by computer, it is desirable to label the x and y axes with "nice" numbers: simple decimal numbers. For example, if the data range is 105 to 543, we'd probably want to plot the range from 100 to 600 and put tick marks every 100 units. Or if the data range is 2.04 to 2.16, we'd probably plot a range from 2.00 to 2.20 with a tick spacing of 0.05. Humans are good at choosing such "nice" numbers, but simplistic algorithms are not. The naïve label-selection algorithm takes the data range and divides it into n equal intervals, but this usually results in ugly tick labels. We here describe a simple method for generating nice graph labels.

The primary observation is that the "nicest" numbers in decimal are 1, 2, and 5, and all power-of-ten multiples of these numbers. We will use only such numbers for the tick spacing, and place tick marks at multiples of the tick spacing...

I used the pseudo-code example in this book to create the following class in Java:

public class NiceScale {

  private double minPoint;
  private double maxPoint;
  private double maxTicks = 10;
  private double tickSpacing;
  private double range;
  private double niceMin;
  private double niceMax;

  /**
   * Instantiates a new instance of the NiceScale class.
   *
   * @param min the minimum data point on the axis
   * @param max the maximum data point on the axis
   */
  public NiceScale(double min, double max) {
    this.minPoint = min;
    this.maxPoint = max;
    calculate();
  }

  /**
   * Calculate and update values for tick spacing and nice
   * minimum and maximum data points on the axis.
   */
  private void calculate() {
    this.range = niceNum(maxPoint - minPoint, false);
    this.tickSpacing = niceNum(range / (maxTicks - 1), true);
    this.niceMin =
      Math.floor(minPoint / tickSpacing) * tickSpacing;
    this.niceMax =
      Math.ceil(maxPoint / tickSpacing) * tickSpacing;
  }

  /**
   * Returns a "nice" number approximately equal to range Rounds
   * the number if round = true Takes the ceiling if round = false.
   *
   * @param range the data range
   * @param round whether to round the result
   * @return a "nice" number to be used for the data range
   */
  private double niceNum(double range, boolean round) {
    double exponent; /** exponent of range */
    double fraction; /** fractional part of range */
    double niceFraction; /** nice, rounded fraction */

    exponent = Math.floor(Math.log10(range));
    fraction = range / Math.pow(10, exponent);

    if (round) {
      if (fraction < 1.5)
        niceFraction = 1;
      else if (fraction < 3)
        niceFraction = 2;
      else if (fraction < 7)
        niceFraction = 5;
      else
        niceFraction = 10;
    } else {
      if (fraction <= 1)
        niceFraction = 1;
      else if (fraction <= 2)
        niceFraction = 2;
      else if (fraction <= 5)
        niceFraction = 5;
      else
        niceFraction = 10;
    }

    return niceFraction * Math.pow(10, exponent);
  }

  /**
   * Sets the minimum and maximum data points for the axis.
   *
   * @param minPoint the minimum data point on the axis
   * @param maxPoint the maximum data point on the axis
   */
  public void setMinMaxPoints(double minPoint, double maxPoint) {
    this.minPoint = minPoint;
    this.maxPoint = maxPoint;
    calculate();
  }

  /**
   * Sets maximum number of tick marks we're comfortable with
   *
   * @param maxTicks the maximum number of tick marks for the axis
   */
  public void setMaxTicks(double maxTicks) {
    this.maxTicks = maxTicks;
    calculate();
  }
}

We can then make use of the above code like this:

NiceScale numScale = new NiceScale(-0.085, 0.173);

System.out.println("Tick Spacing:\t" + numScale.getTickSpacing());
System.out.println("Nice Minimum:\t" + numScale.getNiceMin());
System.out.println("Nice Maximum:\t" + numScale.getNiceMax());

Which will then output nicely formatted numbers for use in whatever application for which you need to create pretty scales. =D

Tick Spacing: 0.05
Nice Minimum: -0.1
Nice Maximum: 0.2

Solution 3

Here is a javascript version:

var minPoint;
var maxPoint;
var maxTicks = 10;
var tickSpacing;
var range;
var niceMin;
var niceMax;

/**
 * Instantiates a new instance of the NiceScale class.
 *
 *  min the minimum data point on the axis
 *  max the maximum data point on the axis
 */
function niceScale( min, max) {
    minPoint = min;
    maxPoint = max;
    calculate();
    return {
        tickSpacing: tickSpacing,
        niceMinimum: niceMin,
        niceMaximum: niceMax
    };
}



/**
 * Calculate and update values for tick spacing and nice
 * minimum and maximum data points on the axis.
 */
function calculate() {
    range = niceNum(maxPoint - minPoint, false);
    tickSpacing = niceNum(range / (maxTicks - 1), true);
    niceMin =
      Math.floor(minPoint / tickSpacing) * tickSpacing;
    niceMax =
      Math.ceil(maxPoint / tickSpacing) * tickSpacing;
}

/**
 * Returns a "nice" number approximately equal to range Rounds
 * the number if round = true Takes the ceiling if round = false.
 *
 *  localRange the data range
 *  round whether to round the result
 *  a "nice" number to be used for the data range
 */
function niceNum( localRange,  round) {
    var exponent; /** exponent of localRange */
    var fraction; /** fractional part of localRange */
    var niceFraction; /** nice, rounded fraction */

    exponent = Math.floor(Math.log10(localRange));
    fraction = localRange / Math.pow(10, exponent);

    if (round) {
        if (fraction < 1.5)
            niceFraction = 1;
        else if (fraction < 3)
            niceFraction = 2;
        else if (fraction < 7)
            niceFraction = 5;
        else
            niceFraction = 10;
    } else {
        if (fraction <= 1)
            niceFraction = 1;
        else if (fraction <= 2)
            niceFraction = 2;
        else if (fraction <= 5)
            niceFraction = 5;
        else
            niceFraction = 10;
    }

    return niceFraction * Math.pow(10, exponent);
}

/**
 * Sets the minimum and maximum data points for the axis.
 *
 *  minPoint the minimum data point on the axis
 *  maxPoint the maximum data point on the axis
 */
function setMinMaxPoints( localMinPoint,  localMaxPoint) {
    minPoint = localMinPoint;
    maxPoint = localMaxoint;
    calculate();
}

/**
 * Sets maximum number of tick marks we're comfortable with
 *
 *  maxTicks the maximum number of tick marks for the axis
 */
function setMaxTicks(localMaxTicks) {
    maxTicks = localMaxTicks;
    calculate();
}

Enjoy!

Solution 4

I have converted above java code to Python as per my requirement.

 import math

  class NiceScale:
    def __init__(self, minv,maxv):
        self.maxTicks = 6
        self.tickSpacing = 0
        self.lst = 10
        self.niceMin = 0
        self.niceMax = 0
        self.minPoint = minv
        self.maxPoint = maxv
        self.calculate()

    def calculate(self):
        self.lst = self.niceNum(self.maxPoint - self.minPoint, False)
        self.tickSpacing = self.niceNum(self.lst / (self.maxTicks - 1), True)
        self.niceMin = math.floor(self.minPoint / self.tickSpacing) * self.tickSpacing
        self.niceMax = math.ceil(self.maxPoint / self.tickSpacing) * self.tickSpacing

    def niceNum(self, lst, rround):
        self.lst = lst
        exponent = 0 # exponent of range */
        fraction = 0 # fractional part of range */
        niceFraction = 0 # nice, rounded fraction */

        exponent = math.floor(math.log10(self.lst));
        fraction = self.lst / math.pow(10, exponent);

        if (self.lst):
            if (fraction < 1.5):
                niceFraction = 1
            elif (fraction < 3):
                niceFraction = 2
            elif (fraction < 7):
                niceFraction = 5;
            else:
                niceFraction = 10;
        else :
            if (fraction <= 1):
                niceFraction = 1
            elif (fraction <= 2):
                niceFraction = 2
            elif (fraction <= 5):
                niceFraction = 5
            else:
                niceFraction = 10

        return niceFraction * math.pow(10, exponent)

    def setMinMaxPoints(self, minPoint, maxPoint):
          self.minPoint = minPoint
          self.maxPoint = maxPoint
          self.calculate()

    def setMaxTicks(self, maxTicks):
        self.maxTicks = maxTicks;
        self.calculate()

a=NiceScale(14024, 17756)
print "a.lst ", a.lst
print "a.maxPoint ", a.maxPoint
print "a.maxTicks ", a.maxTicks
print "a.minPoint ", a.minPoint
print "a.niceMax ", a.niceMax
print "a.niceMin ", a.niceMin
print "a.tickSpacing ", a.tickSpacing

Solution 5

This is the Swift version:

class NiceScale {
    private var minPoint: Double
    private var maxPoint: Double
    private var maxTicks = 10
    private(set) var tickSpacing: Double = 0
    private(set) var range: Double = 0
    private(set) var niceMin: Double = 0
    private(set) var niceMax: Double = 0

    init(min: Double, max: Double) {
        minPoint = min
        maxPoint = max
        calculate()
    }

    func setMinMaxPoints(min: Double, max: Double) {
        minPoint = min
        maxPoint = max
        calculate()
    }

    private func calculate() {
        range = niceNum(maxPoint - minPoint, round: false)
        tickSpacing = niceNum(range / Double((maxTicks - 1)), round: true)
        niceMin = floor(minPoint / tickSpacing) * tickSpacing
        niceMax = floor(maxPoint / tickSpacing) * tickSpacing
    }

    private func niceNum(range: Double, round: Bool) -> Double {
        let exponent = floor(log10(range))
        let fraction = range / pow(10, exponent)
        let niceFraction: Double

        if round {
            if fraction <= 1.5 {
                niceFraction = 1
            } else if fraction <= 3 {
                niceFraction = 2
            } else if fraction <= 7 {
                niceFraction = 5
            } else {
                niceFraction = 10
            }
        } else {
            if fraction <= 1 {
                niceFraction = 1
            } else if fraction <= 2 {
                niceFraction = 2
            } else if fraction <= 5 {
                niceFraction = 5
            } else {
                niceFraction = 10
            }
        }

        return niceFraction * pow(10, exponent)
    }
}
Share:
19,553
Dominik Obermaier
Author by

Dominik Obermaier

Updated on June 04, 2022

Comments

  • Dominik Obermaier
    Dominik Obermaier almost 2 years

    I need to calculate the Ticklabels and the Tickrange for charts manually.

    I know the "standard" algorithm for nice ticks (see http://books.google.de/books?id=fvA7zLEFWZgC&pg=PA61&lpg=PA61&redir_esc=y#v=onepage&q&f=false) and I also know this Java implementation.

    The problem is, that with this algorithm, the ticks are "too smart". That means, The algorithm decides how much ticks should be displayed. My requirement is, that there are always 5 Ticks, but these should of course be "pretty". The naive approach would be to get the maximum value, divide with 5 and multiply with the ticknumber. The values here are - of course - not optimal and the ticks are pretty ugly.

    Does anyone know a solution for the problem or have a hint for a formal algorithm description?