From 91140779bc74a834d2a5d4350736fdb31fe8230f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mert=20Bora=20=C4=B0nevi?= Date: Tue, 21 Apr 2026 15:06:19 +0200 Subject: [PATCH 1/4] Update nitrogen and organic matter balance charts to Recharts v3 --- .../blocks/balance/nitrogen-chart.tsx | 3 +- .../blocks/balance/organic-matter-chart.tsx | 4 +- fdm-app/app/components/ui/card.tsx | 4 +- fdm-app/app/components/ui/chart.tsx | 246 +++++++++--------- fdm-app/package.json | 2 +- pnpm-lock.yaml | 181 ++++++++----- 6 files changed, 246 insertions(+), 194 deletions(-) diff --git a/fdm-app/app/components/blocks/balance/nitrogen-chart.tsx b/fdm-app/app/components/blocks/balance/nitrogen-chart.tsx index fc9ccb9f4..ef8987ef8 100644 --- a/fdm-app/app/components/blocks/balance/nitrogen-chart.tsx +++ b/fdm-app/app/components/blocks/balance/nitrogen-chart.tsx @@ -52,6 +52,7 @@ function buildChartDataAndLegend({ ? balanceData.emission.nitrate : balanceData.emission.nitrate.total const chartData: Record = { + name: "Balans" as unknown as undefined, // Needed for chart to render at y-axis points called "Balans" - see the JSX below deposition: Math.abs( type === "farm" ? balanceData.supply.deposition @@ -539,7 +540,7 @@ export function NitrogenBalanceChart( > {!nestLabel ? tooltipLabel : null}
- {payload.map((item, index) => { - const key = `${nameKey || item.name || item.dataKey || "value"}` - const itemConfig = getPayloadConfigFromPayload( - config, - item, - key, - ) - const indicatorColor = - color || item.payload.fill || item.color - - return ( -
svg]:h-2.5 [&>svg]:w-2.5 [&>svg]:text-muted-foreground", - indicator === "dot" && "items-center", - )} - > - {formatter && - item?.value !== undefined && - item.name ? ( - formatter( - item.value, - item.name, - item, - index, - item.payload, - ) - ) : ( - <> - {itemConfig?.icon ? ( - - ) : ( - !hideIndicator && ( -
item.type !== "none") + .map((item, index) => { + const key = `${nameKey || item.name || item.dataKey || "value"}` + const itemConfig = getPayloadConfigFromPayload( + config, + item, + key, + ) + const indicatorColor = + color || item.payload.fill || item.color + + return ( +
svg]:h-2.5 [&>svg]:w-2.5 [&>svg]:text-muted-foreground", + indicator === "dot" && "items-center", + )} + > + {formatter && + item?.value !== undefined && + item.name ? ( + formatter( + item.value, + item.name, + item, + index, + item.payload, + ) + ) : ( + <> + {itemConfig?.icon ? ( + + ) : ( + !hideIndicator && ( +
- ) - )} -
+ ) )} - > -
- {nestLabel - ? tooltipLabel - : null} - - {itemConfig?.label || - item.name} - +
+
+ {nestLabel + ? tooltipLabel + : null} + + {itemConfig?.label || + item.name} + +
+ {item.value && ( + + {item.value.toLocaleString()} + + )}
- {item.value && ( - - {item.value.toLocaleString()} - - )} -
- - )} -
- ) - })} + + )} +
+ ) + })}
) @@ -319,35 +321,37 @@ const ChartLegendContent = React.forwardRef< className, )} > - {payload.map((item) => { - const key = `${nameKey || item.dataKey || "value"}` - const itemConfig = getPayloadConfigFromPayload( - config, - item, - key, - ) + {payload + .filter((item) => item.type !== "none") + .map((item) => { + const key = `${nameKey || item.dataKey || "value"}` + const itemConfig = getPayloadConfigFromPayload( + config, + item, + key, + ) - return ( -
svg]:h-3 [&>svg]:w-3 [&>svg]:text-muted-foreground", - )} - > - {itemConfig?.icon && !hideIcon ? ( - - ) : ( -
- )} - {itemConfig?.label} -
- ) - })} + return ( +
svg]:h-3 [&>svg]:w-3 [&>svg]:text-muted-foreground", + )} + > + {itemConfig?.icon && !hideIcon ? ( + + ) : ( +
+ )} + {itemConfig?.label} +
+ ) + })}
) }, @@ -395,9 +399,9 @@ function getPayloadConfigFromPayload( export { ChartContainer, + ChartTooltip, + ChartTooltipContent, ChartLegend, ChartLegendContent, ChartStyle, - ChartTooltip, - ChartTooltipContent, } diff --git a/fdm-app/package.json b/fdm-app/package.json index d847f2a97..c8b6c7956 100644 --- a/fdm-app/package.json +++ b/fdm-app/package.json @@ -75,7 +75,7 @@ "react-markdown": "^10.1.0", "react-router": "^7.14.0", "react-router-dom": "^7.14.0", - "recharts": "^2.15.4", + "recharts": "^3.8.1", "remark-gfm": "^4.0.1", "remix-hook-form": "7.1.1", "remix-toast": "^4.0.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c0706a7ed..f9bdc097f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -285,8 +285,8 @@ importers: specifier: ^7.14.0 version: 7.14.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) recharts: - specifier: ^2.15.4 - version: 2.15.4(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + specifier: ^3.8.1 + version: 3.8.1(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react-is@18.3.1)(react@19.2.4)(redux@5.0.1) remark-gfm: specifier: ^4.0.1 version: 4.0.1 @@ -319,7 +319,7 @@ importers: version: 4.3.6 zustand: specifier: ^5.0.12 - version: 5.0.12(@types/react@19.2.14)(react@19.2.4)(use-sync-external-store@1.6.0(react@19.2.4)) + version: 5.0.12(@types/react@19.2.14)(immer@11.1.4)(react@19.2.4)(use-sync-external-store@1.6.0(react@19.2.4)) devDependencies: '@dotenvx/dotenvx': specifier: 'catalog:' @@ -4524,6 +4524,17 @@ packages: peerDependencies: react-router: 7.14.0 + '@reduxjs/toolkit@2.11.2': + resolution: {integrity: sha512-Kd6kAHTA6/nUpp8mySPqj3en3dm0tdMIgbttnQ1xFMVpufoj+ADi8pXLBsd4xzTRHQa7t/Jv8W5UnCuW4kuWMQ==} + 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 + '@remix-run/file-storage@0.13.3': resolution: {integrity: sha512-HBDz9RRsFRvI6EoeasklxH/NleGy0QZBXBcA4gQBW8ueucop21TQI4wvGlhZmXcnJ3nP4RkhdF2Gff2/HD5eiA==} @@ -5780,6 +5791,9 @@ packages: '@types/unist@3.0.3': resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} + '@types/use-sync-external-store@0.0.6': + resolution: {integrity: sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==} + '@types/validator@13.15.10': resolution: {integrity: sha512-T8L6i7wCuyoK8A/ZeLYt1+q0ty3Zb9+qbSSvrIVitzT3YjZqkTZ40IbRsPanlB4h1QB3JVL1SYCdR6ngtFYcuA==} @@ -7113,9 +7127,6 @@ packages: dom-converter@0.2.0: resolution: {integrity: sha512-gd3ypIPfOMr9h5jIKq8E3sHOTCjeirnl0WK5ZdS1AW0Odt0b1PaWaHdJ4Qk4klv+YB9aJBS7mESXjFoDQPu6DA==} - dom-helpers@5.2.1: - resolution: {integrity: sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==} - dom-serializer@1.4.1: resolution: {integrity: sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==} @@ -7397,6 +7408,9 @@ packages: resolution: {integrity: sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==} engines: {node: '>= 0.4'} + es-toolkit@1.45.1: + resolution: {integrity: sha512-/jhoOj/Fx+A+IIyDNOvO3TItGmlMKhtX8ISAHKE90c4b/k1tqaqEZ+uUqfpU8DMnW5cgNJv606zS55jGvza0Xw==} + esast-util-from-estree@2.0.0: resolution: {integrity: sha512-4CyanoAudUSBAn5K13H4JhsMH6L9ZP7XbLVe/dKybkxMO7eDyLsT8UHl9TRNrU2Gr9nz+FovfSIjuXWJ81uVwQ==} @@ -7513,6 +7527,9 @@ packages: eventemitter3@4.0.7: resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==} + eventemitter3@5.0.4: + resolution: {integrity: sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==} + events@3.3.0: resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} engines: {node: '>=0.8.x'} @@ -7575,10 +7592,6 @@ packages: fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} - fast-equals@5.4.0: - resolution: {integrity: sha512-jt2DW/aNFNwke7AUd+Z+e6pz39KO5rzdbbFCg2sGafS4mk13MI7Z8O5z9cADNn5lhGODIgLwug6TZO2ctf7kcw==} - engines: {node: '>=6.0.0'} - fast-glob@3.3.3: resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} engines: {node: '>=8.6.0'} @@ -8247,6 +8260,12 @@ packages: engines: {node: '>=16.x'} hasBin: true + immer@10.2.0: + resolution: {integrity: sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw==} + + immer@11.1.4: + resolution: {integrity: sha512-XREFCPo6ksxVzP4E0ekD5aMdf8WMwmdNaz6vuvxgI40UaEiu6q3p8X52aU6GdyvLY3XXX/8R7JOTXStz/nBbRw==} + import-fresh@3.3.1: resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} engines: {node: '>=6'} @@ -10599,6 +10618,18 @@ packages: '@types/react': '>=18' react: '>=18' + react-redux@9.2.0: + resolution: {integrity: sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==} + peerDependencies: + '@types/react': ^18.2.25 || ^19 + react: ^18.0 || ^19 + redux: ^5.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + redux: + optional: true + react-refresh@0.14.2: resolution: {integrity: sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==} engines: {node: '>=0.10.0'} @@ -10656,12 +10687,6 @@ packages: react-dom: optional: true - react-smooth@4.0.4: - resolution: {integrity: sha512-gnGKTpYwqL0Iii09gHobNolvX4Kiq4PKx6eWBCYYix+8cdw+cGo3do906l1NBPKkSWx1DghC1dlWG9L2uGd61Q==} - 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-style-singleton@2.2.3: resolution: {integrity: sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==} engines: {node: '>=10'} @@ -10672,12 +10697,6 @@ 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.4: resolution: {integrity: sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==} engines: {node: '>=0.10.0'} @@ -10705,15 +10724,13 @@ packages: resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} engines: {node: '>= 14.18.0'} - recharts-scale@0.4.5: - resolution: {integrity: sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w==} - - recharts@2.15.4: - resolution: {integrity: sha512-UT/q6fwS3c1dHbXv2uFgYJ9BMFHu3fwnd7AYZaEQhXuYQ4hgsxLvsUXzGdKeZrW5xopzDCvuA2N41WJ88I7zIw==} - engines: {node: '>=14'} + recharts@3.8.1: + resolution: {integrity: sha512-mwzmO1s9sFL0TduUpwndxCUNoXsBw3u3E/0+A+cLcrSfQitSG62L32N69GhqUrrT5qKcAE3pCGVINC6pqkBBQg==} + engines: {node: '>=18'} peerDependencies: - react: ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + 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 rechoir@0.8.0: resolution: {integrity: sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ==} @@ -10733,6 +10750,14 @@ packages: recma-stringify@1.0.0: resolution: {integrity: sha512-cjwII1MdIIVloKvC9ErQ+OgAtwHBmcZ0Bg4ciz78FtbT8In39aAYbaA7zvxQ61xVMSPE8WxhLwLbhif4Js2C+g==} + redux-thunk@3.1.0: + resolution: {integrity: sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==} + peerDependencies: + redux: ^5.0.0 + + redux@5.0.1: + resolution: {integrity: sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==} + reference-spec-reader@0.2.0: resolution: {integrity: sha512-q0mfCi5yZSSHXpCyxjgQeaORq3tvDsxDyzaadA/5+AbAUwRyRuuTh0aRQuE/vAOt/qzzxidJ5iDeu1cLHaNBlQ==} @@ -10882,6 +10907,9 @@ packages: requires-port@1.0.0: resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==} + reselect@5.1.1: + resolution: {integrity: sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==} + resolve-alpn@1.2.1: resolution: {integrity: sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==} @@ -12004,8 +12032,8 @@ packages: vfile@6.0.3: resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==} - victory-vendor@36.9.2: - resolution: {integrity: sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ==} + victory-vendor@37.3.6: + resolution: {integrity: sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ==} vite-compatible-readable-stream@3.6.1: resolution: {integrity: sha512-t20zYkrSf868+j/p31cRIGN28Phrjm3nRSLR2fyc2tiWi4cZGVdv68yNlwnIINTkMTmPoMiSlc0OadaO7DXZaQ==} @@ -17698,6 +17726,18 @@ snapshots: - supports-color - typescript + '@reduxjs/toolkit@2.11.2(react-redux@9.2.0(@types/react@19.2.14)(react@19.2.4)(redux@5.0.1))(react@19.2.4)': + dependencies: + '@standard-schema/spec': 1.1.0 + '@standard-schema/utils': 0.3.0 + immer: 11.1.4 + redux: 5.0.1 + redux-thunk: 3.1.0(redux@5.0.1) + reselect: 5.1.1 + optionalDependencies: + react: 19.2.4 + react-redux: 9.2.0(@types/react@19.2.14)(react@19.2.4)(redux@5.0.1) + '@remix-run/file-storage@0.13.3': dependencies: '@remix-run/fs': 0.4.2 @@ -18901,6 +18941,8 @@ snapshots: '@types/unist@3.0.3': {} + '@types/use-sync-external-store@0.0.6': {} + '@types/validator@13.15.10': {} '@types/viewport-mercator-project@6.1.6': @@ -20333,11 +20375,6 @@ snapshots: dependencies: utila: 0.4.0 - dom-helpers@5.2.1: - dependencies: - '@babel/runtime': 7.29.2 - csstype: 3.2.3 - dom-serializer@1.4.1: dependencies: domelementtype: 2.3.0 @@ -20597,6 +20634,8 @@ snapshots: is-date-object: 1.1.0 is-symbol: 1.1.1 + es-toolkit@1.45.1: {} + esast-util-from-estree@2.0.0: dependencies: '@types/estree-jsx': 1.0.5 @@ -20775,6 +20814,8 @@ snapshots: eventemitter3@4.0.7: {} + eventemitter3@5.0.4: {} + events@3.3.0: {} eventsource-parser@3.0.6: {} @@ -20892,8 +20933,6 @@ snapshots: fast-deep-equal@3.1.3: {} - fast-equals@5.4.0: {} - fast-glob@3.3.3: dependencies: '@nodelib/fs.stat': 2.0.5 @@ -21771,6 +21810,10 @@ snapshots: image-size@2.0.2: {} + immer@10.2.0: {} + + immer@11.1.4: {} + import-fresh@3.3.1: dependencies: parent-module: 1.0.1 @@ -24602,6 +24645,15 @@ snapshots: transitivePeerDependencies: - supports-color + react-redux@9.2.0(@types/react@19.2.14)(react@19.2.4)(redux@5.0.1): + dependencies: + '@types/use-sync-external-store': 0.0.6 + react: 19.2.4 + use-sync-external-store: 1.6.0(react@19.2.4) + optionalDependencies: + '@types/react': 19.2.14 + redux: 5.0.1 + react-refresh@0.14.2: {} react-remove-scroll-bar@2.3.8(@types/react@19.2.14)(react@19.2.4): @@ -24667,14 +24719,6 @@ snapshots: optionalDependencies: react-dom: 19.2.4(react@19.2.4) - react-smooth@4.0.4(react-dom@19.2.4(react@19.2.4))(react@19.2.4): - dependencies: - fast-equals: 5.4.0 - prop-types: 15.8.1 - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) - react-transition-group: 4.4.5(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - react-style-singleton@2.2.3(@types/react@19.2.14)(react@19.2.4): dependencies: get-nonce: 1.0.1 @@ -24683,15 +24727,6 @@ snapshots: optionalDependencies: '@types/react': 19.2.14 - react-transition-group@4.4.5(react-dom@19.2.4(react@19.2.4))(react@19.2.4): - dependencies: - '@babel/runtime': 7.29.2 - dom-helpers: 5.2.1 - loose-envify: 1.4.0 - prop-types: 15.8.1 - react: 19.2.4 - react-dom: 19.2.4(react@19.2.4) - react@19.2.4: {} read-yaml-file@1.1.0: @@ -24731,22 +24766,25 @@ snapshots: readdirp@4.1.2: {} - recharts-scale@0.4.5: - dependencies: - decimal.js-light: 2.5.1 - - recharts@2.15.4(react-dom@19.2.4(react@19.2.4))(react@19.2.4): + recharts@3.8.1(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react-is@18.3.1)(react@19.2.4)(redux@5.0.1): dependencies: + '@reduxjs/toolkit': 2.11.2(react-redux@9.2.0(@types/react@19.2.14)(react@19.2.4)(redux@5.0.1))(react@19.2.4) clsx: 2.1.1 - eventemitter3: 4.0.7 - lodash: 4.18.1 + decimal.js-light: 2.5.1 + es-toolkit: 1.45.1 + eventemitter3: 5.0.4 + immer: 10.2.0 react: 19.2.4 react-dom: 19.2.4(react@19.2.4) react-is: 18.3.1 - react-smooth: 4.0.4(react-dom@19.2.4(react@19.2.4))(react@19.2.4) - recharts-scale: 0.4.5 + react-redux: 9.2.0(@types/react@19.2.14)(react@19.2.4)(redux@5.0.1) + reselect: 5.1.1 tiny-invariant: 1.3.3 - victory-vendor: 36.9.2 + use-sync-external-store: 1.6.0(react@19.2.4) + victory-vendor: 37.3.6 + transitivePeerDependencies: + - '@types/react' + - redux rechoir@0.8.0: dependencies: @@ -24781,6 +24819,12 @@ snapshots: unified: 11.0.5 vfile: 6.0.3 + redux-thunk@3.1.0(redux@5.0.1): + dependencies: + redux: 5.0.1 + + redux@5.0.1: {} + reference-spec-reader@0.2.0: optional: true @@ -24963,6 +25007,8 @@ snapshots: requires-port@1.0.0: {} + reselect@5.1.1: {} + resolve-alpn@1.2.1: {} resolve-from@4.0.0: {} @@ -26235,7 +26281,7 @@ snapshots: '@types/unist': 3.0.3 vfile-message: 4.0.3 - victory-vendor@36.9.2: + victory-vendor@37.3.6: dependencies: '@types/d3-array': 3.2.2 '@types/d3-ease': 3.0.2 @@ -26787,9 +26833,10 @@ snapshots: zstddec@0.2.0: {} - zustand@5.0.12(@types/react@19.2.14)(react@19.2.4)(use-sync-external-store@1.6.0(react@19.2.4)): + zustand@5.0.12(@types/react@19.2.14)(immer@11.1.4)(react@19.2.4)(use-sync-external-store@1.6.0(react@19.2.4)): optionalDependencies: '@types/react': 19.2.14 + immer: 11.1.4 react: 19.2.4 use-sync-external-store: 1.6.0(react@19.2.4) From 1a06acfbb44286663d865dd08f2edb1f714a7893 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mert=20Bora=20=C4=B0nevi?= Date: Wed, 22 Apr 2026 11:57:18 +0200 Subject: [PATCH 2/4] Use the ChartTooltip shared prop to make the tooltip show data specific to a single bar --- .../blocks/balance/nitrogen-chart.tsx | 144 ++++++++---------- 1 file changed, 66 insertions(+), 78 deletions(-) diff --git a/fdm-app/app/components/blocks/balance/nitrogen-chart.tsx b/fdm-app/app/components/blocks/balance/nitrogen-chart.tsx index ef8987ef8..f0920152d 100644 --- a/fdm-app/app/components/blocks/balance/nitrogen-chart.tsx +++ b/fdm-app/app/components/blocks/balance/nitrogen-chart.tsx @@ -6,7 +6,7 @@ import type { import { format } from "date-fns/format" import { useId, useMemo, useState } from "react" import { nl } from "react-day-picker/locale" -import { Bar, BarChart, XAxis, YAxis } from "recharts" +import { Bar, BarChart, XAxis, YAxis, ZIndexLayer } from "recharts" import { cn } from "@/app/lib/utils" import { getCultivationColor } from "~/components/custom/cultivation-colors" import { Card, CardContent, CardHeader } from "~/components/ui/card" @@ -201,7 +201,7 @@ function buildChartDataAndLegend({ fa.p_app_id === applicationResult.id, ) chartData[dataKey] = Math.abs(applicationResult.value) - ;(chartConfig as ExtendedChartConfig)[dataKey] = application + chartConfig[dataKey] = application ? { styleId: dataKeyPrefix, label: label, @@ -434,7 +434,10 @@ export function NitrogenBalanceChart( }, ) { const { type, balanceData, fieldInput } = props - const [tooltipFocus, setTooltipFocus] = useState() + const [barOutlineFocus, setBarOutlineFocus] = useState< + string | number | undefined + >() + // biome-ignore lint/correctness/useExhaustiveDependencies: each value in props is passed separately const { legend, chartData, chartConfig, supplyBar, removalBar } = useMemo( () => buildChartDataAndLegend(props), @@ -442,24 +445,6 @@ export function NitrogenBalanceChart( ) const svgId = useId() - type ChartMouseEvent = { - tooltipPayload: { dataKey: string }[] - } - - const onTooltipFocus = (e: ChartMouseEvent) => { - const dataKey = e.tooltipPayload[0].dataKey - if (tooltipFocus !== dataKey) setTooltipFocus(dataKey) - } - - const onTooltipBlur = (e: ChartMouseEvent) => { - const dataKey = e.tooltipPayload[0].dataKey - if (tooltipFocus === dataKey) setTooltipFocus(undefined) - } - - const clearTooltipFocus = () => { - setTooltipFocus(undefined) - } - const barRadius = 5 const barRadiusStart: [number, number, number, number] = [ barRadius, @@ -502,8 +487,7 @@ export function NitrogenBalanceChart( radius={pickBarRadius(i, barItem)} stackId={stackId} fill={barStyle.color} - onMouseEnter={onTooltipFocus} - onMouseLeave={onTooltipBlur} + onMouseOver={() => setBarOutlineFocus(dataKey)} /> ) }) @@ -521,15 +505,19 @@ export function NitrogenBalanceChart( radius={barRadius} stackId={stackId} fill={barStyle.color} - onMouseEnter={onTooltipFocus} - onMouseLeave={onTooltipBlur} + onMouseOver={() => setBarOutlineFocus(dataKey)} /> ) }) } return ( - + { + setBarOutlineFocus("") + }} + > value.slice(0, 3)} + tickFormatter={() => ""} /> { - if (active && tooltipFocus) { - const dataKey = - tooltipFocus as keyof typeof chartConfig - const itemConfig = chartConfig[dataKey] - - return ( - - -
-
- {itemConfig.label} -
- {itemConfig.detail?.map((detail, i) => ( - // biome-ignore lint/suspicious/noArrayIndexKey: detail is constant -
-
- {detail} -
- ))} - - -
+ shared={false} + content={({ payload }) => { + if (payload.length === 0) return null + const { name } = payload[0] + if (!name) return null + const itemConfig = chartConfig[name] + if (!itemConfig) return null + return ( + + +
+
+ {itemConfig.label} +
+ {itemConfig.detail?.map((detail, i) => ( + // biome-ignore lint/suspicious/noArrayIndexKey: detail is constant +
- {chartData[dataKey]}{" "} - {itemConfig.unit ?? "kg N / ha"} + {detail}
- - - ) - } - return null + ))} + + +
+
+ {chartData[name]}{" "} + {itemConfig.unit ?? "kg N / ha"} +
+ + + ) }} /> {renderBar("a", supplyBar)} {renderBar("b", removalBar)} - {tooltipFocus && ( - - )} + + {barOutlineFocus && ( + + )} + ) From fcec5775d7cd98f549e79beed9906197ff1096ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mert=20Bora=20=C4=B0nevi?= Date: Wed, 22 Apr 2026 15:44:54 +0200 Subject: [PATCH 3/4] Address nitpicks --- .../app/components/blocks/balance/nitrogen-chart.tsx | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/fdm-app/app/components/blocks/balance/nitrogen-chart.tsx b/fdm-app/app/components/blocks/balance/nitrogen-chart.tsx index f0920152d..fb88e489d 100644 --- a/fdm-app/app/components/blocks/balance/nitrogen-chart.tsx +++ b/fdm-app/app/components/blocks/balance/nitrogen-chart.tsx @@ -51,8 +51,8 @@ function buildChartDataAndLegend({ type === "farm" ? balanceData.emission.nitrate : balanceData.emission.nitrate.total - const chartData: Record = { - name: "Balans" as unknown as undefined, // Needed for chart to render at y-axis points called "Balans" - see the JSX below + const chartData: Record = { + name: "Balans", // Needed for chart to render at y-axis points called "Balans" - see the JSX below deposition: Math.abs( type === "farm" ? balanceData.supply.deposition @@ -538,8 +538,11 @@ export function NitrogenBalanceChart( { - if (payload.length === 0) return null - const { name } = payload[0] + const validPayloadItem = payload.find( + (item) => item && item.type !== "none", + ) + if (!validPayloadItem) return null + const { name } = validPayloadItem if (!name) return null const itemConfig = chartConfig[name] if (!itemConfig) return null From c1100b752d3cb4212ce82c0ce1e98f39a6115407 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mert=20Bora=20=C4=B0nevi?= Date: Tue, 28 Apr 2026 10:21:57 +0200 Subject: [PATCH 4/4] Add onMouseLeave handlers to chart bars again --- .../components/blocks/balance/nitrogen-chart.tsx | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/fdm-app/app/components/blocks/balance/nitrogen-chart.tsx b/fdm-app/app/components/blocks/balance/nitrogen-chart.tsx index fb88e489d..56ce74962 100644 --- a/fdm-app/app/components/blocks/balance/nitrogen-chart.tsx +++ b/fdm-app/app/components/blocks/balance/nitrogen-chart.tsx @@ -487,7 +487,12 @@ export function NitrogenBalanceChart( radius={pickBarRadius(i, barItem)} stackId={stackId} fill={barStyle.color} - onMouseOver={() => setBarOutlineFocus(dataKey)} + onMouseEnter={() => setBarOutlineFocus(dataKey)} + onMouseLeave={() => { + if (dataKey === barOutlineFocus) { + setBarOutlineFocus(undefined) + } + }} /> ) }) @@ -505,7 +510,12 @@ export function NitrogenBalanceChart( radius={barRadius} stackId={stackId} fill={barStyle.color} - onMouseOver={() => setBarOutlineFocus(dataKey)} + onMouseEnter={() => setBarOutlineFocus(dataKey)} + onMouseLeave={() => { + if (dataKey === barOutlineFocus) { + setBarOutlineFocus(undefined) + } + }} /> ) }) @@ -515,7 +525,7 @@ export function NitrogenBalanceChart( { - setBarOutlineFocus("") + setBarOutlineFocus(undefined) }} >