Skip to content

Instantly share code, notes, and snippets.

@thomaswilburn
Created June 2, 2026 14:58
Show Gist options
  • Select an option

  • Save thomaswilburn/c386897e679ab284022f0ecbeab4c5d2 to your computer and use it in GitHub Desktop.

Select an option

Save thomaswilburn/c386897e679ab284022f0ecbeab4c5d2 to your computer and use it in GitHub Desktop.
Animated dots over time on a Leaflet map
var { isMobile } = require("./lib/breakpoints");
var { Sidechain } = require("@nprapps/sidechain");
Sidechain.registerGuest();
var $ = require("./lib/qsa");
var L = require("leaflet");
var colors = require("./lib/helpers/colors");
const TICK_RATE = 1200;
const SCHOOLS = window.DATA.filter(r => r.latlng);
console.clear();
var mapElement = $.one(".map-container");
mapElement.focus = () => HTMLElement.prototype.focus.call(mapElement, { preventScroll: true });
var map = new L.Map(mapElement, {
zoomSnap: .3,
scrollWheelZoom: false
});
L.tileLayer("https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}.png", {
subdomains: "abcd".split("")
}).addTo(map);
var markerLookup = {};
var markerBounds = L.latLngBounds();
var labels = {
year: $.one("#year-text"),
open: $.one("#open-count"),
closed: $.one("#close-count")
};
// called on every tick
function updateMarkers(year) {
// counters
var all_facilities = SCHOOLS.length;
var unopened_facilities = 0;
var closed_facilities = 0;
for (var school of SCHOOLS) {
var { marker, year_min, year_max } = school;
// default styles
var className = "school-icon visible";
var fillOpacity = 1;
var color = colors.peach2;
var fillColor = color;
var radius = 6;
var weight = 2;
if (year < year_min) {
// hide schools that aren't open yet via CSS
className = "school-icon invisible";
// count not-yet-open facilities to subtract from total
unopened_facilities += 1
} else if (year > year_max) {
// handle schools that are closed; color is set in index.html
fillOpacity = .8;
weight = 1;
radius = 4;
color = colors.purple1;
fillColor = "url(#crosshatch)";
// count facilities that have shut down
closed_facilities += 1
}
marker.setStyle({ fillOpacity, fillColor, color, className, weight })
marker.setRadius(radius);
}
labels.year.innerHTML = year;
labels.open.innerHTML = all_facilities - unopened_facilities - closed_facilities;
labels.closed.innerHTML = closed_facilities;
}
// add markers and set up data structures
for (var school of SCHOOLS) {
var [ lat, lng ] = school.latlng.split(",").map(Number);
var details = window.DIRECTORY[school.name] || {}
let tooltipContents = `<b>${school.name}</b><br>
${school.year_min > '2015' ? `Opened in ${school.year_min}.` : 'Open prior to 2015.'}
${school.year_max < '2022' ? `Closed in ${school.year_max}.` : 'Had not closed through 2022.' }
<ul>
<li><em>Placing Agencies:</em> ${details.placing_agencies}</li>
<li><em>Genders Served:</em> ${details.gender}</li>
<li><em>Focus:</em> ${details.focus}</li>
</ul>`;
var marker = L.circleMarker([lat, lng], {
className: "school-icon",
radius: 10
}).bindPopup(tooltipContents)
.addTo(map);
marker.on("click", function() {
tick.stop();
});
markerLookup[school.name] = marker;
marker.data = school;
markerBounds.extend([lat, lng]);
school.marker = marker;
};
var slider = $.one(".slider input");
var label = $.one(".slider label");
slider.addEventListener("input", e => {
var year = slider.value;
updateMarkers(year);
label.innerHTML = year;
tick.stop();
});
requestAnimationFrame(() => updateMarkers(slider.value));
map.fitBounds(markerBounds, { padding: [20, 20] });
window.map = map;
var zoom = map.zoomControl;
function onPopupStatus(e) {
if (e.type == "popupopen") {
zoom.remove();
} else {
zoom.addTo(map);
}
}
"popupopen popupclose".split(" ").forEach(e => map.on(e, onPopupStatus));
// the clock provides tick events at regular intervals, and can be paused on interaction
class Clock extends EventTarget {
interval = null;
running = false;
constructor(callback) {
super();
this.callback = callback;
}
start(time) {
this.running = true;
this.interval = window.setInterval(this.callback, time);
this.dispatchEvent(new CustomEvent("clockchange"));
}
stop() {
this.running = false;
clearInterval(this.interval);
this.interval = null;
this.dispatchEvent(new CustomEvent("clockchange"));
}
}
var hold = false;
function onTick() {
var { value, min, max } = slider;
value++;
if (value > max) {
// pause for a second on the last item
if (hold) {
hold = false;
value = min;
} else {
hold = true;
value = max;
}
}
slider.value = value;
updateMarkers(value);
label.innerHTML = value;
}
var playButton = $.one(".animation-toggle");
var tick = new Clock(onTick);
tick.addEventListener("clockchange", function(e) {
playButton.innerHTML = tick.running ? "Pause" : "Play";
});
tick.start(TICK_RATE);
playButton.addEventListener("click", function() {
if (tick.interval) {
tick.stop();
} else {
onTick();
tick.start(TICK_RATE);
}
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment