blazor_fluentui_dotnet10

Blazor Fluent UI .NET 10 Charts

LightDark

.NET 10: .NET 10 introduces several enhancements, including performance improvements, new APIs, and better support for cloud-native applications. The integration of JSON schema extraction in .NET 10 is a significant feature that aids in data validation and API documentation.

Blazor: Blazor is a web framework that allows developers to build interactive web applications using C# instead of JavaScript. It leverages the power of .NET to create rich client-side applications that can run in the browser via WebAssembly or on the server.

Fluent UI Blazor: Fluent UI Blazor is a set of components that implement Microsoft’s Fluent Design System, allowing developers to create visually appealing and consistent user interfaces in Blazor applications. It provides a rich set of UI components that are easy to use and integrate seamlessly with Blazor.

Charts: Charts visual representations of data that help users interpret information quickly.

Charts.razor
@page "/charts"


<PageTitle>Charts</PageTitle>


@if (loading)
{
    <FluentStack Orientation="Orientation.Vertical" HorizontalAlignment="HorizontalAlignment.Center">
        <FluentCard Width="200px" Margin="100px">
            <FluentText>loading...</FluentText>
            <FluentProgressBar></FluentProgressBar>
        </FluentCard>
    </FluentStack>
}
else
{
    <FluentStack Orientation="Orientation.Vertical" VerticalGap="20px" Class="pt-4">
        <FluentStack Orientation="Orientation.Horizontal" HorizontalGap="20px">
            <FluentCard>
                <FluentStack Orientation="Orientation.Vertical">
                    <FluentStack Orientation="Orientation.Horizontal" HorizontalAlignment="HorizontalAlignment.SpaceBetween">
                        <FluentText Size="TextSize.Size500">@($"Progress")</FluentText>
                        <FluentText Size="TextSize.Size900">@($"{(int)closeRatioPercentage}%")</FluentText>
                    </FluentStack>
                    <FluentHorizontalBarChart ChartData="@dataHorizontalBarChart" Variant="@HorizontalBarChartVariant.SingleBar" />
                </FluentStack>
            </FluentCard>
            <FluentCard>
                <FluentStack Orientation="Orientation.Vertical">
                    <FluentStack Orientation="Orientation.Horizontal" HorizontalAlignment="HorizontalAlignment.SpaceBetween">
                        <FluentText Size="TextSize.Size500">@($"Open Issues")</FluentText>
                        <FluentText Size="TextSize.Size900">@($"{openIssuesTotalCount}")</FluentText>
                    </FluentStack>
                    <FluentHorizontalBarChart ChartData="@dataHorizontalBarOpenIssuesChart" Variant="@HorizontalBarChartVariant.SingleBar" />
                </FluentStack>
            </FluentCard>
            <FluentCard>
                <FluentStack Orientation="Orientation.Vertical">
                    <FluentStack Orientation="Orientation.Horizontal" HorizontalAlignment="HorizontalAlignment.SpaceBetween">
                        <FluentText Size="TextSize.Size500">@($"Closed Issues")</FluentText>
                        <FluentText Size="TextSize.Size900">@($"{closedIssuesTotalCount}")</FluentText>
                    </FluentStack>
                    <FluentHorizontalBarChart ChartData="@dataHorizontalBarClosedIssuesChart" Variant="@HorizontalBarChartVariant.SingleBar" />
                </FluentStack>
            </FluentCard>
        </FluentStack>

        <FluentStack Orientation="Orientation.Horizontal" HorizontalGap="20px">
            <FluentCard Height="430px">
                <FluentTabs>
                    <FluentTab Id="default-tab11" Header="All Issues" IconStart="@(new Icons.Regular.Size16.Chat())">
                        <FluentAreaChart @ref="fluentAreaChart" ChartData="dataAllIssuesByDateAreaChart" Height="300px" TickFormat="%m/%d" />
                    </FluentTab>
                </FluentTabs>
            </FluentCard>
            <FluentCard Height="430px">
                <FluentTabs>
                    <FluentTab Id="default-tab21" Header="Top Contributors" IconStart="@(new Icons.Regular.Size16.Person())">
                        <FluentDonutChart ChartData="dataTopContributorsDonutChart" Height="300px" Width="100%"
                                          ValueInsideDonut="@dataTopContributorsDonutChartTotalItemCount.ToString()"
                                          ShowLabelsInPercent="true"
                                          InnerRadius="55" />
                    </FluentTab>
                    <FluentTab Id="default-tab22" Header="Top Labels" IconStart="@(new Icons.Regular.Size16.SlideText())">
                        <FluentDonutChart ChartData="dataTopLabelsDonutChart" Height="300px" Width="100%"
                                          ValueInsideDonut="@dataTopLabelsDonutChartTotalItemCount.ToString()"
                                          ShowLabelsInPercent="true"
                                          InnerRadius="55" />
                    </FluentTab>
                </FluentTabs>
            </FluentCard>
        </FluentStack>

        <FluentStack Orientation="Orientation.Horizontal" HorizontalGap="20px">
            <FluentCard Height="430px">
                <FluentDataGrid Items="@allIssuesOrderByDate" Pagination="pagination" ResizableColumns=true>
                    <TemplateColumn Title="State" Context="issue" Align="DataGridCellAlignment.Center" Width="100px">
                        <FluentBadge Color=@(issue.State == IssueState.Open ? BadgeColor.Success : BadgeColor.Danger) Content="@issue.State.ToString()" />
                    </TemplateColumn>
                    <PropertyColumn Property="@(c => c.Title)" Sortable="true" Align="DataGridCellAlignment.Start" />
                    <TemplateColumn Title="Labels" Context="issue" Align="DataGridCellAlignment.Center">
                        @foreach (var label in issue.Labels)
                        {
                            <FluentBadge Style=@($"background-color: #{label.Color}")>@Regex.Replace(label.Name, ":\\w+:", "")</FluentBadge>
                        }
                    </TemplateColumn>
                    <TemplateColumn Title="User" Context="issue" Align="DataGridCellAlignment.Center">
                        <FluentStack Orientation="Orientation.Horizontal" VerticalAlignment="VerticalAlignment.Center" HorizontalAlignment="HorizontalAlignment.Center">
                            <img Style="width: 20px; height: 20px; border-radius: 50%; margin-inline-end: 8px; border: 1px solid #cccccc;" src=@issue.User.AvatarUrl />
                            <FluentText>@issue.User.Login</FluentText>
                        </FluentStack>
                    </TemplateColumn>
                </FluentDataGrid>
                <style>
                                .fluent-paginator > .paginator-nav {
                                    max-height: 30px;
                                }
                </style>

                <FluentPaginator State="@pagination" />
            </FluentCard>
        </FluentStack>

    </FluentStack>
}



@code {
    bool loading = true;
    FluentAreaChart fluentAreaChart;
    double closeRatioPercentage = 0;
    IReadOnlyList<HorizontalBarChartSeries> dataHorizontalBarChart;
    int openIssuesTotalCount;
    IReadOnlyList<HorizontalBarChartSeries> dataHorizontalBarOpenIssuesChart;
    int closedIssuesTotalCount;
    IReadOnlyList<HorizontalBarChartSeries> dataHorizontalBarClosedIssuesChart;

    IReadOnlyList<AreaChartSeries> dataAllIssuesByDateAreaChart;
    IReadOnlyList<DonutDataPoint> dataTopContributorsDonutChart;
    int dataTopContributorsDonutChartTotalItemCount;
    IReadOnlyList<DonutDataPoint> dataTopLabelsDonutChart;
    int dataTopLabelsDonutChartTotalItemCount;

    IQueryable<Issue>? allIssuesOrderByDate;
    PaginationState pagination = new PaginationState { ItemsPerPage = 10 };

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            try
            {
                var issues = await GitHubService.GetIssues(DateTime.Now);
                allIssuesOrderByDate = issues.OrderByDescending(issue => issue.CreatedAt).AsQueryable();

                var openIssues = issues.Where(issue => issue.State == IssueState.Open);
                openIssuesTotalCount = openIssues.Count();

                var closedIssues = issues.Where(issue => issue.State == IssueState.Closed);
                closedIssuesTotalCount = closedIssues.Count();

                var closeRatio = closedIssues.Count() / (double)issues.Count();
                closeRatioPercentage = closeRatio * 100;

                dataHorizontalBarChart = new List<HorizontalBarChartSeries>
                {
                    new HorizontalBarChartSeries
                    {
                        ChartSeriesTitle = "Progress",
                        ChartData = new List<HorizontalBarChartDataPoint>
                        {
                            new HorizontalBarChartDataPoint { Legend = "Progress", Data = closedIssues.Count(), Total = issues.Count(), Color = DataVizPalette.Color1 }
                        }
                    }
                };

                dataHorizontalBarOpenIssuesChart = new List<HorizontalBarChartSeries>
                {
                    new HorizontalBarChartSeries
                    {
                        ChartSeriesTitle = "Open Issues",
                        ChartData = new List<HorizontalBarChartDataPoint>
                        {
                            new HorizontalBarChartDataPoint { Legend = "Open Issues", Data = openIssues.Count(), Total = issues.Count(), Color = DataVizPalette.Color3 }
                        }
                    }
                };

                dataHorizontalBarClosedIssuesChart = new List<HorizontalBarChartSeries>
                {
                    new HorizontalBarChartSeries
                    {
                        ChartSeriesTitle = "Open Issues",
                        ChartData = new List<HorizontalBarChartDataPoint>
                        {
                            new HorizontalBarChartDataPoint { Legend = "Closed Issues", Data = closedIssues.Count(), Total = issues.Count(), Color = DataVizPalette.Color2 }
                        }
                    }
                };




                var openIssuesByDate = openIssues.GroupBy(issue => new
                {
                    Year = ISOWeek.GetYear(issue.CreatedAt.Date),
                    WeekNum = ISOWeek.GetWeekOfYear(issue.CreatedAt.Date)
                }).Select(group => new
                {
                    WeekNum = group.Key.WeekNum,
                    WeekStr = ISOWeek.ToDateTime(group.Key.Year, group.Key.WeekNum, DayOfWeek.Monday).ToString("MM-dd"),
                    Week = ISOWeek.ToDateTime(group.Key.Year, group.Key.WeekNum, DayOfWeek.Monday),
                    Count = group.Count()
                });

                var closedIssuesByDate = closedIssues.GroupBy(issue => new
                {
                    Year = ISOWeek.GetYear(issue.ClosedAt.Value.Date),
                    WeekNum = ISOWeek.GetWeekOfYear(issue.ClosedAt.Value.Date)
                }).Select(group => new
                {
                    WeekNum = group.Key.WeekNum,
                    WeekStr = ISOWeek.ToDateTime(group.Key.Year, group.Key.WeekNum, DayOfWeek.Monday).ToString("MM-dd"),
                    Week = ISOWeek.ToDateTime(group.Key.Year, group.Key.WeekNum, DayOfWeek.Monday),
                    Count = group.Count(),
                });
                var chartDataOpenIssuesByDate = new List<AreaChartDataPoint>();
                foreach (var item in openIssuesByDate)
                {
                    chartDataOpenIssuesByDate.Add(new AreaChartDataPoint { X = item.Week, Y = item.Count });
                }
                var openIssuesByDateAreaChartSeries = new AreaChartSeries()
                {
                    Legend = "Open Issues",
                    ChartData = chartDataOpenIssuesByDate
                };
                var chartDataClosedIssuesByDate = new List<AreaChartDataPoint>();
                foreach (var item in closedIssuesByDate)
                {
                    chartDataClosedIssuesByDate.Add(new AreaChartDataPoint { X = item.Week, Y = item.Count });
                }
                var closedIssuesByDateAreaChartSeries = new AreaChartSeries()
                {
                    Legend = "Closed Issues",
                    ChartData = chartDataClosedIssuesByDate
                };
                dataAllIssuesByDateAreaChart = new List<AreaChartSeries>
                {
                    closedIssuesByDateAreaChartSeries,
                    openIssuesByDateAreaChartSeries
                };

                var openByGroups = issues.GroupBy(issue => issue.User.Login)
                                    .Select(group => new { Name = group.Key, Count = group.Count() })
                                    .OrderByDescending(group => group.Count)
                                    .Take(7);
                var dataDonutDataPoint = new List<DonutDataPoint>();
                foreach (var item in openByGroups)
                {
                    var donutDataPoint = new DonutDataPoint { Legend = item.Name, Data = item.Count };
                    dataDonutDataPoint.Add(donutDataPoint);
                }
                dataTopContributorsDonutChart = dataDonutDataPoint;
                dataTopContributorsDonutChartTotalItemCount = openByGroups.Sum(x => x.Count);

                var labelGroups = issues.SelectMany(issue => issue.Labels)
                                    .GroupBy(label => new { Name = label.Name, Color = label.Color })
                                    .Select(group => new { Label = Regex.Replace(group.Key.Name, ":\\w+:", ""), Color = $"#{group.Key.Color}", Count = group.Count() })
                                    .OrderByDescending(group => group.Count)
                                    .Take(5);
                var dataLabelGroupsDonutDataPoint = new List<DonutDataPoint>();
                foreach (var item in labelGroups)
                {
                    var donutDataPoint = new DonutDataPoint { Legend = item.Label, Data = item.Count };
                    dataLabelGroupsDonutDataPoint.Add(donutDataPoint);
                }
                dataTopLabelsDonutChart = dataLabelGroupsDonutDataPoint;
                dataTopLabelsDonutChartTotalItemCount = labelGroups.Sum(x => x.Count);

            }
            catch (Exception ex)
            {
                var error = ex.Message;
            }

            loading = false;

            StateHasChanged();
        }
    }

}

Charts.razor serves as the main view for visualizing issue tracking data. All Issues By Date (Area Chart), first card hosts a tabbed interface. The default tab shows an FluentAreaChart tracking issue counts over time (dataAllIssuesByDateAreaChart). Top Contributors & Top Labels (Donut Charts), second card also uses tabs, Top Contributors shows how many issues are associated with the top 7 users (dataTopContributorsDonutChart). Top Labels shows the most frequently used labels (dataTopLabelsDonutChart). final section displays a detailed, paginated list of all issues. It uses FluentDataGrid to display structured data. The "State" and "Labels" columns use TemplateColumns because they require custom rendering logic.

GithubService.cs
using BlazorAppFluentUINet10Charts.Models;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Text.RegularExpressions;

namespace BlazorAppFluentUINet10Charts.Services;

class Link
{
    public string Next { get; set; }
    public string Last { get; set; }

    public int NextPage { get; set; }
    public int LastPage { get; set; }

    private static int ExtractPage(string value)
    {
        var match = Regex.Match(value, "&page=(?<page>\\d+)");

        if (match != null)
        {
            return Convert.ToInt32(match.Groups["page"].Value);
        }

        return 0;
    }

    public static Link FromHeader(IEnumerable<string> header)
    {
        var result = new Link();

        var links = String.Join("", header).Split(',');

        foreach (var link in links)
        {
            var rel = Regex.Match(link, "(?<=rel=\").+?(?=\")", RegexOptions.IgnoreCase);
            var value = Regex.Match(link, "(?<=<).+?(?=>)", RegexOptions.IgnoreCase);

            if (rel.Success && value.Success)
            {
                if (rel.Value == "next")
                {
                    result.Next = value.Value;
                    result.NextPage = ExtractPage(result.Next);
                }
                if (rel.Value == "last")
                {
                    result.Last = value.Value;
                    result.LastPage = ExtractPage(result.Last);
                }
            }
        }

        return result;
    }
}

class IssueCache
{
    [JsonPropertyName("date")]
    public DateTime Date { get; set; }

    [JsonPropertyName("issues")]
    public IEnumerable<Issue> Issues { get; set; }
}

public class FetchProgressEventArgs
{
    public int Total { get; set; }
    public int Current { get; set; }
}

public class GitHubService
{
    private readonly JsonSerializerOptions options;

    private IEnumerable<Issue> issues;

    public GitHubService()
    {
        options = new JsonSerializerOptions();
        options.Converters.Add(new JsonStringEnumConverter(JsonNamingPolicy.CamelCase));
    }

    public async Task<IEnumerable<Issue>> GetIssues(DateTime date)
    {
        var target = date.AddMonths(-1);

        if (issues == null)
        {
            issues = await FetchIssues(target);
            issues = issues.Where(issue => issue.CreatedAt >= target).OrderBy(issue => issue.CreatedAt);
        }

        return issues;
    }

    private async Task<IEnumerable<Issue>> FetchIssues(DateTime since)
    {
        var issues = new List<Issue>();

        using (var http = new HttpClient())
        {
            var request = new HttpRequestMessage(HttpMethod.Get, $"https://api.github.com/repos/dotnet/aspnetcore/issues?state=all&per_page=100&since={since:yyyy-MM-ddThh:mm:ssZ}");
            request.Headers.Add("User-Agent", "testapplication");

            var response = await http.SendAsync(request);

            if (response.IsSuccessStatusCode)
            {
                using var responseStream = await response.Content.ReadAsStreamAsync();
                var page = await JsonSerializer.DeserializeAsync<IEnumerable<Issue>>(responseStream, options);
                issues.AddRange(page);
                var link = Link.FromHeader(response.Headers.GetValues("Link"));

                while (link.Next != null)
                {
                    request = new HttpRequestMessage(HttpMethod.Get, link.Next);
                    request.Headers.Add("User-Agent", "testapplication");

                    response = await http.SendAsync(request);

                    if (response.IsSuccessStatusCode)
                    {
                        using (var stream = await response.Content.ReadAsStreamAsync())
                        {
                            page = await JsonSerializer.DeserializeAsync<IEnumerable<Issue>>(stream, options);
                            issues.AddRange(page);
                        }

                        link = Link.FromHeader(response.Headers.GetValues("Link"));
                    }
                    else
                    {
                        break;
                    }
                }
            }
            else
            {
                throw new ApplicationException(response.ReasonPhrase);
            }
        }

        return issues;
    }

}

GetIssues is the primary public method users should call. It aims to retrieve all issues for a given date. It calculates a target date (target = date.AddMonths(-1)). After fetching, it filters and orders the results. It keeps only issues where CreatedAt is on or after the calculated target date. It sorts them by their creation date (OrderBy(issue => issue.CreatedAt)). Finally, it returns the filtered list of issues.

FetchIssues method handles the actual network communication and pagination loop. It takes a starting date (since) for the API query. Initializes an empty List<Issue> called issues, uses a HttpClient to make requests. It builds the initial GET request URL, targeting the GitHub API for issues in the dotnet/aspnetcore repository. It specifies fetching up to 100 items per page (per_page=100) and includes the since parameter. It sends the request and checks if the status code is successful (response.IsSuccessStatusCode).

Source

Full source code is available at this repository in GitHub:
https://github.com/akifmt/DotNetCoding/tree/main/src/BlazorAppFluentUINet10Charts