Dominik Obermaier
Dominik Obermaier

Reputation: 5790

Nice Label Algorithm for Charts with minimum ticks

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?

Upvotes: 41

Views: 25296

Answers (22)

StephenDriffill
StephenDriffill

Reputation: 1

First of all thanks very much for this algorithm and everyone who has conveniently converted to their language of choice! 👍

I'm implementing in a JS application with Typescript and have wound up with some variations to the original.

  1. Due to floating point number issues causing the loop condition to fail to match niceMax sometimes, having to round the ticks to the number of dps present in the tick spacing.

  2. Have used <= in the for loop as we wanted the max point to be bounded by an upper tick - this I think is just preference.

  3. I found the the original algorithm did not strictly adhere to the maxTicks constraint and would sometimes return more ticks than specified. Whilst I haven't studied the algorithm, I think the reason was due to converting the range to a "nice number" which can inflate the number of required ticks, although the "rounded nice number" for the spacing seemed to counteract this in many but not all cases.

    In my variation I've kept the original range and ditched the "rounded nice num" for the spacing. This works well - the min and max points are always bound. However, the maxTicks param can be exceeded by 1 in certain cases. For example with maxTicks=8, min=299, max=601 we get 9 ticks with spacing of 50. To counteract this, we run the algorithm a second time with maxTicks reduced by 1. Not the cleanest solution, but I'm pretty sure ensures the maxTicks is strictly guaranteed.

Thoughts greatly welcome on the merits/pitfalls of this version, and whether it can be improved.

Disclaimer: code not thoroughly tested, and only with +ve numbers

function round(num: number, dp: number = 0) {
  return Math.round((num + Number.EPSILON) * 10 ** dp) / 10 ** dp;
}

function countDecimals(value: number) {
  if (Math.floor(value) === value) return 0;
  return value.toString().split('.')[1].length || 0;
}

function niceNumUpper(range: number) {
  const exponent = Math.floor(Math.log10(range));
  const fraction = range / Math.pow(10, exponent);
  let niceFraction: number;

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

/**
 * Calculate optimum number of ticks for an axis.
 * @param min
 * @param max
 * @param maxTicks
 * @returns number[]
 */
function calculateTicks(min: number, max: number, maxTicks: number) {
  function getTickDetails(sections: number) {
    const range = max - min;
    const tickSpacing = niceNumUpper(range / sections);
    const decimals = countDecimals(tickSpacing);

    const niceMin = round(
      Math.floor(min / tickSpacing) * tickSpacing,
      decimals,
    );
    const niceMax = round(Math.ceil(max / tickSpacing) * tickSpacing, decimals);

    return { tickSpacing, niceMin, niceMax, decimals };
  }

  const ticks = [];
  let tickSpacing: number, niceMin: number, niceMax: number, decimals: number;

  // # sections = ticks - 1
  ({ tickSpacing, niceMin, niceMax, decimals } = getTickDetails(maxTicks - 1));

  // -2 if we have overshot max ticks
  const tickCount = (niceMax - niceMin) / tickSpacing + 1;
  if (tickCount > maxTicks) {
    ({ tickSpacing, niceMin, niceMax, decimals } = getTickDetails(
      maxTicks - 2,
    ));
  }

  for (let i = niceMin; i <= niceMax; i = round(i + tickSpacing, decimals)) {
    ticks.push(i);
  }

  return ticks;
}

Upvotes: 0

David
David

Reputation: 311

Here are some Swift Unit Test to verify the definition of "Nice" meets your understanding.

import XCTest @testable import AssetsFolio

final class NiceGridLineValues_Tests: XCTestCase {

override func setUpWithError() throws {
    // Put setup code here. This method is called before the invocation of each test method in the class.
}

override func tearDownWithError() throws {
    // Put teardown code here. This method is called after the invocation of each test method in the class.
}

func test_NiceGridLinesValues_originalExample() throws {
    let numScale = NiceGridLineValues(min: -0.085, max: 0.173)
    
    /*
    // 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
     */
    
    XCTAssertEqual(0.05, numScale.tickSpacing)
    XCTAssertEqual(-0.1, numScale.niceMin)
    XCTAssertEqual(0.2,  numScale.niceMax)
}


func test_NiceGridLinesValues_173_183_example() throws {
    let numScale = NiceGridLineValues(min: 173.82, max: 182.93)
    
    XCTAssertEqual(1.0, numScale.tickSpacing)
    XCTAssertEqual(173.0, numScale.niceMin)
    XCTAssertEqual(183.0,  numScale.niceMax)
}

func test_NiceGridLinesValues_176_180_example() throws {
    let numScale = NiceGridLineValues(min: 176.82, max: 179.99)
    
    XCTAssertEqual(0.5, numScale.tickSpacing)
    XCTAssertEqual(176.5, numScale.niceMin)
    XCTAssertEqual(180.0,  numScale.niceMax)
}

func test_NiceGridLinesValues_179_180_example() throws {
    let numScale = NiceGridLineValues(min: 179.62, max: 179.99)
    
    XCTAssertEqual(0.05, numScale.tickSpacing)
    XCTAssertEqual(179.6, numScale.niceMin, accuracy: 0.01)
    XCTAssertEqual(180.0,  numScale.niceMax)
}

}

Upvotes: 0

Greg
Greg

Reputation: 1251

The solution produces nice results, but often too few tick marks depending on the min and max point values and the snapping effect.

Here's a quick hack fix to improve the tick mark spacing.

Original:

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

New:

  private void calculate() {
    this.range = niceNum(maxPoint - minPoint, false);
    this.tickSpacing = niceNum(range / (maxTicks - 1), true);

    // Attempt to correct tick spacing due to snapping behavior and nice method.
    double tickCountF = (maxPoint - minPoint) / tickSpacing;
    if( (tickCountF / maxTicks) <= 0.5)
        { tickSpacing /= 2.0; }

    this.niceMin = Math.floor(minPoint / tickSpacing) * tickSpacing;
    this.niceMax = Math.ceil(maxPoint / tickSpacing) * tickSpacing;
  }

Upvotes: 0

Enrique Gonz&#225;lez
Enrique Gonz&#225;lez

Reputation: 241

Another JS version:

   const getTicks = {
                minPoint: 0,
                maxPoint: 10,
                maxTicks: 10,
                tickSpacing: 1,
                range: 1,
                niceMin: 1,
                niceMax: 1,
                niceScale(min, max) {
                    this.minPoint = min;
                    this.maxPoint = max;
                    this.calculate();
                    return {
                        tickSpacing: this.tickSpacing,
                        niceMinimum: this.niceMin,
                        niceMaximum: this.niceMax
                    };
                },
                calculate() {
                    this.range = this.niceNum(this.maxPoint - this.minPoint, false);
                    this.tickSpacing = this.niceNum(this.range / (this.maxTicks - 1), true);
                    this.niceMin = Math.floor(this.minPoint / this.tickSpacing) * this.tickSpacing;
                    this.niceMax = Math.ceil(this.maxPoint / this.tickSpacing) * this.tickSpacing;
                },
                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);
                },
                setMinMaxPoints(localMinPoint, localMaxPoint) {
                    this.minPoint = localMinPoint;
                    this.maxPoint = localMaxPoint;
                    this.calculate();
                },
                setMaxTicks(localMaxTicks) {
                    this.maxTicks = localMaxTicks;
                    this.calculate();
                }
            }

Just the code entered before, embedding it inside a JS object (I don't like the global vars). It can be used like this:

const tickScale = getTicks.niceScale(minValue, maxValue);
    
for (let i = tickScale.niceMinimum; i < tickScale.niceMaximum; i+=tickScale.tickSpacing) {
      // Whatever you want to do
    }

I find it quite useful this way, and easer to isolate from the rest of the code.

Upvotes: 1

Ruur
Ruur

Reputation: 205

Here is a Ruby version

class NiceScale
  
  attr_accessor :min_point, :max_point
  attr_reader :tick_spacing, :nice_min, :nice_max
  
  def initialize(options = {})
    @min_point = options[:min_point]
    @max_point = options[:max_point]
    @max_ticks = [(options[:max_ticks] || 5), 2].max
    
    self.calculate
  end
  
  def calculate
    range = nice_num(@max_point - @min_point, false)
    @tick_spacing = nice_num(range / (@max_ticks - 1))
    @nice_min = (@min_point / tick_spacing).floor * tick_spacing
    @nice_max = (@max_point / tick_spacing).floor * tick_spacing
  end
  
  private
  
  def nice_num(num, round = true)
    num = num.to_f
    exponent = num > 0 ? Math.log10(num).floor : 0
    fraction = num / (10 ** exponent)
    
    if round
      if fraction < 1.5
        nice_fraction = 1
      elsif fraction < 3
        nice_fraction = 2
      elsif fraction < 7
        nice_fraction = 5
      else
        nice_fraction = 10
      end
    else
      if fraction <= 1
        nice_fraction = 1
      elsif fraction <= 2
        nice_fraction = 2
      elsif fraction <= 5
        nice_fraction = 5
      else
        nice_fraction = 10
      end
    end
    
    nice_fraction.to_f * (10 ** exponent)
  end
end

Upvotes: 0

user3585825
user3585825

Reputation: 25

//Swift, more compact

 
public struct NiceScale
{
var minPoint: Double
var maxPoint: Double
var maxTicks = 10
var tickSpacing: Double { niceNum(range: range / Double(maxTicks - 1), round: true) }
var range: Double { niceNum(range: maxPoint - minPoint, round: false) }
var niceMin: Double { floor(minPoint / tickSpacing) * tickSpacing }
var niceMax: Double { ceil(maxPoint / tickSpacing) * tickSpacing }

// min the minimum data point on the axis // max the maximum data point on the axis init( min: Double, max: Double, maxTicks: Int = 10) { minPoint = min maxPoint = max self.maxTicks = maxTicks } /** * Returns a "nice" number approximately equal to range Rounds * the number if round = true Takes the ceiling * if round = false range the data range * round whether to round the result * return a "nice" number to be used for the data range */ func niceNum( range: Double, round: Bool) -> Double { let exponent: Double = floor(log10(range)) // exponent of range let fraction: Double = range / pow(10, exponent) // fractional part of range var niceFraction: Double = 10.0 // nice, rounded fraction if round { if fraction < 1.5 { niceFraction = 1 } else if fraction < 3 { niceFraction = 2 } else if fraction < 7 { niceFraction = 5 } } else { if fraction <= 1 { niceFraction = 1 } else if fraction <= 2 { niceFraction = 2 } else if fraction <= 5 { niceFraction = 5 } } return niceFraction * pow(10, exponent) } static func testNiceScale() { var numScale = NiceScale(min: -0.085, max: 0.173) print("Tick Spacing:\t \( numScale.tickSpacing)") print("Nice Minimum:\t\( numScale.niceMin)") print("Nice Maximum:\t\( numScale.niceMax)") numScale = NiceScale(min: 4.44, max: 7.962) print("nice num:\t\( numScale.niceNum(range: 7.962 - 4.44, round: false))") print("Tick Spacing:\t\( numScale.tickSpacing)") print("Nice Minimum:\t\( numScale.niceMin)") print("Nice Maximum:\t\( numScale.niceMax)") }

} <\pre> <\code>

Upvotes: 1

Grzegorz Adam Hankiewicz
Grzegorz Adam Hankiewicz

Reputation: 7681

Since everybody and his dog is posting a translation to other popular languages, here is my version for the Nim programming language. I also added handling of cases where the amount of ticks is less than two:

import math, strutils

const
  defaultMaxTicks = 10

type NiceScale = object
  minPoint: float
  maxPoint: float
  maxTicks: int
  tickSpacing: float
  niceMin: float
  niceMax: float

proc ff(x: float): string =
  result = x.formatFloat(ffDecimal, 3)

proc `$`*(x: NiceScale): string =
  result = "Input minPoint: " & x.minPoint.ff &
    "\nInput maxPoint: " & x.maxPoint.ff &
    "\nInput maxTicks: " & $x.maxTicks &
    "\nOutput niceMin: " & x.niceMin.ff &
    "\nOutput niceMax: " & x.niceMax.ff &
    "\nOutput tickSpacing: " & x.tickSpacing.ff &
    "\n"

proc calculate*(x: var NiceScale)

proc init*(x: var NiceScale; minPoint, maxPoint: float;
    maxTicks = defaultMaxTicks) =
  x.minPoint = minPoint
  x.maxPoint = maxPoint
  x.maxTicks = maxTicks
  x.calculate

proc initScale*(minPoint, maxPoint: float;
    maxTicks = defaultMaxTicks): NiceScale =
  result.init(minPoint, maxPoint, maxTicks)

proc niceNum(scaleRange: float; doRound: bool): float =
  var
    exponent: float ## Exponent of scaleRange.
    fraction: float ## Fractional part of scaleRange.
    niceFraction: float ## Nice, rounded fraction.

  exponent = floor(log10(scaleRange));
  fraction = scaleRange / pow(10, exponent);

  if doRound:
    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 * pow(10, exponent)

proc calculate*(x: var NiceScale) =
  assert x.maxPoint > x.minPoint, "Wrong input range!"
  assert x.maxTicks >= 0, "Sorry, can't have imaginary ticks!"
  let scaleRange = niceNum(x.maxPoint - x.minPoint, false)
  if x.maxTicks < 2:
    x.niceMin = floor(x.minPoint)
    x.niceMax = ceil(x.maxPoint)
    x.tickSpacing = (x.niceMax - x.niceMin) /
      (if x.maxTicks == 1: 2.0 else: 1.0)
  else:
    x.tickSpacing = niceNum(scaleRange / (float(x.maxTicks - 1)), true)
    x.niceMin = floor(x.minPoint / x.tickSpacing) * x.tickSpacing
    x.niceMax = ceil(x.maxPoint / x.tickSpacing) * x.tickSpacing

when isMainModule:
  var s = initScale(57.2, 103.3)
  echo s

This is the comment stripped version. Full one can be read at GitHub integrated into my project.

Upvotes: 1

Jonas
Jonas

Reputation: 11

Dart / Flutter Version:

import 'dart:math';

void main() {
  double min = 3, max = 28;

  var scale = NiceScale(min, max, 5);

  print("Range: $min-$max; Max Point: ${scale.niceMax}; Min Point: ${scale.niceMin}; Steps: ${scale.tickSpacing};");
}

class NiceScale {
  double _niceMin, _niceMax;
  double _tickSpacing;

  double get tickSpacing { return _tickSpacing; }
  double get niceMin{ return _niceMin; }
  double get niceMax{ return _niceMax; }

  double _minPoint, _maxPoint;
  double _maxTicks;
  double _range;

  NiceScale(double minP, double maxP, double maxTicks){
    this._minPoint = minP;
    this._maxPoint = maxP;
    this._maxTicks = maxTicks;
    _calculate();
  }

  void _calculate(){
    _range = _niceNum(_maxPoint - _minPoint, false);
    _tickSpacing = _niceNum(_range / (_maxTicks - 1), true);
    _niceMin = _calcMin();
    _niceMax = _calcMax();
  }

  double _calcMin() {
    int floored = (_minPoint / _tickSpacing).floor();
    return floored * _tickSpacing;
  }

  double _calcMax() {
    int ceiled = (_maxPoint / _tickSpacing).ceil();
    return ceiled * _tickSpacing;
  }

  double _niceNum(double range, bool round){
    double exponent; /** exponent of range */
    double fraction; /** fractional part of range */
    double niceFraction; /** nice, rounded fraction */

    exponent = (log(range)/ln10).floor().toDouble();
    fraction = range / 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 * pow(10, exponent);
  }
}

Upvotes: 1

gentlee
gentlee

Reputation: 3717

Much BETTER and SIMPLER algorythm on swift. Size is fixed, values are not "hardcoded":

class NiceNumbers {
    /// Returns nice range of specified size. Result min <= min argument, result max >= max argument.
    static func getRange(forMin minInt: Int, max maxInt: Int, ofSize size: Int) -> [Int] {
        let niceMinInt = getMinCloseToZero(min: minInt, max: maxInt)
        let step = Double(maxInt - niceMinInt) / Double(size - 1)
        let niceStepInt = Int(get(for: step, min: false))

        var result = [Int]()
        result.append(niceMinInt)
        for i in 1...size - 1 {
            result.append(niceMinInt + i * Int(niceStepInt))
        }
        return result
    }

    /// Returns nice min or zero if it is much smaller than max.
    static func getMinCloseToZero(min: Int, max: Int) -> Int {
        let nice = get(for: Double(min), min: true)
        return nice <= (Double(max) / 10) ? 0 : Int(nice)
    }

    /// Get nice number. If min is true returns smaller number, if false - bigger one.
    static func get(for number: Double, min: Bool) -> Double {
        if number == 0 { return 0 }
        let exponent = floor(log10(number)) - (min ? 0 : 1)
        let fraction = number / pow(10, exponent)
        let niceFraction = min ? floor(fraction) : ceil(fraction)
        return niceFraction * pow(10, exponent)
    }
}

Tested only on positive numbers.

Upvotes: 0

Drunken Daddy
Drunken Daddy

Reputation: 8001

Here'a better organized C# code.

public class NiceScale
{

    public double NiceMin { get; set; }
    public double NiceMax { get; set; }
    public double TickSpacing { get; private set; }

    private double _minPoint;
    private double _maxPoint;
    private double _maxTicks = 5;
    private double _range;

    /**
     * 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)
    {
        _minPoint = min;
        _maxPoint = max;
        Calculate();
    }

    /**
     * Calculate and update values for tick spacing and nice
     * minimum and maximum data points on the axis.
     */
    private void Calculate()
    {
        _range = NiceNum(_maxPoint - _minPoint, false);
        TickSpacing = NiceNum(_range / (_maxTicks - 1), true);
        NiceMin = Math.Floor(_minPoint / TickSpacing) * TickSpacing;
        NiceMax = Math.Ceiling(_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, bool 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)
    {
        _minPoint = minPoint;
        _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)
    {
        _maxTicks = maxTicks;
        Calculate();
    }
}

Upvotes: 2

Algar
Algar

Reputation: 5984

Here's the Kotlin version!

import java.lang.Math.*

/**
 * Instantiates a new instance of the NiceScale class.
 *
 * @param min Double The minimum data point.
 * @param max Double The maximum data point.
 */
class NiceScale(private var minPoint: Double, private var maxPoint: Double) {

    private var maxTicks = 15.0
    private var range: Double = 0.0
    var niceMin: Double = 0.0
    var niceMax: Double = 0.0
    var tickSpacing: Double = 0.0

    init {
        calculate()
    }

    /**
     * Calculate and update values for tick spacing and nice
     * minimum and maximum data points on the axis.
     */
    private fun calculate() {
        range = niceNum(maxPoint - minPoint, false)
        tickSpacing = niceNum(range / (maxTicks - 1), true)
        niceMin = floor(minPoint / tickSpacing) * tickSpacing
        niceMax = 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 Double The data range.
     * @param round Boolean Whether to round the result.
     * @return Double A "nice" number to be used for the data range.
     */
    private fun niceNum(range: Double, round: Boolean): Double {
        /** Exponent of range  */
        val exponent: Double = floor(log10(range))
        /** Fractional part of range  */
        val fraction: Double
        /** Nice, rounded fraction  */
        val niceFraction: Double

        fraction = range / pow(10.0, exponent)

        niceFraction = if (round) {
            when {
                fraction < 1.5 -> 1.0
                fraction < 3 -> 2.0
                fraction < 7 -> 5.0
                else -> 10.0
            }
        } else {
            when {
                fraction <= 1 -> 1.0
                fraction <= 2 -> 2.0
                fraction <= 5 -> 5.0
                else -> 10.0
            }
        }

        return niceFraction * pow(10.0, exponent)
    }

    /**
     * Sets the minimum and maximum data points.
     *
     * @param minPoint Double The minimum data point.
     * @param maxPoint Double The maximum data point.
     */
    fun setMinMaxPoints(minPoint: Double, maxPoint: Double) {
        this.minPoint = minPoint
        this.maxPoint = maxPoint
        calculate()
    }

    /**
     * Sets maximum number of tick marks we're comfortable with.
     *
     * @param maxTicks Double The maximum number of tick marks.
     */
    fun setMaxTicks(maxTicks: Double) {
        this.maxTicks = maxTicks
        calculate()
    }
}

Upvotes: 1

Chris Smith
Chris Smith

Reputation: 3012

Here it is in TypeScript!

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

/**
 * 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
 */
function niceNum(range: number, round: boolean): number {
    let exponent: number;
    /** exponent of range */
    let fraction: number;
    /** fractional part of range */
    let niceFraction: number;
    /** 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);
}

Upvotes: 4

Pierre
Pierre

Reputation: 4414

Here's the C++ version. As a bonus you get a function that returns the minimum number of decimal points to display the tick labels on the axis.

The header file:

class NiceScale 
{   public:

    float minPoint;
    float maxPoint;
    float maxTicks;
    float tickSpacing;
    float range;
    float niceMin;
    float niceMax;

    public:
    NiceScale()
    {   maxTicks = 10;
    }

    /**
    * 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
    */
    NiceScale(float min, float max) 
    {   minPoint = min;
        maxPoint = max;
        calculate();
    }

    /**
    * Calculate and update values for tick spacing and nice
    * minimum and maximum data points on the axis.
    */
    void calculate() ;

    /**
    * 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
    */
    float niceNum(float range, boolean round) ;

    /**
    * 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
    */
    void setMinMaxPoints(float minPoint, float maxPoint) ;

    /**
    * Sets maximum number of tick marks we're comfortable with
    *
    * @param maxTicks the maximum number of tick marks for the axis
    */
    void setMaxTicks(float maxTicks) ;
    int decimals(void);
};

And the CPP file:

/**
* Calculate and update values for tick spacing and nice
* minimum and maximum data points on the axis.
*/
void NiceScale::calculate() 
{
    range = niceNum(maxPoint - minPoint, false);
    tickSpacing = niceNum(range / (maxTicks - 1), true);
    niceMin = floor(minPoint / tickSpacing) * tickSpacing;
    niceMax = 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
*/
float NiceScale::niceNum(float range, boolean round) 
{   float exponent; /** exponent of range */
    float fraction; /** fractional part of range */
    float niceFraction; /** nice, rounded fraction */

    exponent = floor(log10(range));
    fraction = range / pow(10.f, 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 * 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
*/
void NiceScale::setMinMaxPoints(float minPoint, float 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
*/
void NiceScale::setMaxTicks(float maxTicks) 
{
    this->maxTicks = maxTicks;
    calculate();
}

// minimum number of decimals in tick labels
// use in sprintf statement:
// sprintf(buf, "%.*f", decimals(), tickValue);
int NiceScale::decimals(void)
{
    float logTickX = log10(tickSpacing);
    if(logTickX >= 0)
        return 0;
    return (int)(abs(floor(logTickX)));
}

Upvotes: 4

Glenn Gordon
Glenn Gordon

Reputation: 1446

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!

Upvotes: 10

uqji
uqji

Reputation: 195

This is the VB.NET version.

Public Class NiceScale

Private minPoint As Double
Private maxPoint As Double
Private maxTicks As Double = 10
Private tickSpacing
Private range As Double
Private niceMin As Double
Private niceMax As Double

Public Sub New(min As Double, max As Double)
    minPoint = min
    maxPoint = max
    calculate()
End Sub

Private Sub calculate()
    range = niceNum(maxPoint - minPoint, False)
    tickSpacing = niceNum(range / (maxTicks - 1), True)
    niceMin = Math.Floor(minPoint / tickSpacing) * tickSpacing
    niceMax = Math.Ceiling(maxPoint / tickSpacing) * tickSpacing
End Sub

Private Function niceNum(range As Double, round As Boolean) As Double
    Dim exponent As Double '/** exponent of range */
    Dim fraction As Double '/** fractional part of range */
    Dim niceFraction As Double '/** nice, rounded fraction */

    exponent = Math.Floor(Math.Log10(range))
    fraction = range / Math.Pow(10, exponent)

    If round Then
        If (fraction < 1.5) Then
            niceFraction = 1
        ElseIf (fraction < 3) Then
            niceFraction = 2
        ElseIf (fraction < 7) Then
            niceFraction = 5
        Else
            niceFraction = 10
        End If
    Else
        If (fraction <= 1) Then
            niceFraction = 1
        ElseIf (fraction <= 2) Then
            niceFraction = 2
        ElseIf (fraction <= 5) Then
            niceFraction = 5
        Else
            niceFraction = 10
        End If
    End If

    Return niceFraction * Math.Pow(10, exponent)
End Function

Public Sub setMinMaxPoints(minPoint As Double, maxPoint As Double)
    minPoint = minPoint
    maxPoint = maxPoint
    calculate()
End Sub

Public Sub setMaxTicks(maxTicks As Double)
    maxTicks = maxTicks
    calculate()
End Sub

Public Function getTickSpacing() As Double
    Return tickSpacing
End Function

Public Function getNiceMin() As Double
    Return niceMin
End Function

Public Function getNiceMax() As Double
    Return niceMax
End Function

End Class

Upvotes: 0

Kai Stra&#223;mann
Kai Stra&#223;mann

Reputation: 81

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

Upvotes: 4

Incongruous
Incongruous

Reputation: 1081

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

Upvotes: 95

Bradley Odell
Bradley Odell

Reputation: 1258

I needed this algorithm converted to C#, so here it is...

public static class NiceScale {

    public static void Calculate(double min, double max, int maxTicks, out double range, out double tickSpacing, out double niceMin, out double niceMax) {
        range = niceNum(max - min, false);
        tickSpacing = niceNum(range / (maxTicks - 1), true);
        niceMin = Math.Floor(min / tickSpacing) * tickSpacing;
        niceMax = Math.Ceiling(max / tickSpacing) * tickSpacing;
    }

    private static double niceNum(double range, bool round) {
        double pow = Math.Pow(10, Math.Floor(Math.Log10(range)));
        double fraction = range / pow;

        double niceFraction;
        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;
    }

}

Upvotes: 1

Graeme
Graeme

Reputation: 305

I found this thread while writing some php, so now the same code is available in php too!

class CNiceScale {

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

  public function setScale($min, $max) {
    $this->minPoint = $min;
    $this->maxPoint = $max;
    $this->calculate();
  }

  private function calculate() {
    $this->range = $this->niceNum($this->maxPoint - $this->minPoint, false);
    $this->tickSpacing = $this->niceNum($this->range / ($this->maxTicks - 1), true);
    $this->niceMin = floor($this->minPoint / $this->tickSpacing) * $this->tickSpacing;
    $this->niceMax = ceil($this->maxPoint / $this->tickSpacing) * $this->tickSpacing;
  }

  private function niceNum($range, $round) {
    $exponent; /** exponent of range */
    $fraction; /** fractional part of range */
    $niceFraction; /** nice, rounded fraction */

    $exponent = floor(log10($range));
    $fraction = $range / 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 * pow(10, $exponent);
  }

  public function setMinMaxPoints($minPoint, $maxPoint) {
    $this->minPoint = $minPoint;
    $this->maxPoint = $maxPoint;
    $this->calculate();
  }

  public function setMaxTicks($maxTicks) {
    $this->maxTicks = $maxTicks;
    $this->calculate();
  }

  public function getTickSpacing() {
    return $this->tickSpacing;
  }

  public function getNiceMin() {
    return $this->niceMin;
  }

  public function getNiceMax() {
    return $this->niceMax;
  }

}

Upvotes: 2

Yepher
Yepher

Reputation: 1505

Here is the same thing in Objective C

YFRNiceScale.h

#import <Foundation/Foundation.h>

@interface YFRNiceScale : NSObject

@property (nonatomic, readonly) CGFloat minPoint;
@property (nonatomic, readonly) CGFloat maxPoint;
@property (nonatomic, readonly) CGFloat maxTicks;
@property (nonatomic, readonly) CGFloat tickSpacing;
@property (nonatomic, readonly) CGFloat range;
@property (nonatomic, readonly) CGFloat niceRange;
@property (nonatomic, readonly) CGFloat niceMin;
@property (nonatomic, readonly) CGFloat niceMax;


- (id) initWithMin: (CGFloat) min andMax: (CGFloat) max;
- (id) initWithNSMin: (NSDecimalNumber*) min andNSMax: (NSDecimalNumber*) max;

@end

YFRNiceScale.m

#import "YFRNiceScale.h"

@implementation YFRNiceScale

@synthesize minPoint = _minPoint;
@synthesize maxPoint = _maxPoint;
@synthesize maxTicks = _maxTicks;
@synthesize tickSpacing = _tickSpacing;
@synthesize range = _range;
@synthesize niceRange = _niceRange;
@synthesize niceMin = _niceMin;
@synthesize niceMax = _niceMax;

- (id)init {
    self = [super init];
    if (self) {

    }
    return self;
}

- (id) initWithMin: (CGFloat) min andMax: (CGFloat) max {

    if (self) {
        _maxTicks = 10;
        _minPoint = min;
        _maxPoint = max;
        [self calculate];
    }
    return [self init];
}

- (id) initWithNSMin: (NSDecimalNumber*) min andNSMax: (NSDecimalNumber*) max {

    if (self) {
        _maxTicks = 10;
        _minPoint = [min doubleValue];
        _maxPoint = [max doubleValue];
        [self calculate];
    }
    return [self init];
}


/**
 * Calculate and update values for tick spacing and nice minimum and maximum
 * data points on the axis.
 */

- (void) calculate {
    _range = [self niceNumRange: (_maxPoint-_minPoint) roundResult:NO];
    _tickSpacing = [self niceNumRange: (_range / (_maxTicks - 1)) roundResult:YES];
    _niceMin = floor(_minPoint / _tickSpacing) * _tickSpacing;
    _niceMax = ceil(_maxPoint / _tickSpacing) * _tickSpacing;

    _niceRange = _niceMax - _niceMin;
}


/**
 * 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
 */
- (CGFloat) niceNumRange:(CGFloat) aRange roundResult:(BOOL) round {
    CGFloat exponent;
    CGFloat fraction;
    CGFloat niceFraction;

    exponent = floor(log10(aRange));
    fraction = aRange / 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 = 2;
        } else {
            niceFraction = 10;
        }
    }

    return niceFraction * pow(10, exponent);
}

- (NSString*) description {
    return [NSString stringWithFormat:@"NiceScale [minPoint=%.2f, maxPoint=%.2f, maxTicks=%.2f, tickSpacing=%.2f, range=%.2f, niceMin=%.2f, niceMax=%.2f]", _minPoint, _maxPoint, _maxTicks, _tickSpacing, _range, _niceMin, _niceMax ];
}

@end

Usage:

YFRNiceScale* niceScale = [[YFRNiceScale alloc] initWithMin:0 andMax:500];
NSLog(@"Nice: %@", niceScale);

Upvotes: 3

vicky
vicky

Reputation: 103

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

Upvotes: 7

Klas Lindb&#228;ck
Klas Lindb&#228;ck

Reputation: 33283

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.

Upvotes: 3

Related Questions