From 9e5b0f416767d18e247fe616af01ad50164df03b Mon Sep 17 00:00:00 2001 From: Robert Goldmann <deadlocker@gmx.de> Date: Sat, 27 Jul 2019 14:26:49 +0200 Subject: [PATCH] #437 - chart template: incomes/expenditures per month by categories (first draft) --- .../budgetmaster/charts/DefaultCharts.java | 11 +- ...omesAndExpendituresPerMonthByCategories.js | 166 ++++++++++++++++++ src/main/resources/languages/_de.properties | 2 + src/main/resources/languages/_en.properties | 2 + 4 files changed, 178 insertions(+), 3 deletions(-) create mode 100644 src/main/resources/charts/IncomesAndExpendituresPerMonthByCategories.js diff --git a/src/main/java/de/deadlocker8/budgetmaster/charts/DefaultCharts.java b/src/main/java/de/deadlocker8/budgetmaster/charts/DefaultCharts.java index 44424b926..684c1862f 100644 --- a/src/main/java/de/deadlocker8/budgetmaster/charts/DefaultCharts.java +++ b/src/main/java/de/deadlocker8/budgetmaster/charts/DefaultCharts.java @@ -23,15 +23,19 @@ public class DefaultCharts private static final Chart CHART_INCOMES_AND_EXPENDITURES_PER_MONTH_BAR = new Chart("charts.default.incomesAndExpendituresPerMonthBar", getChartFromFile("charts/IncomesAndExpendituresPerMonthBar.js"), - ChartType.DEFAULT, 2); + ChartType.DEFAULT, 3); private static final Chart CHART_INCOMES_AND_EXPENDITURES_PER_MONTH_LINE = new Chart("charts.default.incomesAndExpendituresPerMonthLine", getChartFromFile("charts/IncomesAndExpendituresPerMonthLine.js"), - ChartType.DEFAULT, 2); + ChartType.DEFAULT, 3); private static final Chart CHART_INCOMES_AND_EXPENDITURES_BY_CATEGORY_BAR = new Chart("charts.default.incomesAndExpendituresByCategoryBar", getChartFromFile("charts/IncomesAndExpendituresByCategoryBar.js"), - ChartType.DEFAULT, 11); + ChartType.DEFAULT, 12); + + private static final Chart CHART_INCOMES_AND_EXPENDITURES_PER_MONTH_BY_CATEGORIES = new Chart("charts.default.incomesAndExpendituresPerMonthByCategories", + getChartFromFile("charts/IncomesAndExpendituresPerMonthByCategories.js"), + ChartType.DEFAULT, 3); public static List<Chart> getDefaultCharts() @@ -41,6 +45,7 @@ public class DefaultCharts charts.add(CHART_INCOMES_AND_EXPENDITURES_PER_MONTH_BAR); charts.add(CHART_INCOMES_AND_EXPENDITURES_PER_MONTH_LINE); charts.add(CHART_INCOMES_AND_EXPENDITURES_BY_CATEGORY_BAR); + charts.add(CHART_INCOMES_AND_EXPENDITURES_PER_MONTH_BY_CATEGORIES); charts.sort(Comparator.comparing(Chart::getName)); return charts; diff --git a/src/main/resources/charts/IncomesAndExpendituresPerMonthByCategories.js b/src/main/resources/charts/IncomesAndExpendituresPerMonthByCategories.js new file mode 100644 index 000000000..0e0066378 --- /dev/null +++ b/src/main/resources/charts/IncomesAndExpendituresPerMonthByCategories.js @@ -0,0 +1,166 @@ +/* This list will be dynamically filled with all the transactions between + * the start and and date you select on the "Show Chart" page + * and filtered according to your specified filter. + * An example entry for this list and tutorial about how to create custom charts ca be found in the BudgetMaster wiki: + * https://github.com/deadlocker8/BudgetMaster/wiki/How-to-create-custom-charts + */ +var transactionData = []; + +// Note: All variables starting with "localized" are only available inside default charts. + +transactionData = transactionData.reverse(); + +moment.locale('de'); + + +const NAME = 0; +const COLOR = 1; +const INCOME = 2; +const EXPENDITURE = 3; + +var dates = []; +var values = []; + +for(var i = 0; i < transactionData.length; i++) +{ + var transaction = transactionData[i]; + + var date = moment(transaction.date).startOf('month').format('MMM YY'); + if(!dates.includes(date)) + { + dates.push(date); + values.push([ + [], [], [], [] // NAME, COLOR, INCOME, EXPENDITURE + ]); + } + + // determine index of category name in list + var lastIndex = values.length - 1; + + var categoryName = transaction.category.name; + // create new category if not already in dict + if(!values[lastIndex][NAME].includes(categoryName)) + { + values[lastIndex][NAME].push(categoryName); + values[lastIndex][COLOR].push(transaction.category.color); + values[lastIndex][INCOME].push(0); + values[lastIndex][EXPENDITURE].push(0); + } + + // determine index of category in current last values + var index = values[lastIndex][NAME].indexOf(categoryName); + + // add to income or expenditure sum + var amount = transaction.amount; + if(amount > 0) + { + values[lastIndex][INCOME][index] = values[lastIndex][INCOME][index] + amount; + } + else + { + values[lastIndex][EXPENDITURE][index] = values[lastIndex][EXPENDITURE][index] + Math.abs(amount); + } +} + +var totalIncomeSums = []; +var totalExpenditureSums = []; + +// calculate total sums for all months +for(var i = 0; i < dates.length; i++) +{ + var totalIncomes = 0; + var totalExpenditures = 0; + + values[i][INCOME].forEach(function(value) + { + totalIncomes += value; + }); + + values[i][EXPENDITURE].forEach(function(value) + { + totalExpenditures += value; + }); + + totalIncomeSums.push(totalIncomes); + totalExpenditureSums.push(totalExpenditures); +} + +// Prepare your chart settings here (mandatory) +var plotlyData = []; +var plotlyLayout = { + title: { + text: localizedTitle + }, + barmode: "stack", + hovermode: 'closest', // show hover popup only for hovered item + yaxis: { + rangemode: 'tozero', + tickformat: '.2f %', + showline: true + } +}; + +// create one stacked bar for incomes and one for expenditures for every month and group them by month +for(var i = 0; i < dates.length; i++) +{ + for(var j = 0; j < values[i][NAME].length; j++) + { + var currentValues = values[i]; + + var percentageIncome = (100 / totalIncomeSums[i]) * currentValues[INCOME][j]; + var textIncome = prepareHoverText(percentageIncome, currentValues[INCOME][j]); + + var percentageExpenditure = (100 / totalExpenditureSums[i]) * currentValues[EXPENDITURE][j]; + var textExpenditure = prepareHoverText(percentageExpenditure, currentValues[EXPENDITURE][j]); + + plotlyData.push({ + x: [localizedData['label2'], localizedData['label1']], + y: [percentageIncome, percentageExpenditure], + type: 'bar', + hoverinfo: 'text', + text: [textIncome, textExpenditure], + name: currentValues[NAME][j], + xaxis: 'x' + (i + 1), // for grouping incomes and expenditure bar by month + barmode: 'stack', + marker: { + color: currentValues[COLOR][j] // use the category's color + } + }); + } + + // axis number inside layout uses a different counting in comparison to xaxis definition in plotlyDate + var axisNumber = i + 1; + if(i === 0) + { + axisNumber = ''; + } + + // calculate subplot start and end position (relative between 0 and 1) + var width = 1 / dates.length; + var start = i * width; + var end = (i + 1) * width; + + plotlyLayout['xaxis' + axisNumber] = { + domain: [start, end], + anchor: 'x' + axisNumber, + title: dates[i], + } +} + +// Add your Plotly configuration settings here (optional) +var plotlyConfig = { + showSendToCloud: false, + displaylogo: false, + showLink: false, + responsive: true +}; + +// Don't touch this line +Plotly.newPlot('chart-canvas', plotlyData, plotlyLayout, plotlyConfig); + + +function prepareHoverText(percentage, value) +{ + value = value / 100; + return percentage.toFixed(1) + '% (' + value.toFixed(1) + ' ' + localizedCurrency + ')'; +} \ No newline at end of file diff --git a/src/main/resources/languages/_de.properties b/src/main/resources/languages/_de.properties index edec359bd..c0cfad504 100644 --- a/src/main/resources/languages/_de.properties +++ b/src/main/resources/languages/_de.properties @@ -337,6 +337,8 @@ charts.default.incomesAndExpendituresPerMonthLine=Eingaben/Ausgaben pro Monat (L charts.default.incomesAndExpendituresPerMonthLine.localization='{"axisY": "Summe in ", "traceName1": "Einnahmen", "traceName2": "Ausgaben"'} charts.default.incomesAndExpendituresByCategoryBar=Eingaben/Ausgaben nach Kategorien charts.default.incomesAndExpendituresByCategoryBar.localization='{"label1": "Ausgaben", "label2": "Einnahmen"'} +charts.default.incomesAndExpendituresPerMonthByCategories=Eingaben/Ausgaben pro Monat (nach Kategorien) +charts.default.incomesAndExpendituresPerMonthByCategories.localization='{"label1": "Ausgaben", "label2": "Einnahmen"'} charts.default.categoryBudget=Verbrauch nach Kategorien chart.new.label.name=Name diff --git a/src/main/resources/languages/_en.properties b/src/main/resources/languages/_en.properties index a35b9dbd6..9eb4f0c8e 100644 --- a/src/main/resources/languages/_en.properties +++ b/src/main/resources/languages/_en.properties @@ -337,6 +337,8 @@ charts.default.incomesAndExpendituresPerMonthLine=Incomes/Expenditures per month charts.default.incomesAndExpendituresPerMonthLine.localization='{"axisY": "Sum in ", "traceName1": "Incomes", "traceName2": "Expenditures"'} charts.default.incomesAndExpendituresByCategoryBar=Incomes/Expenditures by categories charts.default.incomesAndExpendituresByCategoryBar.localization='{"label1": "Expenditures", "label2": "Incomes"'} +charts.default.incomesAndExpendituresPerMonthByCategories=Incomes/Expenditures per month (By categories) +charts.default.incomesAndExpendituresPerMonthByCategories.localization='{"label1": "Expenditures", "label2": "Incomes"'} charts.default.categoryBudget=Consumption by categories chart.new.label.name=Name -- GitLab