Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Algorithm for "nice" grid line intervals on a graph

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.

like image 539
cletus Avatar asked Dec 12 '08 01:12

cletus


2 Answers

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 
like image 171
Mark Ransom Avatar answered Sep 19 '22 13:09

Mark Ransom


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.

like image 24
Adam Liss Avatar answered Sep 18 '22 13:09

Adam Liss