AccountingNext/Schaad.Accounting.UI/Components/Pages/Charts/ChartTheme.cs

144 lines
5.3 KiB
C#

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;
}
}