const usd = new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD',
});
const categories = data.data.transactions.reduce((xs, x) => { xs[x.Category] = null; return xs; }, {});
function sortTransactions(transactions) {
return [...transactions].sort((x, y) => {
if (x.Outflow < y.Outflow) {
return 1;
} else if (x.Outflow > y.Outflow) {
return -1;
} else {
return 0;
}
});
}
function transactionKey(x) {
const keys = [
'Account',
'Flag',
'Date',
'Payee',
'Category',
'Memo',
'Outflow',
'Inflow',
'Cleared',
];
return keys.map(k => x[k]).join('|');
}
class App extends React.Component {
constructor(props) {
super(props);
const query = 'Account:/checking/ after:"01/01/2022" before:"01/01/2023"';
this.state = {
query,
transactions: select(query, data.data.transactions),
saved: {},
focus: {
1000: false,
100: false,
10: false,
1: false,
0.1: false,
},
};
}
render() {
const sum = this.state.transactions.reduce((acc, { Outflow }) => acc + Outflow, 0);
const savedSum = Object.values(this.state.saved).reduce((acc, sum) => acc + sum, 0);
return (
this.setState({
query,
})}
onFilter={() => this.setState({
transactions: select(this.state.query, data.data.transactions),
})}
onSave={() => this.setState({
saved: { ...this.state.saved, [this.state.query]: sum }
})}
/>
this.setState({
focus: { ...this.state.focus, [n]: !this.state.focus[n] },
})}
transactions={this.state.transactions}
/>
{Object.keys(this.state.saved).map(k => (
-
{usd.format(this.state.saved[k])} {k}
))}
{usd.format(savedSum)}
this.setState({
saved: { ...this.state.saved, [transactionKey(x)]: x.Outflow }
})}
/>
);
}
}
/**
* Table rendering information about transactions bucketed by its order of
* magnitude.
*/
const Magnitable = ({ label, transactions }) => {
const categories = transactions.reduce((acc, x) => {
if (x.Category === '') {
return acc;
}
if (!(x.Category in acc)) {
acc[x.Category] = 0;
}
acc[x.Category] += x.Outflow;
return acc;
}, {});
// Sort category keys by sum decreasing.
const keys = [...Object.keys(categories)].sort((x, y) => {
if (categories[x] < categories[y]) {
return 1;
} else if (categories[x] > categories[y]) {
return -1;
} else {
return 0;
}
});
return (
{keys.map(k => (
{k} | {usd.format(categories[k])} |
))}
);
};
/**
* Calculates and renders various aggregates over an input list of transactions.
*/
const AggregateTable = ({ focus, onFocus, transactions }) => {
const sum = transactions.reduce((acc, x) => acc + x.Outflow, 0);
const buckets = transactions.reduce((acc, x) => {
const order = Math.floor(Math.log(x.Outflow) / Math.LN10 + 0.000000001);
const bucket = Math.pow(10, order);
acc[bucket].push(x);
return acc;
}, {0.1: [], 0: [], 1: [], 10: [], 100: [], 1000: []});
return (
Aggregations
function |
value |
sum | {usd.format(sum)} |
per day | {usd.format(sum / 365)} |
per week | {usd.format(sum / 52)} |
per month | {usd.format(sum / 12)} |
onFocus(1000)}>Σ Θ($1,000) | {usd.format(buckets[1000].reduce((acc, x) => acc + x.Outflow, 0))} |
{(focus[1000]) && }
onFocus(100)}>Σ Θ($100) | {usd.format(buckets[100].reduce((acc, x) => acc + x.Outflow, 0))} |
{(focus[100]) && }
onFocus(10)}>Σ Θ($10) | {usd.format(buckets[10].reduce((acc, x) => acc + x.Outflow, 0))} |
{(focus[10]) && }
onFocus(1)}>Σ Θ($1) | {usd.format(buckets[1].reduce((acc, x) => acc + x.Outflow, 0))} |
{(focus[1]) && }
onFocus(0.1)}>Σ Θ($0.10) | {usd.format(buckets[0.1].reduce((acc, x) => acc + x.Outflow, 0))} |
{(focus[0.1]) && }
average | {usd.format(sum / transactions.length)} |
count | {transactions.length} |
);
};
const Input = ({ query, onChange, onFilter, onSave }) => (
);
const Table = ({ transactions, onClick }) => (
Transactions
Account |
Category |
Date |
Outflow |
Payee |
Memo |
{transactions.map(x => (
onClick(x)}>
{x.Account} |
{x.Category} |
{x.Date.toLocaleDateString()} |
{usd.format(x.Outflow)} |
{x.Payee} |
{x.Memo} |
))}
);
const domContainer = document.querySelector('#react-mount');
const root = ReactDOM.createRoot(domContainer);
root.render();