diff --git a/src/main/java/de/deadlocker8/budgetmaster/charts/DefaultCharts.java b/src/main/java/de/deadlocker8/budgetmaster/charts/DefaultCharts.java index 96b5e1ec28bfd2e9709d31124e6d7b28bbb371e8..0312b0d152190d9b3d0ca185c3cc4a123223ae29 100644 --- a/src/main/java/de/deadlocker8/budgetmaster/charts/DefaultCharts.java +++ b/src/main/java/de/deadlocker8/budgetmaster/charts/DefaultCharts.java @@ -50,6 +50,10 @@ public class DefaultCharts getChartFromFile("charts/IncomesAndExpendituresPerYearBar.js"), ChartType.DEFAULT, 6, ChartDisplayType.BAR, ChartGroupType.YEAR, "incomesAndExpendituresPerYearBar.png"); + private static final Chart CHART_INCOMES_AND_EXPENDITURES_PER_YEAR_BY_CATEGORIES = new Chart("charts.default.incomesAndExpendituresPerYearByCategories", + getChartFromFile("charts/IncomesAndExpendituresPerYearByCategories.js"), + ChartType.DEFAULT, 2, ChartDisplayType.BAR, ChartGroupType.YEAR, "incomesAndExpendituresPerYearByCategories.png"); + private DefaultCharts() { } @@ -65,6 +69,7 @@ public class DefaultCharts charts.add(CHART_INCOMES_AND_EXPENDITURES_PER_MONTH_BY_CATEGORIES); charts.add(CHART_REST_PER_MONTH); charts.add(CHART_INCOMES_AND_EXPENDITURES_PER_YEAR_BAR); + charts.add(CHART_INCOMES_AND_EXPENDITURES_PER_YEAR_BY_CATEGORIES); return charts; } @@ -83,7 +88,7 @@ public class DefaultCharts } catch(IOException e) { - e.printStackTrace(); + LOGGER.error("Error getting chart from file", e); } return ""; diff --git a/src/main/resources/charts/IncomesAndExpendituresPerYearByCategories.js b/src/main/resources/charts/IncomesAndExpendituresPerYearByCategories.js new file mode 100644 index 0000000000000000000000000000000000000000..4a8b55dccddcabaa5d8338853323aa6c5715f977 --- /dev/null +++ b/src/main/resources/charts/IncomesAndExpendituresPerYearByCategories.js @@ -0,0 +1,205 @@ +/* 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 categoryNames = []; +var categoryColors = []; + +for(var i = 0; i < transactionData.length; i++) +{ + var currentTransaction = transactionData[i]; + if(!categoryNames.includes(currentTransaction.category.name)) + { + categoryNames.push(currentTransaction.category.name); + categoryColors.push(currentTransaction.category.color); + } +} + +var dates = []; +var values = []; + +for(var i = 0; i < transactionData.length; i++) +{ + var transaction = transactionData[i]; + + var date = moment(transaction.date).startOf('year').format('YYYY'); + if(!dates.includes(date)) + { + dates.push(date); + values.push([ + categoryNames, // NAME + categoryColors, // COLOR + new Array(categoryNames.length).fill(0), // INCOME + new Array(categoryNames.length).fill(0) // 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: '.0f', + ticksuffix: localizedCurrency, + 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 currentName = currentValues[NAME][j]; + + var currentIncomeValue = currentValues[INCOME][j]; + var percentageIncome = (100 / totalIncomeSums[i]) * currentIncomeValue; + var textIncome = prepareHoverText(currentName, percentageIncome, currentIncomeValue); + + var currentExpenditureValue = currentValues[EXPENDITURE][j]; + var percentageExpenditure = (100 / totalExpenditureSums[i]) * currentExpenditureValue; + var textExpenditure = prepareHoverText(currentName, percentageExpenditure, currentExpenditureValue); + + // add border if category color is white + var borderWidth = 0; + if(currentValues[COLOR][j] === '#FFFFFF') + { + borderWidth = 1; + } + + plotlyData.push({ + x: [localizedData['label2'], localizedData['label1']], + y: [currentIncomeValue / 100.0, currentExpenditureValue / 100.0], + type: 'bar', + hoverinfo: 'text', + hovertext: [textIncome, textExpenditure], + name: currentName, + xaxis: 'x' + (i + 1), // for grouping incomes and expenditure bar by month + barmode: 'stack', + showlegend: i === 0, + legendgroup: currentName, + marker: { + color: currentValues[COLOR][j], // use the category's color + line: { + color: '#212121', + width: borderWidth + } + } + }); + } + + // 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, + displayModeBar: true, + toImageButtonOptions: { + format: 'png', + filename: 'BudgetMaster_chart_export', + height: 1080, + width: 1920, + } +}; + +// Don't touch this line +Plotly.newPlot("containerID", plotlyData, plotlyLayout, plotlyConfig); + + +function prepareHoverText(categoryName, percentage, value) +{ + value = value / 100; + return categoryName + ' ' + percentage.toFixed(1) + '% (' + value.toFixed(1) + ' ' + localizedCurrency + ')'; +} \ No newline at end of file diff --git a/src/main/resources/languages/base_de.properties b/src/main/resources/languages/base_de.properties index 2d2af29a2e1c726072ddbf1514f6855a9b2b075d..7326352f86bfbf8043e8249847f1484cb2899995 100644 --- a/src/main/resources/languages/base_de.properties +++ b/src/main/resources/languages/base_de.properties @@ -533,6 +533,8 @@ charts.default.restPerMonth=Rest charts.default.restPerMonth.localization='{"label1": "Rest in "'} charts.default.incomesAndExpendituresPerYearBar=Eingaben/Ausgaben charts.default.incomesAndExpendituresPerYearBar.localization='{"axisY": "Summe in ", "traceName1": "Einnahmen", "traceName2": "Ausgaben"'} +charts.default.incomesAndExpendituresPerYearByCategories=Eingaben/Ausgaben nach Kategorien +charts.default.incomesAndExpendituresPerYearByCategories.localization='{"label1": "Ausgaben", "label2": "Einnahmen"'} chart.new.label.name=Name chart.new.label.script=Script diff --git a/src/main/resources/languages/base_en.properties b/src/main/resources/languages/base_en.properties index da0b7609b43d93f421b0378f12a05fced342b316..b0152ba938ad0a4d26c908c7c0b954e92e69ef88 100644 --- a/src/main/resources/languages/base_en.properties +++ b/src/main/resources/languages/base_en.properties @@ -533,6 +533,8 @@ charts.default.restPerMonth=Rest charts.default.restPerMonth.localization='{"label1": "Rest in "'} charts.default.incomesAndExpendituresPerYearBar=Incomes/Expenditures charts.default.incomesAndExpendituresPerYearBar.localization='{"axisY": "Sum in ", "traceName1": "Incomes", "traceName2": "Expenditures"'} +charts.default.incomesAndExpendituresPerYearByCategories=Incomes/Expenditures by categories +charts.default.incomesAndExpendituresPerYearByCategories.localization='{"label1": "Expenditures", "label2": "Incomes"'} chart.new.label.name=Name chart.new.label.script=Script diff --git a/src/main/resources/static/images/charts/incomesAndExpendituresPerYearByCategories.png b/src/main/resources/static/images/charts/incomesAndExpendituresPerYearByCategories.png new file mode 100644 index 0000000000000000000000000000000000000000..d66432ed5b449551296290a09ca3f1db5ee43d24 Binary files /dev/null and b/src/main/resources/static/images/charts/incomesAndExpendituresPerYearByCategories.png differ