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
|
||||
|
||||
<PageTitle>Ausgaben</PageTitle>
|
||||
<PageTitle>Vermögen</PageTitle>
|
||||
|
||||
@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" />
|
||||
}
|
||||
else
|
||||
{
|
||||
<Loading/>
|
||||
}
|
||||
<FluentCard Style="padding: 16px; max-width: 900px;">
|
||||
@if (config is not null && layout is not null && data is not null)
|
||||
{
|
||||
<PlotlyChart style="height: 560px; min-height: 420px" @bind-Config="config" @bind-Layout="layout" @bind-Data="data" @ref="chart" />
|
||||
}
|
||||
else
|
||||
{
|
||||
<Loading/>
|
||||
}
|
||||
</FluentCard>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
using Microsoft.AspNetCore.Components;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using Plotly.Blazor;
|
||||
using Plotly.Blazor.Traces;
|
||||
using Schaad.Accounting.Datasets.Charts;
|
||||
using Schaad.Accounting.Interfaces;
|
||||
|
||||
namespace Schaad.Accounting.UI.Components.Pages.Charts;
|
||||
|
|
@ -17,38 +18,16 @@ public partial class Assets : ComponentBase
|
|||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
config = new Config
|
||||
{
|
||||
Responsive = true
|
||||
};
|
||||
|
||||
layout = new Plotly.Blazor.Layout
|
||||
{
|
||||
};
|
||||
config = ChartTheme.BaseConfig();
|
||||
layout = ChartTheme.BaseLayout("Vermögen nach Konto");
|
||||
|
||||
var accounts = (await viewService.GetAccountViewListAsync()).Where(a => a.Class == ClassIds.Activa);
|
||||
var values = new List<object>();
|
||||
var labels = new List<object>();
|
||||
var ids = new List<object>();
|
||||
foreach (var account in accounts)
|
||||
{
|
||||
if (account.BalanceCHF > 0)
|
||||
{
|
||||
values.Add(account.BalanceCHF);
|
||||
labels.Add(account.Name);
|
||||
ids.Add(account.Id);
|
||||
}
|
||||
}
|
||||
var totals = accounts
|
||||
.Where(a => a.BalanceCHF > 0)
|
||||
.Select(a => new CategoryTotal(a.Name, a.BalanceCHF))
|
||||
.ToList();
|
||||
|
||||
data = new List<ITrace>
|
||||
{
|
||||
new Pie
|
||||
{
|
||||
Values = values,
|
||||
Labels = labels,
|
||||
Ids = ids
|
||||
}
|
||||
};
|
||||
data = new List<ITrace> { ChartTheme.BuildDonutTrace(ChartFormatting.FoldTail(totals)) };
|
||||
|
||||
await base.OnInitializedAsync();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
@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
|
||||
{
|
||||
|
|
|
|||
|
|
@ -20,25 +20,10 @@ public partial class BookingTextBreakdown : ComponentBase
|
|||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
config = new Config
|
||||
{
|
||||
Responsive = true
|
||||
};
|
||||
|
||||
layout = new Plotly.Blazor.Layout
|
||||
{
|
||||
Title = new Plotly.Blazor.LayoutLib.Title { Text = "Nach Buchungstext" }
|
||||
};
|
||||
config = ChartTheme.BaseConfig();
|
||||
layout = ChartTheme.BaseLayout("Nach Buchungstext");
|
||||
|
||||
var totals = await chartService.GetBookingTextTotalsAsync(AccountId);
|
||||
data = new List<ITrace>
|
||||
{
|
||||
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()
|
||||
}
|
||||
};
|
||||
data = new List<ITrace> { ChartTheme.BuildDonutTrace(ChartFormatting.FoldTail(totals)) };
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
@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
|
||||
{
|
||||
|
|
|
|||
|
|
@ -21,36 +21,34 @@ public partial class BookingTextOverTime : ComponentBase
|
|||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
config = new Config
|
||||
{
|
||||
Responsive = true
|
||||
};
|
||||
config = ChartTheme.BaseConfig();
|
||||
|
||||
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" },
|
||||
BarMode = BarModeEnum.Stack,
|
||||
XAxis = new List<XAxis>
|
||||
new()
|
||||
{
|
||||
new()
|
||||
{
|
||||
Type = Plotly.Blazor.LayoutLib.XAxisLib.TypeEnum.Date
|
||||
}
|
||||
Type = Plotly.Blazor.LayoutLib.XAxisLib.TypeEnum.Category,
|
||||
GridColor = ChartTheme.GridColor,
|
||||
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);
|
||||
foreach (var dataSerie in 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;
|
||||
data = ChartTheme.BuildStackedBarTraces(ChartFormatting.FoldTail(dataSeries));
|
||||
|
||||
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
|
||||
|
||||
<PageTitle>Ausgaben</PageTitle>
|
||||
|
||||
@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" />
|
||||
}
|
||||
else
|
||||
{
|
||||
<Loading/>
|
||||
}
|
||||
<FluentCard Style="padding: 16px; max-width: 900px;">
|
||||
@if (config is not null && layout is not null && data is not null)
|
||||
{
|
||||
<PlotlyChart style="height: 560px; min-height: 420px" @bind-Config="config" @bind-Layout="layout" @bind-Data="data" @ref="chart" />
|
||||
}
|
||||
else
|
||||
{
|
||||
<Loading/>
|
||||
}
|
||||
</FluentCard>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
using Microsoft.AspNetCore.Components;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using Plotly.Blazor;
|
||||
using Plotly.Blazor.Traces;
|
||||
using Schaad.Accounting.Datasets.Charts;
|
||||
using Schaad.Accounting.Interfaces;
|
||||
|
||||
namespace Schaad.Accounting.UI.Components.Pages.Charts;
|
||||
|
|
@ -17,35 +18,17 @@ public partial class Spendings : ComponentBase
|
|||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
config = new Config
|
||||
{
|
||||
Responsive = true
|
||||
};
|
||||
|
||||
layout = new Plotly.Blazor.Layout
|
||||
{
|
||||
};
|
||||
config = ChartTheme.BaseConfig();
|
||||
layout = ChartTheme.BaseLayout("Ausgaben nach Kategorie");
|
||||
|
||||
var accounts = (await viewService.GetAccountViewListAsync()).Where(a => a.Class == ClassIds.Expenses);
|
||||
var values = new List<object>();
|
||||
var labels = new List<object>();
|
||||
foreach (var grp in accounts.GroupBy(a => a.SubClass).Select(a => new {Key = a.Key, List = a.ToList()}))
|
||||
{
|
||||
if (grp.List.Sum(g => g.Balance) != 0)
|
||||
{
|
||||
values.Add(grp.List.Sum(g => g.Balance));
|
||||
labels.Add(grp.List.First().SubClassName);
|
||||
}
|
||||
}
|
||||
var totals = accounts
|
||||
.GroupBy(a => a.SubClass)
|
||||
.Select(g => new CategoryTotal(g.First().SubClassName, g.Sum(a => a.Balance)))
|
||||
.Where(t => t.Value != 0)
|
||||
.ToList();
|
||||
|
||||
data = new List<ITrace>
|
||||
{
|
||||
new Pie
|
||||
{
|
||||
Values =values,
|
||||
Labels = labels
|
||||
}
|
||||
};
|
||||
data = new List<ITrace> { ChartTheme.BuildDonutTrace(ChartFormatting.FoldTail(totals)) };
|
||||
|
||||
await base.OnInitializedAsync();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,13 +1,15 @@
|
|||
@page "/SpendingsOverTime"
|
||||
@page "/SpendingsOverTime"
|
||||
@rendermode InteractiveServer
|
||||
|
||||
<PageTitle>Ausgaben im Verlauf</PageTitle>
|
||||
|
||||
@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" />
|
||||
}
|
||||
else
|
||||
{
|
||||
<Loading/>
|
||||
}
|
||||
<FluentCard Style="padding: 16px;">
|
||||
@if (config is not null && layout is not null)
|
||||
{
|
||||
<PlotlyChart style="height: 560px; min-height: 420px" @bind-Config="config" @bind-Layout="layout" @bind-Data="data" @ref="chart" />
|
||||
}
|
||||
else
|
||||
{
|
||||
<Loading/>
|
||||
}
|
||||
</FluentCard>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
using Microsoft.AspNetCore.Components;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using Plotly.Blazor;
|
||||
using Plotly.Blazor.LayoutLib;
|
||||
using Plotly.Blazor.Traces;
|
||||
|
|
@ -18,42 +18,34 @@ public partial class SpendingsOverTime : ComponentBase
|
|||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
config = new Config
|
||||
{
|
||||
Responsive = true
|
||||
};
|
||||
config = ChartTheme.BaseConfig();
|
||||
|
||||
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" },
|
||||
BarMode = BarModeEnum.Stack,
|
||||
YAxis = new List<YAxis>
|
||||
new()
|
||||
{
|
||||
new()
|
||||
{
|
||||
Title = new Plotly.Blazor.LayoutLib.YAxisLib.Title { Text = "CHF" }
|
||||
}
|
||||
},
|
||||
XAxis = new List<XAxis>
|
||||
Type = Plotly.Blazor.LayoutLib.XAxisLib.TypeEnum.Category,
|
||||
GridColor = ChartTheme.GridColor,
|
||||
LineColor = ChartTheme.GridColor
|
||||
}
|
||||
};
|
||||
layout.YAxis = new List<YAxis>
|
||||
{
|
||||
new()
|
||||
{
|
||||
new()
|
||||
{
|
||||
Type = Plotly.Blazor.LayoutLib.XAxisLib.TypeEnum.Date
|
||||
}
|
||||
Title = new Plotly.Blazor.LayoutLib.YAxisLib.Title { Text = "CHF" },
|
||||
GridColor = ChartTheme.GridColor,
|
||||
ZeroLineColor = ChartTheme.GridColor,
|
||||
SeparateThousands = true
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
var dataSeries = await chartService.GetExpensesPerMonthAsync();
|
||||
foreach (var dataSerie in dataSeries)
|
||||
{
|
||||
data.Add(new Bar
|
||||
{
|
||||
Name = dataSerie.Name,
|
||||
X = dataSerie.X.Select(d => (object)d).ToList(),
|
||||
Y = dataSerie.Y.Select(d => (object)d).ToList(),
|
||||
});
|
||||
};
|
||||
data = ChartTheme.BuildStackedBarTraces(ChartFormatting.FoldTail(dataSeries));
|
||||
|
||||
await base.OnInitializedAsync();
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue