Added timestamp ranges to the dashboard pie charts.
This commit is contained in:
parent
5a339cbee6
commit
fb2b8d933b
|
@ -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);
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 -> {
|
||||||
|
|
|
@ -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();
|
||||||
|
|
Loading…
Reference in New Issue