Skip to content

Commit

Permalink
POC Recharts charting library (#1740)
Browse files Browse the repository at this point in the history
  • Loading branch information
shaankhosla authored Oct 11, 2023
1 parent 21effa6 commit 3dfbd23
Show file tree
Hide file tree
Showing 8 changed files with 324 additions and 114 deletions.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions packages/desktop-client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
3 changes: 3 additions & 0 deletions packages/desktop-client/src/components/reports/Overview.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)',
},
Expand Down
233 changes: 144 additions & 89 deletions packages/desktop-client/src/components/reports/graphs/NetWorthGraph.tsx
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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 (
<div
className={`${css(
{
zIndex: 1000,
pointerEvents: 'none',
borderRadius: 2,
boxShadow: '0 1px 6px rgba(0, 0, 0, .20)',
backgroundColor: theme.alt2MenuBackground,
color: theme.alt2MenuItemText,
padding: 10,
},
style,
)}`}
>
<div>
<div style={{ marginBottom: 10 }}>
<strong>{payload[0].payload.date}</strong>
</div>
<div style={{ lineHeight: 1.5 }}>
<AlignedText left="Assets:" right={payload[0].payload.assets} />
<AlignedText left="Debt:" right={payload[0].payload.debt} />
<AlignedText
left="Net worth:"
right={<strong>{payload[0].payload.networth}</strong>}
/>
<AlignedText left="Change:" right={payload[0].payload.change} />
</div>
</div>
</div>
);
}
};

return (
<Container
Expand All @@ -42,82 +120,59 @@ function NetWorthGraph({
>
{(width, height, portalHost) =>
graphData && (
<Chart
scale={{ x: 'time', y: 'linear' }}
theme={chartTheme}
domainPadding={{ x: 0, y: 10 }}
domain={domain}
width={width}
height={height}
containerComponent={
<VictoryVoronoiContainer voronoiDimension="x" />
}
padding={
compact && {
top: 0,
bottom: 0,
left: 0,
right: 0,
}
}
>
<Area start={graphData.start} end={graphData.end} />
{createElement(
// @ts-expect-error defaultProps mismatch causing issue
graphData.data.length === 1 ? VictoryBar : VictoryArea,
{
data: graphData.data,
labelComponent: <Tooltip portalHost={portalHost} />,
labels: x => x.premadeLabel,
style: {
data:
graphData.data.length === 1
? { width: 50 }
: {
clipPath: 'url(#positive)',
fill: 'url(#positive-gradient)',
},
},
},
)}
{graphData.data.length > 1 && (
<VictoryArea
<ResponsiveContainer>
<div>
{!compact && <div style={{ marginTop: '15px' }} />}
<AreaChart
width={width}
height={height}
data={graphData.data}
style={{
data: {
clipPath: 'url(#negative)',
fill: 'url(#negative-gradient)',
stroke: chartTheme.colors.red,
strokeLinejoin: 'round',
},
}}
/>
)}
{/* Somehow the path `d` attributes are stripped from second
`<VictoryArea />` above if this is removed. I’m just as
confused as you are! */}
<VictoryArea
data={graphData.data}
style={{ data: { fill: 'none', stroke: 'none' } }}
/>
{!compact && (
<VictoryAxis
style={{ ticks: { stroke: chartTheme.colors.red } }}
// eslint-disable-next-line rulesdir/typography
tickFormat={x => d.format(x, "MMM ''yy")}
tickValues={graphData.data.map(item => item.x)}
tickCount={Math.min(width / 220, graphData.data.length)}
offsetY={50}
/>
)}
{!compact && (
<VictoryAxis
dependentAxis
tickCount={Math.round(height / 70)}
crossAxis={!graphData.hasNegative}
/>
)}
</Chart>
margin={{ top: 0, right: 0, left: 0, bottom: 0 }}
>
{compact ? null : (
<CartesianGrid strokeDasharray="3 3" vertical={false} />
)}
{compact ? null : <XAxis dataKey="x" />}
{compact ? null : (
<YAxis
dataKey="y"
domain={['auto', 'auto']}
tickFormatter={tickFormatter}
/>
)}
<Tooltip
content={<CustomTooltip />}
formatter={numberFormatterTooltip}
isAnimationActive={false}
/>
<defs>
<linearGradient id="splitColor" x1="0" y1="0" x2="0" y2="1">
<stop
offset={off}
stopColor={theme.reportsBlue}
stopOpacity={0.2}
/>
<stop
offset={off}
stopColor={theme.reportsRed}
stopOpacity={0.2}
/>
</linearGradient>
</defs>

<Area
type="linear"
dot={false}
activeDot={false}
animationDuration={0}
dataKey="y"
stroke={theme.reportsBlue}
fill="url(#splitColor)"
fillOpacity={1}
/>
</AreaChart>
</div>
</ResponsiveContainer>
)
}
</Container>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import React from 'react';

import * as d from 'date-fns';

import q, { runQuery } from 'loot-core/src/client/query-helpers';
Expand All @@ -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(
Expand Down Expand Up @@ -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 = (
<div>
<div style={{ marginBottom: 10 }}>
<strong>{d.format(x, 'MMMM yyyy')}</strong>
</div>
<div style={{ lineHeight: 1.5 }}>
<AlignedText left="Assets:" right={integerToCurrency(assets)} />
<AlignedText left="Debt:" right={`-${integerToCurrency(debt)}`} />
<AlignedText
left="Net worth:"
right={<strong>{integerToCurrency(total)}</strong>}
/>
<AlignedText left="Change:" right={integerToCurrency(change)} />
</div>
</div>
);

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) {
Expand Down
6 changes: 6 additions & 0 deletions upcoming-release-notes/1740.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
category: Enhancements
authors: [shaankhosla]
---

Update the NetWorth graph to use the Recharts library.
Loading

0 comments on commit 3dfbd23

Please sign in to comment.