diff --git a/example/dashboards.yaml b/example/dashboards.yaml index bc084a6..00ebd52 100644 --- a/example/dashboards.yaml +++ b/example/dashboards.yaml @@ -713,37 +713,39 @@ dashboards: }, }; - - title: Expenses Year-Over-Year 💸 + - title: Income Year-Over-Year 💰 + width: 50% height: 700px queries: - bql: | - SELECT year, root(account, 2) AS account, CONVERT(SUM(position), '{{ledger.ccy}}', LAST(date)) AS value - WHERE account ~ "^Expenses:" + SELECT year, root(account, 3) AS account, CONVERT(SUM(position), '{{ledger.ccy}}', LAST(date)) AS value + WHERE account ~ "^Income:" GROUP BY account, year ORDER BY account link: /beancount/account/{account}/?time={time} type: echarts - script: | + script: &year_over_year | const currencyFormat = new Intl.NumberFormat(undefined, { style: "currency", currency: ledger.ccy, maximumFractionDigits: 0, }); const years = helpers.iterateYears(ledger.dateFirst, ledger.dateLast); - const maxExpenseAccounts = 10; // number of expense accounts to show (sorted by biggest expenses) + const maxAccounts = 8; // number of accounts to show, sorted by sum const accountSums = {}; const amounts = {}; for (let row of panel.queries[0].result) { if (!(row.account in accountSums)) accountSums[row.account] = 0; - amounts[`${row.year}/${row.account}`] = row.value[ledger.ccy]; - accountSums[row.account] += row.value[ledger.ccy]; + const value = row.account.startsWith("Income:") ? -row.value[ledger.ccy] : row.value[ledger.ccy]; + amounts[`${row.year}/${row.account}`] = value; + accountSums[row.account] += value; } const accounts = Object.entries(accountSums) .sort(([, a], [, b]) => b - a) .map(([name]) => name) - .slice(0, maxExpenseAccounts) + .slice(0, maxAccounts) .reverse(); return { legend: { @@ -755,7 +757,11 @@ dashboards: }, }, yAxis: { - data: accounts.map((account) => account.split(":").pop()), + data: accounts.map((account) => account.split(":").slice(1).join(":")), + }, + grid: { + containLabel: true, + left: 0, }, series: years.map((year) => ({ type: "bar", @@ -776,6 +782,19 @@ dashboards: }, }; + - title: Expenses Year-Over-Year 💸 + width: 50% + height: 700px + queries: + - bql: | + SELECT year, root(account, 2) AS account, CONVERT(SUM(position), '{{ledger.ccy}}', LAST(date)) AS value + WHERE account ~ "^Expenses:" + GROUP BY account, year + ORDER BY account + link: /beancount/account/{account}/?time={time} + type: echarts + script: *year_over_year + - title: Top 10 biggest expenses queries: - bql: SELECT date, payee, narration, position WHERE account ~ "^Expenses:" ORDER BY position DESC LIMIT 10 @@ -868,7 +887,8 @@ dashboards: valueFormatter: currencyFormat.format, }, grid: { - left: "150px", + containLabel: true, + left: 0, }, xAxis: { type: "value", @@ -884,6 +904,11 @@ dashboards: { type: "bar", data: travels.map((travel) => amounts[travel]), + label: { + show: true, + position: "right", + formatter: (params) => currencyFormat.format(params.value), + }, }, ], onClick: (event) => { @@ -930,7 +955,7 @@ dashboards: if (Math.abs(node.value / divisor) < valueThreshold) continue; nodes.push({ name: node.name, label }); - if (node.name.startsWith("Income")) { + if (node.name.startsWith("Income:")) { links.push({ source: node.name, target: root.name, value: -node.value / divisor }); } else { links.push({ diff --git a/frontend/tests/e2e/__image_snapshots__/dashboard_income_and_expenses.png b/frontend/tests/e2e/__image_snapshots__/dashboard_income_and_expenses.png index b9aba50..c738105 100644 Binary files a/frontend/tests/e2e/__image_snapshots__/dashboard_income_and_expenses.png and b/frontend/tests/e2e/__image_snapshots__/dashboard_income_and_expenses.png differ diff --git a/frontend/tests/e2e/__image_snapshots__/dashboard_travelling.png b/frontend/tests/e2e/__image_snapshots__/dashboard_travelling.png index 9f1531c..8ff1c07 100644 Binary files a/frontend/tests/e2e/__image_snapshots__/dashboard_travelling.png and b/frontend/tests/e2e/__image_snapshots__/dashboard_travelling.png differ diff --git a/frontend/tests/e2e/__snapshots__/dashboards.test.js.snap b/frontend/tests/e2e/__snapshots__/dashboards.test.js.snap index 88a351d..bb6fe08 100644 --- a/frontend/tests/e2e/__snapshots__/dashboards.test.js.snap +++ b/frontend/tests/e2e/__snapshots__/dashboards.test.js.snap @@ -3961,6 +3961,56 @@ exports[`Dashboard: HTML Snapshot Tests Income and Expenses 1`] = ` "type": "echarts", "width": "50%" }, + { + "height": "700px", + "queries": [ + { + "bql": "SELECT year, root(account, 3) AS account, CONVERT(SUM(position), \\u0027{{ledger.ccy}}\\u0027, LAST(date)) AS value\\nWHERE account ~ \\"^Income:\\"\\nGROUP BY account, year\\nORDER BY account\\n", + "link": "/beancount/account/{account}/?time={time}", + "result": [ + { + "account": "Income:US:ETrade", + "value": { + "USD": -29.60 + }, + "year": 2020 + }, + { + "account": "Income:US:ETrade", + "value": { + "USD": -780.62 + }, + "year": 2021 + }, + { + "account": "Income:US:Hooli", + "value": { + "USD": -134521.90 + }, + "year": 2020 + }, + { + "account": "Income:US:Hooli", + "value": { + "USD": -129882.20 + }, + "year": 2021 + }, + { + "account": "Income:US:Hooli", + "value": { + "USD": -20958.80 + }, + "year": 2022 + } + ] + } + ], + "script": "const currencyFormat = new Intl.NumberFormat(undefined, {\\n style: \\"currency\\",\\n currency: ledger.ccy,\\n maximumFractionDigits: 0,\\n});\\nconst years = helpers.iterateYears(ledger.dateFirst, ledger.dateLast);\\nconst maxAccounts = 8; // number of accounts to show, sorted by sum\\n\\nconst accountSums = {};\\nconst amounts = {};\\nfor (let row of panel.queries[0].result) {\\n if (!(row.account in accountSums)) accountSums[row.account] = 0;\\n const value = row.account.startsWith(\\"Income:\\") ? -row.value[ledger.ccy] : row.value[ledger.ccy];\\n amounts[\`\${row.year}/\${row.account}\`] = value;\\n accountSums[row.account] += value;\\n}\\n\\nconst accounts = Object.entries(accountSums)\\n .sort(([, a], [, b]) =\\u003e b - a)\\n .map(([name]) =\\u003e name)\\n .slice(0, maxAccounts)\\n .reverse();\\nreturn {\\n legend: {\\n top: \\"bottom\\",\\n },\\n xAxis: {\\n axisLabel: {\\n formatter: currencyFormat.format,\\n },\\n },\\n yAxis: {\\n data: accounts.map((account) =\\u003e account.split(\\":\\").slice(1).join(\\":\\")),\\n },\\n grid: {\\n containLabel: true,\\n left: 0,\\n },\\n series: years.map((year) =\\u003e ({\\n type: \\"bar\\",\\n name: year,\\n data: accounts.map((account) =\\u003e amounts[\`\${year}/\${account}\`] ?? 0),\\n label: {\\n show: true,\\n position: \\"right\\",\\n formatter: (params) =\\u003e currencyFormat.format(params.value),\\n },\\n })),\\n onClick: (event) =\\u003e {\\n const link = panel.queries[0].link\\n .replaceAll(\\"#\\", \\"%23\\")\\n .replace(\\"{account}\\", accounts[event.dataIndex])\\n .replace(\\"{time}\\", event.seriesName);\\n window.open(link);\\n },\\n};\\n", + "title": "Income Year-Over-Year \\ud83d\\udcb0", + "type": "echarts", + "width": "50%" + }, { "height": "700px", "queries": [ @@ -4111,9 +4161,10 @@ exports[`Dashboard: HTML Snapshot Tests Income and Expenses 1`] = ` ] } ], - "script": "const currencyFormat = new Intl.NumberFormat(undefined, {\\n style: \\"currency\\",\\n currency: ledger.ccy,\\n maximumFractionDigits: 0,\\n});\\nconst years = helpers.iterateYears(ledger.dateFirst, ledger.dateLast);\\nconst maxExpenseAccounts = 10; // number of expense accounts to show (sorted by biggest expenses)\\n\\nconst accountSums = {};\\nconst amounts = {};\\nfor (let row of panel.queries[0].result) {\\n if (!(row.account in accountSums)) accountSums[row.account] = 0;\\n amounts[\`\${row.year}/\${row.account}\`] = row.value[ledger.ccy];\\n accountSums[row.account] += row.value[ledger.ccy];\\n}\\n\\nconst accounts = Object.entries(accountSums)\\n .sort(([, a], [, b]) =\\u003e b - a)\\n .map(([name]) =\\u003e name)\\n .slice(0, maxExpenseAccounts)\\n .reverse();\\nreturn {\\n legend: {\\n top: \\"bottom\\",\\n },\\n xAxis: {\\n axisLabel: {\\n formatter: currencyFormat.format,\\n },\\n },\\n yAxis: {\\n data: accounts.map((account) =\\u003e account.split(\\":\\").pop()),\\n },\\n series: years.map((year) =\\u003e ({\\n type: \\"bar\\",\\n name: year,\\n data: accounts.map((account) =\\u003e amounts[\`\${year}/\${account}\`] ?? 0),\\n label: {\\n show: true,\\n position: \\"right\\",\\n formatter: (params) =\\u003e currencyFormat.format(params.value),\\n },\\n })),\\n onClick: (event) =\\u003e {\\n const link = panel.queries[0].link\\n .replaceAll(\\"#\\", \\"%23\\")\\n .replace(\\"{account}\\", accounts[event.dataIndex])\\n .replace(\\"{time}\\", event.seriesName);\\n window.open(link);\\n },\\n};\\n", + "script": "const currencyFormat = new Intl.NumberFormat(undefined, {\\n style: \\"currency\\",\\n currency: ledger.ccy,\\n maximumFractionDigits: 0,\\n});\\nconst years = helpers.iterateYears(ledger.dateFirst, ledger.dateLast);\\nconst maxAccounts = 8; // number of accounts to show, sorted by sum\\n\\nconst accountSums = {};\\nconst amounts = {};\\nfor (let row of panel.queries[0].result) {\\n if (!(row.account in accountSums)) accountSums[row.account] = 0;\\n const value = row.account.startsWith(\\"Income:\\") ? -row.value[ledger.ccy] : row.value[ledger.ccy];\\n amounts[\`\${row.year}/\${row.account}\`] = value;\\n accountSums[row.account] += value;\\n}\\n\\nconst accounts = Object.entries(accountSums)\\n .sort(([, a], [, b]) =\\u003e b - a)\\n .map(([name]) =\\u003e name)\\n .slice(0, maxAccounts)\\n .reverse();\\nreturn {\\n legend: {\\n top: \\"bottom\\",\\n },\\n xAxis: {\\n axisLabel: {\\n formatter: currencyFormat.format,\\n },\\n },\\n yAxis: {\\n data: accounts.map((account) =\\u003e account.split(\\":\\").slice(1).join(\\":\\")),\\n },\\n grid: {\\n containLabel: true,\\n left: 0,\\n },\\n series: years.map((year) =\\u003e ({\\n type: \\"bar\\",\\n name: year,\\n data: accounts.map((account) =\\u003e amounts[\`\${year}/\${account}\`] ?? 0),\\n label: {\\n show: true,\\n position: \\"right\\",\\n formatter: (params) =\\u003e currencyFormat.format(params.value),\\n },\\n })),\\n onClick: (event) =\\u003e {\\n const link = panel.queries[0].link\\n .replaceAll(\\"#\\", \\"%23\\")\\n .replace(\\"{account}\\", accounts[event.dataIndex])\\n .replace(\\"{time}\\", event.seriesName);\\n window.open(link);\\n },\\n};\\n", "title": "Expenses Year-Over-Year \\ud83d\\udcb8", - "type": "echarts" + "type": "echarts", + "width": "50%" }, { "queries": [ @@ -5499,24 +5550,266 @@ exports[`Dashboard: HTML Snapshot Tests Income and Expenses 1`] = `

- Expenses Year-Over-Year 💸 + Income Year-Over-Year 💰

-
+
- + + + + + + + + + + + + + + + + + + + + + + + + US:ETrade + + + US:Hooli + + + $0 + + + $30,000 + + + $60,000 + + + $90,000 + + + $120,000 + + + $150,000 + + + + + + + + + + + + + + + $30 + + + $134,522 + + + $781 + + + $129,882 + + + $0 + + + $20,959 + + + + + + + 2020 + + + + + 2021 + + + + + 2022 + + + +
+
+
+
+

+ Expenses Year-Over-Year 💸 +

+
+
+ + Top 10 biggest expenses -
+ + $1,056 + + + $556 + + + $1,008 +