Map Scale Explained: How Much Detail a Scale Can Hold

Problem statement

A coastline digitised at 1:10,000 plotted on a page at 1:2,000,000 contains roughly two hundred times more vertices than the page can show. The extra vertices do not make the map more accurate โ€” they make the file larger, the rendering slower and the line noisier, and every one of them is invisible.

The relationship is not a matter of taste. A person can resolve about 0.2 mm on a printed page, so the ground distance corresponding to one visible dot is a fixed function of scale:

scale            0.2 mm on the ground
1:10,000                  2 m
1:50,000                 10 m
1:250,000                50 m
1:1,000,000             200 m
1:10,000,000          2,000 m

Any vertex closer to its neighbour than that distance cannot be seen. Removing them is not a loss of information at this scale โ€” it is the definition of drawing at this scale.

Quick answer

Simplify to the tolerance the scale can show, and measure what it costs:

def tolerance_for_scale(scale_denominator, resolvable_mm=0.2):
    """Ground distance of the smallest visible mark, in map units (metres)."""
    return resolvable_mm / 1000 * scale_denominator


tol = tolerance_for_scale(1_000_000)      # 200 m
simplified = gdf.copy()
simplified["geometry"] = gdf.geometry.simplify(tol, preserve_topology=True)

Measured on 51 US state polygons โ€” 53,352 vertices in an equal-area projection:

tolerance   vertices   % of original   PDF size
   none       53,352        100.0%      457 kB
    50 m      47,217         88.5%      403 kB
   100 m      42,349         79.4%      363 kB
   200 m      34,484         64.6%      298 kB
   500 m      22,590         42.3%      198 kB
 1,000 m      14,943         28.0%      133 kB
 2,000 m       9,469         17.7%       86 kB
 5,000 m       5,140          9.6%       47 kB

At 1:1,000,000 the correct tolerance is 200 m, which removes a third of the vertices and a third of the file size while changing nothing a reader can see.

Bar chart of the ground distance of 0.2 mm at five map scales.
This is the tolerance to simplify to โ€” the detail above it is paid for and invisible.

Step-by-step solution

1. Know the scale you are actually drawing at

Scale is a property of the finished figure, not of the data. Compute it from the extent and the physical width of the output:

def map_scale(ax, figure_width_inches):
    """The representative fraction of the plotted figure."""
    x0, x1 = ax.get_xlim()
    return (x1 - x0) / (figure_width_inches * 0.0254)

A map of Britain 6 inches wide spans about 700 km, which is 1:4,600,000. At that scale the resolvable ground distance is 920 m โ€” and a dataset digitised to 1 m precision is carrying three orders of magnitude more detail than the page can render.

2. Convert the scale to a tolerance

The 0.2 mm rule is the standard cartographic threshold for what a reader can separate on paper. For screen output, work from the pixel size instead: one CSS pixel at typical viewing distance is comparable, so use the ground distance per pixel.

Both give the same kind of number: a distance below which geometry is invisible.

3. Simplify with topology preserved

simplify(tolerance, preserve_topology=True) uses Douglas-Peucker while refusing to produce invalid geometry. It is fast, it is in Shapely, and for a single layer it is usually enough.

What it does not do is keep shared boundaries consistent between neighbouring polygons. Two polygons simplified independently can develop gaps and overlaps along the boundary they used to share, because each one made its own decisions about which vertices to keep.

4. For coverages, simplify the topology, not the polygons

Where polygons share boundaries โ€” administrative areas, land parcels, any coverage โ€” independent simplification produces slivers. The correct approach is topological simplification, which extracts the shared arcs, simplifies each arc once, and rebuilds the polygons from them.

In Python that means a topology-aware tool rather than GeoSeries.simplify. If one is not available, the pragmatic fallback is to simplify at a tolerance small enough that the slivers are sub-pixel โ€” which is exactly the tolerance the scale implies.

5. Check the result at the output size, not zoomed in

Simplification always looks wrong at full zoom, because you are looking at detail the map will never show. Judge it by exporting at the real size and comparing.

The measurement to trust is the file: at 200 m tolerance the PDF fell from 457 kB to 298 kB with no visible change at 1:1,000,000.

6. Keep the full-detail data

Simplify for display, in the plotting pipeline, and keep the source geometry intact. A simplified layer is a rendering artefact; overwriting the original destroys measurements you will want later โ€” areas, lengths and adjacency all change when vertices are removed.

Bar chart of vertex counts and PDF sizes at six simplification tolerances.
File size tracks vertex count almost exactly: 3.6ร— fewer vertices, 3.4ร— fewer bytes.

Code examples

Example 1 โ€” the scale-to-tolerance pipeline

import geopandas as gpd


def simplify_for_scale(gdf, scale_denominator, resolvable_mm=0.2, report=True):
    """Simplify to what the scale can show, and say what it cost."""
    tol = resolvable_mm / 1000 * scale_denominator

    def count(geoms):
        total = 0
        for g in geoms:
            if g is None or g.is_empty:
                continue
            if g.geom_type == "Polygon":
                total += len(g.exterior.coords)
            elif g.geom_type == "MultiPolygon":
                total += sum(len(p.exterior.coords) for p in g.geoms)
            elif g.geom_type in ("LineString", "MultiLineString"):
                total += sum(len(part.coords) for part in getattr(g, "geoms", [g]))
        return total

    before = count(gdf.geometry)
    out = gdf.copy()
    out["geometry"] = gdf.geometry.simplify(tol, preserve_topology=True)
    after = count(out.geometry)

    if report:
        print(f"scale 1:{scale_denominator:,}  โ†’  tolerance {tol:,.0f} m")
        print(f"vertices {before:,} โ†’ {after:,} ({100 * after / before:.1f}%)")
        invalid = (~out.geometry.is_valid).sum()
        if invalid:
            print(f"  ! {invalid} geometries became invalid โ€” lower the tolerance")
    return out

Example 2 โ€” how much detail is invisible, before you simplify

import numpy as np


def invisible_vertex_fraction(gdf, scale_denominator, resolvable_mm=0.2):
    """What fraction of vertices are closer together than the page can resolve?"""
    tol = resolvable_mm / 1000 * scale_denominator
    close = total = 0

    for geom in gdf.geometry:
        if geom is None or geom.is_empty:
            continue
        rings = ([geom.exterior] if geom.geom_type == "Polygon"
                 else [p.exterior for p in geom.geoms] if geom.geom_type == "MultiPolygon"
                 else [geom])
        for ring in rings:
            coords = np.asarray(ring.coords)
            if len(coords) < 2:
                continue
            steps = np.linalg.norm(np.diff(coords, axis=0), axis=1)
            close += int((steps < tol).sum())
            total += len(steps)

    print(f"at 1:{scale_denominator:,} ({tol:,.0f} m per visible dot)")
    print(f"{close:,} of {total:,} segments ({100 * close / total:.1f}%) are shorter "
          f"than one resolvable mark")
    return close / total

This is the number that ends the argument about whether simplification is "losing data". At small scales it is routinely 80% or more of segments, none of which can be drawn.

Example 3 โ€” a simplification ladder to choose from

def simplification_ladder(gdf, tolerances=(50, 100, 200, 500, 1000, 2000, 5000),
                          render=None):
    """Vertices and file size at each tolerance, so the choice is informed."""
    import io
    import matplotlib.pyplot as plt

    base = None
    print(f"{'tolerance':>10} {'vertices':>10} {'% kept':>8} {'PDF kB':>8}")
    for tol in (None, *tolerances):
        layer = gdf if tol is None else gdf.assign(
            geometry=gdf.geometry.simplify(tol, preserve_topology=True))
        n = sum(len(g.exterior.coords) if g.geom_type == "Polygon"
                else sum(len(p.exterior.coords) for p in g.geoms)
                for g in layer.geometry)
        base = base or n

        buf = io.BytesIO()
        fig, ax = plt.subplots(figsize=(8, 5))
        layer.plot(ax=ax, edgecolor="white", linewidth=0.4)
        ax.set_axis_off()
        fig.savefig(buf, format="pdf", bbox_inches="tight")
        plt.close(fig)

        label = "none" if tol is None else f"{tol:,} m"
        print(f"{label:>10} {n:10,} {100 * n / base:7.1f}% {buf.tell() / 1024:8.1f}")

Explanation

Why 0.2 mm and not some other number

Two marks closer than about 0.2 mm merge for a reader at normal viewing distance on paper. That threshold has been the working assumption in cartography for a long time and it survives contact with modern output: at 300 dpi, 0.2 mm is about 2.4 pixels, comfortably above the printer's resolution and at the edge of what the eye separates.

It is a rule of thumb, not a constant. Use 0.1 mm for a map that will be examined closely, 0.3 mm for one seen at arm's length โ€” the arithmetic is the same and the conclusion barely changes.

Why detail beyond the threshold costs more than it looks

Every invisible vertex is paid for three times: in file size, in render time, and in the visual noise that a stroked line accumulates when it doubles back on itself within a pixel.

The file measurements make the first concrete: 53,352 vertices produced a 457 kB PDF; 34,484 produced 298 kB with no visible difference at the intended scale. In a report with twenty such figures, that is 3 MB of invisible geometry.

Why coverages need topological simplification

simplify() operates on one geometry at a time. Two polygons that shared a boundary are simplified independently, and each keeps a different subset of the shared vertices โ€” so the boundary they used to share becomes two slightly different lines with slivers between them.

At the tolerance the scale implies, those slivers are sub-pixel and invisible, which is why the simple approach usually survives. It stops being acceptable when the simplified layer is used for anything but drawing: areas change, adjacency breaks, and a point-in-polygon test can fall in a gap.

Why simplification is a display step and not a data step

Simplified geometry has different areas, different lengths and different topology. Anything measured from it is measured from an approximation chosen for a particular output size.

Keeping the simplification inside the plotting function โ€” rather than saving a simplified file and forgetting โ€” means the measurements always come from the full-detail data and the display always comes from geometry appropriate to its scale.

Flow showing full-detail data feeding both measurement and a simplified display path.
Two consumers, one source: the measurements never see the approximation.

Edge cases or notes

  • preserve_topology=True is not free but it is nearly always right; without it, polygons can self-intersect.
  • Simplify in a projected CRS. A tolerance in degrees is a different distance at every latitude.
  • Check validity afterwards โ€” a large tolerance on a thin polygon can collapse it.
  • Small islands vanish at high tolerances. Filter by area first and decide deliberately.
  • Web maps simplify per zoom level, which is the same rule applied at many scales.
  • Vector export benefits most. A raster export at fixed DPI is already resolution-limited.
  • Do not simplify the layer you measure from. Area and length change.
  • Line data has the same rule โ€” a road network at 1:250,000 needs 50 m tolerance, not 1 m.

FAQ

How much detail can a map at a given scale show?

About 0.2 mm on the page. At 1:1,000,000 that is 200 m on the ground; at 1:50,000 it is 10 m. Vertices closer together than that cannot be drawn.

How do I choose a simplification tolerance?

From the scale: tolerance = 0.0002 ร— scale denominator in metres. At 1:1,000,000 that is 200 m, which removed 35% of vertices from a real dataset with no visible change.

Does simplifying lose accuracy?

It loses detail the output cannot show. Keep the full-detail data for measurement and simplify only in the plotting pipeline โ€” areas and lengths do change.

Why do gaps appear between polygons after simplifying?

Because each polygon was simplified independently and kept a different subset of the shared boundary's vertices. Coverages need topological simplification, or a tolerance small enough that the slivers are sub-pixel.

How much smaller does the file get?

Measured on 51 polygons: 53,352 vertices and 457 kB at full detail; 34,484 vertices and 298 kB at 200 m tolerance; 14,943 and 133 kB at 1,000 m.

Should I simplify before or after reprojecting?

After, and in the projected CRS you will plot in. A tolerance in degrees is a different ground distance at every latitude.