If you have coordinates sitting in a spreadsheet and you want them on a map, the standard advice is "just use QGIS" or "go learn geopandas". Both are fine answers — but if all you need is one figure for a report, that's a lot of yak-shaving for one image.
I ended up writing five small scripts, each self-contained, each doing one job. Here's what each is actually for.
The five
1. Choropleth — shaded regions from your data
The classic. You have a value per region (population, GDP, case counts) and a boundary file. The script joins your CSV to the GeoJSON on a key, then shades each region by value with a colour scale and a legend that doesn't look broken.
2. POI distribution — points on a coordinate grid
Just "where are my things". Good for store locations, sampling sites, sensors. Plots your lat/lon with a light grid so you can eyeball clustering.
3. Density heatmap — hexbin aggregation
When you have too many points and a scatter plot becomes a solid blob. Hexbin aggregates into hexagonal bins and colours by count — it shows clusters that individual points hide.
4. Bubble map — dot size encodes a value
Same as POI, but each point carries a magnitude (revenue, volume, users). Size encodes the value, so one glance shows both where and how much.
5. Flow map — curved origin→destination lines
For movement data: commutes, migration, shipments. Each row is an origin→destination pair with a volume, drawn as a curved line whose width is the volume. Curves (not straight lines) make overlapping routes readable.
Why matplotlib maps are annoying
The plotting itself is easy. The annoying parts are the ones nobody mentions:
- Coordinate systems. Your data might be WGS-84 lat/lon; your boundary file might not. If they disagree your map is subtly wrong, and you won't notice until someone else does.
- Colour scales. A linear scale on skewed data makes everything the same colour. Getting quantile vs linear right changes whether the map says anything at all.
- Legends and labels. Making a colourbar, a title and region labels not collide is genuinely fiddly — and it's about 80% of the "why does my map look amateur" problem.
- Boundary files. You need a GeoJSON. If you don't have one, half the tutorial is over before it starts.
Each script handles those with a CONFIGURATION block at the top: edit the paths and column names, run it, and the figure lands in your output folder.
The setup
pip install pandas numpy matplotlib geopandas
Python 3.9+. Works on Windows, macOS and Linux. No ArcGIS, no QGIS, no account, no API keys.
Every script ships with working sample data so you can run it before pointing it at your own CSV. There's a boundary GeoJSON example too, plus a troubleshooting section for the usual projection mismatches.
Choosing the right map
This is the part that actually matters, and it's not a code question:
| What your data looks like | Which map |
|---|---|
| A value per region | Choropleth |
| Points, want to see clustering | POI distribution |
| Too many points to see anything | Density heatmap |
| Points with a magnitude | Bubble map |
| Origin to destination pairs | Flow map |
Getting that mapping right is worth more than any individual script.
If you'd rather not write them
Everything above is what I built for my own work, and I packaged the five scripts up with the sample data and notes. If it saves you an afternoon of fighting matplotlib projections, that's the whole point: https://mochiway.com/templates.html
Either way — the concepts are the useful part. Happy to answer questions about projections, geopandas joins, or hexbin parameters.
Top comments (0)