diff --git a/src/main/java/com/andrewlalis/perfin/data/TimestampRange.java b/src/main/java/com/andrewlalis/perfin/data/TimestampRange.java index 92482a8..0e1caac 100644 --- a/src/main/java/com/andrewlalis/perfin/data/TimestampRange.java +++ b/src/main/java/com/andrewlalis/perfin/data/TimestampRange.java @@ -2,15 +2,35 @@ package com.andrewlalis.perfin.data; import com.andrewlalis.perfin.data.util.DateUtil; +import java.time.LocalDate; import java.time.LocalDateTime; +import java.time.ZoneId; import java.time.ZoneOffset; public record TimestampRange(LocalDateTime start, LocalDateTime end) { - public static TimestampRange nDaysTillNow(int days) { + public static TimestampRange lastNDays(int days) { LocalDateTime now = DateUtil.nowAsUTC(); return new TimestampRange(now.minusDays(days), now); } + public static TimestampRange thisMonth() { + LocalDateTime localStartOfMonth = LocalDate.now(ZoneId.systemDefault()).atStartOfDay().withDayOfMonth(1); + LocalDateTime utcStart = localStartOfMonth.atZone(ZoneId.systemDefault()) + .withZoneSameInstant(ZoneOffset.UTC) + .toLocalDateTime(); + return new TimestampRange(utcStart, DateUtil.nowAsUTC()); + } + + public static TimestampRange thisYear() { + LocalDateTime utcStart = LocalDate.now(ZoneId.systemDefault()) + .withDayOfYear(1) + .atStartOfDay() + .atZone(ZoneId.systemDefault()) + .withZoneSameInstant(ZoneOffset.UTC) + .toLocalDateTime(); + return new TimestampRange(utcStart, DateUtil.nowAsUTC()); + } + public static TimestampRange unbounded() { LocalDateTime now = DateUtil.nowAsUTC(); return new TimestampRange(LocalDateTime.ofEpochSecond(0, 0, ZoneOffset.UTC), now); diff --git a/src/main/java/com/andrewlalis/perfin/view/component/module/PieChartModule.java b/src/main/java/com/andrewlalis/perfin/view/component/module/PieChartModule.java index 20fe260..79fe381 100644 --- a/src/main/java/com/andrewlalis/perfin/view/component/module/PieChartModule.java +++ b/src/main/java/com/andrewlalis/perfin/view/component/module/PieChartModule.java @@ -1,6 +1,7 @@ package com.andrewlalis.perfin.view.component.module; import com.andrewlalis.perfin.data.AccountRepository; +import com.andrewlalis.perfin.data.TimestampRange; import com.andrewlalis.perfin.data.util.ColorUtil; import com.andrewlalis.perfin.model.Profile; import javafx.application.Platform; @@ -13,30 +14,92 @@ import javafx.scene.paint.Color; import java.util.*; import java.util.concurrent.CompletableFuture; +import java.util.function.Supplier; /** * An abstract dashboard module for displaying a pie chart of data based on a - * selected currency context. + * selected currency context and time window. */ public abstract class PieChartModule extends DashboardModule { + private static final Map> TIMESTAMP_RANGES = Map.of( + "Last 7 days", () -> TimestampRange.lastNDays(7), + "Last 30 days", () -> TimestampRange.lastNDays(30), + "Last 90 days", () -> TimestampRange.lastNDays(90), + "This Month", TimestampRange::thisMonth, + "This Year", TimestampRange::thisYear, + "All Time", TimestampRange::unbounded + ); + private static final String[] RANGE_CHOICES = { + "Last 7 days", + "Last 30 days", + "Last 90 days", + "This Month", + "This Year", + "All Time" + }; + private final ObservableList chartData = FXCollections.observableArrayList(); protected final List dataColors = new ArrayList<>(); private final ChoiceBox currencyChoiceBox = new ChoiceBox<>(); + private final ChoiceBox timeRangeChoiceBox = new ChoiceBox<>(); private final String preferredCurrencySetting; + private final String timeRangeSetting; - public PieChartModule(Pane parent, String title, String preferredCurrencySetting) { + public PieChartModule(Pane parent, String title, String preferredCurrencySetting, String timeRangeSetting) { super(parent); this.preferredCurrencySetting = preferredCurrencySetting; + this.timeRangeSetting = timeRangeSetting; + + this.timeRangeChoiceBox.getItems().addAll(RANGE_CHOICES); + this.timeRangeChoiceBox.getSelectionModel().select("All Time"); + PieChart chart = new PieChart(chartData); chart.setLegendVisible(false); this.getChildren().add(new ModuleHeader( title, + timeRangeChoiceBox, currencyChoiceBox )); this.getChildren().add(chart); currencyChoiceBox.valueProperty().addListener((observable, oldValue, newValue) -> { if (newValue != null) { - getChartData(newValue).exceptionally(throwable -> { + renderChart(); + } else { + chartData.clear(); + } + }); + timeRangeChoiceBox.valueProperty().addListener((observable, oldValue, newValue) -> { + renderChart(); + }); + } + + @Override + public void refreshContents() { + refreshCurrencies(); + String savedTimeRangeLabel = Profile.getCurrent().getSetting(timeRangeSetting).orElse(null); + if (savedTimeRangeLabel != null && TIMESTAMP_RANGES.containsKey(savedTimeRangeLabel)) { + timeRangeChoiceBox.getSelectionModel().select(savedTimeRangeLabel); + } + } + + private TimestampRange getSelectedTimestampRange() { + String selectedLabel = timeRangeChoiceBox.getValue(); + if (selectedLabel == null || !TIMESTAMP_RANGES.containsKey(selectedLabel)) { + return TimestampRange.unbounded(); + } + return TIMESTAMP_RANGES.get(selectedLabel).get(); + } + + private void renderChart() { + final Currency currency = currencyChoiceBox.getValue(); + String timeRangeLabel = timeRangeChoiceBox.getValue(); + if (currency == null || timeRangeLabel == null) { + chartData.clear(); + dataColors.clear(); + return; + } + final TimestampRange range = getSelectedTimestampRange(); + getChartData(currency, range).exceptionally(throwable -> { throwable.printStackTrace(System.err); return Collections.emptyList(); }) @@ -49,16 +112,8 @@ public abstract class PieChartModule extends DashboardModule { } } })); - Profile.getCurrent().setSettingAndSave(preferredCurrencySetting, newValue.getCurrencyCode()); - } else { - chartData.clear(); - } - }); - } - - @Override - public void refreshContents() { - refreshCurrencies(); + Profile.getCurrent().setSettingAndSave(preferredCurrencySetting, currency.getCurrencyCode()); + Profile.getCurrent().setSettingAndSave(timeRangeSetting, timeRangeLabel); } private void refreshCurrencies() { @@ -85,5 +140,5 @@ public abstract class PieChartModule extends DashboardModule { }); } - protected abstract CompletableFuture> getChartData(Currency currency); + protected abstract CompletableFuture> getChartData(Currency currency, TimestampRange range); } diff --git a/src/main/java/com/andrewlalis/perfin/view/component/module/SpendingCategoryChartModule.java b/src/main/java/com/andrewlalis/perfin/view/component/module/SpendingCategoryChartModule.java index 7a1b1bc..f6357a5 100644 --- a/src/main/java/com/andrewlalis/perfin/view/component/module/SpendingCategoryChartModule.java +++ b/src/main/java/com/andrewlalis/perfin/view/component/module/SpendingCategoryChartModule.java @@ -17,13 +17,18 @@ import java.util.concurrent.CompletableFuture; public class SpendingCategoryChartModule extends PieChartModule { public SpendingCategoryChartModule(Pane parent) { - super(parent, "Spending by Category", "charts.category-spend.default-currency"); + super( + parent, + "Spending by Category", + "charts.category-spend.default-currency", + "charts.category-spend.default-time-range" + ); } @Override - protected CompletableFuture> getChartData(Currency currency) { + protected CompletableFuture> getChartData(Currency currency, TimestampRange range) { return Profile.getCurrent().dataSource().mapRepoAsync(AnalyticsRepository.class, repo -> { - var data = repo.getSpendByRootCategory(TimestampRange.unbounded(), currency); + var data = repo.getSpendByRootCategory(range, currency); dataColors.clear(); return data.stream() .map(pair -> { diff --git a/src/main/java/com/andrewlalis/perfin/view/component/module/VendorSpendChartModule.java b/src/main/java/com/andrewlalis/perfin/view/component/module/VendorSpendChartModule.java index 18b9e28..c2d9244 100644 --- a/src/main/java/com/andrewlalis/perfin/view/component/module/VendorSpendChartModule.java +++ b/src/main/java/com/andrewlalis/perfin/view/component/module/VendorSpendChartModule.java @@ -16,13 +16,18 @@ import java.util.concurrent.CompletableFuture; public class VendorSpendChartModule extends PieChartModule { public VendorSpendChartModule(Pane parent) { - super(parent, "Spending by Vendor", "charts.vendor-spend.default-currency"); + super( + parent, + "Spending by Vendor", + "charts.vendor-spend.default-currency", + "charts.vendor-spend.default-time-range" + ); } @Override - protected CompletableFuture> getChartData(Currency currency) { + protected CompletableFuture> getChartData(Currency currency, TimestampRange range) { return Profile.getCurrent().dataSource().mapRepoAsync(AnalyticsRepository.class, repo -> { - var data = repo.getSpendByVendor(TimestampRange.unbounded(), currency); + var data = repo.getSpendByVendor(range, currency); return data.stream() .map(pair -> { TransactionVendor vendor = pair.first();