diff --git a/angular.json b/angular.json
index 065174f..798cd85 100644
--- a/angular.json
+++ b/angular.json
@@ -92,5 +92,8 @@
}
}
}
+ },
+ "cli": {
+ "analytics": false
}
}
diff --git a/package-lock.json b/package-lock.json
index ae79ec3..7b79d01 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -15,6 +15,8 @@
"@angular/platform-browser": "^19.2.0",
"@angular/platform-browser-dynamic": "^19.2.0",
"@angular/router": "^19.2.0",
+ "chart.js": "^4.4.9",
+ "ng2-charts": "^8.0.0",
"rxjs": "~7.8.0",
"tslib": "^2.3.0",
"zone.js": "~0.15.0"
@@ -493,6 +495,22 @@
"node": "^10 || ^12 || >=14"
}
},
+ "node_modules/@angular/cdk": {
+ "version": "19.2.15",
+ "resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-19.2.15.tgz",
+ "integrity": "sha512-srM0O4oVPvMIbv1m+fU3D0X2ZK0Q7raggCD7jcb+d+pXoPESqI91Hn8FIhA2OCbw0vJWJ/Mly8lskmIb8RNLcQ==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "parse5": "^7.1.2",
+ "tslib": "^2.3.0"
+ },
+ "peerDependencies": {
+ "@angular/common": "^19.0.0 || ^20.0.0",
+ "@angular/core": "^19.0.0 || ^20.0.0",
+ "rxjs": "^6.5.3 || ^7.4.0"
+ }
+ },
"node_modules/@angular/cli": {
"version": "19.2.7",
"resolved": "https://registry.npmjs.org/@angular/cli/-/cli-19.2.7.tgz",
@@ -3320,6 +3338,12 @@
"tslib": "2"
}
},
+ "node_modules/@kurkle/color": {
+ "version": "0.3.4",
+ "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.4.tgz",
+ "integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==",
+ "license": "MIT"
+ },
"node_modules/@leichtgewicht/ip-codec": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz",
@@ -4695,6 +4719,21 @@
"linux"
]
},
+ "node_modules/@rollup/rollup-linux-riscv64-musl": {
+ "version": "4.40.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.40.2.tgz",
+ "integrity": "sha512-5W6vNYkhgfh7URiXTO1E9a0cy4fSgfE4+Hl5agb/U1sa0kjOLMLC1wObxwKxecE17j0URxuTrYZZME4/VH57Hg==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "peer": true
+ },
"node_modules/@rollup/rollup-linux-s390x-gnu": {
"version": "4.34.8",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.34.8.tgz",
@@ -6237,6 +6276,18 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/chart.js": {
+ "version": "4.4.9",
+ "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.4.9.tgz",
+ "integrity": "sha512-EyZ9wWKgpAU0fLJ43YAEIF8sr5F2W3LqbS40ZJyHIner2lY14ufqv2VMp69MAiZ2rpwxEUxEhIH/0U3xyRynxg==",
+ "license": "MIT",
+ "dependencies": {
+ "@kurkle/color": "^0.3.0"
+ },
+ "engines": {
+ "pnpm": ">=8"
+ }
+ },
"node_modules/chokidar": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
@@ -7255,7 +7306,6 @@
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
"integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
- "dev": true,
"license": "BSD-2-Clause",
"engines": {
"node": ">=0.12"
@@ -7733,9 +7783,9 @@
}
},
"node_modules/fdir": {
- "version": "6.4.3",
- "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.3.tgz",
- "integrity": "sha512-PMXmW2y1hDDfTSRc9gaXIuCCRpuoz3Kaz8cUelp3smouvfT632ozg2vrT6lJsHKKOF59YLbOGfAWGUcKEfRMQw==",
+ "version": "6.4.4",
+ "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.4.tgz",
+ "integrity": "sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==",
"dev": true,
"license": "MIT",
"peerDependencies": {
@@ -9749,6 +9799,12 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/lodash-es": {
+ "version": "4.17.21",
+ "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz",
+ "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==",
+ "license": "MIT"
+ },
"node_modules/lodash.debounce": {
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz",
@@ -10472,6 +10528,24 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/ng2-charts": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/ng2-charts/-/ng2-charts-8.0.0.tgz",
+ "integrity": "sha512-nofsNHI2Zt+EAwT+BJBVg0kgOhNo9ukO4CxULlaIi7VwZSr7I1km38kWSoU41Oq6os6qqIh5srnL+CcV+RFPFA==",
+ "license": "MIT",
+ "dependencies": {
+ "lodash-es": "^4.17.15",
+ "tslib": "^2.3.0"
+ },
+ "peerDependencies": {
+ "@angular/cdk": ">=19.0.0",
+ "@angular/common": ">=19.0.0",
+ "@angular/core": ">=19.0.0",
+ "@angular/platform-browser": ">=19.0.0",
+ "chart.js": "^3.4.0 || ^4.0.0",
+ "rxjs": "^6.5.3 || ^7.4.0"
+ }
+ },
"node_modules/node-addon-api": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-6.1.0.tgz",
@@ -11146,7 +11220,6 @@
"version": "7.2.1",
"resolved": "https://registry.npmjs.org/parse5/-/parse5-7.2.1.tgz",
"integrity": "sha512-BuBYQYlv1ckiPdQi/ohiivi9Sagc9JG+Ozs0r7b/0iK3sKmrb0b9FdWdBbOdx6hBCM/F9Ir82ofnBhtZOjCRPQ==",
- "dev": true,
"license": "MIT",
"dependencies": {
"entities": "^4.5.0"
@@ -13270,13 +13343,13 @@
"license": "MIT"
},
"node_modules/tinyglobby": {
- "version": "0.2.12",
- "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.12.tgz",
- "integrity": "sha512-qkf4trmKSIiMTs/E63cxH+ojC2unam7rJ0WrauAzpT3ECNTxGRMlaXxVbfxMUC/w0LaYk6jQ4y/nGR9uBO3tww==",
+ "version": "0.2.13",
+ "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.13.tgz",
+ "integrity": "sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw==",
"dev": true,
"license": "MIT",
"dependencies": {
- "fdir": "^6.4.3",
+ "fdir": "^6.4.4",
"picomatch": "^4.0.2"
},
"engines": {
@@ -13645,16 +13718,19 @@
}
},
"node_modules/vite": {
- "version": "6.2.6",
- "resolved": "https://registry.npmjs.org/vite/-/vite-6.2.6.tgz",
- "integrity": "sha512-9xpjNl3kR4rVDZgPNdTL0/c6ao4km69a/2ihNQbcANz8RuCOK3hQBmLSJf3bRKVQjVMda+YvizNE8AwvogcPbw==",
+ "version": "6.3.5",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz",
+ "integrity": "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"esbuild": "^0.25.0",
+ "fdir": "^6.4.4",
+ "picomatch": "^4.0.2",
"postcss": "^8.5.3",
- "rollup": "^4.30.1"
+ "rollup": "^4.34.9",
+ "tinyglobby": "^0.2.13"
},
"bin": {
"vite": "bin/vite.js"
@@ -13717,6 +13793,299 @@
}
}
},
+ "node_modules/vite/node_modules/@rollup/rollup-android-arm-eabi": {
+ "version": "4.40.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.40.2.tgz",
+ "integrity": "sha512-JkdNEq+DFxZfUwxvB58tHMHBHVgX23ew41g1OQinthJ+ryhdRk67O31S7sYw8u2lTjHUPFxwar07BBt1KHp/hg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "peer": true
+ },
+ "node_modules/vite/node_modules/@rollup/rollup-android-arm64": {
+ "version": "4.40.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.40.2.tgz",
+ "integrity": "sha512-13unNoZ8NzUmnndhPTkWPWbX3vtHodYmy+I9kuLxN+F+l+x3LdVF7UCu8TWVMt1POHLh6oDHhnOA04n8oJZhBw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "peer": true
+ },
+ "node_modules/vite/node_modules/@rollup/rollup-darwin-arm64": {
+ "version": "4.40.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.40.2.tgz",
+ "integrity": "sha512-Gzf1Hn2Aoe8VZzevHostPX23U7N5+4D36WJNHK88NZHCJr7aVMG4fadqkIf72eqVPGjGc0HJHNuUaUcxiR+N/w==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "peer": true
+ },
+ "node_modules/vite/node_modules/@rollup/rollup-darwin-x64": {
+ "version": "4.40.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.40.2.tgz",
+ "integrity": "sha512-47N4hxa01a4x6XnJoskMKTS8XZ0CZMd8YTbINbi+w03A2w4j1RTlnGHOz/P0+Bg1LaVL6ufZyNprSg+fW5nYQQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "peer": true
+ },
+ "node_modules/vite/node_modules/@rollup/rollup-freebsd-arm64": {
+ "version": "4.40.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.40.2.tgz",
+ "integrity": "sha512-8t6aL4MD+rXSHHZUR1z19+9OFJ2rl1wGKvckN47XFRVO+QL/dUSpKA2SLRo4vMg7ELA8pzGpC+W9OEd1Z/ZqoQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "peer": true
+ },
+ "node_modules/vite/node_modules/@rollup/rollup-freebsd-x64": {
+ "version": "4.40.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.40.2.tgz",
+ "integrity": "sha512-C+AyHBzfpsOEYRFjztcYUFsH4S7UsE9cDtHCtma5BK8+ydOZYgMmWg1d/4KBytQspJCld8ZIujFMAdKG1xyr4Q==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "peer": true
+ },
+ "node_modules/vite/node_modules/@rollup/rollup-linux-arm-gnueabihf": {
+ "version": "4.40.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.40.2.tgz",
+ "integrity": "sha512-de6TFZYIvJwRNjmW3+gaXiZ2DaWL5D5yGmSYzkdzjBDS3W+B9JQ48oZEsmMvemqjtAFzE16DIBLqd6IQQRuG9Q==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "peer": true
+ },
+ "node_modules/vite/node_modules/@rollup/rollup-linux-arm-musleabihf": {
+ "version": "4.40.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.40.2.tgz",
+ "integrity": "sha512-urjaEZubdIkacKc930hUDOfQPysezKla/O9qV+O89enqsqUmQm8Xj8O/vh0gHg4LYfv7Y7UsE3QjzLQzDYN1qg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "peer": true
+ },
+ "node_modules/vite/node_modules/@rollup/rollup-linux-arm64-gnu": {
+ "version": "4.40.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.40.2.tgz",
+ "integrity": "sha512-KlE8IC0HFOC33taNt1zR8qNlBYHj31qGT1UqWqtvR/+NuCVhfufAq9fxO8BMFC22Wu0rxOwGVWxtCMvZVLmhQg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "peer": true
+ },
+ "node_modules/vite/node_modules/@rollup/rollup-linux-arm64-musl": {
+ "version": "4.40.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.40.2.tgz",
+ "integrity": "sha512-j8CgxvfM0kbnhu4XgjnCWJQyyBOeBI1Zq91Z850aUddUmPeQvuAy6OiMdPS46gNFgy8gN1xkYyLgwLYZG3rBOg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "peer": true
+ },
+ "node_modules/vite/node_modules/@rollup/rollup-linux-loongarch64-gnu": {
+ "version": "4.40.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.40.2.tgz",
+ "integrity": "sha512-Ybc/1qUampKuRF4tQXc7G7QY9YRyeVSykfK36Y5Qc5dmrIxwFhrOzqaVTNoZygqZ1ZieSWTibfFhQ5qK8jpWxw==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "peer": true
+ },
+ "node_modules/vite/node_modules/@rollup/rollup-linux-powerpc64le-gnu": {
+ "version": "4.40.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.40.2.tgz",
+ "integrity": "sha512-3FCIrnrt03CCsZqSYAOW/k9n625pjpuMzVfeI+ZBUSDT3MVIFDSPfSUgIl9FqUftxcUXInvFah79hE1c9abD+Q==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "peer": true
+ },
+ "node_modules/vite/node_modules/@rollup/rollup-linux-riscv64-gnu": {
+ "version": "4.40.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.40.2.tgz",
+ "integrity": "sha512-QNU7BFHEvHMp2ESSY3SozIkBPaPBDTsfVNGx3Xhv+TdvWXFGOSH2NJvhD1zKAT6AyuuErJgbdvaJhYVhVqrWTg==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "peer": true
+ },
+ "node_modules/vite/node_modules/@rollup/rollup-linux-s390x-gnu": {
+ "version": "4.40.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.40.2.tgz",
+ "integrity": "sha512-B7LKIz+0+p348JoAL4X/YxGx9zOx3sR+o6Hj15Y3aaApNfAshK8+mWZEf759DXfRLeL2vg5LYJBB7DdcleYCoQ==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "peer": true
+ },
+ "node_modules/vite/node_modules/@rollup/rollup-linux-x64-gnu": {
+ "version": "4.40.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.40.2.tgz",
+ "integrity": "sha512-lG7Xa+BmBNwpjmVUbmyKxdQJ3Q6whHjMjzQplOs5Z+Gj7mxPtWakGHqzMqNER68G67kmCX9qX57aRsW5V0VOng==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "peer": true
+ },
+ "node_modules/vite/node_modules/@rollup/rollup-linux-x64-musl": {
+ "version": "4.40.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.40.2.tgz",
+ "integrity": "sha512-tD46wKHd+KJvsmije4bUskNuvWKFcTOIM9tZ/RrmIvcXnbi0YK/cKS9FzFtAm7Oxi2EhV5N2OpfFB348vSQRXA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "peer": true
+ },
+ "node_modules/vite/node_modules/@rollup/rollup-win32-arm64-msvc": {
+ "version": "4.40.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.40.2.tgz",
+ "integrity": "sha512-Bjv/HG8RRWLNkXwQQemdsWw4Mg+IJ29LK+bJPW2SCzPKOUaMmPEppQlu/Fqk1d7+DX3V7JbFdbkh/NMmurT6Pg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "peer": true
+ },
+ "node_modules/vite/node_modules/@rollup/rollup-win32-ia32-msvc": {
+ "version": "4.40.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.40.2.tgz",
+ "integrity": "sha512-dt1llVSGEsGKvzeIO76HToiYPNPYPkmjhMHhP00T9S4rDern8P2ZWvWAQUEJ+R1UdMWJ/42i/QqJ2WV765GZcA==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "peer": true
+ },
+ "node_modules/vite/node_modules/@rollup/rollup-win32-x64-msvc": {
+ "version": "4.40.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.40.2.tgz",
+ "integrity": "sha512-bwspbWB04XJpeElvsp+DCylKfF4trJDa2Y9Go8O6A7YLX2LIKGcNK/CYImJN6ZP4DcuOHB4Utl3iCbnR62DudA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "peer": true
+ },
+ "node_modules/vite/node_modules/@types/estree": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz",
+ "integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true
+ },
"node_modules/vite/node_modules/postcss": {
"version": "8.5.3",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz",
@@ -13747,6 +14116,47 @@
"node": "^10 || ^12 || >=14"
}
},
+ "node_modules/vite/node_modules/rollup": {
+ "version": "4.40.2",
+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.40.2.tgz",
+ "integrity": "sha512-tfUOg6DTP4rhQ3VjOO6B4wyrJnGOX85requAXvqYTHsOgb2TFJdZ3aWpT8W2kPoypSGP7dZUyzxJ9ee4buM5Fg==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@types/estree": "1.0.7"
+ },
+ "bin": {
+ "rollup": "dist/bin/rollup"
+ },
+ "engines": {
+ "node": ">=18.0.0",
+ "npm": ">=8.0.0"
+ },
+ "optionalDependencies": {
+ "@rollup/rollup-android-arm-eabi": "4.40.2",
+ "@rollup/rollup-android-arm64": "4.40.2",
+ "@rollup/rollup-darwin-arm64": "4.40.2",
+ "@rollup/rollup-darwin-x64": "4.40.2",
+ "@rollup/rollup-freebsd-arm64": "4.40.2",
+ "@rollup/rollup-freebsd-x64": "4.40.2",
+ "@rollup/rollup-linux-arm-gnueabihf": "4.40.2",
+ "@rollup/rollup-linux-arm-musleabihf": "4.40.2",
+ "@rollup/rollup-linux-arm64-gnu": "4.40.2",
+ "@rollup/rollup-linux-arm64-musl": "4.40.2",
+ "@rollup/rollup-linux-loongarch64-gnu": "4.40.2",
+ "@rollup/rollup-linux-powerpc64le-gnu": "4.40.2",
+ "@rollup/rollup-linux-riscv64-gnu": "4.40.2",
+ "@rollup/rollup-linux-riscv64-musl": "4.40.2",
+ "@rollup/rollup-linux-s390x-gnu": "4.40.2",
+ "@rollup/rollup-linux-x64-gnu": "4.40.2",
+ "@rollup/rollup-linux-x64-musl": "4.40.2",
+ "@rollup/rollup-win32-arm64-msvc": "4.40.2",
+ "@rollup/rollup-win32-ia32-msvc": "4.40.2",
+ "@rollup/rollup-win32-x64-msvc": "4.40.2",
+ "fsevents": "~2.3.2"
+ }
+ },
"node_modules/void-elements": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/void-elements/-/void-elements-2.0.1.tgz",
diff --git a/package.json b/package.json
index 0f3aa75..77201ed 100644
--- a/package.json
+++ b/package.json
@@ -17,6 +17,8 @@
"@angular/platform-browser": "^19.2.0",
"@angular/platform-browser-dynamic": "^19.2.0",
"@angular/router": "^19.2.0",
+ "chart.js": "^4.4.9",
+ "ng2-charts": "^8.0.0",
"rxjs": "~7.8.0",
"tslib": "^2.3.0",
"zone.js": "~0.15.0"
@@ -34,4 +36,4 @@
"karma-jasmine-html-reporter": "~2.1.0",
"typescript": "~5.7.2"
}
-}
+}
\ No newline at end of file
diff --git a/src/app/app.component.css b/src/app/app.component.css
index e69de29..77fa3f9 100644
--- a/src/app/app.component.css
+++ b/src/app/app.component.css
@@ -0,0 +1,84 @@
+.container {
+ max-width: 1200px;
+ margin: 0 auto;
+ padding: 20px;
+ font-family: Arial, sans-serif;
+}
+
+header {
+ margin-bottom: 20px;
+ text-align: center;
+}
+
+h1 {
+ color: #333;
+}
+
+.controls {
+ display: flex;
+ justify-content: space-between;
+ margin-bottom: 20px;
+ flex-wrap: wrap;
+ gap: 10px;
+}
+
+.form-group {
+ margin-bottom: 10px;
+ flex: 1;
+ min-width: 200px;
+}
+
+label {
+ display: block;
+ margin-bottom: 5px;
+ font-weight: bold;
+}
+
+select, input {
+ width: 100%;
+ padding: 8px;
+ border: 1px solid #ccc;
+ border-radius: 4px;
+}
+
+.chart-container {
+ background-color: #f9f9f9;
+ border-radius: 8px;
+ padding: 20px;
+ min-height: 400px;
+ margin-bottom: 20px;
+}
+
+.loading, .error {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ height: 400px;
+}
+
+.error {
+ color: #d9534f;
+}
+
+.price-list {
+ margin-top: 30px;
+}
+
+table {
+ width: 100%;
+ border-collapse: collapse;
+}
+
+th, td {
+ padding: 10px;
+ text-align: left;
+ border-bottom: 1px solid #ddd;
+}
+
+th {
+ background-color: #f2f2f2;
+}
+
+tr:hover {
+ background-color: #f5f5f5;
+}
diff --git a/src/app/app.component.html b/src/app/app.component.html
index 36093e1..2c16ce5 100644
--- a/src/app/app.component.html
+++ b/src/app/app.component.html
@@ -1,336 +1,69 @@
-
-
-
-
-
-
-
-
+
+
-
-
-
-
-
-
-
Hello, {{ title }}
-
Congratulations! Your app is running. 🎉
-
-
-
-
- @for (item of [
- { title: 'Explore the Docs', link: 'https://angular.dev' },
- { title: 'Learn with Tutorials', link: 'https://angular.dev/tutorials' },
- { title: 'CLI Docs', link: 'https://angular.dev/tools/cli' },
- { title: 'Angular Language Service', link: 'https://angular.dev/tools/language-service' },
- { title: 'Angular DevTools', link: 'https://angular.dev/tools/devtools' },
- ]; track item.title) {
-
- {{ item.title }}
-
-
+
+
+
+
-
+
+
+
+
+
+
-
-
-
-
-
-
-
-
+
+ @if (loading) {
+
+
Loading energy price data...
+
+ }
+ @if (error) {
+
+ }
-
+ @if (!loading && !error) {
+
+ }
+
+
+ @if (!loading && !error && priceData.length > 0) {
+
+
Hour-by-hour prices
+
+
+
+ Time |
+ SEK/kWh |
+ EUR/kWh |
+
+
+
+ @for (price of priceData; track price.time_start) {
+
+ {{ price.time_start | date:'HH:00' }} - {{ price.time_end | date:'HH:00' }} |
+ {{ price.SEK_per_kWh | number:'1.2-4' }} |
+ {{ price.EUR_per_kWh | number:'1.2-4' }} |
+
+ }
+
+
+
+ }
diff --git a/src/app/app.component.spec.ts b/src/app/app.component.spec.ts
deleted file mode 100644
index 5558c4f..0000000
--- a/src/app/app.component.spec.ts
+++ /dev/null
@@ -1,29 +0,0 @@
-import { TestBed } from '@angular/core/testing';
-import { AppComponent } from './app.component';
-
-describe('AppComponent', () => {
- beforeEach(async () => {
- await TestBed.configureTestingModule({
- imports: [AppComponent],
- }).compileComponents();
- });
-
- it('should create the app', () => {
- const fixture = TestBed.createComponent(AppComponent);
- const app = fixture.componentInstance;
- expect(app).toBeTruthy();
- });
-
- it(`should have the 'Angular-DotIO' title`, () => {
- const fixture = TestBed.createComponent(AppComponent);
- const app = fixture.componentInstance;
- expect(app.title).toEqual('Angular-DotIO');
- });
-
- it('should render title', () => {
- const fixture = TestBed.createComponent(AppComponent);
- fixture.detectChanges();
- const compiled = fixture.nativeElement as HTMLElement;
- expect(compiled.querySelector('h1')?.textContent).toContain('Hello, Angular-DotIO');
- });
-});
diff --git a/src/app/app.component.ts b/src/app/app.component.ts
index fbb0c2d..8e98ba0 100644
--- a/src/app/app.component.ts
+++ b/src/app/app.component.ts
@@ -1,12 +1,70 @@
-import { Component } from '@angular/core';
-import { RouterOutlet } from '@angular/router';
+import { Component, OnInit, inject } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { FormsModule } from '@angular/forms';
+import { EnergyPriceService, EnergyPrice } from './energy-price.service';
+import { EnergyChartComponent } from './energy-chart/energy-chart.component';
@Component({
selector: 'app-root',
- imports: [RouterOutlet],
templateUrl: './app.component.html',
- styleUrl: './app.component.css'
+ styleUrls: ['./app.component.css'],
+ standalone: true,
+ imports: [CommonModule, FormsModule, EnergyChartComponent]
})
-export class AppComponent {
- title = 'Angular-DotIO';
+export class AppComponent implements OnInit {
+ title = 'Energy Price Dashboard';
+ priceData: EnergyPrice[] = [];
+ loading = true;
+ error = '';
+
+ // Default values
+ selectedDate = new Date();
+ selectedRegion = 'SE3'; // Stockholm / Södra Mellansverige as default
+
+ regions = [
+ { value: 'SE1', label: 'Luleå / Norra Sverige' },
+ { value: 'SE2', label: 'Sundsvall / Norra Mellansverige' },
+ { value: 'SE3', label: 'Stockholm / Södra Mellansverige' },
+ { value: 'SE4', label: 'Malmö / Södra Sverige' }
+ ];
+
+ private energyPriceService = inject(EnergyPriceService);
+
+ ngOnInit() {
+ this.loadPriceData();
+ }
+
+ loadPriceData() {
+ this.loading = true;
+ this.error = '';
+
+ const { year, month, day } = this.energyPriceService.formatDate(this.selectedDate);
+
+ this.energyPriceService.getPrices(year, month, day, this.selectedRegion)
+ .subscribe({
+ next: (data) => {
+ this.priceData = data;
+ this.loading = false;
+ },
+ error: (err) => {
+ this.error = 'Failed to load energy price data. Please try again later.';
+ this.loading = false;
+ console.error('Error fetching price data:', err);
+ }
+ });
+ }
+
+ onRegionChange() {
+ this.loadPriceData();
+ }
+
+ onDateChange(event: any) {
+ this.selectedDate = new Date(event.target.value);
+ this.loadPriceData();
+ }
+
+ get maxDate(): string {
+ const today = new Date();
+ return today.toISOString().split('T')[0];
+ }
}
diff --git a/src/app/app.config.ts b/src/app/app.config.ts
index a1e7d6f..6896639 100644
--- a/src/app/app.config.ts
+++ b/src/app/app.config.ts
@@ -2,7 +2,8 @@ import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core';
import { provideRouter } from '@angular/router';
import { routes } from './app.routes';
+import { provideCharts, withDefaultRegisterables } from 'ng2-charts';
export const appConfig: ApplicationConfig = {
- providers: [provideZoneChangeDetection({ eventCoalescing: true }), provideRouter(routes)]
+ providers: [provideZoneChangeDetection({ eventCoalescing: true }), provideRouter(routes), provideCharts(withDefaultRegisterables())]
};
diff --git a/src/app/app.module.ts b/src/app/app.module.ts
new file mode 100644
index 0000000..afe33a2
--- /dev/null
+++ b/src/app/app.module.ts
@@ -0,0 +1,25 @@
+import { NgModule } from '@angular/core';
+import { BrowserModule } from '@angular/platform-browser';
+import { HttpClientModule } from '@angular/common/http';
+import { FormsModule } from '@angular/forms';
+import { NgChartsModule } from 'ng2-charts';
+
+import { AppComponent } from './app.component';
+import { EnergyPriceService } from './energy-price.service';
+import { EnergyChartComponent } from './energy-chart/energy-chart.component';
+
+@NgModule({
+ declarations: [
+ AppComponent,
+ EnergyChartComponent
+ ],
+ imports: [
+ BrowserModule,
+ HttpClientModule,
+ FormsModule,
+ NgChartsModule
+ ],
+ providers: [EnergyPriceService],
+ bootstrap: [AppComponent]
+})
+export class AppModule { }
diff --git a/src/app/energy-chart/energy-chart.component.css b/src/app/energy-chart/energy-chart.component.css
new file mode 100644
index 0000000..cf8b246
--- /dev/null
+++ b/src/app/energy-chart/energy-chart.component.css
@@ -0,0 +1,4 @@
+.chart-wrapper {
+ height: 400px;
+ width: 100%;
+}
diff --git a/src/app/energy-chart/energy-chart.component.html b/src/app/energy-chart/energy-chart.component.html
new file mode 100644
index 0000000..f0665d3
--- /dev/null
+++ b/src/app/energy-chart/energy-chart.component.html
@@ -0,0 +1,8 @@
+
+
+
+
diff --git a/src/app/energy-chart/energy-chart.component.ts b/src/app/energy-chart/energy-chart.component.ts
new file mode 100644
index 0000000..dc9849e
--- /dev/null
+++ b/src/app/energy-chart/energy-chart.component.ts
@@ -0,0 +1,105 @@
+import { Component, Input, OnChanges, SimpleChanges } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { ChartConfiguration, ChartData, ChartType } from 'chart.js';
+import { BaseChartDirective } from 'ng2-charts';
+import { EnergyPrice } from '../energy-price.service';
+
+@Component({
+ selector: 'app-energy-chart',
+ templateUrl: './energy-chart.component.html',
+ styleUrls: ['./energy-chart.component.css'],
+ standalone: true,
+ imports: [CommonModule, BaseChartDirective]
+})
+export class EnergyChartComponent implements OnChanges {
+ @Input() priceData: EnergyPrice[] = [];
+
+ // Chart configuration
+ public lineChartType: ChartType = 'line';
+
+ public lineChartData: ChartData<'line'> = {
+ datasets: [],
+ labels: []
+ };
+
+ public lineChartOptions: ChartConfiguration['options'] = {
+ responsive: true,
+ maintainAspectRatio: false,
+ scales: {
+ x: {
+ title: {
+ display: true,
+ text: 'Time'
+ }
+ },
+ y: {
+ title: {
+ display: true,
+ text: 'Price (SEK per kWh)'
+ },
+ beginAtZero: true
+ }
+ },
+ plugins: {
+ legend: {
+ display: true,
+ },
+ tooltip: {
+ callbacks: {
+ label: function(context) {
+ let label = context.dataset.label || '';
+ if (label) {
+ label += ': ';
+ }
+ if (context.parsed.y !== null) {
+ label += context.parsed.y.toFixed(2) + ' SEK/kWh';
+ }
+ return label;
+ }
+ }
+ }
+ }
+ };
+
+ ngOnChanges(changes: SimpleChanges): void {
+ if (changes['priceData'] && this.priceData) {
+ this.updateChartData();
+ }
+ }
+
+ private updateChartData(): void {
+ // Extract time labels and price values
+ const labels = this.priceData.map(item => {
+ const startTime = new Date(item.time_start);
+ return startTime.getHours() + ':00';
+ });
+
+ const sekPrices = this.priceData.map(item => item.SEK_per_kWh);
+ const eurPrices = this.priceData.map(item => item.EUR_per_kWh);
+
+ // Update chart data
+ this.lineChartData = {
+ labels: labels,
+ datasets: [
+ {
+ data: sekPrices,
+ label: 'SEK per kWh',
+ backgroundColor: 'rgba(66, 133, 244, 0.2)',
+ borderColor: 'rgb(66, 133, 244)',
+ pointBackgroundColor: 'rgb(66, 133, 244)',
+ fill: 'origin',
+ tension: 0.4
+ },
+ {
+ data: eurPrices,
+ label: 'EUR per kWh',
+ backgroundColor: 'rgba(15, 157, 88, 0.2)',
+ borderColor: 'rgb(15, 157, 88)',
+ pointBackgroundColor: 'rgb(15, 157, 88)',
+ fill: 'origin',
+ tension: 0.4
+ }
+ ]
+ };
+ }
+}
diff --git a/src/app/energy-price.service.ts b/src/app/energy-price.service.ts
new file mode 100644
index 0000000..33440e5
--- /dev/null
+++ b/src/app/energy-price.service.ts
@@ -0,0 +1,32 @@
+import { Injectable, inject } from '@angular/core';
+import { HttpClient } from '@angular/common/http';
+import { Observable } from 'rxjs';
+
+export interface EnergyPrice {
+ SEK_per_kWh: number;
+ EUR_per_kWh: number;
+ EXR: number;
+ time_start: string;
+ time_end: string;
+}
+
+@Injectable({
+ providedIn: 'root'
+})
+export class EnergyPriceService {
+ private apiBaseUrl = 'https://www.elprisetjustnu.se/api/v1/prices';
+ private http = inject(HttpClient);
+
+ getPrices(year: string, month: string, day: string, priceClass: string): Observable
{
+ const url = `${this.apiBaseUrl}/${year}/${month}-${day}_${priceClass}.json`;
+ return this.http.get(url);
+ }
+
+ formatDate(date: Date): { year: string, month: string, day: string } {
+ const year = date.getFullYear().toString();
+ const month = (date.getMonth() + 1).toString().padStart(2, '0');
+ const day = date.getDate().toString().padStart(2, '0');
+
+ return { year, month, day };
+ }
+}
diff --git a/src/main.ts b/src/main.ts
index 35b00f3..4e6be2b 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -1,6 +1,11 @@
import { bootstrapApplication } from '@angular/platform-browser';
-import { appConfig } from './app/app.config';
+import { provideHttpClient } from '@angular/common/http';
+import { provideCharts, withDefaultRegisterables } from 'ng2-charts';
import { AppComponent } from './app/app.component';
-bootstrapApplication(AppComponent, appConfig)
- .catch((err) => console.error(err));
+bootstrapApplication(AppComponent, {
+ providers: [
+ provideHttpClient(),
+ provideCharts(withDefaultRegisterables())
+ ]
+}).catch(err => console.error(err));