File size: 3,454 Bytes
917b070
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
@using TaskTrackingSystem.WebApp.Components.Shared.Charts

<div class="@WrapperClass">
    @if (!Items.Any())
    {
        <div class="flex items-center justify-center py-16 text-sm text-slate-400">@EmptyText</div>
    }
    else
    {
        <div class="min-w-[760px] space-y-3">
            <div class="grid gap-2 text-xs font-semibold uppercase tracking-wider text-slate-400" style="@GridStyle">
                @foreach (var month in Months)
                {
                    <span>@month.ToString("MMM")</span>
                }
            </div>

            @foreach (var item in Bars)
            {
                <div class="grid items-center gap-3" style="grid-template-columns: 180px minmax(0, 1fr);">
                    <div class="truncate text-sm font-semibold text-slate-700">@item.Label</div>
                    <div class="relative h-8 rounded-full bg-slate-100">
                        <div class="absolute inset-y-1 rounded-full" style="left: @item.Left%; width: @item.Width%; background-color: @item.Color;">
                            <div class="h-full rounded-full bg-white/25" style="width: @item.CompletionPercentage%;"></div>
                        </div>
                    </div>
                </div>
            }
        </div>
    }
</div>

@code {
    [Parameter] public IReadOnlyList<TimelineChartItem> Items { get; set; } = Array.Empty<TimelineChartItem>();
    [Parameter] public string EmptyText { get; set; } = "No timeline data available.";
    [Parameter] public string WrapperClass { get; set; } = "overflow-auto";

    private IReadOnlyList<DateTime> Months => BuildMonths();
    private string GridStyle => $"grid-template-columns: 180px repeat({Math.Max(Months.Count, 1)}, minmax(56px, 1fr));";
    private IReadOnlyList<TimelineBar> Bars => BuildBars();

    private IReadOnlyList<DateTime> BuildMonths()
    {
        if (!Items.Any())
        {
            return Array.Empty<DateTime>();
        }

        var start = new DateTime(Items.Min(item => item.StartDate).Year, Items.Min(item => item.StartDate).Month, 1);
        var end = new DateTime(Items.Max(item => item.EndDate).Year, Items.Max(item => item.EndDate).Month, 1);
        var months = new List<DateTime>();

        for (var current = start; current <= end; current = current.AddMonths(1))
        {
            months.Add(current);
        }

        return months;
    }

    private IReadOnlyList<TimelineBar> BuildBars()
    {
        if (!Months.Any())
        {
            return Array.Empty<TimelineBar>();
        }

        var first = Months.First();
        var slots = Months.Count;

        return Items.Select(item =>
        {
            var startIndex = ((item.StartDate.Year - first.Year) * 12) + item.StartDate.Month - first.Month;
            var endIndex = ((item.EndDate.Year - first.Year) * 12) + item.EndDate.Month - first.Month;
            startIndex = Math.Clamp(startIndex, 0, slots - 1);
            endIndex = Math.Clamp(endIndex, startIndex, slots - 1);

            return new TimelineBar(
                item.Label,
                startIndex / (double)slots * 100,
                Math.Max(2, (endIndex - startIndex + 1) / (double)slots * 100),
                Math.Clamp(item.CompletionPercentage, 0, 100),
                item.Color);
        }).ToList();
    }

    private sealed record TimelineBar(string Label, double Left, double Width, double CompletionPercentage, string Color);
}