Blazor Fluent UI .NET 10 Charts

Blazor Fluent UI .NET 10 Charts
| Light | Dark |
|---|---|
![]() | ![]() |
.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


