HAHS.
Back to Catalog

Flow Map

chart

Also known as: migration map, movement map, origin-destination map

Show flowShow geographyShow relationship GeographicNumericalCategorical Map

Description

A flow map overlays directional links on a geographic base map to show the movement of people, goods, information, or other quantities between locations. Each flow is drawn as a line or curved arrow connecting an origin to a destination, with the line’s width proportional to the magnitude of the flow. The result is a visualization that simultaneously communicates the geographic context, the direction of movement, and the relative volume of each transfer.

The earliest and most famous flow map is Charles Joseph Minard’s 1869 depiction of Napoleon’s Russian campaign, widely regarded as one of the greatest statistical graphics ever produced. Modern flow maps inherit this tradition but add interactivity, animation, and data-driven layout to handle the complexity of real-world flow datasets. Edge bundling and curve optimization algorithms help reduce visual clutter when many flows cross or overlap.

Flow maps face an inherent tension between geographic accuracy and visual clarity. Straight lines between distant points often cross irrelevant territory, while bundled or curved flows may misrepresent the actual paths of movement. Careful design — using opacity, animation, hover filtering, and hierarchical aggregation — is needed to balance accuracy with readability when flow counts exceed a few dozen.

Flow Map — interactive example

When to Use

  • Showing migration patterns between regions, cities, or countries
  • Visualizing trade flows, shipping routes, or supply chain movements
  • Displaying commuting patterns or transportation volumes
  • When both the geographic context and the magnitude of movement matter
  • Revealing dominant corridors and surprising connections in origin-destination data

When NOT to Use

  • When geographic context is unnecessary — use a Sankey diagram or chord diagram for abstract flow
  • When there are too many origin-destination pairs (>100) and lines become an unreadable tangle — filter or aggregate first
  • When precise flow values matter more than spatial patterns — use a bar chart or table
  • When flows are within a single region and direction is not important — use a choropleth with an inset bar chart

Anatomy

  • Base map: Geographic reference showing boundaries, coastlines, or terrain.
  • Flow lines/arrows: Lines connecting origin to destination, with width proportional to flow magnitude. May be straight, curved (great-circle), or bundled.
  • Arrow heads or animation: Indicate the direction of movement. Animated particles flowing along paths are an effective alternative to static arrows.
  • Width encoding: Line width scaled to the quantity being transferred.
  • Color encoding: May indicate flow category (e.g., type of goods) or direction (inbound vs. outbound).
  • Origin/destination markers: Dots or symbols at endpoints, sometimes sized by total inflow or outflow.

Variations

  • Minard-style flow map: A classic hand-drawn style where the line narrows as volume decreases along the path, as in Napoleon’s march visualization.
  • Edge-bundled flow map: Flows are routed through shared corridors to reduce clutter, using force-directed edge bundling or kernel density estimation on paths.
  • Animated flow map: Particles or pulses travel along paths from origin to destination, showing directionality and bringing the map to life.
  • OD (origin-destination) matrix + map: Combines a small-multiples matrix of flows with a geographic map, allowing both tabular precision and spatial context.
  • Desire lines: Straight lines from origin to destination, unmodified by routing or bundling, showing raw demand between locations.

Code Reference

// D3 flow map with curved great-circle paths
import * as d3 from "d3";

const projection = d3.geoMercator().fitSize([width, height], geoJson);
const path = d3.geoPath(projection);

const svg = d3.select("#chart").append("svg")
  .attr("viewBox", [0, 0, width, height]);

// Draw base map
svg.append("path").datum(geoJson).attr("d", path)
  .attr("fill", "#f0f0f0").attr("stroke", "#ccc");

// Draw flows as great-circle arcs
svg.selectAll(".flow").data(flows).join("path")
  .attr("d", d => {
    const source = projection([d.sourceLon, d.sourceLat]);
    const target = projection([d.targetLon, d.targetLat]);
    const dx = target[0] - source[0], dy = target[1] - source[1];
    const dr = Math.sqrt(dx * dx + dy * dy) * 1.5;
    return `M${source}A${dr},${dr} 0 0,1 ${target}`;
  })
  .attr("fill", "none")
  .attr("stroke", "#6366f1")
  .attr("stroke-width", d => Math.sqrt(d.value))
  .attr("opacity", 0.6);