algorithm
self-scaling
ruler
plotter
GUI component

Algorithm for self-scaling ruler in plotter GUI component

Master System Design with Codemia

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

Introduction

A self-scaling ruler in a plotting component should choose tick marks that are both mathematically correct and visually readable. The usual solution is not to place ticks at arbitrary fractions, but to compute a "nice" step size such as 0.1, 0.2, 0.5, 1, 2, 5, 10, and then expand the visible range to align with those values.

The Core Idea: Nice Numbers

Suppose your visible data range is from 3.2 to 9.7. A naive algorithm might divide that into five equal pieces and produce ugly labels such as:

text
3.2, 4.5, 5.8, 7.1, 8.4, 9.7

Those are mathematically valid, but not pleasant to read.

A better ruler rounds the range and spacing to "nice" human-friendly values, for example:

text
2, 4, 6, 8, 10

That is the heart of a self-scaling ruler algorithm.

Standard Algorithm

The standard workflow is:

  1. compute raw data minimum and maximum
  2. choose a target number of ticks
  3. estimate a raw step size
  4. round that step to a nice number
  5. extend the axis bounds outward to multiples of that step

The nice-number set is usually based on multiples of:

  • '1'
  • '2'
  • '5'
  • '10'

scaled by powers of ten.

A Working Python Example

Here is a compact implementation of that idea:

python
1import math
2
3
4def nice_number(value, round_value):
5    exponent = math.floor(math.log10(value))
6    fraction = value / (10 ** exponent)
7
8    if round_value:
9        if fraction < 1.5:
10            nice_fraction = 1
11        elif fraction < 3:
12            nice_fraction = 2
13        elif fraction < 7:
14            nice_fraction = 5
15        else:
16            nice_fraction = 10
17    else:
18        if fraction <= 1:
19            nice_fraction = 1
20        elif fraction <= 2:
21            nice_fraction = 2
22        elif fraction <= 5:
23            nice_fraction = 5
24        else:
25            nice_fraction = 10
26
27    return nice_fraction * (10 ** exponent)
28
29
30def nice_axis(data_min, data_max, max_ticks=6):
31    if data_min == data_max:
32        return data_min, data_max, 1, [data_min]
33
34    raw_range = data_max - data_min
35    nice_range = nice_number(raw_range, round_value=False)
36    step = nice_number(nice_range / (max_ticks - 1), round_value=True)
37
38    axis_min = math.floor(data_min / step) * step
39    axis_max = math.ceil(data_max / step) * step
40
41    ticks = []
42    current = axis_min
43    while current <= axis_max + step * 0.5:
44        ticks.append(round(current, 12))
45        current += step
46
47    return axis_min, axis_max, step, ticks
48
49
50print(nice_axis(3.2, 9.7))

This returns a readable axis minimum, axis maximum, step size, and tick list.

Why This Feels Good in a GUI

The algorithm works well because users do not just want a mathematically exact scale. They want labels they can scan quickly. Tick values such as 0.2, 0.5, 2, 20, or 500 are easier to interpret than long fractional artifacts from raw division.

That is why plotting libraries almost always use some variant of this method rather than simply dividing the range by pixel width or by the exact number of requested intervals.

Handle Zooming and Panning

In a GUI plotter, the ruler usually updates whenever:

  • the data range changes
  • the user zooms
  • the user pans

The algorithm stays the same. Only data_min and data_max change. Each redraw recalculates the visible axis and tick labels from the current viewport.

This is also why the algorithm should be cheap. It runs often, but it only involves a small amount of arithmetic.

Edge Cases

There are a few important edge cases:

  • 'data_min == data_max'
  • extremely small ranges such as 0.00012 to 0.00019
  • extremely large ranges such as millions
  • negative-only or mixed-sign ranges

The nice-number approach still works across those cases because the power-of-ten scaling adapts automatically. The special case where min equals max usually needs a fallback step so the ruler can still render something sensible.

Common Pitfalls

  • Dividing the raw range evenly and accepting ugly tick labels that are hard to read.
  • Forgetting to expand the axis bounds outward to clean multiples of the chosen step.
  • Failing to handle the equal-min-and-max case, which can produce division-by-zero logic.
  • Tying tick count directly to pixels without a nice-number rounding stage.
  • Using floating-point values directly for labels without rounding, which can expose representation noise such as 0.30000000000000004.

Summary

  • A self-scaling ruler should choose human-friendly tick spacing, not just mathematically even slices.
  • The standard solution uses nice numbers built from 1, 2, 5, and powers of ten.
  • Compute a raw step, round it to a nice step, then expand the axis to step-aligned bounds.
  • The same algorithm works for zooming and panning because it recalculates from the current visible range.
  • Good ruler behavior depends as much on readability as on correctness.

Course illustration
Course illustration

All Rights Reserved.