From 19b91c4e28b170db63a5a37c5ea9467f21a114cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tam=C3=A1s=20Nagy?= Date: Wed, 25 Mar 2026 18:50:09 +0100 Subject: [PATCH] #EX-299: Remove chart.js --- frontend/Exence/CLAUDE.md | 2 +- frontend/Exence/package-lock.json | 125 ++++++----------- frontend/Exence/package.json | 2 - frontend/Exence/src/app/app.config.ts | 2 - .../modules/statistics/widget-config.model.ts | 11 +- .../dashboard/dashboard.component.html | 10 +- .../dashboard/dashboard.component.scss | 3 +- .../private/dashboard/dashboard.component.ts | 40 +++++- .../app/private/statistics/chart-providers.ts | 10 +- .../chart-widget/chart-widget.component.ts | 29 +++- .../private/statistics/statistic.service.ts | 10 +- .../src/app/shared/chart/chart-config.ts | 129 ------------------ .../src/app/shared/chart/chart.component.html | 4 - .../src/app/shared/chart/chart.component.scss | 5 - .../src/app/shared/chart/chart.component.ts | 108 --------------- frontend/Exence/src/app/shared/util/utils.ts | 8 ++ 16 files changed, 139 insertions(+), 359 deletions(-) delete mode 100644 frontend/Exence/src/app/shared/chart/chart-config.ts delete mode 100644 frontend/Exence/src/app/shared/chart/chart.component.html delete mode 100644 frontend/Exence/src/app/shared/chart/chart.component.scss delete mode 100644 frontend/Exence/src/app/shared/chart/chart.component.ts diff --git a/frontend/Exence/CLAUDE.md b/frontend/Exence/CLAUDE.md index 87b0094a..45e30bdb 100644 --- a/frontend/Exence/CLAUDE.md +++ b/frontend/Exence/CLAUDE.md @@ -90,7 +90,7 @@ src/app/ |---------|---------| | UI Components | @angular/material 21 | | Grid layout | angular-gridster2 (statistics page) | -| Charts | ApexCharts, Chart.js/ng2-charts, ngx-echarts | +| Charts | ApexCharts, ngx-echarts | | Dates | date-fns + material-date-fns-adapter | | Cookies | ngx-cookie-service | | Infinite scroll | ngx-infinite-scroll | diff --git a/frontend/Exence/package-lock.json b/frontend/Exence/package-lock.json index f9cc9ade..8a16c579 100644 --- a/frontend/Exence/package-lock.json +++ b/frontend/Exence/package-lock.json @@ -23,10 +23,8 @@ "angular-gridster2": "^21.0.1", "apexcharts": "^5.6.0", "bootstrap": "^5.3.7", - "chart.js": "^4.4.7", "date-fns": "^4.1.0", "echarts": "^6.0.0", - "ng2-charts": "^8.0.0", "ngx-cookie-service": "^21.1.0", "ngx-echarts": "^21.0.0", "ngx-infinite-scroll": "^21.0.0", @@ -432,7 +430,6 @@ "integrity": "sha512-TCb3qYOC/uXKZCo56cJ6N9sHeWdFhyVqrbbYfFjTi09081T6jllgHDZL5Ms7gOMNY8KywWGGbhxwvzeA0RwTgA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@angular-eslint/bundled-angular-compiler": "21.2.0", "eslint-scope": "^9.0.0" @@ -462,7 +459,6 @@ "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-21.1.3.tgz", "integrity": "sha512-UADMncDd9lkmIT1NPVFcufyP5gJHMPzxNaQpojiGrxT1aT8Du30mao0KSrB4aTwcicv6/cdD5bZbIyg+FL6LkQ==", "license": "MIT", - "peer": true, "dependencies": { "tslib": "^2.3.0" }, @@ -629,7 +625,6 @@ "resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-21.1.3.tgz", "integrity": "sha512-jMiEKCcZMIAnyx2jxrJHmw5c7JXAiN56ErZ4X+OuQ5yFvYRocRVEs25I0OMxntcXNdPTJQvpGwGlhWhS0yDorg==", "license": "MIT", - "peer": true, "dependencies": { "parse5": "^8.0.0", "tslib": "^2.3.0" @@ -647,7 +642,6 @@ "integrity": "sha512-UPtDcpKyrKZRPfym9gTovcibPzl2O/Woy7B8sm45sAnjDH+jDUCcCvuIak7GpH47shQkC2J4yvnHZbD4c6XxcQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@angular-devkit/architect": "0.2101.3", "@angular-devkit/core": "21.1.3", @@ -683,7 +677,6 @@ "resolved": "https://registry.npmjs.org/@angular/common/-/common-21.1.3.tgz", "integrity": "sha512-Wdbln/UqZM5oVnpfIydRdhhL8A9x3bKZ9Zy1/mM0q+qFSftPvmFZIXhEpFqbDwNYbGUhGzx7t8iULC4sVVp/zA==", "license": "MIT", - "peer": true, "dependencies": { "tslib": "^2.3.0" }, @@ -700,7 +693,6 @@ "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-21.1.3.tgz", "integrity": "sha512-gDNLh7MEf7Qf88ktZzS4LJQXCA5U8aQTfK9ak+0mi2ruZ0x4XSjQCro4H6OPKrrbq94+6GcnlSX5+oVIajEY3w==", "license": "MIT", - "peer": true, "dependencies": { "tslib": "^2.3.0" }, @@ -714,7 +706,6 @@ "integrity": "sha512-nKxoQ89W2B1WdonNQ9kgRnvLNS6DAxDrRHBslsKTlV+kbdv7h59M9PjT4ZZ2sp1M/M8LiofnUfa/s2jd/xYj5w==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/core": "7.28.5", "@jridgewell/sourcemap-codec": "^1.4.14", @@ -747,7 +738,6 @@ "resolved": "https://registry.npmjs.org/@angular/core/-/core-21.1.3.tgz", "integrity": "sha512-TbhQxRC7Lb/3WBdm1n8KRsktmVEuGBBp0WRF5mq0Ze4s1YewIM6cULrSw9ACtcL5jdcq7c74ms+uKQsaP/gdcQ==", "license": "MIT", - "peer": true, "dependencies": { "tslib": "^2.3.0" }, @@ -773,7 +763,6 @@ "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-21.1.3.tgz", "integrity": "sha512-YW/YdjM9suZUeJam9agHFXIEE3qQIhGYXMjnnX7xGjOe+CuR2R0qsWn1AR0yrKrNmFspb0lKgM7kTTJyzt8gZg==", "license": "MIT", - "peer": true, "dependencies": { "@standard-schema/spec": "^1.0.0", "tslib": "^2.3.0" @@ -793,7 +782,6 @@ "resolved": "https://registry.npmjs.org/@angular/material/-/material-21.1.3.tgz", "integrity": "sha512-bVjtGSsQYOV6Z2cHCpdQVPVOdDxFKAprGV50BHRlPIIFl0X4hsMquFCMVTMExKP5ABKOzVt8Ae5faaszVcPh3A==", "license": "MIT", - "peer": true, "dependencies": { "tslib": "^2.3.0" }, @@ -825,7 +813,6 @@ "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-21.1.3.tgz", "integrity": "sha512-W+ZMXAioaP7CsACafBCHsIxiiKrRTPOlQ+hcC7XNBwy+bn5mjGONoCgLreQs76M8HNWLtr/OAUAr6h26OguOuA==", "license": "MIT", - "peer": true, "dependencies": { "tslib": "^2.3.0" }, @@ -910,7 +897,6 @@ "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", @@ -1179,7 +1165,8 @@ "integrity": "sha512-sBXGT13cpmPR5BMgHE6UEEfEaShh5Ror6rfN3yEK5si7QVrtZg8LEPQb0VVhiLRUslD2yLnXtnRzG035J/mZXQ==", "dev": true, "license": "(Apache-2.0 AND BSD-3-Clause)", - "optional": true + "optional": true, + "peer": true }, "node_modules/@colors/colors": { "version": "1.5.0", @@ -2194,7 +2181,6 @@ "integrity": "sha512-Dx/y9bCQcXLI5ooQ5KyvA4FTgeo2jYj/7plWfV5Ak5wDPKQZgudKez2ixyfz7tKXzcJciTxqLeK7R9HItwiByg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@inquirer/checkbox": "^4.3.2", "@inquirer/confirm": "^5.1.21", @@ -2405,12 +2391,6 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, - "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/@listr2/prompt-adapter-inquirer": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/@listr2/prompt-adapter-inquirer/-/prompt-adapter-inquirer-3.0.5.tgz", @@ -4429,7 +4409,6 @@ "integrity": "sha512-m0jEgYlYz+mDJZ2+F4v8D1AyQb+QzsNqRuI7xg1VQX/KlKS0qT9r1Mo16yo5F/MtifXFgaofIFsdFMox2SxIbQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~7.16.0" } @@ -4623,7 +4602,6 @@ "integrity": "sha512-hM5faZwg7aVNa819m/5r7D0h0c9yC4DUlWAOvHAtISdFTc8xB86VmX5Xqabrama3wIPJ/q9RbGS1worb6JfnMg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.50.1", "@typescript-eslint/types": "8.50.1", @@ -5015,7 +4993,6 @@ "integrity": "sha512-ujT0Je8GI5BJWi+/mMoR0wxwVEQaxM+pi30xuMiJETlX80OPovb2p9E8ss87gnSVtYXtJoU9U1Cowcr6w2FE0w==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, @@ -5058,7 +5035,6 @@ "integrity": "sha512-BqZEsnPGdYpgyEIkDC1BadNY8oMwckftxBT+C8W0g1iKPdeqKZBtTfnvcq0nf60u7MkjFO8RBvpRGZBPw4L2ow==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.55.0", @@ -5164,7 +5140,6 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -5368,7 +5343,6 @@ "resolved": "https://registry.npmjs.org/apexcharts/-/apexcharts-5.6.0.tgz", "integrity": "sha512-BZua59yedRsaDfnxkzNrkyLCvluq2c3ZDBIz4joxSKtgr0xDQXQ5dzceMhf/TpTbAjaF+2NYIpLP3BEEIG2s/w==", "license": "SEE LICENSE IN LICENSE", - "peer": true, "dependencies": { "@yr/monotone-cubic-spline": "^1.0.3" } @@ -5554,7 +5528,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -5575,7 +5548,8 @@ "integrity": "sha512-7VPMEPuYznPSoR21NE1zvd2Xna6c/CloiZCfcMXR1Jny6PjX0N4Nsa38zcBFo/FMK+BlA+FLKbJCQ0i2yxp+Xg==", "dev": true, "license": "MIT/X11", - "optional": true + "optional": true, + "peer": true }, "node_modules/buffer-from": { "version": "1.1.2", @@ -5747,19 +5721,6 @@ "dev": true, "license": "MIT" }, - "node_modules/chart.js": { - "version": "4.5.1", - "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.5.1.tgz", - "integrity": "sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw==", - "license": "MIT", - "peer": true, - "dependencies": { - "@kurkle/color": "^0.3.0" - }, - "engines": { - "pnpm": ">=8" - } - }, "node_modules/chokidar": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz", @@ -5939,7 +5900,8 @@ "integrity": "sha512-twmVoizEW7ylZSN32OgKdXRmo1qg+wT5/6C3xu5b9QsWzSFAhHLn2xd8ro0diCsKfCj1RdaTP/nrcW+vAoQPIw==", "dev": true, "license": "MIT", - "optional": true + "optional": true, + "peer": true }, "node_modules/concat-map": { "version": "0.0.1", @@ -6159,7 +6121,6 @@ "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz", "integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==", "license": "MIT", - "peer": true, "funding": { "type": "github", "url": "https://github.com/sponsors/kossnocorp" @@ -6646,7 +6607,6 @@ "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -6707,7 +6667,6 @@ "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==", "dev": true, "license": "MIT", - "peer": true, "bin": { "eslint-config-prettier": "bin/cli.js" }, @@ -6999,7 +6958,6 @@ "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", @@ -7543,7 +7501,6 @@ "integrity": "sha512-Eaw2YTGM6WOxA6CXbckaEvslr2Ne4NFsKrvc0v97JD5awbmeBLO5w9Ho9L9kmKonrwF9RJlW6BxT1PVv/agBHQ==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=16.9.0" } @@ -8057,8 +8014,7 @@ "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-5.4.0.tgz", "integrity": "sha512-T4fio3W++llLd7LGSGsioriDHgWyhoL6YTu4k37uwJLF7DzOzspz7mNxRoM3cQdLWtL/ebazQpIf/yZGJx/gzg==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/jose": { "version": "6.1.3", @@ -8187,7 +8143,6 @@ "integrity": "sha512-LrtUxbdvt1gOpo3gxG+VAJlJAEMhbWlM4YrFQgql98FwF7+K8K12LYO4hnDdUkNjeztYrOXEMqgTajSWgmtI/w==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@colors/colors": "1.5.0", "body-parser": "^1.19.0", @@ -8708,7 +8663,6 @@ "integrity": "sha512-ME4Fb83LgEgwNw96RKNvKV4VTLuXfoKudAmm2lP8Kk87KaMK0/Xrx/aAkMWmT8mDb+3MlFDspfbCs7adjRxA2g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "cli-truncate": "^5.0.0", "colorette": "^2.0.20", @@ -8828,12 +8782,6 @@ "dev": true, "license": "MIT" }, - "node_modules/lodash-es": { - "version": "4.17.23", - "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.23.tgz", - "integrity": "sha512-kVI48u3PZr38HdYz98UmfPnXl2DXrpdctLrFLCd3kOx1xUkOmpFPx7gCWWM5MPkL/fD8zb+Ph0QzjGFs4+hHWg==", - "license": "MIT" - }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -9373,24 +9321,6 @@ "rxjs": "^7.8.2" } }, - "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/ngx-cookie-service": { "version": "21.1.0", "resolved": "https://registry.npmjs.org/ngx-cookie-service/-/ngx-cookie-service-21.1.0.tgz", @@ -10145,7 +10075,6 @@ "integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==", "dev": true, "license": "MIT", - "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -10495,7 +10424,6 @@ "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", "license": "Apache-2.0", - "peer": true, "dependencies": { "tslib": "^2.1.0" } @@ -10531,7 +10459,6 @@ "integrity": "sha512-fDz1zJpd5GycprAbu4Q2PV/RprsRtKC/0z82z0JLgdytmcq0+ujJbJ/09bPGDxCLkKY3Np5cRAOcWiVkLXJURg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "chokidar": "^4.0.0", "immutable": "^5.0.2", @@ -10554,6 +10481,7 @@ "dev": true, "license": "MIT", "optional": true, + "peer": true, "dependencies": { "@bufbuild/protobuf": "^2.5.0", "buffer-builder": "^0.2.0", @@ -10604,6 +10532,7 @@ "dev": true, "license": "MIT", "optional": true, + "peer": true, "dependencies": { "sass": "1.92.1" } @@ -10615,6 +10544,7 @@ "dev": true, "license": "MIT", "optional": true, + "peer": true, "dependencies": { "readdirp": "^4.0.1" }, @@ -10632,6 +10562,7 @@ "dev": true, "license": "MIT", "optional": true, + "peer": true, "engines": { "node": ">= 14.18.0" }, @@ -10647,6 +10578,7 @@ "dev": true, "license": "MIT", "optional": true, + "peer": true, "dependencies": { "chokidar": "^4.0.0", "immutable": "^5.0.2", @@ -10675,6 +10607,7 @@ "os": [ "android" ], + "peer": true, "engines": { "node": ">=14.0.0" } @@ -10692,6 +10625,7 @@ "os": [ "android" ], + "peer": true, "engines": { "node": ">=14.0.0" } @@ -10709,6 +10643,7 @@ "os": [ "android" ], + "peer": true, "engines": { "node": ">=14.0.0" } @@ -10726,6 +10661,7 @@ "os": [ "android" ], + "peer": true, "engines": { "node": ">=14.0.0" } @@ -10743,6 +10679,7 @@ "os": [ "darwin" ], + "peer": true, "engines": { "node": ">=14.0.0" } @@ -10760,6 +10697,7 @@ "os": [ "darwin" ], + "peer": true, "engines": { "node": ">=14.0.0" } @@ -10777,6 +10715,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">=14.0.0" } @@ -10794,6 +10733,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">=14.0.0" } @@ -10811,6 +10751,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">=14.0.0" } @@ -10828,6 +10769,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">=14.0.0" } @@ -10845,6 +10787,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">=14.0.0" } @@ -10862,6 +10805,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">=14.0.0" } @@ -10879,6 +10823,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">=14.0.0" } @@ -10896,6 +10841,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">=14.0.0" } @@ -10913,6 +10859,7 @@ "!linux", "!win32" ], + "peer": true, "dependencies": { "sass": "1.92.1" } @@ -10924,6 +10871,7 @@ "dev": true, "license": "MIT", "optional": true, + "peer": true, "dependencies": { "readdirp": "^4.0.1" }, @@ -10941,6 +10889,7 @@ "dev": true, "license": "MIT", "optional": true, + "peer": true, "engines": { "node": ">= 14.18.0" }, @@ -10956,6 +10905,7 @@ "dev": true, "license": "MIT", "optional": true, + "peer": true, "dependencies": { "chokidar": "^4.0.0", "immutable": "^5.0.2", @@ -10984,6 +10934,7 @@ "os": [ "win32" ], + "peer": true, "engines": { "node": ">=14.0.0" } @@ -11001,6 +10952,7 @@ "os": [ "win32" ], + "peer": true, "engines": { "node": ">=14.0.0" } @@ -11012,6 +10964,7 @@ "dev": true, "license": "MIT", "optional": true, + "peer": true, "dependencies": { "has-flag": "^4.0.0" }, @@ -11618,6 +11571,7 @@ "dev": true, "license": "MIT", "optional": true, + "peer": true, "dependencies": { "sync-message-port": "^1.0.0" }, @@ -11632,6 +11586,7 @@ "dev": true, "license": "MIT", "optional": true, + "peer": true, "engines": { "node": ">=16.0.0" } @@ -11746,8 +11701,7 @@ "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "license": "0BSD", - "peer": true + "license": "0BSD" }, "node_modules/tuf-js": { "version": "4.1.0", @@ -11798,7 +11752,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -11813,7 +11766,6 @@ "integrity": "sha512-ytTHO+SoYSbhAH9CrYnMhiLx8To6PSSvqnvXyPUgPETCvB6eBKmTI9w6XMPS3HsBRGkwTVBX+urA8dYQx6bHfQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/eslint-plugin": "8.50.1", "@typescript-eslint/parser": "8.50.1", @@ -12164,7 +12116,8 @@ "integrity": "sha512-cXEIW6cfr15lFv563k4GuVuW/fiwjknytD37jIOLSdSWuOI6WnO/oKwmP2FQTU2l01LP8/M5TSAJpzUaGe3uWg==", "dev": true, "license": "MIT", - "optional": true + "optional": true, + "peer": true }, "node_modules/vary": { "version": "1.1.2", @@ -12182,7 +12135,6 @@ "integrity": "sha512-dZwN5L1VlUBewiP6H9s2+B3e3Jg96D0vzN+Ry73sOefebhYr9f94wwkMNN/9ouoU8pV1BqA1d1zGk8928cx0rg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", @@ -12532,7 +12484,6 @@ "integrity": "sha512-k7Nwx6vuWx1IJ9Bjuf4Zt1PEllcwe7cls3VNzm4CQ1/hgtFUK2bRNG3rvnpPUhFjmqJKAKtjV576KnUkHocg/g==", "dev": true, "license": "MIT", - "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/frontend/Exence/package.json b/frontend/Exence/package.json index e1a105ea..b0c9a2ab 100644 --- a/frontend/Exence/package.json +++ b/frontend/Exence/package.json @@ -32,10 +32,8 @@ "angular-gridster2": "^21.0.1", "apexcharts": "^5.6.0", "bootstrap": "^5.3.7", - "chart.js": "^4.4.7", "date-fns": "^4.1.0", "echarts": "^6.0.0", - "ng2-charts": "^8.0.0", "ngx-cookie-service": "^21.1.0", "ngx-echarts": "^21.0.0", "ngx-infinite-scroll": "^21.0.0", diff --git a/frontend/Exence/src/app/app.config.ts b/frontend/Exence/src/app/app.config.ts index 8973b826..098a7e07 100644 --- a/frontend/Exence/src/app/app.config.ts +++ b/frontend/Exence/src/app/app.config.ts @@ -8,7 +8,6 @@ import { MAT_DATE_LOCALE } from '@angular/material/core'; import { MAT_FORM_FIELD_DEFAULT_OPTIONS } from '@angular/material/form-field'; import { provideAnimations } from '@angular/platform-browser/animations'; import { enUS } from 'date-fns/locale'; -import { provideCharts, withDefaultRegisterables } from 'ng2-charts'; import { CookieService } from 'ngx-cookie-service'; import { routes } from './app.routes'; import { authInterceptor } from './shared/auth/interceptors/auth.interceptor'; @@ -23,7 +22,6 @@ export const appConfig: ApplicationConfig = { provideAnimations(), provideHttpClient(withInterceptors([authInterceptor, refreshTokenInterceptor])), importProvidersFrom(LayoutModule), - provideCharts(withDefaultRegisterables()), { provide: MAT_FORM_FIELD_DEFAULT_OPTIONS, useValue: { appearance: 'outline' }, diff --git a/frontend/Exence/src/app/data-model/modules/statistics/widget-config.model.ts b/frontend/Exence/src/app/data-model/modules/statistics/widget-config.model.ts index 9de949a6..6da07cb7 100644 --- a/frontend/Exence/src/app/data-model/modules/statistics/widget-config.model.ts +++ b/frontend/Exence/src/app/data-model/modules/statistics/widget-config.model.ts @@ -68,6 +68,9 @@ export enum WidgetType { // Sankey CATEGORY_SANKEY = 'CATEGORY_SANKEY', + + // Dashboard + DASHBOARD_BALANCE_TREND = 'DASHBOARD_BALANCE_TREND', } /* eslint-disable-next-line complexity */ @@ -137,6 +140,8 @@ export function mapToExChartType(widgetType: WidgetType): ExChartType { return 'line'; case WidgetType.CATEGORY_SANKEY: return 'sankey'; + case WidgetType.DASHBOARD_BALANCE_TREND: + return 'area'; } } @@ -168,7 +173,7 @@ const GROUP_LABELS: Record = { }; const GROUP_ORDER: WidgetCatalogGroup[] = ['all', 'line_area', 'bar', 'mixed', 'circular', 'point', 'card', 'other']; -const WIDGET_METADATA: Record = { +const WIDGET_METADATA: Partial> = { // Stat Cards [WidgetType.EXPENSE_FREQUENCY_STATCARD]: { type: WidgetType.EXPENSE_FREQUENCY_STATCARD, @@ -417,7 +422,7 @@ const WIDGET_METADATA: Record = { }; export const GROUP_WIDGET_TYPES: Record = { - all: Object.values(WidgetType), + all: Object.values(WidgetType).filter(type => type in WIDGET_METADATA), card: [ WidgetType.EXPENSE_FREQUENCY_STATCARD, WidgetType.INCOME_FREQUENCY_STATCARD, @@ -465,7 +470,7 @@ export const GROUP_WIDGET_TYPES: Record = { export const WIDGET_CATALOG: WidgetCatalogData[] = GROUP_ORDER.map(group => ({ group, label: GROUP_LABELS[group], - widgets: GROUP_WIDGET_TYPES[group].map(groupType => WIDGET_METADATA[groupType]), + widgets: GROUP_WIDGET_TYPES[group].filter(type => type in WIDGET_METADATA).map(type => WIDGET_METADATA[type]!), })); export const WIDGET_CATEGORY_TYPES: Partial> = { diff --git a/frontend/Exence/src/app/private/dashboard/dashboard.component.html b/frontend/Exence/src/app/private/dashboard/dashboard.component.html index 3a8930a4..42cdc58d 100644 --- a/frontend/Exence/src/app/private/dashboard/dashboard.component.html +++ b/frontend/Exence/src/app/private/dashboard/dashboard.component.html @@ -25,7 +25,15 @@

} --> - + @if (dashboardWidget()) { + + } this.transactionStore.incomes()); expenses = computed(() => this.transactionStore.expenses()); - ngOnInit(): void { + dashboardWidget = signal(undefined); + dashboardPayload = signal(undefined); + chartTimeframe = signal(Timeframe.YEAR_TO_DATE); + + constructor() { + super(); + if (this.transactionStore.transactions().content?.length) this.transactionStore.resetState(); + + effect(() => { + console.log(this.chartTimeframe()); + this.statisticService.getDashboardChart(this.chartTimeframe()).then(response => { + this.dashboardWidget.set({ + id: response.widgetId, + type: response.type, + title: '', + info: '', + timeframe: this.chartTimeframe(), + x: 0, + y: 0, + cols: 0, + rows: 0, + }); + this.dashboardPayload.set(response.payload); + }); + }); } async openCreateTransactionDialog(transactionType: TransactionType): Promise { diff --git a/frontend/Exence/src/app/private/statistics/chart-providers.ts b/frontend/Exence/src/app/private/statistics/chart-providers.ts index 09166a6e..1c8cf7bc 100644 --- a/frontend/Exence/src/app/private/statistics/chart-providers.ts +++ b/frontend/Exence/src/app/private/statistics/chart-providers.ts @@ -1,6 +1,12 @@ import { ApexAxisChartSeries, ApexChart, ApexNonAxisChartSeries, ApexOptions, ApexYAxis } from 'ng-apexcharts'; -import { getCssVariableValue } from '../../shared/chart/chart-config'; -import { buildLinks, buildNodeMap, findHubNode, formatNumber, lightenHexColor } from '../../shared/util/utils'; +import { + buildLinks, + buildNodeMap, + findHubNode, + formatNumber, + getCssVariableValue, + lightenHexColor, +} from '../../shared/util/utils'; import { ExChartType } from '../../data-model/modules/statistics/ChartType'; import { BoxplotPayload, diff --git a/frontend/Exence/src/app/private/statistics/chart-widget/chart-widget.component.ts b/frontend/Exence/src/app/private/statistics/chart-widget/chart-widget.component.ts index 43aa19d0..dfbbdb02 100644 --- a/frontend/Exence/src/app/private/statistics/chart-widget/chart-widget.component.ts +++ b/frontend/Exence/src/app/private/statistics/chart-widget/chart-widget.component.ts @@ -1,21 +1,22 @@ -import { Component, computed, effect, inject, input, signal, viewChild } from '@angular/core'; +import { booleanAttribute, Component, computed, effect, inject, input, output, signal, viewChild } from '@angular/core'; import { MatCardModule } from '@angular/material/card'; +import { MatDividerModule } from '@angular/material/divider'; +import { MatIconModule } from '@angular/material/icon'; import { ApexOptions, ChartComponent, NgApexchartsModule } from 'ng-apexcharts'; -import { mapToProvider } from '../chart-providers'; import { ExChartType } from '../../../data-model/modules/statistics/ChartType'; import { Timeframe } from '../../../data-model/modules/statistics/Timeframe'; -import { SankeyChartComponent } from '../sankey-chart/sankey-chart.component'; -import { StatisticService } from '../statistic.service'; import { ChartWidget } from '../../../data-model/modules/statistics/Widget'; +import { WidgetDataPayload } from '../../../data-model/modules/statistics/WidgetDataPayload'; import { mapToExChartType, TIMEFRAME_HIDDEN_WIDGET_TYPES, } from '../../../data-model/modules/statistics/widget-config.model'; import { AnimatedSkeletonLoaderComponent } from '../../../shared/animated-skeleton-loader/animated-skeleton-loader.component'; import { BaseComponent } from '../../../shared/base-component/base.component'; -import { MatIconModule } from '@angular/material/icon'; +import { mapToProvider } from '../chart-providers'; +import { SankeyChartComponent } from '../sankey-chart/sankey-chart.component'; +import { StatisticService } from '../statistic.service'; import { TimeframeComponent } from '../timeframe/timeframe.component'; -import { MatDividerModule } from '@angular/material/divider'; @Component({ selector: 'ex-chart-widget', @@ -36,6 +37,10 @@ export class ChartWidgetComponent extends BaseComponent { widget = input.required(); editing = input.required(); + payload = input(); + dashboardChart = input(false, { transform: booleanAttribute }); + + readonly timeframeChanged = output(); type = computed(() => mapToExChartType(this.widget().type)); isApexChart = computed(() => !['sankey', 'statCard'].includes(this.type())); @@ -54,6 +59,10 @@ export class ChartWidgetComponent extends BaseComponent { this.timeframe.set(this.widget().timeframe); }); + effect(() => { + this.timeframeChanged.emit(this.timeframe()); + }); + effect(() => { const timeframe = this.timeframe(); this.isLoading.set(true); @@ -62,6 +71,14 @@ export class ChartWidgetComponent extends BaseComponent { return; } + if (this.dashboardChart() && this.payload()) { + const payload = this.payload()!; + const providerFn = mapToProvider(this.type()); + this.data.set(providerFn(payload, this.widget().title) as Partial); + this.isLoading.set(false); + return; + } + this.statisticService .getWidgetData(this.widget().id, timeframe) .then(response => { diff --git a/frontend/Exence/src/app/private/statistics/statistic.service.ts b/frontend/Exence/src/app/private/statistics/statistic.service.ts index 9c7484d5..60aef05c 100644 --- a/frontend/Exence/src/app/private/statistics/statistic.service.ts +++ b/frontend/Exence/src/app/private/statistics/statistic.service.ts @@ -1,12 +1,12 @@ import { inject, Injectable } from '@angular/core'; import { lastValueFrom } from 'rxjs'; -import { HttpService } from '../../shared/http/http.service'; import { Timeframe } from '../../data-model/modules/statistics/Timeframe'; -import { WidgetLayoutResponse } from '../../data-model/modules/statistics/WidgetLayoutResponse'; import { UpdateLayoutRequest } from '../../data-model/modules/statistics/UpdateLayoutRequest'; +import { Widget } from '../../data-model/modules/statistics/Widget'; import { WidgetDataPayload } from '../../data-model/modules/statistics/WidgetDataPayload'; import { WidgetDataResponse } from '../../data-model/modules/statistics/WidgetDataReponse'; -import { Widget } from '../../data-model/modules/statistics/Widget'; +import { WidgetLayoutResponse } from '../../data-model/modules/statistics/WidgetLayoutResponse'; +import { HttpService } from '../../shared/http/http.service'; @Injectable() export class StatisticService { @@ -14,6 +14,10 @@ export class StatisticService { private baseUrl = '/api/statistics/widgets'; + public getDashboardChart(timeframe?: Timeframe): Promise { + return lastValueFrom(this.http.get(`${this.baseUrl}/dashboard`, { timeframe })); + } + public getLayout(): Promise { return lastValueFrom(this.http.get(`${this.baseUrl}/layout`)); } diff --git a/frontend/Exence/src/app/shared/chart/chart-config.ts b/frontend/Exence/src/app/shared/chart/chart-config.ts deleted file mode 100644 index 68fa5191..00000000 --- a/frontend/Exence/src/app/shared/chart/chart-config.ts +++ /dev/null @@ -1,129 +0,0 @@ -import { formatCurrency } from '@angular/common'; -import { - Chart, - ChartConfiguration, - ChartData, - ChartTypeRegistry, - PluginOptionsByType, - TooltipItem, - TooltipOptions, -} from 'chart.js'; -import { Transaction } from '../../data-model/modules/transaction/Transaction'; - -export const getCssVariableValue = ( - variableName: string, - element: HTMLElement | null | undefined = document.documentElement, -): string => { - if (!element) return ''; - return getComputedStyle(element).getPropertyValue(variableName).trim(); -}; - -export const hexToRgba = (hex: string, alpha = 1): string => { - const r = parseInt(hex.slice(1, 3), 16); - const g = parseInt(hex.slice(3, 5), 16); - const b = parseInt(hex.slice(5, 7), 16); - return `rgba(${r}, ${g}, ${b}, ${alpha})`; -}; - -export const createCanvasBackgroundPlugin = (): { - id: string; - beforeDraw: (chart: Chart) => void; -} => ({ - id: 'customCanvasBackgroundColor', - beforeDraw: (chart: Chart) => { - const { ctx, canvas } = chart; - const radius = 20; - ctx.save(); - ctx.globalCompositeOperation = 'destination-over'; - - ctx.fillStyle = getCssVariableValue('--app-card-color', canvas); - - // Draw rounded rectangle - ctx.beginPath(); - ctx.roundRect(0, 0, chart.width, chart.height, radius); - ctx.closePath(); - ctx.fill(); - ctx.restore(); - }, -}); - -export const createPointerTooltipConfig = (data: Transaction[]): TooltipOptions<'line'> => - ({ - enabled: true, - callbacks: { - title: (context: TooltipItem<'line'>[]) => { - const dataIndex = context[0]?.dataIndex; - if (typeof dataIndex !== 'number') return ''; - const transaction = data[dataIndex]; - const title = transaction.title; - return title.length > 15 ? title.slice(0, 15) + '...' : title; - }, - label: (context: TooltipItem<'line'>) => { - const dataIndex = context.dataIndex; - if (typeof dataIndex !== 'number') return ''; - const transaction = data[dataIndex]; - return `Amount: ${formatCurrency(transaction.amount, 'en-US', 'Ft', 'hu-HU')}`; - }, - }, - }) as TooltipOptions<'line'>; - -export const getLineChartOptions = ( - color?: string, - gridColor?: string, - plugins?: Omit>, 'legend'>, -): ChartConfiguration['options'] => { - return { - responsive: true, - maintainAspectRatio: false, - layout: { - padding: { top: 30, left: 30, right: 30, bottom: 30 }, - }, - animations: { - tension: { duration: 2000 }, - backgroundClor: { duration: 0 }, - }, - elements: { - line: { tension: 0.3 }, - }, - plugins: { - legend: { display: false }, - ...plugins, - }, - scales: { - x: { - grid: { color: gridColor }, - border: { color: gridColor }, - ticks: { color }, - }, - y: { - beginAtZero: true, - grid: { color: gridColor }, - border: { color: gridColor }, - ticks: { color }, - }, - }, - }; -}; - -export const getLineChartData = ( - data?: number[], - labels?: string[], - bgColor?: string, - hoverColor?: string, -): ChartData<'line'> => { - return { - labels: labels ?? [], - datasets: [ - { - data: data ?? [], - fill: 'origin', - pointRadius: 4, - pointBorderWidth: 0, - backgroundColor: hexToRgba(bgColor ?? '', 0.25), - borderColor: bgColor, - pointBackgroundColor: bgColor, - pointHoverBackgroundColor: hoverColor, - }, - ], - }; -}; diff --git a/frontend/Exence/src/app/shared/chart/chart.component.html b/frontend/Exence/src/app/shared/chart/chart.component.html deleted file mode 100644 index 16f5e458..00000000 --- a/frontend/Exence/src/app/shared/chart/chart.component.html +++ /dev/null @@ -1,4 +0,0 @@ -
- - -
diff --git a/frontend/Exence/src/app/shared/chart/chart.component.scss b/frontend/Exence/src/app/shared/chart/chart.component.scss deleted file mode 100644 index 81de7f8e..00000000 --- a/frontend/Exence/src/app/shared/chart/chart.component.scss +++ /dev/null @@ -1,5 +0,0 @@ -.chart-container { - width: 100%; - height: 15rem; - aspect-ratio: 16 / 9; -} diff --git a/frontend/Exence/src/app/shared/chart/chart.component.ts b/frontend/Exence/src/app/shared/chart/chart.component.ts deleted file mode 100644 index 458a3c31..00000000 --- a/frontend/Exence/src/app/shared/chart/chart.component.ts +++ /dev/null @@ -1,108 +0,0 @@ -import { afterNextRender, Component, computed, effect, inject, input, signal, viewChild } from '@angular/core'; - -import { Chart, ChartConfiguration, ChartData, ChartType } from 'chart.js'; -import { format } from 'date-fns'; -import { BaseChartDirective } from 'ng2-charts'; -import { Transaction } from '../../data-model/modules/transaction/Transaction'; -import { TransactionType } from '../../data-model/modules/transaction/TransactionType'; -import { BaseComponent } from '../base-component/base.component'; -import { DisplayThemeService } from '../display-theme.service'; -import { - createCanvasBackgroundPlugin, - createPointerTooltipConfig, - getCssVariableValue, - getLineChartData, - getLineChartOptions, -} from './chart-config'; - -@Component({ - selector: 'ex-chart', - templateUrl: './chart.component.html', - styleUrl: './chart.component.scss', - imports: [BaseChartDirective], -}) -export class ChartComponent extends BaseComponent { - private themeService = inject(DisplayThemeService); - - private chart = viewChild(BaseChartDirective); - - data = input.required(); - - lineChartType: ChartType = 'line'; - balanceData = computed(() => { - // has to come from backend later - const sortedData = this.data().sort((a, b) => a.date.localeCompare(b.date)); - if (!sortedData.length) return []; - - let currentBalance = 0; - return sortedData.map(transaction => { - if (transaction.type === TransactionType.INCOME) { - currentBalance += transaction.amount; - } else { - currentBalance -= transaction.amount; - } - return currentBalance; - }); - }); - chartLabels = computed(() => - this.data() - .sort((a, b) => a.date.localeCompare(b.date)) - .map(transaction => format(new Date(transaction.date), 'dd/MM')), - ); - lineChartData = signal>(getLineChartData()); - lineChartOptions = signal(getLineChartOptions()); // colors, font style, etc. - - get canvas(): HTMLCanvasElement | undefined { - return this.chart()?.chart?.canvas; - } - - constructor() { - super(); - - // initial - afterNextRender(() => { - this.registerCanvasBackground(); - this.setThemeColors(); - }); - - // when data changes - effect(() => { - this.data(); - this.balanceData(); - this.chartLabels(); - - if (this.canvas) { - this.setThemeColors(); - } - }); - - // when theme changes - this.addSubscription(this.themeService.themeChangedEvent.subscribe(() => this.setThemeColors())); - } - - private registerCanvasBackground(): void { - const canvasBgPlugin = createCanvasBackgroundPlugin(); - Chart.register(canvasBgPlugin); - } - - private setThemeColors(): void { - if (!this.canvas) return; - const element = this.canvas; - - // get colors - const color = getCssVariableValue('--default-text-color', element); - const colorGrid = getCssVariableValue('--border-color', element); - const bgColor = getCssVariableValue('--primary-color', element); - const hoverColor = getCssVariableValue('--app-hover-color', element); - - // update chart options with new colors - this.lineChartOptions.set( - getLineChartOptions(color, colorGrid, { - tooltip: createPointerTooltipConfig(this.data()), - }), - ); - - // update chart data with new colors - this.lineChartData.set(getLineChartData(this.balanceData(), this.chartLabels(), bgColor, hoverColor)); - } -} diff --git a/frontend/Exence/src/app/shared/util/utils.ts b/frontend/Exence/src/app/shared/util/utils.ts index 429e9633..56c97f92 100644 --- a/frontend/Exence/src/app/shared/util/utils.ts +++ b/frontend/Exence/src/app/shared/util/utils.ts @@ -98,6 +98,14 @@ export function buildLinks( })); } +export const getCssVariableValue = ( + variableName: string, + element: HTMLElement | null | undefined = document.documentElement, +): string => { + if (!element) return ''; + return getComputedStyle(element).getPropertyValue(variableName).trim(); +}; + export function formatNumber(value: number, locale = 'hu-HU'): string { if (Math.abs(value) < 10000) return value.toString(); return new Intl.NumberFormat(locale, {