graph plotting
grid line intervals
algorithm design
data visualization
graph aesthetics

Algorithm for nice grid line intervals on a graph

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

Good grid lines make a chart feel obvious. Readers can estimate values quickly because the axis labels land on familiar numbers instead of awkward steps such as 37 or 83.

The usual goal is not to force an exact number of lines. It is to choose spacing that covers the data range, keeps the chart uncluttered, and uses values that people can scan without doing mental arithmetic.

What Makes an Interval "Nice"

Most graphing systems build tick intervals from a small set of values scaled by powers of ten. Common candidates are 11, 22, 2.52.5, 55, and 1010 times 10k10^k.

That produces steps such as 0.1, 0.2, 0.5, 1, 2, 5, 20, 50, and 200. These values are easy to label and easy to compare, which is why chart libraries and plotting tools tend to favor them.

If you divide a range directly by a target tick count, you usually get a raw step that is mathematically correct but visually annoying. For a range from 32 to 258 with about 6 ticks, the raw step is:

rawStep=2583261=45.2\text{rawStep} = \frac{258 - 32}{6 - 1} = 45.2

That is a reasonable intermediate value, but it is not a good label spacing. An axis labeled 32, 77.2, 122.4, 167.6 is much harder to read than one labeled in clean multiples of 50.

The Core Algorithm

The standard solution is a small rounding algorithm.

First, compute the raw step from the requested number of ticks:

rawStep=maxmintargetTicks1\text{rawStep} = \frac{\text{max} - \text{min}}{\text{targetTicks} - 1}

Next, find the power-of-ten scale of that number:

e=log10(rawStep)e = \lfloor \log_{10}(\text{rawStep}) \rfloor

Then normalize the raw step into a fraction between 11 and 1010:

fraction=rawStep10e\text{fraction} = \frac{\text{rawStep}}{10^e}

Now pick the nearest nice fraction from your candidate set. In a common implementation, a fraction near 4.52 rounds to 5. The final step becomes:

niceStep=niceFraction×10e\text{niceStep} = \text{niceFraction} \times 10^e

With the earlier example, the raw step is 45.2, so e=1e = 1 and the normalized fraction is 4.52. Rounding that to the nice fraction 5 gives a final step of 50.

The last step is expanding the visible axis so the first and last grid lines also land on clean multiples:

niceMin=minniceStep×niceStep\text{niceMin} = \left\lfloor \frac{\text{min}}{\text{niceStep}} \right\rfloor \times \text{niceStep}

niceMax=maxniceStep×niceStep\text{niceMax} = \left\lceil \frac{\text{max}}{\text{niceStep}} \right\rceil \times \text{niceStep}

For 32 through 258 with a step of 50, that gives 0 through 300. The chart becomes slightly wider than the raw data range, but the labels are far easier to interpret.

JavaScript Example

The following implementation is small enough to drop into a plotting utility or use before rendering an SVG or canvas chart.

javascript
1function niceNumber(value, round) {
2  const exponent = Math.floor(Math.log10(value));
3  const fraction = value / 10 ** exponent;
4  let niceFraction;
5
6  if (round) {
7    if (fraction < 1.5) niceFraction = 1;
8    else if (fraction < 3) niceFraction = 2;
9    else if (fraction < 7) niceFraction = 5;
10    else niceFraction = 10;
11  } else {
12    if (fraction <= 1) niceFraction = 1;
13    else if (fraction <= 2) niceFraction = 2;
14    else if (fraction <= 5) niceFraction = 5;
15    else niceFraction = 10;
16  }
17
18  return niceFraction * 10 ** exponent;
19}
20
21function buildTicks(min, max, targetTicks = 6) {
22  if (min === max) return [min];
23
24  const range = niceNumber(max - min, false);
25  const step = niceNumber(range / (targetTicks - 1), true);
26  const niceMin = Math.floor(min / step) * step;
27  const niceMax = Math.ceil(max / step) * step;
28  const ticks = [];
29
30  for (let value = niceMin; value <= niceMax + step * 0.5; value += step) {
31    ticks.push(Number(value.toFixed(12)));
32  }
33
34  return ticks;
35}
36
37console.log(buildTicks(32, 258, 6));
38// [0, 50, 100, 150, 200, 250, 300]

The two important details are that niceNumber can either round or expand, and that toFixed keeps floating-point noise out of the labels.

If you want denser labels, increase targetTicks. If you want a tighter fit, you can use a richer candidate set, but the axis may stop looking familiar.

Why This Works Well in Practice

This algorithm is popular because it behaves consistently across wildly different scales. It works for values around 0.003, around 250, and around 3,000,000 without needing separate rules for each range.

It also reflects how people read charts. Viewers generally do not care whether the mathematical optimum step is 43.8 or 47.1. They care whether they can glance at the axis and understand the chart immediately. Nice intervals optimize for that human requirement.

Common Pitfalls

One common mistake is treating the requested tick count as a hard requirement. In practice it is only a hint. A chart with seven clean ticks is usually better than a chart with exactly six ugly ones.

Another mistake is using ordinary decimal rounding on the raw step. That can produce intervals such as 40 or 37.5. Those may be acceptable in special domains, but they are not part of the standard nice-number progression most graphing tools use.

Floating-point drift is another source of confusion. Repeated addition can produce values like 0.30000000000000004, which is why it is worth rounding tick values before display.

Finally, this algorithm is for linear axes. Logarithmic axes are a different problem because their ticks are based on multiplicative jumps rather than additive ones.

Summary

  • Nice grid intervals usually come from a small set of values scaled by powers of ten.
  • Start with a raw step, normalize it, and round it to a nearby nice fraction.
  • Expand the axis bounds to clean multiples of the final step so the whole range is covered.
  • The target number of ticks is a guideline, not a strict promise.
  • A short implementation can generate readable axis labels consistently across many datasets.

Course illustration
Course illustration

All Rights Reserved.