Quellcode durchsuchen

Initial processing of data + show a chart

- First adding a chart showed large spikes, presumably due to wifi taking
  multiple attempts to connect, causing the chip to heat up.
  - One jump went from -4.12*C to +3.65*C, with a 9:33 min gap.
- Spike removal uses rate of change, with an absolute minimum.
  - Multiplied rate of change lets a regular increase/decrease (eg: 2*C every 5
    minutes in the morning) continue to exist.
  - A minimum rate of change prevents things like 0.01 vs 0.1 being removed.
- Aligning data to a 15-minute window will create a more uniform output.
  - 15 minutes is 3x the collection rate, so if 2 spikes are removed, there's
    still some data.
  - Aligning to the middle of the period (eg: 00:15:00 covers data points from
    00:07:30 to 00:22:30) as this gets the closest to real life.
  - Aligning the data means a 12-minute gap doesn't skew the graph's X axis.
Jason Tarka vor 4 Jahren
Ursprung
Commit
a90ad6d76c

+ 505 - 108
web-view/package-lock.json

@@ -9,12 +9,16 @@
       "version": "1.0.0",
       "dependencies": {
         "@types/firebase": "^3.2.1",
+        "chart.js": "^3.7.0",
         "firebase": "^9.6.1"
       },
       "devDependencies": {
-        "mocha": "^9.1.3",
+        "@types/mocha": "^9.0.0",
+        "@types/should": "^13.0.0",
+        "mocha": "^8.4.0",
         "should": "^13.2.3",
         "ts-loader": "^9.2.6",
+        "ts-mocha": "^8.0.0",
         "ts-node": "^10.4.0",
         "typescript": "^4.5.4",
         "webpack-cli": "^4.9.1"
@@ -670,16 +674,39 @@
       "dev": true,
       "peer": true
     },
+    "node_modules/@types/json5": {
+      "version": "0.0.29",
+      "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz",
+      "integrity": "sha1-7ihweulOEdK4J7y+UnC86n8+ce4=",
+      "dev": true,
+      "optional": true
+    },
     "node_modules/@types/long": {
       "version": "4.0.1",
       "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.1.tgz",
       "integrity": "sha512-5tXH6Bx/kNGd3MgffdmP4dy2Z+G4eaXw0SE81Tq3BNadtnMR5/ySMzX4SLEzHJzSmPNn4HIdpQsBvXMUykr58w=="
     },
+    "node_modules/@types/mocha": {
+      "version": "9.0.0",
+      "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-9.0.0.tgz",
+      "integrity": "sha512-scN0hAWyLVAvLR9AyW7HoFF5sJZglyBsbPuHO4fv7JRvfmPBMfp1ozWqOf/e4wwPNxezBZXRfWzMb6iFLgEVRA==",
+      "dev": true
+    },
     "node_modules/@types/node": {
       "version": "17.0.4",
       "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.4.tgz",
       "integrity": "sha512-6xwbrW4JJiJLgF+zNypN5wr2ykM9/jHcL7rQ8fZe2vuftggjzZeRSM4OwRc6Xk8qWjwJ99qVHo/JgOGmomWRog=="
     },
+    "node_modules/@types/should": {
+      "version": "13.0.0",
+      "resolved": "https://registry.npmjs.org/@types/should/-/should-13.0.0.tgz",
+      "integrity": "sha512-Mi6YZ2ABnnGGFMuiBDP0a8s1ZDCDNHqP97UH8TyDmCWuGGavpsFMfJnAMYaaqmDlSCOCNbVLHBrSDEOpx/oLhw==",
+      "deprecated": "This is a stub types definition for should.js (https://github.com/shouldjs/should.js). should.js provides its own type definitions, so you don't need @types/should installed!",
+      "dev": true,
+      "dependencies": {
+        "should": "*"
+      }
+    },
     "node_modules/@ungap/promise-all-settled": {
       "version": "1.1.2",
       "resolved": "https://registry.npmjs.org/@ungap/promise-all-settled/-/promise-all-settled-1.1.2.tgz",
@@ -1011,6 +1038,15 @@
       "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
       "dev": true
     },
+    "node_modules/arrify": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz",
+      "integrity": "sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=",
+      "dev": true,
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
     "node_modules/balanced-match": {
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
@@ -1080,8 +1116,7 @@
       "version": "1.1.2",
       "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
       "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
-      "dev": true,
-      "peer": true
+      "dev": true
     },
     "node_modules/camelcase": {
       "version": "6.2.1",
@@ -1134,25 +1169,30 @@
         "node": ">=8"
       }
     },
+    "node_modules/chart.js": {
+      "version": "3.7.0",
+      "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-3.7.0.tgz",
+      "integrity": "sha512-31gVuqqKp3lDIFmzpKIrBeum4OpZsQjSIAqlOpgjosHDJZlULtvwLEZKtEhIAZc7JMPaHlYMys40Qy9Mf+1AAg=="
+    },
     "node_modules/chokidar": {
-      "version": "3.5.2",
-      "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.2.tgz",
-      "integrity": "sha512-ekGhOnNVPgT77r4K/U3GDhu+FQ2S8TnK/s2KbIGXi0SZWuwkZ2QNyfWdZW+TVfn84DpEP7rLeCt2UI6bJ8GwbQ==",
+      "version": "3.5.1",
+      "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.1.tgz",
+      "integrity": "sha512-9+s+Od+W0VJJzawDma/gvBNQqkTiqYTWLuZoyAsivsI4AaWTCzHG06/TMjsf1cYe9Cb97UCEhjz7HvnPk2p/tw==",
       "dev": true,
       "dependencies": {
-        "anymatch": "~3.1.2",
+        "anymatch": "~3.1.1",
         "braces": "~3.0.2",
-        "glob-parent": "~5.1.2",
+        "glob-parent": "~5.1.0",
         "is-binary-path": "~2.1.0",
         "is-glob": "~4.0.1",
         "normalize-path": "~3.0.0",
-        "readdirp": "~3.6.0"
+        "readdirp": "~3.5.0"
       },
       "engines": {
         "node": ">= 8.10.0"
       },
       "optionalDependencies": {
-        "fsevents": "~2.3.2"
+        "fsevents": "~2.3.1"
       }
     },
     "node_modules/chrome-trace-event": {
@@ -1259,9 +1299,9 @@
       }
     },
     "node_modules/debug": {
-      "version": "4.3.2",
-      "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz",
-      "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==",
+      "version": "4.3.1",
+      "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz",
+      "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==",
       "dev": true,
       "dependencies": {
         "ms": "2.1.2"
@@ -1833,18 +1873,6 @@
         "url": "https://github.com/sponsors/sindresorhus"
       }
     },
-    "node_modules/is-unicode-supported": {
-      "version": "0.1.0",
-      "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz",
-      "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==",
-      "dev": true,
-      "engines": {
-        "node": ">=10"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/sindresorhus"
-      }
-    },
     "node_modules/isarray": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
@@ -1881,9 +1909,9 @@
       }
     },
     "node_modules/js-yaml": {
-      "version": "4.1.0",
-      "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
-      "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.0.0.tgz",
+      "integrity": "sha512-pqon0s+4ScYUvX30wxQi3PogGFAlUyH0awepWvwkj4jD4v+ova3RiYw8bmA6x2rDrEaj8i/oWKoRxpVNW+Re8Q==",
       "dev": true,
       "dependencies": {
         "argparse": "^2.0.1"
@@ -1906,6 +1934,19 @@
       "dev": true,
       "peer": true
     },
+    "node_modules/json5": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz",
+      "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==",
+      "dev": true,
+      "optional": true,
+      "dependencies": {
+        "minimist": "^1.2.0"
+      },
+      "bin": {
+        "json5": "lib/cli.js"
+      }
+    },
     "node_modules/jszip": {
       "version": "3.7.1",
       "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.7.1.tgz",
@@ -1965,19 +2006,15 @@
       "integrity": "sha1-soqmKIorn8ZRA1x3EfZathkDMaY="
     },
     "node_modules/log-symbols": {
-      "version": "4.1.0",
-      "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz",
-      "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==",
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.0.0.tgz",
+      "integrity": "sha512-FN8JBzLx6CzeMrB0tg6pqlGU1wCrXW+ZXGH481kfsBqer0hToTIiHdjH4Mq8xJUbvATujKCvaREGWpGUionraA==",
       "dev": true,
       "dependencies": {
-        "chalk": "^4.1.0",
-        "is-unicode-supported": "^0.1.0"
+        "chalk": "^4.0.0"
       },
       "engines": {
         "node": ">=10"
-      },
-      "funding": {
-        "url": "https://github.com/sponsors/sindresorhus"
       }
     },
     "node_modules/long": {
@@ -2065,33 +2102,52 @@
         "node": "*"
       }
     },
+    "node_modules/minimist": {
+      "version": "1.2.5",
+      "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
+      "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==",
+      "dev": true
+    },
+    "node_modules/mkdirp": {
+      "version": "0.5.5",
+      "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz",
+      "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==",
+      "dev": true,
+      "dependencies": {
+        "minimist": "^1.2.5"
+      },
+      "bin": {
+        "mkdirp": "bin/cmd.js"
+      }
+    },
     "node_modules/mocha": {
-      "version": "9.1.3",
-      "resolved": "https://registry.npmjs.org/mocha/-/mocha-9.1.3.tgz",
-      "integrity": "sha512-Xcpl9FqXOAYqI3j79pEtHBBnQgVXIhpULjGQa7DVb0Po+VzmSIK9kanAiWLHoRR/dbZ2qpdPshuXr8l1VaHCzw==",
+      "version": "8.4.0",
+      "resolved": "https://registry.npmjs.org/mocha/-/mocha-8.4.0.tgz",
+      "integrity": "sha512-hJaO0mwDXmZS4ghXsvPVriOhsxQ7ofcpQdm8dE+jISUOKopitvnXFQmpRR7jd2K6VBG6E26gU3IAbXXGIbu4sQ==",
       "dev": true,
       "dependencies": {
         "@ungap/promise-all-settled": "1.1.2",
         "ansi-colors": "4.1.1",
         "browser-stdout": "1.3.1",
-        "chokidar": "3.5.2",
-        "debug": "4.3.2",
+        "chokidar": "3.5.1",
+        "debug": "4.3.1",
         "diff": "5.0.0",
         "escape-string-regexp": "4.0.0",
         "find-up": "5.0.0",
-        "glob": "7.1.7",
+        "glob": "7.1.6",
         "growl": "1.10.5",
         "he": "1.2.0",
-        "js-yaml": "4.1.0",
-        "log-symbols": "4.1.0",
+        "js-yaml": "4.0.0",
+        "log-symbols": "4.0.0",
         "minimatch": "3.0.4",
         "ms": "2.1.3",
-        "nanoid": "3.1.25",
-        "serialize-javascript": "6.0.0",
+        "nanoid": "3.1.20",
+        "serialize-javascript": "5.0.1",
         "strip-json-comments": "3.1.1",
         "supports-color": "8.1.1",
         "which": "2.0.2",
-        "workerpool": "6.1.5",
+        "wide-align": "1.1.3",
+        "workerpool": "6.1.0",
         "yargs": "16.2.0",
         "yargs-parser": "20.2.4",
         "yargs-unparser": "2.0.0"
@@ -2101,13 +2157,42 @@
         "mocha": "bin/mocha"
       },
       "engines": {
-        "node": ">= 12.0.0"
+        "node": ">= 10.12.0"
       },
       "funding": {
         "type": "opencollective",
         "url": "https://opencollective.com/mochajs"
       }
     },
+    "node_modules/mocha/node_modules/glob": {
+      "version": "7.1.6",
+      "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz",
+      "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==",
+      "dev": true,
+      "dependencies": {
+        "fs.realpath": "^1.0.0",
+        "inflight": "^1.0.4",
+        "inherits": "2",
+        "minimatch": "^3.0.4",
+        "once": "^1.3.0",
+        "path-is-absolute": "^1.0.0"
+      },
+      "engines": {
+        "node": "*"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
+    "node_modules/mocha/node_modules/serialize-javascript": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-5.0.1.tgz",
+      "integrity": "sha512-SaaNal9imEO737H2c05Og0/8LUXG7EnsZyMa8MzkmuHoELfT6txuj0cMqRj6zfPKnmQ1yasR4PCJc8x+M4JSPA==",
+      "dev": true,
+      "dependencies": {
+        "randombytes": "^2.1.0"
+      }
+    },
     "node_modules/ms": {
       "version": "2.1.3",
       "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
@@ -2115,9 +2200,9 @@
       "dev": true
     },
     "node_modules/nanoid": {
-      "version": "3.1.25",
-      "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.25.tgz",
-      "integrity": "sha512-rdwtIXaXCLFAQbnfqDRnI6jaRHp9fTcYBjtFKE8eezcZ7LuLjhUaQGNeMXf1HmRoCH32CLz6XwX0TtxEOS/A3Q==",
+      "version": "3.1.20",
+      "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.20.tgz",
+      "integrity": "sha512-a1cQNyczgKbLX9jwbS/+d7W8fX/RfgYR7lVWwWOGIPNgK2m0MWvrGF6/m4kk6U3QcFMnZf3RIhL0v2Jgh/0Uxw==",
       "dev": true,
       "bin": {
         "nanoid": "bin/nanoid.cjs"
@@ -2428,9 +2513,9 @@
       "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
     },
     "node_modules/readdirp": {
-      "version": "3.6.0",
-      "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
-      "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
+      "version": "3.5.0",
+      "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.5.0.tgz",
+      "integrity": "sha512-cMhu7c/8rdhkHXWsY+osBhfSy0JikwpHK/5+imo+LpeasTF8ouErHrlYkwT0++njiyuDvc7OFY5T3ukvZ8qmFQ==",
       "dev": true,
       "dependencies": {
         "picomatch": "^2.2.1"
@@ -2579,6 +2664,7 @@
       "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz",
       "integrity": "sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==",
       "dev": true,
+      "peer": true,
       "dependencies": {
         "randombytes": "^2.1.0"
       }
@@ -2689,7 +2775,6 @@
       "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
       "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
       "dev": true,
-      "peer": true,
       "engines": {
         "node": ">=0.10.0"
       }
@@ -2699,7 +2784,6 @@
       "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz",
       "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==",
       "dev": true,
-      "peer": true,
       "dependencies": {
         "buffer-from": "^1.0.0",
         "source-map": "^0.6.0"
@@ -2742,6 +2826,16 @@
         "node": ">=8"
       }
     },
+    "node_modules/strip-bom": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz",
+      "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=",
+      "dev": true,
+      "optional": true,
+      "engines": {
+        "node": ">=4"
+      }
+    },
     "node_modules/strip-final-newline": {
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz",
@@ -2905,6 +2999,67 @@
         "webpack": "^5.0.0"
       }
     },
+    "node_modules/ts-mocha": {
+      "version": "8.0.0",
+      "resolved": "https://registry.npmjs.org/ts-mocha/-/ts-mocha-8.0.0.tgz",
+      "integrity": "sha512-Kou1yxTlubLnD5C3unlCVO7nh0HERTezjoVhVw/M5S1SqoUec0WgllQvPk3vzPMc6by8m6xD1uR1yRf8lnVUbA==",
+      "dev": true,
+      "dependencies": {
+        "ts-node": "7.0.1"
+      },
+      "bin": {
+        "ts-mocha": "bin/ts-mocha"
+      },
+      "engines": {
+        "node": ">= 6.X.X"
+      },
+      "optionalDependencies": {
+        "tsconfig-paths": "^3.5.0"
+      },
+      "peerDependencies": {
+        "mocha": "^3.X.X || ^4.X.X || ^5.X.X || ^6.X.X || ^7.X.X || ^8.X.X"
+      }
+    },
+    "node_modules/ts-mocha/node_modules/diff": {
+      "version": "3.5.0",
+      "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz",
+      "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==",
+      "dev": true,
+      "engines": {
+        "node": ">=0.3.1"
+      }
+    },
+    "node_modules/ts-mocha/node_modules/ts-node": {
+      "version": "7.0.1",
+      "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-7.0.1.tgz",
+      "integrity": "sha512-BVwVbPJRspzNh2yfslyT1PSbl5uIk03EZlb493RKHN4qej/D06n1cEhjlOJG69oFsE7OT8XjpTUcYf6pKTLMhw==",
+      "dev": true,
+      "dependencies": {
+        "arrify": "^1.0.0",
+        "buffer-from": "^1.1.0",
+        "diff": "^3.1.0",
+        "make-error": "^1.1.1",
+        "minimist": "^1.2.0",
+        "mkdirp": "^0.5.1",
+        "source-map-support": "^0.5.6",
+        "yn": "^2.0.0"
+      },
+      "bin": {
+        "ts-node": "dist/bin.js"
+      },
+      "engines": {
+        "node": ">=4.2.0"
+      }
+    },
+    "node_modules/ts-mocha/node_modules/yn": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/yn/-/yn-2.0.0.tgz",
+      "integrity": "sha1-5a2ryKz0CPY4X8dklWhMiOavaJo=",
+      "dev": true,
+      "engines": {
+        "node": ">=4"
+      }
+    },
     "node_modules/ts-node": {
       "version": "10.4.0",
       "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.4.0.tgz",
@@ -2955,6 +3110,19 @@
         "node": ">=0.3.1"
       }
     },
+    "node_modules/tsconfig-paths": {
+      "version": "3.12.0",
+      "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.12.0.tgz",
+      "integrity": "sha512-e5adrnOYT6zqVnWqZu7i/BQ3BnhzvGbjEjejFXO20lKIKpwTaupkCPgEfv4GZK1IBciJUEhYs3J3p75FdaTFVg==",
+      "dev": true,
+      "optional": true,
+      "dependencies": {
+        "@types/json5": "^0.0.29",
+        "json5": "^1.0.1",
+        "minimist": "^1.2.0",
+        "strip-bom": "^3.0.0"
+      }
+    },
     "node_modules/tslib": {
       "version": "2.3.1",
       "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz",
@@ -3180,6 +3348,58 @@
         "node": ">= 8"
       }
     },
+    "node_modules/wide-align": {
+      "version": "1.1.3",
+      "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz",
+      "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==",
+      "dev": true,
+      "dependencies": {
+        "string-width": "^1.0.2 || 2"
+      }
+    },
+    "node_modules/wide-align/node_modules/ansi-regex": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz",
+      "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=",
+      "dev": true,
+      "engines": {
+        "node": ">=4"
+      }
+    },
+    "node_modules/wide-align/node_modules/is-fullwidth-code-point": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz",
+      "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=",
+      "dev": true,
+      "engines": {
+        "node": ">=4"
+      }
+    },
+    "node_modules/wide-align/node_modules/string-width": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz",
+      "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==",
+      "dev": true,
+      "dependencies": {
+        "is-fullwidth-code-point": "^2.0.0",
+        "strip-ansi": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=4"
+      }
+    },
+    "node_modules/wide-align/node_modules/strip-ansi": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz",
+      "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=",
+      "dev": true,
+      "dependencies": {
+        "ansi-regex": "^3.0.0"
+      },
+      "engines": {
+        "node": ">=4"
+      }
+    },
     "node_modules/wildcard": {
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.0.tgz",
@@ -3187,9 +3407,9 @@
       "dev": true
     },
     "node_modules/workerpool": {
-      "version": "6.1.5",
-      "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.1.5.tgz",
-      "integrity": "sha512-XdKkCK0Zqc6w3iTxLckiuJ81tiD/o5rBE/m+nXpRCB+/Sq4DqkfXZ/x0jW02DG1tGsfUGXbTJyZDP+eu67haSw==",
+      "version": "6.1.0",
+      "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.1.0.tgz",
+      "integrity": "sha512-toV7q9rWNYha963Pl/qyeZ6wG+3nnsyvolaNUS8+R5Wtw6qJPTxIlOP1ZSvcGhEJw+l3HMMmtiNo9Gl61G4GVg==",
       "dev": true
     },
     "node_modules/wrap-ansi": {
@@ -3866,16 +4086,38 @@
       "dev": true,
       "peer": true
     },
+    "@types/json5": {
+      "version": "0.0.29",
+      "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz",
+      "integrity": "sha1-7ihweulOEdK4J7y+UnC86n8+ce4=",
+      "dev": true,
+      "optional": true
+    },
     "@types/long": {
       "version": "4.0.1",
       "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.1.tgz",
       "integrity": "sha512-5tXH6Bx/kNGd3MgffdmP4dy2Z+G4eaXw0SE81Tq3BNadtnMR5/ySMzX4SLEzHJzSmPNn4HIdpQsBvXMUykr58w=="
     },
+    "@types/mocha": {
+      "version": "9.0.0",
+      "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-9.0.0.tgz",
+      "integrity": "sha512-scN0hAWyLVAvLR9AyW7HoFF5sJZglyBsbPuHO4fv7JRvfmPBMfp1ozWqOf/e4wwPNxezBZXRfWzMb6iFLgEVRA==",
+      "dev": true
+    },
     "@types/node": {
       "version": "17.0.4",
       "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.4.tgz",
       "integrity": "sha512-6xwbrW4JJiJLgF+zNypN5wr2ykM9/jHcL7rQ8fZe2vuftggjzZeRSM4OwRc6Xk8qWjwJ99qVHo/JgOGmomWRog=="
     },
+    "@types/should": {
+      "version": "13.0.0",
+      "resolved": "https://registry.npmjs.org/@types/should/-/should-13.0.0.tgz",
+      "integrity": "sha512-Mi6YZ2ABnnGGFMuiBDP0a8s1ZDCDNHqP97UH8TyDmCWuGGavpsFMfJnAMYaaqmDlSCOCNbVLHBrSDEOpx/oLhw==",
+      "dev": true,
+      "requires": {
+        "should": "*"
+      }
+    },
     "@ungap/promise-all-settled": {
       "version": "1.1.2",
       "resolved": "https://registry.npmjs.org/@ungap/promise-all-settled/-/promise-all-settled-1.1.2.tgz",
@@ -4162,6 +4404,12 @@
       "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
       "dev": true
     },
+    "arrify": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz",
+      "integrity": "sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=",
+      "dev": true
+    },
     "balanced-match": {
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
@@ -4215,8 +4463,7 @@
       "version": "1.1.2",
       "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
       "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
-      "dev": true,
-      "peer": true
+      "dev": true
     },
     "camelcase": {
       "version": "6.2.1",
@@ -4252,20 +4499,25 @@
         }
       }
     },
+    "chart.js": {
+      "version": "3.7.0",
+      "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-3.7.0.tgz",
+      "integrity": "sha512-31gVuqqKp3lDIFmzpKIrBeum4OpZsQjSIAqlOpgjosHDJZlULtvwLEZKtEhIAZc7JMPaHlYMys40Qy9Mf+1AAg=="
+    },
     "chokidar": {
-      "version": "3.5.2",
-      "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.2.tgz",
-      "integrity": "sha512-ekGhOnNVPgT77r4K/U3GDhu+FQ2S8TnK/s2KbIGXi0SZWuwkZ2QNyfWdZW+TVfn84DpEP7rLeCt2UI6bJ8GwbQ==",
+      "version": "3.5.1",
+      "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.1.tgz",
+      "integrity": "sha512-9+s+Od+W0VJJzawDma/gvBNQqkTiqYTWLuZoyAsivsI4AaWTCzHG06/TMjsf1cYe9Cb97UCEhjz7HvnPk2p/tw==",
       "dev": true,
       "requires": {
-        "anymatch": "~3.1.2",
+        "anymatch": "~3.1.1",
         "braces": "~3.0.2",
-        "fsevents": "~2.3.2",
-        "glob-parent": "~5.1.2",
+        "fsevents": "~2.3.1",
+        "glob-parent": "~5.1.0",
         "is-binary-path": "~2.1.0",
         "is-glob": "~4.0.1",
         "normalize-path": "~3.0.0",
-        "readdirp": "~3.6.0"
+        "readdirp": "~3.5.0"
       }
     },
     "chrome-trace-event": {
@@ -4355,9 +4607,9 @@
       }
     },
     "debug": {
-      "version": "4.3.2",
-      "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz",
-      "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==",
+      "version": "4.3.1",
+      "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz",
+      "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==",
       "dev": true,
       "requires": {
         "ms": "2.1.2"
@@ -4783,12 +5035,6 @@
       "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==",
       "dev": true
     },
-    "is-unicode-supported": {
-      "version": "0.1.0",
-      "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz",
-      "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==",
-      "dev": true
-    },
     "isarray": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
@@ -4819,9 +5065,9 @@
       }
     },
     "js-yaml": {
-      "version": "4.1.0",
-      "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
-      "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.0.0.tgz",
+      "integrity": "sha512-pqon0s+4ScYUvX30wxQi3PogGFAlUyH0awepWvwkj4jD4v+ova3RiYw8bmA6x2rDrEaj8i/oWKoRxpVNW+Re8Q==",
       "dev": true,
       "requires": {
         "argparse": "^2.0.1"
@@ -4841,6 +5087,16 @@
       "dev": true,
       "peer": true
     },
+    "json5": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz",
+      "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==",
+      "dev": true,
+      "optional": true,
+      "requires": {
+        "minimist": "^1.2.0"
+      }
+    },
     "jszip": {
       "version": "3.7.1",
       "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.7.1.tgz",
@@ -4888,13 +5144,12 @@
       "integrity": "sha1-soqmKIorn8ZRA1x3EfZathkDMaY="
     },
     "log-symbols": {
-      "version": "4.1.0",
-      "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz",
-      "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==",
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.0.0.tgz",
+      "integrity": "sha512-FN8JBzLx6CzeMrB0tg6pqlGU1wCrXW+ZXGH481kfsBqer0hToTIiHdjH4Mq8xJUbvATujKCvaREGWpGUionraA==",
       "dev": true,
       "requires": {
-        "chalk": "^4.1.0",
-        "is-unicode-supported": "^0.1.0"
+        "chalk": "^4.0.0"
       }
     },
     "long": {
@@ -4964,36 +5219,77 @@
         "brace-expansion": "^1.1.7"
       }
     },
+    "minimist": {
+      "version": "1.2.5",
+      "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
+      "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==",
+      "dev": true
+    },
+    "mkdirp": {
+      "version": "0.5.5",
+      "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz",
+      "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==",
+      "dev": true,
+      "requires": {
+        "minimist": "^1.2.5"
+      }
+    },
     "mocha": {
-      "version": "9.1.3",
-      "resolved": "https://registry.npmjs.org/mocha/-/mocha-9.1.3.tgz",
-      "integrity": "sha512-Xcpl9FqXOAYqI3j79pEtHBBnQgVXIhpULjGQa7DVb0Po+VzmSIK9kanAiWLHoRR/dbZ2qpdPshuXr8l1VaHCzw==",
+      "version": "8.4.0",
+      "resolved": "https://registry.npmjs.org/mocha/-/mocha-8.4.0.tgz",
+      "integrity": "sha512-hJaO0mwDXmZS4ghXsvPVriOhsxQ7ofcpQdm8dE+jISUOKopitvnXFQmpRR7jd2K6VBG6E26gU3IAbXXGIbu4sQ==",
       "dev": true,
       "requires": {
         "@ungap/promise-all-settled": "1.1.2",
         "ansi-colors": "4.1.1",
         "browser-stdout": "1.3.1",
-        "chokidar": "3.5.2",
-        "debug": "4.3.2",
+        "chokidar": "3.5.1",
+        "debug": "4.3.1",
         "diff": "5.0.0",
         "escape-string-regexp": "4.0.0",
         "find-up": "5.0.0",
-        "glob": "7.1.7",
+        "glob": "7.1.6",
         "growl": "1.10.5",
         "he": "1.2.0",
-        "js-yaml": "4.1.0",
-        "log-symbols": "4.1.0",
+        "js-yaml": "4.0.0",
+        "log-symbols": "4.0.0",
         "minimatch": "3.0.4",
         "ms": "2.1.3",
-        "nanoid": "3.1.25",
-        "serialize-javascript": "6.0.0",
+        "nanoid": "3.1.20",
+        "serialize-javascript": "5.0.1",
         "strip-json-comments": "3.1.1",
         "supports-color": "8.1.1",
         "which": "2.0.2",
-        "workerpool": "6.1.5",
+        "wide-align": "1.1.3",
+        "workerpool": "6.1.0",
         "yargs": "16.2.0",
         "yargs-parser": "20.2.4",
         "yargs-unparser": "2.0.0"
+      },
+      "dependencies": {
+        "glob": {
+          "version": "7.1.6",
+          "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz",
+          "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==",
+          "dev": true,
+          "requires": {
+            "fs.realpath": "^1.0.0",
+            "inflight": "^1.0.4",
+            "inherits": "2",
+            "minimatch": "^3.0.4",
+            "once": "^1.3.0",
+            "path-is-absolute": "^1.0.0"
+          }
+        },
+        "serialize-javascript": {
+          "version": "5.0.1",
+          "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-5.0.1.tgz",
+          "integrity": "sha512-SaaNal9imEO737H2c05Og0/8LUXG7EnsZyMa8MzkmuHoELfT6txuj0cMqRj6zfPKnmQ1yasR4PCJc8x+M4JSPA==",
+          "dev": true,
+          "requires": {
+            "randombytes": "^2.1.0"
+          }
+        }
       }
     },
     "ms": {
@@ -5003,9 +5299,9 @@
       "dev": true
     },
     "nanoid": {
-      "version": "3.1.25",
-      "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.25.tgz",
-      "integrity": "sha512-rdwtIXaXCLFAQbnfqDRnI6jaRHp9fTcYBjtFKE8eezcZ7LuLjhUaQGNeMXf1HmRoCH32CLz6XwX0TtxEOS/A3Q==",
+      "version": "3.1.20",
+      "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.20.tgz",
+      "integrity": "sha512-a1cQNyczgKbLX9jwbS/+d7W8fX/RfgYR7lVWwWOGIPNgK2m0MWvrGF6/m4kk6U3QcFMnZf3RIhL0v2Jgh/0Uxw==",
       "dev": true
     },
     "neo-async": {
@@ -5243,9 +5539,9 @@
       }
     },
     "readdirp": {
-      "version": "3.6.0",
-      "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
-      "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
+      "version": "3.5.0",
+      "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.5.0.tgz",
+      "integrity": "sha512-cMhu7c/8rdhkHXWsY+osBhfSy0JikwpHK/5+imo+LpeasTF8ouErHrlYkwT0++njiyuDvc7OFY5T3ukvZ8qmFQ==",
       "dev": true,
       "requires": {
         "picomatch": "^2.2.1"
@@ -5340,6 +5636,7 @@
       "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz",
       "integrity": "sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==",
       "dev": true,
+      "peer": true,
       "requires": {
         "randombytes": "^2.1.0"
       }
@@ -5437,15 +5734,13 @@
       "version": "0.6.1",
       "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
       "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
-      "dev": true,
-      "peer": true
+      "dev": true
     },
     "source-map-support": {
       "version": "0.5.21",
       "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz",
       "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==",
       "dev": true,
-      "peer": true,
       "requires": {
         "buffer-from": "^1.0.0",
         "source-map": "^0.6.0"
@@ -5484,6 +5779,13 @@
         "ansi-regex": "^5.0.1"
       }
     },
+    "strip-bom": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz",
+      "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=",
+      "dev": true,
+      "optional": true
+    },
     "strip-final-newline": {
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz",
@@ -5580,6 +5882,46 @@
         "semver": "^7.3.4"
       }
     },
+    "ts-mocha": {
+      "version": "8.0.0",
+      "resolved": "https://registry.npmjs.org/ts-mocha/-/ts-mocha-8.0.0.tgz",
+      "integrity": "sha512-Kou1yxTlubLnD5C3unlCVO7nh0HERTezjoVhVw/M5S1SqoUec0WgllQvPk3vzPMc6by8m6xD1uR1yRf8lnVUbA==",
+      "dev": true,
+      "requires": {
+        "ts-node": "7.0.1",
+        "tsconfig-paths": "^3.5.0"
+      },
+      "dependencies": {
+        "diff": {
+          "version": "3.5.0",
+          "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz",
+          "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==",
+          "dev": true
+        },
+        "ts-node": {
+          "version": "7.0.1",
+          "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-7.0.1.tgz",
+          "integrity": "sha512-BVwVbPJRspzNh2yfslyT1PSbl5uIk03EZlb493RKHN4qej/D06n1cEhjlOJG69oFsE7OT8XjpTUcYf6pKTLMhw==",
+          "dev": true,
+          "requires": {
+            "arrify": "^1.0.0",
+            "buffer-from": "^1.1.0",
+            "diff": "^3.1.0",
+            "make-error": "^1.1.1",
+            "minimist": "^1.2.0",
+            "mkdirp": "^0.5.1",
+            "source-map-support": "^0.5.6",
+            "yn": "^2.0.0"
+          }
+        },
+        "yn": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/yn/-/yn-2.0.0.tgz",
+          "integrity": "sha1-5a2ryKz0CPY4X8dklWhMiOavaJo=",
+          "dev": true
+        }
+      }
+    },
     "ts-node": {
       "version": "10.4.0",
       "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.4.0.tgz",
@@ -5608,6 +5950,19 @@
         }
       }
     },
+    "tsconfig-paths": {
+      "version": "3.12.0",
+      "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.12.0.tgz",
+      "integrity": "sha512-e5adrnOYT6zqVnWqZu7i/BQ3BnhzvGbjEjejFXO20lKIKpwTaupkCPgEfv4GZK1IBciJUEhYs3J3p75FdaTFVg==",
+      "dev": true,
+      "optional": true,
+      "requires": {
+        "@types/json5": "^0.0.29",
+        "json5": "^1.0.1",
+        "minimist": "^1.2.0",
+        "strip-bom": "^3.0.0"
+      }
+    },
     "tslib": {
       "version": "2.3.1",
       "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz",
@@ -5766,6 +6121,48 @@
         "isexe": "^2.0.0"
       }
     },
+    "wide-align": {
+      "version": "1.1.3",
+      "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz",
+      "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==",
+      "dev": true,
+      "requires": {
+        "string-width": "^1.0.2 || 2"
+      },
+      "dependencies": {
+        "ansi-regex": {
+          "version": "3.0.0",
+          "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz",
+          "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=",
+          "dev": true
+        },
+        "is-fullwidth-code-point": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz",
+          "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=",
+          "dev": true
+        },
+        "string-width": {
+          "version": "2.1.1",
+          "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz",
+          "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==",
+          "dev": true,
+          "requires": {
+            "is-fullwidth-code-point": "^2.0.0",
+            "strip-ansi": "^4.0.0"
+          }
+        },
+        "strip-ansi": {
+          "version": "4.0.0",
+          "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz",
+          "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=",
+          "dev": true,
+          "requires": {
+            "ansi-regex": "^3.0.0"
+          }
+        }
+      }
+    },
     "wildcard": {
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.0.tgz",
@@ -5773,9 +6170,9 @@
       "dev": true
     },
     "workerpool": {
-      "version": "6.1.5",
-      "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.1.5.tgz",
-      "integrity": "sha512-XdKkCK0Zqc6w3iTxLckiuJ81tiD/o5rBE/m+nXpRCB+/Sq4DqkfXZ/x0jW02DG1tGsfUGXbTJyZDP+eu67haSw==",
+      "version": "6.1.0",
+      "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.1.0.tgz",
+      "integrity": "sha512-toV7q9rWNYha963Pl/qyeZ6wG+3nnsyvolaNUS8+R5Wtw6qJPTxIlOP1ZSvcGhEJw+l3HMMmtiNo9Gl61G4GVg==",
       "dev": true
     },
     "wrap-ansi": {

+ 6 - 2
web-view/package.json

@@ -4,7 +4,7 @@
   "description": "",
   "main": "index.js",
   "scripts": {
-    "test": "echo \"Error: no test specified\" && exit 1",
+    "test": "ts-mocha src/**/*.test.ts",
     "clean": "rm -rf build",
     "build": "webpack --mode=development"
   },
@@ -13,12 +13,16 @@
   "license": "",
   "dependencies": {
     "@types/firebase": "^3.2.1",
+    "chart.js": "^3.7.0",
     "firebase": "^9.6.1"
   },
   "devDependencies": {
-    "mocha": "^9.1.3",
+    "@types/mocha": "^9.0.0",
+    "@types/should": "^13.0.0",
+    "mocha": "^8.4.0",
     "should": "^13.2.3",
     "ts-loader": "^9.2.6",
+    "ts-mocha": "^8.0.0",
     "ts-node": "^10.4.0",
     "typescript": "^4.5.4",
     "webpack-cli": "^4.9.1"

+ 2 - 0
web-view/src/index.html

@@ -6,6 +6,8 @@
 </head>
 <body>
 
+<canvas id="temperature" width="200" height="100"></canvas>
+
 <!--suppress HtmlUnknownTarget -->
 <script src="js/main.js" async></script>
 </body>

+ 116 - 0
web-view/src/js/cleanData.test.ts

@@ -0,0 +1,116 @@
+import {alignToWindow, removeSpikes} from './cleanData';
+import {testTemperatureData} from './testData';
+import * as should from 'should';
+
+describe('Clean data', () => {
+	describe('Align to window', () => {
+		it('Aligns data to 15 minute increments', () => {
+			// Adapted from the data in testData.ts
+			const expected = [
+				{ time: 1640296800000, temperature: -4.2 },
+				{ time: 1640297700000, temperature: -4.3 },
+				{ time: 1640298600000, temperature: -4.4 },
+				{ time: 1640299500000, temperature: -4.2 },
+				{ time: 1640300400000, temperature: -4.2 },
+				{ time: 1640301300000, temperature: -4.2 },
+				{ time: 1640302200000, temperature: -0.2 },
+				{ time: 1640303100000, temperature: -2.9 },
+				{ time: 1640304000000, temperature: -3.9 },
+				{ time: 1640304900000, temperature: -4.0 },
+				{ time: 1640305800000, temperature: -3.9 },
+				{ time: 1640306700000, temperature: -3.8 },
+				{ time: 1640307600000, temperature: -3.8 },
+			];
+
+			const actual = alignToWindow(testTemperatureData, 'temperature', 15);
+			should(actual).deepEqual(expected);
+		});
+
+		it('Skips the first period when empty', () => {
+			const input = [
+				{ time: 1640296752000, temperature: -4.21 },
+				{ time: 1640297111475, temperature: -4.28 },
+				{ time: 1640297410320, temperature: -4.29 },
+				{ time: 1640297709214, temperature: -4.34 },
+			];
+
+			const expected = [
+				{ time: 1640296800000, temperature: -4.2 },
+				{ time: 1640297700000, temperature: -4.3 },
+			];
+
+			const actual = alignToWindow(input, 'temperature', 15);
+			should(actual).deepEqual(expected);
+		});
+
+		it('Inserts NaN when a middle period is empty', () => {
+			const input = [
+				{ time: 1640296812283, temperature: -4.21 },
+				{ time: 1640297111475, temperature: -4.28 },
+				// Empty period here
+				{ time: 1640298307129, temperature: -4.42 },
+				{ time: 1640298606065, temperature: -4.4 },
+				{ time: 1640298905167, temperature: -4.27 },
+			];
+
+			const expected = [
+				{ time: 1640296800000, temperature: -4.2 },
+				{ time: 1640297700000, temperature: NaN },
+				{ time: 1640298600000, temperature: -4.4 },
+			];
+
+			const actual = alignToWindow(input, 'temperature', 15);
+			should(actual).deepEqual(expected);
+		});
+	});
+
+	describe('Remove Spikes', () => {
+		it('Removes spiky measurements', () => {
+			// Testing that abnormally large spikes are removed. For example, if
+			// the chip heats up and causes a 2+ Celsius spike in temperature.
+
+			const input = [
+				{ time: 1, temperature: -4.23 },
+				{ time: 2, temperature: -4.14 },
+				{ time: 3, temperature: -4.12 },
+				{ time: 4, temperature: 3.65 }, // Large spike
+				{ time: 5, temperature: -2.05 }, // Smaller, but still big
+				{ time: 6, temperature: -3.12 },
+				{ time: 7, temperature: -3.63 },
+			];
+			const expected = [
+				{ time: 1, temperature: -4.23 },
+				{ time: 2, temperature: -4.14 },
+				{ time: 3, temperature: -4.12 },
+				{ time: 6, temperature: -3.12 },
+				{ time: 7, temperature: -3.63 },
+			];
+
+			const actual = removeSpikes(input, 'temperature');
+			should(actual).deepEqual(expected);
+		});
+
+		it('Sorts data', () => {
+			// Same data as earlier test, but rearranged
+			const input = [
+				{ time: 4, temperature: 3.65 }, // Large spike
+				{ time: 1, temperature: -4.23 },
+				{ time: 7, temperature: -3.63 },
+				{ time: 2, temperature: -4.14 },
+				{ time: 6, temperature: -3.12 },
+				{ time: 5, temperature: -2.05 }, // Smaller, but still big
+				{ time: 3, temperature: -4.12 },
+			];
+			const expected = [
+				{ time: 1, temperature: -4.23 },
+				{ time: 2, temperature: -4.14 },
+				{ time: 3, temperature: -4.12 },
+				{ time: 6, temperature: -3.12 },
+				{ time: 7, temperature: -3.63 },
+			];
+
+			const actual = removeSpikes(input, 'temperature');
+			should(actual).deepEqual(expected);
+		});
+	});
+});

+ 142 - 0
web-view/src/js/cleanData.ts

@@ -0,0 +1,142 @@
+interface TimeSeries {
+	time:number;
+}
+
+/**
+ * Align data to a window, using the average value within that period.
+ * For example, the value for 13:15:00 will be the average of values from
+ * 13:00:00 to 13:14:59.999
+ *
+ * @param data The data to align, with a `time` field as a unix timestamp, and a
+ *             data field specified in `dataField`.
+ * @param dataField The field containing the data to be averaged.
+ * @param period The number of minutes to group on.
+ */
+export function alignToWindow<T extends TimeSeries>(
+	data:T[],
+	dataField:string,
+	period:number = 15
+):T[] {
+	const sorted = data.sort((a,b) => a.time - b.time);
+
+	const firstTime = new Date(data[0].time),
+		firstMinute = firstTime.getMinutes(),
+		// Gets the period increment before the first time.
+		// eg: For `period = 15, firstMinute = 42` then `nextMinute = 30`
+		nextMinute = firstMinute - (firstMinute % period);
+
+	// Half the period, so events can be gathered +/- this amount
+	const periodMs = period * 60 * 1000,
+		halfPeriodMs = periodMs / 2;
+
+	// Get the initial window time
+	let windowTime:number = new Date(
+			firstTime.getFullYear(), firstTime.getMonth(), firstTime.getDate(),
+			firstTime.getHours(), nextMinute, 0, 0).getTime(),
+		windowEnd = windowTime + halfPeriodMs;
+
+	let windows = [],
+		currNums = [];
+	for(let i = 0; i < sorted.length; i++) {
+		let d = sorted[i];
+
+		// Past the end of the window, belongs to the next window
+		if(d.time > windowEnd) {
+			i--; // Keep it in the next window
+
+			// Don't push the first window if it's empty
+			if(windows.length || currNums.length) {
+				// Push the average, or NaN if there are no elements
+				const w = { time: windowTime };
+				w[dataField] = average(currNums);
+				windows.push(w);
+			}
+
+			// Reset for the next iteration
+			windowTime += periodMs;
+			windowEnd = windowTime + halfPeriodMs;
+			currNums = [];
+		} else {
+			currNums.push(d[dataField]);
+		}
+	}
+
+	// Catch the ending period
+	if(currNums.length) {
+		const w = { time: windowTime };
+		w[dataField] = average(currNums);
+		windows.push(w);
+	}
+
+	return windows;
+}
+
+function average(nums:number[], decimalPlaces:number = 1):number {
+	if(!nums.length)
+		return NaN;
+
+	const total = nums.reduce((prev, curr) => prev + curr, 0);
+	return parseFloat((total / nums.length).toFixed(decimalPlaces));
+}
+
+/**
+ * Remove elements that are particularly "spiky" compared to the ones next to them.
+ *
+ * @param data: The data to remove spikes from, where each object contains a
+ *              data field specified in `dataField`.
+ * @param dataField: The field containing the data to be compared.
+ * @param minChange: The minimum absolute difference between elements before
+ *                   it's considered a spike. eg: a difference of 0.1 is 10x an
+ *                   earlier diff of 0.01, but isn't a spike. A `minChange` of
+ *                   2 would ignore this.
+ * @param minDiffMultiple: The minimum multiplier of differences between sibling
+ *                         elements before it's considered a spike. eg: The
+ *                         previous diff was 1.5*C, current diff is 3.8*C, this
+ *                         is a 2.5x difference. A min multiplier of 3 would
+ *                         ignore this.
+ */
+export function removeSpikes<T extends TimeSeries>(
+	data:T[],
+	dataField:string,
+	minChange:number = 2,
+	minDiffMultiple:number = 3
+):T[] {
+	data = data.sort((a, b) => a.time - b.time);
+
+	const minLength = 5;
+	if(data.length < minLength) {
+		console.warn(`Remove Spikes: Too few elements in array (${data.length}. Minimum is ${minLength}.`);
+		return data;
+	}
+
+	for(let i = 0; i < data.length; i++) {
+		let prevDiff:number, thisDiff:number;
+
+		// General case for most of an array
+		if(i > 2) {
+			prevDiff = Math.abs(data[i - 1][dataField] - data[i - 2][dataField]);
+			thisDiff = Math.abs(data[i][dataField] - data[i - 1][dataField]);
+		} else {
+			// First couple of elements
+			prevDiff = Math.abs(data[i + 1][dataField] - data[i + 2][dataField]);
+			thisDiff = Math.abs(data[i][dataField] - data[i + 1][dataField]);
+		}
+
+		const tooBig = (thisDiff - prevDiff) > minChange
+					&& (thisDiff / prevDiff) > minDiffMultiple;
+		if(tooBig) {
+			data = removeElement(data, i);
+			i--; // Removed the element, make sure we don't skip the next
+		}
+	}
+
+	return data;
+}
+
+function removeElement<T>(arr:Array<T>, index:number):Array<T> {
+	if(index < 0 || index >= arr.length) {
+		throw new Error(`Index ${index} out of bounds; Min 0, max ${arr.length-1}`);
+	}
+
+	return arr.slice(0, index).concat(arr.slice(index+1));
+}

+ 75 - 5
web-view/src/js/main.ts

@@ -1,14 +1,33 @@
 import {firebaseConfig} from './config';
 import {initializeApp} from 'firebase/app';
 import {getDatabase, onValue, orderByChild, query, ref, startAt} from 'firebase/database';
+import { Chart, registerables } from 'chart.js';
+Chart.register(...registerables);
+
+import {testTemperatureData} from './testData';
+import {alignToWindow, removeSpikes} from './cleanData';
+
+type Reading = {
+	temperature: number,
+	humidity: number,
+	light: number,
+	pressure: number,
+	time: Date
+};
 
 const app = initializeApp(firebaseConfig);
 const rtdb = getDatabase(app);
+const data = new Map<Date, Reading>();
 
 document.body.onload = init;
 
+let canvas:HTMLCanvasElement;
+
 async function init() {
-	getRecentData();
+	await getRecentData();
+
+	canvas = document.getElementById('temperature') as HTMLCanvasElement;
+	createChart();
 }
 
 async function getRecentData(hoursAgo:number = 2) {
@@ -21,9 +40,60 @@ async function getRecentData(hoursAgo:number = 2) {
 		startAt(since)
 	);
 
-	onValue(search, snapshot => {
-		snapshot.forEach(child => {
-			console.log(child.val())
-		})
+	return new Promise<void>((res, err) => {
+		let values = [];
+
+		onValue(search, snapshot => {
+			snapshot.forEach(child => {
+				values.push(child.val());
+			});
+
+			console.log(values);
+
+			// TODO: Move this to its own function
+			values = removeSpikes(values, 'temperature');
+			values = alignToWindow(values, 'temperature');
+
+			values.forEach(v => {
+				const t = new Date(v.time);
+				v.time = t;
+				data.set(t, v);
+			});
+
+			console.log(Array.from(data.values())
+				.map(d => {
+					return {
+						time: d.time.getTime(),
+						temperature: d.temperature
+					}
+				}));
+			res();
+		});
 	});
 }
+
+function createChart() {
+	let config:any = {
+		type: 'line',
+		data: {
+			labels: Array.from(data.keys())
+				.sort((a,b) => a.getTime() - b.getTime())
+				.map(formatTime),
+			datasets: [{
+				data: Array.from(data.values()).map(d => d.temperature),
+				backgroundColor: 'rgb(255, 99, 132)', // Colour of the dots
+				borderColor: 'rgb(255, 99, 132)' // Colour of the line
+			}]
+		},
+		options: {}
+	};
+	console.log(config);
+	const chart = new Chart(canvas, config);
+}
+
+function formatTime(date:Date):string {
+	return `${pad(date.getHours())}:${pad(date.getMinutes())}`;
+	function pad(num:number):string {
+		return num < 10 ? `0${num}` : num.toString();
+	}
+}

+ 144 - 0
web-view/src/js/testData.ts

@@ -0,0 +1,144 @@
+export const testTemperatureData = [
+	{
+		// 2021-12-23 17:00:12 EST
+		"time": 1640296812283,  // 2021-12-23 22:00:12 UTC
+		"temperature": -4.21
+	},
+	{
+		"time": 1640297111475,  // 2021-12-23 22:05:11 UTC
+		"temperature": -4.28
+	},
+	{
+		"time": 1640297410320,  // 2021-12-23 22:10:10 UTC
+		"temperature": -4.29
+	},
+	{
+		"time": 1640297709214,  // 2021-12-23 22:15:09 UTC
+		"temperature": -4.34
+	},
+	{
+		"time": 1640298008202,  // 2021-12-23 22:20:08 UTC
+		"temperature": -4.38
+	},
+	{
+		"time": 1640298307129,  // 2021-12-23 22:25:07 UTC
+		"temperature": -4.42
+	},
+	{
+		"time": 1640298606065,  // 2021-12-23 22:30:06 UTC
+		"temperature": -4.4
+	},
+	{
+		"time": 1640298905167,  // 2021-12-23 22:35:05 UTC
+		"temperature": -4.27
+	},
+	{
+		"time": 1640299204040,  // 2021-12-23 22:40:04 UTC
+		"temperature": -4.27
+	},
+	{
+		"time": 1640299502889,  // 2021-12-23 22:45:02 UTC
+		"temperature": -4.19
+	},
+	{
+		"time": 1640299801914,  // 2021-12-23 22:50:01 UTC
+		"temperature": -4.27
+	},
+	{
+		"time": 1640300101062,  // 2021-12-23 22:55:01 UTC
+		"temperature": -4.15
+	},
+	{
+		"time": 1640300400244,  // 2021-12-23 23:00:00 UTC
+		"temperature": -4.14
+	},
+	{
+		"time": 1640300699251,  // 2021-12-23 23:04:59 UTC
+		"temperature": -4.22
+	},
+	{
+		"time": 1640300998345,  // 2021-12-23 23:09:58 UTC
+		"temperature": -4.21
+	},
+	{
+		"time": 1640301297811,  // 2021-12-23 23:14:57 UTC
+		"temperature": -4.23
+	},
+	{
+		"time": 1640301596323,  // 2021-12-23 23:19:56 UTC
+		"temperature": -4.14
+	},
+	{
+		"time": 1640301917376,  // 2021-12-23 23:25:17 UTC
+		"temperature": -4.12
+	},
+	{
+		"time": 1640302550740,  // 2021-12-23 23:35:50 UTC
+		"temperature": 3.65
+	},
+	{
+		"time": 1640302829882,  // 2021-12-23 23:40:29 UTC
+		"temperature": -2.05
+	},
+	{
+		"time": 1640303128543,  // 2021-12-23 23:45:28 UTC
+		"temperature": -3.12
+	},
+	{
+		"time": 1640303427668,  // 2021-12-23 23:50:27 UTC
+		"temperature": -3.63
+	},
+	{
+		"time": 1640303726124,  // 2021-12-23 23:55:26 UTC
+		"temperature": -3.79
+	},
+	{
+		"time": 1640304024921,  // 2021-12-24 00:00:24 UTC
+		"temperature": -3.86
+	},
+	{
+		"time": 1640304324159,  // 2021-12-24 00:05:24 UTC
+		"temperature": -3.95
+	},
+	{
+		"time": 1640304622979,  // 2021-12-24 00:10:22 UTC
+		"temperature": -4
+	},
+	{
+		"time": 1640304921711,  // 2021-12-24 00:15:21 UTC
+		"temperature": -3.97
+	},
+	{
+		"time": 1640305220695,  // 2021-12-24 00:20:20 UTC
+		"temperature": -4.03
+	},
+	{
+		"time": 1640305519650,  // 2021-12-24 00:25:19 UTC
+		"temperature": -3.92
+	},
+	{
+		"time": 1640305818511,  // 2021-12-24 00:30:18 UTC
+		"temperature": -3.85
+	},
+	{
+		"time": 1640306117558,  // 2021-12-24 00:35:17 UTC
+		"temperature": -3.88
+	},
+	{
+		"time": 1640306418615,  // 2021-12-24 00:40:18 UTC
+		"temperature": -3.89
+	},
+	{
+		"time": 1640306715454,  // 2021-12-24 00:45:15 UTC
+		"temperature": -3.79
+	},
+	{
+		"time": 1640307014557,  // 2021-12-24 00:50:14 UTC
+		"temperature": -3.82
+	},
+	{
+		// 2021-12-23 19:55:13 EST
+		"time": 1640307313521,  // 2021-12-24 00:55:13 UTC
+		"temperature": -3.8
+	}
+];

+ 1 - 1
web-view/tsconfig.json

@@ -1,6 +1,6 @@
 {
 	"compilerOptions": {
-		"module": "es6",
+		"module": "commonjs",
 		"target": "es5",
 		"sourceMap": true,
 		"outDir": "./build/js/",

+ 1 - 1
web-view/webpack.config.js

@@ -10,7 +10,7 @@ module.exports = {
 			{
 				test: /\.ts$/,
 				use: 'ts-loader',
-				exclude: /node_modules/
+				exclude: /(node_modules|\.test\.ts$)/
 			}
 		]
 	},