diff --git a/client_src/js/LayerHeatmap.mjs b/client_src/js/LayerHeatmap.mjs
index a72f17e..faf80b0 100644
--- a/client_src/js/LayerHeatmap.mjs
+++ b/client_src/js/LayerHeatmap.mjs
@@ -7,6 +7,10 @@ import GetFromUrl from './Helpers/GetFromUrl.mjs';
 
 
 class LayerHeatmap {
+	/**
+	 * Creates a new heatmap manager wrapper class fort he given map.
+	 * @param	{L.Map}	in_map	The leaflet map to attach to.
+	 */
 	constructor(in_map) {
 		this.map = in_map;
 		
@@ -77,8 +81,14 @@ class LayerHeatmap {
 				}
 			}
 		};
+		
+		this.reading_cache = new Map();
 	}
 	
+	/**
+	 * Re-creates the heatmap overlay layer.
+	 * Needed sometimes internally to work around an annoying bug.
+	 */
 	recreate_overlay() {
 		if(typeof this.heatmap != "undefined")
 			this.layer.removeLayer(this.heatmap);
@@ -86,6 +96,10 @@ class LayerHeatmap {
 		this.layer.addLayer(this.heatmap);
 	}
 	
+	/**
+	 * Sets the display data to the given array of data points.
+	 * @param {object[]} readings_list The array of data points to display.
+	 */
 	set_data(readings_list) {
 		let data_object = {
 			max: 0,
@@ -102,6 +116,13 @@ class LayerHeatmap {
 		this.heatmap.setData(data_object);
 	}
 	
+	/**
+	 * Updates the heatmap with data for the specified datetime & reading type,
+	 * fetching new data if necessary.
+	 * @param	{Date}		datetime		The datetime to display.
+	 * @param	{string}	reading_type	The reading type to display data for.
+	 * @return	{Promise}	A promise that resolves when the operation is completed.
+	 */
 	async update_data(datetime, reading_type) {
 		if(!(datetime instanceof Date))
 			throw new Exception("Error: 'datetime' must be an instance of Date.");
@@ -123,15 +144,44 @@ class LayerHeatmap {
 		}
 		
 		try {
-			this.set_data(JSON.parse(await GetFromUrl(
-				`${Config.api_root}?action=fetch-data&datetime=${encodeURIComponent(this.datetime.toISOString())}&reading_type=${encodeURIComponent(this.reading_type)}`
-			)));
+			this.set_data(await this.fetch_data(this.datetime, this.reading_type));
 		} catch(error) {
 			console.log(error);
 			alert(error);
 		}
 	}
 	
+	/**
+	 * Fetches & decodes data for the given datetime and the current reading type.
+	 * @param	{Date}		datetime		The Date to fetch data for.
+	 * @param	{string}	reading_type	The reading type code to fetch data for.
+	 * @return	{Promise}	The requested data array, as the return value of a promise
+	 */
+	async fetch_data(datetime, reading_type) {
+		let cache_key = Symbol.for(`${reading_type}|${datetime.toISOString()}`);
+		let result = this.reading_cache.get(cache_key);
+		
+		if(typeof result == "undefined") {
+			result = JSON.parse(await GetFromUrl(
+				`${Config.api_root}?action=fetch-data&datetime=${encodeURIComponent(datetime.toISOString())}&reading_type=${encodeURIComponent(reading_type)}`
+			));
+			this.reading_cache.set(cache_key, result);
+		}
+		
+		return result;
+	}
+	
+	/**
+	 * Whether the reading cache contains data for the given datetime & reading type.
+	 * @param	{Date}		datetime		The datetime to check.
+	 * @param	{string}	reading_type	The reading type code to check.
+	 * @return	{Boolean}	Whether the reading cache contains data for the requested datetime & reading type.
+	 */
+	is_data_cached(datetime, reading_type) {
+		let cache_key = Symbol.for(`${reading_type}|${datetime.toISOString()}`);
+		return this.reading_cache.has(cache_key);
+	}
+	
 	
 	async update_reading_type(reading_type) {
 		await this.update_data(
@@ -139,12 +189,6 @@ class LayerHeatmap {
 			reading_type
 		);
 	}
-	async update_datetime(datetime) {
-		await this.update_data(
-			datetime,
-			this.reading_type
-		);
-	}
 }
 
 export default LayerHeatmap;
diff --git a/client_src/js/LayerHeatmapGlue.mjs b/client_src/js/LayerHeatmapGlue.mjs
new file mode 100644
index 0000000..6171af7
--- /dev/null
+++ b/client_src/js/LayerHeatmapGlue.mjs
@@ -0,0 +1,86 @@
+"use strict";
+
+class LayerHeatmapGlue {
+	constructor(time_dimension, heatmap) {
+		this.time_dimension = time_dimension;
+		
+		this.heatmap = heatmap;
+		
+		this.glue_layer_const = L.TimeDimension.Layer.extend({
+			isReady: this.isReady,
+		});
+		
+		this.cache = {};
+	}
+	
+	attachTo(map) {
+		this.layer_glue = new this.glue_layer_const({
+			timeDimension: this.time_dimension
+		}).addTo(map);
+		this.time_dimension.on("timeload", this.update.bind(this));
+		this.time_dimension.on("timeloading", this.onNewTime.bind(this));
+		// this.time_dimension.registerSyncedLayer(this.layer_glue);
+	}
+	
+	async onNewTime(event) {
+		console.log("on-new-time", arguments);
+		// event.time here is a number - TODO: Figure out how to keep it as a date
+		
+		if(typeof this.cache[event.time] == "undefined") {
+			await this.loadTime(new Date(event.time));
+		}
+		
+		this.fireLoadComplete(event.time);
+	}
+	
+	/**
+	 * Loads data into the cache for the specified datetime.
+	 * @param  {Date} time The datetime to load data in for.
+	 * @return {[type]}      [description]
+	 */
+	async loadTime(time) {
+		await this.heatmap.fetch_data(
+			time,
+			this.heatmap.reading_type
+		);
+	}
+	
+	/**
+	 * Fires the loadComplete event to leaflet-timedimension know we're done loading.
+	 * @param	{Number}	time	The timestamp to fire the timeload event for.
+	 */
+	fireLoadComplete(time) {
+		this.layer_glue.fire("timeload", {
+			time: time
+		});
+	}
+	
+	/**
+	 * Tells leaflet-timedimension if we're ready for a given time.
+	 * @param	{object}	event	The event object
+	 * @return	{Boolean}	Whether we're ready or not
+	 */
+	isReady(event) {
+		// BUG: This will go awry if the user changes the reading type on the fly
+		// console.log("is-ready", arguments);
+		return this.heatmap.is_data_cached(
+			new Date(event.time),
+			this.heatmap.reading_type
+		);
+	}
+	
+	/**
+	 * Passes an update to the heatmap manager
+	 * @param  {[type]} event [description]
+	 * @return {[type]}       [description]
+	 */
+	async update(event) {
+		//console.log("update", arguments);
+		await this.heatmap.update_data(
+			new Date(event.time),
+			this.heatmap.reading_type
+		);
+	}
+}
+
+export default LayerHeatmapGlue;
diff --git a/client_src/js/MapManager.mjs b/client_src/js/MapManager.mjs
index 580bb53..0c08265 100644
--- a/client_src/js/MapManager.mjs
+++ b/client_src/js/MapManager.mjs
@@ -9,6 +9,7 @@ import '../../node_modules/leaflet-timedimension/dist/leaflet.timedimension.src.
 import Config from './Config.mjs';
 import LayerDeviceMarkers from './LayerDeviceMarkers.mjs';
 import LayerHeatmap from './LayerHeatmap.mjs';
+import LayerHeatmapGlue from './LayerHeatmapGlue.mjs';
 import UI from './UI.mjs';
 
 class MapManager {
@@ -39,10 +40,6 @@ class MapManager {
 		this.map.attributionControl.addAttribution("Data: <a href='https://connectedhumber.org/'>Connected Humber</a>");
 		this.map.attributionControl.addAttribution("<a href='https://github.com/ConnectedHumber/Air-Quality-Web/'>Air Quality Web</a> by <a href='https://starbeamrainbowlabs.com/'>Starbeamrainbowlabs</a>");
 		
-		// Add the time dimension
-		this.layer_time = L.timeDimension()
-			.addTo(this.map);
-		this.layer_time.on("timeloading", console.log);
 		
 		// Add the device markers
 		console.info("[map] Loading device markers....");
@@ -55,14 +52,51 @@ class MapManager {
 		
 		// Add the heatmap
 		console.info("[map] Loading heatmap....");
-		this.setup_heatmap().then(() => {
-			console.info("[map] Heatmap loaded successfully.");
-		});
+		this.setup_heatmap()
+			.then(() => console.info("[map] Heatmap loaded successfully."))
+			// ...and the time dimension
+			.then(this.setup_time_dimension.bind(this))
+			.then(() => console.info("[map] Time dimension initialised."));
 		
 		this.ui = new UI(Config, this);
 		this.ui.setup().then(() => console.log("[map] Settings initialised."));
 	}
 	
+	setup_time_dimension() {
+		this.layer_time = new L.TimeDimension({
+			period: "PT1H", // 1 hour
+			timeInterval: `2019-01-01T12:00:00Z/${new Date().toISOString()}`
+		});
+		//this.layer_time.on("timeloading", console.log.bind(null, "timeloading"));
+		
+		this.layer_time_player = new L.TimeDimension.Player({
+			transitionTime: 100,
+			loop: false,
+			startOver: true,
+			buffer: 10 // Default: 5
+		}, this.layer_time);
+		
+		this.layer_time_control = new L.Control.TimeDimension({
+			player: this.layer_time_player,
+			timeDimension: this.layer_time,
+			position: "bottomright",
+			autoplay: false,
+			minSpeed: 1, 
+			speedStep: 0.25,
+			maxSpeed: 15,
+			timeSliderDragUpdate: true
+		});
+		
+		this.map.addControl(this.layer_time_control);
+		
+		// Create the time dimension <---> heatmap glue object
+		this.layer_heatmap_glue = new LayerHeatmapGlue(
+			this.layer_time,
+			this.heatmap
+		);
+		this.layer_heatmap_glue.attachTo(this.map);
+	}
+	
 	async setup_device_markers() {
 		this.device_markers = new LayerDeviceMarkers(this.map);
 		await this.device_markers.setup();
