Flow Brush Map API - Interactive Migration Visualization
This notebook demonstrates the new flow_brushmap() API in pytidycensus, which provides a simple, high-level interface for creating interactive migration flow visualizations.
The flow_brushmap() function handles all the complexity of:
- Processing migration flow data 
- Converting to GeoArrow format 
- Creating arc, source, and target layers 
- Configuring the BrushingExtension 
- Building an interactive lonboard map 
All you need is data from get_flows() with geometry=True!
Installation
To use the mapping functions, install pytidycensus with the map extra:
pip install pytidycensus[map]
This installs:
- lonboard >= 0.12.1 
- pyarrow >= 19.0.0 
- folium >= 0.20.0 
- contextily >= 1.6.2 
- branca >= 0.8.2 
Imports
import pytidycensus as tc
from pytidycensus.mapping import flow_brushmap, quick_flow_map
Basic Usage
The simplest way to create a flow brush map is to use quick_flow_map(), which fetches data and creates the map in one call:
# Quick one-liner: fetch data and create map
map_ = quick_flow_map(
    state="PA",           
    year=2018,
    flow_threshold=50,    # Show flows >= 50 people
    brushing_radius=50000 # 50km brushing radius
)
map_
Fetching 2018 county-level migration flows for PA...
/home/mmann1123/Documents/github/pytidycensus/pytidycensus/flows.py:654: UserWarning: Could not find centroids for 8 GEOIDs: ['09009', '09003', '09005', '09001', '09013'].... These flows will not have geometry data.
  warnings.warn(
Processing 13778 flow records...
Created 2076 arcs, 2503 sources, and 67 targets
Standard Usage: Two-Step Approach
For more control, fetch the data first, then create the map:
# Step 1: Fetch migration flow data
tx_flows = tc.get_flows(
    geography="county",
    state="TX",
    year=2018,
    geometry=True,      # Required for mapping!
    output="wide"
)
print(f"Retrieved {len(tx_flows)} flow records")
tx_flows.head()
Retrieved 36641 flow records
/home/mmann1123/Documents/github/pytidycensus/pytidycensus/flows.py:654: UserWarning: Could not find centroids for 97 GEOIDs: ['0900108980', '0900715350', '0900308490', '0900761800', '0900387000'].... These flows will not have geometry data.
  warnings.warn(
| GEOID1 | GEOID2 | FULL1_NAME | FULL2_NAME | MOVEDIN | MOVEDIN_M | MOVEDOUT | MOVEDOUT_M | MOVEDNET | MOVEDNET_M | centroid1 | centroid2 | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 48001 | None | Anderson County, Texas | Africa | 38 | 52.0 | NaN | NaN | NaN | NaN | POINT (-95.65236 31.81326) | NaN | 
| 1 | 48001 | None | Anderson County, Texas | Asia | 4 | 6.0 | NaN | NaN | NaN | NaN | POINT (-95.65236 31.81326) | NaN | 
| 2 | 48001 | None | Anderson County, Texas | Central America | 2 | 3.0 | NaN | NaN | NaN | NaN | POINT (-95.65236 31.81326) | NaN | 
| 3 | 48001 | 01089 | Anderson County, Texas | Madison County, Alabama | 13 | 20.0 | 0.0 | 28.0 | 13.0 | 20.0 | POINT (-95.65236 31.81326) | POINT (-86.55022579567611 34.762959383197796) | 
| 4 | 48001 | 02016 | Anderson County, Texas | Aleutians West Census Area, Alaska | 0 | 31.0 | 7.0 | 9.0 | -7.0 | 9.0 | POINT (-95.65236 31.81326) | POINT (-173.77316055538125 52.983281670565866) | 
# Step 2: Create interactive brush map
tx_map = flow_brushmap(
    tx_flows,
    flow_threshold=100,      # Only show flows >= 100 people
    brushing_radius=100000   # 100km brush radius
)
tx_map
Processing 35684 flow records...
Created 2287 arcs, 3049 sources, and 253 targets
Customization Options
The flow_brushmap() function provides many customization options:
# Custom colors, sizes, and interaction settings
custom_map = flow_brushmap(
    tx_flows,
    flow_threshold=200,
    brushing_radius=150000,      # 150km radius
    source_color=(255, 100, 100), # Light red for outflow
    target_color=(100, 100, 255), # Light blue for inflow
    arc_opacity=0.6,              # More visible arcs
    arc_width=2,                  # Thicker arcs
    point_radius_scale=5000,      # Larger points
    picking_radius=15             # Easier to click
)
custom_map
Processing 35684 flow records...
Created 716 arcs, 1037 sources, and 253 targets
Accessing Individual Layers
For advanced use cases, you can access the individual layers:
# Get map and layers separately
map_with_layers, layers = flow_brushmap(
    tx_flows,
    flow_threshold=100,
    return_layers=True
)
print("Available layers:")
print(f"  Source layer: {type(layers['source'])}")
print(f"  Target layer: {type(layers['target'])}")
print(f"  Arc layer: {type(layers['arc'])}")
# You can now modify layers or create custom combinations
map_with_layers
Processing 35684 flow records...
Created 2287 arcs, 3049 sources, and 253 targets
Available layers:
  Source layer: <class 'lonboard._layer.ScatterplotLayer'>
  Target layer: <class 'lonboard._layer.ScatterplotLayer'>
  Arc layer: <class 'lonboard.experimental._layer.ArcLayer'>
Different Geographic Levels
The API works with any geography supported by get_flows():
Metropolitan Statistical Areas
# County-level flows (requires year >= 2013)
county_map = quick_flow_map(
    geography="county",
    state="CA",               # California
    year=2018,
    flow_threshold=1000,      # Higher threshold for larger areas
    brushing_radius=200000    # 200km for larger geographic scale
)
# Note: This will show ALL county flows in the CA, which may be slow
# Consider filtering the data first
county_map
Fetching 2018 county-level migration flows for CA...
/home/mmann1123/Documents/github/pytidycensus/pytidycensus/flows.py:654: UserWarning: Could not find centroids for 115 GEOIDs: ['0901115910', '0900163480', '0900308490', '0900761800', '0900387000'].... These flows will not have geometry data.
  warnings.warn(
Processing 21910 flow records...
Created 66 arcs, 113 sources, and 58 targets
Filtering Data Before Mapping
For large datasets, filter the data to improve performance:
# Get California flows
ca_flows = tc.get_flows(
    geography="county",
    state="CA",
    year=2018,
    geometry=True,
    output="wide"
)
# Filter to only CA-to-CA flows (exclude international and other states)
ca_to_ca = ca_flows[
    ca_flows['GEOID2'].notna() &
    ca_flows['GEOID2'].str.startswith('06', na=False)  # CA FIPS code
]
print(f"Filtered from {len(ca_flows)} to {len(ca_to_ca)} CA-to-CA flows")
# Create map with filtered data
ca_map = flow_brushmap(
    ca_to_ca,
    flow_threshold=100,
    brushing_radius=100000
)
ca_map
Filtered from 22657 to 2604 CA-to-CA flows
Processing 2604 flow records...
Created 428 arcs, 856 sources, and 58 targets
/home/mmann1123/Documents/github/pytidycensus/pytidycensus/flows.py:654: UserWarning: Could not find centroids for 115 GEOIDs: ['0901115910', '0900163480', '0900308490', '0900761800', '0900387000'].... These flows will not have geometry data.
  warnings.warn(
Interpretation Guide
Colors
- Red: Outward migration (people leaving) 
- Blue: Inward migration (people arriving) 
Layers
- Arcs: Lines showing migration flows - Color gradient from source (red) to target (blue) 
- Only visible near cursor (within brushing_radius) 
 
- Source Points: Small filled circles - Origin counties for migrations 
- Color indicates direction relative to that flow 
 
- Target Rings: Hollow circles - Destination counties 
- Ring color shows net migration (red=loss, blue=gain) 
- Ring size indicates magnitude of net migration 
 
Performance Tips
- Start with a higher flow_threshold to reduce number of arcs 
- Filter to specific regions before mapping (e.g., single state) 
- Use smaller brushing_radius for dense urban areas 
- For large datasets, consider aggregating to larger geographies (county → MSA)