diff --git a/README.md b/README.md
index 8517f75..962c1b3 100644
--- a/README.md
+++ b/README.md
@@ -93,6 +93,20 @@ _No parameters are currently supported by this action._
 https://example.com/path/to/api.php?action=list-reading-types
 ```
 
+
+### device-data-bounds
+> Gets the start and end DateTime bounds for the data recorded for a specific device.
+
+Parameter			| Type		| Meaning
+--------------------|-----------|---------------------
+`device-id`         | int		| Required. The id of the device to get the data DateTime bounds for.
+
+```
+https://example.com/path/to/api.php?action=device-data-bounds&device-id=18
+https://example.com/path/to/api.php?action=device-data-bounds&device-id=11
+```
+
+
 ## Notes
  - Readings are taken every 6 minutes as standard.
 
diff --git a/client_src/js/DeviceReadingDisplay.mjs b/client_src/js/DeviceReadingDisplay.mjs
new file mode 100644
index 0000000..510da0c
--- /dev/null
+++ b/client_src/js/DeviceReadingDisplay.mjs
@@ -0,0 +1,42 @@
+"use strict";
+
+// import CreateElement from 'dom-create-element-query-selector';
+// We're using the git repo for now until an update is released, and rollup doesn't like that apparently
+import CreateElement from '../../node_modules/dom-create-element-query-selector/src/index.js';
+
+import Chart from 'chart.js';
+
+import GetFromUrl from './Helpers/GetFromUrl.mjs';
+
+
+class DeviceReadingDisplay {
+	get device_id() { return this._device_id; }
+	set device_id(value) {
+		this._device_id = value;
+		this.update_display();
+	}
+	
+	constructor(in_config, in_device_id) {
+		this.setup_display();
+		
+		this.config = in_config;
+		this._device_id = in_device_id; // We don't want to update until we have everything we need
+	}
+	
+	setup_display() {
+		this.display = CreateElement("div.chart-device-data",
+			CreateElement("canvas.canvas-chart"),
+			CreateElement("ul.reading-types")
+		);
+	}
+	set_reading_types(new_reading_types, starting_index) {
+		this.reading_types = new_reading_types;
+		this.selected_reading_type = this.reading_types[starting_index];
+	}
+	
+	async update_display() {
+		let new_data = JSON.parse(await GetFromUrl(`${this.config.api_root}?action=`))
+	}
+}
+
+export default DeviceReadingDisplay;
diff --git a/logic/Actions/DeviceDataBounds.php b/logic/Actions/DeviceDataBounds.php
new file mode 100644
index 0000000..db8135d
--- /dev/null
+++ b/logic/Actions/DeviceDataBounds.php
@@ -0,0 +1,72 @@
+<?php
+
+namespace AirQuality\Actions;
+
+use \SBRL\TomlConfig;
+use \AirQuality\Repositories\IMeasurementDataRepository;
+use \AirQuality\ApiResponseSender;
+
+use \AirQuality\Validator;
+use \AirQuality\PerfFormatter;
+
+class DeviceDataBounds implements IAction {
+	/** @var TomlConfig */
+	private $settings;
+	/** @var IMeasurementDataRepository */
+	private $measurement_repo;
+	
+	/** @var ApiResponseSender */
+	private $sender;
+	
+	/** @var Validator */
+	private $validator;
+	
+	public function __construct(
+		TomlConfig $in_settings,
+		IMeasurementDataRepository $in_measurement_repo,
+		ApiResponseSender $in_sender) {
+		$this->settings = $in_settings;
+		$this->measurement_repo = $in_measurement_repo;
+		$this->sender = $in_sender;
+		
+		$this->validator = new Validator($_GET);
+	}
+	
+	public function handle() : bool {
+		global $start_time;
+		
+		$start_handle = microtime(true);
+		
+		// 1: Validate params
+		$this->validator->is_numberish("device-id");
+		$this->validator->run();
+		
+		
+		// 2: Pull data from database
+		$data = $this->measurement_repo->get_device_reading_bounds(
+			intval($_GET["device-id"])
+		);
+		
+		// 2.5: Validate data from database
+		if(empty($data)) {
+			http_response_code(404);
+			header("content-type: text/plain");
+			header("x-time-taken: " . PerfFormatter::format_perf_data($start_time, $start_handle, null));
+			echo("Error: No data has been recorded from the device id or it doesn't exist.");
+			return false;
+		}
+		
+		
+		// 3: Serialise data
+		$start_encode = microtime(true);
+		$response = json_encode($data);
+		
+		
+		// 4: Send response
+		header("content-length: " . strlen($response));
+		header("content-type: application/json");
+		header("x-time-taken: " . PerfFormatter::format_perf_data($start_time, $start_handle, $start_encode));
+		echo($response);
+		return true;
+	}
+}
diff --git a/logic/Database.php b/logic/Database.php
index 48d1f0f..0d3a00a 100644
--- a/logic/Database.php
+++ b/logic/Database.php
@@ -42,6 +42,7 @@ class Database
 	}
 	
 	public function query($sql, $variables = []) {
+		error_log("[Database/SQL] $sql");
 		// FUTURE: Optionally cache prepared statements?
 		$statement = $this->connection->prepare($sql);
 		$statement->execute($variables);
diff --git a/logic/Repositories/IMeasurementDataRepository.php b/logic/Repositories/IMeasurementDataRepository.php
index 2670bf4..0724dc9 100644
--- a/logic/Repositories/IMeasurementDataRepository.php
+++ b/logic/Repositories/IMeasurementDataRepository.php
@@ -3,5 +3,29 @@
 namespace AirQuality\Repositories;
 
 interface IMeasurementDataRepository {
-    public function get_readings_by_date(\DateTime $datetime, string $reading_type);
+	/**
+	 * Returns the specified reading type for all devices at the specified date 
+	 * and time.
+	 * @param	DateTime	$datetime		The date and time to get the readings for.
+	 * @param	string		$reading_type	The reading type to fetch.
+	 * @return	array		The requested readings.
+	 */
+	public function get_readings_by_date(\DateTime $datetime, string $reading_type);
+	
+	/**
+	 * Gets the first and last DateTimes of the readings for a specified device.
+	 * @param	int			$device_id	The device id to fetch for.
+	 * @return	DateTime[]	The first and last DateTimes for which readings are stored.
+	 */
+	public function get_device_reading_bounds(int $device_id);
+	/**
+	 * Gets the readings of a specified type for a specific device between the 2 given dates and times.
+	 * @param	int			$device_id		The id of the device to fetch data for.
+	 * @param	string		$reading_type	The reading type to fetch.
+	 * @param	DateTime	$start			The starting DateTime.
+	 * @param	DateTime	$end			The ending DateTime.
+	 * @return	array		The requested data.
+	 */
+	public function get_readings_by_device(int $device_id, string $reading_type, \DateTime $start, \DateTime $end);
+	
 }
diff --git a/logic/Repositories/MariaDBMeasurementDataRepository.php b/logic/Repositories/MariaDBMeasurementDataRepository.php
index a0ddd42..ba1f263 100644
--- a/logic/Repositories/MariaDBMeasurementDataRepository.php
+++ b/logic/Repositories/MariaDBMeasurementDataRepository.php
@@ -93,4 +93,27 @@ class MariaDBMeasurementDataRepository implements IMeasurementDataRepository {
 			]
 		)->fetchAll();
 	}
+	
+	public function get_device_reading_bounds(int $device_id) {
+		$s = $this->get_static;
+		return $this->database->query(
+			"SELECT
+				MIN(COALESCE(
+					{$s("table_name_metadata")}.{$s("column_metadata_recordedon")},
+					{$s("table_name_metadata")}.{$s("column_metadata_storedon")}
+				)) AS start,
+				MAX(COALESCE(
+					{$s("table_name_metadata")}.{$s("column_metadata_recordedon")},
+					{$s("table_name_metadata")}.{$s("column_metadata_storedon")}
+				)) AS end
+			FROM {$s("table_name_metadata")}
+			WHERE {$s("table_name_metadata")}.{$s("column_metadata_device_id")} = :device_id;", [
+				$device_id
+			]
+		)->fetch();
+	}
+	
+	public function get_readings_by_device(int $device_id, string $reading_type, \DateTime $start, \DateTime $end) {
+		
+	}
 }
diff --git a/package-lock.json b/package-lock.json
index 6d470d6..acd84fc 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -4,6 +4,12 @@
   "lockfileVersion": 1,
   "requires": true,
   "dependencies": {
+    "@types/chart.js": {
+      "version": "2.7.42",
+      "resolved": "https://registry.npmjs.org/@types/chart.js/-/chart.js-2.7.42.tgz",
+      "integrity": "sha512-+0v+PV2J9wsV7u36Vqt5Oke7ugtiZqNeYJgNk5mgNMkEkqtjxo2Q9N5Q9znHlgmXeOTfQL6e1sfcAbTnPHAzlA==",
+      "dev": true
+    },
     "@types/estree": {
       "version": "0.0.39",
       "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.39.tgz",
@@ -247,6 +253,39 @@
         "supports-color": "^5.3.0"
       }
     },
+    "chart.js": {
+      "version": "2.7.3",
+      "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-2.7.3.tgz",
+      "integrity": "sha512-3+7k/DbR92m6BsMUYP6M0dMsMVZpMnwkUyNSAbqolHKsbIzH2Q4LWVEHHYq7v0fmEV8whXE0DrjANulw9j2K5g==",
+      "requires": {
+        "chartjs-color": "^2.1.0",
+        "moment": "^2.10.2"
+      }
+    },
+    "chartjs-color": {
+      "version": "2.2.0",
+      "resolved": "https://registry.npmjs.org/chartjs-color/-/chartjs-color-2.2.0.tgz",
+      "integrity": "sha1-hKL7dVeH7YXDndbdjHsdiEKbrq4=",
+      "requires": {
+        "chartjs-color-string": "^0.5.0",
+        "color-convert": "^0.5.3"
+      },
+      "dependencies": {
+        "color-convert": {
+          "version": "0.5.3",
+          "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-0.5.3.tgz",
+          "integrity": "sha1-vbbGnOZg+t/+CwAHzER+G59ygr0="
+        }
+      }
+    },
+    "chartjs-color-string": {
+      "version": "0.5.0",
+      "resolved": "https://registry.npmjs.org/chartjs-color-string/-/chartjs-color-string-0.5.0.tgz",
+      "integrity": "sha512-amWNvCOXlOUYxZVDSa0YOab5K/lmEhbFNKI55PWc4mlv28BDzA7zaoQTGxSBgJMHIW+hGX8YUrvw/FH4LyhwSQ==",
+      "requires": {
+        "color-name": "^1.0.0"
+      }
+    },
     "clap": {
       "version": "1.2.3",
       "resolved": "https://registry.npmjs.org/clap/-/clap-1.2.3.tgz",
@@ -321,8 +360,7 @@
     "color-name": {
       "version": "1.1.3",
       "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
-      "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=",
-      "dev": true
+      "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU="
     },
     "color-string": {
       "version": "0.3.0",
@@ -1193,6 +1231,11 @@
         "minimist": "0.0.8"
       }
     },
+    "moment": {
+      "version": "2.23.0",
+      "resolved": "https://registry.npmjs.org/moment/-/moment-2.23.0.tgz",
+      "integrity": "sha512-3IE39bHVqFbWWaPOMHZF98Q9c3LDKGTmypMiTM2QygGXXElkFWIH7GxfmlwmY2vwa+wmNsoYZmG2iusf1ZjJoA=="
+    },
     "normalize-path": {
       "version": "2.1.1",
       "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz",
diff --git a/package.json b/package.json
index defb183..a0351e0 100644
--- a/package.json
+++ b/package.json
@@ -17,6 +17,7 @@
   },
   "homepage": "https://github.com/sbrl/ConnectedHumber-Air-Quality-Interface#readme",
   "dependencies": {
+    "chart.js": "^2.7.3",
     "dom-create-element-query-selector": "github:hekigan/dom-create-element-query-selector",
     "leaflet": "^1.4.0",
     "leaflet-fullscreen": "^1.0.2",
@@ -25,6 +26,7 @@
     "smartsettings": "^1.2.3"
   },
   "devDependencies": {
+    "@types/chart.js": "^2.7.42",
     "@types/leaflet": "^1.2.14",
     "@types/leaflet-fullscreen": "^1.0.4",
     "leaflet-heatmap": "^1.0.0",
