Skip to content

Commit

Permalink
Add time selection to balance graph (#102)
Browse files Browse the repository at this point in the history
Prerequisite btcpayserver/btcpayserver#6217. Closes #99.
  • Loading branch information
dennisreimann authored Nov 7, 2024
1 parent 7cf2f85 commit ac56558
Show file tree
Hide file tree
Showing 8 changed files with 101 additions and 23 deletions.
2 changes: 1 addition & 1 deletion BTCPayApp.UI/Components/WalletOverview.razor
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (Histogram != null)
await JS.InvokeVoidAsync("Chart.renderLineChart", "#Histogram", Histogram.Labels, Histogram.Series, "BTC", BitcoinUnit, null, Currency);
await JS.InvokeVoidAsync("Chart.renderLineChart", "#Histogram", Histogram.Labels, Histogram.Series, Histogram.Type.ToString(), "BTC", BitcoinUnit, null, Currency);
}

public void Dispose()
Expand Down
25 changes: 21 additions & 4 deletions BTCPayApp.UI/Features/StoreState.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,13 @@ public record StoreState
private static string[] RateFetchExcludes = ["BTC", "SATS"];

public record SetStoreInfo(AppUserStoreInfo? StoreInfo);
public record SetHistogramType(HistogramType? HistogramType);
public record SetHistogramType(HistogramType Type);
public record FetchStore(string StoreId);
public record FetchOnchainBalance(string StoreId);
public record FetchLightningBalance(string StoreId);
public record FetchOnchainHistogram(string StoreId, HistogramType? Type = null);
public record FetchLightningHistogram(string StoreId, HistogramType? Type = null);
public record FetchHistograms(string StoreId, HistogramType? Type = null);
public record FetchBalances(string StoreId, HistogramType? Type = null);
public record FetchNotifications(string StoreId);
public record UpdateNotification(string NotificationId, bool Seen);
Expand Down Expand Up @@ -97,7 +98,7 @@ public override StoreState Reduce(StoreState state, SetHistogramType action)
{
return state with
{
HistogramType = action.HistogramType,
HistogramType = action.Type,
};
}
}
Expand Down Expand Up @@ -553,7 +554,7 @@ public override StoreState Reduce(StoreState state, SetPosSalesStats action)
return histogram;
}

public class StoreEffects(IAccountManager accountManager)
public class StoreEffects(IState<StoreState> state, IState<UIState> uiState, IAccountManager accountManager)
{
[EffectMethod]
public Task SetStoreInfoEffect(SetStoreInfo action, IDispatcher dispatcher)
Expand All @@ -563,8 +564,9 @@ public Task SetStoreInfoEffect(SetStoreInfo action, IDispatcher dispatcher)
{
var storeId = store.Id;
var posId = store.PosAppId!;
var histogramType = state.Value.HistogramType ?? uiState.Value.HistogramType;
dispatcher.Dispatch(new FetchStore(storeId));
dispatcher.Dispatch(new FetchBalances(storeId));
dispatcher.Dispatch(new FetchBalances(storeId, histogramType));
dispatcher.Dispatch(new FetchNotifications(storeId));
dispatcher.Dispatch(new FetchInvoices(storeId));
dispatcher.Dispatch(new FetchRates(store));
Expand Down Expand Up @@ -609,6 +611,13 @@ public Task FetchBalancesEffect(FetchBalances action, IDispatcher dispatcher)
{
dispatcher.Dispatch(new FetchOnchainBalance(action.StoreId));
dispatcher.Dispatch(new FetchLightningBalance(action.StoreId));
dispatcher.Dispatch(new FetchHistograms(action.StoreId, action.Type));
return Task.CompletedTask;
}

[EffectMethod]
public Task FetchHistogramsEffect(FetchHistograms action, IDispatcher dispatcher)
{
dispatcher.Dispatch(new FetchOnchainHistogram(action.StoreId, action.Type));
dispatcher.Dispatch(new FetchLightningHistogram(action.StoreId, action.Type));
return Task.CompletedTask;
Expand Down Expand Up @@ -833,6 +842,14 @@ public async Task FetchInvoicePaymentMethodsEffect(FetchInvoicePaymentMethods ac
dispatcher.Dispatch(new SetInvoicePaymentMethods(null, error, action.InvoiceId));
}
}

[EffectMethod]
public async Task SetHistogramTypeEffect(SetHistogramType action, IDispatcher dispatcher)
{
var storeInfo = state.Value.StoreInfo;
if (storeInfo != null)
dispatcher.Dispatch(new FetchHistograms(storeInfo.Id, action.Type));
}
}
}

Expand Down
23 changes: 22 additions & 1 deletion BTCPayApp.UI/Features/UIState.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System.Text.Json.Serialization;
using BTCPayApp.CommonServer.Models;
using BTCPayApp.Core;
using BTCPayServer.Client.Models;
using Fluxor;
using Microsoft.JSInterop;

Expand All @@ -24,7 +25,9 @@ public record UIState
{
public string SelectedTheme { get; set; } = Themes.System;
public string SystemTheme { get; set; } = Themes.Light;
public string BitcoinUnit{ get; set; } = CurrencyUnit.SATS;
public string BitcoinUnit { get; set; } = CurrencyUnit.SATS;
public HistogramType HistogramType { get; set; } = HistogramType.Week;

[JsonIgnore]
public RemoteData<AppInstanceInfo>? Instance;

Expand All @@ -33,6 +36,7 @@ public record SetUserTheme(string Theme);
public record FetchInstanceInfo(string? Url);
public record SetInstanceInfo(AppInstanceInfo? Instance, string? Error);
public record ToggleBitcoinUnit(string? BitcoinUnit = null);
public record SetHistogramType(HistogramType Type);

protected class SetUserThemeReducer : Reducer<UIState, SetUserTheme>
{
Expand Down Expand Up @@ -92,6 +96,17 @@ public override UIState Reduce(UIState state, ToggleBitcoinUnit action)
}
}

protected class SetHistogramTypeReducer : Reducer<UIState, SetHistogramType>
{
public override UIState Reduce(UIState state, SetHistogramType action)
{
return state with
{
HistogramType = action.Type
};
}
}

public class UIEffects(IJSRuntime jsRuntime, IHttpClientFactory httpClientFactory, IState<UIState> state)
{
[EffectMethod]
Expand Down Expand Up @@ -137,5 +152,11 @@ public async Task ToggleBitcoinUnitEffect(ToggleBitcoinUnit action, IDispatcher
{
await jsRuntime.InvokeVoidAsync("Interop.setBitcoinUnit", state.Value.BitcoinUnit);
}

[EffectMethod]
public async Task SetHistogramTypeEffect(SetHistogramType action, IDispatcher dispatcher)
{
dispatcher.Dispatch(new StoreState.SetHistogramType(action.Type));
}
}
}
22 changes: 22 additions & 0 deletions BTCPayApp.UI/Pages/DashboardPage.razor
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,22 @@
<WalletOverview BitcoinBalance="@TotalBalance" BitcoinUnit="@BitcoinUnit" MoneyUnit="@UnitMoney" Histogram="@Histogram"
Currency="@Currency" Rate="@Rate" Error="@BalanceError" Loading="@BalanceLoading" OnBalanceClick="ToggleDisplayCurrency"
class="my-3"/>
<div class="btn-group w-100 justify-content-center" role="group" aria-label="Period">
<InputRadioGroup Name="HistogramPeriod" @bind-Value="@HistogramPeriod">
<InputRadio Name="HistogramPeriod" Value="HistogramType.Day" class="btn-check" id="BalancePeriodDay"/>
<label class="btn btn-link" for="BalancePeriodDay">1D</label>
<InputRadio Name="HistogramPeriod" Value="HistogramType.Week" class="btn-check" id="BalancePeriodWeek"/>
<label class="btn btn-link" for="BalancePeriodWeek">1W</label>
<InputRadio Name="HistogramPeriod" Value="HistogramType.Month" class="btn-check" id="BalancePeriodMonth"/>
<label class="btn btn-link" for="BalancePeriodMonth">1M</label>
<InputRadio Name="HistogramPeriod" Value="HistogramType.YTD" class="btn-check" id="BalancePeriodYTD"/>
<label class="btn btn-link" for="BalancePeriodYTD">YTD</label>
<InputRadio Name="HistogramPeriod" Value="HistogramType.Year" class="btn-check" id="BalancePeriodYear"/>
<label class="btn btn-link" for="BalancePeriodYear">1Y</label>
<InputRadio Name="HistogramPeriod" Value="HistogramType.TwoYears" class="btn-check" id="BalancePeriodTwoYears"/>
<label class="btn btn-link" for="BalancePeriodTwoYears">2Y</label>
</InputRadioGroup>
</div>
@if (TotalBalance is > 0)
{
<div class="text-center mt-4 mb-5">
Expand Down Expand Up @@ -104,6 +120,12 @@
: (OnchainConfirmedBalance ?? 0) + (OnchainUnconfirmedBalance ?? 0) + (LightningOnchainBalance ?? 0) + (LightningOffchainBalance ?? 0);
private decimal? Rate => StoreState.Value.Rates?.Data?.FirstOrDefault()?.Rate;
private string BitcoinUnit => UIState.Value.BitcoinUnit;
private HistogramType HistogramPeriod
{
get => UIState.Value.HistogramType;
set => Dispatcher.Dispatch(new UIState.SetHistogramType(value));
}

private HistogramData? Histogram => StoreState.Value.UnifiedHistogram;
private MoneyUnit UnitMoney => BitcoinUnit == CurrencyUnit.BTC ? MoneyUnit.BTC : MoneyUnit.Satoshi;
private LightMoneyUnit UnitLightMoney => BitcoinUnit == CurrencyUnit.BTC ? LightMoneyUnit.BTC : LightMoneyUnit.Satoshi;
Expand Down
10 changes: 9 additions & 1 deletion BTCPayApp.UI/Pages/DashboardPage.razor.css
Original file line number Diff line number Diff line change
@@ -1,4 +1,12 @@
::deep #WalletOverview {
margin-top: var(--btcpay-space-m);
margin-bottom: var(--btcpay-space-l);
margin-bottom: var(--btcpay-space-s);
}

.btn-group label {
--btcpay-btn-color: var(--btcpay-body-text-muted);
--btcpay-btn-active-color: var(--btcpay-body-text);
--btcpay-btn-padding-x: var(--btcpay-space-s);
font-weight: var(--btcpay-font-weight-semibold);
max-width: 4rem;
}
9 changes: 6 additions & 3 deletions BTCPayApp.UI/wwwroot/chartist/chartist-plugin-tooltip.css
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,14 @@
gap: 4px;
}
.chartist-tooltip .chartist-tooltip-value {
color: var(--btcpay-body-text-muted);
background-color: var(--btcpay-bg-tile);
border-radius: var(--btcpay-border-radius);
padding: 0 var(--btcpay-space-s);
padding: var(--btcpay-space-xs) var(--btcpay-space-s);
font-size: var(--btcpay-font-size-s);
text-align: center;
}
.chartist-tooltip .chartist-tooltip-value-date {
color: var(--btcpay-body-text-muted);
}
.chartist-tooltip .chartist-tooltip-line {
width: 1px;
Expand All @@ -41,7 +44,7 @@
opacity: 1;
}

/* Tooltip arrow
/* Tooltip arrow
.chartist-tooltip::before {
content: '\25BC';
position: absolute;
Expand Down
18 changes: 11 additions & 7 deletions BTCPayApp.UI/wwwroot/chartist/chartist-plugin-tooltip.js
Original file line number Diff line number Diff line change
Expand Up @@ -317,14 +317,16 @@
*/
function setTooltipPosition(relativeElement, ignoreClasses) {
containerRect = chart.container.getBoundingClientRect();
var positionData = getTooltipPosition(relativeElement);

var isLine = tooltipElement.innerHTML.match('chartist-tooltip-line');
var positionData = getTooltipPosition(relativeElement, isLine);
tooltipElement.style.transform = 'translate(' + positionData.left + 'px, ' + positionData.top + 'px)';
tooltipElement.style.height = containerRect.height + options.offset.y + 'px';
if (isLine) {
var tooltipRect = tooltipElement.querySelector('.chartist-tooltip-value').getBoundingClientRect();
tooltipElement.style.height = containerRect.height + options.offset.y + options.offset.lineY + tooltipRect.height + 'px';
}

if (ignoreClasses) {
if (ignoreClasses)
return;
}

tooltipElement.classList.remove(options.cssClass + '--right');
tooltipElement.classList.remove(options.cssClass + '--left');
Expand All @@ -336,7 +338,7 @@
* @param Element relativeElement
* @return Object positionData
*/
function getTooltipPosition(relativeElement) {
function getTooltipPosition(relativeElement, isLine) {
var positionData = {
alignment: 'center',
};
Expand All @@ -345,7 +347,9 @@

var boxData = relativeElement.getBoundingClientRect();
var left = boxData.left + window.scrollX + options.offset.x - width / 2 + boxData.width / 2;
var top = containerRect.top + window.scrollY + options.offset.y;
var top = isLine
? containerRect.top + window.scrollY + options.offset.y
: boxData.top + window.scrollY - height + options.offset.y;

// Minimum horizontal collision detection
if (left + width > document.body.clientWidth) {
Expand Down
15 changes: 9 additions & 6 deletions BTCPayApp.UI/wwwroot/js/global.js
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ Interop = {
}

Chart = {
renderLineChart (selector, labels, series, seriesUnit, displayUnit, rate, defaultCurrency, divisibility) {
renderLineChart (selector, labels, series, type, seriesUnit, displayUnit, rate, defaultCurrency, divisibility) {
const $el = document.querySelector(selector);
if (!$el) return;
const valueTransform = (value, fromUnit, toUnit) =>{
Expand All @@ -94,13 +94,15 @@ Chart = {
if (fromUnit === 'SATS' && toUnit === 'BTC') return value / 100000000;
else return value;
}
const labelCount = 6
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/DateTimeFormat
const dateFormatter = new Intl.DateTimeFormat('default', type.toLowerCase() === 'day' ? { hour: 'numeric', minute: 'numeric' } : { month: 'short', day: 'numeric' })
const dateFormatterDetails = new Intl.DateTimeFormat('default', { month: 'short', day: 'numeric', hour: 'numeric', minute: 'numeric' })
Chart.lineChartTooltipValueTransform = (value, label) => {
const date = dateFormatterDetails.format(new Date(label))
const val = valueTransform(value, seriesUnit, displayUnit)
return displayCurrency(val, rate, displayUnit, divisibility) + ' ' + (displayUnit === 'SATS' ? 'sats' : displayUnit)
return `<div class="chartist-tooltip-value-amount">${displayCurrency(val, rate, displayUnit, divisibility) + ' ' + (displayUnit === 'SATS' ? 'sats' : displayUnit)}</div><div class="chartist-tooltip-value-date">${date}</div>`
}
const labelCount = 6
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/DateTimeFormat
const dateFormatter = new Intl.DateTimeFormat('default', { month: 'short', day: 'numeric' })
const min = Math.min(...series);
const max = Math.max(...series);
const low = Math.max(min - ((max - min) / 5), 0);
Expand All @@ -127,7 +129,8 @@ Chart = {
template: '<div class="chartist-tooltip-value">{{value}}</div><div class="chartist-tooltip-line"></div>',
offset: {
x: 0,
y: -16
y: -32,
lineY: -17.5
},
valueTransformFunction(value, label) {
return Chart.lineChartTooltipValueTransform(value, label)
Expand Down

0 comments on commit ac56558

Please sign in to comment.