File size: 3,815 Bytes
3dd5c9a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
@using TaskTrackingSystem.WebApp.Components.Shared.Charts

<div class="@WrapperClass">
    @if (!Items.Any() || Items.Sum(item => item.Value) <= 0)
    {
        <div class="flex h-56 items-center justify-center text-sm text-slate-400">@EmptyText</div>
    }
    else
    {
        <div class="flex justify-center">
            <svg viewBox="0 0 190 190" class="h-56 w-56" role="img" aria-label="Task distribution pie chart">
                @foreach (var slice in Slices)
                {
                    if (slice.IsFullCircle)
                    {
                        <circle cx="95" cy="95" r="72" fill="@slice.Color" stroke="var(--surface)" stroke-width="2" />
                    }
                    else
                    {
                        <path d="@slice.Path" fill="@slice.Color" stroke="var(--surface)" stroke-width="2" stroke-linejoin="round" />
                    }

                }
            </svg>
        </div>

        @if (ShowLegend)
        {
            <div class="mt-3 space-y-3">
                @foreach (var item in Items)
                {
                    <div class="flex items-center justify-between rounded-2xl border border-slate-200 bg-slate-50 px-4 py-3">
                        <div class="flex items-center gap-3">
                            <span class="h-3 w-3 rounded-full" style="background-color: @item.Color"></span>
                            <span class="text-sm text-slate-600">@item.Label</span>
                        </div>
                        <span class="text-sm font-semibold text-slate-900">@AppLocalization.Number(item.Value, ValueFormat)</span>
                    </div>
                }
            </div>
        }
    }
</div>

@code {
    [Parameter] public IReadOnlyList<ChartDataPoint> Items { get; set; } = Array.Empty<ChartDataPoint>();
    [Parameter] public bool ShowLegend { get; set; } = true;
    [Parameter] public string EmptyText { get; set; } = AppLocalization.Text("report.noChartData", "No data available.");
    [Parameter] public string ValueFormat { get; set; } = "0";
    [Parameter] public string WrapperClass { get; set; } = string.Empty;

    private IReadOnlyList<Slice> Slices => BuildSlices();

    private IReadOnlyList<Slice> BuildSlices()
    {
        const double center = 95;
        const double radius = 72;
        var total = Items.Sum(item => Math.Max(0, item.Value));
        var startAngle = -90.0;
        var slices = new List<Slice>();

        foreach (var item in Items)
        {
            var value = Math.Max(0, item.Value);
            if (value <= 0 || total <= 0)
            {
                continue;
            }

            var sweep = 360 * value / total;
            var endAngle = startAngle + sweep;

            if (sweep >= 359.999)
            {
                slices.Add(new Slice(item.Color, string.Empty, true));
                startAngle = endAngle;
                continue;
            }

            var start = PointOnCircle(center, center, radius, startAngle);
            var end = PointOnCircle(center, center, radius, endAngle);
            var largeArc = sweep > 180 ? 1 : 0;
            var path = $"M {center:0.##} {center:0.##} L {start.X:0.##} {start.Y:0.##} A {radius:0.##} {radius:0.##} 0 {largeArc} 1 {end.X:0.##} {end.Y:0.##} Z";

            slices.Add(new Slice(item.Color, path, false));

            startAngle = endAngle;
        }

        return slices;
    }

    private static (double X, double Y) PointOnCircle(double centerX, double centerY, double radius, double angleDegrees)
    {
        var radians = angleDegrees * Math.PI / 180;
        return (centerX + radius * Math.Cos(radians), centerY + radius * Math.Sin(radians));
    }

    private sealed record Slice(string Color, string Path, bool IsFullCircle);
}