diff --git a/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-loads-the-budget-page-with-budgeted-amounts-1-chromium-linux.png b/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-loads-the-budget-page-with-budgeted-amounts-1-chromium-linux.png index 1e3ca8430d1..465ea69cb5f 100644 Binary files a/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-loads-the-budget-page-with-budgeted-amounts-1-chromium-linux.png and b/packages/desktop-client/e2e/mobile.test.js-snapshots/Mobile-loads-the-budget-page-with-budgeted-amounts-1-chromium-linux.png differ diff --git a/packages/desktop-client/e2e/reports.test.js-snapshots/Reports-loads-net-worth-and-cash-flow-reports-1-chromium-linux.png b/packages/desktop-client/e2e/reports.test.js-snapshots/Reports-loads-net-worth-and-cash-flow-reports-1-chromium-linux.png index 6648df99dd5..15672c025c7 100644 Binary files a/packages/desktop-client/e2e/reports.test.js-snapshots/Reports-loads-net-worth-and-cash-flow-reports-1-chromium-linux.png and b/packages/desktop-client/e2e/reports.test.js-snapshots/Reports-loads-net-worth-and-cash-flow-reports-1-chromium-linux.png differ diff --git a/packages/desktop-client/package.json b/packages/desktop-client/package.json index 0036b72eda7..e2b86fe6b52 100644 --- a/packages/desktop-client/package.json +++ b/packages/desktop-client/package.json @@ -56,6 +56,7 @@ "react-simple-pull-to-refresh": "^1.3.3", "react-spring": "^9.7.1", "react-virtualized-auto-sizer": "^1.0.2", + "recharts": "^2.8.0", "redux": "^4.0.5", "redux-thunk": "^2.3.0", "remark-gfm": "^3.0.1", diff --git a/packages/desktop-client/src/components/reports/Overview.js b/packages/desktop-client/src/components/reports/Overview.js index d2237ac4704..dbe0f6ba36c 100644 --- a/packages/desktop-client/src/components/reports/Overview.js +++ b/packages/desktop-client/src/components/reports/Overview.js @@ -38,6 +38,9 @@ function Card({ flex, to, style, children }) { height: 200, boxShadow: '0 2px 6px rgba(0, 0, 0, .15)', transition: 'box-shadow .25s', + '& .recharts-surface:hover': { + cursor: 'pointer', + }, ':hover': to && { boxShadow: '0 4px 6px rgba(0, 0, 0, .15)', }, diff --git a/packages/desktop-client/src/components/reports/graphs/NetWorthGraph.tsx b/packages/desktop-client/src/components/reports/graphs/NetWorthGraph.tsx index ae18bd1f94a..5ef62d35914 100644 --- a/packages/desktop-client/src/components/reports/graphs/NetWorthGraph.tsx +++ b/packages/desktop-client/src/components/reports/graphs/NetWorthGraph.tsx @@ -1,21 +1,20 @@ -import React, { createElement } from 'react'; +import React from 'react'; -import * as d from 'date-fns'; +import { css } from 'glamor'; import { - VictoryChart, - VictoryBar, - VictoryArea, - VictoryAxis, - VictoryVoronoiContainer, - VictoryGroup, -} from 'victory'; + AreaChart, + Area, + CartesianGrid, + XAxis, + YAxis, + Tooltip, + ResponsiveContainer, +} from 'recharts'; +import { theme } from '../../../style'; import { type CSSProperties } from '../../../style'; -import { chartTheme } from '../chart-theme'; +import AlignedText from '../../common/AlignedText'; import Container from '../Container'; -import Tooltip from '../Tooltip'; - -import { Area } from './common'; type NetWorthGraphProps = { style?: CSSProperties; @@ -25,13 +24,92 @@ type NetWorthGraphProps = { y?: [number, number]; }; }; +type PotentialNumber = number | string | undefined | null; + +const numberFormatterTooltip = (value: PotentialNumber): number | null => { + if (typeof value === 'number') { + return Math.round(value); + } + return null; // or some default value for other cases +}; + function NetWorthGraph({ style, graphData, compact, domain, }: NetWorthGraphProps) { - const Chart = compact ? VictoryGroup : VictoryChart; + const tickFormatter = tick => { + return `${Math.round(tick).toLocaleString()}`; // Formats the tick values as strings with commas + }; + + const gradientOffset = () => { + const dataMax = Math.max(...graphData.data.map(i => i.y)); + const dataMin = Math.min(...graphData.data.map(i => i.y)); + + if (dataMax <= 0) { + return 0; + } + if (dataMin >= 0) { + return 1; + } + + return dataMax / (dataMax - dataMin); + }; + + const off = gradientOffset(); + + type PayloadItem = { + payload: { + date: string; + assets: number | string; + debt: number | string; + networth: number | string; + change: number | string; + }; + }; + + type CustomTooltipProps = { + active?: boolean; + payload?: PayloadItem[]; + label?: string; + }; + + const CustomTooltip = ({ active, payload, label }: CustomTooltipProps) => { + if (active && payload && payload.length) { + return ( +
+
+
+ {payload[0].payload.date} +
+
+ + + {payload[0].payload.networth}} + /> + +
+
+
+ ); + } + }; return ( {(width, height, portalHost) => graphData && ( - - } - padding={ - compact && { - top: 0, - bottom: 0, - left: 0, - right: 0, - } - } - > - - {createElement( - // @ts-expect-error defaultProps mismatch causing issue - graphData.data.length === 1 ? VictoryBar : VictoryArea, - { - data: graphData.data, - labelComponent: , - labels: x => x.premadeLabel, - style: { - data: - graphData.data.length === 1 - ? { width: 50 } - : { - clipPath: 'url(#positive)', - fill: 'url(#positive-gradient)', - }, - }, - }, - )} - {graphData.data.length > 1 && ( - +
+ {!compact &&
} + - )} - {/* Somehow the path `d` attributes are stripped from second - `` above if this is removed. I’m just as - confused as you are! */} - - {!compact && ( - d.format(x, "MMM ''yy")} - tickValues={graphData.data.map(item => item.x)} - tickCount={Math.min(width / 220, graphData.data.length)} - offsetY={50} - /> - )} - {!compact && ( - - )} - + margin={{ top: 0, right: 0, left: 0, bottom: 0 }} + > + {compact ? null : ( + + )} + {compact ? null : } + {compact ? null : ( + + )} + } + formatter={numberFormatterTooltip} + isAnimationActive={false} + /> + + + + + + + + + +
+ ) } diff --git a/packages/desktop-client/src/components/reports/graphs/net-worth-spreadsheet.tsx b/packages/desktop-client/src/components/reports/graphs/net-worth-spreadsheet.tsx index 4750d289f64..81a685be8e6 100644 --- a/packages/desktop-client/src/components/reports/graphs/net-worth-spreadsheet.tsx +++ b/packages/desktop-client/src/components/reports/graphs/net-worth-spreadsheet.tsx @@ -1,5 +1,3 @@ -import React from 'react'; - import * as d from 'date-fns'; import q, { runQuery } from 'loot-core/src/client/query-helpers'; @@ -11,7 +9,6 @@ import { amountToInteger, } from 'loot-core/src/shared/util'; -import AlignedText from '../../common/AlignedText'; import { index } from '../util'; export default function createSpreadsheet( @@ -119,29 +116,20 @@ function recalculate(data, start, end) { const x = d.parseISO(month + '-01'); const change = last ? total - amountToInteger(last.y) : 0; - const label = ( -
-
- {d.format(x, 'MMMM yyyy')} -
-
- - - {integerToCurrency(total)}} - /> - -
-
- ); - if (arr.length === 0) { startNetWorth = total; } endNetWorth = total; - arr.push({ x, y: integerToAmount(total), premadeLabel: label }); + arr.push({ + x: d.format(x, 'MMM ’yy'), + y: integerToAmount(total), + assets: integerToCurrency(assets), + debt: `-${integerToCurrency(debt)}`, + change: integerToCurrency(change), + networth: integerToCurrency(total), + date: d.format(x, 'MMMM yyyy'), + }); arr.forEach(item => { if (item.y < lowestNetWorth || lowestNetWorth === null) { diff --git a/upcoming-release-notes/1740.md b/upcoming-release-notes/1740.md new file mode 100644 index 00000000000..8a7b1d32e3e --- /dev/null +++ b/upcoming-release-notes/1740.md @@ -0,0 +1,6 @@ +--- +category: Enhancements +authors: [shaankhosla] +--- + +Update the NetWorth graph to use the Recharts library. \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index c68af424224..84e07d99b37 100644 --- a/yarn.lock +++ b/yarn.lock @@ -102,6 +102,7 @@ __metadata: react-simple-pull-to-refresh: ^1.3.3 react-spring: ^9.7.1 react-virtualized-auto-sizer: ^1.0.2 + recharts: ^2.8.0 redux: ^4.0.5 redux-thunk: ^2.3.0 remark-gfm: ^3.0.1 @@ -1680,6 +1681,15 @@ __metadata: languageName: node linkType: hard +"@babel/runtime@npm:^7.1.2": + version: 7.23.1 + resolution: "@babel/runtime@npm:7.23.1" + dependencies: + regenerator-runtime: ^0.14.0 + checksum: 0cd0d43e6e7dc7f9152fda8c8312b08321cda2f56ef53d6c22ebdd773abdc6f5d0a69008de90aa41908d00e2c1facb24715ff121274e689305c858355ff02c70 + languageName: node + linkType: hard + "@babel/runtime@npm:^7.11.2, @babel/runtime@npm:^7.12.5, @babel/runtime@npm:^7.14.8, @babel/runtime@npm:^7.16.3, @babel/runtime@npm:^7.20.7, @babel/runtime@npm:^7.21.0, @babel/runtime@npm:^7.5.5, @babel/runtime@npm:^7.8.4, @babel/runtime@npm:^7.9.2": version: 7.22.11 resolution: "@babel/runtime@npm:7.22.11" @@ -6715,6 +6725,13 @@ __metadata: languageName: node linkType: hard +"classnames@npm:^2.2.5": + version: 2.3.2 + resolution: "classnames@npm:2.3.2" + checksum: 2c62199789618d95545c872787137262e741f9db13328e216b093eea91c85ef2bfb152c1f9e63027204e2559a006a92eb74147d46c800a9f96297ae1d9f96f4e + languageName: node + linkType: hard + "clean-css@npm:^5.2.2": version: 5.3.2 resolution: "clean-css@npm:5.3.2" @@ -7484,6 +7501,13 @@ __metadata: languageName: node linkType: hard +"css-unit-converter@npm:^1.1.1": + version: 1.1.2 + resolution: "css-unit-converter@npm:1.1.2" + checksum: 07888033346a5128f34dbe2f72884c966d24e9f29db24416dcde92860242490617ef9a178ac193a92f730834bbeea026cdc7027701d92ba9bbbe59db7a37eb2a + languageName: node + linkType: hard + "css-what@npm:^3.2.1": version: 3.4.2 resolution: "css-what@npm:3.4.2" @@ -7822,6 +7846,13 @@ __metadata: languageName: node linkType: hard +"decimal.js-light@npm:^2.4.1": + version: 2.5.1 + resolution: "decimal.js-light@npm:2.5.1" + checksum: f5a2c7eac1c4541c8ab8a5c8abea64fc1761cefc7794bd5f8afd57a8a78d1b51785e0c4e4f85f4895a043eaa90ddca1edc3981d1263eb6ddce60f32bf5fe66c9 + languageName: node + linkType: hard + "decimal.js@npm:^10.2.1": version: 10.4.3 resolution: "decimal.js@npm:10.4.3" @@ -8321,6 +8352,15 @@ __metadata: languageName: node linkType: hard +"dom-helpers@npm:^3.4.0": + version: 3.4.0 + resolution: "dom-helpers@npm:3.4.0" + dependencies: + "@babel/runtime": ^7.1.2 + checksum: 58d9f1c4a96daf77eddc63ae1236b826e1cddd6db66bbf39b18d7e21896d99365b376593352d52a60969d67fa4a8dbef26adc1439fa2c1b355efa37cacbaf637 + languageName: node + linkType: hard + "dom-serializer@npm:0": version: 0.2.2 resolution: "dom-serializer@npm:0.2.2" @@ -9409,7 +9449,7 @@ __metadata: languageName: node linkType: hard -"eventemitter3@npm:^4.0.0": +"eventemitter3@npm:^4.0.0, eventemitter3@npm:^4.0.1": version: 4.0.7 resolution: "eventemitter3@npm:4.0.7" checksum: 1875311c42fcfe9c707b2712c32664a245629b42bb0a5a84439762dd0fd637fc54d078155ea83c2af9e0323c9ac13687e03cfba79b03af9f40c89b4960099374 @@ -9646,6 +9686,13 @@ __metadata: languageName: node linkType: hard +"fast-equals@npm:^5.0.0": + version: 5.0.1 + resolution: "fast-equals@npm:5.0.1" + checksum: fbb3b6a74f3a0fa930afac151ff7d01639159b4fddd2678b5d50708e0ba38e9ec14602222d10dadb8398187342692c04fbef5a62b1cfcc7942fe03e754e064bc + languageName: node + linkType: hard + "fast-glob@npm:^3.2.11, fast-glob@npm:^3.2.12, fast-glob@npm:^3.2.9": version: 3.2.12 resolution: "fast-glob@npm:3.2.12" @@ -16290,6 +16337,13 @@ __metadata: languageName: node linkType: hard +"postcss-value-parser@npm:^3.3.0": + version: 3.3.1 + resolution: "postcss-value-parser@npm:3.3.1" + checksum: 62cd26e1cdbcf2dcc6bcedf3d9b409c9027bc57a367ae20d31dd99da4e206f730689471fd70a2abe866332af83f54dc1fa444c589e2381bf7f8054c46209ce16 + languageName: node + linkType: hard + "postcss-value-parser@npm:^4.0.0, postcss-value-parser@npm:^4.1.0, postcss-value-parser@npm:^4.2.0": version: 4.2.0 resolution: "postcss-value-parser@npm:4.2.0" @@ -16505,7 +16559,7 @@ __metadata: languageName: node linkType: hard -"prop-types@npm:^15.0.0, prop-types@npm:^15.5.10, prop-types@npm:^15.7.2, prop-types@npm:^15.8.1": +"prop-types@npm:^15.0.0, prop-types@npm:^15.5.10, prop-types@npm:^15.6.2, prop-types@npm:^15.7.2, prop-types@npm:^15.8.1": version: 15.8.1 resolution: "prop-types@npm:15.8.1" dependencies: @@ -16790,7 +16844,7 @@ __metadata: languageName: node linkType: hard -"react-is@npm:^16.13.1, react-is@npm:^16.7.0, react-is@npm:^16.9.0": +"react-is@npm:^16.10.2, react-is@npm:^16.13.1, react-is@npm:^16.7.0, react-is@npm:^16.9.0": version: 16.13.1 resolution: "react-is@npm:16.13.1" checksum: f7a19ac3496de32ca9ae12aa030f00f14a3d45374f1ceca0af707c831b2a6098ef0d6bdae51bd437b0a306d7f01d4677fcc8de7c0d331eb47ad0f46130e53c5f @@ -16811,7 +16865,7 @@ __metadata: languageName: node linkType: hard -"react-lifecycles-compat@npm:^3.0.0": +"react-lifecycles-compat@npm:^3.0.0, react-lifecycles-compat@npm:^3.0.4": version: 3.0.4 resolution: "react-lifecycles-compat@npm:3.0.4" checksum: a904b0fc0a8eeb15a148c9feb7bc17cec7ef96e71188280061fc340043fd6d8ee3ff233381f0e8f95c1cf926210b2c4a31f38182c8f35ac55057e453d6df204f @@ -16901,6 +16955,18 @@ __metadata: languageName: node linkType: hard +"react-resize-detector@npm:^8.0.4": + version: 8.1.0 + resolution: "react-resize-detector@npm:8.1.0" + dependencies: + lodash: ^4.17.21 + peerDependencies: + react: ^16.0.0 || ^17.0.0 || ^18.0.0 + react-dom: ^16.0.0 || ^17.0.0 || ^18.0.0 + checksum: 45e6b87ea7331406bed2a806d0cea98c1467d53a7cfcdf19c2dd55a3460047917d3b175d9cceea6f314b65eb54858cbb981acffd007d67aa16388e517dafb83e + languageName: node + linkType: hard + "react-router-dom@npm:6.11.2": version: 6.11.2 resolution: "react-router-dom@npm:6.11.2" @@ -17002,6 +17068,20 @@ __metadata: languageName: node linkType: hard +"react-smooth@npm:^2.0.2": + version: 2.0.4 + resolution: "react-smooth@npm:2.0.4" + dependencies: + fast-equals: ^5.0.0 + react-transition-group: 2.9.0 + peerDependencies: + prop-types: ^15.6.0 + react: ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 + react-dom: ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 + checksum: 21731e2f9ebc9594eae0f0d875526185392a87c00abf013c9769ed642a4077b62c04c1001b2527a196aabafb87af208f6c7107db674538c4bb95c253ed123447 + languageName: node + linkType: hard + "react-spring@npm:^9.7.1": version: 9.7.1 resolution: "react-spring@npm:9.7.1" @@ -17019,6 +17099,21 @@ __metadata: languageName: node linkType: hard +"react-transition-group@npm:2.9.0": + version: 2.9.0 + resolution: "react-transition-group@npm:2.9.0" + dependencies: + dom-helpers: ^3.4.0 + loose-envify: ^1.4.0 + prop-types: ^15.6.2 + react-lifecycles-compat: ^3.0.4 + peerDependencies: + react: ">=15.0.0" + react-dom: ">=15.0.0" + checksum: d8c9e50aabdc2cfc324e5cdb0ad1c6eecb02e1c0cd007b26d5b30ccf49015e900683dd489348c71fba4055858308d9ba7019e0d37d0e8d37bd46ed098788f670 + languageName: node + linkType: hard + "react-virtualized-auto-sizer@npm:^1.0.2": version: 1.0.15 resolution: "react-virtualized-auto-sizer@npm:1.0.15" @@ -17140,6 +17235,36 @@ __metadata: languageName: node linkType: hard +"recharts-scale@npm:^0.4.4": + version: 0.4.5 + resolution: "recharts-scale@npm:0.4.5" + dependencies: + decimal.js-light: ^2.4.1 + checksum: e970377190a610e684a32c7461c7684ac3603c2e0ac0020bbba1eea9d099b38138143a8e80bf769bb49c0b7cecf22a2f5c6854885efed2d56f4540d4aa7052bd + languageName: node + linkType: hard + +"recharts@npm:^2.8.0": + version: 2.8.0 + resolution: "recharts@npm:2.8.0" + dependencies: + classnames: ^2.2.5 + eventemitter3: ^4.0.1 + lodash: ^4.17.19 + react-is: ^16.10.2 + react-resize-detector: ^8.0.4 + react-smooth: ^2.0.2 + recharts-scale: ^0.4.4 + reduce-css-calc: ^2.1.8 + victory-vendor: ^36.6.8 + peerDependencies: + prop-types: ^15.6.0 + react: ^16.0.0 || ^17.0.0 || ^18.0.0 + react-dom: ^16.0.0 || ^17.0.0 || ^18.0.0 + checksum: 4638bd5c6c2af8f5c79de5e13cce0e38f06e0bbb0a3c4df27a9b12632fd72c0a0604c8246f55e830f323dfa84a3da7cb2634c2243bb9c775d899fd71f9d4c87a + languageName: node + linkType: hard + "rechoir@npm:^0.8.0": version: 0.8.0 resolution: "rechoir@npm:0.8.0" @@ -17158,6 +17283,16 @@ __metadata: languageName: node linkType: hard +"reduce-css-calc@npm:^2.1.8": + version: 2.1.8 + resolution: "reduce-css-calc@npm:2.1.8" + dependencies: + css-unit-converter: ^1.1.1 + postcss-value-parser: ^3.3.0 + checksum: 8fd27c06c4b443b84749a69a8b97d10e6ec7d142b625b41923a8807abb22b9e37e44df14e26cc606a802957be07bdce5e8ee2976a6952a7b438a7727007101e9 + languageName: node + linkType: hard + "redux-thunk@npm:^2.3.0": version: 2.4.2 resolution: "redux-thunk@npm:2.4.2" @@ -20438,6 +20573,28 @@ __metadata: languageName: node linkType: hard +"victory-vendor@npm:^36.6.8": + version: 36.6.11 + resolution: "victory-vendor@npm:36.6.11" + 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 + checksum: 55800076dfa6abedf7758840986a302778a904678d4b66fe47d977c48b6f9484276b780871e6e5105b31c1eb936e9f1331ee39afcc2869bf65ceb7d456143172 + languageName: node + linkType: hard + "victory-voronoi-container@npm:^36.6.10": version: 36.6.10 resolution: "victory-voronoi-container@npm:36.6.10"