Flow Maps Explained: What a Line on a Map Says About Movement

On this page

Problem statement

A flow map draws movement as lines between places, usually with width showing volume. It is one of the oldest thematic map types. Charles Minard drew trade and migration flows in the 1800s. It is also one of the easiest to misread. A line has length, curvature, direction, colour and width, but only width is meant to carry the data. The eye weighs a line by all of them at once, and length dominates: a long line to a distant city draws far more ink than a short, heavy flow to the next county. Every design decision on a flow map either fights that bias or feeds it.

This page explains what a flow map encodes, the main kinds of flow map, and how much the length bias distorts the picture. It measures that bias on a map of moves out of Los Angeles County.

Quick answer

On a flow map, width carries the volume; length, curvature and position carry geography, not data. Because ink grows with length × width, long flows look more important than they are. Measure the bias on a map before trusting it:

top = flows.nlargest(40, "movers").copy()
top["km"] = [np.hypot(*(np.array(CENTROID[d]) - np.array(ORIGIN))) / 1000 for d in top.dest]
top["width"] = 0.3 + 5 * np.sqrt(top.movers / top.movers.max())
top["ink"] = top.km * top.width
far = top.km > 1000
print(f"flows over 1,000 km: {far.mean():.0%} of the lines, {top.loc[far, 'movers'].sum() / top.movers.sum():.0%} of the movers, "
      f"{top.loc[far, 'ink'].sum() / top.ink.sum():.0%} of the ink")
flows over 1,000 km: 35% of the lines, 14% of the movers, 73% of the ink
Grouped bar chart of the 40 largest flows out of Los Angeles County by distance band: flows under 100 km carry 21% of movers but 1% of the ink; 100 to 500 km carry 53% of movers and 14% of ink; 500 to 1,000 km 12% and 12%; flows over 1,000 km carry 14% of movers and 73% of the ink. Grouped bar chart of the 40 largest flows out of Los Angeles County by distance band: flows under 100 km carry 21% of movers but 1% of the ink; 100 to 500 km carry 53% of movers and 14% of ink; 500 to 1,000 km 12% and 12%; flows over 1,000 km carry 14% of movers and 73% of the ink.
What the eye sees against what the data says. Long flows dominate the ink on a flow map although short flows carry most of the movers.

What each visual property means, and the distance bias

1. Know what each visual property means

A flow line's width is scaled to the flow (here by its square root). Its endpoints are fixed by geography. Its length is the distance between them. Curvature and colour are free design choices, commonly used for direction. A reader who equates "big on the map" with "important" is reading length and width together, so the map must show widths clearly in a legend.

2. See the bias by distance

Group the flows by distance and compare their share of movers with their share of ink:

band = pd.cut(top.km, [0, 100, 500, 1000, 5000], labels=["<100 km", "100–500 km", "500–1,000 km", ">1,000 km"])
summary = top.groupby(band, observed=True).agg(lines=("movers", "size"), movers=("movers", "sum"), ink=("ink", "sum"))
summary["movers %"] = summary.movers / summary.movers.sum()
summary["ink %"] = summary.ink / summary.ink.sum()
print(summary[["lines", "movers %", "ink %"]].to_string(formatters={"movers %": "{:.0%}".format, "ink %": "{:.0%}".format}))
              lines movers % ink %
km                                
<100 km           2      21%    1%
100–500 km       15      53%   14%
500–1,000 km      9      12%   12%
>1,000 km        14      14%   73%
The three shortest of the 40 largest flows out of Los Angeles County and their share of movers and ink. short and heavy, nearly invisibledestinationdistancemoversOrange County, CA82 km38,761Ventura County, CA89 km10,187Kern County, CA123 km10,503share of the 40 flows26% of movers, 2% of inkDistances between county representative points spatialworkflow.io
The heaviest flows are nearly invisible.

3. Counter it

Three habits reduce the bias. Use a legend with real widths. Add a companion chart or choropleth of the same numbers, where length plays no part. And in the caption, name the largest flows, which are often the shortest.

The shortest heavy flows

The data used on this page

The Census Bureau's county-to-county migration flows, 2016–2020 ACS for Los Angeles County and 2023 county outlines, projected to EPSG:5070 for distances.

from pathlib import Path

import geopandas as gpd
import numpy as np
import pandas as pd
import requests

Path("data").mkdir(exist_ok=True)
SRC = {"county-to-county-2016-2020-ins-outs-nets-gross.xlsx": "https://www2.census.gov/programs-surveys/demo/tables/geographic-mobility/2020/"
       "county-to-county-migration-2016-2020/county-to-county-migration-flows/county-to-county-2016-2020-ins-outs-nets-gross.xlsx",
       "cb_2023_us_county_20m.zip": "https://www2.census.gov/geo/tiger/GENZ2023/shp/cb_2023_us_county_20m.zip"}
for name, url in SRC.items():
    if not Path("data", name).exists():
        Path("data", name).write_bytes(requests.get(url, timeout=600).content)
COLS = ["a_st", "a_cty", "b_st", "b_cty", "a_state", "a_county", "b_state", "b_county",
        "flow", "flow_moe", "counter", "counter_moe", "net", "net_moe", "gross", "gross_moe"]
ca = pd.read_excel("data/county-to-county-2016-2020-ins-outs-nets-gross.xlsx", sheet_name="California",
                   header=None, skiprows=3, names=COLS, dtype=str)
la = ca[(ca.a_cty == "037") & ca.b_st.str.fullmatch(r"\d{3}", na=False)]
flows = pd.DataFrame({"dest": la.b_st.str[1:] + la.b_cty, "movers": pd.to_numeric(la.counter, errors="coerce")}).dropna()
counties = gpd.read_file("data/cb_2023_us_county_20m.zip").query("STUSPS not in ['AK', 'HI', 'PR']").to_crs(5070)
flows = flows[flows.dest.isin(counties.GEOID)]
CENTROID = {g: (p.x, p.y) for g, p in zip(counties.GEOID, counties.representative_point())}
ORIGIN = CENTROID["06037"]
print(f"{len(flows):,} flows out of Los Angeles County")
1,192 flows out of Los Angeles County

Example 1: the shortest heavy flows

print(top.nsmallest(3, "km")[["dest", "km", "movers"]].to_string(index=False, formatters={"km": "{:.0f}".format, "movers": "{:,.0f}".format}))
print(f"these three: {top.nsmallest(3, 'km').movers.sum() / top.movers.sum():.0%} of the 40 flows' movers, "
      f"{top.nsmallest(3, 'km').ink.sum() / top.ink.sum():.0%} of the ink")
 dest  km movers
06059  82 38,761
06111  89 10,187
06029 123 10,503
these three: 26% of the 40 flows' movers, 2% of the ink

Radial, network and other flow maps

There are several kinds of flow map, and they suit different data. A radial or "spider" map shows flows from one origin, like the Los Angeles map here. A network flow map shows flows among many places, and needs filtering or aggregation to stay readable. Desire lines are straight lines between zone centres, used in transport planning. Arc maps raise flows above a 3D map in the browser. All of them share the same grammar: two endpoints fixed by geography, and a width (sometimes a colour) for the data.

Kinds of flow map and what each is for. kinds of flow mapshapeused forradialone origin, many lineswhere one place sends peoplenetworkmany to manya system: filter or aggregatedesire linesstraight, zone to zonetransport demandarc map3D arcs in a browserinteractive explorationAll share one grammar: geography fixes the ends, width carries the data spatialworkflow.io
Four kinds of flow map, one grammar.

The length bias is the price of that grammar, and it is large. On the Los Angeles map, the flows over 1,000 km are a small share of the movers but a large share of the ink. The three shortest flows, to neighbouring counties, carry a big share of the movers in very little ink. A flow map is good at showing where people go and poor at showing how many. Pair it with something that shows how many, and the reader gets both.

Minimum widths, opacity and direction

Habits that reduce the length bias of flow maps. countering the length biasa legend with real widthse.g. 1,000 · 10,000 · 38,761name the biggest flowsoften the shortesta companion chart or choroplethlength plays no part thereopacity by volumelong weak flows recedejudge by ink73% of ink, 14% of moversLos Angeles County out-migration, ACS 2016–2020 spatialworkflow.io
Habits that keep a flow map honest.
  • Minimum widths make tiny flows visible but add ink; a map of many tiny flows inherits the length bias at full strength.
  • Opacity by volume helps long weak flows recede.
  • Proportional symbols at the destinations (circles sized by movers) show volume without length.
  • Direction needs its own encoding (arrows, curve side, two-colour ends); width alone does not say which way.
  • Uncertainty: survey flows have margins of error; widths should not imply precision the data lacks.

FAQ

What is a flow map?

A map that shows movement between places as lines, usually with line width proportional to the amount moving.

What are the types of flow map?

Radial (one origin to many destinations), network (many to many), desire lines (straight lines between zone centres for transport planning) and interactive arc maps.

Why can flow maps be misleading?

Long lines draw more ink than short ones, so distant flows look more important than nearby flows of the same or larger size.

How do I make a flow map less misleading?

Give a width legend, name the largest flows in the caption, and show the same numbers in a chart or choropleth where length plays no part.