Added timestamp ranges to the dashboard pie charts.

This commit is contained in:
Andrew Lalis 2024-02-08 09:49:21 -05:00
parent 5a339cbee6
commit fb2b8d933b
4 changed files with 106 additions and 21 deletions

View File

@ -2,15 +2,35 @@ package com.andrewlalis.perfin.data;
import com.andrewlalis.perfin.data.util.DateUtil; import com.andrewlalis.perfin.data.util.DateUtil;
import java.time.LocalDate;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZoneOffset; import java.time.ZoneOffset;
public record TimestampRange(LocalDateTime start, LocalDateTime end) { public record TimestampRange(LocalDateTime start, LocalDateTime end) {
public static TimestampRange nDaysTillNow(int days) { public static TimestampRange lastNDays(int days) {
LocalDateTime now = DateUtil.nowAsUTC(); LocalDateTime now = DateUtil.nowAsUTC();
return new TimestampRange(now.minusDays(days), now); 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() { public static TimestampRange unbounded() {
LocalDateTime now = DateUtil.nowAsUTC(); LocalDateTime now = DateUtil.nowAsUTC();
return new TimestampRange(LocalDateTime.ofEpochSecond(0, 0, ZoneOffset.UTC), now); return new TimestampRange(LocalDateTime.ofEpochSecond(0, 0, ZoneOffset.UTC), now);

View File

@ -1,6 +1,7 @@
package com.andrewlalis.perfin.view.component.module; package com.andrewlalis.perfin.view.component.module;
import com.andrewlalis.perfin.data.AccountRepository; import com.andrewlalis.perfin.data.AccountRepository;
import com.andrewlalis.perfin.data.TimestampRange;
import com.andrewlalis.perfin.data.util.ColorUtil; import com.andrewlalis.perfin.data.util.ColorUtil;
import com.andrewlalis.perfin.model.Profile; import com.andrewlalis.perfin.model.Profile;
import javafx.application.Platform; import javafx.application.Platform;
@ -13,30 +14,92 @@ import javafx.scene.paint.Color;
import java.util.*; import java.util.*;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
import java.util.function.Supplier;
/** /**
* An abstract dashboard module for displaying a pie chart of data based on a * 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 { public abstract class PieChartModule extends DashboardModule {
private static final Map<String, Supplier<TimestampRange>> 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<PieChart.Data> chartData = FXCollections.observableArrayList(); private final ObservableList<PieChart.Data> chartData = FXCollections.observableArrayList();
protected final List<Color> dataColors = new ArrayList<>(); protected final List<Color> dataColors = new ArrayList<>();
private final ChoiceBox<Currency> currencyChoiceBox = new ChoiceBox<>(); private final ChoiceBox<Currency> currencyChoiceBox = new ChoiceBox<>();
private final ChoiceBox<String> timeRangeChoiceBox = new ChoiceBox<>();
private final String preferredCurrencySetting; 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); super(parent);
this.preferredCurrencySetting = preferredCurrencySetting; this.preferredCurrencySetting = preferredCurrencySetting;
this.timeRangeSetting = timeRangeSetting;
this.timeRangeChoiceBox.getItems().addAll(RANGE_CHOICES);
this.timeRangeChoiceBox.getSelectionModel().select("All Time");
PieChart chart = new PieChart(chartData); PieChart chart = new PieChart(chartData);
chart.setLegendVisible(false); chart.setLegendVisible(false);
this.getChildren().add(new ModuleHeader( this.getChildren().add(new ModuleHeader(
title, title,
timeRangeChoiceBox,
currencyChoiceBox currencyChoiceBox
)); ));
this.getChildren().add(chart); this.getChildren().add(chart);
currencyChoiceBox.valueProperty().addListener((observable, oldValue, newValue) -> { currencyChoiceBox.valueProperty().addListener((observable, oldValue, newValue) -> {
if (newValue != null) { 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); throwable.printStackTrace(System.err);
return Collections.emptyList(); return Collections.emptyList();
}) })
@ -49,16 +112,8 @@ public abstract class PieChartModule extends DashboardModule {
} }
} }
})); }));
Profile.getCurrent().setSettingAndSave(preferredCurrencySetting, newValue.getCurrencyCode()); Profile.getCurrent().setSettingAndSave(preferredCurrencySetting, currency.getCurrencyCode());
} else { Profile.getCurrent().setSettingAndSave(timeRangeSetting, timeRangeLabel);
chartData.clear();
}
});
}
@Override
public void refreshContents() {
refreshCurrencies();
} }
private void refreshCurrencies() { private void refreshCurrencies() {
@ -85,5 +140,5 @@ public abstract class PieChartModule extends DashboardModule {
}); });
} }
protected abstract CompletableFuture<List<PieChart.Data>> getChartData(Currency currency); protected abstract CompletableFuture<List<PieChart.Data>> getChartData(Currency currency, TimestampRange range);
} }

View File

@ -17,13 +17,18 @@ import java.util.concurrent.CompletableFuture;
public class SpendingCategoryChartModule extends PieChartModule { public class SpendingCategoryChartModule extends PieChartModule {
public SpendingCategoryChartModule(Pane parent) { 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 @Override
protected CompletableFuture<List<PieChart.Data>> getChartData(Currency currency) { protected CompletableFuture<List<PieChart.Data>> getChartData(Currency currency, TimestampRange range) {
return Profile.getCurrent().dataSource().mapRepoAsync(AnalyticsRepository.class, repo -> { return Profile.getCurrent().dataSource().mapRepoAsync(AnalyticsRepository.class, repo -> {
var data = repo.getSpendByRootCategory(TimestampRange.unbounded(), currency); var data = repo.getSpendByRootCategory(range, currency);
dataColors.clear(); dataColors.clear();
return data.stream() return data.stream()
.map(pair -> { .map(pair -> {

View File

@ -16,13 +16,18 @@ import java.util.concurrent.CompletableFuture;
public class VendorSpendChartModule extends PieChartModule { public class VendorSpendChartModule extends PieChartModule {
public VendorSpendChartModule(Pane parent) { 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 @Override
protected CompletableFuture<List<PieChart.Data>> getChartData(Currency currency) { protected CompletableFuture<List<PieChart.Data>> getChartData(Currency currency, TimestampRange range) {
return Profile.getCurrent().dataSource().mapRepoAsync(AnalyticsRepository.class, repo -> { return Profile.getCurrent().dataSource().mapRepoAsync(AnalyticsRepository.class, repo -> {
var data = repo.getSpendByVendor(TimestampRange.unbounded(), currency); var data = repo.getSpendByVendor(range, currency);
return data.stream() return data.stream()
.map(pair -> { .map(pair -> {
TransactionVendor vendor = pair.first(); TransactionVendor vendor = pair.first();