From 274610f1d3a0263b058ae4327af0445ca34f27d6 Mon Sep 17 00:00:00 2001 From: William Carroll Date: Mon, 23 Jan 2023 07:51:38 -0800 Subject: chore(wpcarro/ynabsql): Delete stale files This also removes the globally available `data.data.transactions`. Change-Id: I674a772ac91f01ff8c2d211157bd567391ab1765 Reviewed-on: https://cl.tvl.fyi/c/depot/+/7913 Autosubmit: wpcarro Tested-by: BuildkiteCI Reviewed-by: wpcarro --- users/wpcarro/ynabsql/dataviz/chart.js | 66 -- users/wpcarro/ynabsql/dataviz/components.js | 1109 -------------------------- users/wpcarro/ynabsql/dataviz/components.jsx | 21 +- users/wpcarro/ynabsql/dataviz/index.html | 12 +- 4 files changed, 12 insertions(+), 1196 deletions(-) delete mode 100644 users/wpcarro/ynabsql/dataviz/chart.js delete mode 100644 users/wpcarro/ynabsql/dataviz/components.js (limited to 'users/wpcarro/ynabsql') diff --git a/users/wpcarro/ynabsql/dataviz/chart.js b/users/wpcarro/ynabsql/dataviz/chart.js deleted file mode 100644 index 7ec8f00aae06..000000000000 --- a/users/wpcarro/ynabsql/dataviz/chart.js +++ /dev/null @@ -1,66 +0,0 @@ -const colors = { - red: 'rgb(255, 45, 70)', - green: 'rgb(75, 192, 35)', -}; - -//////////////////////////////////////////////////////////////////////////////// -// Main -//////////////////////////////////////////////////////////////////////////////// - -const mount = document.getElementById('mount'); - -const chart = new Chart(mount, { - type: 'scatter', - data: { - datasets: [ - { - label: 'Revenue', - data: data.data.transactions.filter(x => x.Inflow > 0).map(x => ({ - x: x.Date, - y: x.Inflow, - metadata: x, - })), - backgroundColor: colors.green, - }, - { - label: 'Expenses', - data: data.data.transactions.filter(x => x.Outflow).map(x => ({ - x: x.Date, - y: x.Outflow, - metadata: x, - })), - backgroundColor: colors.red, - }, - ], - }, - options: { - scales: { - x: { - type: 'time', - title: { - display: true, - text: 'Date', - }, - }, - y: { - title: { - display: true, - text: 'Amount ($USD)' - }, - }, - }, - plugins: { - tooltip: { - callbacks: { - title: function(x) { - return `$${x[0].raw.y} (${x[0].raw.metadata.Date.toLocaleDateString()})`; - }, - label: function(x) { - const { Category, Payee, Memo } = x.raw.metadata; - return `${Payee} - ${Category} (${Memo})`; - }, - }, - }, - }, - }, -}); diff --git a/users/wpcarro/ynabsql/dataviz/components.js b/users/wpcarro/ynabsql/dataviz/components.js deleted file mode 100644 index c385e84e6311..000000000000 --- a/users/wpcarro/ynabsql/dataviz/components.js +++ /dev/null @@ -1,1109 +0,0 @@ -const colors = { - red: 'rgb(255, 45, 70)', - green: 'rgb(75, 192, 35)', - white: 'rgb(249, 246, 238)', - blue: 'rgb(137, 207, 240)', - fadedBlue: 'rgb(137, 207, 240, 0.25)', - purple: 'rgb(203, 195, 227)', - brown: 'rgb(205, 127, 50)', - black: 'rgb(53, 57, 53)', -}; - -const months = [ - 'January', - 'February', - 'March', - 'April', - 'May', - 'June', - 'July', - 'August', - 'September', - 'October', - 'November', - 'December', -]; - -function getWeek(x) { - const dowOffset = 0; - var newYear = new Date(x.getFullYear(), 0, 1); - var day = newYear.getDay() - dowOffset; //the day of week the year begins on - day = (day >= 0 ? day : day + 7); - var daynum = Math.floor((x.getTime() - newYear.getTime() - - (x.getTimezoneOffset() - newYear.getTimezoneOffset()) * 60000) / 86400000) + 1; - var weeknum; - //if the year starts before the middle of a week - if (day < 4) { - weeknum = Math.floor((daynum + day - 1) / 7) + 1; - if (weeknum > 52) { - nYear = new Date(x.getFullYear() + 1, 0, 1); - nday = nYear.getDay() - dowOffset; - nday = nday >= 0 ? nday : nday + 7; - /*if the next year starts before the middle of - the week, it is week #1 of that year*/ - weeknum = nday < 4 ? 1 : 53; - } - } - else { - weeknum = Math.floor((daynum + day - 1) / 7); - } - return weeknum; -} - -function dollars(n, sensitive) { - if (sensitive) { - const order = magnitude(n); - // Shortcut to avoid writing comma-insertion logic v0v. - if (n === 0) { - return '$X.XX'; - } - if (order <= 0) { - return '$X.XX'; - } - if (order === 1) { - return '$XX.XX'; - } - if (order === 2) { - return '$XXX.XX'; - } - if (order === 3) { - return '$X,XXX.XX'; - } - if (order === 4) { - return '$XX,XXX.XX'; - } - if (order === 4) { - return '$XX,XXX.XX'; - } - if (order === 5) { - return '$XXX,XXX.XX'; - } - // Coming soon! :P - if (order === 6) { - return '$X,XXX,XXX.XX'; - } - if (order === 7) { - return '$XX,XXX,XXX.XX'; - } - if (order === 8) { - return '$XXX,XXX,XXX.XX'; - } - // Unsupported - else { - return '$???.??'; - } - } - return usd.format(n); -} - -const usd = new Intl.NumberFormat('en-US', { - style: 'currency', - currency: 'USD', -}); - -const categories = data.data.transactions.reduce((xs, x) => { - if (!(x.Category in xs)) { - xs[x.Category] = []; - } - xs[x.Category].push(x); - return xs; -}, {}); - -const queries = { - housing: 'Category:/(rent|electric)/', - food: 'Category:/(eating|alcohol|grocer)/', - commute: 'Category:/(vacation|gasoline|parking|car maintenance)/', -}; - -/** - * Return the Order of Magnitude of some value, x. - */ -function magnitude(x) { - return Math.floor(Math.log(x) / Math.LN10 + 0.000000001); -} - -function getSum(transactions) { - return transactions.reduce((acc, x) => acc + x.Outflow, 0); -} - -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 ScatterChart extends React.Component { - constructor(props) { - super(props); - this.chart = null; - // Generate a 1/1M random ID. - this.id = btoa(Math.floor(Math.random() * 1e9)); - } - componentDidUpdate(prevProps) { - if (this.props.transactions !== prevProps.transactions) { - this.chart.data.datasets[0].data = this.props.transactions.filter(x => x.Inflow > 0).map(x => ({ - x: x.Date, - y: x.Inflow, - metadata: x, - })); - this.chart.data.datasets[1].data = this.props.transactions.filter(x => x.Outflow > 0).map(x => ({ - x: x.Date, - y: x.Outflow, - metadata: x, - })); - this.chart.update(); - } - } - componentDidMount() { - const mount = document.getElementById(this.id); - this.chart = new Chart(mount, { - type: 'scatter', - data: { - datasets: [ - { - label: 'Revenue', - data: this.props.transactions.filter(x => x.Inflow > 0).map(x => ({ - x: x.Date, - y: x.Inflow, - metadata: x, - })), - backgroundColor: colors.green, - }, - { - label: 'Expenses', - data: this.props.transactions.filter(x => x.Outflow).map(x => ({ - x: x.Date, - y: x.Outflow, - metadata: x, - })), - backgroundColor: colors.red, - }, - ], - }, - options: { - scales: { - x: { - type: 'time', - title: { - display: true, - text: 'Date', - }, - }, - y: { - title: { - display: true, - text: 'Amount ($USD)' - }, - }, - }, - plugins: { - tooltip: { - callbacks: { - title: function(x) { - return `$${x[0].raw.y} (${x[0].raw.metadata.Date.toLocaleDateString()})`; - }, - label: function(x) { - const { Category, Payee, Memo } = x.raw.metadata; - return `${Payee} - ${Category} (${Memo})`; - }, - }, - }, - }, - }, - }); - } - render() { - return ; - } -} - -/** - * Generic line chart parameterized by: - * - datasets: forwarded to chart.js library - * - x: string label for x-axis - * - y: string label for y-axis - */ -class GenLineChart extends React.Component { - constructor(props) { - super(props); - this.chart = null; - // Generate a 1/1M random ID. - this.id = btoa(Math.floor(Math.random() * 1e9)); - } - - componentDidUpdate(prevProps, prevState) { - if (this.props.datasets != prevProps.datasets) { - this.chart.data.datasets = this.props.datasets; - this.chart.update(); - } - } - - componentDidMount() { - const mount = document.getElementById(this.id); - this.chart = new Chart(mount, { - type: 'line', - data: { - datasets: this.props.datasets, - }, - options: { - scales: { - x: { - type: 'time', - title: { - display: true, - text: this.props.x, - }, - }, - y: { - title: { - display: true, - text: this.props.y - }, - }, - }, - }, - }); - } - - render() { - return ; - } -} - -class DonutChart extends React.Component { - constructor(props) { - super(props); - this.chart = null; - // Generate a 1/1M random ID. - this.id = btoa(Math.floor(Math.random() * 1e9)); - } - - componentDidUpdate(prevProps, prevState) { - if (this.props.datasets != prevProps.datasets) { - this.chart.data.datasets = this.props.datasets; - this.chart.update(); - } - } - - componentDidMount() { - const mount = document.getElementById(this.id); - this.chart = new Chart(mount, { - type: 'doughnut', - data: { - labels: this.props.labels, - datasets: this.props.datasets, - }, - options: { - resonsive: true, - }, - }); - } - - render() { - return ; - } -} - -class StackedHistogram extends React.Component { - constructor(props) { - super(props); - this.chart = null; - // Generate a 1/1M random ID. - this.id = btoa(Math.floor(Math.random() * 1e9)); - } - - componentDidUpdate(prevProps, prevState) { - if (this.props.datasets != prevProps.datasets) { - this.chart.data.datasets = this.props.datasets; - this.chart.update(); - } - } - - componentDidMount() { - const mount = document.getElementById(this.id); - this.chart = new Chart(mount, { - type: 'bar', - data: { - labels: this.props.labels, - datasets: this.props.datasets, - }, - options: { - scales: { - x: { - stacked: true, - }, - y: { - stacked: true, - }, - }, - }, - }); - } - - render() { - return ; - } -} - -/** - * Display the "Actual Savings Rate" (bucketed by month) as a line chart with - * the "Expected Savings Rate" overlay. - */ -class SavingsRateLineChart extends React.Component { - constructor(props) { - super(props); - this.chart = null; - // Generate a 1/1M random ID. - this.id = btoa(Math.floor(Math.random() * 1e9)); - } - - static getRevenue(transactions) { - // Bucket revenues into months. - return transactions.reduce((acc, x) => { - const month = x.Date.getMonth(); - acc[month] += x.Inflow; - return acc; - }, new Array(12).fill(0)); - } - - static getExpenses(transactions) { - // Bucket revenues into months. - return transactions.reduce((acc, x) => { - const month = x.Date.getMonth(); - acc[month] += x.Outflow; - return acc; - }, new Array(12).fill(0)); - } - - componentDidMount() { - const mount = document.getElementById(this.id); - const revenue = SavingsRateLineChart.getRevenue(this.props.transactions); - const expenses = SavingsRateLineChart.getExpenses(this.props.transactions); - - this.chart = new Chart(mount, { - type: 'line', - data: { - datasets: [ - { - label: 'actual savings (by month)', - data: new Array(12).fill(null).map((_, i) => ({ - x: i, - y: (revenue[i] - expenses[i]) / revenue[i], - })), - cubicInterpolationMode: 'monotone', - tension: 0.4, - borderColor: colors.fadedBlue, - backgroundColor: colors.fadedBlue, - }, - { - label: 'actual savings (overall)', - data: new Array(12).fill(null).map((_, i) => ({ - x: i, - y: this.props.rate, - })), - cubicInterpolationMode: 'monotone', - tension: 0.4, - borderColor: colors.blue, - backgroundColor: colors.blue, - }, - // 0% marker (out of debt) - { - label: 'beginner (0%)', - data: new Array(12).fill(null).map((x, i) => ({ - x: i, - y: 0.00, - })), - cubicInterpolationMode: 'monotone', - tension: 0.4, - borderColor: colors.white, - backgroundColor: colors.white, - }, - // 25% marker (quarter "Washington" club) - { - label: 'healthy (25%)', - data: new Array(12).fill(null).map((x, i) => ({ - x: i, - y: 0.25, - })), - cubicInterpolationMode: 'monotone', - tension: 0.4, - borderColor: colors.purple, - backgroundColor: colors.purple, - }, - // 50% marker (1/2-dollar "Kennedy" club) - { - label: 'rich (50%)', - data: new Array(12).fill(null).map((x, i) => ({ - x: i, - y: 0.50, - })), - cubicInterpolationMode: 'monotone', - tension: 0.4, - borderColor: colors.brown, - backgroundColor: colors.brown, - }, - // 75% marker - { - label: 'wealthy (75%)', - data: new Array(12).fill(null).map((x, i) => ({ - x: i, - y: 0.75, - })), - cubicInterpolationMode: 'monotone', - tension: 0.4, - borderColor: colors.black, - backgroundColor: colors.black, - }, - ], - labels: months, - }, - options: { - scales: { - y: { - max: 1.0, - min: -1.0, - title: { - display: true, - text: 'Savings Rate (%)' - }, - }, - }, - }, - }); - } - - componentDidUpdate(prevProps, prevState) { - // Bucket revenues into months. - const revenue = SavingsRateLineChart.getRevenue(this.props.transactions); - const expenses = SavingsRateLineChart.getExpenses(this.props.transactions); - - this.chart.data.datasets[0].data = new Array(12).fill(null).map((_, i) => ({ - x: i, - y: (revenue[i] - expenses[i]) / revenue[i], - })); - this.chart.update(); - } - - render() { - return ; - } -} - -class App extends React.Component { - constructor(props) { - super(props); - const query = 'Account:/checking/ after:"01/01/2022" before:"01/01/2023"'; - const savingsView = 'after:"01/01/2022" before:"01/01/2023"'; - const inflowQuery = 'Account:/checking/'; - const outflowQuery = 'Account:/checking/ -Category:/(stocks|crypto)/'; - - this.state = { - query, - sensitive: false, - transactions: select(query, data.data.transactions), - saved: {}, - focus: { - sum: false, - 1000: false, - 100: false, - 10: false, - 1: false, - 0.1: false, - }, - budget: [ - { - label: 'Flexible', - children: [ - { label: 'groceries', savings: false, monthly: 400.00 }, - { label: 'eating out', savings: false, monthly: 200.00 }, - { label: 'alcohol', savings: false, monthly: 200.00 }, - { label: 'household items', savings: false, monthly: 50.00 }, - { label: 'toiletries', savings: false, monthly: 200.00 / 12 }, - { label: 'haircuts', savings: false, monthly: 400.00 / 12 }, - { label: 'gasoline', savings: false, monthly: 100.00 }, - { label: 'parking', savings: false, monthly: 10.00 }, - { label: 'ride services', savings: false, monthly: 50.00 }, - { label: 'LMNT', savings: false, monthly: 45.00 }, - { label: 'books', savings: false, monthly: 25.00 }, - { label: 'vacation', savings: false, monthly: 4000.00 / 12 }, - { label: 'reimbursement', savings: false, monthly: 0.00 }, - ], - }, - { - label: 'Fixed', - children: [ - { label: 'rent', savings: false, monthly: 3100.00 }, - { label: 'electric', savings: false, monthly: 50.00 }, - { label: 'gas', savings: false, monthly: 30.00 }, - { label: 'YNAB', savings: false, monthly: 100.00 / 12 }, - { label: 'Robinhood Gold', savings: false, monthly: 5.00 }, - { label: 'Spotify', savings: false, monthly: 10.00 }, - { label: 'Surfline', savings: false, monthly: 100.00 / 12 }, - { label: 'HBO Max', savings: false, monthly: 170.00 }, - { label: 'Clear', savings: false, monthly: 179.00 }, - { label: 'car insurance', savings: false, monthly: 100.00 }, - { label: 'Making Sense', savings: false, monthly: 50.00 / 12 }, - { label: 'internet', savings: false, monthly: 100.00 }, - { label: 'tax return', savings: false, monthly: 200.00 / 12 }, - ], - }, - { - label: 'Rainy Day (dont touch)', - children: [ - { label: 'emergency fund', savings: false, monthly: 0.00 }, - { label: 'check-ups', savings: false, monthly: 7.50 }, - { label: 'car maintenance', savings: false, monthly: 98.33 }, - ], - }, - { - label: 'Savings (dont touch)', - children: [ - { label: 'stocks', savings: true, monthly: 4000.00 }, - { label: 'crypto', savings: true, monthly: 736.00 }, - ], - }, - { - label: 'Gifts (dont touch)', - children: [ - { label: 'birthdays', savings: false, monthly: 250.00 / 12 }, - { label: 'Valentines Day', savings: false, monthly: 100.00 / 12 }, - { label: 'Mothers Day', savings: false, monthly: 25.00 / 12 }, - { label: 'Fathers Day', savings: false, monthly: 25.00 / 12 }, - { label: 'Christmas', savings: false, monthly: 500.00 / 12 }, - ], - }, - { - label: 'Error Budget', - children: [ - { label: 'stuff I forgot to budget for', savings: false, monthly: 0.00 }, - ], - }, - ], - paycheck: 6000.00, - view: 'budget', - savingsView, - inflowQuery, - outflowQuery, - inflowTransactions: select(inflowQuery, select(savingsView, data.data.transactions)), - outflowTransactions: select(outflowQuery, select(savingsView, data.data.transactions)), - }; - } - - 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); - - let view = null; - if (this.state.view === 'query') { - view = ( - - ); - } else if (this.state.view === 'savings') { - view = ( - this.setState({ - inflowTransactions: select(this.state.inflowQuery, select(this.state.savingsView, data.data.transactions)), - })} - onFilterOutflow={() => this.setState({ - outflowTransactions: select(this.state.outflowQuery, select(this.state.savingsView, data.data.transactions)), - })} - onFilterSavingsView={() => this.setState({ - inflowTransactions: select(this.state.inflowQuery, select(this.state.savingsView, data.data.transactions)), - outflowTransactions: select(this.state.outflowQuery, select(this.state.savingsView, data.data.transactions)), - })} - onSavingsViewChange={x => this.setState({ savingsView: x })} - onInflowQueryChange={x => this.setState({ inflowQuery: x })} - onOutflowQueryChange={x => this.setState({ outflowQuery: x })} - /> - ); - } else if (this.state.view === 'budget') { - // Planned expenses: - // - minus planned assignment to emergency fund (not an expense) - // - minus planned spend to investments (e.g. stocks, crypto) - const budgetedSpend = this.state.budget.reduce((acc, x) => acc + x.children.filter(x => x.savings).reduce((acc, x) => acc + x.monthly, 0), 0); - - view = ( -
-
- Details -
- - -
-
- - -
-
-
    -
  • Available Spend: {dollars(this.state.paycheck * 2, this.state.sensitive)}
  • -
  • Target Spend: {dollars(this.state.paycheck, this.state.sensitive)}
  • -
  • Budgeted Spend (minus savings): {dollars(budgetedSpend, this.state.sensitive)}
  • -
  • Emergency Fund Size (recommended): {dollars(budgetedSpend * 3, this.state.sensitive)}
  • -
-
- x.label)} datasets={[ - { - label: 'Categories', - data: this.state.budget.map(x => x.children.reduce((acc, y) => acc + y.monthly, 0)), - } - ]} /> -
-
    - {this.state.budget.map(x => ( -
  • -
    {x.label} - {dollars(x.children.reduce((acc, x) => acc + x.monthly, 0), this.state.sensitive)}
    -
      {x.children.map(y =>
    • {y.label} - {dollars(y.monthly, this.state.sensitive)}
    • )}
    -
  • - ))} -
-
- ); - } - - return ( - - ); - } -} - -const QueryView = ({ sensitive, query, focus, transactions, saved, setState }) => ( -
- setState({ - query, - })} - onFilter={() => setState({ - transactions: select(query, data.data.transactions), - })} - /> -
- -
- setState({ - saved: { ...saved, [transactionKey(x)]: x.Outflow } - })} - /> -
-); - -function classifyRate(x) { - if (x < 0.25) { - return 'needs improvement'; - } - if (x < 0.50) { - return 'healthy'; - } - if (x < 0.75) { - return 'rich'; - } - if (x < 1.00) { - return 'wealthy'; - } -} - -const SavingsView = ({ - sensitive, - savingsView, - inflowQuery, - outflowQuery, - inflowTransactions, - outflowTransactions, - onSavingsViewChange, - onInflowQueryChange, - onOutflowQueryChange, - onFilterInflow, - onFilterOutflow, - onFilterSavingsView, -}) => { - const revenue = inflowTransactions.reduce((acc, x) => { - acc[x.Date.getMonth()] += x.Inflow; - return acc; - }, new Array(12).fill(0)); - - const inflow = inflowTransactions.reduce((acc, x) => acc + x.Inflow, 0); - const outflow = outflowTransactions.reduce((acc, x) => acc + x.Outflow, 0); - - const delta25Sum = new Array(12).fill(0); - for (let i = 1; i < 12; i += 1) { - delta25Sum[i] = delta25Sum[i - 1] + revenue[i] * 0.25; - } - - const delta50Sum = new Array(12).fill(0); - for (let i = 1; i < 12; i += 1) { - delta50Sum[i] = delta50Sum[i - 1] + revenue[i] * 0.5; - } - - const delta75Sum = new Array(12).fill(0); - for (let i = 1; i < 12; i += 1) { - delta75Sum[i] = delta75Sum[i - 1] + revenue[i] * 0.75; - } - - return ( -
-
- Filtering -
- - onSavingsViewChange(e.target.value)} /> - -
-
- - onInflowQueryChange(e.target.value)} /> - -
-
- - onOutflowQueryChange(e.target.value)} /> - -
-
-
    -
  • inflow: {dollars(inflow, sensitive)}
  • -
  • outflow: {dollars(outflow)}
  • -
  • savings: {dollars(inflow - outflow)}
  • -
  • rate: {parseFloat((inflow - outflow) / inflow * 100).toFixed(2)+"%"} ({classifyRate((inflow - outflow) / inflow)})
  • -
- - {/* O($1,000) */} - ({ - label: k, - data: categories[k].reduce((acc, x) => { - acc[x.Date.getMonth()] += x.Outflow; - return acc; - }, new Array(12).fill(0)).map((x, i) => ({ - x: i, - y: x, - })) - }))} /> - {/* O($100) */} - ({ - label: k, - data: categories[k].reduce((acc, x) => { - acc[x.Date.getMonth()] += x.Outflow; - return acc; - }, new Array(12).fill(0)).map((x, i) => ({ - x: i, - y: x, - })) - }))} /> - {/* O($10) */} - ({ - label: k, - data: categories[k].reduce((acc, x) => { - acc[x.Date.getMonth()] += x.Outflow; - return acc; - }, new Array(12).fill(0)).map((x, i) => ({ - x: i, - y: x, - })) - }))} /> - {/* ({ - x: i, - y: x, - })), - }, - { - label: '50%', - data: delta50Sum.map((x, i) => ({ - x: i, - y: x, - })), - }, - { - label: '75%', - data: delta75Sum.map((x, i) => ({ - x: i, - y: x, - })), - }, - ]} /> */} -
- - - - - - - - - - - {months.map((x, i) => ( - - - - - - - ))} - -
MonthDelta (25%)Delta (50%)Delta (75%)
{x}{dollars(delta25Sum[i], sensitive)}{dollars(delta50Sum[i], sensitive)}{dollars(delta75Sum[i], sensitive)}
-
- ); -}; - -const Calculator = ({ sensitive, saved, onClear }) => ( -
-
    - {Object.keys(saved).map(k => ( -
  • - {dollars(saved[k], sensitive)} {k} -
  • - ))} -
-

{dollars(savedSum, sensitive)}

- -
-); - -const Forecast = ({ sensitive, paycheck, onPaycheck }) => { - const getModel = k => { - const max = paycheck / 3; - const lastYear = getSum(select(`Account:/checking/ after:"01/01/2022" before:"01/01/2023" ${queries[k]}`, data.data.transactions)) / 12; - return { - max, - lastYear, - surplus: max - lastYear, - }; - }; - - const housing = getModel('housing'); - const food = getModel('food'); - const commute = getModel('commute'); - - return ( -
- Forecasting - - onPaycheck(parseFloat(e.target.value))} /> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
categorymaxlast yearsurplus
Housing{dollars(housing.max, sensitive)}{dollars(housing.lastYear, sensitive)}{dollars(housing.surplus, sensitive)}
Food{dollars(food.max, sensitive)}{dollars(food.lastYear, sensitive)}{dollars(food.surplus, sensitive)}
Commute{dollars(commute.max, sensitive)}{dollars(commute.lastYear, sensitive)}{dollars(commute.surplus, sensitive)}
Sum--{dollars(housing.surplus + food.surplus + commute.surplus, sensitive)}
-
- ); -}; - -/** - * Table rendering information about transactions bucketed by its order of - * magnitude. - */ -const Magnitable = ({ sensitive, 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}{dollars(categories[k], sensitive)} - - ))} - - ); -}; - -/** - * Calculates and renders various aggregates over an input list of transactions. - */ -const AggregateTable = ({ sensitive, focus, onFocus, transactions }) => { - const net = transactions.reduce((acc, x) => acc + x.Inflow - x.Outflow, 0); - const sum = transactions.reduce((acc, x) => acc + x.Outflow, 0); - const buckets = transactions.reduce((acc, x) => { - const order = magnitude(x.Outflow); - const bucket = Math.pow(10, order); - acc[bucket].push(x); - return acc; - }, {0.1: [], 0: [], 1: [], 10: [], 100: [], 1000: []}); - - return ( -
- - - - - - - - - - - onFocus('sum')}> - {focus.sum && } - - - - onFocus(1000)}> - {(focus[1000]) && } - onFocus(100)}> - {(focus[100]) && } - onFocus(10)}> - {(focus[10]) && } - onFocus(1)}> - {(focus[1]) && } - onFocus(0.1)}> - {(focus[0.1]) && } - - - -
Aggregations
functionvalue
net{dollars(net, sensitive)}
sum{dollars(sum, sensitive)}
per day{dollars(sum / 365, sensitive)}
per week{dollars(sum / 52, sensitive)}
per month{dollars(sum / 12, sensitive)}
Σ Θ($1,000){dollars(buckets[1000].reduce((acc, x) => acc + x.Outflow, 0), sensitive)}
Σ Θ($100){dollars(buckets[100].reduce((acc, x, sensitive) => acc + x.Outflow, 0), sensitive)}
Σ Θ($10){dollars(buckets[10].reduce((acc, x) => acc + x.Outflow, 0), sensitive)}
Σ Θ($1){dollars(buckets[1].reduce((acc, x) => acc + x.Outflow, 0), sensitive)}
Σ Θ($0.10){dollars(buckets[0.1].reduce((acc, x) => acc + x.Outflow, 0), sensitive)}
average{dollars(sum / transactions.length, sensitive)}
count{transactions.length}
-
- ); -}; - -const Query = ({ query, onChange, onFilter }) => ( -
- Query -
- onChange(e.target.value)} /> -
- -
-
-
-); - -const Transactions = ({ sensitive, transactions, onClick }) => ( - - - - - - - - - - - - - - - {transactions.map(x => ( - onClick(x)}> - - - - - - - - - ))} - -
Transactions
AccountCategoryDateInflowOutflowPayeeMemo
{x.Account}{x.Category}{x.Date.toLocaleDateString()}{dollars(x.Inflow, sensitive)}{dollars(x.Outflow, sensitive)}{x.Payee}{x.Memo}
-); - -const domContainer = document.querySelector('#mount'); -const root = ReactDOM.createRoot(domContainer); - -root.render(); diff --git a/users/wpcarro/ynabsql/dataviz/components.jsx b/users/wpcarro/ynabsql/dataviz/components.jsx index 7a9b7ae958dd..88f69b5f5152 100644 --- a/users/wpcarro/ynabsql/dataviz/components.jsx +++ b/users/wpcarro/ynabsql/dataviz/components.jsx @@ -132,14 +132,6 @@ const usd = new Intl.NumberFormat('en-US', { currency: 'USD', }); -const categories = data.data.transactions.reduce((xs, x) => { - if (!(x.Category in xs)) { - xs[x.Category] = []; - } - xs[x.Category].push(x); - return xs; -}, {}); - const queries = { housing: 'Category:/(rent|electric)/', food: 'Category:/(eating|alcohol|grocer)/', @@ -581,8 +573,8 @@ class SavingsRateLineChart extends React.Component { class App extends React.Component { constructor(props) { super(props); - const query = 'Account:/checking/ (Inflow>1000 OR Outflow>1000)'; - const allTransactions = data.data.transactions; + const query = 'Account:/checking/'; + const allTransactions = []; const savingsView = 'after:"01/01/2022"'; const inflowQuery = 'Account:/checking/'; const outflowQuery = 'Account:/checking/ -Category:/(stocks|crypto)/'; @@ -713,6 +705,13 @@ class App extends React.Component { render() { const sum = this.state.filteredTransactions.reduce((acc, { Outflow }) => acc + Outflow, 0); const savedSum = Object.values(this.state.saved).reduce((acc, sum) => acc + sum, 0); + const categories = this.state.allTransactions.reduce((acc, x) => { + if (!(x.Category in acc)) { + acc[x.Category] = []; + } + acc[x.Category].push(x); + return acc; + }, {}); let view = null; if (this.state.view === 'query') { @@ -738,6 +737,7 @@ class App extends React.Component { } else if (this.state.view === 'savings') { view = (
- - - - - - - - - - - + -- cgit 1.4.1