Generating North Arrows and Graticules Programmatically

Derive the graticule interval from the frame extent using the same one-two-five ladder a scale bar uses, place the labels outside the neatline where they cannot collide with map content, and add a north arrow only when the graticule cannot already state the orientation.

Core Algorithm and Workflow

A graticule is a reference frame, and it works when it carries roughly three to seven labelled lines per axis. Fewer and it provides no reference; more and it becomes visual noise competing with the data. So the interval is a function of the extent, not a constant, and it has to be recomputed per sheet in any series whose sheets differ in scale.

The selection rule is the same one that governs scale bar length: take the coarsest candidate from a one-two-five ladder that still yields enough lines. What differs is only the units and the minimum count.

Once the interval is chosen, three things follow. The lines must be generated in geographic coordinates and densified before projection, or they render as chords across their true curves. The labels must be placed outside the neatline, where they occupy reserved space rather than competing with map content. And the orientation reference — an arrow, or nothing — depends on whether the graticule has already answered the question.

Why a meridian must be densified before it is projected A frame in a conic projection with two versions of the same meridian drawn. The two-point version is a straight chord from the top of the frame to the bottom. The densified version, with a vertex every degree, follows the true curve and deviates from the chord by up to nine millimetres at the middle of the frame. A note records that the error grows with latitude and frame width. 9 mm chord error two-point projection dashed — a straight chord densified first solid — one vertex per degree The error grows with latitude and with frame width, so it is worst where graticules matter. A graticule drawn as chords is not a decorative flaw — it misplaces the coordinate reference.
Both lines claim to be the same meridian. Only one of them is, and the difference is a real positional error a reader would use the graticule to avoid.

Production-Ready Python Implementation

import math
from pyproj import Transformer


def graticule_interval(span: float, min_lines: int = 3) -> float:
    """Coarsest 1-2-5 interval that still yields `min_lines` across a span."""
    candidates = sorted(n * 10 ** e for e in range(-3, 4) for n in (1, 2, 5))
    usable = [c for c in candidates if span / c >= min_lines]
    if not usable:
        raise ValueError(f"span of {span} is too small for a graticule")
    return usable[-1]


def graticule_lines(west: float, south: float, east: float, north: float,
                    to_crs: str, step_deg: float = 1.0) -> dict:
    """Densified, projected meridians and parallels for a frame extent."""
    lon_int = graticule_interval(east - west)
    lat_int = graticule_interval(north - south)
    tf = Transformer.from_crs("EPSG:4326", to_crs, always_xy=True)

    def densify(a: float, b: float, n: int) -> list:
        return [a + (b - a) * i / n for i in range(n + 1)]

    meridians, parallels = [], []
    lon = math.ceil(west / lon_int) * lon_int
    while lon <= east:
        lats = densify(south, north, max(8, int((north - south) / step_deg)))
        meridians.append({"value": lon,
                          "xy": [tf.transform(lon, la) for la in lats]})
        lon += lon_int

    lat = math.ceil(south / lat_int) * lat_int
    while lat <= north:
        lons = densify(west, east, max(8, int((east - west) / step_deg)))
        parallels.append({"value": lat,
                          "xy": [tf.transform(lo, lat) for lo in lons]})
        lat += lat_int

    return {"meridians": meridians, "parallels": parallels,
            "lon_interval": lon_int, "lat_interval": lat_int}


def grid_convergence(lon: float, lat: float, central_meridian: float) -> float:
    """Angle in degrees between grid north and true north at a point."""
    return math.degrees(math.atan(math.tan(math.radians(lon - central_meridian))
                                  * math.sin(math.radians(lat))))

The math.ceil on the starting value is what makes lines land on round coordinates rather than on the frame edge. A graticule whose first meridian is at 3.847 degrees is arithmetically correct and useless as a reference.

grid_convergence is the function that decides whether an arrow is warranted. At the centre of a UTM zone it returns zero; three degrees of longitude away at 55 degrees north it returns about 2.5 degrees, which is visible on a large sheet and worth stating.

Performance Tuning and Cartographic Best Practices

  • Label outside the neatline. Graticule labels placed inside the frame have to enter the label collision pass, where they can be suppressed — and a suppressed graticule label leaves an unlabelled line, which is less useful than no line at all. Outside the neatline they sit in reserved marginal space and are never in contention with map content.
  • Units on the first and last label only. Repeating “°E” on every tick is the most common reason a graticule label run exceeds the space allocated to it, the same failure mode described for scale bars in How to Automate Scale Bar Generation in Python.
  • Densify by angular step, not by vertex count. A fixed vertex count produces smooth lines on a small extent and visible faceting on a large one. One vertex per degree, with a floor of eight, holds across both.
  • Omit the arrow when the graticule speaks. On a north-up sheet with labelled meridians, an arrow adds nothing. On a small-scale map where meridians visibly converge within the frame, a single arrow is worse than nothing because north is a different direction at each edge.
  • State which north the arrow means. Grid, true and magnetic north differ by amounts that matter — convergence reaches several degrees at a UTM zone edge, and magnetic declination exceeds ten degrees across much of the populated world. An unlabelled arrow forces the reader to guess which.
Three norths, and why an unlabelled arrow is ambiguous A diagram of three north directions radiating from one point. Grid north is drawn vertically. True north sits 2.5 degrees to the east of it, the grid convergence at this location. Magnetic north sits 11.4 degrees west of true north, the declination for the year given. A note records that an arrow with no label leaves the reader to guess which of the three it represents. grid N true N — +2.5° convergence magnetic N −11.4° (2026) which one is the arrow? grid — for use with this map's grid true — for a general reference map magnetic — navigation only, dated Fourteen degrees separate the outer two here. On a navigation map that is a kilometre of error over four kilometres walked, which is the reason the label is not optional.
The angles are small on the page and large on the ground. Labelling the arrow costs one string and removes the ambiguity entirely.

Integration and Next Steps

The graticule takes its extent from the map frame, so it is computed after the layout allocation described in Map Layout and Composition Automation, and its label band is one of the marginalia that allocation must reserve. In an atlas the interval varies per sheet where the scale varies and must be held constant where it does not, which is the same publication-versus-page distinction that governs class breaks in Atlas and Map Series Automation.

Labels inside the neatline compete; labels outside it do not Two frames. In the first, graticule labels are placed inside the neatline where they overlap a road network and two of them have been suppressed by the collision pass, leaving unlabelled lines. In the second, the labels sit in a reserved band outside the neatline, all present, with the coordinate units shown only on the first and last. labels inside the neatline 3°E 5°E one label suppressed — the line is now unlabelled labels in the reserved margin 3°E 4 5°E all present; units on first and last only The margin band costs a few millimetres of frame and removes the graticule from contention entirely.
A suppressed graticule label is worse than a missing line, because the line remains and now references nothing. Reserving margin space avoids the contest rather than winning it.

Frequently Asked Questions

Why do my graticule lines render as straight chords?

Because they were projected as two-point segments. A meridian is straight in geographic coordinates and curved in nearly every projected CRS, so transforming only its endpoints and joining them gives a chord across the true curve. Densify each line — a vertex every degree, with a floor of eight vertices — before reprojecting. The chord error grows with latitude and with frame width, so it is largest precisely on the small-scale sheets where a graticule does the most work.

Is a north arrow required on every map?

No. On a north-up sheet with a labelled graticule, the meridians state the orientation more precisely than an arrow can, and the arrow is redundant decoration. Add one when the map is deliberately rotated, when grid convergence within the frame is large enough to notice, or when there is no graticule at all. On a small-scale map where meridians visibly converge inside the frame, a single arrow is actively wrong: north points in a measurably different direction at each edge, and one arrow cannot say that.

Should the arrow point to true north, grid north or magnetic north?

Grid north for a map that will be used with its own coordinate grid, true north for a general reference map, and magnetic north only for navigation and only with a dated declination. Whichever is chosen has to be stated on the map. The three can differ by well over ten degrees in populated regions, which on a walked route is hundreds of metres of error — enough that leaving the reader to infer which north the arrow means is a real defect rather than a stylistic omission.

How do I keep graticule labels from colliding with the map?

Place them outside the neatline in a reserved margin band. Labels inside the frame have to compete in the collision pass, and losing that contest leaves a drawn line with no label, which references nothing and is worse than having drawn no line. The margin band costs a few millimetres of frame width and takes the graticule out of contention permanently, which is the correct trade for an element whose whole purpose is to be a fixed reference.


Back to Map Layout and Composition Automation