From 0196555f07d7295a40aefd5aec266f3932efbb2b Mon Sep 17 00:00:00 2001 From: William Carroll Date: Fri, 13 Jan 2023 17:36:49 -0800 Subject: [PATCH] feat(wpcarro/slx): Render transactions Wire-up clientside slx with HTML. Change-Id: Ieef517b47fae8d1af67bb0c7fcb7eae853f138e1 Reviewed-on: https://cl.tvl.fyi/c/depot/+/7832 Reviewed-by: wpcarro Tested-by: BuildkiteCI --- users/wpcarro/ynabsql/dataviz/components.js | 235 ++++++++++++++++++++ users/wpcarro/ynabsql/dataviz/index.css | 1 - users/wpcarro/ynabsql/dataviz/index.html | 17 +- 3 files changed, 250 insertions(+), 3 deletions(-) create mode 100644 users/wpcarro/ynabsql/dataviz/components.js delete mode 100644 users/wpcarro/ynabsql/dataviz/index.css diff --git a/users/wpcarro/ynabsql/dataviz/components.js b/users/wpcarro/ynabsql/dataviz/components.js new file mode 100644 index 000000000..406531897 --- /dev/null +++ b/users/wpcarro/ynabsql/dataviz/components.js @@ -0,0 +1,235 @@ +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 => ( + + + + ))} + + ); +}; + +/** + * 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 ( +
+
{k}{usd.format(categories[k])}
+ + + + + + + + + + + + + onFocus(1000)}> + {(focus[1000]) && } + onFocus(100)}> + {(focus[100]) && } + onFocus(10)}> + {(focus[10]) && } + onFocus(1)}> + {(focus[1]) && } + onFocus(0.1)}> + {(focus[0.1]) && } + + + +
Aggregations
functionvalue
sum{usd.format(sum)}
per day{usd.format(sum / 365)}
per week{usd.format(sum / 52)}
per month{usd.format(sum / 12)}
Σ Θ($1,000){usd.format(buckets[1000].reduce((acc, x) => acc + x.Outflow, 0))}
Σ Θ($100){usd.format(buckets[100].reduce((acc, x) => acc + x.Outflow, 0))}
Σ Θ($10){usd.format(buckets[10].reduce((acc, x) => acc + x.Outflow, 0))}
Σ Θ($1){usd.format(buckets[1].reduce((acc, x) => acc + x.Outflow, 0))}
Σ Θ($0.10){usd.format(buckets[0.1].reduce((acc, x) => acc + x.Outflow, 0))}
average{usd.format(sum / transactions.length)}
count{transactions.length}
+
+ ); +}; + +const Input = ({ query, onChange, onFilter, onSave }) => ( +
+ Query +
+ onChange(e.target.value)} /> +
+ + +
+
+
+); + +const Table = ({ transactions, onClick }) => ( + + + + + + + + + + + + + + {transactions.map(x => ( + onClick(x)}> + + + + + + + + ))} + +
Transactions
AccountCategoryDateOutflowPayeeMemo
{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(); \ No newline at end of file diff --git a/users/wpcarro/ynabsql/dataviz/index.css b/users/wpcarro/ynabsql/dataviz/index.css deleted file mode 100644 index 82130daf4..000000000 --- a/users/wpcarro/ynabsql/dataviz/index.css +++ /dev/null @@ -1 +0,0 @@ -/* testing */ diff --git a/users/wpcarro/ynabsql/dataviz/index.html b/users/wpcarro/ynabsql/dataviz/index.html index 00f35a780..823ffdc58 100644 --- a/users/wpcarro/ynabsql/dataviz/index.html +++ b/users/wpcarro/ynabsql/dataviz/index.html @@ -2,11 +2,24 @@ - + + + - +
+ + + + + + + + + + +