72

I need a reasonably smart algorithm to come up with "nice" grid lines for a graph (chart).

For example, assume a bar chart with values of 10, 30, 72 and 60. You know:

Min value: 10 Max value: 72 Range: 62

The first question is: what do you start from? In this case, 0 would be the intuitive value but this won't hold up on other data sets so I'm guessing:

Grid min value should be either 0 or a "nice" value lower than the min value of the data in range. Alternatively, it can be specified.

Grid max value should be a "nice" value above the max value in the range. Alternatively, it can be specified (eg you might want 0 to 100 if you're showing percentages, irrespective of the actual values).

The number of grid lines (ticks) in the range should be either specified or a number within a given range (eg 3-8) such that the values are "nice" (ie round numbers) and you maximise use of the chart area. In our example, 80 would be a sensible max as that would use 90% of the chart height (72/80) whereas 100 would create more wasted space.

Anyone know of a good algorithm for this? Language is irrelevant as I'll implement it in what I need to.

cletus
  • 616,129
  • 168
  • 910
  • 942
  • I can't parse the For example part. Can you provide a clearer example? – S.Lott Dec 12 '08 at 02:00
  • 2
    How about nice round numbers like 2,4,8,16,32,64.... – Kibbee Dec 12 '08 at 02:03
  • @S.Lott, for the example, you have a data set of {10, 30, 72, 60}. X values (perhaps labels) are along the X axis of the graph; bars rise up the Y axis on a scale to the corresponding value in the data set. – strager Dec 12 '08 at 02:20
  • @strager: thanks, but that was the only part I could parse in the original post. The revised is much, much clearer. – S.Lott Dec 12 '08 at 02:23

16 Answers16

34

I've done this with kind of a brute force method. First, figure out the maximum number of tick marks you can fit into the space. Divide the total range of values by the number of ticks; this is the minimum spacing of the tick. Now calculate the floor of the logarithm base 10 to get the magnitude of the tick, and divide by this value. You should end up with something in the range of 1 to 10. Simply choose the round number greater than or equal to the value and multiply it by the logarithm calculated earlier. This is your final tick spacing.

Example in Python:

import math

def BestTick(largest, mostticks):
    minimum = largest / mostticks
    magnitude = 10 ** math.floor(math.log(minimum, 10))
    residual = minimum / magnitude
    if residual > 5:
        tick = 10 * magnitude
    elif residual > 2:
        tick = 5 * magnitude
    elif residual > 1:
        tick = 2 * magnitude
    else:
        tick = magnitude
    return tick

Edit: you are free to alter the selection of "nice" intervals. One commenter appears to be dissatisfied with the selections provided, because the actual number of ticks can be up to 2.5 times less than the maximum. Here's a slight modification that defines a table for the nice intervals. In the example, I've expanded the selections so that the number of ticks won't be less than 3/5 of the maximum.

import bisect

def BestTick2(largest, mostticks):
    minimum = largest / mostticks
    magnitude = 10 ** math.floor(math.log(minimum, 10))
    residual = minimum / magnitude
    # this table must begin with 1 and end with 10
    table = [1, 1.5, 2, 3, 5, 7, 10]
    tick = table[bisect.bisect_right(table, residual)] if residual < 10 else 10
    return tick * magnitude
Mark Ransom
  • 299,747
  • 42
  • 398
  • 622
  • @MarkRansom I'm currently having an issue that could be solved by this answer (see: http://stackoverflow.com/questions/28987264) but I'm not sure how to `figure out the maximum number of tick marks you can fit into the space`. Any pointers? (I know it's been a long time, but if you could give some more details on your answer, it'd be great. Thanks!) – Gabriel Mar 11 '15 at 20:25
  • 1
    @Gabriel sorry I don't know anything about matplotlib. For the general question though, you need to know how much space is taken by one label (with space added for padding) and how much is taken by the whole axis; divide one by the other to get the maximum number of ticks. – Mark Ransom Mar 11 '15 at 20:38
  • this solution is not ok for huge numbers. the difference is to big. – puppeteer701 Nov 06 '15 at 16:26
  • @MatjažJurečič can you explain exactly what you mean? What parameters are you using? – Mark Ransom Nov 06 '15 at 17:15
  • Value: 140000000, Number of ticks: 11. Value/Ticks = 12727272.727272727, but when used this function it becomes 220000000, which is way to big. – puppeteer701 Nov 09 '15 at 09:12
  • @MatjažJurečič I hope 220000000 is just a typo, because it should only be 20000000. I've presented a modified algorithm that you might find more to your liking. – Mark Ransom Nov 10 '15 at 00:15
  • This only works when your graph starts with 0, right? – Tomáš Zato Jan 08 '16 at 10:03
  • @TomášZato you can pass `largest-origin` for `largest` and it should work fine if your origin isn't 0. – Mark Ransom Jan 08 '16 at 12:22
  • @MarkRansom Thanks, I already figured that out :) Just forgot to delete the comment. – Tomáš Zato Jan 08 '16 at 12:25
  • The second example fails for things like BestTick2(5000, 5) – Mark Aug 01 '16 at 20:07
  • @Mark thanks for finding that. It's due to a very slight rounding problem when calculating the log base 10. I've modified it to be more robust in that case. – Mark Ransom Aug 01 '16 at 21:27
  • @MarkRansom Thanks, this is helpful. I'm wondering if you can check this use-case of larger numbers: BestTick2(65000000, 14) through BestTick2(65000000, 21) all seem to produce the same value. – Mark Aug 02 '16 at 23:57
  • @Mark it's correct. They all return a tick spacing of 5000000, which yields a tick count of 13. The next available tick spacing would be 3000000, which yields a tick count of 21.67 - larger than your limit of 21. – Mark Ransom Aug 03 '16 at 03:54
31

There are 2 pieces to the problem:

  1. Determine the order of magnitude involved, and
  2. Round to something convenient.

You can handle the first part by using logarithms:

range = max - min;  
exponent = int(log(range));       // See comment below.
magnitude = pow(10, exponent);

So, for example, if your range is from 50 - 1200, the exponent is 3 and the magnitude is 1000.

Then deal with the second part by deciding how many subdivisions you want in your grid:

value_per_division = magnitude / subdivisions;

This is a rough calculation because the exponent has been truncated to an integer. You may want to tweak the exponent calculation to handle boundary conditions better, e.g. by rounding instead of taking the int() if you end up with too many subdivisions.

Adam Liss
  • 47,594
  • 12
  • 108
  • 150
  • 2
    Note to self: for "exponent" using javascript using base10, see accepted answer to: http://stackoverflow.com/questions/3019278/any-way-to-specify-the-base-of-math-log-in-javascript – mg1075 Apr 28 '12 at 18:35
17

I use the following algorithm. It's similar to others posted here but it's the first example in C#.

public static class AxisUtil
{
    public static float CalcStepSize(float range, float targetSteps)
    {
        // calculate an initial guess at step size
        var tempStep = range/targetSteps;

        // get the magnitude of the step size
        var mag = (float)Math.Floor(Math.Log10(tempStep));
        var magPow = (float)Math.Pow(10, mag);

        // calculate most significant digit of the new step size
        var magMsd = (int)(tempStep/magPow + 0.5);

        // promote the MSD to either 1, 2, or 5
        if (magMsd > 5)
            magMsd = 10;
        else if (magMsd > 2)
            magMsd = 5;
        else if (magMsd > 1)
            magMsd = 2;

        return magMsd*magPow;
    }
}
Drew Noakes
  • 300,895
  • 165
  • 679
  • 742
  • This webpage shows how to also set the min and max for the axis: http://www.trollop.org/2011/03/15/algorithm-for-optimal-scaling-on-a-chart-axis/ – Curt Feb 06 '12 at 19:14
  • @Curt Thanks! That link is now dead, but I've found the page here: http://www.esurient-systems.ca/2011/03/algorithm-for-optimal-scaling-on-chart_8199.html – kol Sep 30 '13 at 07:09
  • 1
    @both links above are now dead. I believe the same c# impementation is at http://erison.blogspot.com/2011/07/algorithm-for-optimal-scaling-on-chart.html – Bryant Miano Dec 05 '15 at 04:58
12

CPAN provides an implementation here (see source link)

See also Tickmark algorithm for a graph axis

FYI, with your sample data:

  • Maple: Min=8, Max=74, Labels=10,20,..,60,70, Ticks=10,12,14,..70,72
  • MATLAB: Min=10, Max=80, Labels=10,20,,..,60,80
Community
  • 1
  • 1
Christoph Rüegg
  • 4,626
  • 1
  • 20
  • 34
6

Here's another implementation in JavaScript:

var calcStepSize = function(range, targetSteps)
{
  // calculate an initial guess at step size
  var tempStep = range / targetSteps;

  // get the magnitude of the step size
  var mag = Math.floor(Math.log(tempStep) / Math.LN10);
  var magPow = Math.pow(10, mag);

  // calculate most significant digit of the new step size
  var magMsd = Math.round(tempStep / magPow + 0.5);

  // promote the MSD to either 1, 2, or 5
  if (magMsd > 5.0)
    magMsd = 10.0;
  else if (magMsd > 2.0)
    magMsd = 5.0;
  else if (magMsd > 1.0)
    magMsd = 2.0;

  return magMsd * magPow;
};
Drew Noakes
  • 300,895
  • 165
  • 679
  • 742
  • This method requires to change max number quit a lot in certain cases. – puppeteer701 Oct 29 '15 at 16:56
  • 1
    A tiny quibble. `Math.LN10`? A known constant seems a little better than a function call! – Auspex May 24 '19 at 14:23
  • hmmm, assuming min = 112, max = 5847 (range = 5735), targetSteps = 8: calcStepSize(5735, 8) = 1000, but it's too high, it means to have for example 8 steps starting from 0 and ending to 8000 – user2010955 Jul 01 '19 at 11:19
  • @user2010955 what output would you expect in such a case? – Drew Noakes Jul 02 '19 at 01:38
  • Hi, I'm thinking that it's probably a trade-off between the number of targetSteps and the definition of "nice", it's impossible to have 8 "nice" steps between 112 and 5847, the algorithm outputs 1000, to it means a tick at 0, 1k, 2k, 3k... 8k. The problem here is that 8k is too much highter than 5847, so in this case I would prefer a tick at 0, 750, 1500, 2250, ... and 6000. 6000 is much closer to 5847 – user2010955 Jul 02 '19 at 06:58
  • The intention here is not to always have `targetSteps` steps in the resulting chart. It's just a _target_. Whatever output this gives, use until your range is covered. In your example you'd have 0, 1k, 2k, 3k, 4k, 5k, 6k. That's only seven chart lines, not eight, but the output will look good. If you need an exact number of steps, you'll want a different algorithm. – Drew Noakes Jul 02 '19 at 10:36
3

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.

Please see my answer to a related question.

Community
  • 1
  • 1
Incongruous
  • 1,051
  • 7
  • 5
3

Taken from Mark above, a slightly more complete Util class in c#. That also calculates a suitable first and last tick.

public  class AxisAssists
{
    public double Tick { get; private set; }

    public AxisAssists(double aTick)
    {
        Tick = aTick;
    }
    public AxisAssists(double range, int mostticks)
    {
        var minimum = range / mostticks;
        var magnitude = Math.Pow(10.0, (Math.Floor(Math.Log(minimum) / Math.Log(10))));
        var residual = minimum / magnitude;
        if (residual > 5)
        {
            Tick = 10 * magnitude;
        }
        else if (residual > 2)
        {
            Tick = 5 * magnitude;
        }
        else if (residual > 1)
        {
            Tick = 2 * magnitude;
        }
        else
        {
            Tick = magnitude;
        }
    }

    public double GetClosestTickBelow(double v)
    {
        return Tick* Math.Floor(v / Tick);
    }
    public double GetClosestTickAbove(double v)
    {
        return Tick * Math.Ceiling(v / Tick);
    }
}

With ability to create an instance, but if you just want calculate and throw it away:

    double tickX = new AxisAssists(aMaxX - aMinX, 8).Tick;
tobbiefox
  • 83
  • 2
  • 15
Gregor
  • 358
  • 2
  • 7
2

I wrote an objective-c method to return a nice axis scale and nice ticks for given min- and max values of your data set:

- (NSArray*)niceAxis:(double)minValue :(double)maxValue
{
    double min_ = 0, max_ = 0, min = minValue, max = maxValue, power = 0, factor = 0, tickWidth, minAxisValue = 0, maxAxisValue = 0;
    NSArray *factorArray = [NSArray arrayWithObjects:@"0.0f",@"1.2f",@"2.5f",@"5.0f",@"10.0f",nil];
    NSArray *scalarArray = [NSArray arrayWithObjects:@"0.2f",@"0.2f",@"0.5f",@"1.0f",@"2.0f",nil];

    // calculate x-axis nice scale and ticks
    // 1. min_
    if (min == 0) {
        min_ = 0;
    }
    else if (min > 0) {
        min_ = MAX(0, min-(max-min)/100);
    }
    else {
        min_ = min-(max-min)/100;
    }

    // 2. max_
    if (max == 0) {
        if (min == 0) {
            max_ = 1;
        }
        else {
            max_ = 0;
        }
    }
    else if (max < 0) {
        max_ = MIN(0, max+(max-min)/100);
    }
    else {
        max_ = max+(max-min)/100;
    }

    // 3. power
    power = log(max_ - min_) / log(10);

    // 4. factor
    factor = pow(10, power - floor(power));

    // 5. nice ticks
    for (NSInteger i = 0; factor > [[factorArray objectAtIndex:i]doubleValue] ; i++) {
        tickWidth = [[scalarArray objectAtIndex:i]doubleValue] * pow(10, floor(power));
    }

    // 6. min-axisValues
    minAxisValue = tickWidth * floor(min_/tickWidth);

    // 7. min-axisValues
    maxAxisValue = tickWidth * floor((max_/tickWidth)+1);

    // 8. create NSArray to return
    NSArray *niceAxisValues = [NSArray arrayWithObjects:[NSNumber numberWithDouble:minAxisValue], [NSNumber numberWithDouble:maxAxisValue],[NSNumber numberWithDouble:tickWidth], nil];

    return niceAxisValues;
}

You can call the method like this:

NSArray *niceYAxisValues = [self niceAxis:-maxy :maxy];

and get you axis setup:

double minYAxisValue = [[niceYAxisValues objectAtIndex:0]doubleValue];
double maxYAxisValue = [[niceYAxisValues objectAtIndex:1]doubleValue];
double ticksYAxis = [[niceYAxisValues objectAtIndex:2]doubleValue];

Just in case you want to limit the number of axis ticks do this:

NSInteger maxNumberOfTicks = 9;
NSInteger numberOfTicks = valueXRange / ticksXAxis;
NSInteger newNumberOfTicks = floor(numberOfTicks / (1 + floor(numberOfTicks/(maxNumberOfTicks+0.5))));
double newTicksXAxis = ticksXAxis * (1 + floor(numberOfTicks/(maxNumberOfTicks+0.5)));

The first part of the code is based on the calculation I found here to calculate nice graph axis scale and ticks similar to excel graphs. It works excellent for all kind of data sets. Here is an example of an iPhone implementation:

enter image description here

JFS
  • 2,992
  • 3
  • 37
  • 48
  • what is doubleValue in the code, I am trying to convert it to javascript? – puppeteer701 Nov 02 '15 at 14:19
  • @MatjažJurečič, it is needed in objective C to read back the value as double. I'm not sure but this is most probably only objective-c related. – JFS Nov 02 '15 at 19:22
1

Using a lot of inspiration from answers already availible here, here's my implementation in C. Note that there's some extendibility built into the ndex array.

float findNiceDelta(float maxvalue, int count)
{
    float step = maxvalue/count,
         order = powf(10, floorf(log10(step))),
         delta = (int)(step/order + 0.5);

    static float ndex[] = {1, 1.5, 2, 2.5, 5, 10};
    static int ndexLenght = sizeof(ndex)/sizeof(float);
    for(int i = ndexLenght - 2; i > 0; --i)
        if(delta > ndex[i]) return ndex[i + 1] * order;
    return delta*order;
}
Gleno
  • 16,621
  • 12
  • 64
  • 85
1

Another idea is to have the range of the axis be the range of the values, but put the tick marks at the appropriate position.. i.e. for 7 to 22 do:

[- - - | - - - - | - - - - | - - ]
       10        15        20

As for selecting the tick spacing, I would suggest any number of the form 10^x * i / n, where i < n, and 0 < n < 10. Generate this list, and sort them, and you can find the largest number smaller than value_per_division (as in adam_liss) using a binary search.

FryGuy
  • 8,614
  • 3
  • 33
  • 47
0

In R, use

tickSize <- function(range,minCount){
    logMaxTick <- log10(range/minCount)
    exponent <- floor(logMaxTick)
    mantissa <- 10^(logMaxTick-exponent)
    af <- c(1,2,5) # allowed factors
    mantissa <- af[findInterval(mantissa,af)]
    return(mantissa*10^exponent)
}

where range argument is max-min of domain.

Museful
  • 6,711
  • 5
  • 42
  • 68
0

Here is a javascript function I wrote to round grid intervals (max-min)/gridLinesNumber to beautiful values. It works with any numbers, see the gist with detailed commets to find out how it works and how to call it.

var ceilAbs = function(num, to, bias) {
  if (to == undefined) to = [-2, -5, -10]
  if (bias == undefined) bias = 0
  var numAbs = Math.abs(num) - bias
  var exp = Math.floor( Math.log10(numAbs) )

    if (typeof to == 'number') {
        return Math.sign(num) * to * Math.ceil(numAbs/to) + bias
    }

  var mults = to.filter(function(value) {return value > 0})
  to = to.filter(function(value) {return value < 0}).map(Math.abs)
  var m = Math.abs(numAbs) * Math.pow(10, -exp)
  var mRounded = Infinity

  for (var i=0; i<mults.length; i++) {
    var candidate = mults[i] * Math.ceil(m / mults[i])
    if (candidate < mRounded)
      mRounded = candidate
  }
  for (var i=0; i<to.length; i++) {
    if (to[i] >= m && to[i] < mRounded)
      mRounded = to[i]
  }
  return Math.sign(num) * mRounded * Math.pow(10, exp) + bias
}

Calling ceilAbs(number, [0.5]) for different numbers will round numbers like that:

301573431.1193228 -> 350000000
14127.786597236991 -> 15000
-63105746.17236853 -> -65000000
-718854.2201183736 -> -750000
-700660.340487957 -> -750000
0.055717507097870114 -> 0.06
0.0008068701205775142 -> 0.00085
-8.66660070605576 -> -9
-400.09256079792976 -> -450
0.0011740548815578223 -> 0.0015
-5.3003294346854085e-8 -> -6e-8
-0.00005815960629843176 -> -0.00006
-742465964.5184875 -> -750000000
-81289225.90985894 -> -85000000
0.000901771713513881 -> 0.00095
-652726598.5496342 -> -700000000
-0.6498901364393532 -> -0.65
0.9978325804695487 -> 1
5409.4078950583935 -> 5500
26906671.095639467 -> 30000000

Check out the fiddle to experiment with the code. Code in the answer, the gist and the fiddle is slightly different I'm using the one given in the answer.

grabantot
  • 2,111
  • 20
  • 31
0

If you are trying to get the scales looking right on VB.NET charts, then I've used the example from Adam Liss, but make sure when you set the min and max scale values that you pass them in from a variable of type decimal (not of type single or double) otherwise the tick mark values end up being set to like 8 decimal places. So as an example, I had 1 chart where I set the min Y Axis value to 0.0001 and the max Y Axis value to 0.002. If I pass these values to the chart object as singles I get tick mark values of 0.00048000001697801, 0.000860000036482233 .... Whereas if I pass these values to the chart object as decimals I get nice tick mark values of 0.00048, 0.00086 ......

Kristian
  • 406
  • 4
  • 12
0

In python:

steps = [numpy.round(x) for x in np.linspace(min, max, num=num_of_steps)]
Zoe
  • 27,060
  • 21
  • 118
  • 148
MichaelLo
  • 1,289
  • 1
  • 14
  • 26
0

Answer that can dynamically always plot 0, handle positive and negatives, and small and large numbers, gives the tick interval size and how many to plot; written in Go

forcePlotZero changes how the max values are rounded so it'll always make a nice multiple to then get back to zero. Example:

if forcePlotZero == false then 237 --> 240

if forcePlotZero == true then 237 --> 300

Intervals are calculated by getting the multiple of 10/100/1000 etc for max and then subtracting till the cumulative total of these subtractions is < min

Here's the output from the function, along with showing forcePlotZero

Force to plot zero max and min inputs rounded max and min intervals
forcePlotZero=false min: -104 max: 240 minned: -160 maxed: 240 intervalCount: 5 intervalSize: 100
forcePlotZero=true min: -104 max: 240 minned: -200 maxed: 300 intervalCount: 6 intervalSize: 100
forcePlotZero=false min: 40 max: 1240 minned: 0 maxed: 1300 intervalCount: 14 intervalSize: 100
forcePlotZero=false min: 200 max: 240 minned: 190 maxed: 240 intervalCount: 6 intervalSize: 10
forcePlotZero=false min: 0.7 max: 1.12 minned: 0.6 maxed: 1.2 intervalCount: 7 intervalSize: 0.1
forcePlotZero=false min: -70.5 max: -12.5 minned: -80 maxed: -10 intervalCount: 8 intervalSize: 10

Here's the playground link https://play.golang.org/p/1IhiX_hRQvo

func getMaxMinIntervals(max float64, min float64, forcePlotZero bool) (maxRounded float64, minRounded float64, intervalCount float64, intervalSize float64) {

//STEP 1: start off determining the maxRounded value for the axis
precision := 0.0
precisionDampener := 0.0 //adjusts to prevent 235 going to 300, instead dampens the scaling to get 240
epsilon := 0.0000001
if math.Abs(max) >= 0 && math.Abs(max) < 2 {
    precision = math.Floor(-math.Log10(epsilon + math.Abs(max) - math.Floor(math.Abs(max)))) //counting number of zeros between decimal point and rightward digits
    precisionDampener = 1
    precision = precision + precisionDampener
} else if math.Abs(max) >= 2 && math.Abs(max) < 100 {
    precision = math.Ceil(math.Log10(math.Abs(max)+1)) * -1 //else count number of digits before decimal point
    precisionDampener = 1
    precision = precision + precisionDampener
} else {
    precision = math.Ceil(math.Log10(math.Abs(max)+1)) * -1 //else count number of digits before decimal point
    precisionDampener = 2
    if forcePlotZero == true {
        precisionDampener = 1
    }
    precision = precision + precisionDampener
}

useThisFactorForIntervalCalculation := 0.0 // this is needed because intervals are calculated from the max value with a zero origin, this uses range for min - max
if max < 0 {
    maxRounded = (math.Floor(math.Abs(max)*(math.Pow10(int(precision)))) / math.Pow10(int(precision)) * -1)
    useThisFactorForIntervalCalculation = (math.Floor(math.Abs(max)*(math.Pow10(int(precision)))) / math.Pow10(int(precision))) + ((math.Ceil(math.Abs(min)*(math.Pow10(int(precision)))) / math.Pow10(int(precision))) * -1)
} else {
    maxRounded = math.Ceil(max*(math.Pow10(int(precision)))) / math.Pow10(int(precision))
    useThisFactorForIntervalCalculation = maxRounded
}

minNumberOfIntervals := 2.0
maxNumberOfIntervals := 19.0
intervalSize = 0.001
intervalCount = minNumberOfIntervals

//STEP 2: get interval size (the step size on the axis)
for {
    if math.Abs(useThisFactorForIntervalCalculation)/intervalSize < minNumberOfIntervals || math.Abs(useThisFactorForIntervalCalculation)/intervalSize > maxNumberOfIntervals {
        intervalSize = intervalSize * 10
    } else {
        break
    }
}

//STEP 3: check that intervals are not too large, safety for max and min values that are close together (240, 220 etc)
for {
    if max-min < intervalSize {
        intervalSize = intervalSize / 10
    } else {
        break
    }
}

//STEP 4: now we can get minRounded by adding the interval size to 0 till we get to the point where another increment would make cumulative increments > min, opposite for negative in
minRounded = 0.0

if min >= 0 {
    for {
        if minRounded < min {
            minRounded = minRounded + intervalSize
        } else {
            minRounded = minRounded - intervalSize
            break
        }
    }
} else {
    minRounded = maxRounded //keep going down, decreasing by the interval size till minRounded < min
    for {
        if minRounded > min {
            minRounded = minRounded - intervalSize

        } else {
            break
        }
    }
}

//STEP 5: get number of intervals to draw
intervalCount = (maxRounded - minRounded) / intervalSize
intervalCount = math.Ceil(intervalCount) + 1 // include the origin as an interval

//STEP 6: Check that the intervalCount isn't too high
if intervalCount-1 >= (intervalSize * 2) && intervalCount > maxNumberOfIntervals {
    intervalCount = math.Ceil(intervalCount / 2)
    intervalSize *= 2
}

return}
0

This is in python and for base 10. Doesn't cover all your questions but I think you can build on it

import numpy as np

def create_ticks(lo,hi):
    s = 10**(np.floor(np.log10(hi - lo)))
    start = s * np.floor(lo / s)
    end = s * np.ceil(hi / s)
    ticks = [start]
    t = start
    while (t <  end):
        ticks += [t]
        t = t + s
        
    return ticks
Fady Megally
  • 135
  • 6