Choosing an attractive linear scale for the YY axis

I am writing some code to display a graph (or line) in our software. Everything goes well. What puzzled me is the designation of the Y axis.

The caller can tell me how precisely they want to assign the Y scale, but I seem to be fixated on what to label them in an “attractive” way. I can’t describe it as “attractive,” and you probably can't either, but we know it when we see it, right?

So if the data points are:

15, 234, 140, 65, 90 

And the user asks for 10 marks on the Y axis, a little tearing away with paper and pencil:

  0, 25, 50, 75, 100, 125, 150, 175, 200, 225, 250 

So, there are 10 (not including 0), the last of them goes beyond the highest value (234 <250), and this is a “good” increment of 25 each. If they asked for 8 tags, an increase of 30 would have looked nice:

  0, 30, 60, 90, 120, 150, 180, 210, 240 

Nine would be difficult. Maybe just used 8 or 10 and called it close enough, it would be nice. And what to do when some of the negative points?

I see that Excel is doing a good job of this problem.

Does anyone know a universal algorithm (even some brute forces are OK) to solve this? I don't have to do it fast, but it should look good.

+68
math algorithm graph
Nov 28 '08 at 21:06
source share
12 answers

Once upon a time, I wrote a graphics module that covered it well. When digging in gray mass, the following is obtained:

  • Define the lower and upper bounds of the data. (Beware of the special case when the lower bound = upper bound!
  • Divide the range by the required number of ticks.
  • Round the tick range in good amounts.
  • Adjust the lower and upper bounds accordingly.

Let's take your example:

 15, 234, 140, 65, 90 with 10 ticks 
  1. lower bound = 15
  2. upper bound = 234
  3. range = 234-15 = 219
  4. tick range = 21.9. It should be 25.0
  5. new lower bound = 25 * round (15/25) = 0
  6. new upper bound = 25 * round (1 + 235/25) = 250

So the range = 0.25.50, ..., 225.250

You can get a good range of ticks with the following steps:

  1. divide by 10 ^ x so that the result is between 0.1 and 1.0 (including 0.1, excluding 1).
  2. translate accordingly:
    • 0,1 → 0,1
    • <= 0.2 → 0.2
    • <= 0.25 → 0.25
    • <= 0.3 → 0.3
    • <= 0.4 → 0.4
    • <= 0.5 → 0.5
    • <= 0.6 → 0.6
    • <= 0.7 → 0.7
    • <= 0.75 → 0.75
    • <= 0.8 → 0.8
    • <= 0.9 → 0.9
    • <= 1.0 → 1.0
  3. multiply by 10 ^ x.

In this case, 21.9 is divided by 10 ^ 2 to get 0.219. This is <= 0.25, so now we have 0.25. Multiply by 10 ^ 2 this gives 25.

Let's look at the same example with 8 ticks:

 15, 234, 140, 65, 90 with 8 ticks 
  1. lower bound = 15
  2. upper bound = 234
  3. range = 234-15 = 219
  4. tick range = 27.375
    1. Divided by 10 ^ 2 for 0.27375 translates to 0.3, which gives (multiplied by 10 ^ 2) 30.
  5. new lower bound = 30 * round (15/30) = 0
  6. new upper bound = 30 * round (1 + 235/30) = 240

Which give the result you requested ;-).

------ Added by KD ------

Here is the code that implements this algorithm without using lookup tables, etc ...:

 double range = ...; int tickCount = ...; double unroundedTickSize = range/(tickCount-1); double x = Math.ceil(Math.log10(unroundedTickSize)-1); double pow10x = Math.pow(10, x); double roundedTickRange = Math.ceil(unroundedTickSize / pow10x) * pow10x; return roundedTickRange; 

Generally speaking, the number of ticks includes the bottom tick, so the actual segments of the Y axis are one less than the number of ticks.

+90
Nov 28 '08 at 21:44
source share

Here is an example of PHP that I am using. This function returns an array of pretty Y-axis values ​​that span the min and max Y values. Of course, this procedure can also be used for X-axis values.

This allows you to “tell” how many ticks you may need, but the procedure will return which looks good. I added some sample data and showed the results for it.

 #!/usr/bin/php -q <?php function makeYaxis($yMin, $yMax, $ticks = 10) { // This routine creates the Y axis values for a graph. // // Calculate Min amd Max graphical labels and graph // increments. The number of ticks defaults to // 10 which is the SUGGESTED value. Any tick value // entered is used as a suggested value which is // adjusted to be a 'pretty' value. // // Output will be an array of the Y axis values that // encompass the Y values. $result = array(); // If yMin and yMax are identical, then // adjust the yMin and yMax values to actually // make a graph. Also avoids division by zero errors. if($yMin == $yMax) { $yMin = $yMin - 10; // some small value $yMax = $yMax + 10; // some small value } // Determine Range $range = $yMax - $yMin; // Adjust ticks if needed if($ticks < 2) $ticks = 2; else if($ticks > 2) $ticks -= 2; // Get raw step value $tempStep = $range/$ticks; // Calculate pretty step value $mag = floor(log10($tempStep)); $magPow = pow(10,$mag); $magMsd = (int)($tempStep/$magPow + 0.5); $stepSize = $magMsd*$magPow; // build Y label array. // Lower and upper bounds calculations $lb = $stepSize * floor($yMin/$stepSize); $ub = $stepSize * ceil(($yMax/$stepSize)); // Build array $val = $lb; while(1) { $result[] = $val; $val += $stepSize; if($val > $ub) break; } return $result; } // Create some sample data for demonstration purposes $yMin = 60; $yMax = 330; $scale = makeYaxis($yMin, $yMax); print_r($scale); $scale = makeYaxis($yMin, $yMax,5); print_r($scale); $yMin = 60847326; $yMax = 73425330; $scale = makeYaxis($yMin, $yMax); print_r($scale); ?> 

Output from sample data

 # ./test1.php Array ( [0] => 60 [1] => 90 [2] => 120 [3] => 150 [4] => 180 [5] => 210 [6] => 240 [7] => 270 [8] => 300 [9] => 330 ) Array ( [0] => 0 [1] => 90 [2] => 180 [3] => 270 [4] => 360 ) Array ( [0] => 60000000 [1] => 62000000 [2] => 64000000 [3] => 66000000 [4] => 68000000 [5] => 70000000 [6] => 72000000 [7] => 74000000 ) 
+19
Jan 25 2018-12-17T00:
source share

Try this code. I used it in several graphical scenarios and it works well. This is also pretty fast.

 public static class AxisUtil { public static float CalculateStepSize(float range, float targetSteps) { // calculate an initial guess at step size float tempStep = range/targetSteps; // get the magnitude of the step size float mag = (float)Math.Floor(Math.Log10(tempStep)); float magPow = (float)Math.Pow(10, mag); // calculate most significant digit of the new step size float magMsd = (int)(tempStep/magPow + 0.5); // promote the MSD to either 1, 2, or 5 if (magMsd > 5.0) magMsd = 10.0f; else if (magMsd > 2.0) magMsd = 5.0f; else if (magMsd > 1.0) magMsd = 2.0f; return magMsd*magPow; } } 
+8
06 Oct '09 at 12:07
source share

It seems that the caller is not telling you the ranges that he wants.

This way you can change the endpoints until you get it beautifully divisible by the number of shortcuts.

Define "nice." I would call it nice if the labels are disabled:

 1. 2^n, for some integer n. eg. ..., .25, .5, 1, 2, 4, 8, 16, ... 2. 10^n, for some integer n. eg. ..., .01, .1, 1, 10, 100 3. n/5 == 0, for some positive integer n, eg, 5, 10, 15, 20, 25, ... 4. n/2 == 0, for some positive integer n, eg, 2, 4, 6, 8, 10, 12, 14, ... 

Find the maximum and minimum amount of your data series. Let me call these points:

 min_point and max_point. 

Now all you have to do is find 3 values:

 - start_label, where start_label < min_point and start_label is an integer - end_label, where end_label > max_point and end_label is an integer - label_offset, where label_offset is "nice" 

which correspond to the equation:

 (end_label - start_label)/label_offset == label_count 

There are probably many solutions, so just pick one. In most cases, I am sure you can install

 start_label to 0 

so just try a different integer

 end_label 

until the shift is "pleasant"

+6
Nov 28 '08 at 21:33
source share

I'm still struggling with this :)

Gamecat's initial answer seems to work most of the time, but try connecting, say, “3 ticks” as you need the number of ticks (for the same data values ​​15, 234, 140, 65, 90) ... It seems to give a tick range 73, which after dividing by 10 ^ 2 gives 0.73, which corresponds to 0.75, which gives a "good" range of ticks of 75.

Then calculate the upper bound: 75 * round (1 + 234/75) = 300

and lower bound: 75 * round (15/75) = 0

But it’s clear that if you start from 0 and follow steps from 75 to the upper limit of 300, you get 0.75,150,225,300 .... this is without a doubt useful, but it is 4 ticks (not including 0), 3 ticks are not required.

Just disappointment that it does not work in 100% of cases ... which may well be, of course, before my mistake!

+3
Nov 10 '12 at 11:43
source share

Toon Krijthe's answer works most of the time. But sometimes this leads to an excess of ticks. It will not work with negative numbers. A sound approach to the problem is fine, but there is a better way to handle it. The algorithm you want to use will depend on what you really want to get. Below I present to you my code, which I used in my JS Ploting library. I tested it and it always works (hopefully);). Here are the basic steps:

  • get global extrema xMin and xMax (inlucde all the graphs that you want to print in the algorithm)
  • calculate the range between xMin and xMax
  • calculate the order of your range.
  • calculate tick size by dividing the range by the number of ticks minus one
  • This option is optional. If you want the zero tick to be printed, you use the tick size to calculate the number of positive and negative ticks. The total number of ticks will be their sum + 1 (zero tick)
  • This is not necessary if you have a zero tick. Calculate the lower and upper boundaries, but do not forget to focus the chart

Let's start. Basic calculations first

  var range = Math.abs(xMax - xMin); //both can be negative var rangeOrder = Math.floor(Math.log10(range)) - 1; var power10 = Math.pow(10, rangeOrder); var maxRound = (xMax > 0) ? Math.ceil(xMax / power10) : Math.floor(xMax / power10); var minRound = (xMin < 0) ? Math.floor(xMin / power10) : Math.ceil(xMin / power10); 

I round minimum and maximum values ​​should be 100% sure that my plot will cover all the data. It is also very important to fill in the log10 range, or not negatively, and subtract 1 later. Otherwise, your algorithm will not work for numbers that are less than one.

  var fullRange = Math.abs(maxRound - minRound); var tickSize = Math.ceil(fullRange / (this.XTickCount - 1)); //You can set nice looking ticks if you want //You can find exemplary method below tickSize = this.NiceLookingTick(tickSize); //Here you can write a method to determine if you need zero tick //You can find exemplary method below var isZeroNeeded = this.HasZeroTick(maxRound, minRound, tickSize); 

I use cute ticks to avoid ticks like 7, 13, 17, etc. The method I use here is pretty simple. It's also nice to have zeroTick when necessary. The plot looks much more professional. You will find all the methods at the end of this answer.

Now you need to calculate the upper and lower boundaries. This is very easy with a zero tick, but in another case, a little more effort is required. What for? Because we want to focus the plot on the upper and lower boundaries. Take a look at my code. Some of the variables are defined outside this area, and some of them are properties of the object in which all the presented code is stored.

  if (isZeroNeeded) { var positiveTicksCount = 0; var negativeTickCount = 0; if (maxRound != 0) { positiveTicksCount = Math.ceil(maxRound / tickSize); XUpperBound = tickSize * positiveTicksCount * power10; } if (minRound != 0) { negativeTickCount = Math.floor(minRound / tickSize); XLowerBound = tickSize * negativeTickCount * power10; } XTickRange = tickSize * power10; this.XTickCount = positiveTicksCount - negativeTickCount + 1; } else { var delta = (tickSize * (this.XTickCount - 1) - fullRange) / 2.0; if (delta % 1 == 0) { XUpperBound = maxRound + delta; XLowerBound = minRound - delta; } else { XUpperBound = maxRound + Math.ceil(delta); XLowerBound = minRound - Math.floor(delta); } XTickRange = tickSize * power10; XUpperBound = XUpperBound * power10; XLowerBound = XLowerBound * power10; } 

And here are the methods that I mentioned before you can write yourself, but you can also use mine

 this.NiceLookingTick = function (tickSize) { var NiceArray = [1, 2, 2.5, 3, 4, 5, 10]; var tickOrder = Math.floor(Math.log10(tickSize)); var power10 = Math.pow(10, tickOrder); tickSize = tickSize / power10; var niceTick; var minDistance = 10; var index = 0; for (var i = 0; i < NiceArray.length; i++) { var dist = Math.abs(NiceArray[i] - tickSize); if (dist < minDistance) { minDistance = dist; index = i; } } return NiceArray[index] * power10; } this.HasZeroTick = function (maxRound, minRound, tickSize) { if (maxRound * minRound < 0) { return true; } else if (Math.abs(maxRound) < tickSize || Math.round(minRound) < tickSize) { return true; } else { return false; } } 

There is one more thing that is not included here. These are "beautiful grades." These are the lower bounds, which are numbers close to the numbers in “pretty ticks”. For example, it is better to have a lower border starting at 5 with a tick size of 5 than with a chart starting at 6 with the same tick size. But this is my dismissal, I leave it to you.

Hope this helps. Hurrah!

+3
Apr 02 '17 at 11:49 on
source share

it works like a charm if you want 10 steps + zero

 //get proper scale for y $maximoyi_temp= max($institucion); //get max value from data array for ($i=10; $i< $maximoyi_temp; $i=($i*10)) { if (($divisor = ($maximoyi_temp / $i)) < 2) break; //get which divisor will give a number between 1-2 } $factor_d = $maximoyi_temp / $i; $factor_d = ceil($factor_d); //round up number to 2 $maximoyi = $factor_d * $i; //get new max value for y if ( ($maximoyi/ $maximoyi_temp) > 2) $maximoyi = $maximoyi /2; //check if max value is too big, then split by 2 
+1
Jul 01 '12 at 16:59
source share

Thanks for the question and answer, very helpful. Gamecat, I wonder how you determine which type should be rounded.

tick range = 21.9. It should be 25.0

To do this algorithmically, do you need to add logic to the above algorithm to make this scale beautiful for large numbers? For example, with 10 ticks, if the range is 3346, then the range of ticks will be estimated to 334.6, and rounding to the nearest 10 will give 340, when 350 is likely to be more enjoyable.

What do you think?

0
Jun 11 '09 at 21:23
source share

Based on the @Gamecat algorithm, I produced the following helper class

 public struct Interval { public readonly double Min, Max, TickRange; public static Interval Find(double min, double max, int tickCount, double padding = 0.05) { double range = max - min; max += range*padding; min -= range*padding; var attempts = new List<Interval>(); for (int i = tickCount; i > tickCount / 2; --i) attempts.Add(new Interval(min, max, i)); return attempts.MinBy(a => a.Max - a.Min); } private Interval(double min, double max, int tickCount) { var candidates = (min <= 0 && max >= 0 && tickCount <= 8) ? new[] {2, 2.5, 3, 4, 5, 7.5, 10} : new[] {2, 2.5, 5, 10}; double unroundedTickSize = (max - min) / (tickCount - 1); double x = Math.Ceiling(Math.Log10(unroundedTickSize) - 1); double pow10X = Math.Pow(10, x); TickRange = RoundUp(unroundedTickSize/pow10X, candidates) * pow10X; Min = TickRange * Math.Floor(min / TickRange); Max = TickRange * Math.Ceiling(max / TickRange); } // 1 < scaled <= 10 private static double RoundUp(double scaled, IEnumerable<double> candidates) { return candidates.First(candidate => scaled <= candidate); } } 
0
Dec 21 '12 at 11:26
source share

The above algorithms do not take into account the case when the range between the minimum and maximum values ​​is too small. But what if these values ​​are much higher than zero? Then we have the opportunity to run the y axis with a value greater than zero. In addition, to prevent our line from being completely on the top or bottom of the chart, we need to give it “air to breathe”.

To cover these cases, I wrote (in PHP) the above code:

 function calculateStartingPoint($min, $ticks, $times, $scale) { $starting_point = $min - floor((($ticks - $times) * $scale)/2); if ($starting_point < 0) { $starting_point = 0; } else { $starting_point = floor($starting_point / $scale) * $scale; $starting_point = ceil($starting_point / $scale) * $scale; $starting_point = round($starting_point / $scale) * $scale; } return $starting_point; } function calculateYaxis($min, $max, $ticks = 7) { print "Min = " . $min . "\n"; print "Max = " . $max . "\n"; $range = $max - $min; $step = floor($range/$ticks); print "First step is " . $step . "\n"; $available_steps = array(5, 10, 20, 25, 30, 40, 50, 100, 150, 200, 300, 400, 500); $distance = 1000; $scale = 0; foreach ($available_steps as $i) { if (($i - $step < $distance) && ($i - $step > 0)) { $distance = $i - $step; $scale = $i; } } print "Final scale step is " . $scale . "\n"; $times = floor($range/$scale); print "range/scale = " . $times . "\n"; print "floor(times/2) = " . floor($times/2) . "\n"; $starting_point = calculateStartingPoint($min, $ticks, $times, $scale); if ($starting_point + ($ticks * $scale) < $max) { $ticks += 1; } print "starting_point = " . $starting_point . "\n"; // result calculation $result = []; for ($x = 0; $x <= $ticks; $x++) { $result[] = $starting_point + ($x * $scale); } return $result; } 
0
Sep 30 '16 at 12:08
source share

Converted this answer as Swift 4

 extension Int { static func makeYaxis(yMin: Int, yMax: Int, ticks: Int = 10) -> [Int] { var yMin = yMin var yMax = yMax var ticks = ticks // This routine creates the Y axis values for a graph. // // Calculate Min amd Max graphical labels and graph // increments. The number of ticks defaults to // 10 which is the SUGGESTED value. Any tick value // entered is used as a suggested value which is // adjusted to be a 'pretty' value. // // Output will be an array of the Y axis values that // encompass the Y values. var result = [Int]() // If yMin and yMax are identical, then // adjust the yMin and yMax values to actually // make a graph. Also avoids division by zero errors. if yMin == yMax { yMin -= ticks // some small value yMax += ticks // some small value } // Determine Range let range = yMax - yMin // Adjust ticks if needed if ticks < 2 { ticks = 2 } else if ticks > 2 { ticks -= 2 } // Get raw step value let tempStep: CGFloat = CGFloat(range) / CGFloat(ticks) // Calculate pretty step value let mag = floor(log10(tempStep)) let magPow = pow(10,mag) let magMsd = Int(tempStep / magPow + 0.5) let stepSize = magMsd * Int(magPow) // build Y label array. // Lower and upper bounds calculations let lb = stepSize * Int(yMin/stepSize) let ub = stepSize * Int(ceil(CGFloat(yMax)/CGFloat(stepSize))) // Build array var val = lb while true { result.append(val) val += stepSize if val > ub { break } } return result } } 
0
Mar 13 '19 at 21:00
source share

For those who need it in ES5 Javascript, they fought a bit, but here it is:

 var min=52; var max=173; var actualHeight=500; // 500 pixels high graph var tickCount =Math.round(actualHeight/100); // we want lines about every 100 pixels. if(tickCount <3) tickCount =3; var range=Math.abs(max-min); var unroundedTickSize = range/(tickCount-1); var x = Math.ceil(Math.log10(unroundedTickSize)-1); var pow10x = Math.pow(10, x); var roundedTickRange = Math.ceil(unroundedTickSize / pow10x) * pow10x; var min_rounded=roundedTickRange * Math.floor(min/roundedTickRange); var max_rounded= roundedTickRange * Math.ceil(max/roundedTickRange); var nr=tickCount; var str=""; for(var x=min_rounded;x<=max_rounded;x+=roundedTickRange) { str+=x+", "; } console.log("nice Y axis "+str); 

Based on Toon Krijtje's excellent answer.

0
Jun 04 '19 at 10:36
source share



All Articles