From 74206c727fe2ca2353822691a65ac8f3f66940ca Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 7 May 2026 22:47:48 +0000 Subject: [PATCH 01/11] feat: add /dashboard page rendering @buildcanada/charts Grapher Wires the @buildcanada/charts library (plus its @buildcanada/colours and @buildcanada/components peers) into TradingPost as file: dependencies pointing at the sibling bcds checkout. The dashboard renders an interactive Grapher with the LifeExpectancyGrapher sample dataset and exposes controls to switch chart type (Line / Discrete Bar / Stacked Area / Slope) and entity preset. Notes on the integration: - The Grapher component uses MobX, mounts via observers that touch window/document, and is not RSC-safe. The page boundary loads it via next/dynamic with ssr: false from a client wrapper so Next does not try to prerender it (mobx decorators initialize on import, so even SSR of an otherwise-client component fails). - @buildcanada/charts ships a single SCSS entry point as its './styles.css' export. Adding 'sass' as a dependency lets Next compile that .scss directly when imported from the dashboard route. - mobx (^6.15) and mobx-react (^7.6) are required as peer deps by the charts package. mobx-react 7.6 still declares React 16/17/18 as its peer but works against React 19 for the @observer usage in Grapher. Together with the matching fix in bcds (TC-39 stage 3 decorator emit + automatic JSX runtime in the charts build), /dashboard now mounts the Grapher cleanly and survives chart-type / entity-preset switching. --- package.json | 6 + pnpm-lock.yaml | 1439 ++++++++++++++++++++++++- src/app/dashboard/Dashboard.tsx | 123 +++ src/app/dashboard/DashboardClient.tsx | 16 + src/app/dashboard/page.tsx | 14 + 5 files changed, 1596 insertions(+), 2 deletions(-) create mode 100644 src/app/dashboard/Dashboard.tsx create mode 100644 src/app/dashboard/DashboardClient.tsx create mode 100644 src/app/dashboard/page.tsx diff --git a/package.json b/package.json index 564ba36..fcd8913 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,9 @@ }, "dependencies": { "@base-ui/react": "^1.3.0", + "@buildcanada/charts": "file:../bcds/packages/charts", + "@buildcanada/colours": "file:../bcds/packages/colours", + "@buildcanada/components": "file:../bcds/packages/components", "@cloudflare/stream-react": "^1.9.3", "@radix-ui/react-select": "^2.2.5", "chart.js": "^4.4.7", @@ -21,12 +24,15 @@ "clsx": "^2.1.1", "date-fns": "^4.1.0", "lucide-react": "^1.7.0", + "mobx": "^6.15.3", + "mobx-react": "^7.6.0", "next": "16.1.6", "posthog-js": "^1.368.0", "react": "19.2.3", "react-chartjs-2": "^5.3.0", "react-dom": "19.2.3", "react-markdown": "^10.1.0", + "sass": "^1.99.0", "schema-dts": "^2.0.0", "sonner": "^2.0.7", "swr": "^2.3.8", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a846e7f..f6d975e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,6 +11,15 @@ importers: '@base-ui/react': specifier: ^1.3.0 version: 1.3.0(@types/react@19.2.14)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@buildcanada/charts': + specifier: file:../bcds/packages/charts + version: file:../bcds/packages/charts(@types/react@19.2.14)(d3-selection@3.0.0)(mobx-react@7.6.0(mobx@6.15.3)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(mobx@6.15.3)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3) + '@buildcanada/colours': + specifier: file:../bcds/packages/colours + version: file:../bcds/packages/colours(typescript@5.9.3) + '@buildcanada/components': + specifier: file:../bcds/packages/components + version: file:../bcds/packages/components(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@cloudflare/stream-react': specifier: ^1.9.3 version: 1.9.3(react@19.2.3) @@ -35,9 +44,15 @@ importers: lucide-react: specifier: ^1.7.0 version: 1.7.0(react@19.2.3) + mobx: + specifier: ^6.15.3 + version: 6.15.3 + mobx-react: + specifier: ^7.6.0 + version: 7.6.0(mobx@6.15.3)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) next: specifier: 16.1.6 - version: 16.1.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + version: 16.1.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(sass@1.99.0) posthog-js: specifier: ^1.368.0 version: 1.368.0 @@ -53,6 +68,9 @@ importers: react-markdown: specifier: ^10.1.0 version: 10.1.0(@types/react@19.2.14)(react@19.2.3) + sass: + specifier: ^1.99.0 + version: 1.99.0 schema-dts: specifier: ^2.0.0 version: 2.0.0(typescript@5.9.3) @@ -198,6 +216,36 @@ packages: '@types/react': optional: true + '@buildcanada/charts@file:../bcds/packages/charts': + resolution: {directory: ../bcds/packages/charts, type: directory} + peerDependencies: + mobx: ^6.13.0 + mobx-react: ^7.6.0 + react: ^19.0.0 + react-dom: ^19.0.0 + + '@buildcanada/colours@0.3.3': + resolution: {integrity: sha512-Zzqqf7a4plntgIA0gXI13rOkQvuJyIPh6XKZ8CWnehACMOyDcyRXRD1s87KwKZ0tW66PF6G5baJQ6m5mXkCV0A==} + peerDependencies: + typescript: ^5 + + '@buildcanada/colours@file:../bcds/packages/colours': + resolution: {directory: ../bcds/packages/colours, type: directory} + peerDependencies: + typescript: ^5 + + '@buildcanada/components@0.3.5': + resolution: {integrity: sha512-PffM8aJ6zge/lDjVXtg4GZY5uYIc5tbwOgnzMBTqA+RQKUdT9Bry9ykluuqNRJhOT5I2f8M+v+ZekF/OiDty0w==} + peerDependencies: + react: ^19.0.0 + react-dom: ^19.0.0 + + '@buildcanada/components@file:../bcds/packages/components': + resolution: {directory: ../bcds/packages/components, type: directory} + peerDependencies: + react: ^19.0.0 + react-dom: ^19.0.0 + '@cloudflare/stream-react@1.9.3': resolution: {integrity: sha512-ocr7B+zHk/jq0r/wgtFeyoz7Hr+v3Qn0Ho6yUBkWT07ahr/0GU1dhliYJESwNXMuMYCU1sZ02sFH9pW5r1/BIA==} engines: {node: '>=10'} @@ -213,6 +261,47 @@ packages: '@emnapi/wasi-threads@1.2.0': resolution: {integrity: sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg==} + '@emotion/babel-plugin@11.13.5': + resolution: {integrity: sha512-pxHCpT2ex+0q+HH91/zsdHkw/lXd468DIN2zvfvLtPKLLMo6gQj7oLObq8PhkrxOZb/gGCq03S3Z7PDhS8pduQ==} + + '@emotion/cache@11.14.0': + resolution: {integrity: sha512-L/B1lc/TViYk4DcpGxtAVbx0ZyiKM5ktoIyafGkH6zg/tj+mA+NE//aPYKG0k8kCHSHVJrpLpcAlOBEXQ3SavA==} + + '@emotion/hash@0.9.2': + resolution: {integrity: sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==} + + '@emotion/memoize@0.9.0': + resolution: {integrity: sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==} + + '@emotion/react@11.14.0': + resolution: {integrity: sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==} + peerDependencies: + '@types/react': '*' + react: '>=16.8.0' + peerDependenciesMeta: + '@types/react': + optional: true + + '@emotion/serialize@1.3.3': + resolution: {integrity: sha512-EISGqt7sSNWHGI76hC7x1CksiXPahbxEOrC5RjmFRJTqLyEK9/9hZvBbiYn70dw4wuwMKiEMCUlR6ZXTSWQqxA==} + + '@emotion/sheet@1.4.0': + resolution: {integrity: sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg==} + + '@emotion/unitless@0.10.0': + resolution: {integrity: sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg==} + + '@emotion/use-insertion-effect-with-fallbacks@1.2.0': + resolution: {integrity: sha512-yJMtVdH59sxi/aVJBpk9FQq+OR8ll5GT8oWd57UpeaKEVGab41JWaCFA7FRLoMLloOZF/c/wsPoe+bfGmRKgDg==} + peerDependencies: + react: '>=16.8.0' + + '@emotion/utils@1.4.2': + resolution: {integrity: sha512-3vLclRofFziIa3J2wDh9jjbkUz9qk5Vi3IZ/FSTKViB0k+ef0fPV7dYrUIugbgupYDx7v9ud/SjrtEP8Y4xLoA==} + + '@emotion/weak-memoize@0.4.0': + resolution: {integrity: sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==} + '@esbuild/aix-ppc64@0.27.7': resolution: {integrity: sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==} engines: {node: '>=18'} @@ -422,6 +511,29 @@ packages: '@floating-ui/utils@0.2.11': resolution: {integrity: sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==} + '@fortawesome/fontawesome-common-types@6.7.2': + resolution: {integrity: sha512-Zs+YeHUC5fkt7Mg1l6XTniei3k4bwG/yo3iFUtZWd/pMx9g3fdvkSK9E0FOC+++phXOka78uJcYb8JaFkW52Xg==} + engines: {node: '>=6'} + + '@fortawesome/fontawesome-svg-core@6.7.2': + resolution: {integrity: sha512-yxtOBWDrdi5DD5o1pmVdq3WMCvnobT0LU6R8RyyVXPvFRd2o79/0NCuQoCjNTeZz9EzA9xS3JxNWfv54RIHFEA==} + engines: {node: '>=6'} + + '@fortawesome/free-brands-svg-icons@6.7.2': + resolution: {integrity: sha512-zu0evbcRTgjKfrr77/2XX+bU+kuGfjm0LbajJHVIgBWNIDzrhpRxiCPNT8DW5AdmSsq7Mcf9D1bH0aSeSUSM+Q==} + engines: {node: '>=6'} + + '@fortawesome/free-solid-svg-icons@6.7.2': + resolution: {integrity: sha512-GsBrnOzU8uj0LECDfD5zomZJIjrPhIlWU82AHwa2s40FKH+kcxQaBvBo3Z4TxyZHIyX8XTDxsyA33/Vx9eFuQA==} + engines: {node: '>=6'} + + '@fortawesome/react-fontawesome@0.2.6': + resolution: {integrity: sha512-mtBFIi1UsYQo7rYonYFkjgYKGoL8T+fEH6NGUpvuqtY3ytMsAoDaPo5rk25KuMtKDipY4bGYM/CkmCHA1N3FUg==} + deprecated: v0.2.x is no longer supported. Unless you are still using FontAwesome 5, please update to v3.1.1 or greater. + peerDependencies: + '@fortawesome/fontawesome-svg-core': ~1 || ~6 || ~7 + react: ^16.3 || ^17.0.0 || ^18.0.0 || ^19.0.0 + '@humanfs/core@0.19.1': resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} engines: {node: '>=18.18.0'} @@ -575,6 +687,15 @@ packages: cpu: [x64] os: [win32] + '@internationalized/date@3.12.1': + resolution: {integrity: sha512-6IedsVWXyq4P9Tj+TxuU8WGWM70hYLl12nbYU8jkikVpa6WXapFazPUcHUMDMoWftIDE2ILDkFFte6W2nFCkRQ==} + + '@internationalized/number@3.6.6': + resolution: {integrity: sha512-iFgmQaXHE0vytNfpLZWOC2mEJCBRzcUxt53Xf/yCXG93lRvqas237i3r7X4RKMwO3txiyZD4mQjKAByFv6UGSQ==} + + '@internationalized/string@3.2.8': + resolution: {integrity: sha512-NdbMQUSfXLYIQol5VyMtinm9pZDciiMfN7RtmSuSB78io1hqwJ0naYfxyW6vgxWBkzWymQa/3uLDlbfmshtCaA==} + '@jridgewell/gen-mapping@0.3.13': resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} @@ -739,6 +860,91 @@ packages: resolution: {integrity: sha512-cifvXDhcqMwwTlTK04GBNeIe7yyo28Mfby85QXFe1Yk8nmi36Ab/5UQwptOx84SsoGNRg+EVSjwzfSZMy6pmlw==} engines: {node: '>=14'} + '@parcel/watcher-android-arm64@2.5.6': + resolution: {integrity: sha512-YQxSS34tPF/6ZG7r/Ih9xy+kP/WwediEUsqmtf0cuCV5TPPKw/PQHRhueUo6JdeFJaqV3pyjm0GdYjZotbRt/A==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [android] + + '@parcel/watcher-darwin-arm64@2.5.6': + resolution: {integrity: sha512-Z2ZdrnwyXvvvdtRHLmM4knydIdU9adO3D4n/0cVipF3rRiwP+3/sfzpAwA/qKFL6i1ModaabkU7IbpeMBgiVEA==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [darwin] + + '@parcel/watcher-darwin-x64@2.5.6': + resolution: {integrity: sha512-HgvOf3W9dhithcwOWX9uDZyn1lW9R+7tPZ4sug+NGrGIo4Rk1hAXLEbcH1TQSqxts0NYXXlOWqVpvS1SFS4fRg==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [darwin] + + '@parcel/watcher-freebsd-x64@2.5.6': + resolution: {integrity: sha512-vJVi8yd/qzJxEKHkeemh7w3YAn6RJCtYlE4HPMoVnCpIXEzSrxErBW5SJBgKLbXU3WdIpkjBTeUNtyBVn8TRng==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [freebsd] + + '@parcel/watcher-linux-arm-glibc@2.5.6': + resolution: {integrity: sha512-9JiYfB6h6BgV50CCfasfLf/uvOcJskMSwcdH1PHH9rvS1IrNy8zad6IUVPVUfmXr+u+Km9IxcfMLzgdOudz9EQ==} + engines: {node: '>= 10.0.0'} + cpu: [arm] + os: [linux] + + '@parcel/watcher-linux-arm-musl@2.5.6': + resolution: {integrity: sha512-Ve3gUCG57nuUUSyjBq/MAM0CzArtuIOxsBdQ+ftz6ho8n7s1i9E1Nmk/xmP323r2YL0SONs1EuwqBp2u1k5fxg==} + engines: {node: '>= 10.0.0'} + cpu: [arm] + os: [linux] + + '@parcel/watcher-linux-arm64-glibc@2.5.6': + resolution: {integrity: sha512-f2g/DT3NhGPdBmMWYoxixqYr3v/UXcmLOYy16Bx0TM20Tchduwr4EaCbmxh1321TABqPGDpS8D/ggOTaljijOA==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [linux] + + '@parcel/watcher-linux-arm64-musl@2.5.6': + resolution: {integrity: sha512-qb6naMDGlbCwdhLj6hgoVKJl2odL34z2sqkC7Z6kzir8b5W65WYDpLB6R06KabvZdgoHI/zxke4b3zR0wAbDTA==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [linux] + + '@parcel/watcher-linux-x64-glibc@2.5.6': + resolution: {integrity: sha512-kbT5wvNQlx7NaGjzPFu8nVIW1rWqV780O7ZtkjuWaPUgpv2NMFpjYERVi0UYj1msZNyCzGlaCWEtzc+exjMGbQ==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [linux] + + '@parcel/watcher-linux-x64-musl@2.5.6': + resolution: {integrity: sha512-1JRFeC+h7RdXwldHzTsmdtYR/Ku8SylLgTU/reMuqdVD7CtLwf0VR1FqeprZ0eHQkO0vqsbvFLXUmYm/uNKJBg==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [linux] + + '@parcel/watcher-win32-arm64@2.5.6': + resolution: {integrity: sha512-3ukyebjc6eGlw9yRt678DxVF7rjXatWiHvTXqphZLvo7aC5NdEgFufVwjFfY51ijYEWpXbqF5jtrK275z52D4Q==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [win32] + + '@parcel/watcher-win32-ia32@2.5.6': + resolution: {integrity: sha512-k35yLp1ZMwwee3Ez/pxBi5cf4AoBKYXj00CZ80jUz5h8prpiaQsiRPKQMxoLstNuqe2vR4RNPEAEcjEFzhEz/g==} + engines: {node: '>= 10.0.0'} + cpu: [ia32] + os: [win32] + + '@parcel/watcher-win32-x64@2.5.6': + resolution: {integrity: sha512-hbQlYcCq5dlAX9Qx+kFb0FHue6vbjlf0FrNzSKdYK2APUf7tGfGxQCk2ihEREmbR6ZMc0MVAD5RIX/41gpUzTw==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [win32] + + '@parcel/watcher@2.5.6': + resolution: {integrity: sha512-tmmZ3lQxAe/k/+rNnXQRawJ4NjxO2hqiOLTHvWchtGZULp4RyFeh6aU4XdOYBFe2KE1oShQTv4AblOs2iOrNnQ==} + engines: {node: '>= 10.0.0'} + + '@popperjs/core@2.11.8': + resolution: {integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==} + '@posthog/core@1.25.2': resolution: {integrity: sha512-h2FO7ut/BbfwpAXWpwdDHTzQgUo9ibDFEs6ZO+3cI3KPWQt5XwczK1OLAuPprcjm8T/jl0SH8jSFo5XdU4RbTg==} @@ -1027,6 +1233,11 @@ packages: '@radix-ui/rect@1.1.1': resolution: {integrity: sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==} + '@react-types/shared@3.34.0': + resolution: {integrity: sha512-gp6xo/s2lX54AlTjOiqwDnxA7UW79BNvI9dB9pr3LZTzRKCd1ZA+ZbgKw/ReIiWuvvVw/8QFJpnqeeFyLocMcQ==} + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + '@rtsao/scc@1.1.0': resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==} @@ -1121,6 +1332,42 @@ packages: '@tailwindcss/postcss@4.2.2': resolution: {integrity: sha512-n4goKQbW8RVXIbNKRB/45LzyUqN451deQK0nzIeauVEqjlI49slUlgKYJM2QyUzap/PcpnS7kzSUmPb1sCRvYQ==} + '@tippyjs/react@4.2.6': + resolution: {integrity: sha512-91RicDR+H7oDSyPycI13q3b7o4O60wa2oRbjlz2fyRLmHImc4vyDwuUP8NtZaN0VARJY5hybvDYrFzhY9+Lbyw==} + peerDependencies: + react: '>=16.8' + react-dom: '>=16.8' + + '@turf/bbox@7.3.5': + resolution: {integrity: sha512-oG1ya/HtBjAIg4TimbWx+nOYPbY0bCvt82Bq8tm6sBw3qqtbOyRSfDz79Sq90TnH7DXJprJ1qnVGKNtZ6jemfw==} + + '@turf/boolean-disjoint@7.3.5': + resolution: {integrity: sha512-Pz1GGUC6iL6xGVqhyo+fYg35kl4j/HONMEoC1voN3DcBOCcVlzq+ljvMpvE+oQiR7Q38xLhIxZneLCqMp5YrQA==} + + '@turf/boolean-intersects@7.3.5': + resolution: {integrity: sha512-Z6GPYjozrmTuzWQD0x7o8RPm+4HC7hz9q23hdB3U1+Qahesv8Mtc+wo82tO4CG6/NRnJ9u79DlEhmR1DxUU4iQ==} + + '@turf/boolean-point-in-polygon@7.3.5': + resolution: {integrity: sha512-ba7+B0wzaS9GtERZOoXUZ6oW8IcIJHNQZf3c+tiD9ESjcsPO1Q/4qIJGTKl92nBLhhracHJxMWBM/U6hAVkaRg==} + + '@turf/center@7.3.5': + resolution: {integrity: sha512-eub5/Kfdmn89ZqwCONHI7astmTDEtN5M6+JfOkgoSyhKKFhUJYNxUyH1F/vCtIP7j1K369Vs4L9TYiuGapvIKQ==} + + '@turf/helpers@7.3.5': + resolution: {integrity: sha512-E/NMGV5MwbjjP7AJXBtsanC3yY8N2MQ87IGdIgkB2ji5AtBpwnH4L3gEqpYN4RlCJJWbLbzO91BbKv2waUd0eg==} + + '@turf/invariant@7.3.5': + resolution: {integrity: sha512-ZVIvsBvjr8lO7WxC5zYNjRsjSDvyGvWkJMjuWaJjTU8x+1tmfNnw3gDX/TI2Sit83gcRYLYkNo23lB/udqx/Hg==} + + '@turf/line-intersect@7.3.5': + resolution: {integrity: sha512-2Cl4oPsjaDdfIwz/5IRDdG2fNdfp3W6atICm81vnzl/GwURoVP+CLjXJ64QWWzpzIbgX2XprJQTmamByDt5MDw==} + + '@turf/meta@7.3.5': + resolution: {integrity: sha512-r+ohqxoyqeigFB0oFrQx/YEHIkOKqcKpCjvZkvZs7Tkv+IFco5MezAd2zd4rzK+0DfFgDP3KpJc7HqrYjvEjhg==} + + '@turf/polygon-to-line@7.3.5': + resolution: {integrity: sha512-Mat5tvJcW3grpXCNFcMvjHL3d8hO4eoIgF3qYpXj25BHx/S7SJUOgyCV5x3arC0rCfM/cB71VmNDm9k57ec7bw==} + '@tybys/wasm-util@0.10.1': resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==} @@ -1133,6 +1380,9 @@ packages: '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + '@types/geojson@7946.0.16': + resolution: {integrity: sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==} + '@types/hast@3.0.4': resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==} @@ -1151,11 +1401,19 @@ packages: '@types/node@20.19.37': resolution: {integrity: sha512-8kzdPJ3FsNsVIurqBs7oodNnCEVbni9yUEkaHbgptDACOPW04jimGagZ51E6+lXUwJjgnBw+hyko/lkFWCldqw==} + '@types/parse-json@4.0.2': + resolution: {integrity: sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==} + '@types/react-dom@19.2.3': resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==} peerDependencies: '@types/react': ^19.2.0 + '@types/react-transition-group@4.4.12': + resolution: {integrity: sha512-8TV6R3h2j7a91c+1DXdJi3Syo69zzIZbz7Lg5tORM5LEJG7X/E6a1V3drRyBRZq7/utz7A+c4OgYLiLcYGHG6w==} + peerDependencies: + '@types/react': '*' + '@types/react@19.2.14': resolution: {integrity: sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==} @@ -1404,6 +1662,10 @@ packages: resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==} engines: {node: '>= 0.4'} + babel-plugin-macros@3.1.0: + resolution: {integrity: sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==} + engines: {node: '>=10', npm: '>=6'} + bail@2.0.2: resolution: {integrity: sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==} @@ -1483,9 +1745,16 @@ packages: chart.js: '>=2.8.0' date-fns: '>=2.0.0' + chokidar@4.0.3: + resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} + engines: {node: '>= 14.16.0'} + class-variance-authority@0.7.1: resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==} + classnames@2.5.1: + resolution: {integrity: sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==} + client-only@0.0.1: resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==} @@ -1500,18 +1769,35 @@ packages: color-name@1.1.4: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + colorbrewer@1.7.0: + resolution: {integrity: sha512-hctXi+S4uPJFnzLS/v8LJqe9WQjOITs9Wo9sGQMHpB5kapFDMB28PUY2zKG54LkxF7qzME3BEbTzRloFM241hQ==} + comma-separated-tokens@2.0.3: resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==} + commander@2.20.3: + resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} + + commander@7.2.0: + resolution: {integrity: sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==} + engines: {node: '>= 10'} + concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + convert-source-map@1.9.0: + resolution: {integrity: sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==} + convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} core-js@3.49.0: resolution: {integrity: sha512-es1U2+YTtzpwkxVLwAFdSpaIMyQaq0PBgm3YD1W3Qpsn1NAmO3KSgZfu+oGSWVu6NvLHoHCV/aYcsE5wiB7ALg==} + cosmiconfig@7.1.0: + resolution: {integrity: sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==} + engines: {node: '>=10'} + cross-spawn@7.0.6: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} @@ -1519,6 +1805,136 @@ packages: csstype@3.2.3: resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + d3-array@3.2.4: + resolution: {integrity: sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==} + engines: {node: '>=12'} + + d3-axis@3.0.0: + resolution: {integrity: sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==} + engines: {node: '>=12'} + + d3-brush@3.0.0: + resolution: {integrity: sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==} + engines: {node: '>=12'} + + d3-chord@3.0.1: + resolution: {integrity: sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==} + engines: {node: '>=12'} + + d3-color@3.1.0: + resolution: {integrity: sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==} + engines: {node: '>=12'} + + d3-contour@4.0.2: + resolution: {integrity: sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==} + engines: {node: '>=12'} + + d3-delaunay@6.0.4: + resolution: {integrity: sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==} + engines: {node: '>=12'} + + d3-dispatch@3.0.1: + resolution: {integrity: sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==} + engines: {node: '>=12'} + + d3-drag@3.0.0: + resolution: {integrity: sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==} + engines: {node: '>=12'} + + d3-dsv@3.0.1: + resolution: {integrity: sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==} + engines: {node: '>=12'} + hasBin: true + + d3-ease@3.0.1: + resolution: {integrity: sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==} + engines: {node: '>=12'} + + d3-fetch@3.0.1: + resolution: {integrity: sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==} + engines: {node: '>=12'} + + d3-force@3.0.0: + resolution: {integrity: sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==} + engines: {node: '>=12'} + + d3-format@3.1.2: + resolution: {integrity: sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==} + engines: {node: '>=12'} + + d3-geo@3.1.1: + resolution: {integrity: sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==} + engines: {node: '>=12'} + + d3-hierarchy@3.1.2: + resolution: {integrity: sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==} + engines: {node: '>=12'} + + d3-interpolate@3.0.1: + resolution: {integrity: sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==} + engines: {node: '>=12'} + + d3-path@3.1.0: + resolution: {integrity: sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==} + engines: {node: '>=12'} + + d3-polygon@3.0.1: + resolution: {integrity: sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==} + engines: {node: '>=12'} + + d3-quadtree@3.0.1: + resolution: {integrity: sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==} + engines: {node: '>=12'} + + d3-random@3.0.1: + resolution: {integrity: sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==} + engines: {node: '>=12'} + + d3-scale-chromatic@3.1.0: + resolution: {integrity: sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==} + engines: {node: '>=12'} + + d3-scale@4.0.2: + resolution: {integrity: sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==} + engines: {node: '>=12'} + + d3-selection@3.0.0: + resolution: {integrity: sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==} + engines: {node: '>=12'} + + d3-shape@3.2.0: + resolution: {integrity: sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==} + engines: {node: '>=12'} + + d3-time-format@4.1.0: + resolution: {integrity: sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==} + engines: {node: '>=12'} + + d3-time@3.1.0: + resolution: {integrity: sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==} + engines: {node: '>=12'} + + d3-timer@1.0.10: + resolution: {integrity: sha512-B1JDm0XDaQC+uvo4DT79H0XmBskgS3l6Ve+1SBCfxgmtIb1AVrPIoqd+nPSv+loMX8szQ0sVUhGngL7D5QPiXw==} + + d3-timer@3.0.1: + resolution: {integrity: sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==} + engines: {node: '>=12'} + + d3-transition@3.0.1: + resolution: {integrity: sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==} + engines: {node: '>=12'} + peerDependencies: + d3-selection: 2 - 3 + + d3-zoom@3.0.0: + resolution: {integrity: sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==} + engines: {node: '>=12'} + + d3@7.9.0: + resolution: {integrity: sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA==} + engines: {node: '>=12'} + damerau-levenshtein@1.0.8: resolution: {integrity: sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==} @@ -1537,6 +1953,9 @@ packages: date-fns@4.1.0: resolution: {integrity: sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==} + dayjs@1.11.20: + resolution: {integrity: sha512-YbwwqR/uYpeoP4pu043q+LTDLFBLApUP6VxRihdfNTqu4ubqMlGDLd6ErXhEgsyvY0K6nCs7nggYumAN+9uEuQ==} + debug@3.2.7: resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==} peerDependencies: @@ -1568,6 +1987,9 @@ packages: resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} engines: {node: '>= 0.4'} + delaunator@5.1.0: + resolution: {integrity: sha512-AGrQ4QSgssa1NGmWmLPqN5NY2KajF5MqxetNEO+o0n3ZwZZeTmt7bBnvzHWrmkZFxGgr4HdyFgelzgi06otLuQ==} + dequal@2.0.3: resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} engines: {node: '>=6'} @@ -1586,6 +2008,9 @@ packages: resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==} engines: {node: '>=0.10.0'} + dom-helpers@5.2.1: + resolution: {integrity: sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==} + dompurify@3.3.3: resolution: {integrity: sha512-Oj6pzI2+RqBfFG+qOaOLbFXLQ90ARpcGG6UePL82bJLtdsa6CYJD7nmiU8MW9nQNOtCHV3lZ/Bzq1X0QYbBZCA==} @@ -1603,6 +2028,9 @@ packages: resolution: {integrity: sha512-Qohcme7V1inbAfvjItgw0EaxVX5q2rdVEZHRBrEQdRZTssLDGsL8Lwrznl8oQ/6kuTJONLaDcGjkNP247XEhcA==} engines: {node: '>=10.13.0'} + error-ex@1.3.4: + resolution: {integrity: sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==} + es-abstract@1.24.1: resolution: {integrity: sha512-zHXBLhP+QehSSbsS9Pt23Gg964240DPd6QCf8WpkqEXxQ7fhdZzYsocOr5u7apWonsS5EjZDmTF+/slGMyasvw==} engines: {node: '>= 0.4'} @@ -1648,6 +2076,10 @@ packages: resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} engines: {node: '>=10'} + escape-string-regexp@5.0.0: + resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==} + engines: {node: '>=12'} + eslint-config-next@16.1.6: resolution: {integrity: sha512-vKq40io2B0XtkkNDYyleATwblNt8xuh3FWp8SpSz3pt7P01OkBFlKsJZ2mWt5WsCySlDQLckb1zMY9yE9Qy0LA==} peerDependencies: @@ -1810,6 +2242,9 @@ packages: resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} engines: {node: '>=8'} + find-root@1.1.0: + resolution: {integrity: sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==} + find-up@5.0.0: resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} engines: {node: '>=10'} @@ -1821,10 +2256,17 @@ packages: flatted@3.4.2: resolution: {integrity: sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==} + flip-toolkit@7.2.4: + resolution: {integrity: sha512-NT81ikyHPk72riMe1U01x698YIMSypMF5mQBhRklWVgf2xgWH3EPfrrVRAkz/+TSCq0rLPsr/uKIYkwHrowKZQ==} + engines: {node: '>=8', npm: '>=5'} + for-each@0.3.5: resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==} engines: {node: '>= 0.4'} + fparser@3.1.0: + resolution: {integrity: sha512-P9hS9RjO7l4JvWHcDUqos0BXAGzJN4WwJBCh7gwja/23TuW7jfpOKZ+jlGoYp4ZUDnbAJ+rDyKLkIJFCLzgZ+w==} + fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} @@ -1840,6 +2282,9 @@ packages: functions-have-names@1.2.3: resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} + fuzzysort@3.1.0: + resolution: {integrity: sha512-sR9BNCjBg6LNgwvxlBd0sBABvQitkLzoVY9MYYROQVX/FvfJ4Mai9LsGhDgd8qYdds0bY77VzYd5iuB+v5rwQQ==} + generator-function@2.0.1: resolution: {integrity: sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==} engines: {node: '>= 0.4'} @@ -1933,9 +2378,16 @@ packages: hermes-parser@0.25.1: resolution: {integrity: sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==} + hoist-non-react-statics@3.3.2: + resolution: {integrity: sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==} + html-url-attributes@3.0.1: resolution: {integrity: sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==} + iconv-lite@0.6.3: + resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} + engines: {node: '>=0.10.0'} + ignore@5.3.2: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} @@ -1944,6 +2396,9 @@ packages: resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} engines: {node: '>= 4'} + immutable@5.1.5: + resolution: {integrity: sha512-t7xcm2siw+hlUM68I+UEOK+z84RzmN59as9DZ7P1l0994DKUWV7UXBMQZVxaoMSRQ+PBZbHCOoBt7a2wxOMt+A==} + import-fresh@3.3.1: resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} engines: {node: '>=6'} @@ -1952,6 +2407,10 @@ packages: resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} engines: {node: '>=0.8.19'} + indefinite@2.5.2: + resolution: {integrity: sha512-J3ELLIk835hmgDMUfNltTCrHz9+CteTnSuXJqvxZT18wo1U2M9/QeeJMw99QdZwPEEr1DE2aBYda101Ojjdw5g==} + engines: {node: '>=6.0.0'} + inline-style-parser@0.2.7: resolution: {integrity: sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==} @@ -1959,6 +2418,10 @@ packages: resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==} engines: {node: '>= 0.4'} + internmap@2.0.3: + resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==} + engines: {node: '>=12'} + is-alphabetical@2.0.1: resolution: {integrity: sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==} @@ -1969,6 +2432,9 @@ packages: resolution: {integrity: sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==} engines: {node: '>= 0.4'} + is-arrayish@0.2.1: + resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} + is-async-function@2.1.1: resolution: {integrity: sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==} engines: {node: '>= 0.4'} @@ -2092,6 +2558,10 @@ packages: resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} hasBin: true + js-cookie@3.0.5: + resolution: {integrity: sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==} + engines: {node: '>=14'} + js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} @@ -2107,6 +2577,9 @@ packages: json-buffer@3.0.1: resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + json-parse-even-better-errors@2.3.1: + resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} + json-schema-traverse@0.4.1: resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} @@ -2126,6 +2599,9 @@ packages: resolution: {integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==} engines: {node: '>=4.0'} + kapellmeister@3.0.1: + resolution: {integrity: sha512-S7+gYcziMREv8RxG46138mb1O4Xf9II/bCxEJPYkhlZ7PgGWTlicgsyNad/DGc5oEAlWGLXE5ExLbTDVvJmgDA==} + keyv@4.5.4: resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} @@ -2210,10 +2686,19 @@ packages: resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==} engines: {node: '>= 12.0.0'} + lines-and-columns@1.2.4: + resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + locate-path@6.0.0: resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} engines: {node: '>=10'} + lodash-es@4.18.1: + resolution: {integrity: sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==} + + lodash.deburr@4.1.0: + resolution: {integrity: sha512-m/M1U1f3ddMCs6Hq2tAsYThTBDaAKFDX3dwDo97GEYzamXi9SqUpjWi/Rrj/gf3X2n8ktwgZrlP1z6E3v/IExQ==} + lodash.merge@4.6.2: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} @@ -2242,6 +2727,9 @@ packages: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} + mdast-util-find-and-replace@3.0.2: + resolution: {integrity: sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==} + mdast-util-from-markdown@2.0.3: resolution: {integrity: sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q==} @@ -2266,6 +2754,9 @@ packages: mdast-util-to-string@4.0.0: resolution: {integrity: sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==} + memoize-one@6.0.0: + resolution: {integrity: sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==} + merge2@1.4.1: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} engines: {node: '>= 8'} @@ -2347,6 +2838,38 @@ packages: minimist@1.2.8: resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + mobx-react-lite@3.4.3: + resolution: {integrity: sha512-NkJREyFTSUXR772Qaai51BnE1voWx56LOL80xG7qkZr6vo8vEaLF3sz1JNUVh+rxmUzxYaqOhfuxTfqUh0FXUg==} + peerDependencies: + mobx: ^6.1.0 + react: ^16.8.0 || ^17 || ^18 + react-dom: '*' + react-native: '*' + peerDependenciesMeta: + react-dom: + optional: true + react-native: + optional: true + + mobx-react@7.6.0: + resolution: {integrity: sha512-+HQUNuh7AoQ9ZnU6c4rvbiVVl+wEkb9WqYsVDzGLng+Dqj1XntHu79PvEWKtSMoMj67vFp/ZPXcElosuJO8ckA==} + peerDependencies: + mobx: ^6.1.0 + react: ^16.8.0 || ^17 || ^18 + react-dom: '*' + react-native: '*' + peerDependenciesMeta: + react-dom: + optional: true + react-native: + optional: true + + mobx@6.15.3: + resolution: {integrity: sha512-6+ZSYDs5zgH5CdGfEU2q2Lsa5PztVryL1ys7kAImTU25n2A9LAMj/yneVsQpd03MfwMLDQF+7kakJR9Z+cQxSw==} + + mousetrap@1.6.5: + resolution: {integrity: sha512-QNo4kEepaIBwiT8CDhP98umTetp+JNfQYBWvC1pc6/OAibuXtRcxZ58Qz8skvEHYvURne/7R8T5VoOI7rDsEUA==} + ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} @@ -2384,6 +2907,9 @@ packages: sass: optional: true + node-addon-api@7.1.1: + resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==} + node-exports-info@1.6.0: resolution: {integrity: sha512-pyFS63ptit/P5WqUkt+UUfe+4oevH+bFeIiPPdfb0pFeYEu/1ELnJu5l+5EcTKYL5M7zaAa7S8ddywgXypqKCw==} engines: {node: '>= 0.4'} @@ -2439,6 +2965,9 @@ packages: resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} engines: {node: '>=10'} + papaparse@5.5.3: + resolution: {integrity: sha512-5QvjGxYVjxO59MGU2lHVYpRWBBtKHnlIAcSe1uNFCkkptUh63NFRj0FJQm7nR67puEruUci/ZkjmEFrjCAyP4A==} + parent-module@1.0.1: resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} engines: {node: '>=6'} @@ -2446,6 +2975,10 @@ packages: parse-entities@4.0.2: resolution: {integrity: sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==} + parse-json@5.2.0: + resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} + engines: {node: '>=8'} + path-exists@4.0.0: resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} engines: {node: '>=8'} @@ -2457,6 +2990,10 @@ packages: path-parse@1.0.7: resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + path-type@4.0.0: + resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} + engines: {node: '>=8'} + picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} @@ -2468,6 +3005,9 @@ packages: resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} engines: {node: '>=12'} + point-in-polygon-hao@1.2.4: + resolution: {integrity: sha512-x2pcvXeqhRHlNRdhLs/tgFapAbSSe86wa/eqmj1G6pWftbEs5aVRJhRGM6FYSUERKu0PjekJzMq0gsI2XyiclQ==} + possible-typed-array-names@1.1.0: resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==} engines: {node: '>= 0.4'} @@ -2507,9 +3047,24 @@ packages: query-selector-shadow-dom@1.0.1: resolution: {integrity: sha512-lT5yCqEBgfoMYpf3F2xQRK7zEr1rhIIZuceDK6+xRkJQ4NMbHTwXqk4NkwDwQMNqXgG9r9fyHnzwNVs6zV5KRw==} + querystringify@2.2.0: + resolution: {integrity: sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==} + queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + react-aria-components@1.17.0: + resolution: {integrity: sha512-0EyisMgvsFJ2aML3crDYv2tW5vT2Ryf8PGzY/g63JjDdCbLshlwazhS8JNtPF1vkTkungJJ6sVJbKyX+YKSoFA==} + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + + react-aria@3.48.0: + resolution: {integrity: sha512-jQjd4rBEIMqecBaAKYJbVGK6EqIHLa5znVQ7jwFyK5vCyljoj6KhgtiahmcIPsG5vG5vEDLw+ba+bEWn6A2P4w==} + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + react-chartjs-2@5.3.1: resolution: {integrity: sha512-h5IPXKg9EXpjoBzUfyWJvllMjG2mQ4EiuHQFhms/AjUm0XSZHhyRy2xVmLXHKrtcdrPO4mnGqRtYoD0vp95A0A==} peerDependencies: @@ -2521,6 +3076,13 @@ packages: peerDependencies: react: ^19.2.3 + react-flip-toolkit@7.2.4: + resolution: {integrity: sha512-+aZBZwzHBDfJzMRLjAAOKiBkrk8PV0YFVE0k1x85+UsykApLwg8a0LlQ5H80uFltFZmGtrQ7EE1krhiZ1jI5ig==} + engines: {node: '>=8', npm: '>=5'} + peerDependencies: + react: '>= 16.x' + react-dom: '>= 16.x' + react-is@16.13.1: resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} @@ -2530,6 +3092,11 @@ packages: '@types/react': '>=18' react: '>=18' + react-move@6.5.0: + resolution: {integrity: sha512-tl8zwCqtXXWfmrUJGnkyPMNhx8DUTy1NugEuPW/JTMp2TGSEC819aMXGYMG8FWFzV9I6jy4kbgoZJnBpmZRktA==} + peerDependencies: + react: '>=16.3.0' + react-remove-scroll-bar@2.3.8: resolution: {integrity: sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==} engines: {node: '>=10'} @@ -2550,6 +3117,17 @@ packages: '@types/react': optional: true + react-select@5.10.2: + resolution: {integrity: sha512-Z33nHdEFWq9tfnfVXaiM12rbJmk+QjFEztWLtmXqQhz6Al4UZZ9xc0wiatmGtUOCCnHN0WizL3tCMYRENX4rVQ==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + react-stately@3.46.0: + resolution: {integrity: sha512-OdxhWvHgs2L4OJGIs7hnuTr5WjjMM6enhNEAMRqiekhF8+ITvA2LRwNftOZwcogaoCslGYq5S2VQTQwnm0GbCA==} + peerDependencies: + react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1 + react-style-singleton@2.2.3: resolution: {integrity: sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==} engines: {node: '>=10'} @@ -2560,10 +3138,20 @@ packages: '@types/react': optional: true + react-transition-group@4.4.5: + resolution: {integrity: sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==} + peerDependencies: + react: '>=16.6.0' + react-dom: '>=16.6.0' + react@19.2.3: resolution: {integrity: sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==} engines: {node: '>=0.10.0'} + readdirp@4.1.2: + resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} + engines: {node: '>= 14.18.0'} + reflect.getprototypeof@1.0.10: resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==} engines: {node: '>= 0.4'} @@ -2578,6 +3166,15 @@ packages: remark-rehype@11.1.2: resolution: {integrity: sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==} + rematrix@0.2.2: + resolution: {integrity: sha512-agFFS3RzrLXJl5LY5xg/xYyXvUuVAnkhgKO7RaO9J1Ssth6yvbO+PIiV67V59MB5NCdAK2flvGvNT4mdKVniFA==} + + remeda@2.34.0: + resolution: {integrity: sha512-zL4cEPkLHxwmlDRPyvJZjojpG5M5HXrDiABNKof+dq7kkuyQttP6NrF2uJB0DKIU09K8cTq+sQDlbo2r7mdR5Q==} + + requires-port@1.0.0: + resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==} + reselect@5.1.1: resolution: {integrity: sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==} @@ -2602,9 +3199,15 @@ packages: resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + robust-predicates@3.0.3: + resolution: {integrity: sha512-NS3levdsRIUOmiJ8FZWCP7LG3QpJyrs/TE0Zpf1yvZu8cAJJ6QMW92H1c7kWpdIHo8RvmLxN/o2JXTKHp74lUA==} + run-parallel@1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + rw@1.3.3: + resolution: {integrity: sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==} + safe-array-concat@1.1.3: resolution: {integrity: sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==} engines: {node: '>=0.4'} @@ -2617,6 +3220,14 @@ packages: resolution: {integrity: sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==} engines: {node: '>= 0.4'} + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + + sass@1.99.0: + resolution: {integrity: sha512-kgW13M54DUB7IsIRM5LvJkNlpH+WhMpooUcaWGFARkF1Tc82v9mIWkCbCYf+MBvpIUBSeSOTilpZjEPr2VYE6Q==} + engines: {node: '>=14.0.0'} + hasBin: true + scheduler@0.27.0: resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} @@ -2678,6 +3289,9 @@ packages: resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} engines: {node: '>= 0.4'} + simple-statistics@7.8.9: + resolution: {integrity: sha512-YT6MLqYsz7y1rQZOLFlOCCgSRpCi6bqY417yhoOLI7aVoBi29dD39EPrOE03W9DY25H0J0jizVsHZnkLzyGJFg==} + sonner@2.0.7: resolution: {integrity: sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w==} peerDependencies: @@ -2688,6 +3302,10 @@ packages: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} + source-map@0.5.7: + resolution: {integrity: sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==} + engines: {node: '>=0.10.0'} + space-separated-tokens@2.0.2: resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==} @@ -2698,6 +3316,9 @@ packages: resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==} engines: {node: '>= 0.4'} + string-pixel-width@1.11.0: + resolution: {integrity: sha512-GeKuNcCza7Gf3tlMJiZY8SF1LtbFGeMUEpHifncgJn+ZcUpnoPyE69HEyb0rXrJ3bejY/M/kBylu7IDlPJD9Ng==} + string.prototype.includes@2.0.1: resolution: {integrity: sha512-o7+c9bW6zpAdJHTtujeePODAhkuicdAryFsfVKwA+wGw89wJ4GTY484WTucM9hLtDEOpOvI+aHnzqnC5lHp4Rg==} engines: {node: '>= 0.4'} @@ -2732,6 +3353,9 @@ packages: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} engines: {node: '>=8'} + striptags@3.2.0: + resolution: {integrity: sha512-g45ZOGzHDMe2bdYMdIvdAfCQkCTDMGBazSw1ypMowwGIee7ZQ5dU0rBJ8Jqgl+jAKIv4dbeE1jscZq9wid1Tkw==} + style-to-js@1.1.21: resolution: {integrity: sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ==} @@ -2751,6 +3375,9 @@ packages: babel-plugin-macros: optional: true + stylis@4.2.0: + resolution: {integrity: sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==} + supports-color@7.2.0: resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} engines: {node: '>=8'} @@ -2759,6 +3386,9 @@ packages: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} + sweepline-intersections@1.5.0: + resolution: {integrity: sha512-AoVmx72QHpKtItPu72TzFL+kcYjd67BPLDoR0LarIk+xyaRg+pDTMFXndIEvZf9xEKnJv6JdhgRMnocoG0D3AQ==} + swr@2.4.1: resolution: {integrity: sha512-2CC6CiKQtEwaEeNiqWTAw9PGykW8SR5zZX8MZk6TeAvEAnVS7Visz8WzphqgtQ8v2xz/4Q5K+j+SeMaKXeeQIA==} peerDependencies: @@ -2781,10 +3411,20 @@ packages: resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} engines: {node: '>=12.0.0'} + tinyqueue@2.0.3: + resolution: {integrity: sha512-ppJZNDuKGgxzkHihX8v9v9G5f+18gzaTfrukGrq6ueg0lmH4nqVnA2IPG0AEH3jKEk2GRJCUhDoqpoiw3PHLBA==} + + tippy.js@6.3.7: + resolution: {integrity: sha512-E1d3oP2emgJ9dRQZdf3Kkn0qJgI6ZLpyS5z6ZkY1DF3kaQaBsGZsndEpHwx+eC+tYM41HaSNvNtLx8tU57FzTQ==} + to-regex-range@5.0.1: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} + topojson-client@3.1.0: + resolution: {integrity: sha512-605uxS6bcYxGXw9qi62XyrV6Q3xwbndjachmNxu8HWTtVPxZfEJN9fd/SZS1Q54Sn2y0TMyMxFj/cJINqGHrKw==} + hasBin: true + trim-lines@3.0.1: resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==} @@ -2797,6 +3437,9 @@ packages: peerDependencies: typescript: '>=4.8.4' + ts-pattern@5.9.0: + resolution: {integrity: sha512-6s5V71mX8qBUmlgbrfL33xDUwO0fq48rxAu2LBE11WBeGdpCPOsXksQbZJHvHwhrd3QjUusd3mAOM5Gg0mFBLg==} + tsconfig-paths@3.15.0: resolution: {integrity: sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==} @@ -2880,6 +3523,17 @@ packages: uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + url-join@5.0.0: + resolution: {integrity: sha512-n2huDr9h9yzd6exQVnH/jU5mr+Pfx08LRXXZhkLLetAMESRj+anQsTAh940iMrIetKAmry9coFuZQ2jY8/p3WA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + url-parse@1.5.10: + resolution: {integrity: sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==} + + url-slug@4.0.1: + resolution: {integrity: sha512-OkHgffjR6bce7jNTp5BUDBhg2IcnqSAi9DEhLH8Rhxrq84uPBMbHFzvOxniEIRpSSGBcG13LhrtNR5XzUdztfQ==} + engines: {node: '>=18.0.0'} + use-callback-ref@1.3.3: resolution: {integrity: sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==} engines: {node: '>=10'} @@ -2890,6 +3544,15 @@ packages: '@types/react': optional: true + use-isomorphic-layout-effect@1.2.1: + resolution: {integrity: sha512-tpZZ+EX0gaghDAiFR37hj5MgY6ZN55kLiPkJsKxBMZ6GZdOSPJXiOzPM984oPYZ5AnehYx5WQp1+ME8I/P/pRA==} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + use-sidecar@1.1.3: resolution: {integrity: sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==} engines: {node: '>=10'} @@ -2905,6 +3568,14 @@ packages: peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + uuidv7@1.2.1: + resolution: {integrity: sha512-4kPkK3/XTQW9Hbm4CaqfICn+kY9LJtDVEOfgsRRra/+n2Ofg4NqzRFceAkxvQ/Ud/6BpHOPzj8cirqM7TzTN5Q==} + hasBin: true + + versor@0.2.0: + resolution: {integrity: sha512-2UJ32VNh+lMUP9RlLoneEdrnupW3eIM9TGdTuaa+HSUqKACt9Vvw+xoGFGdd0YsUnGidTNyuxdhQRhJVpDAvqw==} + engines: {node: '>=12'} + vfile-message@4.0.3: resolution: {integrity: sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==} @@ -2942,6 +3613,10 @@ packages: yallist@3.1.1: resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + yaml@1.10.3: + resolution: {integrity: sha512-vIYeF1u3CjlhAFekPPAk2h/Kv4T3mAkMox5OymRiJQB0spDP10LHvt+K7G9Ny6NuuMAb25/6n1qyUjAcGNf/AA==} + engines: {node: '>= 6'} + yocto-queue@0.1.0: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} @@ -3106,6 +3781,84 @@ snapshots: optionalDependencies: '@types/react': 19.2.14 + '@buildcanada/charts@file:../bcds/packages/charts(@types/react@19.2.14)(d3-selection@3.0.0)(mobx-react@7.6.0(mobx@6.15.3)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(mobx@6.15.3)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3)': + dependencies: + '@buildcanada/colours': 0.3.3(typescript@5.9.3) + '@buildcanada/components': 0.3.5(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@fortawesome/fontawesome-svg-core': 6.7.2 + '@fortawesome/free-brands-svg-icons': 6.7.2 + '@fortawesome/free-solid-svg-icons': 6.7.2 + '@fortawesome/react-fontawesome': 0.2.6(@fortawesome/fontawesome-svg-core@6.7.2)(react@19.2.3) + '@tippyjs/react': 4.2.6(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@turf/boolean-intersects': 7.3.5 + '@turf/center': 7.3.5 + classnames: 2.5.1 + colorbrewer: 1.7.0 + d3: 7.9.0 + d3-transition: 3.0.1(d3-selection@3.0.0) + dayjs: 1.11.20 + fparser: 3.1.0 + fuzzysort: 3.1.0 + indefinite: 2.5.2 + js-cookie: 3.0.5 + lodash-es: 4.18.1 + mdast-util-find-and-replace: 3.0.2 + mdast-util-from-markdown: 2.0.3 + mobx: 6.15.3 + mobx-react: 7.6.0(mobx@6.15.3)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + mousetrap: 1.6.5 + papaparse: 5.5.3 + react: 19.2.3 + react-aria-components: 1.17.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + react-dom: 19.2.3(react@19.2.3) + react-flip-toolkit: 7.2.4(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + react-markdown: 10.1.0(@types/react@19.2.14)(react@19.2.3) + react-move: 6.5.0(react@19.2.3) + react-select: 5.10.2(@types/react@19.2.14)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + remeda: 2.34.0 + simple-statistics: 7.8.9 + string-pixel-width: 1.11.0 + striptags: 3.2.0 + topojson-client: 3.1.0 + ts-pattern: 5.9.0 + unist-util-visit: 5.1.0 + url-join: 5.0.0 + url-parse: 1.5.10 + url-slug: 4.0.1 + uuidv7: 1.2.1 + versor: 0.2.0 + transitivePeerDependencies: + - '@types/react' + - d3-selection + - supports-color + - typescript + + '@buildcanada/colours@0.3.3(typescript@5.9.3)': + dependencies: + typescript: 5.9.3 + + '@buildcanada/colours@file:../bcds/packages/colours(typescript@5.9.3)': + dependencies: + typescript: 5.9.3 + + '@buildcanada/components@0.3.5(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@fortawesome/fontawesome-svg-core': 6.7.2 + '@fortawesome/free-solid-svg-icons': 6.7.2 + '@fortawesome/react-fontawesome': 0.2.6(@fortawesome/fontawesome-svg-core@6.7.2)(react@19.2.3) + classnames: 2.5.1 + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + + '@buildcanada/components@file:../bcds/packages/components(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@fortawesome/fontawesome-svg-core': 6.7.2 + '@fortawesome/free-solid-svg-icons': 6.7.2 + '@fortawesome/react-fontawesome': 0.2.6(@fortawesome/fontawesome-svg-core@6.7.2)(react@19.2.3) + classnames: 2.5.1 + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + '@cloudflare/stream-react@1.9.3(react@19.2.3)': dependencies: react: 19.2.3 @@ -3126,6 +3879,70 @@ snapshots: tslib: 2.8.1 optional: true + '@emotion/babel-plugin@11.13.5': + dependencies: + '@babel/helper-module-imports': 7.28.6 + '@babel/runtime': 7.29.2 + '@emotion/hash': 0.9.2 + '@emotion/memoize': 0.9.0 + '@emotion/serialize': 1.3.3 + babel-plugin-macros: 3.1.0 + convert-source-map: 1.9.0 + escape-string-regexp: 4.0.0 + find-root: 1.1.0 + source-map: 0.5.7 + stylis: 4.2.0 + transitivePeerDependencies: + - supports-color + + '@emotion/cache@11.14.0': + dependencies: + '@emotion/memoize': 0.9.0 + '@emotion/sheet': 1.4.0 + '@emotion/utils': 1.4.2 + '@emotion/weak-memoize': 0.4.0 + stylis: 4.2.0 + + '@emotion/hash@0.9.2': {} + + '@emotion/memoize@0.9.0': {} + + '@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.3)': + dependencies: + '@babel/runtime': 7.29.2 + '@emotion/babel-plugin': 11.13.5 + '@emotion/cache': 11.14.0 + '@emotion/serialize': 1.3.3 + '@emotion/use-insertion-effect-with-fallbacks': 1.2.0(react@19.2.3) + '@emotion/utils': 1.4.2 + '@emotion/weak-memoize': 0.4.0 + hoist-non-react-statics: 3.3.2 + react: 19.2.3 + optionalDependencies: + '@types/react': 19.2.14 + transitivePeerDependencies: + - supports-color + + '@emotion/serialize@1.3.3': + dependencies: + '@emotion/hash': 0.9.2 + '@emotion/memoize': 0.9.0 + '@emotion/unitless': 0.10.0 + '@emotion/utils': 1.4.2 + csstype: 3.2.3 + + '@emotion/sheet@1.4.0': {} + + '@emotion/unitless@0.10.0': {} + + '@emotion/use-insertion-effect-with-fallbacks@1.2.0(react@19.2.3)': + dependencies: + react: 19.2.3 + + '@emotion/utils@1.4.2': {} + + '@emotion/weak-memoize@0.4.0': {} + '@esbuild/aix-ppc64@0.27.7': optional: true @@ -3267,6 +4084,26 @@ snapshots: '@floating-ui/utils@0.2.11': {} + '@fortawesome/fontawesome-common-types@6.7.2': {} + + '@fortawesome/fontawesome-svg-core@6.7.2': + dependencies: + '@fortawesome/fontawesome-common-types': 6.7.2 + + '@fortawesome/free-brands-svg-icons@6.7.2': + dependencies: + '@fortawesome/fontawesome-common-types': 6.7.2 + + '@fortawesome/free-solid-svg-icons@6.7.2': + dependencies: + '@fortawesome/fontawesome-common-types': 6.7.2 + + '@fortawesome/react-fontawesome@0.2.6(@fortawesome/fontawesome-svg-core@6.7.2)(react@19.2.3)': + dependencies: + '@fortawesome/fontawesome-svg-core': 6.7.2 + prop-types: 15.8.1 + react: 19.2.3 + '@humanfs/core@0.19.1': {} '@humanfs/node@0.16.7': @@ -3375,6 +4212,18 @@ snapshots: '@img/sharp-win32-x64@0.34.5': optional: true + '@internationalized/date@3.12.1': + dependencies: + '@swc/helpers': 0.5.15 + + '@internationalized/number@3.6.6': + dependencies: + '@swc/helpers': 0.5.15 + + '@internationalized/string@3.2.8': + dependencies: + '@swc/helpers': 0.5.15 + '@jridgewell/gen-mapping@0.3.13': dependencies: '@jridgewell/sourcemap-codec': 1.5.5 @@ -3523,6 +4372,69 @@ snapshots: '@opentelemetry/semantic-conventions@1.40.0': {} + '@parcel/watcher-android-arm64@2.5.6': + optional: true + + '@parcel/watcher-darwin-arm64@2.5.6': + optional: true + + '@parcel/watcher-darwin-x64@2.5.6': + optional: true + + '@parcel/watcher-freebsd-x64@2.5.6': + optional: true + + '@parcel/watcher-linux-arm-glibc@2.5.6': + optional: true + + '@parcel/watcher-linux-arm-musl@2.5.6': + optional: true + + '@parcel/watcher-linux-arm64-glibc@2.5.6': + optional: true + + '@parcel/watcher-linux-arm64-musl@2.5.6': + optional: true + + '@parcel/watcher-linux-x64-glibc@2.5.6': + optional: true + + '@parcel/watcher-linux-x64-musl@2.5.6': + optional: true + + '@parcel/watcher-win32-arm64@2.5.6': + optional: true + + '@parcel/watcher-win32-ia32@2.5.6': + optional: true + + '@parcel/watcher-win32-x64@2.5.6': + optional: true + + '@parcel/watcher@2.5.6': + dependencies: + detect-libc: 2.1.2 + is-glob: 4.0.3 + node-addon-api: 7.1.1 + picomatch: 4.0.4 + optionalDependencies: + '@parcel/watcher-android-arm64': 2.5.6 + '@parcel/watcher-darwin-arm64': 2.5.6 + '@parcel/watcher-darwin-x64': 2.5.6 + '@parcel/watcher-freebsd-x64': 2.5.6 + '@parcel/watcher-linux-arm-glibc': 2.5.6 + '@parcel/watcher-linux-arm-musl': 2.5.6 + '@parcel/watcher-linux-arm64-glibc': 2.5.6 + '@parcel/watcher-linux-arm64-musl': 2.5.6 + '@parcel/watcher-linux-x64-glibc': 2.5.6 + '@parcel/watcher-linux-x64-musl': 2.5.6 + '@parcel/watcher-win32-arm64': 2.5.6 + '@parcel/watcher-win32-ia32': 2.5.6 + '@parcel/watcher-win32-x64': 2.5.6 + optional: true + + '@popperjs/core@2.11.8': {} + '@posthog/core@1.25.2': {} '@posthog/types@1.368.0': {} @@ -3768,6 +4680,10 @@ snapshots: '@radix-ui/rect@1.1.1': {} + '@react-types/shared@3.34.0(react@19.2.3)': + dependencies: + react: 19.2.3 + '@rtsao/scc@1.1.0': {} '@swc/helpers@0.5.15': @@ -3843,6 +4759,83 @@ snapshots: postcss: 8.5.8 tailwindcss: 4.2.2 + '@tippyjs/react@4.2.6(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + tippy.js: 6.3.7 + + '@turf/bbox@7.3.5': + dependencies: + '@turf/helpers': 7.3.5 + '@turf/meta': 7.3.5 + '@types/geojson': 7946.0.16 + tslib: 2.8.1 + + '@turf/boolean-disjoint@7.3.5': + dependencies: + '@turf/boolean-point-in-polygon': 7.3.5 + '@turf/helpers': 7.3.5 + '@turf/line-intersect': 7.3.5 + '@turf/meta': 7.3.5 + '@turf/polygon-to-line': 7.3.5 + '@types/geojson': 7946.0.16 + tslib: 2.8.1 + + '@turf/boolean-intersects@7.3.5': + dependencies: + '@turf/boolean-disjoint': 7.3.5 + '@turf/helpers': 7.3.5 + '@turf/meta': 7.3.5 + '@types/geojson': 7946.0.16 + tslib: 2.8.1 + + '@turf/boolean-point-in-polygon@7.3.5': + dependencies: + '@turf/helpers': 7.3.5 + '@turf/invariant': 7.3.5 + '@types/geojson': 7946.0.16 + point-in-polygon-hao: 1.2.4 + tslib: 2.8.1 + + '@turf/center@7.3.5': + dependencies: + '@turf/bbox': 7.3.5 + '@turf/helpers': 7.3.5 + '@types/geojson': 7946.0.16 + tslib: 2.8.1 + + '@turf/helpers@7.3.5': + dependencies: + '@types/geojson': 7946.0.16 + tslib: 2.8.1 + + '@turf/invariant@7.3.5': + dependencies: + '@turf/helpers': 7.3.5 + '@types/geojson': 7946.0.16 + tslib: 2.8.1 + + '@turf/line-intersect@7.3.5': + dependencies: + '@turf/helpers': 7.3.5 + '@types/geojson': 7946.0.16 + sweepline-intersections: 1.5.0 + tslib: 2.8.1 + + '@turf/meta@7.3.5': + dependencies: + '@turf/helpers': 7.3.5 + '@types/geojson': 7946.0.16 + tslib: 2.8.1 + + '@turf/polygon-to-line@7.3.5': + dependencies: + '@turf/helpers': 7.3.5 + '@turf/invariant': 7.3.5 + '@types/geojson': 7946.0.16 + tslib: 2.8.1 + '@tybys/wasm-util@0.10.1': dependencies: tslib: 2.8.1 @@ -3858,6 +4851,8 @@ snapshots: '@types/estree@1.0.8': {} + '@types/geojson@7946.0.16': {} + '@types/hast@3.0.4': dependencies: '@types/unist': 3.0.3 @@ -3876,10 +4871,16 @@ snapshots: dependencies: undici-types: 6.21.0 + '@types/parse-json@4.0.2': {} + '@types/react-dom@19.2.3(@types/react@19.2.14)': dependencies: '@types/react': 19.2.14 + '@types/react-transition-group@4.4.12(@types/react@19.2.14)': + dependencies: + '@types/react': 19.2.14 + '@types/react@19.2.14': dependencies: csstype: 3.2.3 @@ -4147,6 +5148,12 @@ snapshots: axobject-query@4.1.0: {} + babel-plugin-macros@3.1.0: + dependencies: + '@babel/runtime': 7.29.2 + cosmiconfig: 7.1.0 + resolve: 1.22.11 + bail@2.0.2: {} balanced-match@1.0.2: {} @@ -4221,10 +5228,16 @@ snapshots: chart.js: 4.5.1 date-fns: 4.1.0 + chokidar@4.0.3: + dependencies: + readdirp: 4.1.2 + class-variance-authority@0.7.1: dependencies: clsx: 2.1.1 + classnames@2.5.1: {} + client-only@0.0.1: {} clsx@2.1.1: {} @@ -4235,14 +5248,30 @@ snapshots: color-name@1.1.4: {} + colorbrewer@1.7.0: {} + comma-separated-tokens@2.0.3: {} + commander@2.20.3: {} + + commander@7.2.0: {} + concat-map@0.0.1: {} + convert-source-map@1.9.0: {} + convert-source-map@2.0.0: {} core-js@3.49.0: {} + cosmiconfig@7.1.0: + dependencies: + '@types/parse-json': 4.0.2 + import-fresh: 3.3.1 + parse-json: 5.2.0 + path-type: 4.0.0 + yaml: 1.10.3 + cross-spawn@7.0.6: dependencies: path-key: 3.1.1 @@ -4251,6 +5280,160 @@ snapshots: csstype@3.2.3: {} + d3-array@3.2.4: + dependencies: + internmap: 2.0.3 + + d3-axis@3.0.0: {} + + d3-brush@3.0.0: + dependencies: + d3-dispatch: 3.0.1 + d3-drag: 3.0.0 + d3-interpolate: 3.0.1 + d3-selection: 3.0.0 + d3-transition: 3.0.1(d3-selection@3.0.0) + + d3-chord@3.0.1: + dependencies: + d3-path: 3.1.0 + + d3-color@3.1.0: {} + + d3-contour@4.0.2: + dependencies: + d3-array: 3.2.4 + + d3-delaunay@6.0.4: + dependencies: + delaunator: 5.1.0 + + d3-dispatch@3.0.1: {} + + d3-drag@3.0.0: + dependencies: + d3-dispatch: 3.0.1 + d3-selection: 3.0.0 + + d3-dsv@3.0.1: + dependencies: + commander: 7.2.0 + iconv-lite: 0.6.3 + rw: 1.3.3 + + d3-ease@3.0.1: {} + + d3-fetch@3.0.1: + dependencies: + d3-dsv: 3.0.1 + + d3-force@3.0.0: + dependencies: + d3-dispatch: 3.0.1 + d3-quadtree: 3.0.1 + d3-timer: 3.0.1 + + d3-format@3.1.2: {} + + d3-geo@3.1.1: + dependencies: + d3-array: 3.2.4 + + d3-hierarchy@3.1.2: {} + + d3-interpolate@3.0.1: + dependencies: + d3-color: 3.1.0 + + d3-path@3.1.0: {} + + d3-polygon@3.0.1: {} + + d3-quadtree@3.0.1: {} + + d3-random@3.0.1: {} + + d3-scale-chromatic@3.1.0: + dependencies: + d3-color: 3.1.0 + d3-interpolate: 3.0.1 + + d3-scale@4.0.2: + dependencies: + d3-array: 3.2.4 + d3-format: 3.1.2 + d3-interpolate: 3.0.1 + d3-time: 3.1.0 + d3-time-format: 4.1.0 + + d3-selection@3.0.0: {} + + d3-shape@3.2.0: + dependencies: + d3-path: 3.1.0 + + d3-time-format@4.1.0: + dependencies: + d3-time: 3.1.0 + + d3-time@3.1.0: + dependencies: + d3-array: 3.2.4 + + d3-timer@1.0.10: {} + + d3-timer@3.0.1: {} + + d3-transition@3.0.1(d3-selection@3.0.0): + dependencies: + d3-color: 3.1.0 + d3-dispatch: 3.0.1 + d3-ease: 3.0.1 + d3-interpolate: 3.0.1 + d3-selection: 3.0.0 + d3-timer: 3.0.1 + + d3-zoom@3.0.0: + dependencies: + d3-dispatch: 3.0.1 + d3-drag: 3.0.0 + d3-interpolate: 3.0.1 + d3-selection: 3.0.0 + d3-transition: 3.0.1(d3-selection@3.0.0) + + d3@7.9.0: + dependencies: + d3-array: 3.2.4 + d3-axis: 3.0.0 + d3-brush: 3.0.0 + d3-chord: 3.0.1 + d3-color: 3.1.0 + d3-contour: 4.0.2 + d3-delaunay: 6.0.4 + d3-dispatch: 3.0.1 + d3-drag: 3.0.0 + d3-dsv: 3.0.1 + d3-ease: 3.0.1 + d3-fetch: 3.0.1 + d3-force: 3.0.0 + d3-format: 3.1.2 + d3-geo: 3.1.1 + d3-hierarchy: 3.1.2 + d3-interpolate: 3.0.1 + d3-path: 3.1.0 + d3-polygon: 3.0.1 + d3-quadtree: 3.0.1 + d3-random: 3.0.1 + d3-scale: 4.0.2 + d3-scale-chromatic: 3.1.0 + d3-selection: 3.0.0 + d3-shape: 3.2.0 + d3-time: 3.1.0 + d3-time-format: 4.1.0 + d3-timer: 3.0.1 + d3-transition: 3.0.1(d3-selection@3.0.0) + d3-zoom: 3.0.0 + damerau-levenshtein@1.0.8: {} data-view-buffer@1.0.2: @@ -4273,6 +5456,8 @@ snapshots: date-fns@4.1.0: {} + dayjs@1.11.20: {} + debug@3.2.7: dependencies: ms: 2.1.3 @@ -4299,6 +5484,10 @@ snapshots: has-property-descriptors: 1.0.2 object-keys: 1.1.1 + delaunator@5.1.0: + dependencies: + robust-predicates: 3.0.3 + dequal@2.0.3: {} detect-libc@2.1.2: {} @@ -4313,6 +5502,11 @@ snapshots: dependencies: esutils: 2.0.3 + dom-helpers@5.2.1: + dependencies: + '@babel/runtime': 7.29.2 + csstype: 3.2.3 + dompurify@3.3.3: optionalDependencies: '@types/trusted-types': 2.0.7 @@ -4332,6 +5526,10 @@ snapshots: graceful-fs: 4.2.11 tapable: 2.3.2 + error-ex@1.3.4: + dependencies: + is-arrayish: 0.2.1 + es-abstract@1.24.1: dependencies: array-buffer-byte-length: 1.0.2 @@ -4467,6 +5665,8 @@ snapshots: escape-string-regexp@4.0.0: {} + escape-string-regexp@5.0.0: {} + eslint-config-next@16.1.6(@typescript-eslint/parser@8.58.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3): dependencies: '@next/eslint-plugin-next': 16.1.6 @@ -4708,6 +5908,8 @@ snapshots: dependencies: to-regex-range: 5.0.1 + find-root@1.1.0: {} + find-up@5.0.0: dependencies: locate-path: 6.0.0 @@ -4720,10 +5922,16 @@ snapshots: flatted@3.4.2: {} + flip-toolkit@7.2.4: + dependencies: + rematrix: 0.2.2 + for-each@0.3.5: dependencies: is-callable: 1.2.7 + fparser@3.1.0: {} + fsevents@2.3.3: optional: true @@ -4740,6 +5948,8 @@ snapshots: functions-have-names@1.2.3: {} + fuzzysort@3.1.0: {} + generator-function@2.0.1: {} gensync@1.0.0-beta.2: {} @@ -4847,12 +6057,22 @@ snapshots: dependencies: hermes-estree: 0.25.1 + hoist-non-react-statics@3.3.2: + dependencies: + react-is: 16.13.1 + html-url-attributes@3.0.1: {} + iconv-lite@0.6.3: + dependencies: + safer-buffer: 2.1.2 + ignore@5.3.2: {} ignore@7.0.5: {} + immutable@5.1.5: {} + import-fresh@3.3.1: dependencies: parent-module: 1.0.1 @@ -4860,6 +6080,8 @@ snapshots: imurmurhash@0.1.4: {} + indefinite@2.5.2: {} + inline-style-parser@0.2.7: {} internal-slot@1.1.0: @@ -4868,6 +6090,8 @@ snapshots: hasown: 2.0.2 side-channel: 1.1.0 + internmap@2.0.3: {} + is-alphabetical@2.0.1: {} is-alphanumerical@2.0.1: @@ -4881,6 +6105,8 @@ snapshots: call-bound: 1.0.4 get-intrinsic: 1.3.0 + is-arrayish@0.2.1: {} + is-async-function@2.1.1: dependencies: async-function: 1.0.0 @@ -5008,6 +6234,8 @@ snapshots: jiti@2.6.1: {} + js-cookie@3.0.5: {} + js-tokens@4.0.0: {} js-yaml@4.1.1: @@ -5018,6 +6246,8 @@ snapshots: json-buffer@3.0.1: {} + json-parse-even-better-errors@2.3.1: {} + json-schema-traverse@0.4.1: {} json-stable-stringify-without-jsonify@1.0.1: {} @@ -5035,6 +6265,10 @@ snapshots: object.assign: 4.1.7 object.values: 1.2.1 + kapellmeister@3.0.1: + dependencies: + d3-timer: 1.0.10 + keyv@4.5.4: dependencies: json-buffer: 3.0.1 @@ -5099,10 +6333,16 @@ snapshots: lightningcss-win32-arm64-msvc: 1.32.0 lightningcss-win32-x64-msvc: 1.32.0 + lines-and-columns@1.2.4: {} + locate-path@6.0.0: dependencies: p-locate: 5.0.0 + lodash-es@4.18.1: {} + + lodash.deburr@4.1.0: {} + lodash.merge@4.6.2: {} long@5.3.2: {} @@ -5127,6 +6367,13 @@ snapshots: math-intrinsics@1.1.0: {} + mdast-util-find-and-replace@3.0.2: + dependencies: + '@types/mdast': 4.0.4 + escape-string-regexp: 5.0.0 + unist-util-is: 6.0.1 + unist-util-visit-parents: 6.0.2 + mdast-util-from-markdown@2.0.3: dependencies: '@types/mdast': 4.0.4 @@ -5216,6 +6463,8 @@ snapshots: dependencies: '@types/mdast': 4.0.4 + memoize-one@6.0.0: {} + merge2@1.4.1: {} micromark-core-commonmark@2.0.3: @@ -5366,6 +6615,25 @@ snapshots: minimist@1.2.8: {} + mobx-react-lite@3.4.3(mobx@6.15.3)(react-dom@19.2.3(react@19.2.3))(react@19.2.3): + dependencies: + mobx: 6.15.3 + react: 19.2.3 + optionalDependencies: + react-dom: 19.2.3(react@19.2.3) + + mobx-react@7.6.0(mobx@6.15.3)(react-dom@19.2.3(react@19.2.3))(react@19.2.3): + dependencies: + mobx: 6.15.3 + mobx-react-lite: 3.4.3(mobx@6.15.3)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + react: 19.2.3 + optionalDependencies: + react-dom: 19.2.3(react@19.2.3) + + mobx@6.15.3: {} + + mousetrap@1.6.5: {} + ms@2.1.3: {} nanoid@3.3.11: {} @@ -5374,7 +6642,7 @@ snapshots: natural-compare@1.4.0: {} - next@16.1.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3): + next@16.1.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(sass@1.99.0): dependencies: '@next/env': 16.1.6 '@swc/helpers': 0.5.15 @@ -5394,11 +6662,15 @@ snapshots: '@next/swc-win32-arm64-msvc': 16.1.6 '@next/swc-win32-x64-msvc': 16.1.6 '@opentelemetry/api': 1.9.1 + sass: 1.99.0 sharp: 0.34.5 transitivePeerDependencies: - '@babel/core' - babel-plugin-macros + node-addon-api@7.1.1: + optional: true + node-exports-info@1.6.0: dependencies: array.prototype.flatmap: 1.3.3 @@ -5473,6 +6745,8 @@ snapshots: dependencies: p-limit: 3.1.0 + papaparse@5.5.3: {} + parent-module@1.0.1: dependencies: callsites: 3.1.0 @@ -5487,18 +6761,31 @@ snapshots: is-decimal: 2.0.1 is-hexadecimal: 2.0.1 + parse-json@5.2.0: + dependencies: + '@babel/code-frame': 7.29.0 + error-ex: 1.3.4 + json-parse-even-better-errors: 2.3.1 + lines-and-columns: 1.2.4 + path-exists@4.0.0: {} path-key@3.1.1: {} path-parse@1.0.7: {} + path-type@4.0.0: {} + picocolors@1.1.1: {} picomatch@2.3.2: {} picomatch@4.0.4: {} + point-in-polygon-hao@1.2.4: + dependencies: + robust-predicates: 3.0.3 + possible-typed-array-names@1.1.0: {} postcss@8.4.31: @@ -5560,8 +6847,35 @@ snapshots: query-selector-shadow-dom@1.0.1: {} + querystringify@2.2.0: {} + queue-microtask@1.2.3: {} + react-aria-components@1.17.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3): + dependencies: + '@internationalized/date': 3.12.1 + '@react-types/shared': 3.34.0(react@19.2.3) + '@swc/helpers': 0.5.15 + client-only: 0.0.1 + react: 19.2.3 + react-aria: 3.48.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + react-dom: 19.2.3(react@19.2.3) + react-stately: 3.46.0(react@19.2.3) + + react-aria@3.48.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3): + dependencies: + '@internationalized/date': 3.12.1 + '@internationalized/number': 3.6.6 + '@internationalized/string': 3.2.8 + '@react-types/shared': 3.34.0(react@19.2.3) + '@swc/helpers': 0.5.15 + aria-hidden: 1.2.6 + clsx: 2.1.1 + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + react-stately: 3.46.0(react@19.2.3) + use-sync-external-store: 1.6.0(react@19.2.3) + react-chartjs-2@5.3.1(chart.js@4.5.1)(react@19.2.3): dependencies: chart.js: 4.5.1 @@ -5572,6 +6886,13 @@ snapshots: react: 19.2.3 scheduler: 0.27.0 + react-flip-toolkit@7.2.4(react-dom@19.2.3(react@19.2.3))(react@19.2.3): + dependencies: + flip-toolkit: 7.2.4 + prop-types: 15.8.1 + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + react-is@16.13.1: {} react-markdown@10.1.0(@types/react@19.2.14)(react@19.2.3): @@ -5592,6 +6913,13 @@ snapshots: transitivePeerDependencies: - supports-color + react-move@6.5.0(react@19.2.3): + dependencies: + '@babel/runtime': 7.29.2 + kapellmeister: 3.0.1 + prop-types: 15.8.1 + react: 19.2.3 + react-remove-scroll-bar@2.3.8(@types/react@19.2.14)(react@19.2.3): dependencies: react: 19.2.3 @@ -5611,6 +6939,33 @@ snapshots: optionalDependencies: '@types/react': 19.2.14 + react-select@5.10.2(@types/react@19.2.14)(react-dom@19.2.3(react@19.2.3))(react@19.2.3): + dependencies: + '@babel/runtime': 7.29.2 + '@emotion/cache': 11.14.0 + '@emotion/react': 11.14.0(@types/react@19.2.14)(react@19.2.3) + '@floating-ui/dom': 1.7.6 + '@types/react-transition-group': 4.4.12(@types/react@19.2.14) + memoize-one: 6.0.0 + prop-types: 15.8.1 + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + react-transition-group: 4.4.5(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + use-isomorphic-layout-effect: 1.2.1(@types/react@19.2.14)(react@19.2.3) + transitivePeerDependencies: + - '@types/react' + - supports-color + + react-stately@3.46.0(react@19.2.3): + dependencies: + '@internationalized/date': 3.12.1 + '@internationalized/number': 3.6.6 + '@internationalized/string': 3.2.8 + '@react-types/shared': 3.34.0(react@19.2.3) + '@swc/helpers': 0.5.15 + react: 19.2.3 + use-sync-external-store: 1.6.0(react@19.2.3) + react-style-singleton@2.2.3(@types/react@19.2.14)(react@19.2.3): dependencies: get-nonce: 1.0.1 @@ -5619,8 +6974,19 @@ snapshots: optionalDependencies: '@types/react': 19.2.14 + react-transition-group@4.4.5(react-dom@19.2.3(react@19.2.3))(react@19.2.3): + dependencies: + '@babel/runtime': 7.29.2 + dom-helpers: 5.2.1 + loose-envify: 1.4.0 + prop-types: 15.8.1 + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + react@19.2.3: {} + readdirp@4.1.2: {} + reflect.getprototypeof@1.0.10: dependencies: call-bind: 1.0.8 @@ -5658,6 +7024,12 @@ snapshots: unified: 11.0.5 vfile: 6.0.3 + rematrix@0.2.2: {} + + remeda@2.34.0: {} + + requires-port@1.0.0: {} + reselect@5.1.1: {} resolve-from@4.0.0: {} @@ -5681,10 +7053,14 @@ snapshots: reusify@1.1.0: {} + robust-predicates@3.0.3: {} + run-parallel@1.2.0: dependencies: queue-microtask: 1.2.3 + rw@1.3.3: {} + safe-array-concat@1.1.3: dependencies: call-bind: 1.0.8 @@ -5704,6 +7080,16 @@ snapshots: es-errors: 1.3.0 is-regex: 1.2.1 + safer-buffer@2.1.2: {} + + sass@1.99.0: + dependencies: + chokidar: 4.0.3 + immutable: 5.1.5 + source-map-js: 1.2.1 + optionalDependencies: + '@parcel/watcher': 2.5.6 + scheduler@0.27.0: {} schema-dts-lib@1.0.0(typescript@5.9.3): @@ -5808,6 +7194,8 @@ snapshots: side-channel-map: 1.0.1 side-channel-weakmap: 1.0.2 + simple-statistics@7.8.9: {} + sonner@2.0.7(react-dom@19.2.3(react@19.2.3))(react@19.2.3): dependencies: react: 19.2.3 @@ -5815,6 +7203,8 @@ snapshots: source-map-js@1.2.1: {} + source-map@0.5.7: {} + space-separated-tokens@2.0.2: {} stable-hash@0.0.5: {} @@ -5824,6 +7214,10 @@ snapshots: es-errors: 1.3.0 internal-slot: 1.1.0 + string-pixel-width@1.11.0: + dependencies: + lodash.deburr: 4.1.0 + string.prototype.includes@2.0.1: dependencies: call-bind: 1.0.8 @@ -5883,6 +7277,8 @@ snapshots: strip-json-comments@3.1.1: {} + striptags@3.2.0: {} + style-to-js@1.1.21: dependencies: style-to-object: 1.0.14 @@ -5898,12 +7294,18 @@ snapshots: optionalDependencies: '@babel/core': 7.29.0 + stylis@4.2.0: {} + supports-color@7.2.0: dependencies: has-flag: 4.0.0 supports-preserve-symlinks-flag@1.0.0: {} + sweepline-intersections@1.5.0: + dependencies: + tinyqueue: 2.0.3 + swr@2.4.1(react@19.2.3): dependencies: dequal: 2.0.3 @@ -5923,10 +7325,20 @@ snapshots: fdir: 6.5.0(picomatch@4.0.4) picomatch: 4.0.4 + tinyqueue@2.0.3: {} + + tippy.js@6.3.7: + dependencies: + '@popperjs/core': 2.11.8 + to-regex-range@5.0.1: dependencies: is-number: 7.0.0 + topojson-client@3.1.0: + dependencies: + commander: 2.20.3 + trim-lines@3.0.1: {} trough@2.2.0: {} @@ -5935,6 +7347,8 @@ snapshots: dependencies: typescript: 5.9.3 + ts-pattern@5.9.0: {} + tsconfig-paths@3.15.0: dependencies: '@types/json5': 0.0.29 @@ -6079,6 +7493,15 @@ snapshots: dependencies: punycode: 2.3.1 + url-join@5.0.0: {} + + url-parse@1.5.10: + dependencies: + querystringify: 2.2.0 + requires-port: 1.0.0 + + url-slug@4.0.1: {} + use-callback-ref@1.3.3(@types/react@19.2.14)(react@19.2.3): dependencies: react: 19.2.3 @@ -6086,6 +7509,12 @@ snapshots: optionalDependencies: '@types/react': 19.2.14 + use-isomorphic-layout-effect@1.2.1(@types/react@19.2.14)(react@19.2.3): + dependencies: + react: 19.2.3 + optionalDependencies: + '@types/react': 19.2.14 + use-sidecar@1.1.3(@types/react@19.2.14)(react@19.2.3): dependencies: detect-node-es: 1.1.0 @@ -6098,6 +7527,10 @@ snapshots: dependencies: react: 19.2.3 + uuidv7@1.2.1: {} + + versor@0.2.0: {} + vfile-message@4.0.3: dependencies: '@types/unist': 3.0.3 @@ -6159,6 +7592,8 @@ snapshots: yallist@3.1.1: {} + yaml@1.10.3: {} + yocto-queue@0.1.0: {} zod-validation-error@4.0.2(zod@4.3.6): diff --git a/src/app/dashboard/Dashboard.tsx b/src/app/dashboard/Dashboard.tsx new file mode 100644 index 0000000..7d3f13a --- /dev/null +++ b/src/app/dashboard/Dashboard.tsx @@ -0,0 +1,123 @@ +"use client"; + +import { useEffect, useMemo, useState } from "react"; +import { + Grapher, + GrapherState, + Bounds, + GRAPHER_CHART_TYPES, + LifeExpectancyGrapher, +} from "@buildcanada/charts"; + +type ChartTypeKey = "LineChart" | "DiscreteBar" | "StackedArea" | "SlopeChart"; + +const CHART_TYPES: { key: ChartTypeKey; label: string }[] = [ + { key: "LineChart", label: "Line" }, + { key: "DiscreteBar", label: "Discrete Bar" }, + { key: "StackedArea", label: "Stacked Area" }, + { key: "SlopeChart", label: "Slope" }, +]; + +const ENTITY_PRESETS: { label: string; entities: string[] }[] = [ + { + label: "G7 (sample)", + entities: ["Canada", "United States", "Germany", "Japan"], + }, + { + label: "Americas", + entities: ["Canada", "United States", "Brazil", "Mexico"], + }, + { + label: "Asia", + entities: ["Japan", "China", "India", "Korea"], + }, +]; + +export default function Dashboard() { + const [chartType, setChartType] = useState("LineChart"); + const [presetIndex, setPresetIndex] = useState(0); + const [size, setSize] = useState({ width: 1100, height: 660 }); + + const grapherState = useMemo(() => { + const state: GrapherState = LifeExpectancyGrapher({ + bounds: new Bounds(0, 0, size.width, size.height), + chartTypes: [GRAPHER_CHART_TYPES[chartType]], + selectedEntityNames: ENTITY_PRESETS[presetIndex].entities, + }); + return state; + }, [chartType, presetIndex, size.width, size.height]); + + useEffect(() => { + const onResize = () => { + const w = Math.min(1100, window.innerWidth - 64); + const h = Math.max(420, Math.min(660, Math.round(w * 0.6))); + setSize({ width: w, height: h }); + }; + onResize(); + window.addEventListener("resize", onResize); + return () => window.removeEventListener("resize", onResize); + }, []); + + return ( +
+
+

Dashboard

+

+ Sample data rendered with the @buildcanada/charts Grapher. Switch + chart type or country group to verify interactivity. +

+
+ +
+
+ + Chart type: + +
+ {CHART_TYPES.map((t) => ( + + ))} +
+
+
+ Entities: + +
+
+ +
+ +
+ +

+ Try hovering, clicking the legend, or switching tabs in the chart + toolbar to confirm interactivity. +

+
+ ); +} diff --git a/src/app/dashboard/DashboardClient.tsx b/src/app/dashboard/DashboardClient.tsx new file mode 100644 index 0000000..3357d71 --- /dev/null +++ b/src/app/dashboard/DashboardClient.tsx @@ -0,0 +1,16 @@ +"use client"; + +import dynamic from "next/dynamic"; + +const Dashboard = dynamic(() => import("./Dashboard"), { + ssr: false, + loading: () => ( +
+
+
+ ), +}); + +export default function DashboardClient() { + return ; +} diff --git a/src/app/dashboard/page.tsx b/src/app/dashboard/page.tsx new file mode 100644 index 0000000..d7c988e --- /dev/null +++ b/src/app/dashboard/page.tsx @@ -0,0 +1,14 @@ +import type { Metadata } from "next"; + +import "@buildcanada/charts/styles.css"; +import DashboardClient from "./DashboardClient"; + +export const metadata: Metadata = { + title: "Dashboard", + description: + "Interactive data dashboard rendered with the Build Canada charts library.", +}; + +export default function DashboardPage() { + return ; +} From 07bf2885ad4d885e3cc59d46670130d32bfe87c7 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 8 May 2026 22:11:34 +0000 Subject: [PATCH 02/11] Switch @buildcanada/* deps from file: link to npm versions Now that bcds#7 is merged and @buildcanada/charts@0.3.9 is published (with the TC-39 stage 3 decorator emit + automatic JSX runtime fix), TradingPost no longer needs the local file:../bcds/packages/* links. Switching to ^0.3.9 / ^0.3.5 / ^0.3.3 so the integration works on a fresh checkout (CI, other developers) without a sibling bcds tree. Verified end-to-end: pnpm install resolves the registry packages, `next dev` boots, /dashboard mounts the Grapher (12 SVGs, 17 interactive buttons), chart-type and entity-preset switching both work, no pageerrors. --- package.json | 6 +++--- pnpm-lock.yaml | 42 +++++++++--------------------------------- 2 files changed, 12 insertions(+), 36 deletions(-) diff --git a/package.json b/package.json index fcd8913..559b40d 100644 --- a/package.json +++ b/package.json @@ -13,9 +13,9 @@ }, "dependencies": { "@base-ui/react": "^1.3.0", - "@buildcanada/charts": "file:../bcds/packages/charts", - "@buildcanada/colours": "file:../bcds/packages/colours", - "@buildcanada/components": "file:../bcds/packages/components", + "@buildcanada/charts": "^0.3.9", + "@buildcanada/colours": "^0.3.3", + "@buildcanada/components": "^0.3.5", "@cloudflare/stream-react": "^1.9.3", "@radix-ui/react-select": "^2.2.5", "chart.js": "^4.4.7", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f6d975e..a2b9fbf 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -12,14 +12,14 @@ importers: specifier: ^1.3.0 version: 1.3.0(@types/react@19.2.14)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@buildcanada/charts': - specifier: file:../bcds/packages/charts - version: file:../bcds/packages/charts(@types/react@19.2.14)(d3-selection@3.0.0)(mobx-react@7.6.0(mobx@6.15.3)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(mobx@6.15.3)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3) + specifier: ^0.3.9 + version: 0.3.9(@types/react@19.2.14)(d3-selection@3.0.0)(mobx-react@7.6.0(mobx@6.15.3)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(mobx@6.15.3)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3) '@buildcanada/colours': - specifier: file:../bcds/packages/colours - version: file:../bcds/packages/colours(typescript@5.9.3) + specifier: ^0.3.3 + version: 0.3.3(typescript@5.9.3) '@buildcanada/components': - specifier: file:../bcds/packages/components - version: file:../bcds/packages/components(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + specifier: ^0.3.5 + version: 0.3.5(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@cloudflare/stream-react': specifier: ^1.9.3 version: 1.9.3(react@19.2.3) @@ -216,8 +216,8 @@ packages: '@types/react': optional: true - '@buildcanada/charts@file:../bcds/packages/charts': - resolution: {directory: ../bcds/packages/charts, type: directory} + '@buildcanada/charts@0.3.9': + resolution: {integrity: sha512-VyUMy+y7qlb+P0TJ+h2nJzwc4SdOsuXRjqBTcUpy8WweU68MsZEJKUAujZsXYK9hjMkq6Mzt5hB/POCfryqB1g==} peerDependencies: mobx: ^6.13.0 mobx-react: ^7.6.0 @@ -229,23 +229,12 @@ packages: peerDependencies: typescript: ^5 - '@buildcanada/colours@file:../bcds/packages/colours': - resolution: {directory: ../bcds/packages/colours, type: directory} - peerDependencies: - typescript: ^5 - '@buildcanada/components@0.3.5': resolution: {integrity: sha512-PffM8aJ6zge/lDjVXtg4GZY5uYIc5tbwOgnzMBTqA+RQKUdT9Bry9ykluuqNRJhOT5I2f8M+v+ZekF/OiDty0w==} peerDependencies: react: ^19.0.0 react-dom: ^19.0.0 - '@buildcanada/components@file:../bcds/packages/components': - resolution: {directory: ../bcds/packages/components, type: directory} - peerDependencies: - react: ^19.0.0 - react-dom: ^19.0.0 - '@cloudflare/stream-react@1.9.3': resolution: {integrity: sha512-ocr7B+zHk/jq0r/wgtFeyoz7Hr+v3Qn0Ho6yUBkWT07ahr/0GU1dhliYJESwNXMuMYCU1sZ02sFH9pW5r1/BIA==} engines: {node: '>=10'} @@ -3781,7 +3770,7 @@ snapshots: optionalDependencies: '@types/react': 19.2.14 - '@buildcanada/charts@file:../bcds/packages/charts(@types/react@19.2.14)(d3-selection@3.0.0)(mobx-react@7.6.0(mobx@6.15.3)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(mobx@6.15.3)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3)': + '@buildcanada/charts@0.3.9(@types/react@19.2.14)(d3-selection@3.0.0)(mobx-react@7.6.0(mobx@6.15.3)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(mobx@6.15.3)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3)': dependencies: '@buildcanada/colours': 0.3.3(typescript@5.9.3) '@buildcanada/components': 0.3.5(react-dom@19.2.3(react@19.2.3))(react@19.2.3) @@ -3837,10 +3826,6 @@ snapshots: dependencies: typescript: 5.9.3 - '@buildcanada/colours@file:../bcds/packages/colours(typescript@5.9.3)': - dependencies: - typescript: 5.9.3 - '@buildcanada/components@0.3.5(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: '@fortawesome/fontawesome-svg-core': 6.7.2 @@ -3850,15 +3835,6 @@ snapshots: react: 19.2.3 react-dom: 19.2.3(react@19.2.3) - '@buildcanada/components@file:../bcds/packages/components(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': - dependencies: - '@fortawesome/fontawesome-svg-core': 6.7.2 - '@fortawesome/free-solid-svg-icons': 6.7.2 - '@fortawesome/react-fontawesome': 0.2.6(@fortawesome/fontawesome-svg-core@6.7.2)(react@19.2.3) - classnames: 2.5.1 - react: 19.2.3 - react-dom: 19.2.3(react@19.2.3) - '@cloudflare/stream-react@1.9.3(react@19.2.3)': dependencies: react: 19.2.3 From 8a6924da6e4a2336404d45345e31f66d02fb6d67 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 8 May 2026 22:17:29 +0000 Subject: [PATCH 03/11] Drop mobx, mobx-react, sass from explicit deps MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit These are peer dependencies that pnpm auto-installs on our behalf: - sass is a peer of next 16 (Next ships built-in Sass support) - mobx and mobx-react are peers of @buildcanada/charts Since this repo is pnpm-only (packageManager: pnpm@9.5.0) and pnpm 8+ defaults auto-install-peers to true, declaring them ourselves is redundant — pnpm pulls them in either way. Removing keeps package.json focused on direct dependencies and avoids version-pin drift between us and the charts peer-dep declaration. Verified: pnpm install resolves mobx@6.15.3 and mobx-react@7.6.0 via peer auto-install; /dashboard mounts the Grapher cleanly (12 SVGs, 17 interactive buttons, no pageerrors). --- package.json | 3 --- pnpm-lock.yaml | 17 ++++++----------- 2 files changed, 6 insertions(+), 14 deletions(-) diff --git a/package.json b/package.json index 559b40d..dda55a4 100644 --- a/package.json +++ b/package.json @@ -24,15 +24,12 @@ "clsx": "^2.1.1", "date-fns": "^4.1.0", "lucide-react": "^1.7.0", - "mobx": "^6.15.3", - "mobx-react": "^7.6.0", "next": "16.1.6", "posthog-js": "^1.368.0", "react": "19.2.3", "react-chartjs-2": "^5.3.0", "react-dom": "19.2.3", "react-markdown": "^10.1.0", - "sass": "^1.99.0", "schema-dts": "^2.0.0", "sonner": "^2.0.7", "swr": "^2.3.8", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a2b9fbf..7f024ba 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -44,12 +44,6 @@ importers: lucide-react: specifier: ^1.7.0 version: 1.7.0(react@19.2.3) - mobx: - specifier: ^6.15.3 - version: 6.15.3 - mobx-react: - specifier: ^7.6.0 - version: 7.6.0(mobx@6.15.3)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) next: specifier: 16.1.6 version: 16.1.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.1)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(sass@1.99.0) @@ -68,9 +62,6 @@ importers: react-markdown: specifier: ^10.1.0 version: 10.1.0(@types/react@19.2.14)(react@19.2.3) - sass: - specifier: ^1.99.0 - version: 1.99.0 schema-dts: specifier: ^2.0.0 version: 2.0.0(typescript@5.9.3) @@ -5207,6 +5198,7 @@ snapshots: chokidar@4.0.3: dependencies: readdirp: 4.1.2 + optional: true class-variance-authority@0.7.1: dependencies: @@ -6047,7 +6039,8 @@ snapshots: ignore@7.0.5: {} - immutable@5.1.5: {} + immutable@5.1.5: + optional: true import-fresh@3.3.1: dependencies: @@ -6961,7 +6954,8 @@ snapshots: react@19.2.3: {} - readdirp@4.1.2: {} + readdirp@4.1.2: + optional: true reflect.getprototypeof@1.0.10: dependencies: @@ -7065,6 +7059,7 @@ snapshots: source-map-js: 1.2.1 optionalDependencies: '@parcel/watcher': 2.5.6 + optional: true scheduler@0.27.0: {} From b9237b347c669248de0031e56e21132f67bf2806 Mon Sep 17 00:00:00 2001 From: xrendan Date: Mon, 25 May 2026 18:36:23 -0600 Subject: [PATCH 04/11] dashboard: per-org KPI dashboards backed by york_factory KPI API MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaces the life-expectancy demo at /dashboard with a real KPI browser: - /dashboard — index of jurisdictions and their organizations - /dashboard/[jurisdiction]/[org] — per-org dashboard showing every numeric measure with a Grapher chart (actual + target series across the available years), grouped by service category in a sidebar Adds src/lib/api/kpis.ts with typed fetchers for jurisdictions, organizations, measures, and facts (auto-paginating). The KPI endpoints live under /api/v1/kpis on the york_factory API (see ../york_factory/docs/api/kpis). Reads are unauthenticated and revalidated server-side every 10 minutes. --- src/app/dashboard/Dashboard.tsx | 123 --------- src/app/dashboard/DashboardClient.tsx | 16 -- .../[jurisdiction]/[org]/OrgDashboard.tsx | 245 ++++++++++++++++++ .../[org]/OrgDashboardClient.tsx | 19 ++ .../dashboard/[jurisdiction]/[org]/page.tsx | 110 ++++++++ .../dashboard/[jurisdiction]/[org]/types.ts | 6 + src/app/dashboard/page.tsx | 120 ++++++++- src/lib/api/kpis.ts | 166 ++++++++++++ 8 files changed, 659 insertions(+), 146 deletions(-) delete mode 100644 src/app/dashboard/Dashboard.tsx delete mode 100644 src/app/dashboard/DashboardClient.tsx create mode 100644 src/app/dashboard/[jurisdiction]/[org]/OrgDashboard.tsx create mode 100644 src/app/dashboard/[jurisdiction]/[org]/OrgDashboardClient.tsx create mode 100644 src/app/dashboard/[jurisdiction]/[org]/page.tsx create mode 100644 src/app/dashboard/[jurisdiction]/[org]/types.ts create mode 100644 src/lib/api/kpis.ts diff --git a/src/app/dashboard/Dashboard.tsx b/src/app/dashboard/Dashboard.tsx deleted file mode 100644 index 7d3f13a..0000000 --- a/src/app/dashboard/Dashboard.tsx +++ /dev/null @@ -1,123 +0,0 @@ -"use client"; - -import { useEffect, useMemo, useState } from "react"; -import { - Grapher, - GrapherState, - Bounds, - GRAPHER_CHART_TYPES, - LifeExpectancyGrapher, -} from "@buildcanada/charts"; - -type ChartTypeKey = "LineChart" | "DiscreteBar" | "StackedArea" | "SlopeChart"; - -const CHART_TYPES: { key: ChartTypeKey; label: string }[] = [ - { key: "LineChart", label: "Line" }, - { key: "DiscreteBar", label: "Discrete Bar" }, - { key: "StackedArea", label: "Stacked Area" }, - { key: "SlopeChart", label: "Slope" }, -]; - -const ENTITY_PRESETS: { label: string; entities: string[] }[] = [ - { - label: "G7 (sample)", - entities: ["Canada", "United States", "Germany", "Japan"], - }, - { - label: "Americas", - entities: ["Canada", "United States", "Brazil", "Mexico"], - }, - { - label: "Asia", - entities: ["Japan", "China", "India", "Korea"], - }, -]; - -export default function Dashboard() { - const [chartType, setChartType] = useState("LineChart"); - const [presetIndex, setPresetIndex] = useState(0); - const [size, setSize] = useState({ width: 1100, height: 660 }); - - const grapherState = useMemo(() => { - const state: GrapherState = LifeExpectancyGrapher({ - bounds: new Bounds(0, 0, size.width, size.height), - chartTypes: [GRAPHER_CHART_TYPES[chartType]], - selectedEntityNames: ENTITY_PRESETS[presetIndex].entities, - }); - return state; - }, [chartType, presetIndex, size.width, size.height]); - - useEffect(() => { - const onResize = () => { - const w = Math.min(1100, window.innerWidth - 64); - const h = Math.max(420, Math.min(660, Math.round(w * 0.6))); - setSize({ width: w, height: h }); - }; - onResize(); - window.addEventListener("resize", onResize); - return () => window.removeEventListener("resize", onResize); - }, []); - - return ( -
-
-

Dashboard

-

- Sample data rendered with the @buildcanada/charts Grapher. Switch - chart type or country group to verify interactivity. -

-
- -
-
- - Chart type: - -
- {CHART_TYPES.map((t) => ( - - ))} -
-
-
- Entities: - -
-
- -
- -
- -

- Try hovering, clicking the legend, or switching tabs in the chart - toolbar to confirm interactivity. -

-
- ); -} diff --git a/src/app/dashboard/DashboardClient.tsx b/src/app/dashboard/DashboardClient.tsx deleted file mode 100644 index 3357d71..0000000 --- a/src/app/dashboard/DashboardClient.tsx +++ /dev/null @@ -1,16 +0,0 @@ -"use client"; - -import dynamic from "next/dynamic"; - -const Dashboard = dynamic(() => import("./Dashboard"), { - ssr: false, - loading: () => ( -
-
-
- ), -}); - -export default function DashboardClient() { - return ; -} diff --git a/src/app/dashboard/[jurisdiction]/[org]/OrgDashboard.tsx b/src/app/dashboard/[jurisdiction]/[org]/OrgDashboard.tsx new file mode 100644 index 0000000..9d206a6 --- /dev/null +++ b/src/app/dashboard/[jurisdiction]/[org]/OrgDashboard.tsx @@ -0,0 +1,245 @@ +"use client"; + +import { useEffect, useMemo, useState } from "react"; +import { + Bounds, + createTestDataset, + DimensionProperty, + GRAPHER_CHART_TYPES, + Grapher, + GrapherState, + legacyToChartsTableAndDimensionsWithMandatorySlug, +} from "@buildcanada/charts"; +import type { MeasureWithFacts } from "./types"; +import type { KPIFact, KPIValueType } from "@/lib/api/kpis"; + +type ChartTypeKey = "LineChart" | "DiscreteBar"; + +const CHART_TYPES: { key: ChartTypeKey; label: string }[] = [ + { key: "LineChart", label: "Line" }, + { key: "DiscreteBar", label: "Bar (latest year)" }, +]; + +const VALUE_TYPE_ORDER: KPIValueType[] = [ + "actual", + "target", + "projected", + "plan", + "budget", +]; + +const VALUE_TYPE_ENTITY_IDS: Record = { + actual: 900001, + target: 900002, + projected: 900003, + plan: 900004, + budget: 900005, +}; + +const VALUE_TYPE_LABELS: Record = { + actual: "Actual", + target: "Target", + projected: "Projected", + plan: "Plan", + budget: "Budget", +}; + +function buildGrapherState( + item: MeasureWithFacts, + bounds: Bounds, + chartType: ChartTypeKey, +): GrapherState | null { + const numericFacts = item.facts.filter( + (f) => f.value_numeric !== null && f.period_basis === "full_year", + ); + if (numericFacts.length === 0) return null; + + const presentTypes = Array.from( + new Set(numericFacts.map((f) => f.value_type)), + ); + const orderedTypes = VALUE_TYPE_ORDER.filter((t) => presentTypes.includes(t)); + + // Synthetic single indicator id — Grapher requires a numeric variableId. + const variableId = item.measure.id || 1; + + const data = numericFacts.map((f) => ({ + year: f.measurement_year, + entity: { + id: VALUE_TYPE_ENTITY_IDS[f.value_type] ?? 900099, + code: f.value_type, + name: VALUE_TYPE_LABELS[f.value_type] ?? f.value_type, + }, + value: f.value_numeric as number, + })); + + const metadata = { + id: variableId, + display: { + name: item.measure.canonical_name, + unit: item.measure.unit.base_unit, + shortUnit: item.measure.unit.symbol, + numDecimalPlaces: pickDecimals(numericFacts), + }, + }; + + const dimensions = [{ variableId, property: DimensionProperty.y }]; + + const grapherState = new GrapherState({ + bounds, + chartTypes: [GRAPHER_CHART_TYPES[chartType]], + selectedEntityNames: orderedTypes.map((t) => VALUE_TYPE_LABELS[t]), + dimensions, + }); + + grapherState.inputTable = legacyToChartsTableAndDimensionsWithMandatorySlug( + createTestDataset([{ data, metadata }]), + dimensions, + {}, + ); + + return grapherState; +} + +function pickDecimals(facts: KPIFact[]): number { + const sample = facts.find((f) => f.value_numeric !== null)?.value_numeric; + if (sample === undefined || sample === null) return 2; + const abs = Math.abs(sample); + if (abs >= 1000) return 0; + if (abs >= 10) return 1; + return 2; +} + +function rangeLabel(item: MeasureWithFacts): string { + const years = item.facts + .map((f) => f.measurement_year) + .filter((y, i, arr) => arr.indexOf(y) === i) + .sort((a, b) => a - b); + if (years.length === 0) return "—"; + if (years.length === 1) return String(years[0]); + return `${years[0]}–${years[years.length - 1]}`; +} + +export default function OrgDashboard({ items }: { items: MeasureWithFacts[] }) { + const [selectedId, setSelectedId] = useState(items[0]?.measure.id); + const [chartType, setChartType] = useState("LineChart"); + const [size, setSize] = useState({ width: 800, height: 520 }); + + useEffect(() => { + const onResize = () => { + const w = Math.min(900, Math.max(360, window.innerWidth - 64)); + const h = Math.max(380, Math.min(560, Math.round(w * 0.62))); + setSize({ width: w, height: h }); + }; + onResize(); + window.addEventListener("resize", onResize); + return () => window.removeEventListener("resize", onResize); + }, []); + + const selected = useMemo( + () => items.find((i) => i.measure.id === selectedId) ?? items[0], + [items, selectedId], + ); + + const grapherState = useMemo(() => { + if (!selected) return null; + return buildGrapherState( + selected, + new Bounds(0, 0, size.width, size.height), + chartType, + ); + }, [selected, size.width, size.height, chartType]); + + if (!selected) return null; + + const categories = Array.from( + new Set(items.map((i) => i.measure.service_category ?? "Other")), + ).sort(); + + return ( +
+ + +
+
+
+

+ {selected.measure.canonical_name} +

+

+ {selected.measure.unit.symbol} · {rangeLabel(selected)} + {selected.measure.service_category + ? ` · ${selected.measure.service_category}` + : ""} +

+
+
+ {CHART_TYPES.map((t) => ( + + ))} +
+
+ +
+ {grapherState ? ( + + ) : ( +
+ No numeric data for this measure. +
+ )} +
+
+
+ ); +} diff --git a/src/app/dashboard/[jurisdiction]/[org]/OrgDashboardClient.tsx b/src/app/dashboard/[jurisdiction]/[org]/OrgDashboardClient.tsx new file mode 100644 index 0000000..2ecd1ec --- /dev/null +++ b/src/app/dashboard/[jurisdiction]/[org]/OrgDashboardClient.tsx @@ -0,0 +1,19 @@ +"use client"; + +import dynamic from "next/dynamic"; +import type { MeasureWithFacts } from "./types"; + +const OrgDashboard = dynamic(() => import("./OrgDashboard"), { + ssr: false, + loading: () => ( +
+ ), +}); + +export default function OrgDashboardClient({ + items, +}: { + items: MeasureWithFacts[]; +}) { + return ; +} diff --git a/src/app/dashboard/[jurisdiction]/[org]/page.tsx b/src/app/dashboard/[jurisdiction]/[org]/page.tsx new file mode 100644 index 0000000..bb11482 --- /dev/null +++ b/src/app/dashboard/[jurisdiction]/[org]/page.tsx @@ -0,0 +1,110 @@ +import "@buildcanada/charts/styles.css"; +import type { Metadata } from "next"; +import Link from "next/link"; +import { notFound } from "next/navigation"; +import { + getOrganization, + listFactsForOrg, + listMeasuresForOrg, + listJurisdictions, +} from "@/lib/api/kpis"; +import type { KPIFact, KPIMeasure } from "@/lib/api/kpis"; +import OrgDashboardClient from "./OrgDashboardClient"; +import type { MeasureWithFacts } from "./types"; + +interface PageParams { + jurisdiction: string; + org: string; +} + +export async function generateMetadata({ + params, +}: { + params: Promise; +}): Promise { + const { jurisdiction, org } = await params; + try { + const o = await getOrganization(jurisdiction, org); + return { + title: `${o.canonical_name} — KPI Dashboard`, + description: + o.description ?? + `Tracked performance measures for ${o.canonical_name}.`, + alternates: { canonical: `/dashboard/${jurisdiction}/${org}` }, + }; + } catch { + return { title: "KPI Dashboard" }; + } +} + +export default async function OrgDashboardPage({ + params, +}: { + params: Promise; +}) { + const { jurisdiction, org } = await params; + + let orgData; + try { + orgData = await getOrganization(jurisdiction, org); + } catch { + notFound(); + } + + const [allJurisdictions, measures, facts] = await Promise.all([ + listJurisdictions().catch(() => []), + listMeasuresForOrg(org).catch(() => [] as KPIMeasure[]), + listFactsForOrg(org).catch(() => [] as KPIFact[]), + ]); + + const jurisdictionName = + allJurisdictions.find((j) => j.slug === jurisdiction)?.name ?? jurisdiction; + + const factsByMeasure = new Map(); + for (const f of facts) { + if (!factsByMeasure.has(f.measure_id)) factsByMeasure.set(f.measure_id, []); + factsByMeasure.get(f.measure_id)!.push(f); + } + + const items: MeasureWithFacts[] = measures + .map((m) => ({ measure: m, facts: factsByMeasure.get(m.id) ?? [] })) + .filter((item) => + item.facts.some((f) => f.value_numeric !== null), + ) + .sort((a, b) => a.measure.canonical_name.localeCompare(b.measure.canonical_name)); + + return ( +
+
+ + ← All dashboards + +

+ {orgData.canonical_name} +

+

+ {jurisdictionName} + {orgData.kind ? ` · ${orgData.kind}` : ""} + {" · "} + {items.length} measure{items.length === 1 ? "" : "s"} with data +

+ {orgData.description && ( +

+ {orgData.description} +

+ )} +
+ + {items.length === 0 ? ( +
+ No numeric measures are currently tracked for this organization. +
+ ) : ( + + )} +
+ ); +} diff --git a/src/app/dashboard/[jurisdiction]/[org]/types.ts b/src/app/dashboard/[jurisdiction]/[org]/types.ts new file mode 100644 index 0000000..c0eccce --- /dev/null +++ b/src/app/dashboard/[jurisdiction]/[org]/types.ts @@ -0,0 +1,6 @@ +import type { KPIFact, KPIMeasure } from "@/lib/api/kpis"; + +export interface MeasureWithFacts { + measure: KPIMeasure; + facts: KPIFact[]; +} diff --git a/src/app/dashboard/page.tsx b/src/app/dashboard/page.tsx index d7c988e..746aecf 100644 --- a/src/app/dashboard/page.tsx +++ b/src/app/dashboard/page.tsx @@ -1,14 +1,120 @@ import type { Metadata } from "next"; - -import "@buildcanada/charts/styles.css"; -import DashboardClient from "./DashboardClient"; +import Link from "next/link"; +import { listJurisdictions, listOrganizations } from "@/lib/api/kpis"; +import type { KPIJurisdiction, KPIOrganization } from "@/lib/api/kpis"; export const metadata: Metadata = { - title: "Dashboard", + title: "KPI Dashboards", description: - "Interactive data dashboard rendered with the Build Canada charts library.", + "Government performance indicators sourced from federal, provincial, and municipal departmental plans and results reports.", +}; + +const LEVEL_ORDER: Record = { + federal: 0, + provincial: 1, + territorial: 2, + municipal: 3, + regional: 4, + crown_corp: 5, + authority: 6, +}; + +const LEVEL_LABELS: Record = { + federal: "Federal", + provincial: "Provincial", + territorial: "Territorial", + municipal: "Municipal", + regional: "Regional", + crown_corp: "Crown Corporation", + authority: "Authority", }; -export default function DashboardPage() { - return ; +export default async function DashboardIndexPage() { + let jurisdictions: KPIJurisdiction[] = []; + try { + jurisdictions = await listJurisdictions(); + } catch { + jurisdictions = []; + } + + jurisdictions = [...jurisdictions].sort((a, b) => { + const la = LEVEL_ORDER[a.level] ?? 99; + const lb = LEVEL_ORDER[b.level] ?? 99; + if (la !== lb) return la - lb; + return a.name.localeCompare(b.name); + }); + + const orgsByJurisdiction = await Promise.all( + jurisdictions.map(async (j) => { + try { + const orgs = await listOrganizations(j.slug); + return { jurisdiction: j, orgs }; + } catch { + return { jurisdiction: j, orgs: [] as KPIOrganization[] }; + } + }), + ); + + const populated = orgsByJurisdiction.filter((g) => g.orgs.length > 0); + + return ( +
+
+

+ KPI Dashboards +

+

+ Government performance indicators sourced from federal Departmental + Plans / Results Reports, provincial Annual Business Plans, and + municipal budget notes. Pick an organization to see its tracked + measures over time. +

+
+ + {populated.length === 0 ? ( +
+ KPI data is not currently available. Once the York Factory KPI API is + deployed at {`/api/v1/kpis`}, dashboards will appear + here. +
+ ) : ( +
+ {populated.map(({ jurisdiction, orgs }) => ( +
+
+

+ {jurisdiction.name} +

+ + {LEVEL_LABELS[jurisdiction.level] ?? jurisdiction.level} •{" "} + {orgs.length} org{orgs.length === 1 ? "" : "s"} + +
+
    + {[...orgs] + .sort((a, b) => + a.canonical_name.localeCompare(b.canonical_name), + ) + .map((org) => ( +
  • + + {org.canonical_name} + {org.kind && ( + + {org.kind} + + )} + +
  • + ))} +
+
+ ))} +
+ )} +
+ ); } diff --git a/src/lib/api/kpis.ts b/src/lib/api/kpis.ts new file mode 100644 index 0000000..d065005 --- /dev/null +++ b/src/lib/api/kpis.ts @@ -0,0 +1,166 @@ +import { apiFetch } from "./client"; + +export interface KPIJurisdiction { + id: number; + slug: string; + name: string; + code: string; + level: + | "federal" + | "provincial" + | "territorial" + | "municipal" + | "regional" + | "crown_corp" + | "authority"; + region_code: string | null; + fiscal_year_start_month: number; + default_currency: string; +} + +export interface KPIOrganization { + id: number; + slug: string; + canonical_name: string; + kind: string | null; + active_from_year: number | null; + active_to_year: number | null; + description: string | null; + jurisdiction_id: number; +} + +export interface KPIUnit { + id: number; + symbol: string; + kind: "absolute" | "rate" | "ratio" | string; + base_unit: string; + scale: number; + currency_code: string | null; + denominator_unit: string | null; + denominator_scale: number | null; +} + +export interface KPIMeasure { + id: number; + slug: string; + canonical_name: string; + organization: { + id: number; + slug: string; + canonical_name: string; + active_from_year: number | null; + active_to_year: number | null; + } | null; + unit: KPIUnit; + service_category: string | null; + first_seen_year: number | null; + last_seen_year: number | null; + description?: string | null; +} + +export type KPIValueType = + | "actual" + | "target" + | "projected" + | "plan" + | "budget"; + +export type KPIPeriodBasis = + | "full_year" + | "ytd_q1" + | "ytd_q2" + | "ytd_q3" + | "as_of_date"; + +export interface KPIFact { + measure_id: number; + measurement_year: number; + value_type: KPIValueType; + period_basis: KPIPeriodBasis; + value_numeric: number | null; + value_text: string | null; + citation_id: number; + document_id: number; +} + +interface KPIListResponse { + data: T[]; +} + +interface KPIPaginatedResponse { + data: T[]; + meta: { page: number; pages: number; count: number; per_page: number }; +} + +const REVALIDATE = 600; + +export async function listJurisdictions(): Promise { + const res = await apiFetch>( + "/kpis/jurisdictions", + { revalidate: REVALIDATE }, + ); + return res.data; +} + +export async function listOrganizations( + jurisdictionSlug: string, +): Promise { + const res = await apiFetch>( + `/kpis/jurisdictions/${jurisdictionSlug}/organizations`, + { revalidate: REVALIDATE }, + ); + return res.data; +} + +export async function getOrganization( + jurisdictionSlug: string, + orgSlug: string, +): Promise { + return apiFetch( + `/kpis/jurisdictions/${jurisdictionSlug}/organizations/${orgSlug}`, + { revalidate: REVALIDATE }, + ); +} + +export async function listMeasuresForOrg( + orgSlug: string, +): Promise { + const all: KPIMeasure[] = []; + let page = 1; + while (true) { + const res = await apiFetch>( + "/kpis/measures", + { + params: { + organization_slug: orgSlug, + per_page: "100", + page: String(page), + }, + revalidate: REVALIDATE, + }, + ); + all.push(...res.data); + if (page >= res.meta.pages) break; + page++; + } + return all; +} + +export async function listFactsForOrg(orgSlug: string): Promise { + const all: KPIFact[] = []; + let page = 1; + while (true) { + const res = await apiFetch>("/kpis/facts", { + params: { + organization_slug: orgSlug, + per_page: "100", + page: String(page), + }, + revalidate: REVALIDATE, + }); + all.push(...res.data); + if (page >= res.meta.pages) break; + page++; + } + return all; +} From 6dba7bfda81dd6a4529c31f53dd98cf27d0f9f6f Mon Sep 17 00:00:00 2001 From: xrendan Date: Mon, 25 May 2026 18:47:57 -0600 Subject: [PATCH 05/11] dashboard: give each measure its own URL Restructures the per-org dashboard so each KPI measure is a separately shareable, SEO-friendly route: /dashboard/[jurisdiction]/[org]/[measure] - New [org]/layout.tsx owns the org header and the sidebar of measures (Link-based, active state via usePathname). - [org]/page.tsx becomes an overview: cards for each measure showing the latest actual value with a link to the measure's chart page. - New [org]/[measure]/page.tsx fetches the measure (via getMeasure for description + lineages) and its facts, then renders a single Grapher chart with line/bar toggle. Adds listFactsForMeasure and getMeasure fetchers to src/lib/api/kpis.ts. --- .../[jurisdiction]/[org]/MeasureSidebar.tsx | 71 +++++ .../[jurisdiction]/[org]/OrgDashboard.tsx | 245 ------------------ .../[org]/OrgDashboardClient.tsx | 19 -- .../[org]/[measure]/MeasureChart.tsx | 178 +++++++++++++ .../[org]/[measure]/MeasureChartClient.tsx | 21 ++ .../[jurisdiction]/[org]/[measure]/page.tsx | 93 +++++++ .../dashboard/[jurisdiction]/[org]/layout.tsx | 86 ++++++ .../dashboard/[jurisdiction]/[org]/page.tsx | 126 +++++---- .../dashboard/[jurisdiction]/[org]/types.ts | 6 - src/lib/api/kpis.ts | 26 ++ 10 files changed, 555 insertions(+), 316 deletions(-) create mode 100644 src/app/dashboard/[jurisdiction]/[org]/MeasureSidebar.tsx delete mode 100644 src/app/dashboard/[jurisdiction]/[org]/OrgDashboard.tsx delete mode 100644 src/app/dashboard/[jurisdiction]/[org]/OrgDashboardClient.tsx create mode 100644 src/app/dashboard/[jurisdiction]/[org]/[measure]/MeasureChart.tsx create mode 100644 src/app/dashboard/[jurisdiction]/[org]/[measure]/MeasureChartClient.tsx create mode 100644 src/app/dashboard/[jurisdiction]/[org]/[measure]/page.tsx create mode 100644 src/app/dashboard/[jurisdiction]/[org]/layout.tsx delete mode 100644 src/app/dashboard/[jurisdiction]/[org]/types.ts diff --git a/src/app/dashboard/[jurisdiction]/[org]/MeasureSidebar.tsx b/src/app/dashboard/[jurisdiction]/[org]/MeasureSidebar.tsx new file mode 100644 index 0000000..20619c5 --- /dev/null +++ b/src/app/dashboard/[jurisdiction]/[org]/MeasureSidebar.tsx @@ -0,0 +1,71 @@ +"use client"; + +import Link from "next/link"; +import { usePathname } from "next/navigation"; + +export interface SidebarMeasure { + id: number; + slug: string; + canonical_name: string; + service_category: string | null; +} + +export default function MeasureSidebar({ + jurisdiction, + org, + measures, +}: { + jurisdiction: string; + org: string; + measures: SidebarMeasure[]; +}) { + const pathname = usePathname(); + const orgRoot = `/dashboard/${jurisdiction}/${org}`; + + const categories = Array.from( + new Set(measures.map((m) => m.service_category ?? "Other")), + ).sort(); + + return ( + + ); +} diff --git a/src/app/dashboard/[jurisdiction]/[org]/OrgDashboard.tsx b/src/app/dashboard/[jurisdiction]/[org]/OrgDashboard.tsx deleted file mode 100644 index 9d206a6..0000000 --- a/src/app/dashboard/[jurisdiction]/[org]/OrgDashboard.tsx +++ /dev/null @@ -1,245 +0,0 @@ -"use client"; - -import { useEffect, useMemo, useState } from "react"; -import { - Bounds, - createTestDataset, - DimensionProperty, - GRAPHER_CHART_TYPES, - Grapher, - GrapherState, - legacyToChartsTableAndDimensionsWithMandatorySlug, -} from "@buildcanada/charts"; -import type { MeasureWithFacts } from "./types"; -import type { KPIFact, KPIValueType } from "@/lib/api/kpis"; - -type ChartTypeKey = "LineChart" | "DiscreteBar"; - -const CHART_TYPES: { key: ChartTypeKey; label: string }[] = [ - { key: "LineChart", label: "Line" }, - { key: "DiscreteBar", label: "Bar (latest year)" }, -]; - -const VALUE_TYPE_ORDER: KPIValueType[] = [ - "actual", - "target", - "projected", - "plan", - "budget", -]; - -const VALUE_TYPE_ENTITY_IDS: Record = { - actual: 900001, - target: 900002, - projected: 900003, - plan: 900004, - budget: 900005, -}; - -const VALUE_TYPE_LABELS: Record = { - actual: "Actual", - target: "Target", - projected: "Projected", - plan: "Plan", - budget: "Budget", -}; - -function buildGrapherState( - item: MeasureWithFacts, - bounds: Bounds, - chartType: ChartTypeKey, -): GrapherState | null { - const numericFacts = item.facts.filter( - (f) => f.value_numeric !== null && f.period_basis === "full_year", - ); - if (numericFacts.length === 0) return null; - - const presentTypes = Array.from( - new Set(numericFacts.map((f) => f.value_type)), - ); - const orderedTypes = VALUE_TYPE_ORDER.filter((t) => presentTypes.includes(t)); - - // Synthetic single indicator id — Grapher requires a numeric variableId. - const variableId = item.measure.id || 1; - - const data = numericFacts.map((f) => ({ - year: f.measurement_year, - entity: { - id: VALUE_TYPE_ENTITY_IDS[f.value_type] ?? 900099, - code: f.value_type, - name: VALUE_TYPE_LABELS[f.value_type] ?? f.value_type, - }, - value: f.value_numeric as number, - })); - - const metadata = { - id: variableId, - display: { - name: item.measure.canonical_name, - unit: item.measure.unit.base_unit, - shortUnit: item.measure.unit.symbol, - numDecimalPlaces: pickDecimals(numericFacts), - }, - }; - - const dimensions = [{ variableId, property: DimensionProperty.y }]; - - const grapherState = new GrapherState({ - bounds, - chartTypes: [GRAPHER_CHART_TYPES[chartType]], - selectedEntityNames: orderedTypes.map((t) => VALUE_TYPE_LABELS[t]), - dimensions, - }); - - grapherState.inputTable = legacyToChartsTableAndDimensionsWithMandatorySlug( - createTestDataset([{ data, metadata }]), - dimensions, - {}, - ); - - return grapherState; -} - -function pickDecimals(facts: KPIFact[]): number { - const sample = facts.find((f) => f.value_numeric !== null)?.value_numeric; - if (sample === undefined || sample === null) return 2; - const abs = Math.abs(sample); - if (abs >= 1000) return 0; - if (abs >= 10) return 1; - return 2; -} - -function rangeLabel(item: MeasureWithFacts): string { - const years = item.facts - .map((f) => f.measurement_year) - .filter((y, i, arr) => arr.indexOf(y) === i) - .sort((a, b) => a - b); - if (years.length === 0) return "—"; - if (years.length === 1) return String(years[0]); - return `${years[0]}–${years[years.length - 1]}`; -} - -export default function OrgDashboard({ items }: { items: MeasureWithFacts[] }) { - const [selectedId, setSelectedId] = useState(items[0]?.measure.id); - const [chartType, setChartType] = useState("LineChart"); - const [size, setSize] = useState({ width: 800, height: 520 }); - - useEffect(() => { - const onResize = () => { - const w = Math.min(900, Math.max(360, window.innerWidth - 64)); - const h = Math.max(380, Math.min(560, Math.round(w * 0.62))); - setSize({ width: w, height: h }); - }; - onResize(); - window.addEventListener("resize", onResize); - return () => window.removeEventListener("resize", onResize); - }, []); - - const selected = useMemo( - () => items.find((i) => i.measure.id === selectedId) ?? items[0], - [items, selectedId], - ); - - const grapherState = useMemo(() => { - if (!selected) return null; - return buildGrapherState( - selected, - new Bounds(0, 0, size.width, size.height), - chartType, - ); - }, [selected, size.width, size.height, chartType]); - - if (!selected) return null; - - const categories = Array.from( - new Set(items.map((i) => i.measure.service_category ?? "Other")), - ).sort(); - - return ( -
- - -
-
-
-

- {selected.measure.canonical_name} -

-

- {selected.measure.unit.symbol} · {rangeLabel(selected)} - {selected.measure.service_category - ? ` · ${selected.measure.service_category}` - : ""} -

-
-
- {CHART_TYPES.map((t) => ( - - ))} -
-
- -
- {grapherState ? ( - - ) : ( -
- No numeric data for this measure. -
- )} -
-
-
- ); -} diff --git a/src/app/dashboard/[jurisdiction]/[org]/OrgDashboardClient.tsx b/src/app/dashboard/[jurisdiction]/[org]/OrgDashboardClient.tsx deleted file mode 100644 index 2ecd1ec..0000000 --- a/src/app/dashboard/[jurisdiction]/[org]/OrgDashboardClient.tsx +++ /dev/null @@ -1,19 +0,0 @@ -"use client"; - -import dynamic from "next/dynamic"; -import type { MeasureWithFacts } from "./types"; - -const OrgDashboard = dynamic(() => import("./OrgDashboard"), { - ssr: false, - loading: () => ( -
- ), -}); - -export default function OrgDashboardClient({ - items, -}: { - items: MeasureWithFacts[]; -}) { - return ; -} diff --git a/src/app/dashboard/[jurisdiction]/[org]/[measure]/MeasureChart.tsx b/src/app/dashboard/[jurisdiction]/[org]/[measure]/MeasureChart.tsx new file mode 100644 index 0000000..28f4acd --- /dev/null +++ b/src/app/dashboard/[jurisdiction]/[org]/[measure]/MeasureChart.tsx @@ -0,0 +1,178 @@ +"use client"; + +import { useEffect, useMemo, useState } from "react"; +import { + Bounds, + createTestDataset, + DimensionProperty, + GRAPHER_CHART_TYPES, + Grapher, + GrapherState, + legacyToChartsTableAndDimensionsWithMandatorySlug, +} from "@buildcanada/charts"; +import type { KPIFact, KPIMeasure, KPIValueType } from "@/lib/api/kpis"; + +type ChartTypeKey = "LineChart" | "DiscreteBar"; + +const CHART_TYPES: { key: ChartTypeKey; label: string }[] = [ + { key: "LineChart", label: "Line" }, + { key: "DiscreteBar", label: "Bar (latest year)" }, +]; + +const VALUE_TYPE_ORDER: KPIValueType[] = [ + "actual", + "target", + "projected", + "plan", + "budget", +]; + +const VALUE_TYPE_ENTITY_IDS: Record = { + actual: 900001, + target: 900002, + projected: 900003, + plan: 900004, + budget: 900005, +}; + +const VALUE_TYPE_LABELS: Record = { + actual: "Actual", + target: "Target", + projected: "Projected", + plan: "Plan", + budget: "Budget", +}; + +function pickDecimals(facts: KPIFact[]): number { + const sample = facts.find((f) => f.value_numeric !== null)?.value_numeric; + if (sample === undefined || sample === null) return 2; + const abs = Math.abs(sample); + if (abs >= 1000) return 0; + if (abs >= 10) return 1; + return 2; +} + +function buildGrapherState( + measure: KPIMeasure, + facts: KPIFact[], + bounds: Bounds, + chartType: ChartTypeKey, +): GrapherState | null { + const numericFacts = facts.filter( + (f) => f.value_numeric !== null && f.period_basis === "full_year", + ); + if (numericFacts.length === 0) return null; + + const presentTypes = Array.from( + new Set(numericFacts.map((f) => f.value_type)), + ); + const orderedTypes = VALUE_TYPE_ORDER.filter((t) => presentTypes.includes(t)); + const variableId = measure.id || 1; + + const data = numericFacts.map((f) => ({ + year: f.measurement_year, + entity: { + id: VALUE_TYPE_ENTITY_IDS[f.value_type] ?? 900099, + code: f.value_type, + name: VALUE_TYPE_LABELS[f.value_type] ?? f.value_type, + }, + value: f.value_numeric as number, + })); + + const metadata = { + id: variableId, + display: { + name: measure.canonical_name, + unit: measure.unit.base_unit, + shortUnit: measure.unit.symbol, + numDecimalPlaces: pickDecimals(numericFacts), + }, + }; + + const dimensions = [{ variableId, property: DimensionProperty.y }]; + + const grapherState = new GrapherState({ + bounds, + chartTypes: [GRAPHER_CHART_TYPES[chartType]], + selectedEntityNames: orderedTypes.map((t) => VALUE_TYPE_LABELS[t]), + dimensions, + }); + + grapherState.inputTable = legacyToChartsTableAndDimensionsWithMandatorySlug( + createTestDataset([{ data, metadata }]), + dimensions, + {}, + ); + + return grapherState; +} + +export default function MeasureChart({ + measure, + facts, +}: { + measure: KPIMeasure; + facts: KPIFact[]; +}) { + const [chartType, setChartType] = useState("LineChart"); + const [size, setSize] = useState({ width: 800, height: 520 }); + + useEffect(() => { + const onResize = () => { + const w = Math.min(900, Math.max(360, window.innerWidth - 360)); + const h = Math.max(380, Math.min(560, Math.round(w * 0.62))); + setSize({ width: w, height: h }); + }; + onResize(); + window.addEventListener("resize", onResize); + return () => window.removeEventListener("resize", onResize); + }, []); + + const grapherState = useMemo( + () => + buildGrapherState( + measure, + facts, + new Bounds(0, 0, size.width, size.height), + chartType, + ), + [measure, facts, size.width, size.height, chartType], + ); + + return ( +
+
+
+ {CHART_TYPES.map((t) => ( + + ))} +
+
+ +
+ {grapherState ? ( + + ) : ( +
+ No numeric data for this measure. +
+ )} +
+
+ ); +} diff --git a/src/app/dashboard/[jurisdiction]/[org]/[measure]/MeasureChartClient.tsx b/src/app/dashboard/[jurisdiction]/[org]/[measure]/MeasureChartClient.tsx new file mode 100644 index 0000000..7e14783 --- /dev/null +++ b/src/app/dashboard/[jurisdiction]/[org]/[measure]/MeasureChartClient.tsx @@ -0,0 +1,21 @@ +"use client"; + +import dynamic from "next/dynamic"; +import type { KPIFact, KPIMeasure } from "@/lib/api/kpis"; + +const MeasureChart = dynamic(() => import("./MeasureChart"), { + ssr: false, + loading: () => ( +
+ ), +}); + +export default function MeasureChartClient({ + measure, + facts, +}: { + measure: KPIMeasure; + facts: KPIFact[]; +}) { + return ; +} diff --git a/src/app/dashboard/[jurisdiction]/[org]/[measure]/page.tsx b/src/app/dashboard/[jurisdiction]/[org]/[measure]/page.tsx new file mode 100644 index 0000000..6dd0ea9 --- /dev/null +++ b/src/app/dashboard/[jurisdiction]/[org]/[measure]/page.tsx @@ -0,0 +1,93 @@ +import type { Metadata } from "next"; +import { notFound } from "next/navigation"; +import { + getMeasure, + listFactsForMeasure, + listMeasuresForOrg, +} from "@/lib/api/kpis"; +import MeasureChartClient from "./MeasureChartClient"; + +interface PageParams { + jurisdiction: string; + org: string; + measure: string; +} + +async function resolveMeasure(orgSlug: string, measureSlug: string) { + const measures = await listMeasuresForOrg(orgSlug); + const found = measures.find((m) => m.slug === measureSlug); + if (!found) return null; + // Index lookup returns most of the measure, but the show endpoint includes + // description and lineages. Fetch the full record for the detail page. + try { + return await getMeasure(found.id); + } catch { + return found; + } +} + +export async function generateMetadata({ + params, +}: { + params: Promise; +}): Promise { + const { jurisdiction, org, measure } = await params; + const m = await resolveMeasure(org, measure).catch(() => null); + if (!m) return { title: "Measure" }; + return { + title: `${m.canonical_name} — ${m.organization?.canonical_name ?? "KPI"}`, + description: + m.description ?? + `Performance measure tracked for ${m.organization?.canonical_name ?? "this organization"}.`, + alternates: { + canonical: `/dashboard/${jurisdiction}/${org}/${measure}`, + }, + }; +} + +export default async function MeasurePage({ + params, +}: { + params: Promise; +}) { + const { org, measure } = await params; + + const measureData = await resolveMeasure(org, measure).catch(() => null); + if (!measureData) notFound(); + + const facts = await listFactsForMeasure(measureData.id).catch(() => []); + + const numericYears = facts + .filter((f) => f.value_numeric !== null) + .map((f) => f.measurement_year); + const range = + numericYears.length === 0 + ? null + : numericYears.length === 1 + ? `${numericYears[0]}` + : `${Math.min(...numericYears)}–${Math.max(...numericYears)}`; + + return ( +
+
+

+ {measureData.canonical_name} +

+

+ {measureData.unit.symbol} + {range ? ` · ${range}` : ""} + {measureData.service_category + ? ` · ${measureData.service_category}` + : ""} +

+ {measureData.description && ( +

+ {measureData.description} +

+ )} +
+ + +
+ ); +} diff --git a/src/app/dashboard/[jurisdiction]/[org]/layout.tsx b/src/app/dashboard/[jurisdiction]/[org]/layout.tsx new file mode 100644 index 0000000..62c94cd --- /dev/null +++ b/src/app/dashboard/[jurisdiction]/[org]/layout.tsx @@ -0,0 +1,86 @@ +import "@buildcanada/charts/styles.css"; +import Link from "next/link"; +import { notFound } from "next/navigation"; +import { + getOrganization, + listJurisdictions, + listMeasuresForOrg, +} from "@/lib/api/kpis"; +import type { KPIMeasure } from "@/lib/api/kpis"; +import MeasureSidebar from "./MeasureSidebar"; + +interface LayoutParams { + jurisdiction: string; + org: string; +} + +export default async function OrgLayout({ + children, + params, +}: { + children: React.ReactNode; + params: Promise; +}) { + const { jurisdiction, org } = await params; + + let orgData; + try { + orgData = await getOrganization(jurisdiction, org); + } catch { + notFound(); + } + + const [allJurisdictions, measures] = await Promise.all([ + listJurisdictions().catch(() => []), + listMeasuresForOrg(org).catch(() => [] as KPIMeasure[]), + ]); + + const jurisdictionName = + allJurisdictions.find((j) => j.slug === jurisdiction)?.name ?? jurisdiction; + + const sortedMeasures = [...measures].sort((a, b) => + a.canonical_name.localeCompare(b.canonical_name), + ); + + return ( +
+
+ + ← All dashboards + +

+ + {orgData.canonical_name} + +

+

+ {jurisdictionName} + {orgData.kind ? ` · ${orgData.kind}` : ""} + {" · "} + {sortedMeasures.length} measure + {sortedMeasures.length === 1 ? "" : "s"} +

+
+ +
+ ({ + id: m.id, + slug: m.slug, + canonical_name: m.canonical_name, + service_category: m.service_category, + }))} + /> +
{children}
+
+
+ ); +} diff --git a/src/app/dashboard/[jurisdiction]/[org]/page.tsx b/src/app/dashboard/[jurisdiction]/[org]/page.tsx index bb11482..64af449 100644 --- a/src/app/dashboard/[jurisdiction]/[org]/page.tsx +++ b/src/app/dashboard/[jurisdiction]/[org]/page.tsx @@ -1,16 +1,11 @@ -import "@buildcanada/charts/styles.css"; import type { Metadata } from "next"; import Link from "next/link"; -import { notFound } from "next/navigation"; import { getOrganization, listFactsForOrg, listMeasuresForOrg, - listJurisdictions, } from "@/lib/api/kpis"; import type { KPIFact, KPIMeasure } from "@/lib/api/kpis"; -import OrgDashboardClient from "./OrgDashboardClient"; -import type { MeasureWithFacts } from "./types"; interface PageParams { jurisdiction: string; @@ -37,73 +32,112 @@ export async function generateMetadata({ } } -export default async function OrgDashboardPage({ +function latestActual(facts: KPIFact[]): KPIFact | null { + const actuals = facts.filter( + (f) => + f.value_type === "actual" && + f.period_basis === "full_year" && + f.value_numeric !== null, + ); + if (actuals.length === 0) return null; + return actuals.reduce((acc, f) => + f.measurement_year > acc.measurement_year ? f : acc, + ); +} + +function formatValue(value: number, unit: KPIMeasure["unit"]): string { + const abs = Math.abs(value); + const decimals = abs >= 1000 ? 0 : abs >= 10 ? 1 : 2; + const formatted = value.toLocaleString("en-CA", { + minimumFractionDigits: 0, + maximumFractionDigits: decimals, + }); + return `${formatted} ${unit.symbol}`; +} + +export default async function OrgOverviewPage({ params, }: { params: Promise; }) { const { jurisdiction, org } = await params; - let orgData; - try { - orgData = await getOrganization(jurisdiction, org); - } catch { - notFound(); - } - - const [allJurisdictions, measures, facts] = await Promise.all([ - listJurisdictions().catch(() => []), + const [orgData, measures, facts] = await Promise.all([ + getOrganization(jurisdiction, org).catch(() => null), listMeasuresForOrg(org).catch(() => [] as KPIMeasure[]), listFactsForOrg(org).catch(() => [] as KPIFact[]), ]); - const jurisdictionName = - allJurisdictions.find((j) => j.slug === jurisdiction)?.name ?? jurisdiction; - const factsByMeasure = new Map(); for (const f of facts) { if (!factsByMeasure.has(f.measure_id)) factsByMeasure.set(f.measure_id, []); factsByMeasure.get(f.measure_id)!.push(f); } - const items: MeasureWithFacts[] = measures + const items = measures .map((m) => ({ measure: m, facts: factsByMeasure.get(m.id) ?? [] })) - .filter((item) => - item.facts.some((f) => f.value_numeric !== null), - ) - .sort((a, b) => a.measure.canonical_name.localeCompare(b.measure.canonical_name)); + .filter((i) => i.facts.some((f) => f.value_numeric !== null)) + .sort((a, b) => + a.measure.canonical_name.localeCompare(b.measure.canonical_name), + ); return ( -
-
- - ← All dashboards - -

- {orgData.canonical_name} -

-

- {jurisdictionName} - {orgData.kind ? ` · ${orgData.kind}` : ""} - {" · "} - {items.length} measure{items.length === 1 ? "" : "s"} with data +

+ {orgData?.description && ( +

+ {orgData.description}

- {orgData.description && ( -

- {orgData.description} -

- )} -
+ )} {items.length === 0 ? (
No numeric measures are currently tracked for this organization.
) : ( - + <> +

+ Pick a measure to see its trend over time, or browse the cards + below. +

+
    + {items.map(({ measure, facts: mFacts }) => { + const latest = latestActual(mFacts); + return ( +
  • + +
    + {measure.canonical_name} +
    + {measure.service_category && ( +
    + {measure.service_category} +
    + )} +
    + {latest && latest.value_numeric !== null ? ( + <> + + {formatValue(latest.value_numeric, measure.unit)} + + + actual · {latest.measurement_year} + + + ) : ( + + target/projection only + + )} +
    + +
  • + ); + })} +
+ )}
); diff --git a/src/app/dashboard/[jurisdiction]/[org]/types.ts b/src/app/dashboard/[jurisdiction]/[org]/types.ts deleted file mode 100644 index c0eccce..0000000 --- a/src/app/dashboard/[jurisdiction]/[org]/types.ts +++ /dev/null @@ -1,6 +0,0 @@ -import type { KPIFact, KPIMeasure } from "@/lib/api/kpis"; - -export interface MeasureWithFacts { - measure: KPIMeasure; - facts: KPIFact[]; -} diff --git a/src/lib/api/kpis.ts b/src/lib/api/kpis.ts index d065005..aaca21b 100644 --- a/src/lib/api/kpis.ts +++ b/src/lib/api/kpis.ts @@ -146,6 +146,32 @@ export async function listMeasuresForOrg( return all; } +export async function listFactsForMeasure( + measureId: number, +): Promise { + const all: KPIFact[] = []; + let page = 1; + while (true) { + const res = await apiFetch>( + `/kpis/measures/${measureId}/facts`, + { + params: { per_page: "100", page: String(page) }, + revalidate: REVALIDATE, + }, + ); + all.push(...res.data); + if (page >= res.meta.pages) break; + page++; + } + return all; +} + +export async function getMeasure(measureId: number): Promise { + return apiFetch(`/kpis/measures/${measureId}`, { + revalidate: REVALIDATE, + }); +} + export async function listFactsForOrg(orgSlug: string): Promise { const all: KPIFact[] = []; let page = 1; From 28ce10b8682ad9d627da12db3bdfc88431ee7252 Mon Sep 17 00:00:00 2001 From: xrendan Date: Tue, 26 May 2026 08:19:50 -0600 Subject: [PATCH 06/11] dashboard: add sources panel under each measure chart Lists every source document the measure's citations come from (PDF title, publication date, fiscal year, years covered, page numbers, and value types contributed). Documents are unique per row and sorted by publication date descending. Adds listCitationsForMeasure to src/lib/api/kpis.ts. --- .../[org]/[measure]/MeasureSources.tsx | 134 ++++++++++++++++++ .../[jurisdiction]/[org]/[measure]/page.tsx | 12 +- src/lib/api/kpis.ts | 43 ++++++ 3 files changed, 188 insertions(+), 1 deletion(-) create mode 100644 src/app/dashboard/[jurisdiction]/[org]/[measure]/MeasureSources.tsx diff --git a/src/app/dashboard/[jurisdiction]/[org]/[measure]/MeasureSources.tsx b/src/app/dashboard/[jurisdiction]/[org]/[measure]/MeasureSources.tsx new file mode 100644 index 0000000..c45a123 --- /dev/null +++ b/src/app/dashboard/[jurisdiction]/[org]/[measure]/MeasureSources.tsx @@ -0,0 +1,134 @@ +import type { KPICitation } from "@/lib/api/kpis"; + +interface DocSummary { + id: number; + doc_title: string; + doc_url: string; + published_at: string | null; + fiscal_year: number | null; + years: Set; + valueTypes: Set; + pages: Set; +} + +function formatDate(iso: string | null): string | null { + if (!iso) return null; + // Render as a stable date — server-rendered so locale flux is fine here. + const d = new Date(iso); + if (Number.isNaN(d.getTime())) return iso; + return d.toLocaleDateString("en-CA", { + year: "numeric", + month: "short", + day: "numeric", + }); +} + +function summarizeYears(years: number[]): string { + if (years.length === 0) return ""; + const sorted = [...years].sort((a, b) => a - b); + if (sorted.length === 1) return String(sorted[0]); + // Build run-length compressed ranges. + const ranges: string[] = []; + let start = sorted[0]; + let prev = sorted[0]; + for (let i = 1; i < sorted.length; i++) { + const y = sorted[i]; + if (y === prev + 1) { + prev = y; + continue; + } + ranges.push(start === prev ? `${start}` : `${start}–${prev}`); + start = y; + prev = y; + } + ranges.push(start === prev ? `${start}` : `${start}–${prev}`); + return ranges.join(", "); +} + +export default function MeasureSources({ + citations, +}: { + citations: KPICitation[]; +}) { + if (citations.length === 0) return null; + + const byDoc = new Map(); + for (const c of citations) { + const docId = c.document.id; + let entry = byDoc.get(docId); + if (!entry) { + entry = { + id: docId, + doc_title: c.document.doc_title, + doc_url: c.document.doc_url, + published_at: c.document.published_at, + fiscal_year: c.document.fiscal_year, + years: new Set(), + valueTypes: new Set(), + pages: new Set(), + }; + byDoc.set(docId, entry); + } + entry.years.add(c.measurement_year); + entry.valueTypes.add(c.value_type); + if (c.page_number != null) entry.pages.add(c.page_number); + } + + const docs = Array.from(byDoc.values()).sort((a, b) => { + const ad = a.published_at ?? ""; + const bd = b.published_at ?? ""; + if (ad !== bd) return bd.localeCompare(ad); + return (b.fiscal_year ?? 0) - (a.fiscal_year ?? 0); + }); + + return ( +
+

+ Sources +

+
    + {docs.map((doc) => { + const published = formatDate(doc.published_at); + const years = summarizeYears(Array.from(doc.years)); + const pages = + doc.pages.size === 0 + ? null + : doc.pages.size === 1 + ? `p. ${Array.from(doc.pages)[0]}` + : `pp. ${Array.from(doc.pages).sort((a, b) => a - b).join(", ")}`; + return ( +
  • + + {doc.doc_title} + +
    + {published && {published}} + {doc.fiscal_year && ( + + {published ? " · " : ""}FY {doc.fiscal_year} + + )} + {years && · {years}} + {pages && · {pages}} + {doc.valueTypes.size > 0 && ( + + {" · "} + {Array.from(doc.valueTypes).sort().join(", ")} + + )} +
    +
  • + ); + })} +
+
+ ); +} diff --git a/src/app/dashboard/[jurisdiction]/[org]/[measure]/page.tsx b/src/app/dashboard/[jurisdiction]/[org]/[measure]/page.tsx index 6dd0ea9..bddce28 100644 --- a/src/app/dashboard/[jurisdiction]/[org]/[measure]/page.tsx +++ b/src/app/dashboard/[jurisdiction]/[org]/[measure]/page.tsx @@ -2,10 +2,13 @@ import type { Metadata } from "next"; import { notFound } from "next/navigation"; import { getMeasure, + listCitationsForMeasure, listFactsForMeasure, listMeasuresForOrg, } from "@/lib/api/kpis"; +import type { KPICitation } from "@/lib/api/kpis"; import MeasureChartClient from "./MeasureChartClient"; +import MeasureSources from "./MeasureSources"; interface PageParams { jurisdiction: string; @@ -55,7 +58,12 @@ export default async function MeasurePage({ const measureData = await resolveMeasure(org, measure).catch(() => null); if (!measureData) notFound(); - const facts = await listFactsForMeasure(measureData.id).catch(() => []); + const [facts, citations] = await Promise.all([ + listFactsForMeasure(measureData.id).catch(() => []), + listCitationsForMeasure(measureData.id).catch( + () => [] as KPICitation[], + ), + ]); const numericYears = facts .filter((f) => f.value_numeric !== null) @@ -88,6 +96,8 @@ export default async function MeasurePage({
+ +
); } diff --git a/src/lib/api/kpis.ts b/src/lib/api/kpis.ts index aaca21b..e2822a4 100644 --- a/src/lib/api/kpis.ts +++ b/src/lib/api/kpis.ts @@ -166,6 +166,49 @@ export async function listFactsForMeasure( return all; } +export interface KPICitationDocument { + id: number; + fiscal_year: number | null; + published_at: string | null; + doc_url: string; + doc_title: string; +} + +export interface KPICitation { + id: number; + measure_id: number; + measurement_year: number; + value_type: KPIValueType; + period_basis: KPIPeriodBasis; + value_numeric: number | null; + value_text: string | null; + value_raw_text: string | null; + page_number: number | null; + notes: string | null; + agent_run_id: number | null; + document: KPICitationDocument; +} + +export async function listCitationsForMeasure( + measureId: number, +): Promise { + const all: KPICitation[] = []; + let page = 1; + while (true) { + const res = await apiFetch>( + `/kpis/measures/${measureId}/citations`, + { + params: { per_page: "100", page: String(page) }, + revalidate: REVALIDATE, + }, + ); + all.push(...res.data); + if (page >= res.meta.pages) break; + page++; + } + return all; +} + export async function getMeasure(measureId: number): Promise { return apiFetch(`/kpis/measures/${measureId}`, { revalidate: REVALIDATE, From b0d204629e4ca791b1c20c8f2a3449a182976bc8 Mon Sep 17 00:00:00 2001 From: xrendan Date: Tue, 26 May 2026 08:29:34 -0600 Subject: [PATCH 07/11] dashboard: surface citations inside the chart MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaces the standalone Sources panel with in-chart citation rendering on the @buildcanada/charts Grapher: - variable metadata.origins[] — one entry per source document (title, producer, urlMain, datePublished, citationFull with page numbers). Grapher's Sources tab renders these natively. - GrapherState.sourceDesc — compact "Data source:" footer line. - GrapherState.originUrl — clickable footer link to the most recent source PDF. --- .../[org]/[measure]/MeasureChart.tsx | 95 ++++++++++++- .../[org]/[measure]/MeasureChartClient.tsx | 6 +- .../[org]/[measure]/MeasureSources.tsx | 134 ------------------ .../[jurisdiction]/[org]/[measure]/page.tsx | 9 +- 4 files changed, 102 insertions(+), 142 deletions(-) delete mode 100644 src/app/dashboard/[jurisdiction]/[org]/[measure]/MeasureSources.tsx diff --git a/src/app/dashboard/[jurisdiction]/[org]/[measure]/MeasureChart.tsx b/src/app/dashboard/[jurisdiction]/[org]/[measure]/MeasureChart.tsx index 28f4acd..5bda321 100644 --- a/src/app/dashboard/[jurisdiction]/[org]/[measure]/MeasureChart.tsx +++ b/src/app/dashboard/[jurisdiction]/[org]/[measure]/MeasureChart.tsx @@ -10,7 +10,12 @@ import { GrapherState, legacyToChartsTableAndDimensionsWithMandatorySlug, } from "@buildcanada/charts"; -import type { KPIFact, KPIMeasure, KPIValueType } from "@/lib/api/kpis"; +import type { + KPICitation, + KPIFact, + KPIMeasure, + KPIValueType, +} from "@/lib/api/kpis"; type ChartTypeKey = "LineChart" | "DiscreteBar"; @@ -52,9 +57,68 @@ function pickDecimals(facts: KPIFact[]): number { return 2; } +interface DocAggregate { + id: number; + doc_title: string; + doc_url: string; + published_at: string | null; + fiscal_year: number | null; + years: Set; + pages: Set; +} + +function aggregateDocs(citations: KPICitation[]): DocAggregate[] { + const byDoc = new Map(); + for (const c of citations) { + const docId = c.document.id; + let entry = byDoc.get(docId); + if (!entry) { + entry = { + id: docId, + doc_title: c.document.doc_title, + doc_url: c.document.doc_url, + published_at: c.document.published_at, + fiscal_year: c.document.fiscal_year, + years: new Set(), + pages: new Set(), + }; + byDoc.set(docId, entry); + } + entry.years.add(c.measurement_year); + if (c.page_number != null) entry.pages.add(c.page_number); + } + return Array.from(byDoc.values()).sort((a, b) => { + const ad = a.published_at ?? ""; + const bd = b.published_at ?? ""; + if (ad !== bd) return bd.localeCompare(ad); + return (b.fiscal_year ?? 0) - (a.fiscal_year ?? 0); + }); +} + +function buildSourceDesc(docs: DocAggregate[]): string { + if (docs.length === 0) return ""; + const titles = new Set(docs.map((d) => d.doc_title)); + if (titles.size === 1) { + const docYears = docs + .map((d) => d.fiscal_year ?? null) + .filter((y): y is number => y != null) + .sort((a, b) => a - b); + if (docYears.length > 0) { + const span = + docYears[0] === docYears[docYears.length - 1] + ? `FY ${docYears[0]}` + : `FY ${docYears[0]}–${docYears[docYears.length - 1]}`; + return `${docs[0].doc_title} (${span})`; + } + return docs[0].doc_title; + } + return Array.from(titles).slice(0, 3).join("; "); +} + function buildGrapherState( measure: KPIMeasure, facts: KPIFact[], + citations: KPICitation[], bounds: Bounds, chartType: ChartTypeKey, ): GrapherState | null { @@ -79,6 +143,25 @@ function buildGrapherState( value: f.value_numeric as number, })); + const docs = aggregateDocs(citations); + const origins = docs.map((d) => ({ + id: d.id, + title: d.doc_title, + producer: measure.organization?.canonical_name, + urlMain: d.doc_url, + datePublished: d.published_at ?? undefined, + attribution: + d.fiscal_year != null + ? `${measure.organization?.canonical_name ?? ""}, FY ${d.fiscal_year}`.trim() + : (measure.organization?.canonical_name ?? undefined), + citationFull: + d.pages.size > 0 + ? `${d.doc_title} (pp. ${Array.from(d.pages) + .sort((a, b) => a - b) + .join(", ")})` + : d.doc_title, + })); + const metadata = { id: variableId, display: { @@ -87,6 +170,7 @@ function buildGrapherState( shortUnit: measure.unit.symbol, numDecimalPlaces: pickDecimals(numericFacts), }, + origins, }; const dimensions = [{ variableId, property: DimensionProperty.y }]; @@ -98,6 +182,10 @@ function buildGrapherState( dimensions, }); + const sourceDesc = buildSourceDesc(docs); + if (sourceDesc) grapherState.sourceDesc = sourceDesc; + if (docs[0]?.doc_url) grapherState.originUrl = docs[0].doc_url; + grapherState.inputTable = legacyToChartsTableAndDimensionsWithMandatorySlug( createTestDataset([{ data, metadata }]), dimensions, @@ -110,9 +198,11 @@ function buildGrapherState( export default function MeasureChart({ measure, facts, + citations, }: { measure: KPIMeasure; facts: KPIFact[]; + citations: KPICitation[]; }) { const [chartType, setChartType] = useState("LineChart"); const [size, setSize] = useState({ width: 800, height: 520 }); @@ -133,10 +223,11 @@ export default function MeasureChart({ buildGrapherState( measure, facts, + citations, new Bounds(0, 0, size.width, size.height), chartType, ), - [measure, facts, size.width, size.height, chartType], + [measure, facts, citations, size.width, size.height, chartType], ); return ( diff --git a/src/app/dashboard/[jurisdiction]/[org]/[measure]/MeasureChartClient.tsx b/src/app/dashboard/[jurisdiction]/[org]/[measure]/MeasureChartClient.tsx index 7e14783..7e3851a 100644 --- a/src/app/dashboard/[jurisdiction]/[org]/[measure]/MeasureChartClient.tsx +++ b/src/app/dashboard/[jurisdiction]/[org]/[measure]/MeasureChartClient.tsx @@ -1,7 +1,7 @@ "use client"; import dynamic from "next/dynamic"; -import type { KPIFact, KPIMeasure } from "@/lib/api/kpis"; +import type { KPICitation, KPIFact, KPIMeasure } from "@/lib/api/kpis"; const MeasureChart = dynamic(() => import("./MeasureChart"), { ssr: false, @@ -13,9 +13,11 @@ const MeasureChart = dynamic(() => import("./MeasureChart"), { export default function MeasureChartClient({ measure, facts, + citations, }: { measure: KPIMeasure; facts: KPIFact[]; + citations: KPICitation[]; }) { - return ; + return ; } diff --git a/src/app/dashboard/[jurisdiction]/[org]/[measure]/MeasureSources.tsx b/src/app/dashboard/[jurisdiction]/[org]/[measure]/MeasureSources.tsx deleted file mode 100644 index c45a123..0000000 --- a/src/app/dashboard/[jurisdiction]/[org]/[measure]/MeasureSources.tsx +++ /dev/null @@ -1,134 +0,0 @@ -import type { KPICitation } from "@/lib/api/kpis"; - -interface DocSummary { - id: number; - doc_title: string; - doc_url: string; - published_at: string | null; - fiscal_year: number | null; - years: Set; - valueTypes: Set; - pages: Set; -} - -function formatDate(iso: string | null): string | null { - if (!iso) return null; - // Render as a stable date — server-rendered so locale flux is fine here. - const d = new Date(iso); - if (Number.isNaN(d.getTime())) return iso; - return d.toLocaleDateString("en-CA", { - year: "numeric", - month: "short", - day: "numeric", - }); -} - -function summarizeYears(years: number[]): string { - if (years.length === 0) return ""; - const sorted = [...years].sort((a, b) => a - b); - if (sorted.length === 1) return String(sorted[0]); - // Build run-length compressed ranges. - const ranges: string[] = []; - let start = sorted[0]; - let prev = sorted[0]; - for (let i = 1; i < sorted.length; i++) { - const y = sorted[i]; - if (y === prev + 1) { - prev = y; - continue; - } - ranges.push(start === prev ? `${start}` : `${start}–${prev}`); - start = y; - prev = y; - } - ranges.push(start === prev ? `${start}` : `${start}–${prev}`); - return ranges.join(", "); -} - -export default function MeasureSources({ - citations, -}: { - citations: KPICitation[]; -}) { - if (citations.length === 0) return null; - - const byDoc = new Map(); - for (const c of citations) { - const docId = c.document.id; - let entry = byDoc.get(docId); - if (!entry) { - entry = { - id: docId, - doc_title: c.document.doc_title, - doc_url: c.document.doc_url, - published_at: c.document.published_at, - fiscal_year: c.document.fiscal_year, - years: new Set(), - valueTypes: new Set(), - pages: new Set(), - }; - byDoc.set(docId, entry); - } - entry.years.add(c.measurement_year); - entry.valueTypes.add(c.value_type); - if (c.page_number != null) entry.pages.add(c.page_number); - } - - const docs = Array.from(byDoc.values()).sort((a, b) => { - const ad = a.published_at ?? ""; - const bd = b.published_at ?? ""; - if (ad !== bd) return bd.localeCompare(ad); - return (b.fiscal_year ?? 0) - (a.fiscal_year ?? 0); - }); - - return ( -
-

- Sources -

-
    - {docs.map((doc) => { - const published = formatDate(doc.published_at); - const years = summarizeYears(Array.from(doc.years)); - const pages = - doc.pages.size === 0 - ? null - : doc.pages.size === 1 - ? `p. ${Array.from(doc.pages)[0]}` - : `pp. ${Array.from(doc.pages).sort((a, b) => a - b).join(", ")}`; - return ( -
  • - - {doc.doc_title} - -
    - {published && {published}} - {doc.fiscal_year && ( - - {published ? " · " : ""}FY {doc.fiscal_year} - - )} - {years && · {years}} - {pages && · {pages}} - {doc.valueTypes.size > 0 && ( - - {" · "} - {Array.from(doc.valueTypes).sort().join(", ")} - - )} -
    -
  • - ); - })} -
-
- ); -} diff --git a/src/app/dashboard/[jurisdiction]/[org]/[measure]/page.tsx b/src/app/dashboard/[jurisdiction]/[org]/[measure]/page.tsx index bddce28..040eb10 100644 --- a/src/app/dashboard/[jurisdiction]/[org]/[measure]/page.tsx +++ b/src/app/dashboard/[jurisdiction]/[org]/[measure]/page.tsx @@ -8,7 +8,6 @@ import { } from "@/lib/api/kpis"; import type { KPICitation } from "@/lib/api/kpis"; import MeasureChartClient from "./MeasureChartClient"; -import MeasureSources from "./MeasureSources"; interface PageParams { jurisdiction: string; @@ -95,9 +94,11 @@ export default async function MeasurePage({ )}
- - - +
); } From 7f02948f1cc4bc9c150007fa32e04ec4ba666925 Mon Sep 17 00:00:00 2001 From: xrendan Date: Tue, 26 May 2026 08:32:29 -0600 Subject: [PATCH 08/11] dashboard: tidy chart's Data source line for multi-doc measures MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Joining raw doc titles with "; " produced noisy footers like "Transportation Services - Operating Budget; Operating Budget". For measures backed by multiple source documents, the footer line now reads " · N source documents · FY " — individual titles still live in the Sources tab. --- .../[org]/[measure]/MeasureChart.tsx | 47 ++++++++++++------- 1 file changed, 30 insertions(+), 17 deletions(-) diff --git a/src/app/dashboard/[jurisdiction]/[org]/[measure]/MeasureChart.tsx b/src/app/dashboard/[jurisdiction]/[org]/[measure]/MeasureChart.tsx index 5bda321..6dfa300 100644 --- a/src/app/dashboard/[jurisdiction]/[org]/[measure]/MeasureChart.tsx +++ b/src/app/dashboard/[jurisdiction]/[org]/[measure]/MeasureChart.tsx @@ -95,24 +95,37 @@ function aggregateDocs(citations: KPICitation[]): DocAggregate[] { }); } -function buildSourceDesc(docs: DocAggregate[]): string { +function buildSourceDesc( + measure: KPIMeasure, + docs: DocAggregate[], +): string { if (docs.length === 0) return ""; - const titles = new Set(docs.map((d) => d.doc_title)); - if (titles.size === 1) { - const docYears = docs - .map((d) => d.fiscal_year ?? null) - .filter((y): y is number => y != null) - .sort((a, b) => a - b); - if (docYears.length > 0) { - const span = - docYears[0] === docYears[docYears.length - 1] - ? `FY ${docYears[0]}` - : `FY ${docYears[0]}–${docYears[docYears.length - 1]}`; - return `${docs[0].doc_title} (${span})`; - } - return docs[0].doc_title; + + const fiscalYears = docs + .map((d) => d.fiscal_year) + .filter((y): y is number => y != null); + const fySpan = + fiscalYears.length === 0 + ? "" + : Math.min(...fiscalYears) === Math.max(...fiscalYears) + ? `FY ${fiscalYears[0]}` + : `FY ${Math.min(...fiscalYears)}–${Math.max(...fiscalYears)}`; + + const producer = measure.organization?.canonical_name ?? ""; + + if (docs.length === 1) { + const parts = [docs[0].doc_title, fySpan].filter(Boolean); + return parts.join(" · "); } - return Array.from(titles).slice(0, 3).join("; "); + + // Multiple source documents — collapse to a producer + range summary so the + // footer line stays readable. Individual titles live in the Sources tab. + const parts = [ + producer, + `${docs.length} source documents`, + fySpan, + ].filter(Boolean); + return parts.join(" · "); } function buildGrapherState( @@ -182,7 +195,7 @@ function buildGrapherState( dimensions, }); - const sourceDesc = buildSourceDesc(docs); + const sourceDesc = buildSourceDesc(measure, docs); if (sourceDesc) grapherState.sourceDesc = sourceDesc; if (docs[0]?.doc_url) grapherState.originUrl = docs[0].doc_url; From a04d78819a2c916fda32554ff8b679dd9624911e Mon Sep 17 00:00:00 2001 From: xrendan Date: Mon, 1 Jun 2026 22:01:00 -0600 Subject: [PATCH 09/11] dashboard: scope KPI org API calls by jurisdiction --- .../[org]/[measure]/MeasureChart.tsx | 2 +- .../[jurisdiction]/[org]/[measure]/page.tsx | 16 +++++++++++----- .../dashboard/[jurisdiction]/[org]/layout.tsx | 2 +- src/app/dashboard/[jurisdiction]/[org]/page.tsx | 4 ++-- src/lib/api/kpis.ts | 16 ++++++++++++---- 5 files changed, 27 insertions(+), 13 deletions(-) diff --git a/src/app/dashboard/[jurisdiction]/[org]/[measure]/MeasureChart.tsx b/src/app/dashboard/[jurisdiction]/[org]/[measure]/MeasureChart.tsx index 6dfa300..bea2066 100644 --- a/src/app/dashboard/[jurisdiction]/[org]/[measure]/MeasureChart.tsx +++ b/src/app/dashboard/[jurisdiction]/[org]/[measure]/MeasureChart.tsx @@ -85,7 +85,7 @@ function aggregateDocs(citations: KPICitation[]): DocAggregate[] { byDoc.set(docId, entry); } entry.years.add(c.measurement_year); - if (c.page_number != null) entry.pages.add(c.page_number); + if (c.source_page != null) entry.pages.add(c.source_page); } return Array.from(byDoc.values()).sort((a, b) => { const ad = a.published_at ?? ""; diff --git a/src/app/dashboard/[jurisdiction]/[org]/[measure]/page.tsx b/src/app/dashboard/[jurisdiction]/[org]/[measure]/page.tsx index 040eb10..880a605 100644 --- a/src/app/dashboard/[jurisdiction]/[org]/[measure]/page.tsx +++ b/src/app/dashboard/[jurisdiction]/[org]/[measure]/page.tsx @@ -15,8 +15,12 @@ interface PageParams { measure: string; } -async function resolveMeasure(orgSlug: string, measureSlug: string) { - const measures = await listMeasuresForOrg(orgSlug); +async function resolveMeasure( + jurisdictionSlug: string, + orgSlug: string, + measureSlug: string, +) { + const measures = await listMeasuresForOrg(jurisdictionSlug, orgSlug); const found = measures.find((m) => m.slug === measureSlug); if (!found) return null; // Index lookup returns most of the measure, but the show endpoint includes @@ -34,7 +38,7 @@ export async function generateMetadata({ params: Promise; }): Promise { const { jurisdiction, org, measure } = await params; - const m = await resolveMeasure(org, measure).catch(() => null); + const m = await resolveMeasure(jurisdiction, org, measure).catch(() => null); if (!m) return { title: "Measure" }; return { title: `${m.canonical_name} — ${m.organization?.canonical_name ?? "KPI"}`, @@ -52,9 +56,11 @@ export default async function MeasurePage({ }: { params: Promise; }) { - const { org, measure } = await params; + const { jurisdiction, org, measure } = await params; - const measureData = await resolveMeasure(org, measure).catch(() => null); + const measureData = await resolveMeasure(jurisdiction, org, measure).catch( + () => null, + ); if (!measureData) notFound(); const [facts, citations] = await Promise.all([ diff --git a/src/app/dashboard/[jurisdiction]/[org]/layout.tsx b/src/app/dashboard/[jurisdiction]/[org]/layout.tsx index 62c94cd..15c2b61 100644 --- a/src/app/dashboard/[jurisdiction]/[org]/layout.tsx +++ b/src/app/dashboard/[jurisdiction]/[org]/layout.tsx @@ -32,7 +32,7 @@ export default async function OrgLayout({ const [allJurisdictions, measures] = await Promise.all([ listJurisdictions().catch(() => []), - listMeasuresForOrg(org).catch(() => [] as KPIMeasure[]), + listMeasuresForOrg(jurisdiction, org).catch(() => [] as KPIMeasure[]), ]); const jurisdictionName = diff --git a/src/app/dashboard/[jurisdiction]/[org]/page.tsx b/src/app/dashboard/[jurisdiction]/[org]/page.tsx index 64af449..de1142e 100644 --- a/src/app/dashboard/[jurisdiction]/[org]/page.tsx +++ b/src/app/dashboard/[jurisdiction]/[org]/page.tsx @@ -64,8 +64,8 @@ export default async function OrgOverviewPage({ const [orgData, measures, facts] = await Promise.all([ getOrganization(jurisdiction, org).catch(() => null), - listMeasuresForOrg(org).catch(() => [] as KPIMeasure[]), - listFactsForOrg(org).catch(() => [] as KPIFact[]), + listMeasuresForOrg(jurisdiction, org).catch(() => [] as KPIMeasure[]), + listFactsForOrg(jurisdiction, org).catch(() => [] as KPIFact[]), ]); const factsByMeasure = new Map(); diff --git a/src/lib/api/kpis.ts b/src/lib/api/kpis.ts index e2822a4..667e69f 100644 --- a/src/lib/api/kpis.ts +++ b/src/lib/api/kpis.ts @@ -79,7 +79,9 @@ export interface KPIFact { period_basis: KPIPeriodBasis; value_numeric: number | null; value_text: string | null; - citation_id: number; + citation_id?: number; + canonical_observation_id?: number; + extracted_observation_id?: number; document_id: number; } @@ -123,6 +125,7 @@ export async function getOrganization( } export async function listMeasuresForOrg( + jurisdictionSlug: string, orgSlug: string, ): Promise { const all: KPIMeasure[] = []; @@ -132,6 +135,7 @@ export async function listMeasuresForOrg( "/kpis/measures", { params: { + jurisdiction_slug: jurisdictionSlug, organization_slug: orgSlug, per_page: "100", page: String(page), @@ -182,8 +186,8 @@ export interface KPICitation { period_basis: KPIPeriodBasis; value_numeric: number | null; value_text: string | null; - value_raw_text: string | null; - page_number: number | null; + value_raw: string | null; + source_page: number | null; notes: string | null; agent_run_id: number | null; document: KPICitationDocument; @@ -215,12 +219,16 @@ export async function getMeasure(measureId: number): Promise { }); } -export async function listFactsForOrg(orgSlug: string): Promise { +export async function listFactsForOrg( + jurisdictionSlug: string, + orgSlug: string, +): Promise { const all: KPIFact[] = []; let page = 1; while (true) { const res = await apiFetch>("/kpis/facts", { params: { + jurisdiction_slug: jurisdictionSlug, organization_slug: orgSlug, per_page: "100", page: String(page), From a4cb459bf8e4cf8e25c7668d042bd474c42ade4c Mon Sep 17 00:00:00 2001 From: xrendan Date: Mon, 1 Jun 2026 22:03:36 -0600 Subject: [PATCH 10/11] dashboard: add coverage facets, year-range filter, and sparklines to org overview --- .../dashboard/[jurisdiction]/[org]/page.tsx | 404 ++++++++++++++++-- 1 file changed, 379 insertions(+), 25 deletions(-) diff --git a/src/app/dashboard/[jurisdiction]/[org]/page.tsx b/src/app/dashboard/[jurisdiction]/[org]/page.tsx index de1142e..0f64a94 100644 --- a/src/app/dashboard/[jurisdiction]/[org]/page.tsx +++ b/src/app/dashboard/[jurisdiction]/[org]/page.tsx @@ -12,6 +12,20 @@ interface PageParams { org: string; } +interface SearchParams { + from?: string; + to?: string; + coverage?: string; +} + +const COVERAGE_BUCKETS: { key: string; label: string; test: (n: number) => boolean }[] = [ + { key: "1", label: "1 year", test: (n) => n === 1 }, + { key: "2-3", label: "2–3 years", test: (n) => n >= 2 && n <= 3 }, + { key: "4-6", label: "4–6 years", test: (n) => n >= 4 && n <= 6 }, + { key: "7-10", label: "7–10 years", test: (n) => n >= 7 && n <= 10 }, + { key: "11plus", label: "11+ years", test: (n) => n >= 11 }, +]; + export async function generateMetadata({ params, }: { @@ -32,6 +46,13 @@ export async function generateMetadata({ } } +const TYPE_PRIORITY: Record = { + actual: 0, + target: 1, + forecast: 2, + planned: 3, +}; + function latestActual(facts: KPIFact[]): KPIFact | null { const actuals = facts.filter( (f) => @@ -45,6 +66,119 @@ function latestActual(facts: KPIFact[]): KPIFact | null { ); } +function latestActualInRange( + facts: KPIFact[], + from: number, + to: number, +): KPIFact | null { + const inRange = facts.filter( + (f) => + f.measurement_year >= from && + f.measurement_year <= to && + f.period_basis === "full_year" && + f.value_numeric !== null, + ); + if (inRange.length === 0) return null; + return inRange.reduce((acc, f) => { + const ap = TYPE_PRIORITY[acc.value_type] ?? 99; + const fp = TYPE_PRIORITY[f.value_type] ?? 99; + if (fp !== ap) return fp < ap ? f : acc; + return f.measurement_year > acc.measurement_year ? f : acc; + }); +} + +function distinctYears( + facts: KPIFact[], + from: number | null, + to: number | null, +): number { + const years = new Set(); + for (const f of facts) { + if (f.value_numeric === null || f.period_basis !== "full_year") continue; + if (from !== null && f.measurement_year < from) continue; + if (to !== null && f.measurement_year > to) continue; + years.add(f.measurement_year); + } + return years.size; +} + +function Sparkline({ + facts, + width = 140, + height = 36, +}: { + facts: KPIFact[]; + width?: number; + height?: number; +}) { + const points = facts + .filter( + (f) => + f.value_numeric !== null && + f.period_basis === "full_year" && + f.value_type === "actual", + ) + .map((f) => ({ x: f.measurement_year, y: f.value_numeric as number })) + .sort((a, b) => a.x - b.x); + + if (points.length === 0) { + return ( +
+ no actuals +
+ ); + } + + const pad = 2; + const xs = points.map((p) => p.x); + const ys = points.map((p) => p.y); + const xMin = Math.min(...xs); + const xMax = Math.max(...xs); + const yMin = Math.min(...ys); + const yMax = Math.max(...ys); + const xSpan = xMax - xMin || 1; + const ySpan = yMax - yMin || Math.max(1, Math.abs(yMax)); + + const toX = (x: number) => + pad + ((x - xMin) / xSpan) * (width - pad * 2); + const toY = (y: number) => + height - pad - ((y - yMin) / ySpan) * (height - pad * 2); + + if (points.length === 1) { + return ( + + + + ); + } + + const path = points.map((p) => `${toX(p.x)},${toY(p.y)}`).join(" "); + const last = points[points.length - 1]; + + return ( + + + + + ); +} + function formatValue(value: number, unit: KPIMeasure["unit"]): string { const abs = Math.abs(value); const decimals = abs >= 1000 ? 0 : abs >= 10 ? 1 : 2; @@ -57,10 +191,17 @@ function formatValue(value: number, unit: KPIMeasure["unit"]): string { export default async function OrgOverviewPage({ params, + searchParams, }: { params: Promise; + searchParams: Promise; }) { const { jurisdiction, org } = await params; + const { + from: fromParam, + to: toParam, + coverage: coverageParam, + } = await searchParams; const [orgData, measures, facts] = await Promise.all([ getOrganization(jurisdiction, org).catch(() => null), @@ -74,12 +215,71 @@ export default async function OrgOverviewPage({ factsByMeasure.get(f.measure_id)!.push(f); } - const items = measures + const availableYears = Array.from( + new Set( + facts + .filter((f) => f.value_numeric !== null && f.period_basis === "full_year") + .map((f) => f.measurement_year), + ), + ).sort((a, b) => b - a); + + const rawFrom = fromParam ? Number.parseInt(fromParam, 10) : NaN; + const rawTo = toParam ? Number.parseInt(toParam, 10) : NaN; + const fromYear = Number.isFinite(rawFrom) ? rawFrom : null; + const toYear = Number.isFinite(rawTo) ? rawTo : null; + const [normFrom, normTo] = + fromYear !== null && toYear !== null && fromYear > toYear + ? [toYear, fromYear] + : [fromYear, toYear]; + const bothBounds = normFrom !== null && normTo !== null; + const anyBound = normFrom !== null || normTo !== null; + const spanLength = bothBounds ? normTo - normFrom + 1 : 0; + + const allItems = measures .map((m) => ({ measure: m, facts: factsByMeasure.get(m.id) ?? [] })) - .filter((i) => i.facts.some((f) => f.value_numeric !== null)) - .sort((a, b) => - a.measure.canonical_name.localeCompare(b.measure.canonical_name), - ); + .filter((i) => i.facts.some((f) => f.value_numeric !== null)); + + const itemsWithCoverage = allItems.map((i) => ({ + ...i, + coverage: distinctYears(i.facts, null, null), + coverageInRange: anyBound ? distinctYears(i.facts, normFrom, normTo) : 0, + })); + + const coverageBucket = COVERAGE_BUCKETS.find((b) => b.key === coverageParam) ?? null; + const facetCounts = COVERAGE_BUCKETS.map((b) => ({ + bucket: b, + count: itemsWithCoverage.filter((i) => b.test(i.coverage)).length, + })); + + const afterRange = bothBounds + ? itemsWithCoverage.filter((i) => i.coverageInRange === spanLength) + : anyBound + ? itemsWithCoverage.filter((i) => i.coverageInRange > 0) + : itemsWithCoverage; + + const filtered = coverageBucket + ? afterRange.filter((i) => coverageBucket.test(i.coverage)) + : afterRange; + + const items = filtered.sort((a, b) => { + if (b.coverage !== a.coverage) return b.coverage - a.coverage; + return a.measure.canonical_name.localeCompare(b.measure.canonical_name); + }); + + const basePath = `/dashboard/${jurisdiction}/${org}`; + + const buildQuery = (overrides: Partial): string => { + const next: Record = {}; + if (normFrom !== null) next.from = String(normFrom); + if (normTo !== null) next.to = String(normTo); + if (coverageParam) next.coverage = coverageParam; + for (const [k, v] of Object.entries(overrides)) { + if (v === null || v === undefined || v === "") delete next[k]; + else next[k] = v; + } + const qs = new URLSearchParams(next).toString(); + return qs ? `${basePath}?${qs}` : basePath; + }; return (
@@ -89,48 +289,202 @@ export default async function OrgOverviewPage({

)} + {availableYears.length > 0 && ( +
+ {coverageParam && ( + + )} +
+ + +
+
+ + +
+ + {anyBound && ( + + Clear range + + )} + {bothBounds && ( + + Long-running only: measures with data in every year of{" "} + {normFrom}–{normTo}. + + )} + {!bothBounds && normFrom !== null && ( + + Measures with at least one data point in {normFrom} or later. + + )} + {!bothBounds && normTo !== null && ( + + Measures with at least one data point in {normTo} or earlier. + + )} +
+ )} + +
+
+ Data points (distinct years, lifetime) +
+
+ + Any ({itemsWithCoverage.length}) + + {facetCounts.map(({ bucket, count }) => { + const active = coverageBucket?.key === bucket.key; + const disabled = count === 0 && !active; + return ( + + {bucket.label} ({count}) + + ); + })} +
+
+ {items.length === 0 ? (
- No numeric measures are currently tracked for this organization. + {coverageBucket + ? `No measures match ${coverageBucket.label}${anyBound ? ` within range` : ""}.` + : bothBounds + ? `No measures have continuous data across ${normFrom}–${normTo}.` + : anyBound + ? `No measures have data within the selected range.` + : "No numeric measures are currently tracked for this organization."}
) : ( <>

- Pick a measure to see its trend over time, or browse the cards - below. + Showing {items.length} measure{items.length === 1 ? "" : "s"} + {bothBounds + ? ` with data in every year of ${normFrom}–${normTo}` + : normFrom !== null + ? ` with data in ${normFrom} or later` + : normTo !== null + ? ` with data in ${normTo} or earlier` + : ""} + {coverageBucket ? ` · ${coverageBucket.label}` : ""}.

    - {items.map(({ measure, facts: mFacts }) => { - const latest = latestActual(mFacts); + {items.map(({ measure, facts: mFacts, coverage }) => { + const fact = anyBound + ? latestActualInRange( + mFacts, + normFrom ?? Number.NEGATIVE_INFINITY, + normTo ?? Number.POSITIVE_INFINITY, + ) + : latestActual(mFacts); return (
  • -
    - {measure.canonical_name} +
    +
    + {measure.canonical_name} +
    + + {coverage}y +
    {measure.service_category && (
    {measure.service_category}
    )} -
    - {latest && latest.value_numeric !== null ? ( - <> - - {formatValue(latest.value_numeric, measure.unit)} - +
    +
    + {fact && fact.value_numeric !== null ? ( + <> + + {formatValue(fact.value_numeric, measure.unit)} + + + {fact.value_type} · {fact.measurement_year} + + + ) : ( - actual · {latest.measurement_year} + target/projection only - - ) : ( - - target/projection only - - )} + )} +
    +
  • From a8ca45d41117e7bd438b6c33d6cb87d6f6551449 Mon Sep 17 00:00:00 2001 From: xrendan Date: Mon, 1 Jun 2026 22:03:36 -0600 Subject: [PATCH 11/11] dashboard: gate /dashboard behind the "dashboard" PostHog flag Server-side gate via a Next proxy that evaluates the flag before the route renders, so a disabled flag returns 404 with no york_factory fetch or RSC leak. Falls open when PostHog isn't configured (e.g. local dev). Adds posthog-node for server-side flag evaluation. --- package.json | 1 + pnpm-lock.yaml | 28 +++++++++++++++++++++++++++ src/lib/posthog/server.ts | 24 +++++++++++++++++++++++ src/proxy.ts | 40 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 93 insertions(+) create mode 100644 src/lib/posthog/server.ts create mode 100644 src/proxy.ts diff --git a/package.json b/package.json index dda55a4..58b58ca 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "lucide-react": "^1.7.0", "next": "16.1.6", "posthog-js": "^1.368.0", + "posthog-node": "^5.35.11", "react": "19.2.3", "react-chartjs-2": "^5.3.0", "react-dom": "19.2.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7f024ba..6de003a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -50,6 +50,9 @@ importers: posthog-js: specifier: ^1.368.0 version: 1.368.0 + posthog-node: + specifier: ^5.35.11 + version: 5.35.11 react: specifier: 19.2.3 version: 19.2.3 @@ -928,9 +931,15 @@ packages: '@posthog/core@1.25.2': resolution: {integrity: sha512-h2FO7ut/BbfwpAXWpwdDHTzQgUo9ibDFEs6ZO+3cI3KPWQt5XwczK1OLAuPprcjm8T/jl0SH8jSFo5XdU4RbTg==} + '@posthog/core@1.30.2': + resolution: {integrity: sha512-d7RTpfi+/q5+SZ+4f1WhanfEtNBz9onMmUxn3BO0GDT8N5ZT4DEP3LqFisqeP+xkJTaFPWCOVA/nGyKmUX9y9g==} + '@posthog/types@1.368.0': resolution: {integrity: sha512-ORuGmgEmEkwaf/bEI4mOlrzIdOSJJ44vuz+XHPFycmt5Y6swjTXH/1NmIKas5KdKi/SOaKGvSkHGqUv7hHcNaw==} + '@posthog/types@1.378.1': + resolution: {integrity: sha512-bKOXVWySe5oKFjV6X9VW9jngIm14d4BvnT7l/Eb7e6DrT5uD+XclvbRdhC5f1/l5KwoIU+qswBobHRPlix2D1w==} + '@protobufjs/aspromise@1.1.2': resolution: {integrity: sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==} @@ -3003,6 +3012,15 @@ packages: posthog-js@1.368.0: resolution: {integrity: sha512-Smh2Q49oIOzGph73A3dLXCjepze8Nk0ApPtbJvbARFXI8XQp0mD+ia0RFcYQ25m2+PA1ViE8pR+Ga8yG7awzsw==} + posthog-node@5.35.11: + resolution: {integrity: sha512-HDgHr5eRmR9AYxoNCM5pyn2T/CLF4rjuYzgAo6BVopXBFrpyTVEt5+B9DxK2lKhsrIZTzg5bJ4G0VdPA139P9A==} + engines: {node: ^20.20.0 || >=22.22.0} + peerDependencies: + rxjs: ^7.0.0 + peerDependenciesMeta: + rxjs: + optional: true + preact@10.29.1: resolution: {integrity: sha512-gQCLc/vWroE8lIpleXtdJhTFDogTdZG9AjMUpVkDf2iTCNwYNWA+u16dL41TqUDJO4gm2IgrcMv3uTpjd4Pwmg==} @@ -4404,8 +4422,14 @@ snapshots: '@posthog/core@1.25.2': {} + '@posthog/core@1.30.2': + dependencies: + '@posthog/types': 1.378.1 + '@posthog/types@1.368.0': {} + '@posthog/types@1.378.1': {} + '@protobufjs/aspromise@1.1.2': {} '@protobufjs/base64@1.1.2': {} @@ -6785,6 +6809,10 @@ snapshots: query-selector-shadow-dom: 1.0.1 web-vitals: 5.2.0 + posthog-node@5.35.11: + dependencies: + '@posthog/core': 1.30.2 + preact@10.29.1: {} prelude-ls@1.2.1: {} diff --git a/src/lib/posthog/server.ts b/src/lib/posthog/server.ts new file mode 100644 index 0000000..53ea68a --- /dev/null +++ b/src/lib/posthog/server.ts @@ -0,0 +1,24 @@ +import { PostHog } from "posthog-node"; + +let client: PostHog | null = null; + +// Server-side PostHog client for evaluating feature flags during SSR. +// Returns null when PostHog isn't configured (e.g. local dev without a token), +// letting callers fall back to an "open" default. +export function getPostHogServer(): PostHog | null { + const key = process.env.NEXT_PUBLIC_POSTHOG_TOKEN; + if (!key) return null; + + if (!client) { + client = new PostHog(key, { + // Talk to PostHog directly — the `/ph` rewrite proxy only exists for the + // browser, not for server-side requests. + host: process.env.NEXT_PUBLIC_POSTHOG_HOST || "https://us.i.posthog.com", + // Flag checks don't need batching; keep the client from holding events. + flushAt: 1, + flushInterval: 0, + }); + } + + return client; +} diff --git a/src/proxy.ts b/src/proxy.ts new file mode 100644 index 0000000..6b3a876 --- /dev/null +++ b/src/proxy.ts @@ -0,0 +1,40 @@ +import { randomUUID } from "crypto"; +import { NextRequest, NextResponse } from "next/server"; +import { getPostHogServer } from "@/lib/posthog/server"; + +const FLAG = "dashboard"; + +// Gate the entire /dashboard section behind the `dashboard` PostHog flag. +// Running here (before the route renders) means a disabled flag never executes +// the dashboard pages — no york_factory fetch, no RSC payload, no title leak. +export const config = { + matcher: ["/dashboard", "/dashboard/:path*"], +}; + +export async function proxy(req: NextRequest) { + const posthog = getPostHogServer(); + + // PostHog not configured (e.g. local dev without a token) → gate is open. + if (!posthog) return NextResponse.next(); + + const token = process.env.NEXT_PUBLIC_POSTHOG_TOKEN as string; + const raw = req.cookies.get(`ph_${token}_posthog`)?.value; + let distinctId: string | undefined; + if (raw) { + try { + distinctId = JSON.parse(raw).distinct_id; + } catch { + // ignore malformed cookie + } + } + // Throwaway id for cookieless visitors so "release to everyone" flags pass. + // (Percentage rollouts need a stable id and can't be evaluated cookieless.) + distinctId ||= randomUUID(); + + const flags = await posthog.evaluateFlags(distinctId, { flagKeys: [FLAG] }); + if (flags.isEnabled(FLAG)) return NextResponse.next(); + + // Flag off → render the app's not-found page (404) without touching the + // dashboard routes. Rewriting to an unmatched path keeps the URL as-is. + return NextResponse.rewrite(new URL("/dashboard-unavailable-404", req.url)); +}