diff --git a/package-lock.json b/package-lock.json index 5ad564d..f427042 100644 --- a/package-lock.json +++ b/package-lock.json @@ -27,6 +27,7 @@ "@tauri-apps/plugin-store": "^2.4.0", "chart.js": "^4.4.3", "chartjs-adapter-date-fns": "^3.0.0", + "chartjs-plugin-annotation": "^3.1.0", "date-fns": "^4.1.0", "rxjs": "~7.8.0", "tslib": "^2.3.0" @@ -4963,6 +4964,15 @@ "date-fns": ">=2.0.0" } }, + "node_modules/chartjs-plugin-annotation": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/chartjs-plugin-annotation/-/chartjs-plugin-annotation-3.1.0.tgz", + "integrity": "sha512-EkAed6/ycXD/7n0ShrlT1T2Hm3acnbFhgkIEJLa0X+M6S16x0zwj1Fv4suv/2bwayCT3jGPdAtI9uLcAMToaQQ==", + "license": "MIT", + "peerDependencies": { + "chart.js": ">=4.0.0" + } + }, "node_modules/chokidar": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz", diff --git a/package.json b/package.json index 3c9e957..c5f1eab 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ "@tauri-apps/plugin-store": "^2.4.0", "chart.js": "^4.4.3", "chartjs-adapter-date-fns": "^3.0.0", + "chartjs-plugin-annotation": "^3.1.0", "date-fns": "^4.1.0", "rxjs": "~7.8.0", "tslib": "^2.3.0" diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index a401daa..fd17859 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -155,9 +155,9 @@ dependencies = [ [[package]] name = "arrayvec" -version = "0.7.6" +version = "0.7.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" +checksum = "f02882884d3e1bc524fb12c79f107f6ad0e1cfd498c536ffb494301740995dfe" [[package]] name = "async-broadcast" @@ -384,9 +384,9 @@ dependencies = [ [[package]] name = "bitvec" -version = "1.0.1" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" +checksum = "ddcec3d12c579d40898fe0a9a358a803c23e9c52ca3c425707f81c9436211837" dependencies = [ "funty", "radium", @@ -436,9 +436,9 @@ dependencies = [ [[package]] name = "borsh" -version = "1.6.1" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfd1e3f8955a5d7de9fab72fc8373fade9fb8a703968cb200ae3dc6cf08e185a" +checksum = "2f3f6da4992df95bbcd9af42a6c7dcb994498fc9048230405f3b36ff7cd3f145" dependencies = [ "borsh-derive", "bytes", @@ -447,9 +447,9 @@ dependencies = [ [[package]] name = "borsh-derive" -version = "1.6.1" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfcfdc083699101d5a7965e49925975f2f55060f94f9a05e7187be95d530ca59" +checksum = "3ae8fb4fb5740e4b2c4884ff95f5f32f5e8479db1e8fd8eb49ddbe09eb09bb7c" dependencies = [ "once_cell", "proc-macro-crate 3.5.0", @@ -542,9 +542,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.11.1" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" +checksum = "8ae3f5d315924270530207e2a68396c3cc547f6dca3fbdca317cfb1a51edb593" dependencies = [ "serde", ] @@ -576,9 +576,9 @@ dependencies = [ [[package]] name = "camino" -version = "1.2.2" +version = "1.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e629a66d692cb9ff1a1c664e41771b3dcaf961985a9774c0eb0bd1b51cf60a48" +checksum = "b4ce8d3bd5823c7504d3f579f13e7b2f3da252fcb938c594d5680ee508bf846f" dependencies = [ "serde_core", ] @@ -627,9 +627,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.64" +version = "1.2.65" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dad887fd958be91b5098c0248def011f4523ab786cd411be668777e55063501f" +checksum = "e228eec9be7c17ccb640b59b36a5cd805ea2a564a4c5e162c2f659fea30d3b96" dependencies = [ "find-msvc-tools", "shlex", @@ -897,9 +897,9 @@ checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" [[package]] name = "crypto-common" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" dependencies = [ "generic-array", "typenum", @@ -1785,9 +1785,9 @@ dependencies = [ [[package]] name = "generic-array" -version = "0.14.9" +version = "0.14.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bb6743198531e02858aeaea5398fcc883e71851fcbcb5a2f773e2fb6cb1edf2" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ "typenum", "version_check", @@ -1818,15 +1818,13 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.4.2" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" dependencies = [ "cfg-if", "libc", "r-efi 6.0.0", - "wasip2", - "wasip3", ] [[package]] @@ -2369,12 +2367,6 @@ dependencies = [ "zerovec", ] -[[package]] -name = "id-arena" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" - [[package]] name = "ident_case" version = "1.0.1" @@ -2610,12 +2602,6 @@ dependencies = [ "spin", ] -[[package]] -name = "leb128fmt" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" - [[package]] name = "libappindicator" version = "0.9.0" @@ -2717,9 +2703,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.32" +version = "0.4.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "953f07c43838f8e6f9758cab68bf5bed85465e7587ebe0b823f1bcd81978ad3a" +checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" dependencies = [ "value-bag", ] @@ -3587,16 +3573,6 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" -[[package]] -name = "prettyplease" -version = "0.2.37" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" -dependencies = [ - "proc-macro2", - "syn 2.0.118", -] - [[package]] name = "proc-macro-crate" version = "1.3.1" @@ -3690,9 +3666,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.45" +version = "1.0.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368" dependencies = [ "proc-macro2", ] @@ -4056,9 +4032,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.40" +version = "0.23.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b" +checksum = "6b92b125634d9b795e7beca796cc790df15a7fb38323bf3196fda83292d06b1f" dependencies = [ "once_cell", "rustls-pki-types", @@ -5059,9 +5035,9 @@ checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" [[package]] name = "tauri" -version = "2.11.2" +version = "2.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "437404997acf375d85f1177afa7e11bb971f274ed6a7b83a2a3e339015f4cc28" +checksum = "c2616f96cb644bf2c5c456d9de4d5d5100e592d7424c74d8b55c5cb96e359e93" dependencies = [ "anyhow", "bytes", @@ -5110,9 +5086,9 @@ dependencies = [ [[package]] name = "tauri-build" -version = "2.6.2" +version = "2.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4aa1f9055fc23919a54e4e125052bed16ed04aef0487086e758fe01a67b451c7" +checksum = "bc9ce40b16101cb6ea63d3e221567affd1c3a9205f95d7bc574941a10636b632" dependencies = [ "anyhow", "cargo_toml", @@ -5131,9 +5107,9 @@ dependencies = [ [[package]] name = "tauri-codegen" -version = "2.6.2" +version = "2.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4a0319528a025a38c4078e7dae2c446f4e63620ddb0659a643ede1cb38f90e9" +checksum = "08279169ff42f8fc45a1dbc9dcae888893ba95288142e5880c59b93a26d2cfc5" dependencies = [ "base64 0.22.1", "brotli", @@ -5158,9 +5134,9 @@ dependencies = [ [[package]] name = "tauri-macros" -version = "2.6.2" +version = "2.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae6cb4e3896c21d2f6da5b31251d2faea0153bba56ed0e970f918115dbee4924" +checksum = "e8b394794f399a421811d06966343e7933fcae92d59f5180b9388d1174497a45" dependencies = [ "heck 0.5.0", "proc-macro2", @@ -5172,9 +5148,9 @@ dependencies = [ [[package]] name = "tauri-plugin" -version = "2.6.2" +version = "2.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e126abc9e84e35cdfd01596140a73a1850cdb0df0a23acf0185776c30b469a6e" +checksum = "74be5dd4bed9afbd145e5716b5fa2ec28cbc29c34ffa61c258c9273d896c8020" dependencies = [ "anyhow", "glob", @@ -5310,9 +5286,9 @@ dependencies = [ [[package]] name = "tauri-runtime" -version = "2.11.2" +version = "2.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48222d7116c8807eaa6fe2f372e023fae125084e61e6eca6d70b7961cdf129ef" +checksum = "b0b4bc95aed361b0019067d189a1174a603d460d0f6c72606512d59fc9c12ec8" dependencies = [ "cookie", "dpi", @@ -5335,9 +5311,9 @@ dependencies = [ [[package]] name = "tauri-runtime-wry" -version = "2.11.2" +version = "2.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b83849ee63ecb27a8e8d0fe51915ca215076914aca43f96db1179f0f415f6cd9" +checksum = "fe41e015bf8fc4d6477ff4926a0ef769dc64ff34c7b0038b6f7cacae892acb5c" dependencies = [ "gtk", "http", @@ -5361,9 +5337,9 @@ dependencies = [ [[package]] name = "tauri-utils" -version = "2.9.2" +version = "2.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "092379df9a707631978e6c56b1bc2401d387f01e2d4a3c123360d167bbb9aa95" +checksum = "3e176a18e67764923c4f1ce66f25ae4abe5f688384d5eb1a0fa6c77f3d90f887" dependencies = [ "anyhow", "brotli", @@ -5415,7 +5391,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", - "getrandom 0.4.2", + "getrandom 0.4.3", "once_cell", "rustix", "windows-sys 0.61.2", @@ -5473,9 +5449,9 @@ dependencies = [ [[package]] name = "time" -version = "0.3.49" +version = "0.3.51" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "711a53c2d47bbd818258c498c8dbfe186a2526c631495cfe7e078567f86b8469" +checksum = "85c17d80feb7334b40c484e45ed1a5273dfd8bfda537c3be2e74a06a6686f327" dependencies = [ "deranged", "libc", @@ -5495,9 +5471,9 @@ checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" [[package]] name = "time-macros" -version = "0.2.29" +version = "0.2.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71c652a3727a9cbb9a02f707f530b618ce00d0ccd762009c8c23bd191df3c17d" +checksum = "dcef1a61bdb119096e153208ec5cbec23944ce8bca13be5c7f60c634f7403935" dependencies = [ "num-conv", "time-core", @@ -5800,9 +5776,9 @@ dependencies = [ [[package]] name = "tray-icon" -version = "0.23.1" +version = "0.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15edbb0d80583e85ee8df283410038e17314df5cba30da2087a54a85216c0773" +checksum = "65ba1e5f6b9ef9fd87e21b9c6f351554dbd717960089168fcfdef854686961dc" dependencies = [ "crossbeam-channel", "dirs", @@ -5923,12 +5899,6 @@ version = "1.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" -[[package]] -name = "unicode-xid" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" - [[package]] name = "untrusted" version = "0.9.0" @@ -5990,7 +5960,7 @@ version = "1.23.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "144d6b123cef80b301b8f72a9e2ca4370ddec21950d0a103dd22c437006d2db7" dependencies = [ - "getrandom 0.4.2", + "getrandom 0.4.3", "js-sys", "serde_core", "wasm-bindgen", @@ -6071,16 +6041,7 @@ version = "1.0.4+wasi-0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" dependencies = [ - "wit-bindgen 0.57.1", -] - -[[package]] -name = "wasip3" -version = "0.4.0+wasi-0.3.0-rc-2026-01-06" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" -dependencies = [ - "wit-bindgen 0.51.0", + "wit-bindgen", ] [[package]] @@ -6145,28 +6106,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "wasm-encoder" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" -dependencies = [ - "leb128fmt", - "wasmparser", -] - -[[package]] -name = "wasm-metadata" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" -dependencies = [ - "anyhow", - "indexmap 2.14.0", - "wasm-encoder", - "wasmparser", -] - [[package]] name = "wasm-streams" version = "0.5.0" @@ -6180,18 +6119,6 @@ dependencies = [ "web-sys", ] -[[package]] -name = "wasmparser" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" -dependencies = [ - "bitflags 2.13.0", - "hashbrown 0.15.5", - "indexmap 2.14.0", - "semver", -] - [[package]] name = "web-sys" version = "0.3.102" @@ -6204,9 +6131,9 @@ dependencies = [ [[package]] name = "web_atoms" -version = "0.2.4" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7cff6eef815df1834fd250e3a2ff436044d82a9f1bc1980ca1dbdf07effc538" +checksum = "075474b12bcb3d2e3d4546580e9de478eeeead668a1761e2a8860c836b7ef297" dependencies = [ "phf", "phf_codegen", @@ -6862,100 +6789,12 @@ dependencies = [ "windows-sys 0.59.0", ] -[[package]] -name = "wit-bindgen" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" -dependencies = [ - "wit-bindgen-rust-macro", -] - [[package]] name = "wit-bindgen" version = "0.57.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" -[[package]] -name = "wit-bindgen-core" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" -dependencies = [ - "anyhow", - "heck 0.5.0", - "wit-parser", -] - -[[package]] -name = "wit-bindgen-rust" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" -dependencies = [ - "anyhow", - "heck 0.5.0", - "indexmap 2.14.0", - "prettyplease", - "syn 2.0.118", - "wasm-metadata", - "wit-bindgen-core", - "wit-component", -] - -[[package]] -name = "wit-bindgen-rust-macro" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" -dependencies = [ - "anyhow", - "prettyplease", - "proc-macro2", - "quote", - "syn 2.0.118", - "wit-bindgen-core", - "wit-bindgen-rust", -] - -[[package]] -name = "wit-component" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" -dependencies = [ - "anyhow", - "bitflags 2.13.0", - "indexmap 2.14.0", - "log", - "serde", - "serde_derive", - "serde_json", - "wasm-encoder", - "wasm-metadata", - "wasmparser", - "wit-parser", -] - -[[package]] -name = "wit-parser" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" -dependencies = [ - "anyhow", - "id-arena", - "indexmap 2.14.0", - "log", - "semver", - "serde", - "serde_derive", - "serde_json", - "unicode-xid", - "wasmparser", -] - [[package]] name = "writeable" version = "0.6.3" diff --git a/src/app/app.config.ts b/src/app/app.config.ts index 17198cb..80f1210 100644 --- a/src/app/app.config.ts +++ b/src/app/app.config.ts @@ -1,3 +1,5 @@ +import { Chart } from 'chart.js'; +import annotationPlugin from 'chartjs-plugin-annotation'; import { enGB } from 'date-fns/locale'; import { @@ -8,6 +10,7 @@ import { import { ApplicationConfig, ErrorHandler, + provideEnvironmentInitializer, provideZonelessChangeDetection, } from '@angular/core'; import { provideDateFnsAdapter } from '@angular/material-date-fns-adapter'; @@ -27,5 +30,8 @@ export const appConfig: ApplicationConfig = { { provide: MAT_DATE_LOCALE, useValue: enGB }, provideDateFnsAdapter(), { provide: ErrorHandler, useClass: GlobalErrorHandler }, + provideEnvironmentInitializer(() => { + Chart.register(annotationPlugin); + }), ], }; diff --git a/src/app/app.routes.ts b/src/app/app.routes.ts index 895a13e..ef98be5 100644 --- a/src/app/app.routes.ts +++ b/src/app/app.routes.ts @@ -4,6 +4,7 @@ import { AboutComponent } from './components/about/about.component'; import { BrightSettingsComponent } from './components/bright-settings/bright-settings.component'; import { DataManagementSettingsComponent } from './components/data-management-settings/data-management-settings.component'; import { ElectricityConsumptionChartComponent } from './components/electricity-consumption-chart/electricity-consumption-chart.component'; +import { ElectricityDailyPaceComponent } from './components/electricity-daily-pace/electricity-daily-pace.component'; import { ElectricityTariffHistoryComponent } from './components/electricity-tariff-history/electricity-tariff-history.component'; import { EnergyCostHistoryComponent } from './components/energy-cost-history/energy-cost-history.component'; import { GasConsumptionChartComponent } from './components/gas-consumption-chart/gas-consumption-chart.component'; @@ -21,6 +22,10 @@ export const routes: Routes = [ { path: 'electricity', component: ElectricityConsumptionChartComponent }, { path: 'gas', component: GasConsumptionChartComponent }, { path: 'electricity/usage-profile', component: LoadDurationComponent }, + { + path: 'electricity/daily-pace', + component: ElectricityDailyPaceComponent, + }, { path: 'electricity/tariff', component: ElectricityTariffHistoryComponent }, { path: 'electricity/costs', diff --git a/src/app/components/chart/chart.component.html b/src/app/components/chart/chart.component.html index 012006b..d7441c1 100644 --- a/src/app/components/chart/chart.component.html +++ b/src/app/components/chart/chart.component.html @@ -1,3 +1,3 @@
- +
diff --git a/src/app/components/chart/chart.component.ts b/src/app/components/chart/chart.component.ts index 1793c54..5acadbb 100644 --- a/src/app/components/chart/chart.component.ts +++ b/src/app/components/chart/chart.component.ts @@ -3,10 +3,13 @@ import Chart from 'chart.js/auto'; import { ChangeDetectionStrategy, Component, + ElementRef, HostListener, OnDestroy, effect, input, + output, + viewChild, } from '@angular/core'; @Component({ @@ -17,17 +20,19 @@ import { changeDetection: ChangeDetectionStrategy.OnPush, }) export class ChartComponent implements OnDestroy { - public chartConfiguration = input(); + public readonly chartConfiguration = input(); + public readonly chartInstanceChange = output(); + private readonly canvasRef = + viewChild.required>('canvas'); - public chart?: Chart; + private chart?: Chart; public constructor() { effect(() => { + const canvasRef = this.canvasRef(); const config = this.chartConfiguration(); - if (config) { - this.chart?.destroy(); - this.chart = new Chart('canvas', config as any); - } + + this.createNewChartInstance(canvasRef, config); }); } @@ -35,10 +40,27 @@ export class ChartComponent implements OnDestroy { this.chart?.destroy(); } - @HostListener('window:resize', ['$event']) - public onResize(_event: Event): void { + @HostListener('window:resize') + public onResize(): void { + const canvasRef = this.canvasRef(); + if (!canvasRef?.nativeElement?.ownerDocument) { + return; + } + // To avoid a created chart from overflowing, recreate it so it fits the parent + this.createNewChartInstance(canvasRef, this.chartConfiguration()); + } + + private createNewChartInstance( + canvasRef: ElementRef, + config: any, + ): void { + if (!canvasRef || !config) { + return; + } + this.chart?.destroy(); - this.chart = new Chart('canvas', this.chartConfiguration() as any); + this.chart = new Chart(canvasRef.nativeElement, config); + this.chartInstanceChange.emit(this.chart); } } diff --git a/src/app/components/electricity-daily-pace/electricity-daily-pace.component.html b/src/app/components/electricity-daily-pace/electricity-daily-pace.component.html new file mode 100644 index 0000000..e9cbb77 --- /dev/null +++ b/src/app/components/electricity-daily-pace/electricity-daily-pace.component.html @@ -0,0 +1,36 @@ +
+
+
+
+

Daily pace

+
+
+ + Date + + + + DD/MM/YYYY + + + +
+
+ + @if (chartConfiguration()) { + + } +
+
diff --git a/src/app/components/electricity-daily-pace/electricity-daily-pace.component.scss b/src/app/components/electricity-daily-pace/electricity-daily-pace.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/src/app/components/electricity-daily-pace/electricity-daily-pace.component.spec.ts b/src/app/components/electricity-daily-pace/electricity-daily-pace.component.spec.ts new file mode 100644 index 0000000..215153c --- /dev/null +++ b/src/app/components/electricity-daily-pace/electricity-daily-pace.component.spec.ts @@ -0,0 +1,22 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ElectricityDailyPaceComponent } from './electricity-daily-pace.component'; + +describe('ElectricityDailyPaceComponent', () => { + let component: ElectricityDailyPaceComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ElectricityDailyPaceComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(ElectricityDailyPaceComponent); + component = fixture.componentInstance; + await fixture.whenStable(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/components/electricity-daily-pace/electricity-daily-pace.component.ts b/src/app/components/electricity-daily-pace/electricity-daily-pace.component.ts new file mode 100644 index 0000000..ec90987 --- /dev/null +++ b/src/app/components/electricity-daily-pace/electricity-daily-pace.component.ts @@ -0,0 +1,281 @@ +import { Chart, ChartConfiguration } from 'chart.js'; +import { addDays, format, set, startOfToday } from 'date-fns'; + +import { + Component, + OnDestroy, + Signal, + afterRenderEffect, + computed, + inject, + resource, + signal, +} from '@angular/core'; +import { FormField, form } from '@angular/forms/signals'; +import { MatDatepickerModule } from '@angular/material/datepicker'; +import { MatFormFieldModule } from '@angular/material/form-field'; +import { MatInputModule } from '@angular/material/input'; + +import { UnlistenFn, listen } from '@tauri-apps/api/event'; + +import { + ElectricityConsumption, + ElectricityConsumptionService, +} from '../../services/electricity-consumption/electricity-consumption.service'; +import { ChartComponent } from '../chart/chart.component'; + +interface InputParams { + date: Date; +} + +interface ChartPoint { + x: Date; + y: number; +} + +type MyChart = ChartConfiguration<'line', ChartPoint[]>; + +@Component({ + selector: 'app-electricity-daily-pace-component', + imports: [ + ChartComponent, + MatInputModule, + MatFormFieldModule, + MatDatepickerModule, + FormField, + ], + templateUrl: './electricity-daily-pace.component.html', + styleUrl: './electricity-daily-pace.component.scss', +}) +export class ElectricityDailyPaceComponent implements OnDestroy { + private readonly dateToNormalizeTo = startOfToday(); + + private readonly electricityConsumptionService = inject( + ElectricityConsumptionService, + ); + + private readonly inputParams = signal({ + date: addDays(startOfToday(), -1), + }); + + protected readonly chartInstance = signal(undefined); + + private electricityUpdateUnlistenFn?: UnlistenFn; + + private readonly totalKwhUsedToday = signal< + { timestamp: Date; electricityConsumptionKwh: number } | undefined + >(undefined); + + protected readonly inputParamsForm = form(this.inputParams); + + public readonly chartConfiguration: Signal; + + public constructor() { + const chartConfigurationResource = resource({ + params: () => ({ date: this.inputParamsForm.date().value() }), + loader: ({ params }) => this.getChartConfiguration(params.date), + }); + + this.chartConfiguration = computed(() => { + if (!chartConfigurationResource.hasValue()) { + return undefined; + } + return chartConfigurationResource.value(); + }); + + afterRenderEffect({ + write: () => { + const chartConfig = this.chartConfiguration(); + const liveData = this.totalKwhUsedToday(); + const chartInstance = this.chartInstance(); + + if (chartConfig && liveData && chartInstance) { + const plugins = chartInstance.options?.plugins || {}; + + plugins.annotation = { + annotations: this.getAnnotationsChartConfig( + liveData.timestamp, + liveData.electricityConsumptionKwh, + ) as any, + }; + + chartInstance.update('none'); + } + }, + }); + + this.listenForRealTimeData(); + } + + public ngOnDestroy(): void { + this.electricityUpdateUnlistenFn?.(); + } + + private listenForRealTimeData() { + listen('electricityUpdate', (message) => { + const lastUpdated = new Date(message.payload.electricitymeter.timestamp); + const energy = message.payload.electricitymeter.energy; + + if (energy.import.units !== 'kWh') { + console.warn('units are incorrect: ' + energy.import.units); + } + + this.totalKwhUsedToday.set({ + timestamp: this.normalizeTimestamp(lastUpdated), + electricityConsumptionKwh: energy.import.day, + }); + }).then((unlisten) => { + this.electricityUpdateUnlistenFn = unlisten; + }); + } + + private async getChartConfiguration( + date: Date, + ): Promise> { + const datasets = await Promise.all([ + (async () => { + const data = + await this.electricityConsumptionService.getRawElectricityConsumption( + date, + date, + ); + + const cumulativeSeries = this.normalizeTimestamps( + this.createCumulativeSeriesFromRawConsumption(data), + ); + + const label = format(date, 'eee d MMM'); + return { label, data: cumulativeSeries }; + })(), + this.getDatasetForOtherDay(-1, date), + this.getDatasetForOtherDay(-7, date), + ]); + + return this.createChartConfiguration(datasets); + } + + private async getDatasetForOtherDay(dayDelta: number, referenceDate: Date) { + const otherDate = addDays(referenceDate, dayDelta); + + const label = format(otherDate, 'eee d MMM'); + + const nextData = + await this.electricityConsumptionService.getRawElectricityConsumption( + otherDate, + otherDate, + ); + + return { + label, + data: this.normalizeTimestamps( + this.createCumulativeSeriesFromRawConsumption(nextData), + ), + }; + } + + private createCumulativeSeriesFromRawConsumption( + series: ElectricityConsumption[], + ) { + series.sort((a, b) => a.timestamp.getTime() - b.timestamp.getTime()); + + let result: ChartPoint[] = []; + + const cumulativeSeries = series.reduce( + (acc, current) => { + const newTotal = acc.sum + current.electricityConsumptionKwh; + acc.result.push({ + x: current.timestamp, + y: newTotal, + }); + return { result, sum: newTotal }; + }, + { result, sum: 0 }, + ); + + return cumulativeSeries.result; + } + + private createChartConfiguration( + datasets: { label: string; data: ChartPoint[] }[], + ): ChartConfiguration<'line', ChartPoint[]> { + return { + type: 'line', + data: { + datasets, + }, + options: { + responsive: true, + maintainAspectRatio: false, + scales: { + x: { + type: 'time', + time: { + unit: 'minute', + displayFormats: { + minute: 'HH:mm', + }, + tooltipFormat: 'HH:mm', + }, + title: { + display: true, + text: 'Time', + }, + }, + y: { + beginAtZero: true, + title: { + display: true, + text: 'Cumulative Energy Consumption (kWh)', + }, + }, + }, + }, + }; + } + + private getAnnotationsChartConfig(now: Date, totalConsumptionKwh: number) { + return { + todayProgressBox: { + type: 'box', + xMin: this.normalizeTimestamp(startOfToday()).getTime(), + xMax: now.getTime(), + + yMin: 0, + yMax: totalConsumptionKwh, + + backgroundColor: 'rgba(0, 230, 118, 0.15)', + borderColor: 'rgba(0, 230, 118, 0.8)', + borderWidth: 2, + borderDash: [4, 4], + label: { + display: true, + content: `Today: ${totalConsumptionKwh} kWh`, + position: { + x: 'end', + y: 'end', + }, + font: { + size: 11, + weight: 'bold', + }, + }, + }, + }; + } + + private normalizeTimestamps(series: ChartPoint[]): ChartPoint[] { + return series.map((p) => ({ + x: this.normalizeTimestamp(p.x), + y: p.y, + })); + } + + private normalizeTimestamp(date: Date): Date { + return set(this.dateToNormalizeTo, { + hours: date.getHours(), + minutes: date.getMinutes(), + seconds: date.getSeconds(), + milliseconds: 0, + }); + } +} diff --git a/src/app/components/navigation-bar/navigation-bar.component.html b/src/app/components/navigation-bar/navigation-bar.component.html index 22ef353..f71d75f 100644 --- a/src/app/components/navigation-bar/navigation-bar.component.html +++ b/src/app/components/navigation-bar/navigation-bar.component.html @@ -13,6 +13,9 @@

Smart Energy Explorer

+