diff --git a/layouts/locations/list.html b/layouts/locations/list.html
index cd93bb3..7bb3625 100644
--- a/layouts/locations/list.html
+++ b/layouts/locations/list.html
@@ -21,46 +21,7 @@
{{ end }}
diff --git a/layouts/partials/cv/locations/heatmap.js b/layouts/partials/cv/locations/heatmap.js
new file mode 100644
index 0000000..1465b5a
--- /dev/null
+++ b/layouts/partials/cv/locations/heatmap.js
@@ -0,0 +1,47 @@
+/*
+ Recursively render an ordered or unordered list from a set of arbitrarily-
+ deeply-nested items.
+
+ @params all_pages All site pages
+*/
+
+var map = L.map('map', {
+ fullscreenControl: true
+}).setView([55, -3], 3);
+
+L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
+ maxZoom: 19,
+ attribution: '© OpenStreetMap'
+}).addTo(map);
+
+{{- $locations := partialCached "util/get_location_leaf_nodes.html" $.Site.Data.locations -}}
+
+{{/*
+{{- range .Site.AllPages -}}
+ {{- with .Params.locations -}}
+ {{- range . -}}
+ {{- with ( index $locations ( replaceRE `, [A-Z]+` "" . ) ) -}}
+ L.marker([{{ .lat }}, {{ .lon }}]).addTo(map);
+ {{- end -}}
+ {{- end -}}
+ {{- end -}}
+{{- end -}}
+*/}}
+
+L.heatLayer(
+ [
+ {{- range .all_pages -}}
+ {{- with .Params.locations -}}
+ {{- range . -}}
+ {{- with ( index $locations ( replaceRE `, [A-Z]+` "" . ) ) -}}
+ [ {{ .lat }}, {{ .lon }} ],
+ {{- end -}}
+ {{- end -}}
+ {{- end -}}
+ {{- end -}}
+ ],
+ {
+ minOpacity: 0.5,
+ gradient: {0.1: 'blue', 0.3: 'lime', 0.8: 'orange', 1: 'red'}
+ }
+).addTo(map);