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.
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.
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.
Edge cases or notes
preserve_topology=Trueis 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.
Internal links
- How to simplify geometry in GeoPandas โ the operation itself
- Zoom and generalisation explained โ the same rule applied per web-map zoom level
- Which map elements are actually required โ the other space budget
- Fixing a map export that is hundreds of megabytes โ when the invisible detail has been shipped
- How to export a map at print quality โ where scale meets DPI
- How to reduce GIS file size โ the storage side of the same problem
- Coordinate precision explained โ precision that exceeds accuracy
- How to fix gaps and overlaps in a polygon coverage โ repairing slivers after simplification
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.