diff --git a/resources/assets/v2/src/pages/dashboard/sankey.js b/resources/assets/v2/src/pages/dashboard/sankey.js index e03b4476a2..51c03becf9 100644 --- a/resources/assets/v2/src/pages/dashboard/sankey.js +++ b/resources/assets/v2/src/pages/dashboard/sankey.js @@ -29,17 +29,16 @@ import i18next from "i18next"; Chart.register({SankeyController, Flow}); const SANKEY_CACHE_KEY = 'ds_sankey_data'; -let currencies = []; -let afterPromises = false; -let chart = null; -let transactions = []; -let convertToNative = false; -let translations = { +let currencies = []; +let afterPromises = false; +let chart = null; +let transactions = []; +let convertToNative = false; +let translations = { category: null, unknown_category: null, in: null, out: null, - // TODO unknown_source: null, unknown_dest: null, unknown_account: null, @@ -80,75 +79,97 @@ const getColor = function (key) { // little helper function getObjectName(type, name, direction, code) { + if(convertToNative) { + return getObjectNameWithoutCurrency(type, name, direction); + } + return getObjectNameWithCurrency(type, name, direction, code); - // category 4x - if ('category' === type && null !== name && 'in' === direction) { - return translations.category + ' "' + name + '" (' + translations.in + (convertToNative ? ', ' + code + ')' : ')'); - } - if ('category' === type && null === name && 'in' === direction) { - return translations.unknown_category + ' (' + translations.in + (convertToNative ? ', ' + code + ')' : ')'); - } - if ('category' === type && null !== name && 'out' === direction) { - return translations.category + ' "' + name + '" (' + translations.out + (convertToNative ? ', ' + code + ')' : ')'); - } - if ('category' === type && null === name && 'out' === direction) { - return translations.unknown_category + ' (' + translations.out + (convertToNative ? ', ' + code + ')' : ')'); - } - - // account 4x - if ('account' === type && null === name && 'in' === direction) { - return translations.unknown_source + (convertToNative ? ' (' + code + ')' : ''); - } - if ('account' === type && null !== name && 'in' === direction) { - return translations.revenue_account + '"' + name + '"' + (convertToNative ? ' (' + code + ')' : ''); - } - if ('account' === type && null === name && 'out' === direction) { - return translations.unknown_dest + (convertToNative ? ' (' + code + ')' : ''); - } - if ('account' === type && null !== name && 'out' === direction) { - return translations.expense_account + ' "' + name + '"' + (convertToNative ? ' (' + code + ')' : ''); - } - - // budget 2x - if ('budget' === type && null !== name) { - return translations.budget + ' "' + name + '"' + (convertToNative ? ' (' + code + ')' : ''); - } - if ('budget' === type && null === name) { - return translations.unknown_budget + (convertToNative ? ' (' + code + ')' : ''); - } - console.error('Cannot handle: type:"' + type + '", dir: "' + direction + '"'); } -function getLabelName(type, name, code) { - // category - if ('category' === type && null !== name) { - return translations.category + ' "' + name + '"' + (convertToNative ? ' (' + code + ')' : ''); +function getObjectNameWithoutCurrency(type, name, direction) { + if('category' === type) { + let catName = null === name ? translations.unknown_category : translations.category + ' "' + name + '"'; + let directionText = 'in' === direction ? translations.in : translations.out; + return catName + ' (' + directionText + ')'; } - if ('category' === type && null === name) { - return translations.unknown_category + (convertToNative ? ' (' + code + ')' : ''); + if('account' === type) { + let accountName = null === name ? translations.unknown_account : name; + let directionText = 'in' === direction ? translations.in : translations.out; + let fullAccountName = 'in' === direction ? translations.revenue_account + ' "' + accountName + '"' : translations.expense_account + ' "' + accountName + '"'; + return fullAccountName + ' (' + directionText + ')'; } - // account - if ('account' === type && null === name) { - return translations.unknown_account + (convertToNative ? ' (' + code + ')' : ''); + if('budget' === type) { + return null === name ? translations.unknown_budget : translations.budget + ' "' + name + '"'; } - if ('account' === type && null !== name) { - return name + (convertToNative ? ' (' + code + ')' : ''); + console.error('[a] Cannot handle: type:"' + type + '", dir: "' + direction + '"'); +} +function getObjectNameWithCurrency(type, name, direction, code) { + if('category' === type) { + let catName = null === name ? translations.unknown_category : translations.category + ' "' + name + '"'; + let directionText = 'in' === direction ? translations.in : translations.out; + return catName + ' (' + directionText + ', ' + code + ')'; } - - // budget 2x - if ('budget' === type && null !== name) { - return translations.budget + ' "' + name + '"' + (convertToNative ? ' (' + code + ')' : ''); + if('account' === type) { + let accountName = null === name ? translations.unknown_account : name; + let directionText = 'in' === direction ? translations.in : translations.out; + let fullAccountName = 'in' === direction ? translations.revenue_account + ' "' + accountName + '"' : translations.expense_account + ' "' + accountName + '"'; + return fullAccountName + ' (' + directionText + ', ' + code + ')'; } - if ('budget' === type && null === name) { - return translations.unknown_budget + (convertToNative ? ' (' + code + ')' : ''); + if('budget' === type) { + return (null === name ? translations.unknown_budget : translations.budget + ' "' + name + '"') + ' (' + code + ')'; } - console.error('Cannot handle: type:"' + type + '"'); + console.error('[b] Cannot handle: type:"' + type + '", dir: "' + direction + '"'); } +function getLabel(type, name, code) { + if(convertToNative) { + return getLabelWithoutCurrency(type, name); + } + return getLabelWithCurrency(type, name, code); + +} + +function getLabelWithoutCurrency(type, name) { + if('category' === type) { + return null === name ? translations.unknown_category : translations.category + ' "' + name + '"'; + } + if('account' === type) { + return null === name ? translations.unknown_account : name; + } + if('budget' === type) { + return null === name ? translations.unknown_budget : translations.budget + ' "' + name + '"'; + } + console.error('[a] Cannot handle: type:"' + type + '"'); +} +function getLabelWithCurrency(type, name, code) { + if('category' === type) { + return (null === name ? translations.unknown_category : translations.category + ' "' + name + '"') + ' ('+ code + ')'; + } + if('account' === type) { + return (null === name ? translations.unknown_account : name) + ' (' + code + ')'; + } + if('budget' === type) { + return (null === name ? translations.unknown_budget : translations.budget + ' "' + name + '"') + ' (' + code + ')';; + } + console.error('[b] Cannot handle: type:"' + type + '"'); +} + export default () => ({ loading: false, convertToNative: false, + processedData: null, + eventListeners: { + ['@convert-to-native.window'](event){ + console.log('I heard that! (dashboard/sankey)'); + this.convertToNative = event.detail; + convertToNative = event.detail; + this.processedData = null; + this.loadChart(); + } + }, + + generateOptions() { let options = getDefaultChartSettings('sankey'); @@ -156,125 +177,22 @@ export default () => ({ currencies = []; // variables collected for the sankey chart: - let amounts = {}; - let labels = {}; - for (let i in transactions) { - if (transactions.hasOwnProperty(i)) { - let group = transactions[i]; - for (let ii in group.attributes.transactions) { - if (group.attributes.transactions.hasOwnProperty(ii)) { - // properties of the transaction, used in the generation of the chart: - let transaction = group.attributes.transactions[ii]; - let currencyCode = this.convertToNative ? transaction.native_currency_code : transaction.currency_code; - if(this.convertToNative && (!transaction.hasOwnProperty('native_amount') || null === transaction.native_amount)) { - // skip this transaction, it has no native amount. - console.error('No native amount for transaction #' + group.id + ' ('+this.convertToNative+')'); - continue; - } - let amount = this.convertToNative ? parseFloat(transaction.native_amount) : parseFloat(transaction.amount); - let flowKey; - - /* - Two entries in the sankey diagram for deposits: - 1. From the revenue account (source) to a category (in). - 2. From the category (in) to the big inbox. - */ - if ('deposit' === transaction.type) { - // nr 1 - let category = getObjectName('category', transaction.category_name, 'in', currencyCode); - let revenueAccount = getObjectName('account', transaction.source_name, 'in', currencyCode); - labels[category] = getLabelName('category', transaction.category_name, currencyCode); - labels[revenueAccount] = getLabelName('account', transaction.source_name, currencyCode); - flowKey = revenueAccount + '-' + category + '-' + currencyCode; - if (!amounts.hasOwnProperty(flowKey)) { - amounts[flowKey] = { - from: revenueAccount, - to: category, - amount: 0 - }; - } - amounts[flowKey].amount += amount; - - // nr 2 - flowKey = category + '-' + translations.all_money + '-' + currencyCode; - if (!amounts.hasOwnProperty(flowKey)) { - amounts[flowKey] = { - from: category, - to: translations.all_money + (this.convertToNative ? ' (' + currencyCode + ')' : ''), - amount: 0 - }; - } - amounts[flowKey].amount += amount; - } - /* - Three entries in the sankey diagram for withdrawals: - 1. From the big box to a budget. - 2. From a budget to a category. - 3. From a category to an expense account. - */ - if ('withdrawal' === transaction.type) { - // 1. - let budget = getObjectName('budget', transaction.budget_name, 'out', currencyCode); - labels[budget] = getLabelName('budget', transaction.budget_name, currencyCode); - flowKey = translations.all_money + '-' + budget + '-' + currencyCode; - - if (!amounts.hasOwnProperty(flowKey)) { - amounts[flowKey] = { - from: translations.all_money + (this.convertToNative ? ' (' + currencyCode + ')' : ''), - to: budget, - amount: 0 - }; - } - amounts[flowKey].amount += amount; - - - // 2. - let category = getObjectName('category', transaction.category_name, 'out', currencyCode); - labels[category] = getLabelName('category', transaction.category_name, currencyCode); - flowKey = budget + '-' + category + '-' + currencyCode; - - if (!amounts.hasOwnProperty(flowKey)) { - amounts[flowKey] = { - from: budget, - to: category, - amount: 0 - }; - } - amounts[flowKey].amount += amount; - - // 3. - let expenseAccount = getObjectName('account', transaction.destination_name, 'out', currencyCode); - labels[expenseAccount] = getLabelName('account', transaction.destination_name, currencyCode); - flowKey = category + '-' + expenseAccount + '-' + currencyCode; - - if (!amounts.hasOwnProperty(flowKey)) { - amounts[flowKey] = { - from: category, - to: expenseAccount, - amount: 0 - }; - } - amounts[flowKey].amount += amount; - } - } - } - } - } + this.parseTransactionGroups(transactions); let dataSet = - // sankey chart has one data set. - { - label: 'Firefly III dashboard sankey chart', - data: [], - colorFrom: (c) => getColor(c.dataset.data[c.dataIndex] ? c.dataset.data[c.dataIndex].from : ''), - colorTo: (c) => getColor(c.dataset.data[c.dataIndex] ? c.dataset.data[c.dataIndex].to : ''), - colorMode: 'gradient', // or 'from' or 'to' - labels: labels, - size: 'min', // or 'min' if flow overlap is preferred - }; - for (let i in amounts) { - if (amounts.hasOwnProperty(i)) { - let amount = amounts[i]; + // sankey chart has one data set. + { + label: 'Firefly III dashboard sankey chart', + data: [], + colorFrom: (c) => getColor(c.dataset.data[c.dataIndex] ? c.dataset.data[c.dataIndex].from : ''), + colorTo: (c) => getColor(c.dataset.data[c.dataIndex] ? c.dataset.data[c.dataIndex].to : ''), + colorMode: 'gradient', // or 'from' or 'to' + labels: this.processedData.labels, + size: 'min', // or 'min' if flow overlap is preferred + }; + for (let i in this.processedData.amounts) { + if (this.processedData.amounts.hasOwnProperty(i)) { + let amount = this.processedData.amounts[i]; dataSet.data.push({from: amount.from, to: amount.to, flow: amount.amount}); } } @@ -282,6 +200,133 @@ export default () => ({ return options; }, + parseTransactionGroups(groups) { + this.processedData = { + amounts: {}, + labels: {} + }; + for (let i in groups) { + if (groups.hasOwnProperty(i)) { + let group = groups[i]; + this.parseTransactionGroup(group); + } + } + }, + parseTransactionGroup(group) { + for (let ii in group.attributes.transactions) { + if (group.attributes.transactions.hasOwnProperty(ii)) { + // properties of the transaction, used in the generation of the chart: + let transaction = group.attributes.transactions[ii]; + this.parseTransaction(transaction); + } + } + + }, + parseTransaction(transaction) { + let currencyCode = transaction.currency_code; + let amount = parseFloat(transaction.amount); + let flowKey; + if (this.convertToNative) { + currencyCode = transaction.native_currency_code; + amount = parseFloat(transaction.native_amount); + } + if ('deposit' === transaction.type) { + this.parseDeposit(transaction, currencyCode, amount); + return; + } + + if ('withdrawal' === transaction.type) { + this.parseWithdrawal(transaction, currencyCode, amount); + } + }, + parseWithdrawal(transaction, currencyCode, amount) { + /* + Three entries in the sankey diagram for withdrawals: + 1. From the big box to a budget. + 2. From a budget to a category. + 3. From a category to an expense account. + */ + + // first one: + let budget = getObjectName('budget', transaction.budget_name, 'out', currencyCode); + this.processedData.labels[budget] = getLabel('budget', transaction.budget_name, currencyCode); + let flowKey = translations.all_money + '-' + budget + '-' + currencyCode; + + if (!this.processedData.amounts.hasOwnProperty(flowKey)) { + this.processedData.amounts[flowKey] = { + from: translations.all_money + (this.convertToNative ? ' (' + currencyCode + ')' : ''), + to: budget, + amount: 0 + }; + } + this.processedData.amounts[flowKey].amount += amount; + + + // second one: + let category = getObjectName('category', transaction.category_name, 'out', currencyCode); + this.processedData.labels[category] = getLabel('category', transaction.category_name, currencyCode); + flowKey = budget + '-' + category + '-' + currencyCode; + + if (!this.processedData.amounts.hasOwnProperty(flowKey)) { + this.processedData.amounts[flowKey] = { + from: budget, + to: category, + amount: 0 + }; + } + this.processedData.amounts[flowKey].amount += amount; + + // third one: + let expenseAccount = getObjectName('account', transaction.destination_name, 'out', currencyCode); + this.processedData.labels[expenseAccount] = getLabel('account', transaction.destination_name, currencyCode); + flowKey = category + '-' + expenseAccount + '-' + currencyCode; + + if (!this.processedData.amounts.hasOwnProperty(flowKey)) { + this.processedData.amounts[flowKey] = { + from: category, + to: expenseAccount, + amount: 0 + }; + } + this.processedData.amounts[flowKey].amount += amount; + }, + parseDeposit(transaction, currencyCode, amount) { + /* + Two entries in the sankey diagram for deposits: + 1. From the revenue account (source) to a category (in). + 2. From the category (in) to the big inbox. + */ + + // this is the first one: + let category = getObjectName('category', transaction.category_name, 'in', currencyCode); + let revenueAccount = getObjectName('account', transaction.source_name, 'in', currencyCode); + let flowKey = revenueAccount + '-' + category + '-' + currencyCode; + this.processedData.labels[category] = getLabel('category', transaction.category_name, currencyCode); + this.processedData.labels[revenueAccount] = getLabel('account', transaction.source_name, currencyCode); + + // create if necessary: + if (!this.processedData.amounts.hasOwnProperty(flowKey)) { + this.processedData.amounts[flowKey] = { + from: revenueAccount, + to: category, + amount: 0 + }; + } + this.processedData.amounts[flowKey].amount += amount; + + // this is the second one: + flowKey = category + '-' + translations.all_money + '-' + currencyCode; + if (!this.processedData.amounts.hasOwnProperty(flowKey)) { + this.processedData.amounts[flowKey] = { + from: category, + to: translations.all_money + (this.convertToNative ? ' (' + currencyCode + ')' : ''), + amount: 0 + }; + } + this.processedData.amounts[flowKey].amount += amount; + }, + + drawChart(options) { if (null !== chart) { chart.data.datasets = options.data.datasets; @@ -292,12 +337,12 @@ export default () => ({ }, getFreshData() { - const start = new Date(window.store.get('start')); - const end = new Date(window.store.get('end')); + const start = new Date(window.store.get('start')); + const end = new Date(window.store.get('end')); const cacheKey = getCacheKey(SANKEY_CACHE_KEY, {start: start, end: end}); const cacheValid = window.store.get('cacheValid'); - let cachedData = window.store.get(cacheKey); + let cachedData = window.store.get(cacheKey); if (cacheValid && typeof cachedData !== 'undefined') { transactions = cachedData; @@ -316,8 +361,8 @@ export default () => ({ this.downloadTransactions(params); }, downloadTransactions(params) { - const start = new Date(window.store.get('start')); - const end = new Date(window.store.get('end')); + const start = new Date(window.store.get('start')); + const end = new Date(window.store.get('end')); const cacheKey = getCacheKey(SANKEY_CACHE_KEY, {convertToNative: this.convertToNative, start: start, end: end}); //console.log('Downloading page ' + params.page + '...'); @@ -356,25 +401,25 @@ export default () => ({ transactions = []; Promise.all([getVariable('convert_to_native', false)]).then((values) => { this.convertToNative = values[0]; - convertToNative = values[0]; + convertToNative = values[0]; - // some translations: - translations.all_money = i18next.t('firefly.all_money'); - translations.category = i18next.t('firefly.category'); - translations.in = i18next.t('firefly.money_flowing_in'); - translations.out = i18next.t('firefly.money_flowing_out'); - translations.unknown_category = i18next.t('firefly.unknown_category_plain'); - translations.unknown_source = i18next.t('firefly.unknown_source_plain'); - translations.unknown_dest = i18next.t('firefly.unknown_dest_plain'); - translations.unknown_account = i18next.t('firefly.unknown_any_plain'); - translations.unknown_budget = i18next.t('firefly.unknown_budget_plain'); - translations.expense_account = i18next.t('firefly.expense_account'); - translations.revenue_account = i18next.t('firefly.revenue_account'); - translations.budget = i18next.t('firefly.budget'); + // some translations: + translations.all_money = i18next.t('firefly.all_money'); + translations.category = i18next.t('firefly.category'); + translations.in = i18next.t('firefly.money_flowing_in'); + translations.out = i18next.t('firefly.money_flowing_out'); + translations.unknown_category = i18next.t('firefly.unknown_category_plain'); + translations.unknown_source = i18next.t('firefly.unknown_source_plain'); + translations.unknown_dest = i18next.t('firefly.unknown_dest_plain'); + translations.unknown_account = i18next.t('firefly.unknown_any_plain'); + translations.unknown_budget = i18next.t('firefly.unknown_budget_plain'); + translations.expense_account = i18next.t('firefly.expense_account'); + translations.revenue_account = i18next.t('firefly.revenue_account'); + translations.budget = i18next.t('firefly.budget'); - // console.log('sankey after promises'); - afterPromises = true; - this.loadChart(); + // console.log('sankey after promises'); + afterPromises = true; + this.loadChart(); }); window.store.observe('end', () => { diff --git a/resources/views/v2/partials/dashboard/sankey.blade.php b/resources/views/v2/partials/dashboard/sankey.blade.php index 158145de83..a1f622b7dc 100644 --- a/resources/views/v2/partials/dashboard/sankey.blade.php +++ b/resources/views/v2/partials/dashboard/sankey.blade.php @@ -6,7 +6,7 @@ >{{ __('firefly.income_and_expense') }} -
+