Algorithm of "nice" grid intervals on a chart

I need a reasonably smart algorithm to come up with "good" grids for the graph (chart).

For example, suppose a histogram with values ​​of 10, 30, 72, and 60. You know:

Minimum value: 10 Maximum value: 72 Range: 62

First question: where do you start? In this case, 0 will be an intuitive value, but it will not linger on other datasets, so I assume:

The minimum grid value must be either 0 or a “good” value below the minimum data value in the range. You can specify it as an alternative.

The maximum grid value should be a “good” value above the maximum value in the range. Alternatively, you can specify it (for example, you may need from 0 to 100 if you show percentages, regardless of the actual values).

The number of grid lines (ticks) in the range must be either specified or a number in the given range (for example, 3-8), so the values ​​are "good" (i.e. round numbers) and you maximize the use of the chart area. In our example, 80 would be a reasonable max since it will use 90% of the height of the chart (72/80), while 100 will create more lost space.

Does anyone know of a good algorithm for this? Language does not matter, because I implement it in what I need.

+59
algorithm graph charts
Dec 12 '08 at 1:54
source share
14 answers

CPAN provides an implementation here (see source link)

See also Marking Algorithm for Graph Axis.

FYI, with your example 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
+8
Dec 12 '08 at 2:19
source share

I did this using brute force. First find out the maximum number of labels that you can put in space. Divide the total range of values ​​by the number of ticks; This is the minimum tick interval. Now calculate the floor of the logarithmic base 10 to get the check mark, and divide by that value. You should get something in the range of 1 to 10. Just select a round number greater than or equal to the value, and multiply it by the logarithm calculated earlier. This is your final mark spacing.

An 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 can change the selection of “nice” intervals. One commenter seems dissatisfied with the elections, as the actual number of ticks can be 2.5 times less than the maximum. Here's a slight change that defines a table for good intervals. In this example, I expanded the selection so that the number of ticks does not exceed 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 
+34
Dec 12 '08 at 2:02
source share

There are two problems:

  • Determine the order of magnitude and
  • Round to something convenient.

You can handle the first part 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 to 1200, the metric is 3, and the value is 1000.

Then consider the second part, deciding how many units you want in your grid:

 value_per_division = magnitude / subdivisions; 

This is a rough calculation, since the exponent has been truncated to an integer. You might want to tweak the exponent calculation to better control the boundary conditions, for example. by rounding, instead of accepting int() if you have too many units.

+29
Dec 12 '08 at 2:09
source share

I am using the following algorithm. This is similar to others posted here, but this is 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; } } 
+14
Feb 13 '09 at 13:41
source share

Here is another JavaScript implementation:

 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; }; 
+5
Feb 25 '13 at 16:47
source share

I wrote an objective-c method to return a beautiful axis scale and good ticks for the given minimum and maximum values ​​of your dataset:

 - (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 as follows:

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

and get the axis setting:

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

Just in case, if you want to limit the number of ticks of the axis, do the following:

 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 a calculation that I found here to calculate a beautiful graph scale and ticks similar to excel graphs. It works great for all types of datasets. Here is an example iPhone implementation:

enter image description here

+2
Apr 03 '14 at 20:33
source share

Taken from the above, a bit more complete Util class in C #. It 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; 
+2
01 Oct '14 at 16:36
source share

Another idea is for the axis range to be a range of values, but put the marks in the appropriate position. i.e. from 7 to 22:

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

Regarding the choice of tick interval, I would suggest any number of the form 10 ^ x * i / n, where I <n and 0 <n <10. Create this list and sort it, and you can find the largest number smaller than value_per_division ( as in adam_liss) using binary search.

+1
Dec 12 '08 at 2:22
source share

I am the author of the algorithm for optimal scaling on the axis of the diagram . "It used to be hosted on trollop.org, but recently I switched to domain / blog servers.

Pay attention to my answer to the corresponding question .

+1
May 03 '13 at 16:16
source share

Using a lot of inspiration from the answer options already available here, here is my implementation in C. Note that there is some extensibility 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; } 
0
Aug 05 '13 at 1:38 am
source share

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 the range argument is the max-min domain.

0
04 Oct '13 at 12:57 on
source share

Here is the javascript function I wrote for the rounded grid spacing (max-min)/gridLinesNumber to nice values. It works with any numbers, see the gist with detailed comets to find out how it works and what 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 such numbers:

 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 fiddle to experiment with the code. The code in the answer, the essence and the violin are a bit different. I use the one indicated in the answer.

0
Feb 07 '16 at 13:22
source share

If you are trying to get scales that look directly on the VB.NET diagrams, I used an example from Adam Liss, but make sure that when you specify the values ​​of the min and max scale that you pass to them from a variable (not one or two types), otherwise If the label value is set to 8 decimal places. So, for example, I had 1 diagram, where I set the value of the minimum Y axis to 0.0001, and the maximum value of the Y axis to 0.002. If I pass these values ​​to the chart object as singles, I get the values ​​of the mark 0.00048000001697801, 0.000860000036482233 .... If I pass these values ​​to the chart object as decimal places, I get good values ​​of the mark 0.00048, 0.00086 ......

0
Apr 08 '16 at 12:44 on
source share

In python:

 steps = [numpy.round(x) for x in np.linspace(min, max, num=num_of_steps)] 
0
Jan 01 '18 at 20:29
source share



All Articles