From b6fef8d42fc16a30fee10ec844bb386197afdc0a Mon Sep 17 00:00:00 2001 From: andrewlalis Date: Fri, 7 Jun 2024 09:41:28 -0400 Subject: [PATCH] Added basic implementation of a total assets graph. --- .../perfin/control/DashboardController.java | 6 +- .../andrewlalis/perfin/data/DataSource.java | 10 ++- .../module/TotalAssetsGraphModule.java | 63 +++++++++++++++++++ src/main/java/module-info.java | 1 + 4 files changed, 77 insertions(+), 3 deletions(-) create mode 100644 src/main/java/com/andrewlalis/perfin/view/component/module/TotalAssetsGraphModule.java diff --git a/src/main/java/com/andrewlalis/perfin/control/DashboardController.java b/src/main/java/com/andrewlalis/perfin/control/DashboardController.java index 3fdfdac..d94e569 100644 --- a/src/main/java/com/andrewlalis/perfin/control/DashboardController.java +++ b/src/main/java/com/andrewlalis/perfin/control/DashboardController.java @@ -2,6 +2,7 @@ package com.andrewlalis.perfin.control; import com.andrewlalis.javafx_scene_router.RouteSelectionListener; import com.andrewlalis.perfin.model.Profile; +import com.andrewlalis.perfin.view.component.module.TotalAssetsGraphModule; import com.andrewlalis.perfin.view.component.module.*; import javafx.fxml.FXML; import javafx.geometry.Bounds; @@ -30,7 +31,10 @@ public class DashboardController implements RouteSelectionListener { var m4 = new VendorSpendChartModule(modulesFlowPane); m4.columnsProperty.set(2); - modulesFlowPane.getChildren().addAll(accountsModule, transactionsModule, m3, m4); + var m5 = new TotalAssetsGraphModule(modulesFlowPane); + m5.columnsProperty.set(1); + + modulesFlowPane.getChildren().addAll(accountsModule, transactionsModule, m3, m4, m5); } @Override diff --git a/src/main/java/com/andrewlalis/perfin/data/DataSource.java b/src/main/java/com/andrewlalis/perfin/data/DataSource.java index a0a4720..fe8d2b4 100644 --- a/src/main/java/com/andrewlalis/perfin/data/DataSource.java +++ b/src/main/java/com/andrewlalis/perfin/data/DataSource.java @@ -9,6 +9,7 @@ import javafx.application.Platform; import java.math.BigDecimal; import java.nio.file.Path; +import java.time.Instant; import java.util.*; import java.util.concurrent.CompletableFuture; import java.util.function.Consumer; @@ -111,15 +112,16 @@ public interface DataSource { /** * Gets a list of combined total assets for each currency that's tracked, * ordered with highest assets first. + * @param timestamp The timestamp at which to get the balance. * @return A future that resolves to the list of amounts for each currency. */ - default CompletableFuture> getCombinedAccountBalances() { + default CompletableFuture> getCombinedAccountBalances(Instant timestamp) { return mapRepoAsync(AccountRepository.class, repo -> { List accounts = repo.findAll(PageRequest.unpaged()).items(); Map totals = new HashMap<>(); for (var account : accounts) { BigDecimal currencyTotal = totals.computeIfAbsent(account.getCurrency(), c -> BigDecimal.ZERO); - BigDecimal accountBalance = repo.deriveCurrentBalance(account.id); + BigDecimal accountBalance = repo.deriveBalance(account.id, timestamp); if (account.getType() == AccountType.CREDIT_CARD) accountBalance = accountBalance.negate(); totals.put(account.getCurrency(), currencyTotal.add(accountBalance)); } @@ -131,4 +133,8 @@ public interface DataSource { return values; }); } + + default CompletableFuture> getCombinedAccountBalances() { + return getCombinedAccountBalances(Instant.now()); + } } diff --git a/src/main/java/com/andrewlalis/perfin/view/component/module/TotalAssetsGraphModule.java b/src/main/java/com/andrewlalis/perfin/view/component/module/TotalAssetsGraphModule.java new file mode 100644 index 0000000..99f1f49 --- /dev/null +++ b/src/main/java/com/andrewlalis/perfin/view/component/module/TotalAssetsGraphModule.java @@ -0,0 +1,63 @@ +package com.andrewlalis.perfin.view.component.module; + +import com.andrewlalis.perfin.model.Profile; +import javafx.application.Platform; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import javafx.scene.chart.*; +import javafx.scene.layout.Pane; + +import java.time.Instant; +import java.time.LocalDate; +import java.time.ZoneId; +import java.time.temporal.ChronoUnit; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CompletableFuture; + +/** + * A module for visualizing the total asset value in the user's profile over + * a configurable period of time. + */ +public class TotalAssetsGraphModule extends DashboardModule { + private final ObservableList> totalAssetDataPoints = FXCollections.observableArrayList(); + + public TotalAssetsGraphModule(Pane parent) { + super(parent); + Axis xAxis = new CategoryAxis(); + Axis yAxis = new NumberAxis(); + + LineChart chart = new LineChart<>(xAxis, yAxis, FXCollections.observableArrayList( + new XYChart.Series<>("Total Assets", totalAssetDataPoints) + )); + chart.setLegendVisible(false); + this.getChildren().add(new ModuleHeader( + "Total Assets over Time" + )); + this.getChildren().add(chart); + } + + @Override + public void refreshContents() { + totalAssetDataPoints.clear(); + String[] dateLabels = new String[12]; + double[] values = new double[12]; + CompletableFuture[] futures = new CompletableFuture[12]; + for (int i = 0; i < 12; i++) { + final int idx = i; + Instant timestamp = Instant.now().minus((12 - i - 1) * 30, ChronoUnit.DAYS); + dateLabels[i] = LocalDate.from(timestamp.atZone(ZoneId.systemDefault())).toString(); + futures[i] = Profile.getCurrent().dataSource().getCombinedAccountBalances(timestamp) + .thenAccept(moneyValues -> { + values[idx] = moneyValues.getFirst().amount().doubleValue(); + }); + } + CompletableFuture.allOf(futures).thenRun(() -> { + List> dataPoints = new ArrayList<>(12); + for (int i = 0; i < 12; i++) { + dataPoints.add(new XYChart.Data<>(dateLabels[i], values[i])); + } + Platform.runLater(() -> totalAssetDataPoints.addAll(dataPoints)); + }); + } +} diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java index 766abce..5f7f3ba 100644 --- a/src/main/java/module-info.java +++ b/src/main/java/module-info.java @@ -20,4 +20,5 @@ module com.andrewlalis.perfin { opens com.andrewlalis.perfin.view.component to javafx.fxml; opens com.andrewlalis.perfin.view.component.validation to javafx.fxml; exports com.andrewlalis.perfin.model.history to javafx.graphics; + opens com.andrewlalis.perfin.view.component.module to javafx.fxml; } \ No newline at end of file