Semi-log graph paper looks like someone drew it by hand with a shaky ruler: the lines bunch up
toward the top of every band and then start over. It is actually one of the most regular grids
there is. Every line comes from a single formula, and the only real decision is how many lines
to draw before the page turns grey.
I recently added semi-log and log-log paper to a printable graph paper generator, and these are
the notes I wish I'd had before starting.
One decade, one formula
On a log axis, a value v sits at a distance proportional to log10(v). If one decade (the
stretch from 1 to 10, or 10 to 100) is L points long, then inside a decade the value v
(between 1 and 10) is drawn at
const pos = L * Math.log10(v); // 0 for v = 1, L for v = 10 That's the whole trick. 2 lands at 30.1% of the decade, 3 at 47.7%, 5 at 69.9%, and 9 at 95.4%.
That's why the lines crowd together near the top: the gap between 9 and 10 is only 4.6% of the
decade, while the gap between 1 and 2 is 30%.
Paper with several cycles just repeats the decade. With cycles decades stacked in a height H:
const L = H / cycles; for (let k = 0; k < cycles; k++) { for (let v = 1; v < 10; v++) { const y = bottom - (k + Math.log10(v)) * L; // PDF/SVG y grows downward drawLine(y, v === 1 ? "decade" : "integer"); } } drawLine(bottom - H, "decade"); // the top edge closes the last decade The labels are the classic 1, 2, 3 … 9, 1, 2, 3 … with no powers of ten. On real log paper the
user decides whether the first "1" is 0.01 or 1000, so the paper shouldn't decide for them.
Subdivisions: the part that needs judgement
Good log paper also has finer lines: 1.1, 1.2 … 1.9 between 1 and 2, then steps of 0.2 up to 5,
then 0.5 up to 10. The step changes because the space shrinks. A fixed 0.1 step from 5 to 10
would put lines only a fraction of a point apart.
The question is when to leave them out. Three cycles on a Letter page give a decade of about
250 pt, and every subdivision fits easily. Six cycles in landscape give less than 100 pt, and the
same lines would merge into a grey smear when printed.
The closest pair of lines in each band is always at its top end, because that's where the log
curve is flattest. So the test is a single subtraction per band:
export function logTicks(decadeLen) { const ticks = []; for (let k = 1; k < 10; k++) ticks.push({ t: Math.log10(k), rank: k === 1 ? 2 : 1, k }); const bands = [[1, 2, 0.1], [2, 5, 0.2], [5, 10, 0.5]]; for (const [a, b, step] of bands) { // closest pair in this band: (b - step, b) if (decadeLen * (Math.log10(b) - Math.log10(b - step)) < 2.2) continue; for (let v = a + step; v < b - 1e-9; v += step) { if (Math.abs(v - Math.round(v)) < 1e-9) continue; // integers are already there ticks.push({ t: Math.log10(v), rank: 0 }); } } return ticks.sort((p, q) => p.t - q.t); } 2.2 pt is about 0.8 mm. Much closer than that, thin 0.35 pt lines stop reading as separate lines
on a typical office print and turn into a grey band. Working the inequality
backwards gives the decade length each band needs:
| Band | Step | Minimum decade length |
|---|---|---|
| 1–2 | 0.1 | 98.8 pt (1.37 in) |
| 2–5 | 0.2 | 124.1 pt (1.72 in) |
| 5–10 | 0.5 | 98.8 pt (1.37 in) |
So the 2–5 band is the first to drop out as cycles increase. Each band is decided separately, so the paper loses detail gradually
instead of jumping from "everything" to "nothing".
Two details that bit me:
- Floating-point steps.
1 + 0.1 + 0.1 + …doesn't land exactly on 2, so the loop bound isb - 1e-9and integers are skipped with a tolerance. Without that you get a duplicate line at 2.0000000000000004, drawn twice and looking slightly heavier than its neighbours. - Three weights, not two. Decade lines, integer lines and subdivisions each need their own weight. With only major and minor, either the 2…9 lines vanish or the decades stop standing out. I use 1.4× the major weight for decades, 0.7× for integers and the normal minor weight for the rest.
Semi-log vs log-log
Semi-log paper has one log axis and one ordinary linear axis. The linear axis is a normal grid of
whole squares, fitted and centred inside the margin like any graph paper. Only the log axis
stretches to fill the available length exactly, because a decade has to be a whole decade.
Log-log paper simply runs the same function along both axes, usually with different cycle counts
(say 3 across and 5 up). With a helper that takes a callback for "draw a line at fraction f of
this axis", both papers share all of the tick code:
function logAxisLines(len, cycles, place) { const ticks = logTicks(len / cycles); for (let k = 0; k < cycles; k++) for (const tk of ticks) place((k + tk.t) / cycles, tk.rank); place(1, 2); // closing decade line } // vertical log axis logAxisLines(H, cyclesY, (f, rank) => hline(bottom - f * H, rank)); // horizontal log axis (log-log only) logAxisLines(W, cyclesX, (f, rank) => vline(left + f * W, rank)); Checking it worked
A quick way to test log paper: plot y = 10^x on semi-log paper and it must be a perfectly
straight line through one decade per unit of x. On log-log paper, y = x² must be a straight line
with a slope of exactly 2 (two decades up for every decade across). If you get a bend, the cycle
length is off, usually because the margin was subtracted on one side only.
Students often meet log paper in physics labs (radioactive decay, RC discharge) and in
chemistry (pH, reaction rates). Engineers use it for Bode plots and S-N fatigue curves. All of
those need the same thing: lines exactly where log10 says they belong, and only as many as the
printer can show.
If you want to try the result, the generator with 1–6 cycles, either axis logarithmic and PDF
export is at graphpaperpdf.net.

