UI Chart improvements
This commit is contained in:
parent
4cc0888627
commit
0fdecc9e67
12 changed files with 330 additions and 167 deletions
|
|
@ -1,13 +1,15 @@
|
||||||
@page "/Assets"
|
@page "/Assets"
|
||||||
@rendermode InteractiveServer
|
@rendermode InteractiveServer
|
||||||
|
|
||||||
<PageTitle>Ausgaben</PageTitle>
|
<PageTitle>Vermögen</PageTitle>
|
||||||
|
|
||||||
@if (config is not null && layout is not null && data is not null)
|
<FluentCard Style="padding: 16px; max-width: 900px;">
|
||||||
{
|
@if (config is not null && layout is not null && data is not null)
|
||||||
<PlotlyChart style="height: 700px; min-height: 500px" @bind-Config="config" @bind-Layout="layout" @bind-Data="data" @ref="chart" />
|
{
|
||||||
}
|
<PlotlyChart style="height: 560px; min-height: 420px" @bind-Config="config" @bind-Layout="layout" @bind-Data="data" @ref="chart" />
|
||||||
else
|
}
|
||||||
{
|
else
|
||||||
<Loading/>
|
{
|
||||||
}
|
<Loading/>
|
||||||
|
}
|
||||||
|
</FluentCard>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
using Microsoft.AspNetCore.Components;
|
using Microsoft.AspNetCore.Components;
|
||||||
using Plotly.Blazor;
|
using Plotly.Blazor;
|
||||||
using Plotly.Blazor.Traces;
|
using Plotly.Blazor.Traces;
|
||||||
|
using Schaad.Accounting.Datasets.Charts;
|
||||||
using Schaad.Accounting.Interfaces;
|
using Schaad.Accounting.Interfaces;
|
||||||
|
|
||||||
namespace Schaad.Accounting.UI.Components.Pages.Charts;
|
namespace Schaad.Accounting.UI.Components.Pages.Charts;
|
||||||
|
|
@ -17,38 +18,16 @@ public partial class Assets : ComponentBase
|
||||||
|
|
||||||
protected override async Task OnInitializedAsync()
|
protected override async Task OnInitializedAsync()
|
||||||
{
|
{
|
||||||
config = new Config
|
config = ChartTheme.BaseConfig();
|
||||||
{
|
layout = ChartTheme.BaseLayout("Vermögen nach Konto");
|
||||||
Responsive = true
|
|
||||||
};
|
|
||||||
|
|
||||||
layout = new Plotly.Blazor.Layout
|
|
||||||
{
|
|
||||||
};
|
|
||||||
|
|
||||||
var accounts = (await viewService.GetAccountViewListAsync()).Where(a => a.Class == ClassIds.Activa);
|
var accounts = (await viewService.GetAccountViewListAsync()).Where(a => a.Class == ClassIds.Activa);
|
||||||
var values = new List<object>();
|
var totals = accounts
|
||||||
var labels = new List<object>();
|
.Where(a => a.BalanceCHF > 0)
|
||||||
var ids = new List<object>();
|
.Select(a => new CategoryTotal(a.Name, a.BalanceCHF))
|
||||||
foreach (var account in accounts)
|
.ToList();
|
||||||
{
|
|
||||||
if (account.BalanceCHF > 0)
|
|
||||||
{
|
|
||||||
values.Add(account.BalanceCHF);
|
|
||||||
labels.Add(account.Name);
|
|
||||||
ids.Add(account.Id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
data = new List<ITrace>
|
data = new List<ITrace> { ChartTheme.BuildDonutTrace(ChartFormatting.FoldTail(totals)) };
|
||||||
{
|
|
||||||
new Pie
|
|
||||||
{
|
|
||||||
Values = values,
|
|
||||||
Labels = labels,
|
|
||||||
Ids = ids
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
await base.OnInitializedAsync();
|
await base.OnInitializedAsync();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
@if (config is not null && layout is not null && data is not null)
|
@if (config is not null && layout is not null && data is not null)
|
||||||
{
|
{
|
||||||
<PlotlyChart style="height: 400px; min-height: 300px" @bind-Config="config" @bind-Layout="layout" @bind-Data="data" @ref="chart" />
|
<FluentCard Style="padding: 16px;">
|
||||||
|
<PlotlyChart style="height: 400px; min-height: 300px" @bind-Config="config" @bind-Layout="layout" @bind-Data="data" @ref="chart" />
|
||||||
|
</FluentCard>
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -20,25 +20,10 @@ public partial class BookingTextBreakdown : ComponentBase
|
||||||
|
|
||||||
protected override async Task OnInitializedAsync()
|
protected override async Task OnInitializedAsync()
|
||||||
{
|
{
|
||||||
config = new Config
|
config = ChartTheme.BaseConfig();
|
||||||
{
|
layout = ChartTheme.BaseLayout("Nach Buchungstext");
|
||||||
Responsive = true
|
|
||||||
};
|
|
||||||
|
|
||||||
layout = new Plotly.Blazor.Layout
|
|
||||||
{
|
|
||||||
Title = new Plotly.Blazor.LayoutLib.Title { Text = "Nach Buchungstext" }
|
|
||||||
};
|
|
||||||
|
|
||||||
var totals = await chartService.GetBookingTextTotalsAsync(AccountId);
|
var totals = await chartService.GetBookingTextTotalsAsync(AccountId);
|
||||||
data = new List<ITrace>
|
data = new List<ITrace> { ChartTheme.BuildDonutTrace(ChartFormatting.FoldTail(totals)) };
|
||||||
{
|
|
||||||
new Pie
|
|
||||||
{
|
|
||||||
// Values are shown as their absolute magnitude - a pie slice can't represent a signed amount.
|
|
||||||
Values = totals.Select(t => (object)Math.Abs(t.Value)).ToList(),
|
|
||||||
Labels = totals.Select(t => (object)t.Name).ToList()
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
@if (config is not null && layout is not null)
|
@if (config is not null && layout is not null)
|
||||||
{
|
{
|
||||||
<PlotlyChart style="height: 400px; min-height: 300px" @bind-Config="config" @bind-Layout="layout" @bind-Data="data" @ref="chart" />
|
<FluentCard Style="padding: 16px;">
|
||||||
|
<PlotlyChart style="height: 400px; min-height: 300px" @bind-Config="config" @bind-Layout="layout" @bind-Data="data" @ref="chart" />
|
||||||
|
</FluentCard>
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -21,36 +21,34 @@ public partial class BookingTextOverTime : ComponentBase
|
||||||
|
|
||||||
protected override async Task OnInitializedAsync()
|
protected override async Task OnInitializedAsync()
|
||||||
{
|
{
|
||||||
config = new Config
|
config = ChartTheme.BaseConfig();
|
||||||
{
|
|
||||||
Responsive = true
|
|
||||||
};
|
|
||||||
|
|
||||||
layout = new Plotly.Blazor.Layout
|
layout = ChartTheme.BaseLayout("Nach Buchungstext im Verlauf");
|
||||||
|
layout.BarMode = BarModeEnum.Stack;
|
||||||
|
layout.BarCornerRadius = 4;
|
||||||
|
layout.HoverMode = HoverModeEnum.XUnified;
|
||||||
|
layout.XAxis = new List<XAxis>
|
||||||
{
|
{
|
||||||
Title = new Title { Text = "Nach Buchungstext im Verlauf" },
|
new()
|
||||||
BarMode = BarModeEnum.Stack,
|
|
||||||
XAxis = new List<XAxis>
|
|
||||||
{
|
{
|
||||||
new()
|
Type = Plotly.Blazor.LayoutLib.XAxisLib.TypeEnum.Category,
|
||||||
{
|
GridColor = ChartTheme.GridColor,
|
||||||
Type = Plotly.Blazor.LayoutLib.XAxisLib.TypeEnum.Date
|
LineColor = ChartTheme.GridColor
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
layout.YAxis = new List<YAxis>
|
||||||
|
{
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
Title = new Plotly.Blazor.LayoutLib.YAxisLib.Title { Text = "CHF" },
|
||||||
|
GridColor = ChartTheme.GridColor,
|
||||||
|
ZeroLineColor = ChartTheme.GridColor,
|
||||||
|
SeparateThousands = true
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
var newData = new List<ITrace>();
|
|
||||||
var dataSeries = await chartService.GetBookingTextExpensesPerMonthAsync(AccountId);
|
var dataSeries = await chartService.GetBookingTextExpensesPerMonthAsync(AccountId);
|
||||||
foreach (var dataSerie in dataSeries)
|
data = ChartTheme.BuildStackedBarTraces(ChartFormatting.FoldTail(dataSeries));
|
||||||
{
|
|
||||||
newData.Add(new Bar
|
|
||||||
{
|
|
||||||
Name = dataSerie.Name,
|
|
||||||
X = dataSerie.X.Select(d => (object)d).ToList(),
|
|
||||||
Y = dataSerie.Y.Select(d => (object)d).ToList(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
data = newData;
|
|
||||||
|
|
||||||
await base.OnInitializedAsync();
|
await base.OnInitializedAsync();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,72 @@
|
||||||
|
using Schaad.Accounting;
|
||||||
|
using Schaad.Accounting.Datasets.Charts;
|
||||||
|
|
||||||
|
namespace Schaad.Accounting.UI.Components.Pages.Charts;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Swiss (de-CH) formatting for chart labels/tooltips, plus the top-N + "Andere"
|
||||||
|
/// folding used to keep pies and stacked series legible. Plotly's own d3-format
|
||||||
|
/// tokens can't produce the apostrophe thousands separator, so values are
|
||||||
|
/// pre-formatted here in C# and passed to Plotly as literal text.
|
||||||
|
/// </summary>
|
||||||
|
public static class ChartFormatting
|
||||||
|
{
|
||||||
|
private static readonly string[] MonthAbbreviations =
|
||||||
|
[
|
||||||
|
"Jan", "Feb", "Mrz", "Apr", "Mai", "Jun", "Jul", "Aug", "Sep", "Okt", "Nov", "Dez"
|
||||||
|
];
|
||||||
|
|
||||||
|
public static string MonthLabel(DateOnly date) => $"{MonthAbbreviations[date.Month - 1]} {date.Year % 100:00}";
|
||||||
|
|
||||||
|
public static string Chf(decimal value) => $"{value.ToFormattedString()} CHF";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sorts by magnitude descending and, past <see cref="ChartTheme.MaxCategories"/>,
|
||||||
|
/// collapses the remainder into a single "Andere" entry so a pie/legend never
|
||||||
|
/// has to carry more slices than the categorical palette can distinguish.
|
||||||
|
/// </summary>
|
||||||
|
public static IReadOnlyList<CategoryTotal> FoldTail(IReadOnlyList<CategoryTotal> items)
|
||||||
|
{
|
||||||
|
var sorted = items.OrderByDescending(i => Math.Abs(i.Value)).ToList();
|
||||||
|
if (sorted.Count <= ChartTheme.MaxCategories)
|
||||||
|
{
|
||||||
|
return sorted;
|
||||||
|
}
|
||||||
|
|
||||||
|
var head = sorted.Take(ChartTheme.MaxCategories - 1).ToList();
|
||||||
|
var tailTotal = sorted.Skip(ChartTheme.MaxCategories - 1).Sum(i => i.Value);
|
||||||
|
head.Add(new CategoryTotal(ChartTheme.OtherLabel, tailTotal));
|
||||||
|
return head;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Same idea as <see cref="FoldTail(IReadOnlyList{CategoryTotal})"/> for a set of
|
||||||
|
/// monthly series (e.g. a stacked bar chart): series are ranked by total magnitude
|
||||||
|
/// across all months, and everything past the cap is summed month-by-month into
|
||||||
|
/// a single "Andere" series.
|
||||||
|
/// </summary>
|
||||||
|
public static IReadOnlyList<DataSerie> FoldTail(IReadOnlyList<DataSerie> series)
|
||||||
|
{
|
||||||
|
if (series.Count <= ChartTheme.MaxCategories)
|
||||||
|
{
|
||||||
|
return series;
|
||||||
|
}
|
||||||
|
|
||||||
|
var ranked = series.OrderByDescending(s => s.Y.Sum(Math.Abs)).ToList();
|
||||||
|
var head = ranked.Take(ChartTheme.MaxCategories - 1).ToList();
|
||||||
|
var tail = ranked.Skip(ChartTheme.MaxCategories - 1).ToList();
|
||||||
|
|
||||||
|
var x = tail[0].X;
|
||||||
|
var otherY = new decimal[x.Count];
|
||||||
|
foreach (var serie in tail)
|
||||||
|
{
|
||||||
|
for (var i = 0; i < x.Count; i++)
|
||||||
|
{
|
||||||
|
otherY[i] += serie.Y[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
head.Add(new DataSerie(ChartTheme.OtherLabel, ChartTheme.OtherLabel, x, otherY));
|
||||||
|
return head;
|
||||||
|
}
|
||||||
|
}
|
||||||
144
Schaad.Accounting.UI/Components/Pages/Charts/ChartTheme.cs
Normal file
144
Schaad.Accounting.UI/Components/Pages/Charts/ChartTheme.cs
Normal file
|
|
@ -0,0 +1,144 @@
|
||||||
|
using Plotly.Blazor;
|
||||||
|
using Plotly.Blazor.LayoutLib;
|
||||||
|
using Plotly.Blazor.Traces;
|
||||||
|
using Schaad.Accounting.Datasets.Charts;
|
||||||
|
|
||||||
|
namespace Schaad.Accounting.UI.Components.Pages.Charts;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Shared look for every chart: a fixed-order, colour-vision-safe categorical
|
||||||
|
/// palette plus Fluent-matching chrome. The palette is the validated default
|
||||||
|
/// from the project's data-viz guidelines (scripts/validate_palette.js) - do
|
||||||
|
/// not add or reorder hues without re-running the validator.
|
||||||
|
/// </summary>
|
||||||
|
public static class ChartTheme
|
||||||
|
{
|
||||||
|
public static readonly string[] Categorical =
|
||||||
|
[
|
||||||
|
"#2a78d6", // blue
|
||||||
|
"#eb6834", // orange
|
||||||
|
"#1baf7a", // aqua
|
||||||
|
"#eda100", // yellow
|
||||||
|
"#e87ba4", // magenta
|
||||||
|
"#008300", // green
|
||||||
|
"#4a3aa7", // violet
|
||||||
|
"#e34948" // red
|
||||||
|
];
|
||||||
|
|
||||||
|
/// <summary>Beyond this many categories, fold the tail into a single "Andere" bucket.</summary>
|
||||||
|
public const int MaxCategories = 7;
|
||||||
|
|
||||||
|
public const string OtherLabel = "Andere";
|
||||||
|
|
||||||
|
// Muted ink, not a series hue - "Andere" must never look like a real category.
|
||||||
|
public const string OtherColor = "#898781";
|
||||||
|
|
||||||
|
public const string GridColor = "#e1e0d9";
|
||||||
|
public const string PrimaryInk = "#0b0b0b";
|
||||||
|
public const string MutedInk = "#52514e";
|
||||||
|
public const string PlotSurface = "#fcfcfb";
|
||||||
|
|
||||||
|
public const string FontFamily = "'Helvetica Neue', Helvetica, Arial, sans-serif";
|
||||||
|
|
||||||
|
public static string ColorAt(int index) => index < Categorical.Length ? Categorical[index] : OtherColor;
|
||||||
|
|
||||||
|
public static Config BaseConfig() => new()
|
||||||
|
{
|
||||||
|
Responsive = true,
|
||||||
|
DisplayLogo = false
|
||||||
|
};
|
||||||
|
|
||||||
|
public static Plotly.Blazor.Layout BaseLayout(string? title = null)
|
||||||
|
{
|
||||||
|
var layout = new Plotly.Blazor.Layout
|
||||||
|
{
|
||||||
|
Font = new Font { Family = FontFamily, Color = PrimaryInk, Size = 12 },
|
||||||
|
PaperBgColor = PlotSurface,
|
||||||
|
PlotBgColor = PlotSurface,
|
||||||
|
Margin = new Margin { L = 48, R = 24, T = string.IsNullOrEmpty(title) ? 16 : 48, B = 40, Pad = 4 },
|
||||||
|
Legend = new List<Legend>
|
||||||
|
{
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
Font = new() { Family = FontFamily, Color = MutedInk, Size = 12 },
|
||||||
|
BgColor = "rgba(0,0,0,0)"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
HoverLabel = new()
|
||||||
|
{
|
||||||
|
BgColor = "#ffffff",
|
||||||
|
BorderColor = GridColor,
|
||||||
|
Font = new() { Family = FontFamily, Color = PrimaryInk, Size = 12 }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(title))
|
||||||
|
{
|
||||||
|
layout.Title = new Title { Text = title, Font = new() { Family = FontFamily, Color = PrimaryInk, Size = 15 } };
|
||||||
|
}
|
||||||
|
|
||||||
|
return layout;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A donut trace for a set of already-folded (<see cref="ChartFormatting.FoldTail(IReadOnlyList{CategoryTotal})"/>)
|
||||||
|
/// categories: fixed-order palette, CH-formatted hover text, and direct labels
|
||||||
|
/// outside the slice (values are magnitudes - a slice can't represent a sign).
|
||||||
|
/// </summary>
|
||||||
|
public static Pie BuildDonutTrace(IReadOnlyList<CategoryTotal> items)
|
||||||
|
{
|
||||||
|
var labels = new List<object>();
|
||||||
|
var values = new List<object>();
|
||||||
|
var text = new List<object>();
|
||||||
|
var colors = new List<object>();
|
||||||
|
|
||||||
|
for (var i = 0; i < items.Count; i++)
|
||||||
|
{
|
||||||
|
var item = items[i];
|
||||||
|
labels.Add(item.Name);
|
||||||
|
values.Add(Math.Abs(item.Value));
|
||||||
|
text.Add(ChartFormatting.Chf(item.Value));
|
||||||
|
colors.Add(item.Name == OtherLabel ? OtherColor : ColorAt(i));
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Pie
|
||||||
|
{
|
||||||
|
Labels = labels,
|
||||||
|
Values = values,
|
||||||
|
Text = text,
|
||||||
|
Sort = false,
|
||||||
|
Hole = 0.55m,
|
||||||
|
Marker = new() { Colors = colors, Line = new() { Color = PlotSurface, Width = 2 } },
|
||||||
|
TextInfo = Plotly.Blazor.Traces.PieLib.TextInfoFlag.Label,
|
||||||
|
TextPosition = Plotly.Blazor.Traces.PieLib.TextPositionEnum.Outside,
|
||||||
|
OutsideTextFont = new() { Family = FontFamily, Color = MutedInk, Size = 12 },
|
||||||
|
HoverTemplate = "<b>%{label}</b><br>%{text}<extra></extra>"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// One stacked-bar trace per already-folded (<see cref="ChartFormatting.FoldTail(IReadOnlyList{DataSerie})"/>)
|
||||||
|
/// series: fixed-order palette, a hairline surface gap between stacked segments,
|
||||||
|
/// CH month labels on the x-axis, and CH-formatted hover text.
|
||||||
|
/// </summary>
|
||||||
|
public static List<ITrace> BuildStackedBarTraces(IReadOnlyList<DataSerie> series)
|
||||||
|
{
|
||||||
|
var traces = new List<ITrace>();
|
||||||
|
for (var i = 0; i < series.Count; i++)
|
||||||
|
{
|
||||||
|
var serie = series[i];
|
||||||
|
var color = serie.Name == OtherLabel ? OtherColor : ColorAt(i);
|
||||||
|
|
||||||
|
traces.Add(new Bar
|
||||||
|
{
|
||||||
|
Name = serie.Name,
|
||||||
|
X = serie.X.Select(d => (object)ChartFormatting.MonthLabel(d)).ToList(),
|
||||||
|
Y = serie.Y.Select(v => (object)v).ToList(),
|
||||||
|
TextArray = serie.Y.Select(ChartFormatting.Chf).ToList(),
|
||||||
|
Marker = new() { Color = color, Line = new() { Color = PlotSurface, Width = 2 } },
|
||||||
|
HoverTemplate = "%{fullData.name}: %{text}<extra></extra>"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return traces;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,13 +1,15 @@
|
||||||
@page "/spendings"
|
@page "/spendings"
|
||||||
@rendermode InteractiveServer
|
@rendermode InteractiveServer
|
||||||
|
|
||||||
<PageTitle>Ausgaben</PageTitle>
|
<PageTitle>Ausgaben</PageTitle>
|
||||||
|
|
||||||
@if (config is not null && layout is not null && data is not null)
|
<FluentCard Style="padding: 16px; max-width: 900px;">
|
||||||
{
|
@if (config is not null && layout is not null && data is not null)
|
||||||
<PlotlyChart style="height: 700px; min-height: 500px" @bind-Config="config" @bind-Layout="layout" @bind-Data="data" @ref="chart" />
|
{
|
||||||
}
|
<PlotlyChart style="height: 560px; min-height: 420px" @bind-Config="config" @bind-Layout="layout" @bind-Data="data" @ref="chart" />
|
||||||
else
|
}
|
||||||
{
|
else
|
||||||
<Loading/>
|
{
|
||||||
}
|
<Loading/>
|
||||||
|
}
|
||||||
|
</FluentCard>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
using Microsoft.AspNetCore.Components;
|
using Microsoft.AspNetCore.Components;
|
||||||
using Plotly.Blazor;
|
using Plotly.Blazor;
|
||||||
using Plotly.Blazor.Traces;
|
using Plotly.Blazor.Traces;
|
||||||
|
using Schaad.Accounting.Datasets.Charts;
|
||||||
using Schaad.Accounting.Interfaces;
|
using Schaad.Accounting.Interfaces;
|
||||||
|
|
||||||
namespace Schaad.Accounting.UI.Components.Pages.Charts;
|
namespace Schaad.Accounting.UI.Components.Pages.Charts;
|
||||||
|
|
@ -17,35 +18,17 @@ public partial class Spendings : ComponentBase
|
||||||
|
|
||||||
protected override async Task OnInitializedAsync()
|
protected override async Task OnInitializedAsync()
|
||||||
{
|
{
|
||||||
config = new Config
|
config = ChartTheme.BaseConfig();
|
||||||
{
|
layout = ChartTheme.BaseLayout("Ausgaben nach Kategorie");
|
||||||
Responsive = true
|
|
||||||
};
|
|
||||||
|
|
||||||
layout = new Plotly.Blazor.Layout
|
|
||||||
{
|
|
||||||
};
|
|
||||||
|
|
||||||
var accounts = (await viewService.GetAccountViewListAsync()).Where(a => a.Class == ClassIds.Expenses);
|
var accounts = (await viewService.GetAccountViewListAsync()).Where(a => a.Class == ClassIds.Expenses);
|
||||||
var values = new List<object>();
|
var totals = accounts
|
||||||
var labels = new List<object>();
|
.GroupBy(a => a.SubClass)
|
||||||
foreach (var grp in accounts.GroupBy(a => a.SubClass).Select(a => new {Key = a.Key, List = a.ToList()}))
|
.Select(g => new CategoryTotal(g.First().SubClassName, g.Sum(a => a.Balance)))
|
||||||
{
|
.Where(t => t.Value != 0)
|
||||||
if (grp.List.Sum(g => g.Balance) != 0)
|
.ToList();
|
||||||
{
|
|
||||||
values.Add(grp.List.Sum(g => g.Balance));
|
|
||||||
labels.Add(grp.List.First().SubClassName);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
data = new List<ITrace>
|
data = new List<ITrace> { ChartTheme.BuildDonutTrace(ChartFormatting.FoldTail(totals)) };
|
||||||
{
|
|
||||||
new Pie
|
|
||||||
{
|
|
||||||
Values =values,
|
|
||||||
Labels = labels
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
await base.OnInitializedAsync();
|
await base.OnInitializedAsync();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,15 @@
|
||||||
@page "/SpendingsOverTime"
|
@page "/SpendingsOverTime"
|
||||||
@rendermode InteractiveServer
|
@rendermode InteractiveServer
|
||||||
|
|
||||||
<PageTitle>Ausgaben im Verlauf</PageTitle>
|
<PageTitle>Ausgaben im Verlauf</PageTitle>
|
||||||
|
|
||||||
@if (config is not null && layout is not null)
|
<FluentCard Style="padding: 16px;">
|
||||||
{
|
@if (config is not null && layout is not null)
|
||||||
<PlotlyChart style="height: 700px; min-height: 500px" @bind-Config="config" @bind-Layout="layout" @bind-Data="data" @ref="chart" />
|
{
|
||||||
}
|
<PlotlyChart style="height: 560px; min-height: 420px" @bind-Config="config" @bind-Layout="layout" @bind-Data="data" @ref="chart" />
|
||||||
else
|
}
|
||||||
{
|
else
|
||||||
<Loading/>
|
{
|
||||||
}
|
<Loading/>
|
||||||
|
}
|
||||||
|
</FluentCard>
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
using Microsoft.AspNetCore.Components;
|
using Microsoft.AspNetCore.Components;
|
||||||
using Plotly.Blazor;
|
using Plotly.Blazor;
|
||||||
using Plotly.Blazor.LayoutLib;
|
using Plotly.Blazor.LayoutLib;
|
||||||
using Plotly.Blazor.Traces;
|
using Plotly.Blazor.Traces;
|
||||||
|
|
@ -18,42 +18,34 @@ public partial class SpendingsOverTime : ComponentBase
|
||||||
|
|
||||||
protected override async Task OnInitializedAsync()
|
protected override async Task OnInitializedAsync()
|
||||||
{
|
{
|
||||||
config = new Config
|
config = ChartTheme.BaseConfig();
|
||||||
{
|
|
||||||
Responsive = true
|
|
||||||
};
|
|
||||||
|
|
||||||
layout = new Plotly.Blazor.Layout
|
layout = ChartTheme.BaseLayout("Ausgaben im Verlauf");
|
||||||
|
layout.BarMode = BarModeEnum.Stack;
|
||||||
|
layout.BarCornerRadius = 4;
|
||||||
|
layout.HoverMode = HoverModeEnum.XUnified;
|
||||||
|
layout.XAxis = new List<XAxis>
|
||||||
{
|
{
|
||||||
Title = new Title { Text = "Ausgaben im Verlauf" },
|
new()
|
||||||
BarMode = BarModeEnum.Stack,
|
|
||||||
YAxis = new List<YAxis>
|
|
||||||
{
|
{
|
||||||
new()
|
Type = Plotly.Blazor.LayoutLib.XAxisLib.TypeEnum.Category,
|
||||||
{
|
GridColor = ChartTheme.GridColor,
|
||||||
Title = new Plotly.Blazor.LayoutLib.YAxisLib.Title { Text = "CHF" }
|
LineColor = ChartTheme.GridColor
|
||||||
}
|
}
|
||||||
},
|
};
|
||||||
XAxis = new List<XAxis>
|
layout.YAxis = new List<YAxis>
|
||||||
|
{
|
||||||
|
new()
|
||||||
{
|
{
|
||||||
new()
|
Title = new Plotly.Blazor.LayoutLib.YAxisLib.Title { Text = "CHF" },
|
||||||
{
|
GridColor = ChartTheme.GridColor,
|
||||||
Type = Plotly.Blazor.LayoutLib.XAxisLib.TypeEnum.Date
|
ZeroLineColor = ChartTheme.GridColor,
|
||||||
}
|
SeparateThousands = true
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
var dataSeries = await chartService.GetExpensesPerMonthAsync();
|
var dataSeries = await chartService.GetExpensesPerMonthAsync();
|
||||||
foreach (var dataSerie in dataSeries)
|
data = ChartTheme.BuildStackedBarTraces(ChartFormatting.FoldTail(dataSeries));
|
||||||
{
|
|
||||||
data.Add(new Bar
|
|
||||||
{
|
|
||||||
Name = dataSerie.Name,
|
|
||||||
X = dataSerie.X.Select(d => (object)d).ToList(),
|
|
||||||
Y = dataSerie.Y.Select(d => (object)d).ToList(),
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
await base.OnInitializedAsync();
|
await base.OnInitializedAsync();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue