diff --git a/client_src/js/Helpers/SVGWriter.mjs b/client_src/js/Helpers/SVGWriter.mjs
new file mode 100644
index 0000000..83fba5b
--- /dev/null
+++ b/client_src/js/Helpers/SVGWriter.mjs
@@ -0,0 +1,197 @@
+"use strict";
+
+import XMLWriter from 'xml-writer';
+
+import Vector2 from './Vector2.mjs';
+
+/*
+ * Simplifies the process for creating an SVG dynamically.
+ * Originally written for MusicBoxConverter, but lifted, reused, and extended for FloatingIslands.
+ * Ported from C# to Javascript for AirQualityWeb.
+ * @license MPL-2.0
+ */
+class SvgWriter {
+	constructor(viewBox) {
+		// string outputFilename, string widthspec = "100%", string heightspec = "100%", Rectangle? rawViewBox = null
+		this.unitSuffix = "";
+		
+		// ----------------------------------
+		
+		this.xml = new XMLWriter();
+	
+		this.xml.startDocument();
+		this.xml.writeDocType("svg", "-//W3C//DTD SVG 1.1//EN", "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd", null);
+		this.xml.writeComment("Generated by SVGWriter which was written by Starbeamrainbowlabs");
+		this.xml.startElement("svg", "http://www.w3.org/2000/svg");
+		this.xml.writeAttribute("version", "1.1");
+		this.xml.writeAttribute("x", "0");
+		this.xml.writeAttribute("y", "0");
+		this.xml.writeAttribute("width", widthspec);
+		this.xml.writeAttribute("height", heightspec);
+
+		if (rawViewBox != null) {
+			this.xml.writeAttribute(
+				"viewBox",
+				`${viewBox.x} ${viewBox.y} ${viewBox.width} ${viewBox.height}`
+			);
+		}
+	}
+	
+	/**
+	 * Completes the SVG image currently being generated and closes the underlying file stream.
+	 * @return {this}
+	 */
+	complete() {
+		this.xml.endElement();
+		this.xml.endDocument();
+		return this;
+	}
+	
+	toString() {
+		return this.xml.toString();
+	}
+	
+	/**
+	 * Adds a line to the image.
+	 * @param	{Vector2}	start						The start position of the line.
+	 * @param	{Vector2}	end							The end position of the line.
+	 * @param	{String}	[strokeStyle="darkgreen"]	The colour to draw the line.
+	 * @param	{Number}	[strokeWidth=3]				The width to draw the line.
+	 */
+	addLine(start, end, strokeStyle = "darkgreen", strokeWidth = 3) {
+		this.xml.startElement("line");
+		this.xml.writeAttribute("x1", `${start.X}${UnitSuffix}`);
+		this.xml.writeAttribute("y1", `${start.Y}${UnitSuffix}`);
+		this.xml.writeAttribute("x2", `${end.X}${UnitSuffix}`);
+		this.xml.writeAttribute("y2", `${end.Y}${UnitSuffix}`);
+		this.xml.writeAttribute("stroke", strokeStyle);
+		this.xml.writeAttribute("stroke-width", strokeWidth.toString());
+		this.xml.endElement();
+		return this;
+	}
+	
+	/**
+	 * Opens a new SVG group.
+	 * @param  {string} [classes=null]   The class(es) to apply to the new group.
+	 * @param  {string} [transform=null] The transform(s) to apply to the new group.
+	 * @return {this}
+	 */
+	startGroup(classes = null, transform = null) {
+		this.xml.startElement("g");
+		if(classes != null)
+			this.xml.writeAttribute("class", classes);
+		if (transform != null)
+			this.xml.writeAttribute("transform", transform);
+		return this;
+	}
+	/**
+	 * Ends the most recently created unclosed group.
+	 * @return {this}
+	 */
+	endGroup() {
+		this.xml.endElement();
+		return this;
+	}
+	/// <summary>
+	/// 
+	/// </summary>
+	/// <param name="scale"></param>
+	/**
+	 * Starts a scale transform.
+	 * @param  {number} scale	The scale to enlarge (or shrink!) the next items by.
+	 * @return {this}	
+	 */
+	startScaleTransform(scale)
+	{
+		this.xml.startElement("g");
+		this.xml.writeAttribute("transform", `scale(${scale})`);
+		return this;
+	}
+	
+	/**
+	 * Ends the most recently created scale transform.
+	 * @return {this}
+	 */
+	endTransform() {
+		this.xml.endElement();
+		return this;
+	}
+	
+	/**
+	 * Adds a hollow rectangle to the image.
+	 * @param	{Vector2}	position	The position of the rectangle.
+	 * @param	{Vector2}	size		The size of the rectangle.
+	 * @param	{string}	strokeStyle	The colour to use when drawing.
+	 * @param	{float}		strokeWidth	The line width to use when drawing.
+	 * @return	{this}
+	 */
+	addRectangle(position, size, strokeStyle = "red", strokeWidth = 3) {
+		this.xml.startElement("rect");
+		this.xml.writeAttribute("x", `${position.x}${this.unitSuffix}`);
+		this.xml.writeAttribute("y", `${position.y}${this.unitSuffix}`);
+		this.xml.writeAttribute("width", `${size.X}${this.unitSuffix}`);
+		this.xml.writeAttribute("height", `${size.Y}${this.unitSuffix}`);
+		this.xml.writeAttribute("fill", "none");
+		this.xml.writeAttribute("stroke", strokeStyle);
+		this.xml.writeAttribute("stroke-width", strokeWidth.toString());
+		this.xml.endElement();
+		return this;
+	}
+	/**
+	 * Adds a circle to the image.
+	 * @param	{Vector2}	centre				The position of the centre of the circle.
+	 * @param	{Number}	radius				The radius of the circle.
+	 * @param	{String}	[fillStyle="blue"]	The colour to fill the circle with.
+	 * @return	{this}
+	 */
+	addCircle(centre, radius, fillStyle = "blue") {
+		this.xml.startElement("circle");
+		this.xml.writeAttribute("cx", `${centre.x}${this.unitSuffix}`);
+		this.xml.writeAttribute("cy", `${centre.y}${this.unitSuffix}`);
+		this.xml.writeAttribute("r", `${radius}${this.unitSuffix}`);
+		this.xml.writeAttribute("fill", fillStyle);
+		this.xml.endElement();
+		return this;
+	}
+	
+	/**
+	 * Adds a solid n-sided polygon to the image.
+	 * @param	{string}	fillStyle	The colour to fill the polygon with.
+	 * @param	{Vector2[]}	points		The co-ordinates that make up the polygon.
+	 * @return	{this}
+	 */
+	addPolygon(fillStyle, points)
+	{
+		this.xml.startElement("polygon");
+		this.xml.writeAttribute("fill", fillStyle);
+		this.xml.writeAttribute(
+			"points",
+			points.map((point) => `${point.x}${point.y}`).join(" ")
+		);
+		this.xml.endElement();
+		return this;
+	}
+	
+	/**
+	 * Adds an isosceles / equilateral triangle to the image.
+	 * This is a shorthand method that calls AddPolygon() under-the-hood.
+	 * @param	{Vector2}	position	The position to draw the triangle at.
+	 * @param	{Number}	baseWidth	The width of the triangle's base.
+	 * @param	{Number}	height		The height of the triangle.
+	 * @param	{Boolean}	upsideDown	If set to true, then the triangle will point downwards instead of upwards.
+	 * @param	{string}	fillStyle	The colour to fill the triangle with.
+	 * @return	{this}
+	 */
+	addTriangleRegular(position, baseWidth, height, upsideDown, fillStyle)
+	{
+		this.addPolygon(
+			fillStyle,
+			position.subtract(new Vector2(baseWidth / 2, 0)),
+			position.add(new Vector2(baseWidth / 2, 0)),
+			position.subtract(new Vector2(0, upsideDown ? -height : height))
+		);
+		return this;
+	}
+}
+
+export default SvgWriter;
diff --git a/client_src/js/Helpers/Vector2.mjs b/client_src/js/Helpers/Vector2.mjs
new file mode 100644
index 0000000..b614144
--- /dev/null
+++ b/client_src/js/Helpers/Vector2.mjs
@@ -0,0 +1,316 @@
+"use strict";
+
+/******************************************************
+ ************** Simple ES6 Vector Class  **************
+ ******************************************************
+ * Author: Starbeamrainbowlabs
+ * Twitter: @SBRLabs
+ * Email: feedback at starbeamrainbowlabs dot com
+ * 
+ * From https://gist.github.com/sbrl/69a8fa588865cacef9c0
+ ******************************************************
+ * Originally written for my 2D Graphics ACW at Hull
+ * University.
+ ******************************************************
+ * Changelog
+ ******************************************************
+ * 19th December 2015:
+	* Added this changelog.
+ * 28th December 2015:
+	* Rewrite tests with klud.js + Node.js
+ * 30th January 2016:
+	* Tweak angleFrom function to make it work properly.
+ * 31st January 2016:
+	* Add the moveTowards function.
+	* Add the minComponent getter.
+	* Add the maxComponent getter.
+	* Add the equalTo function.
+	* Tests still need to be written for all of the above.
+ * 19th September 2016:
+	* Added Vector support to the multiply method.
+ * 10th June 2017:
+	* Fixed a grammatical mistake in a comment.
+	* Added Vector.fromBearing static method.
+ * 21st October 2017:
+ 	* Converted to ES6 module.
+ 	* Added Vector.zero and Vector.one constants. Remember to clone them!
+ * 4th August 2018: (#LOWREZJAM!)
+ 	* Optimised equalTo()
+ * 6th August 2018: (#LOWREZJAM again!)
+ 	* Added round(), floor(), and ceil()
+ * 7th August 2018: (moar #LOWREZJAM :D)
+ 	* Added area() and snapTo(grid_size)
+ * 10th August 2018: (even more #LOWREZJAM!)
+ 	* Added Vector support to divide()
+ */
+
+class Vector {
+	// Constructor
+	constructor(inX, inY) {
+		if(typeof inX != "number")
+			throw new Error("Invalid x value.");
+		if(typeof inY != "number")
+			throw new Error("Invalid y value.");
+		
+		// Store the (x, y) coordinates
+		this.x = inX;
+		this.y = inY;
+	}
+
+	/**
+	 * Add another vector to this vector.
+	 * @param	{Vector}	v	The vector to add.
+	 * @return	{Vector}	The current vector. useful for daisy-chaining calls.
+	 */
+	add(v) {
+		this.x += v.x;
+		this.y += v.y;
+
+		return this;
+	}
+
+	/**
+	 * Take another vector from this vector.
+	 * @param  {Vector} v The vector to subtrace from this one.
+	 * @return {Vector}   The current vector. useful for daisy-chaining calls.
+	 */
+	subtract(v) {
+		this.x -= v.x;
+		this.y -= v.y;
+
+		return this;
+	}
+
+	/**
+	 * Divide the current vector by a given value.
+	 * @param  {Number|Vector} value The number (or Vector) to divide by.
+	 * @return {Vector}	   The current vector. Useful for daisy-chaining calls.
+	 */
+	divide(value) {
+		if(value instanceof Vector)
+		{
+			this.x /= value.x;
+			this.y /= value.y;
+		}
+		else if(typeof value == "number")
+		{
+			this.x /= value;
+			this.y /= value;
+		}
+		else
+			throw new Error("Can't divide by non-number value.");
+		
+		return this;
+	}
+
+	/**
+	 * Multiply the current vector by a given value.
+	 * @param  {Number|Vector} value The number (or Vector) to multiply the current vector by.
+	 * @return {Vector}	   The current vector. useful for daisy-chaining calls.
+	 */
+	multiply(value) {
+		if(value instanceof Vector)
+		{
+			this.x *= value.x;
+			this.y *= value.y;
+		}
+		else if(typeof value == "number")
+		{
+			this.x *= value;
+			this.y *= value;
+		}
+		else
+			throw new Error("Can't multiply by non-number value.");
+		
+		return this;
+	}
+	
+	/**
+	 * Move the vector towards the given vector by the given amount.
+	 * @param  {Vector} v      The vector to move towards.
+	 * @param  {Number} amount The distance to move towards the given vector.
+	 */
+	moveTowards(v, amount)
+	{
+		// From http://stackoverflow.com/a/2625107/1460422
+		var dir = new Vector(
+			v.x - this.x,
+			v.y - this.y
+		).limitTo(amount);
+		this.x += dir.x;
+		this.y += dir.y;
+		
+		return this;
+	}
+	
+	/**
+	 * Rounds the x and y components of this vector down to the next integer.
+	 * @return	{Vector}	This vector - useful for diasy-chaining.
+	 */
+	floor() {
+		this.x = Math.floor(this.x);
+		this.y = Math.floor(this.y);
+		
+		return this;
+	}
+	/**
+	 * Rounds the x and y components of this vector up to the next integer.
+	 * @return	{Vector}	This vector - useful for diasy-chaining.
+	 */
+	ceil() {
+		this.x = Math.ceil(this.x);
+		this.y = Math.ceil(this.y);
+		
+		return this;
+	}
+	/**
+	 * Rounds the x and y components of this vector to the nearest integer.
+	 * @return	{Vector}	This vector - useful for diasy-chaining.
+	 */
+	round() {
+		this.x = Math.round(this.x);
+		this.y = Math.round(this.y);
+		
+		return this;
+	}
+	
+	/**
+	 * Calculates the 'area' of this vector and returns the result.
+	 * In other words, returns x * y. Useful if you're using a Vector to store 
+	 * a size.
+	 * @return {Number} The 'area' of this vector.
+	 */
+	area() {
+		return this.x * this.y;
+	}
+	
+	/**
+	 * Snaps this vector to an imaginary square grid with the specified sized 
+	 * squares.
+	 * @param	{Number}	grid_size	The size of the squares on the imaginary  grid to which to snap.
+	 * @return	{Vector}	The current vector - useful for daisy-chaining.
+	 */
+	snapTo(grid_size) {
+		this.x = Math.floor(this.x / grid_size) * grid_size;
+		this.y = Math.floor(this.y / grid_size) * grid_size;
+		
+		return this;
+	}
+
+	/**
+	 * Limit the length of the current vector to value without changing the
+	 * direction in which the vector is pointing.
+	 * @param  {Number} value The number to limit the current vector's length to.
+	 * @return {Vector}	   The current vector. useful for daisy-chaining calls.
+	 */
+	limitTo(value) {
+		if(typeof value != "number")
+			throw new Error("Can't limit to non-number value.");
+		
+		this.divide(this.length);
+		this.multiply(value);
+
+		return this;
+	}
+
+	/**
+	 * Return the dot product of the current vector and another vector.
+	 * @param  {Vector} v   The other vector we should calculate the dot product with.
+	 * @return {Vector}	 The current vector. Useful for daisy-chaining calls.
+	 */
+	dotProduct(v) {
+		return (this.x * v.x) + (this.y * v.y);
+	}
+
+	/**
+	 * Calculate the angle, in radians, from north to another vector.
+	 * @param  {Vector} v The other vector to which to calculate the angle.
+	 * @return {Vector}	 The current vector. useful for daisy-chaining calls.
+	 */
+	angleFrom(v) {
+		// From http://stackoverflow.com/a/16340752/1460422
+		var angle = Math.atan2(v.y - this.y, v.x - this.x) - (Math.PI / 2);
+		angle += Math.PI/2;
+		if(angle < 0) angle += Math.PI * 2;
+		return angle;
+	}
+
+	/**
+	 * Clones the current vector.
+	 * @return {Vector} A clone of the current vector. Very useful for passing around copies of a vector if you don't want the original to be altered.
+	 */
+	clone() {
+		return new Vector(this.x, this.y);
+	}
+
+	/*
+	 * Returns a representation of the current vector as a string.
+	 * @returns {string} A representation of the current vector as a string.
+	 */
+	toString() {
+		return `(${this.x}, ${this.y})`;
+	}
+	
+	/**
+	 * Whether the vector is equal to another vector.
+	 * @param  {Vector} v The vector to compare to.
+	 * @return {boolean}  Whether the current vector is equal to the given vector.
+	 */
+	equalTo(v)
+	{
+		return this.x == v.x && this.y == v.y;
+	}
+
+	/**
+	 * Get the unit vector of the current vector - that is a vector poiting in the same direction with a length of 1. Note that this does *not* alter the original vector.
+	 * @return {Vector} The current vector's unit form.
+	 */
+	get unitVector() {
+		var length = this.length;
+		return new Vector(
+		this.x / length,
+		this.y / length);
+	}
+
+	/**
+	 * Get the length of the current vector.
+	 * @return {Number} The length of the current vector.
+	 */
+	get length() {
+		return Math.sqrt((this.x * this.x) + (this.y * this.y));
+	}
+	
+	/**
+	 * Get the value of the minimum component of the vector.
+	 * @return {Number} The minimum component of the vector.
+	 */
+	get minComponent() {
+		return Math.min(this.x, this.y);
+	}
+	
+	/**
+	 * Get the value of the maximum component of the vector.
+	 * @return {Number} The maximum component of the vector.
+	 */
+	get maxComponent() {
+		return Math.min(this.x, this.y);
+	}
+}
+
+/**
+ * Returns a new vector based on an angle and a length.
+ * @param	{Number}	angle	The angle, in radians.
+ * @param	{Number}	length	The length.
+ * @return	{Vector}	A new vector that represents the (x, y) of the specified angle and length.
+ */
+Vector.fromBearing = function(angle, length) {
+	return new Vector(
+		length * Math.cos(angle),
+		length * Math.sin(angle)
+	);
+}
+
+Vector.zero = new Vector(0, 0);
+Vector.one = new Vector(1, 1);
+
+export default Vector;
diff --git a/client_src/js/Overlay/VoronoiOverlay.mjs b/client_src/js/Overlay/VoronoiOverlay.mjs
new file mode 100644
index 0000000..466bf46
--- /dev/null
+++ b/client_src/js/Overlay/VoronoiOverlay.mjs
@@ -0,0 +1,9 @@
+"use strict";
+
+class VoronoiOverlay {
+	constructor() {
+		
+	}
+}
+
+export default VoronoiOverlay;
diff --git a/package-lock.json b/package-lock.json
index f9db811..4b23cae 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,6 +1,6 @@
 {
   "name": "air-quality-mapper",
-  "version": "0.1.0",
+  "version": "0.9.1",
   "lockfileVersion": 1,
   "requires": true,
   "dependencies": {
@@ -1367,12 +1367,6 @@
         }
       }
     },
-    "heatmap.js": {
-      "version": "2.0.5",
-      "resolved": "https://registry.npmjs.org/heatmap.js/-/heatmap.js-2.0.5.tgz",
-      "integrity": "sha1-Rm07hlE/XUkRKknSVwCrJzAUkVM=",
-      "dev": true
-    },
     "hex-color-regex": {
       "version": "1.1.0",
       "resolved": "https://registry.npmjs.org/hex-color-regex/-/hex-color-regex-1.1.0.tgz",
@@ -1711,37 +1705,6 @@
       "resolved": "https://registry.npmjs.org/leaflet-fullscreen/-/leaflet-fullscreen-1.0.2.tgz",
       "integrity": "sha1-CcYcS6xF9jsu4Sav2H5c2XZQ/Bs="
     },
-    "leaflet-heatmap": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/leaflet-heatmap/-/leaflet-heatmap-1.0.0.tgz",
-      "integrity": "sha1-g8wEJJ057ZFSauqn12ab2qhtax8=",
-      "dev": true,
-      "requires": {
-        "heatmap.js": "*",
-        "leaflet": "*"
-      }
-    },
-    "leaflet-timedimension": {
-      "version": "1.1.0",
-      "resolved": "https://registry.npmjs.org/leaflet-timedimension/-/leaflet-timedimension-1.1.0.tgz",
-      "integrity": "sha1-nuy9+SZqLvFKzoKC2x0b/VsVEAY=",
-      "requires": {
-        "iso8601-js-period": "0.2.0",
-        "leaflet": "~0.7.4 || ~1.0.0"
-      },
-      "dependencies": {
-        "iso8601-js-period": {
-          "version": "0.2.0",
-          "resolved": "https://registry.npmjs.org/iso8601-js-period/-/iso8601-js-period-0.2.0.tgz",
-          "integrity": "sha1-5XQP1OcguWCoXraF/CBSAWIcAYw="
-        },
-        "leaflet": {
-          "version": "1.0.3",
-          "resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.0.3.tgz",
-          "integrity": "sha1-H0AbmLRcgZITTGyNaWhiU4BQB8g="
-        }
-      }
-    },
     "leaflet.markercluster": {
       "version": "1.4.1",
       "resolved": "https://registry.npmjs.org/leaflet.markercluster/-/leaflet.markercluster-1.4.1.tgz",
@@ -4398,6 +4361,11 @@
       "resolved": "https://registry.npmjs.org/vendors/-/vendors-1.0.2.tgz",
       "integrity": "sha512-w/hry/368nO21AN9QljsaIhb9ZiZtZARoVH5f3CsFbawdLdayCgKRPup7CggujvySMxx0I91NOyxdVENohprLQ==",
       "dev": true
+    },
+    "xml-writer": {
+      "version": "1.7.0",
+      "resolved": "https://registry.npmjs.org/xml-writer/-/xml-writer-1.7.0.tgz",
+      "integrity": "sha1-t28dWRwWomNOvbcDx729D9aBkGU="
     }
   }
 }
diff --git a/package.json b/package.json
index 5e46503..d4348d8 100644
--- a/package.json
+++ b/package.json
@@ -23,13 +23,12 @@
 		"iso8601-js-period": "^0.2.1",
 		"leaflet": "^1.4.0",
 		"leaflet-fullscreen": "^1.0.2",
-		"leaflet-timedimension": "^1.1.0",
 		"leaflet.markercluster": "^1.4.1",
-		"leaflet-heatmap": "^1.0.0",
 		"moment": "^2.24.0",
 		"nanomodal": "^5.1.1",
 		"smartsettings": "^1.2.3",
-		"tabs": "^0.2.0"
+		"tabs": "^0.2.0",
+		"xml-writer": "^1.7.0"
 	},
 	"devDependencies": {
 		"@types/chart.js": "^2.7.51",
