diff --git a/ui/package-lock.json b/ui/package-lock.json index 43a8823c..76496c5b 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -15,6 +15,7 @@ "react": "^19.2.4", "react-dom": "^19.2.4", "react-router": "^7.13.1", + "recharts": "^3.8.1", "swagger-ui-dist": "^5.32.0", "zustand": "^5.0.11" }, @@ -711,6 +712,42 @@ "node": ">=10" } }, + "node_modules/@reduxjs/toolkit": { + "version": "2.11.2", + "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.11.2.tgz", + "integrity": "sha512-Kd6kAHTA6/nUpp8mySPqj3en3dm0tdMIgbttnQ1xFMVpufoj+ADi8pXLBsd4xzTRHQa7t/Jv8W5UnCuW4kuWMQ==", + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "@standard-schema/utils": "^0.3.0", + "immer": "^11.0.0", + "redux": "^5.0.1", + "redux-thunk": "^3.1.0", + "reselect": "^5.1.0" + }, + "peerDependencies": { + "react": "^16.9.0 || ^17.0.0 || ^18 || ^19", + "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-redux": { + "optional": true + } + } + }, + "node_modules/@reduxjs/toolkit/node_modules/immer": { + "version": "11.1.4", + "resolved": "https://registry.npmjs.org/immer/-/immer-11.1.4.tgz", + "integrity": "sha512-XREFCPo6ksxVzP4E0ekD5aMdf8WMwmdNaz6vuvxgI40UaEiu6q3p8X52aU6GdyvLY3XXX/8R7JOTXStz/nBbRw==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, "node_modules/@rolldown/binding-android-arm64": { "version": "1.0.0-rc.10", "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.10.tgz", @@ -980,6 +1017,18 @@ "hasInstallScript": true, "license": "Apache-2.0" }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "license": "MIT" + }, + "node_modules/@standard-schema/utils": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz", + "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==", + "license": "MIT" + }, "node_modules/@tanstack/query-core": { "version": "5.91.2", "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.91.2.tgz", @@ -1017,6 +1066,69 @@ "tslib": "^2.4.0" } }, + "node_modules/@types/d3-array": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz", + "integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==", + "license": "MIT" + }, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", + "license": "MIT" + }, + "node_modules/@types/d3-ease": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", + "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", + "license": "MIT" + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "license": "MIT", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-path": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", + "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==", + "license": "MIT" + }, + "node_modules/@types/d3-scale": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz", + "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", + "license": "MIT", + "dependencies": { + "@types/d3-time": "*" + } + }, + "node_modules/@types/d3-shape": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.8.tgz", + "integrity": "sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==", + "license": "MIT", + "dependencies": { + "@types/d3-path": "*" + } + }, + "node_modules/@types/d3-time": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz", + "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==", + "license": "MIT" + }, + "node_modules/@types/d3-timer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", + "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", + "license": "MIT" + }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", @@ -1061,6 +1173,12 @@ "@types/react": "^19.2.0" } }, + "node_modules/@types/use-sync-external-store": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz", + "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==", + "license": "MIT" + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "8.57.1", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.57.1.tgz", @@ -1585,6 +1703,15 @@ "dev": true, "license": "MIT" }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -1679,6 +1806,127 @@ "devOptional": true, "license": "MIT" }, + "node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "license": "ISC", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-format": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.2.tgz", + "integrity": "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "license": "ISC", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "license": "ISC", + "dependencies": { + "d3-path": "^3.1.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "license": "ISC", + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "license": "ISC", + "dependencies": { + "d3-time": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", @@ -1697,6 +1945,12 @@ } } }, + "node_modules/decimal.js-light": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz", + "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==", + "license": "MIT" + }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -1721,6 +1975,16 @@ "dev": true, "license": "ISC" }, + "node_modules/es-toolkit": { + "version": "1.45.1", + "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.45.1.tgz", + "integrity": "sha512-/jhoOj/Fx+A+IIyDNOvO3TItGmlMKhtX8ISAHKE90c4b/k1tqaqEZ+uUqfpU8DMnW5cgNJv606zS55jGvza0Xw==", + "license": "MIT", + "workspaces": [ + "docs", + "benchmarks" + ] + }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", @@ -1928,6 +2192,12 @@ "node": ">=0.10.0" } }, + "node_modules/eventemitter3": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", + "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", + "license": "MIT" + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -2120,6 +2390,16 @@ "node": ">= 4" } }, + "node_modules/immer": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/immer/-/immer-10.2.0.tgz", + "integrity": "sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, "node_modules/import-fresh": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", @@ -2160,6 +2440,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -2944,6 +3233,36 @@ "react": "^19.2.4" } }, + "node_modules/react-is": { + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.4.tgz", + "integrity": "sha512-W+EWGn2v0ApPKgKKCy/7s7WHXkboGcsrXE+2joLyVxkbyVQfO3MUEaUQDHoSmb8TFFrSKYa9mw64WZHNHSDzYA==", + "license": "MIT", + "peer": true + }, + "node_modules/react-redux": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz", + "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==", + "license": "MIT", + "dependencies": { + "@types/use-sync-external-store": "^0.0.6", + "use-sync-external-store": "^1.4.0" + }, + "peerDependencies": { + "@types/react": "^18.2.25 || ^19", + "react": "^18.0 || ^19", + "redux": "^5.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "redux": { + "optional": true + } + } + }, "node_modules/react-router": { "version": "7.13.2", "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.13.2.tgz", @@ -2982,6 +3301,51 @@ "react-dom": ">=18" } }, + "node_modules/recharts": { + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/recharts/-/recharts-3.8.1.tgz", + "integrity": "sha512-mwzmO1s9sFL0TduUpwndxCUNoXsBw3u3E/0+A+cLcrSfQitSG62L32N69GhqUrrT5qKcAE3pCGVINC6pqkBBQg==", + "license": "MIT", + "workspaces": [ + "www" + ], + "dependencies": { + "@reduxjs/toolkit": "^1.9.0 || 2.x.x", + "clsx": "^2.1.1", + "decimal.js-light": "^2.5.1", + "es-toolkit": "^1.39.3", + "eventemitter3": "^5.0.1", + "immer": "^10.1.1", + "react-redux": "8.x.x || 9.x.x", + "reselect": "5.1.1", + "tiny-invariant": "^1.3.3", + "use-sync-external-store": "^1.2.2", + "victory-vendor": "^37.0.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-is": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/redux": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", + "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", + "license": "MIT" + }, + "node_modules/redux-thunk": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz", + "integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==", + "license": "MIT", + "peerDependencies": { + "redux": "^5.0.0" + } + }, "node_modules/require-from-string": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", @@ -2992,6 +3356,12 @@ "node": ">=0.10.0" } }, + "node_modules/reselect": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz", + "integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==", + "license": "MIT" + }, "node_modules/resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", @@ -3133,6 +3503,12 @@ "@scarf/scarf": "=1.4.0" } }, + "node_modules/tiny-invariant": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", + "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", + "license": "MIT" + }, "node_modules/tinyglobby": { "version": "0.2.15", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", @@ -3290,6 +3666,37 @@ "dev": true, "license": "MIT" }, + "node_modules/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/victory-vendor": { + "version": "37.3.6", + "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-37.3.6.tgz", + "integrity": "sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ==", + "license": "MIT AND ISC", + "dependencies": { + "@types/d3-array": "^3.0.3", + "@types/d3-ease": "^3.0.0", + "@types/d3-interpolate": "^3.0.1", + "@types/d3-scale": "^4.0.2", + "@types/d3-shape": "^3.1.0", + "@types/d3-time": "^3.0.0", + "@types/d3-timer": "^3.0.0", + "d3-array": "^3.1.6", + "d3-ease": "^3.0.1", + "d3-interpolate": "^3.0.1", + "d3-scale": "^4.0.2", + "d3-shape": "^3.1.0", + "d3-time": "^3.0.0", + "d3-timer": "^3.0.1" + } + }, "node_modules/vite": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.1.tgz", diff --git a/ui/package.json b/ui/package.json index 48ee9bae..10814bfe 100644 --- a/ui/package.json +++ b/ui/package.json @@ -21,6 +21,7 @@ "react": "^19.2.4", "react-dom": "^19.2.4", "react-router": "^7.13.1", + "recharts": "^3.8.1", "swagger-ui-dist": "^5.32.0", "zustand": "^5.0.11" }, diff --git a/ui/src/pages/DashboardTab/DashboardL1.tsx b/ui/src/pages/DashboardTab/DashboardL1.tsx index e7125867..27fd2753 100644 --- a/ui/src/pages/DashboardTab/DashboardL1.tsx +++ b/ui/src/pages/DashboardTab/DashboardL1.tsx @@ -454,17 +454,15 @@ export default function DashboardL1() { navigate(`/dashboard/${id}`)} />
- + - +
diff --git a/ui/src/pages/DashboardTab/DashboardL2.tsx b/ui/src/pages/DashboardTab/DashboardL2.tsx index 5133fa5a..79a51198 100644 --- a/ui/src/pages/DashboardTab/DashboardL2.tsx +++ b/ui/src/pages/DashboardTab/DashboardL2.tsx @@ -433,17 +433,15 @@ export default function DashboardL2() { navigate(`/dashboard/${appId}/${id}`)} />
- + - +
diff --git a/ui/src/pages/DashboardTab/PunchcardHeatmap.tsx b/ui/src/pages/DashboardTab/PunchcardHeatmap.tsx index 6ab7bd90..1d5cdae2 100644 --- a/ui/src/pages/DashboardTab/PunchcardHeatmap.tsx +++ b/ui/src/pages/DashboardTab/PunchcardHeatmap.tsx @@ -1,4 +1,9 @@ import { useMemo } from 'react'; +import { + ScatterChart, Scatter, XAxis, YAxis, Tooltip, + ResponsiveContainer, Rectangle, +} from 'recharts'; +import { rechartsTheme } from '@cameleer/design-system'; export interface PunchcardCell { weekday: number; @@ -10,116 +15,121 @@ export interface PunchcardCell { interface PunchcardHeatmapProps { cells: PunchcardCell[]; mode: 'transactions' | 'errors'; - width: number; - height: number; } const DAYS = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']; -const LEFT_MARGIN = 28; -const TOP_MARGIN = 18; -const BOTTOM_MARGIN = 4; -const RIGHT_MARGIN = 4; function transactionColor(ratio: number): string { - if (ratio === 0) return 'hsl(220, 15%, 95%)'; + if (ratio === 0) return 'var(--bg-inset)'; const lightness = 90 - ratio * 55; return `hsl(220, 60%, ${Math.round(lightness)}%)`; } function errorColor(ratio: number): string { - if (ratio === 0) return 'hsl(0, 10%, 95%)'; + if (ratio === 0) return 'var(--bg-inset)'; const lightness = 90 - ratio * 55; return `hsl(0, 65%, ${Math.round(lightness)}%)`; } -export function PunchcardHeatmap({ cells, mode, width, height }: PunchcardHeatmapProps) { - const grid = useMemo(() => { - const map = new Map(); - for (const c of cells) { - map.set(`${c.weekday}-${c.hour}`, c); - } +interface HeatmapPoint { + weekday: number; + hour: number; + value: number; + fill: string; +} - const values: number[] = []; - for (const c of cells) { - values.push(mode === 'errors' ? c.failedCount : c.totalCount); - } +function HeatmapCell(props: Record) { + const { cx, cy, payload } = props as { + cx: number; cy: number; payload: HeatmapPoint; + }; + if (!payload) return null; + // Cell size: chart area / grid divisions. Approximate from scatter positioning. + const cellW = 32; + const cellH = 10; + return ( + + ); +} + +function formatDay(value: number): string { + return DAYS[value] ?? ''; +} + +function formatHour(value: number): string { + return String(value).padStart(2, '0'); +} + +export function PunchcardHeatmap({ cells, mode }: PunchcardHeatmapProps) { + const data: HeatmapPoint[] = useMemo(() => { + const values = cells.map(c => mode === 'errors' ? c.failedCount : c.totalCount); const maxVal = Math.max(...values, 1); - const gridWidth = width - LEFT_MARGIN - RIGHT_MARGIN; - const gridHeight = height - TOP_MARGIN - BOTTOM_MARGIN; - const cellW = gridWidth / 7; - const cellH = gridHeight / 24; - - const rects: { x: number; y: number; w: number; h: number; fill: string; value: number; day: string; hour: number }[] = []; + // Build full 7x24 grid + const points: HeatmapPoint[] = []; + const cellMap = new Map(); + for (const c of cells) cellMap.set(`${c.weekday}-${c.hour}`, c); for (let d = 0; d < 7; d++) { for (let h = 0; h < 24; h++) { - const cell = map.get(`${d}-${h}`); + const cell = cellMap.get(`${d}-${h}`); const val = cell ? (mode === 'errors' ? cell.failedCount : cell.totalCount) : 0; const ratio = maxVal > 0 ? val / maxVal : 0; - const fill = mode === 'errors' ? errorColor(ratio) : transactionColor(ratio); - - rects.push({ - x: LEFT_MARGIN + d * cellW, - y: TOP_MARGIN + h * cellH, - w: cellW, - h: cellH, - fill, - value: val, - day: DAYS[d], + points.push({ + weekday: d, hour: h, + value: val, + fill: mode === 'errors' ? errorColor(ratio) : transactionColor(ratio), }); } } - return { rects, cellW, cellH }; - }, [cells, mode, width, height]); + return points; + }, [cells, mode]); return ( - - {/* Day labels (top) */} - {DAYS.map((day, i) => ( - - {day} - - ))} - - {/* Hour labels (left, every 4 hours) */} - {[0, 4, 8, 12, 16, 20].map((h) => ( - - {String(h).padStart(2, '0')} - - ))} - - {/* Cells */} - {grid.rects.map((r) => ( - - {`${r.day} ${String(r.hour).padStart(2, '0')}:00 — ${r.value.toLocaleString()} ${mode}`} - - ))} - + + + + + { + const p = entry.payload; + if (!p) return ''; + return [`${p.value.toLocaleString()} ${mode}`, `${DAYS[p.weekday]} ${formatHour(p.hour)}:00`]; + }} + labelFormatter={() => ''} + /> + + + ); } diff --git a/ui/src/pages/DashboardTab/Treemap.tsx b/ui/src/pages/DashboardTab/Treemap.tsx index f4a3e5f1..4a930793 100644 --- a/ui/src/pages/DashboardTab/Treemap.tsx +++ b/ui/src/pages/DashboardTab/Treemap.tsx @@ -1,4 +1,6 @@ -import { useMemo } from 'react'; +import { useCallback } from 'react'; +import { Treemap as RechartsTreemap, ResponsiveContainer, Tooltip } from 'recharts'; +import { rechartsTheme } from '@cameleer/design-system'; export interface TreemapItem { id: string; @@ -10,19 +12,9 @@ export interface TreemapItem { interface TreemapProps { items: TreemapItem[]; - width: number; - height: number; onItemClick?: (id: string) => void; } -interface LayoutRect { - item: TreemapItem; - x: number; - y: number; - w: number; - h: number; -} - function slaColor(pct: number): string { if (pct >= 99) return 'hsl(120, 45%, 85%)'; if (pct >= 97) return 'hsl(90, 45%, 85%)'; @@ -44,116 +36,93 @@ function slaTextColor(pct: number): string { return 'hsl(0, 40%, 30%)'; } -/** Squarified treemap layout */ -function layoutTreemap(items: TreemapItem[], x: number, y: number, w: number, h: number): LayoutRect[] { - if (items.length === 0) return []; - const total = items.reduce((s, i) => s + i.value, 0); - if (total === 0) return items.map((item, i) => ({ item, x: x + i, y, w: 1, h: 1 })); +/** Custom cell renderer for the Recharts Treemap */ +function CustomCell(props: Record) { + const { x, y, width, height, name, slaCompliance, onItemClick } = props as { + x: number; y: number; width: number; height: number; + name: string; slaCompliance: number; onItemClick?: (id: string) => void; + }; - const sorted = [...items].sort((a, b) => b.value - a.value); - const rects: LayoutRect[] = []; - layoutSlice(sorted, total, x, y, w, h, rects); - return rects; + const w = width ?? 0; + const h = height ?? 0; + if (w < 2 || h < 2) return null; + + const showLabel = w > 40 && h > 20; + const showSla = w > 60 && h > 34; + const sla = slaCompliance ?? 100; + + return ( + onItemClick?.(name)} + style={{ cursor: onItemClick ? 'pointer' : 'default' }} + > + + {showLabel && ( + + {name.length > w / 6.5 ? name.slice(0, Math.floor(w / 6.5)) + '\u2026' : name} + + )} + {showSla && ( + + {sla.toFixed(1)}% SLA + + )} + + ); } -function layoutSlice( - items: TreemapItem[], total: number, - x: number, y: number, w: number, h: number, - out: LayoutRect[], -) { - if (items.length === 0) return; - if (items.length === 1) { - out.push({ item: items[0], x, y, w, h }); - return; - } +export function Treemap({ items, onItemClick }: TreemapProps) { + // Recharts Treemap expects { name, size, ...extra } + const data = items.map(i => ({ + name: i.label, + size: i.value, + slaCompliance: i.slaCompliance, + })); - const isWide = w >= h; - let partialSum = 0; - let splitIndex = 0; - - // Find split point closest to half the total area - const halfTotal = total / 2; - for (let i = 0; i < items.length - 1; i++) { - partialSum += items[i].value; - if (partialSum >= halfTotal) { - splitIndex = i + 1; - break; - } - splitIndex = i + 1; - } - - const leftTotal = items.slice(0, splitIndex).reduce((s, i) => s + i.value, 0); - const ratio = total > 0 ? leftTotal / total : 0.5; - - if (isWide) { - const splitX = x + w * ratio; - layoutSlice(items.slice(0, splitIndex), leftTotal, x, y, w * ratio, h, out); - layoutSlice(items.slice(splitIndex), total - leftTotal, splitX, y, w * (1 - ratio), h, out); - } else { - const splitY = y + h * ratio; - layoutSlice(items.slice(0, splitIndex), leftTotal, x, y, w, h * ratio, out); - layoutSlice(items.slice(splitIndex), total - leftTotal, x, splitY, w, h * (1 - ratio), out); - } -} - -export function Treemap({ items, width, height, onItemClick }: TreemapProps) { - const rects = useMemo(() => layoutTreemap(items, 1, 1, width - 2, height - 2), [items, width, height]); + const renderContent = useCallback( + (props: Record) => , + [onItemClick], + ); if (items.length === 0) { - return ( - - No data - - ); + return
No data
; } return ( - - {rects.map(({ item, x, y, w, h }) => { - const pad = 1; - const rx = x + pad; - const ry = y + pad; - const rw = Math.max(w - pad * 2, 0); - const rh = Math.max(h - pad * 2, 0); - const showLabel = rw > 40 && rh > 20; - const showSla = rw > 60 && rh > 34; - - return ( - onItemClick?.(item.id)} - style={{ cursor: onItemClick ? 'pointer' : 'default' }} - > - - {showLabel && ( - - {item.label.length > rw / 6.5 ? item.label.slice(0, Math.floor(rw / 6.5)) + '\u2026' : item.label} - - )} - {showSla && ( - - {item.slaCompliance.toFixed(1)}% SLA - - )} - - ); - })} - + + + { + const sla = entry.payload?.slaCompliance ?? 0; + return [`${value.toLocaleString()} exchanges · ${sla.toFixed(1)}% SLA`]; + }} + /> + + ); }