using Plotly.Blazor; using Plotly.Blazor.LayoutLib; using Plotly.Blazor.Traces; using Schaad.Accounting.Datasets.Charts; namespace Schaad.Accounting.UI.Components.Pages.Charts; /// /// 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. /// 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 ]; /// Beyond this many categories, fold the tail into a single "Andere" bucket. 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 { 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; } /// /// A donut trace for a set of already-folded () /// categories: fixed-order palette, CH-formatted hover text, and direct labels /// outside the slice (values are magnitudes - a slice can't represent a sign). /// public static Pie BuildDonutTrace(IReadOnlyList items) { var labels = new List(); var values = new List(); var text = new List(); var colors = new List(); 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 = "%{label}
%{text}" }; } /// /// One stacked-bar trace per already-folded () /// series: fixed-order palette, a hairline surface gap between stacked segments, /// CH month labels on the x-axis, and CH-formatted hover text. /// public static List BuildStackedBarTraces(IReadOnlyList series) { var traces = new List(); 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}" }); } return traces; } }