Skip to content
GitLab
Projects
Groups
Snippets
/
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Sign in
Toggle navigation
Menu
Open sidebar
HFT-informatics-weekend
SI - Time Series Map Viewer
Commits
048d0ef1
Commit
048d0ef1
authored
Oct 12, 2025
by
Heisenberg5124
Browse files
Initial commit
parent
36353266
Changes
9
Hide whitespace changes
Inline
Side-by-side
frontend/package-lock.json
View file @
048d0ef1
...
...
@@ -9,7 +9,9 @@
"version"
:
"0.0.0"
,
"dependencies"
:
{
"@napi-rs/canvas"
:
"^0.1.80"
,
"@radix-ui/react-dialog"
:
"^1.1.15"
,
"@radix-ui/react-label"
:
"^2.1.7"
,
"@radix-ui/react-progress"
:
"^1.1.7"
,
"@radix-ui/react-radio-group"
:
"^1.3.8"
,
"@radix-ui/react-select"
:
"^2.2.6"
,
"@radix-ui/react-slider"
:
"^1.3.6"
,
...
...
@@ -28,6 +30,7 @@
"react"
:
"^19.1.1"
,
"react-dom"
:
"^19.1.1"
,
"react-map-gl"
:
"^8.1.0"
,
"recharts"
:
"^3.2.1"
,
"tailwind-merge"
:
"^3.3.1"
,
"tailwindcss"
:
"^4.1.14"
},
...
...
@@ -1828,6 +1831,42 @@
}
}
},
"node_modules/@radix-ui/react-dialog"
:
{
"version"
:
"1.1.15"
,
"resolved"
:
"https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.15.tgz"
,
"integrity"
:
"sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw=="
,
"license"
:
"MIT"
,
"dependencies"
:
{
"@radix-ui/primitive"
:
"1.1.3"
,
"@radix-ui/react-compose-refs"
:
"1.1.2"
,
"@radix-ui/react-context"
:
"1.1.2"
,
"@radix-ui/react-dismissable-layer"
:
"1.1.11"
,
"@radix-ui/react-focus-guards"
:
"1.1.3"
,
"@radix-ui/react-focus-scope"
:
"1.1.7"
,
"@radix-ui/react-id"
:
"1.1.1"
,
"@radix-ui/react-portal"
:
"1.1.9"
,
"@radix-ui/react-presence"
:
"1.1.5"
,
"@radix-ui/react-primitive"
:
"2.1.3"
,
"@radix-ui/react-slot"
:
"1.2.3"
,
"@radix-ui/react-use-controllable-state"
:
"1.2.2"
,
"aria-hidden"
:
"^1.2.4"
,
"react-remove-scroll"
:
"^2.6.3"
},
"peerDependencies"
:
{
"@types/react"
:
"*"
,
"@types/react-dom"
:
"*"
,
"react"
:
"^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
,
"react-dom"
:
"^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta"
:
{
"@types/react"
:
{
"optional"
:
true
},
"@types/react-dom"
:
{
"optional"
:
true
}
}
},
"node_modules/@radix-ui/react-direction"
:
{
"version"
:
"1.1.1"
,
"resolved"
:
"https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz"
,
...
...
@@ -2054,6 +2093,30 @@
}
}
},
"node_modules/@radix-ui/react-progress"
:
{
"version"
:
"1.1.7"
,
"resolved"
:
"https://registry.npmjs.org/@radix-ui/react-progress/-/react-progress-1.1.7.tgz"
,
"integrity"
:
"sha512-vPdg/tF6YC/ynuBIJlk1mm7Le0VgW6ub6J2UWnTQ7/D23KXcPI1qy+0vBkgKgd38RCMJavBXpB83HPNFMTb0Fg=="
,
"license"
:
"MIT"
,
"dependencies"
:
{
"@radix-ui/react-context"
:
"1.1.2"
,
"@radix-ui/react-primitive"
:
"2.1.3"
},
"peerDependencies"
:
{
"@types/react"
:
"*"
,
"@types/react-dom"
:
"*"
,
"react"
:
"^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
,
"react-dom"
:
"^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta"
:
{
"@types/react"
:
{
"optional"
:
true
},
"@types/react-dom"
:
{
"optional"
:
true
}
}
},
"node_modules/@radix-ui/react-radio-group"
:
{
"version"
:
"1.3.8"
,
"resolved"
:
"https://registry.npmjs.org/@radix-ui/react-radio-group/-/react-radio-group-1.3.8.tgz"
,
...
...
@@ -2405,6 +2468,42 @@
"integrity"
:
"sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw=="
,
"license"
:
"MIT"
},
"node_modules/@reduxjs/toolkit"
:
{
"version"
:
"2.9.0"
,
"resolved"
:
"https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.9.0.tgz"
,
"integrity"
:
"sha512-fSfQlSRu9Z5yBkvsNhYF2rPS8cGXn/TZVrlwN1948QyZ8xMZ0JvP50S2acZNaf+o63u6aEeMjipFyksjIcWrog=="
,
"license"
:
"MIT"
,
"dependencies"
:
{
"@standard-schema/spec"
:
"^1.0.0"
,
"@standard-schema/utils"
:
"^0.3.0"
,
"immer"
:
"^10.0.3"
,
"redux"
:
"^5.0.1"
,
"redux-thunk"
:
"^3.1.0"
,
"reselect"
:
"^5.1.0"
},
"peerDependencies"
:
{
"react"
:
"^16.9.0 || ^17.0.0 || ^18 || ^19"
,
"react-redux"
:
"^7.2.1 || ^8.1.3 || ^9.0.0"
},
"peerDependenciesMeta"
:
{
"react"
:
{
"optional"
:
true
},
"react-redux"
:
{
"optional"
:
true
}
}
},
"node_modules/@reduxjs/toolkit/node_modules/immer"
:
{
"version"
:
"10.1.3"
,
"resolved"
:
"https://registry.npmjs.org/immer/-/immer-10.1.3.tgz"
,
"integrity"
:
"sha512-tmjF/k8QDKydUlm3mZU+tjM6zeq9/fFpPqH9SzWmBnVVKsPBg/V66qsMwb3/Bo90cgUN+ghdVBess+hPsxUyRw=="
,
"license"
:
"MIT"
,
"funding"
:
{
"type"
:
"opencollective"
,
"url"
:
"https://opencollective.com/immer"
}
},
"node_modules/@rolldown/pluginutils"
:
{
"version"
:
"1.0.0-beta.38"
,
"resolved"
:
"https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.38.tgz"
,
...
...
@@ -2747,6 +2846,18 @@
"dev"
:
true
,
"license"
:
"MIT"
},
"node_modules/@standard-schema/spec"
:
{
"version"
:
"1.0.0"
,
"resolved"
:
"https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz"
,
"integrity"
:
"sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA=="
,
"license"
:
"MIT"
},
"node_modules/@standard-schema/utils"
:
{
"version"
:
"0.3.0"
,
"resolved"
:
"https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz"
,
"integrity"
:
"sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g=="
,
"license"
:
"MIT"
},
"node_modules/@stoplight/json"
:
{
"version"
:
"3.21.7"
,
"resolved"
:
"https://registry.npmjs.org/@stoplight/json/-/json-3.21.7.tgz"
,
...
...
@@ -3652,6 +3763,69 @@
"@babel/types"
:
"^7.28.2"
}
},
"node_modules/@types/d3-array"
:
{
"version"
:
"3.2.2"
,
"resolved"
:
"https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz"
,
"integrity"
:
"sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw=="
,
"license"
:
"MIT"
},
"node_modules/@types/d3-color"
:
{
"version"
:
"3.1.3"
,
"resolved"
:
"https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz"
,
"integrity"
:
"sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A=="
,
"license"
:
"MIT"
},
"node_modules/@types/d3-ease"
:
{
"version"
:
"3.0.2"
,
"resolved"
:
"https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz"
,
"integrity"
:
"sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA=="
,
"license"
:
"MIT"
},
"node_modules/@types/d3-interpolate"
:
{
"version"
:
"3.0.4"
,
"resolved"
:
"https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz"
,
"integrity"
:
"sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA=="
,
"license"
:
"MIT"
,
"dependencies"
:
{
"@types/d3-color"
:
"*"
}
},
"node_modules/@types/d3-path"
:
{
"version"
:
"3.1.1"
,
"resolved"
:
"https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz"
,
"integrity"
:
"sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg=="
,
"license"
:
"MIT"
},
"node_modules/@types/d3-scale"
:
{
"version"
:
"4.0.9"
,
"resolved"
:
"https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz"
,
"integrity"
:
"sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw=="
,
"license"
:
"MIT"
,
"dependencies"
:
{
"@types/d3-time"
:
"*"
}
},
"node_modules/@types/d3-shape"
:
{
"version"
:
"3.1.7"
,
"resolved"
:
"https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.7.tgz"
,
"integrity"
:
"sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg=="
,
"license"
:
"MIT"
,
"dependencies"
:
{
"@types/d3-path"
:
"*"
}
},
"node_modules/@types/d3-time"
:
{
"version"
:
"3.0.4"
,
"resolved"
:
"https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz"
,
"integrity"
:
"sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g=="
,
"license"
:
"MIT"
},
"node_modules/@types/d3-timer"
:
{
"version"
:
"3.0.2"
,
"resolved"
:
"https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz"
,
"integrity"
:
"sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw=="
,
"license"
:
"MIT"
},
"node_modules/@types/es-aggregate-error"
:
{
"version"
:
"1.0.6"
,
"resolved"
:
"https://registry.npmjs.org/@types/es-aggregate-error/-/es-aggregate-error-1.0.6.tgz"
,
...
...
@@ -3753,6 +3927,12 @@
"dev"
:
true
,
"license"
:
"MIT"
},
"node_modules/@types/use-sync-external-store"
:
{
"version"
:
"0.0.6"
,
"resolved"
:
"https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz"
,
"integrity"
:
"sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg=="
,
"license"
:
"MIT"
},
"node_modules/@typescript-eslint/eslint-plugin"
:
{
"version"
:
"8.46.0"
,
"resolved"
:
"https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.46.0.tgz"
,
...
...
@@ -4766,6 +4946,127 @@
"devOptional"
:
true
,
"license"
:
"MIT"
},
"node_modules/d3-array"
:
{
"version"
:
"3.2.4"
,
"resolved"
:
"https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz"
,
"integrity"
:
"sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg=="
,
"license"
:
"ISC"
,
"dependencies"
:
{
"internmap"
:
"1 - 2"
},
"engines"
:
{
"node"
:
">=12"
}
},
"node_modules/d3-color"
:
{
"version"
:
"3.1.0"
,
"resolved"
:
"https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz"
,
"integrity"
:
"sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA=="
,
"license"
:
"ISC"
,
"engines"
:
{
"node"
:
">=12"
}
},
"node_modules/d3-ease"
:
{
"version"
:
"3.0.1"
,
"resolved"
:
"https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz"
,
"integrity"
:
"sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w=="
,
"license"
:
"BSD-3-Clause"
,
"engines"
:
{
"node"
:
">=12"
}
},
"node_modules/d3-format"
:
{
"version"
:
"3.1.0"
,
"resolved"
:
"https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz"
,
"integrity"
:
"sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA=="
,
"license"
:
"ISC"
,
"engines"
:
{
"node"
:
">=12"
}
},
"node_modules/d3-interpolate"
:
{
"version"
:
"3.0.1"
,
"resolved"
:
"https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz"
,
"integrity"
:
"sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g=="
,
"license"
:
"ISC"
,
"dependencies"
:
{
"d3-color"
:
"1 - 3"
},
"engines"
:
{
"node"
:
">=12"
}
},
"node_modules/d3-path"
:
{
"version"
:
"3.1.0"
,
"resolved"
:
"https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz"
,
"integrity"
:
"sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ=="
,
"license"
:
"ISC"
,
"engines"
:
{
"node"
:
">=12"
}
},
"node_modules/d3-scale"
:
{
"version"
:
"4.0.2"
,
"resolved"
:
"https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz"
,
"integrity"
:
"sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ=="
,
"license"
:
"ISC"
,
"dependencies"
:
{
"d3-array"
:
"2.10.0 - 3"
,
"d3-format"
:
"1 - 3"
,
"d3-interpolate"
:
"1.2.0 - 3"
,
"d3-time"
:
"2.1.1 - 3"
,
"d3-time-format"
:
"2 - 4"
},
"engines"
:
{
"node"
:
">=12"
}
},
"node_modules/d3-shape"
:
{
"version"
:
"3.2.0"
,
"resolved"
:
"https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz"
,
"integrity"
:
"sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA=="
,
"license"
:
"ISC"
,
"dependencies"
:
{
"d3-path"
:
"^3.1.0"
},
"engines"
:
{
"node"
:
">=12"
}
},
"node_modules/d3-time"
:
{
"version"
:
"3.1.0"
,
"resolved"
:
"https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz"
,
"integrity"
:
"sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q=="
,
"license"
:
"ISC"
,
"dependencies"
:
{
"d3-array"
:
"2 - 3"
},
"engines"
:
{
"node"
:
">=12"
}
},
"node_modules/d3-time-format"
:
{
"version"
:
"4.1.0"
,
"resolved"
:
"https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz"
,
"integrity"
:
"sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg=="
,
"license"
:
"ISC"
,
"dependencies"
:
{
"d3-time"
:
"1 - 3"
},
"engines"
:
{
"node"
:
">=12"
}
},
"node_modules/d3-timer"
:
{
"version"
:
"3.0.1"
,
"resolved"
:
"https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz"
,
"integrity"
:
"sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA=="
,
"license"
:
"ISC"
,
"engines"
:
{
"node"
:
">=12"
}
},
"node_modules/data-view-buffer"
:
{
"version"
:
"1.0.2"
,
"resolved"
:
"https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz"
,
...
...
@@ -4838,6 +5139,12 @@
}
}
},
"node_modules/decimal.js-light"
:
{
"version"
:
"2.5.1"
,
"resolved"
:
"https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz"
,
"integrity"
:
"sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg=="
,
"license"
:
"MIT"
},
"node_modules/decompress-response"
:
{
"version"
:
"6.0.0"
,
"resolved"
:
"https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz"
,
...
...
@@ -5186,6 +5493,16 @@
"url"
:
"https://github.com/sponsors/ljharb"
}
},
"node_modules/es-toolkit"
:
{
"version"
:
"1.40.0"
,
"resolved"
:
"https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.40.0.tgz"
,
"integrity"
:
"sha512-8o6w0KFmU0CiIl0/Q/BCEOabF2IJaELM1T2PWj6e8KqzHv1gdx+7JtFnDwOx1kJH/isJ5NwlDG1nCr1HrRF94Q=="
,
"license"
:
"MIT"
,
"workspaces"
:
[
"docs"
,
"benchmarks"
]
},
"node_modules/es6-promise"
:
{
"version"
:
"3.3.1"
,
"resolved"
:
"https://registry.npmjs.org/es6-promise/-/es6-promise-3.3.1.tgz"
,
...
...
@@ -5445,6 +5762,12 @@
"node"
:
">=6"
}
},
"node_modules/eventemitter3"
:
{
"version"
:
"5.0.1"
,
"resolved"
:
"https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz"
,
"integrity"
:
"sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA=="
,
"license"
:
"MIT"
},
"node_modules/execa"
:
{
"version"
:
"5.1.1"
,
"resolved"
:
"https://registry.npmjs.org/execa/-/execa-5.1.1.tgz"
,
...
...
@@ -6195,6 +6518,15 @@
"node"
:
">= 0.4"
}
},
"node_modules/internmap"
:
{
"version"
:
"2.0.3"
,
"resolved"
:
"https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz"
,
"integrity"
:
"sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg=="
,
"license"
:
"ISC"
,
"engines"
:
{
"node"
:
">=12"
}
},
"node_modules/is-array-buffer"
:
{
"version"
:
"3.0.5"
,
"resolved"
:
"https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz"
,
...
...
@@ -8156,6 +8488,13 @@
"react"
:
"^19.2.0"
}
},
"node_modules/react-is"
:
{
"version"
:
"19.2.0"
,
"resolved"
:
"https://registry.npmjs.org/react-is/-/react-is-19.2.0.tgz"
,
"integrity"
:
"sha512-x3Ax3kNSMIIkyVYhWPyO09bu0uttcAIoecO/um/rKGQ4EltYWVYtyiGkS/3xMynrbVQdS69Jhlv8FXUEZehlzA=="
,
"license"
:
"MIT"
,
"peer"
:
true
},
"node_modules/react-map-gl"
:
{
"version"
:
"8.1.0"
,
"resolved"
:
"https://registry.npmjs.org/react-map-gl/-/react-map-gl-8.1.0.tgz"
,
...
...
@@ -8180,6 +8519,29 @@
}
}
},
"node_modules/react-redux"
:
{
"version"
:
"9.2.0"
,
"resolved"
:
"https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz"
,
"integrity"
:
"sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g=="
,
"license"
:
"MIT"
,
"dependencies"
:
{
"@types/use-sync-external-store"
:
"^0.0.6"
,
"use-sync-external-store"
:
"^1.4.0"
},
"peerDependencies"
:
{
"@types/react"
:
"^18.2.25 || ^19"
,
"react"
:
"^18.0 || ^19"
,
"redux"
:
"^5.0.0"
},
"peerDependenciesMeta"
:
{
"@types/react"
:
{
"optional"
:
true
},
"redux"
:
{
"optional"
:
true
}
}
},
"node_modules/react-refresh"
:
{
"version"
:
"0.17.0"
,
"resolved"
:
"https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz"
,
...
...
@@ -8287,6 +8649,58 @@
"url"
:
"https://paulmillr.com/funding/"
}
},
"node_modules/recharts"
:
{
"version"
:
"3.2.1"
,
"resolved"
:
"https://registry.npmjs.org/recharts/-/recharts-3.2.1.tgz"
,
"integrity"
:
"sha512-0JKwHRiFZdmLq/6nmilxEZl3pqb4T+aKkOkOi/ZISRZwfBhVMgInxzlYU9D4KnCH3KINScLy68m/OvMXoYGZUw=="
,
"license"
:
"MIT"
,
"dependencies"
:
{
"@reduxjs/toolkit"
:
"1.x.x || 2.x.x"
,
"clsx"
:
"^2.1.1"
,
"decimal.js-light"
:
"^2.5.1"
,
"es-toolkit"
:
"^1.39.3"
,
"eventemitter3"
:
"^5.0.1"
,
"immer"
:
"^10.1.1"
,
"react-redux"
:
"8.x.x || 9.x.x"
,
"reselect"
:
"5.1.1"
,
"tiny-invariant"
:
"^1.3.3"
,
"use-sync-external-store"
:
"^1.2.2"
,
"victory-vendor"
:
"^37.0.2"
},
"engines"
:
{
"node"
:
">=18"
},
"peerDependencies"
:
{
"react"
:
"^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
,
"react-dom"
:
"^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
,
"react-is"
:
"^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/recharts/node_modules/immer"
:
{
"version"
:
"10.1.3"
,
"resolved"
:
"https://registry.npmjs.org/immer/-/immer-10.1.3.tgz"
,
"integrity"
:
"sha512-tmjF/k8QDKydUlm3mZU+tjM6zeq9/fFpPqH9SzWmBnVVKsPBg/V66qsMwb3/Bo90cgUN+ghdVBess+hPsxUyRw=="
,
"license"
:
"MIT"
,
"funding"
:
{
"type"
:
"opencollective"
,
"url"
:
"https://opencollective.com/immer"
}
},
"node_modules/redux"
:
{
"version"
:
"5.0.1"
,
"resolved"
:
"https://registry.npmjs.org/redux/-/redux-5.0.1.tgz"
,
"integrity"
:
"sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w=="
,
"license"
:
"MIT"
},
"node_modules/redux-thunk"
:
{
"version"
:
"3.1.0"
,
"resolved"
:
"https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz"
,
"integrity"
:
"sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw=="
,
"license"
:
"MIT"
,
"peerDependencies"
:
{
"redux"
:
"^5.0.0"
}
},
"node_modules/reflect.getprototypeof"
:
{
"version"
:
"1.0.10"
,
"resolved"
:
"https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz"
,
...
...
@@ -8361,6 +8775,12 @@
"node"
:
">=0.10.0"
}
},
"node_modules/reselect"
:
{
"version"
:
"5.1.1"
,
"resolved"
:
"https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz"
,
"integrity"
:
"sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w=="
,
"license"
:
"MIT"
},
"node_modules/resolve-from"
:
{
"version"
:
"4.0.0"
,
"resolved"
:
"https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz"
,
...
...
@@ -9800,6 +10220,28 @@
"node"
:
">= 0.10"
}
},
"node_modules/victory-vendor"
:
{
"version"
:
"37.3.6"
,
"resolved"
:
"https://registry.npmjs.org/victory-vendor/-/victory-vendor-37.3.6.tgz"
,
"integrity"
:
"sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ=="
,
"license"
:
"MIT AND ISC"
,
"dependencies"
:
{
"@types/d3-array"
:
"^3.0.3"
,
"@types/d3-ease"
:
"^3.0.0"
,
"@types/d3-interpolate"
:
"^3.0.1"
,
"@types/d3-scale"
:
"^4.0.2"
,
"@types/d3-shape"
:
"^3.1.0"
,
"@types/d3-time"
:
"^3.0.0"
,
"@types/d3-timer"
:
"^3.0.0"
,
"d3-array"
:
"^3.1.6"
,
"d3-ease"
:
"^3.0.1"
,
"d3-interpolate"
:
"^3.0.1"
,
"d3-scale"
:
"^4.0.2"
,
"d3-shape"
:
"^3.1.0"
,
"d3-time"
:
"^3.0.0"
,
"d3-timer"
:
"^3.0.1"
}
},
"node_modules/vite"
:
{
"version"
:
"7.1.9"
,
"resolved"
:
"https://registry.npmjs.org/vite/-/vite-7.1.9.tgz"
,
...
...
frontend/package.json
View file @
048d0ef1
...
...
@@ -12,7 +12,9 @@
},
"dependencies"
:
{
"@napi-rs/canvas"
:
"^0.1.80"
,
"@radix-ui/react-dialog"
:
"^1.1.15"
,
"@radix-ui/react-label"
:
"^2.1.7"
,
"@radix-ui/react-progress"
:
"^1.1.7"
,
"@radix-ui/react-radio-group"
:
"^1.3.8"
,
"@radix-ui/react-select"
:
"^2.2.6"
,
"@radix-ui/react-slider"
:
"^1.3.6"
,
...
...
@@ -31,6 +33,7 @@
"react"
:
"^19.1.1"
,
"react-dom"
:
"^19.1.1"
,
"react-map-gl"
:
"^8.1.0"
,
"recharts"
:
"^3.2.1"
,
"tailwind-merge"
:
"^3.3.1"
,
"tailwindcss"
:
"^4.1.14"
},
...
...
frontend/src/App.tsx
View file @
048d0ef1
...
...
@@ -4,9 +4,31 @@ import {useState} from "react";
import
{
SingleMap
}
from
"
@/components/map/single-map.tsx
"
;
import
{
SplitMap
}
from
"
@/components/map/split-map.tsx
"
;
import
{
SplitMapCard
}
from
"
@/components/map/split-map-card.tsx
"
;
import
KpiDialog
from
"
@/components/map/kpi-dialog.tsx
"
;
type
Mode
=
'
single
'
|
'
split
'
;
const
kpiMock
=
{
left
:
{
year
:
2020
,
median
:
0.22810404002666473
,
p95
:
0.6100596785545349
,
frac_gt_0_4
:
0.18748562226823096
,
frac_gt_0_6
:
0.05244996549344375
,
},
right
:
{
year
:
2025
,
median
:
0.15449757874011993
,
p95
:
0.4455701410770416
,
frac_gt_0_4
:
0.07292385553255118
,
frac_gt_0_6
:
0.0006901311249137336
,
},
change
:
{
fracLoss
:
0.3400046008741661
,
// Δ ≤ -0.1
fracGain
:
0.006211180124223602
,
// Δ ≥ +0.1
},
}
as
const
;
function
App
()
{
const
MIN_YEAR
=
2020
;
const
MAX_YEAR
=
2025
;
...
...
@@ -19,6 +41,7 @@ function App() {
<
div
className
=
"absolute right-4 top-4 z-20 w-[min(320px,42vw)] space-y-3"
>
<
MapLayerCard
/>
<
SplitMapCard
mode
=
{
mode
}
setMode
=
{
setMode
}
/>
{
mode
===
'
split
'
&&
(<
KpiDialog
{
...
kpiMock
}
/>)
}
</
div
>
<
div
className
=
"h-full w-full"
>
...
...
frontend/src/api/__generated__/index.ts
View file @
048d0ef1
...
...
@@ -7,7 +7,7 @@ An API that returns a satellite image file for a given year and computes NDVI &
- Swagger UI: `/docs`
- ReDoc: `/redoc`
* OpenAPI spec version: 1.
3
.0
* OpenAPI spec version: 1.
4
.0
*/
import
{
useMutation
,
...
...
@@ -192,6 +192,31 @@ clip_to_aoi?: boolean;
compare_res_m
?:
number
;
};
export
type
NdviChangeValueNdviChangeValueGetParams
=
{
/**
* @minimum 2013
*/
start_year
:
number
;
/**
* @minimum 2013
*/
end_year
:
number
;
/**
* NDVI summary to compare: mean | median | p05 | p95
*/
metric
?:
string
;
/**
* @minimum 0
* @maximum 100
*/
max_cloud
?:
number
;
/**
* Optional AOI buffer (m)
* @minimum 0
*/
buffer_meters
?:
number
;
};
/**
* Returns a GeoTIFF image file for the requested `year`.
By default returns the best (least cloudy) June–August image intersecting your AOI.
...
...
@@ -534,3 +559,96 @@ export function useNdviChangeNdviChangeGet<TData = Awaited<ReturnType<typeof ndv
return
query
;
}
/**
* @summary Fast NDVI change value (in-memory, no file writes)
*/
export
const
ndviChangeValueNdviChangeValueGet
=
(
params
:
NdviChangeValueNdviChangeValueGetParams
,
signal
?:
AbortSignal
)
=>
{
return
apiFetch
<
unknown
>
(
{
url
:
`/ndvi-change/value`
,
method
:
'
GET
'
,
params
,
signal
},
);
}
export
const
getNdviChangeValueNdviChangeValueGetQueryKey
=
(
params
?:
NdviChangeValueNdviChangeValueGetParams
,)
=>
{
return
[
`/ndvi-change/value`
,
...(
params
?
[
params
]:
[])
]
as
const
;
}
export
const
getNdviChangeValueNdviChangeValueGetQueryOptions
=
<
TData
=
Awaited
<
ReturnType
<
typeof
ndviChangeValueNdviChangeValueGet
>>
,
TError
=
HTTPValidationError
>
(
params
:
NdviChangeValueNdviChangeValueGetParams
,
options
?:
{
query
?:
Partial
<
UseQueryOptions
<
Awaited
<
ReturnType
<
typeof
ndviChangeValueNdviChangeValueGet
>>
,
TError
,
TData
>>
,
}
)
=>
{
const
{
query
:
queryOptions
}
=
options
??
{};
const
queryKey
=
queryOptions
?.
queryKey
??
getNdviChangeValueNdviChangeValueGetQueryKey
(
params
);
const
queryFn
:
QueryFunction
<
Awaited
<
ReturnType
<
typeof
ndviChangeValueNdviChangeValueGet
>>>
=
({
signal
})
=>
ndviChangeValueNdviChangeValueGet
(
params
,
signal
);
return
{
queryKey
,
queryFn
,
...
queryOptions
}
as
UseQueryOptions
<
Awaited
<
ReturnType
<
typeof
ndviChangeValueNdviChangeValueGet
>>
,
TError
,
TData
>
&
{
queryKey
:
DataTag
<
QueryKey
,
TData
,
TError
>
}
}
export
type
NdviChangeValueNdviChangeValueGetQueryResult
=
NonNullable
<
Awaited
<
ReturnType
<
typeof
ndviChangeValueNdviChangeValueGet
>>>
export
type
NdviChangeValueNdviChangeValueGetQueryError
=
HTTPValidationError
export
function
useNdviChangeValueNdviChangeValueGet
<
TData
=
Awaited
<
ReturnType
<
typeof
ndviChangeValueNdviChangeValueGet
>>
,
TError
=
HTTPValidationError
>
(
params
:
NdviChangeValueNdviChangeValueGetParams
,
options
:
{
query
:
Partial
<
UseQueryOptions
<
Awaited
<
ReturnType
<
typeof
ndviChangeValueNdviChangeValueGet
>>
,
TError
,
TData
>>
&
Pick
<
DefinedInitialDataOptions
<
Awaited
<
ReturnType
<
typeof
ndviChangeValueNdviChangeValueGet
>>
,
TError
,
Awaited
<
ReturnType
<
typeof
ndviChangeValueNdviChangeValueGet
>>
>
,
'
initialData
'
>
,
}
,
queryClient
?:
QueryClient
):
DefinedUseQueryResult
<
TData
,
TError
>
&
{
queryKey
:
DataTag
<
QueryKey
,
TData
,
TError
>
}
export
function
useNdviChangeValueNdviChangeValueGet
<
TData
=
Awaited
<
ReturnType
<
typeof
ndviChangeValueNdviChangeValueGet
>>
,
TError
=
HTTPValidationError
>
(
params
:
NdviChangeValueNdviChangeValueGetParams
,
options
?:
{
query
?:
Partial
<
UseQueryOptions
<
Awaited
<
ReturnType
<
typeof
ndviChangeValueNdviChangeValueGet
>>
,
TError
,
TData
>>
&
Pick
<
UndefinedInitialDataOptions
<
Awaited
<
ReturnType
<
typeof
ndviChangeValueNdviChangeValueGet
>>
,
TError
,
Awaited
<
ReturnType
<
typeof
ndviChangeValueNdviChangeValueGet
>>
>
,
'
initialData
'
>
,
}
,
queryClient
?:
QueryClient
):
UseQueryResult
<
TData
,
TError
>
&
{
queryKey
:
DataTag
<
QueryKey
,
TData
,
TError
>
}
export
function
useNdviChangeValueNdviChangeValueGet
<
TData
=
Awaited
<
ReturnType
<
typeof
ndviChangeValueNdviChangeValueGet
>>
,
TError
=
HTTPValidationError
>
(
params
:
NdviChangeValueNdviChangeValueGetParams
,
options
?:
{
query
?:
Partial
<
UseQueryOptions
<
Awaited
<
ReturnType
<
typeof
ndviChangeValueNdviChangeValueGet
>>
,
TError
,
TData
>>
,
}
,
queryClient
?:
QueryClient
):
UseQueryResult
<
TData
,
TError
>
&
{
queryKey
:
DataTag
<
QueryKey
,
TData
,
TError
>
}
/**
* @summary Fast NDVI change value (in-memory, no file writes)
*/
export
function
useNdviChangeValueNdviChangeValueGet
<
TData
=
Awaited
<
ReturnType
<
typeof
ndviChangeValueNdviChangeValueGet
>>
,
TError
=
HTTPValidationError
>
(
params
:
NdviChangeValueNdviChangeValueGetParams
,
options
?:
{
query
?:
Partial
<
UseQueryOptions
<
Awaited
<
ReturnType
<
typeof
ndviChangeValueNdviChangeValueGet
>>
,
TError
,
TData
>>
,
}
,
queryClient
?:
QueryClient
):
UseQueryResult
<
TData
,
TError
>
&
{
queryKey
:
DataTag
<
QueryKey
,
TData
,
TError
>
}
{
const
queryOptions
=
getNdviChangeValueNdviChangeValueGetQueryOptions
(
params
,
options
)
const
query
=
useQuery
(
queryOptions
,
queryClient
)
as
UseQueryResult
<
TData
,
TError
>
&
{
queryKey
:
DataTag
<
QueryKey
,
TData
,
TError
>
};
query
.
queryKey
=
queryOptions
.
queryKey
;
return
query
;
}
frontend/src/components/map/kpi-dialog.tsx
0 → 100644
View file @
048d0ef1
import
{
Dialog
,
DialogContent
,
DialogHeader
,
DialogTitle
,
DialogTrigger
,
}
from
"
@/components/ui/dialog
"
;
import
{
Button
}
from
"
@/components/ui/button
"
;
import
{
Badge
}
from
"
@/components/ui/badge
"
;
import
{
Progress
}
from
"
@/components/ui/progress
"
;
import
{
ResponsiveContainer
,
ComposedChart
,
XAxis
,
YAxis
,
Line
,
Scatter
,
}
from
"
recharts
"
;
import
{
BarChart3
}
from
"
lucide-react
"
;
import
{
cn
}
from
"
@/lib/utils
"
;
type
YearStats
=
{
year
:
number
;
median
:
number
;
p95
:
number
;
frac_gt_0_4
?:
number
;
// 0..1
frac_gt_0_6
?:
number
;
// 0..1
};
type
ChangeStats
=
{
fracLoss
:
number
;
// ≤ -0.1 (0..1)
fracGain
:
number
;
// ≥ +0.1 (0..1)
};
type
Props
=
{
left
:
YearStats
;
right
:
YearStats
;
change
?:
ChangeStats
;
title
?:
string
;
className
?:
string
;
// for the trigger button container
open
?:
boolean
;
onOpenChange
?:
(
open
:
boolean
)
=>
void
;
triggerLabel
?:
string
;
};
const
fmtNDVI
=
(
v
:
number
)
=>
(
Number
.
isFinite
(
v
)
?
v
.
toFixed
(
3
)
:
"
—
"
);
const
fmtPct
=
(
f
?:
number
)
=>
f
==
null
||
!
Number
.
isFinite
(
f
)
?
"
—
"
:
`
${(
f
*
100
).
toFixed
(
1
)}
%`
;
// tiny slopegraph
function
SlopeMini
({
a
,
b
,
domain
,
width
=
180
,
height
=
26
,
}:
{
a
:
number
;
b
:
number
;
domain
?:
[
number
,
number
];
width
?:
number
;
height
?:
number
;
})
{
const
both
=
[
{
x
:
"
A
"
,
v
:
a
},
{
x
:
"
B
"
,
v
:
b
},
];
const
up
=
b
>
a
;
const
lineColor
=
up
?
"
#22c55e
"
/* emerald-500 */
:
"
#ef4444
"
/* rose-500 */
;
return
(
<
ResponsiveContainer
width
=
{
width
}
height
=
{
height
}
>
<
ComposedChart
data
=
{
both
}
margin
=
{
{
top
:
6
,
bottom
:
6
,
left
:
6
,
right
:
6
}
}
>
<
XAxis
dataKey
=
"x"
type
=
"category"
hide
/>
<
YAxis
type
=
"number"
domain
=
{
domain
??
[
Math
.
min
(
a
,
b
),
Math
.
max
(
a
,
b
)
+
1
e
-
6
]
}
hide
/>
<
Line
data
=
{
both
}
dataKey
=
"v"
type
=
"linear"
stroke
=
{
lineColor
}
strokeWidth
=
{
2
}
dot
=
{
false
}
/>
<
Scatter
data
=
{
[
both
[
0
]]
}
dataKey
=
"v"
fill
=
"#9ca3af"
/>
{
/* left year */
}
<
Scatter
data
=
{
[
both
[
1
]]
}
dataKey
=
"v"
fill
=
"#3b82f6"
/>
{
/* right year */
}
</
ComposedChart
>
</
ResponsiveContainer
>
);
}
function
Row
({
label
,
a
,
b
,
fmt
,
domain
,
}:
{
label
:
string
;
a
:
number
;
b
:
number
;
fmt
:
(
v
:
number
)
=>
string
;
domain
?:
[
number
,
number
];
})
{
return
(
<
div
className
=
"grid grid-cols-[1fr_auto] items-center gap-2"
>
<
div
className
=
"text-sm text-muted-foreground"
>
{
label
}
</
div
>
<
div
className
=
"flex items-center gap-2"
>
<
span
className
=
"text-xs tabular-nums text-muted-foreground"
>
{
fmt
(
a
)
}
</
span
>
<
SlopeMini
a
=
{
a
}
b
=
{
b
}
domain
=
{
domain
}
/>
<
span
className
=
{
cn
(
"
text-xs font-medium tabular-nums
"
,
b
-
a
>=
0
?
"
text-emerald-600
"
:
"
text-rose-600
"
)
}
title
=
"Delta (right - left)"
>
{
(
b
-
a
>=
0
?
"
+
"
:
""
)
+
fmt
(
b
-
a
)
}
</
span
>
</
div
>
</
div
>
);
}
export
default
function
KpiDialog
({
left
,
right
,
change
,
title
=
"
NDVI comparison
"
,
className
,
open
,
onOpenChange
,
triggerLabel
=
"
KPIs
"
,
}:
Props
)
{
const
ndviDomain
:
[
number
,
number
]
=
[
-
0.2
,
0.8
];
// adjust if needed
return
(
<
Dialog
open
=
{
open
}
onOpenChange
=
{
onOpenChange
}
>
<
div
className
=
{
cn
(
"
flex justify-end
"
,
className
)
}
>
<
DialogTrigger
asChild
>
<
Button
size
=
"icon"
variant
=
"secondary"
className
=
"rounded-full shadow"
>
<
BarChart3
className
=
"h-4 w-4"
/>
<
span
className
=
"sr-only"
>
{
triggerLabel
}
</
span
>
</
Button
>
</
DialogTrigger
>
</
div
>
<
DialogContent
className
=
"sm:max-w-[420px] w-[min(92vw,420px)]"
>
<
DialogHeader
>
<
DialogTitle
className
=
"flex items-center justify-between"
>
<
span
>
{
title
}
</
span
>
<
span
className
=
"flex items-center gap-3"
>
<
span
className
=
"flex items-center gap-1 text-xs text-muted-foreground"
>
<
span
className
=
"inline-block h-2.5 w-2.5 rounded-full bg-gray-400"
/>
{
left
.
year
}
</
span
>
<
span
className
=
"flex items-center gap-1 text-xs"
>
<
span
className
=
"inline-block h-2.5 w-2.5 rounded-full bg-blue-500"
/>
{
right
.
year
}
</
span
>
</
span
>
</
DialogTitle
>
</
DialogHeader
>
<
div
className
=
"space-y-3 pt-1"
>
<
Row
label
=
"Median NDVI"
a
=
{
left
.
median
}
b
=
{
right
.
median
}
fmt
=
{
fmtNDVI
}
domain
=
{
ndviDomain
}
/>
<
Row
label
=
"p95 NDVI"
a
=
{
left
.
p95
}
b
=
{
right
.
p95
}
fmt
=
{
fmtNDVI
}
domain
=
{
ndviDomain
}
/>
<
Row
label
=
"% area > 0.4"
a
=
{
left
.
frac_gt_0_4
??
NaN
}
b
=
{
right
.
frac_gt_0_4
??
NaN
}
fmt
=
{
(
v
)
=>
fmtPct
(
v
)
}
domain
=
{
[
0
,
1
]
}
/>
<
Row
label
=
"% area > 0.6"
a
=
{
left
.
frac_gt_0_6
??
NaN
}
b
=
{
right
.
frac_gt_0_6
??
NaN
}
fmt
=
{
(
v
)
=>
fmtPct
(
v
)
}
domain
=
{
[
0
,
1
]
}
/>
<
div
className
=
"pt-2 flex items-center justify-between"
>
<
div
className
=
"text-sm text-muted-foreground"
>
Δ strong change
</
div
>
<
div
className
=
"flex gap-2"
>
<
Badge
className
=
"bg-rose-50 text-rose-600 border-rose-200"
variant
=
"secondary"
>
Loss
{
fmtPct
(
change
?.
fracLoss
)
}
</
Badge
>
<
Badge
className
=
"bg-emerald-50 text-emerald-600 border-emerald-200"
variant
=
"secondary"
>
Gain
{
fmtPct
(
change
?.
fracGain
)
}
</
Badge
>
</
div
>
</
div
>
<
div
className
=
"space-y-2"
>
<
div
className
=
"text-xs text-muted-foreground"
>
Loss ≤ −0.1
</
div
>
<
Progress
value
=
{
Math
.
max
(
0
,
Math
.
min
(
100
,
(
change
?.
fracLoss
??
0
)
*
100
))
}
className
=
"h-2 [&>div]:bg-rose-500"
/>
<
div
className
=
"text-xs text-muted-foreground pt-2"
>
Gain ≥ +0.1
</
div
>
<
Progress
value
=
{
Math
.
max
(
0
,
Math
.
min
(
100
,
(
change
?.
fracGain
??
0
)
*
100
))
}
className
=
"h-2 [&>div]:bg-emerald-500"
/>
</
div
>
</
div
>
</
DialogContent
>
</
Dialog
>
);
}
frontend/src/components/map/split-map.tsx
View file @
048d0ef1
...
...
@@ -70,13 +70,10 @@ function SplitMap({
{
/* Bottom-centered selector (robust centering, like slider) */
}
<
div
className
=
"absolute inset-x-0 bottom-5 z-50 flex
justify-center
pointer-events-none pb-[env(safe-area-inset-bottom)]"
>
<
div
className
=
"pointer-events-auto w-[
9
0%] max-w-sm"
>
className
=
"absolute inset-x-0
opacity-80
bottom-5 z-50 flex pointer-events-none pb-[env(safe-area-inset-bottom)]"
>
<
div
className
=
"pointer-events-auto w-[
2
0%] max-w-sm"
>
<
Card
className
=
"bg-white/90 backdrop-blur"
>
<
CardContent
>
<
Label
htmlFor
=
"leftYear"
className
=
"text-xs text-muted-foreground mb-1 block"
>
Left year
</
Label
>
<
Select
value
=
{
String
(
leftYear
)
}
onValueChange
=
{
(
v
)
=>
setLeftYear
(
Number
(
v
))
}
>
<
SelectTrigger
id
=
"leftYear"
className
=
"h-9"
>
<
SelectValue
placeholder
=
"Select year"
/>
...
...
@@ -108,13 +105,10 @@ function SplitMap({
{
/* Bottom-centered selector */
}
<
div
className
=
"absolute inset-x-0 bottom-5 z-50 flex
justify-center
pointer-events-none pb-[env(safe-area-inset-bottom)]"
>
<
div
className
=
"pointer-events-auto w-[
9
0%] max-w-sm"
>
className
=
"absolute inset-x-0
opacity-80
bottom-5 z-50 flex pointer-events-none pb-[env(safe-area-inset-bottom)]"
>
<
div
className
=
"pointer-events-auto w-[
2
0%] max-w-sm"
>
<
Card
className
=
"bg-white/90 backdrop-blur"
>
<
CardContent
>
<
Label
htmlFor
=
"rightYear"
className
=
"text-xs text-muted-foreground mb-1 block"
>
Right year
</
Label
>
<
Select
value
=
{
String
(
rightYear
)
}
onValueChange
=
{
(
v
)
=>
setRightYear
(
Number
(
v
))
}
>
<
SelectTrigger
id
=
"rightYear"
className
=
"h-9"
>
<
SelectValue
placeholder
=
"Select year"
/>
...
...
frontend/src/components/ui/badge.tsx
0 → 100644
View file @
048d0ef1
import
*
as
React
from
"
react
"
import
{
Slot
}
from
"
@radix-ui/react-slot
"
import
{
cva
,
type
VariantProps
}
from
"
class-variance-authority
"
import
{
cn
}
from
"
@/lib/utils
"
const
badgeVariants
=
cva
(
"
inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden
"
,
{
variants
:
{
variant
:
{
default
:
"
border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90
"
,
secondary
:
"
border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90
"
,
destructive
:
"
border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60
"
,
outline
:
"
text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground
"
,
},
},
defaultVariants
:
{
variant
:
"
default
"
,
},
}
)
function
Badge
({
className
,
variant
,
asChild
=
false
,
...
props
}:
React
.
ComponentProps
<
"
span
"
>
&
VariantProps
<
typeof
badgeVariants
>
&
{
asChild
?:
boolean
})
{
const
Comp
=
asChild
?
Slot
:
"
span
"
return
(
<
Comp
data-slot
=
"badge"
className
=
{
cn
(
badgeVariants
({
variant
}),
className
)
}
{
...
props
}
/>
)
}
export
{
Badge
,
badgeVariants
}
frontend/src/components/ui/dialog.tsx
0 → 100644
View file @
048d0ef1
import
*
as
React
from
"
react
"
import
*
as
DialogPrimitive
from
"
@radix-ui/react-dialog
"
import
{
XIcon
}
from
"
lucide-react
"
import
{
cn
}
from
"
@/lib/utils
"
function
Dialog
({
...
props
}:
React
.
ComponentProps
<
typeof
DialogPrimitive
.
Root
>
)
{
return
<
DialogPrimitive
.
Root
data-slot
=
"dialog"
{
...
props
}
/>
}
function
DialogTrigger
({
...
props
}:
React
.
ComponentProps
<
typeof
DialogPrimitive
.
Trigger
>
)
{
return
<
DialogPrimitive
.
Trigger
data-slot
=
"dialog-trigger"
{
...
props
}
/>
}
function
DialogPortal
({
...
props
}:
React
.
ComponentProps
<
typeof
DialogPrimitive
.
Portal
>
)
{
return
<
DialogPrimitive
.
Portal
data-slot
=
"dialog-portal"
{
...
props
}
/>
}
function
DialogClose
({
...
props
}:
React
.
ComponentProps
<
typeof
DialogPrimitive
.
Close
>
)
{
return
<
DialogPrimitive
.
Close
data-slot
=
"dialog-close"
{
...
props
}
/>
}
function
DialogOverlay
({
className
,
...
props
}:
React
.
ComponentProps
<
typeof
DialogPrimitive
.
Overlay
>
)
{
return
(
<
DialogPrimitive
.
Overlay
data-slot
=
"dialog-overlay"
className
=
{
cn
(
"
data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50
"
,
className
)
}
{
...
props
}
/>
)
}
function
DialogContent
({
className
,
children
,
showCloseButton
=
true
,
...
props
}:
React
.
ComponentProps
<
typeof
DialogPrimitive
.
Content
>
&
{
showCloseButton
?:
boolean
})
{
return
(
<
DialogPortal
data-slot
=
"dialog-portal"
>
<
DialogOverlay
/>
<
DialogPrimitive
.
Content
data-slot
=
"dialog-content"
className
=
{
cn
(
"
bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg
"
,
className
)
}
{
...
props
}
>
{
children
}
{
showCloseButton
&&
(
<
DialogPrimitive
.
Close
data-slot
=
"dialog-close"
className
=
"ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4"
>
<
XIcon
/>
<
span
className
=
"sr-only"
>
Close
</
span
>
</
DialogPrimitive
.
Close
>
)
}
</
DialogPrimitive
.
Content
>
</
DialogPortal
>
)
}
function
DialogHeader
({
className
,
...
props
}:
React
.
ComponentProps
<
"
div
"
>
)
{
return
(
<
div
data-slot
=
"dialog-header"
className
=
{
cn
(
"
flex flex-col gap-2 text-center sm:text-left
"
,
className
)
}
{
...
props
}
/>
)
}
function
DialogFooter
({
className
,
...
props
}:
React
.
ComponentProps
<
"
div
"
>
)
{
return
(
<
div
data-slot
=
"dialog-footer"
className
=
{
cn
(
"
flex flex-col-reverse gap-2 sm:flex-row sm:justify-end
"
,
className
)
}
{
...
props
}
/>
)
}
function
DialogTitle
({
className
,
...
props
}:
React
.
ComponentProps
<
typeof
DialogPrimitive
.
Title
>
)
{
return
(
<
DialogPrimitive
.
Title
data-slot
=
"dialog-title"
className
=
{
cn
(
"
text-lg leading-none font-semibold
"
,
className
)
}
{
...
props
}
/>
)
}
function
DialogDescription
({
className
,
...
props
}:
React
.
ComponentProps
<
typeof
DialogPrimitive
.
Description
>
)
{
return
(
<
DialogPrimitive
.
Description
data-slot
=
"dialog-description"
className
=
{
cn
(
"
text-muted-foreground text-sm
"
,
className
)
}
{
...
props
}
/>
)
}
export
{
Dialog
,
DialogClose
,
DialogContent
,
DialogDescription
,
DialogFooter
,
DialogHeader
,
DialogOverlay
,
DialogPortal
,
DialogTitle
,
DialogTrigger
,
}
frontend/src/components/ui/progress.tsx
0 → 100644
View file @
048d0ef1
import
*
as
React
from
"
react
"
import
*
as
ProgressPrimitive
from
"
@radix-ui/react-progress
"
import
{
cn
}
from
"
@/lib/utils
"
function
Progress
({
className
,
value
,
...
props
}:
React
.
ComponentProps
<
typeof
ProgressPrimitive
.
Root
>
)
{
return
(
<
ProgressPrimitive
.
Root
data-slot
=
"progress"
className
=
{
cn
(
"
bg-primary/20 relative h-2 w-full overflow-hidden rounded-full
"
,
className
)
}
{
...
props
}
>
<
ProgressPrimitive
.
Indicator
data-slot
=
"progress-indicator"
className
=
"bg-primary h-full w-full flex-1 transition-all"
style
=
{
{
transform
:
`translateX(-
${
100
-
(
value
||
0
)}
%)`
}
}
/>
</
ProgressPrimitive
.
Root
>
)
}
export
{
Progress
}
Write
Preview
Supports
Markdown
0%
Try again
or
attach a new file
.
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment