Skip to content

Commit a6aa717

Browse files
authored
Merge pull request #42 from veloek/dev/statistics-page
Dev/statistics page
2 parents d7cfd95 + 84183df commit a6aa717

File tree

9 files changed

+157
-1
lines changed

9 files changed

+157
-1
lines changed

Tevling/App.razor

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,5 +27,6 @@
2727
</div>
2828
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js" integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz" crossorigin="anonymous"></script>
2929
<script src="_framework/blazor.web.js"></script>
30+
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/chart.umd.js" integrity="sha384-4W8gMhOYqk0FPg9sAMkb3g/P2fXtzbUyDUUgMlA1EEig8z/U/13EYUjErc1H382b" crossorigin="anonymous"></script>
3031
</body>
3132
</html>

Tevling/Model/ActivityFilter.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
namespace Tevling.Model;
22

3-
public record ActivityFilter(int AthleteId, bool IncludeFollowing);
3+
public record ActivityFilter(int AthleteId, bool IncludeFollowing, DateTimeOffset? From = null);

Tevling/Pages/About.razor

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,10 @@
2323
<a href="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/erossini/BlazorAutocomplete" target="_blank">PSC.Blazor.Components.AutoComplete</a>
2424
- <a href="https://github.com/erossini/BlazorAutocomplete/blob/main/LICENSE" target="_blank">GNU GPLv3</a>
2525
</li>
26+
<li>
27+
<a href="https://www.chartjs.org/" target="_blank">Chart.js</a>
28+
- <a href="https://github.com/chartjs/Chart.js/blob/master/LICENSE.md" target="_blank">MIT license</a>
29+
</li>
2630
<li>
2731
<a href="https://www.flaticon.com/free-icons/arm-wrestling" title="arm wrestling icons">Arm wrestling icons created by Freepik - Flaticon</a>
2832
</li>

Tevling/Pages/Statistics.razor

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
@page "/Statistics"
2+
<PageTitle>Tevling - Statistics</PageTitle>
3+
4+
<h1 class="title mb-5">Statistics</h1>
5+
<div class="d-flex flex-wrap p-2" >
6+
<canvas id="totalDistanceChart"></canvas>
7+
</div>
8+
<div class="d-flex flex-wrap p-2" >
9+
<canvas id="totalElevationChart"></canvas>
10+
</div><div class="d-flex flex-wrap p-2" >
11+
<canvas id="totalTimeChart"></canvas>
12+
</div>
13+
<script src="Pages/Statistics.razor.js"></script>

Tevling/Pages/Statistics.razor.cs

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
using System.Globalization;
2+
using Microsoft.JSInterop;
3+
4+
namespace Tevling.Pages;
5+
6+
public partial class Statistics : ComponentBase
7+
{
8+
[Inject] private IJSRuntime Js { get; set; } = null!;
9+
[Inject] private IAuthenticationService AuthenticationService { get; set; } = null!;
10+
[Inject] private IActivityService ActivityService { get; set; } = null!;
11+
12+
private Athlete _athlete = null!;
13+
private Activity[] _activities = [];
14+
15+
16+
protected override async Task OnInitializedAsync()
17+
{
18+
_athlete = await AuthenticationService.GetCurrentAthleteAsync();
19+
20+
ActivityFilter filter = new(_athlete.Id, false, DateTimeOffset.Now.AddMonths(-2));
21+
_activities = await ActivityService.GetActivitiesAsync(filter);
22+
}
23+
24+
private Dictionary<string, float[]> GetAggregatedMeasurementData(Func<Activity, float> selector, int monthCount = 3)
25+
{
26+
DateTimeOffset now = DateTimeOffset.Now;
27+
28+
Dictionary<string, float[]> aggregatedData = _activities
29+
.GroupBy(a => a.Details.Type)
30+
.ToDictionary(
31+
g => g.Key.ToString(),
32+
g => Enumerable.Range(-monthCount + 1, monthCount)
33+
.Select(m =>
34+
{
35+
int month = now.AddMonths(m).Month;
36+
return g
37+
.Where(a => a.Details.StartDate.Month == month)
38+
.Sum(selector);
39+
})
40+
.ToArray()
41+
);
42+
43+
if (aggregatedData.Count != 0)
44+
{
45+
aggregatedData["Total"] =
46+
[
47+
.. aggregatedData.Values.Aggregate((sum, next) => [.. sum.Zip(next, (a, b) => a + b)]),
48+
];
49+
}
50+
51+
return aggregatedData;
52+
}
53+
54+
protected override async Task OnAfterRenderAsync(bool firstRender)
55+
{
56+
if (firstRender)
57+
{
58+
List<int> lastThreeMonths =
59+
[
60+
DateTimeOffset.Now.AddMonths(-2).Month,
61+
DateTimeOffset.Now.AddMonths(-1).Month,
62+
DateTimeOffset.Now.Month,
63+
];
64+
65+
Dictionary<string, float[]> distancesLastThreeMonths =
66+
GetAggregatedMeasurementData(a => a.Details.DistanceInMeters);
67+
Dictionary<string, float[]> elevationLastThreeMonths =
68+
GetAggregatedMeasurementData(a => a.Details.TotalElevationGain);
69+
Dictionary<string, float[]> timeLastThreeMonths =
70+
GetAggregatedMeasurementData(a => (float)a.Details.MovingTimeInSeconds / 3600);
71+
72+
73+
await Js.InvokeVoidAsync(
74+
"drawChart",
75+
distancesLastThreeMonths,
76+
lastThreeMonths.Select(m => CultureInfo.CurrentCulture.DateTimeFormat.GetMonthName(m)).ToList(),
77+
"totalDistanceChart",
78+
"Total Distance [m]");
79+
await Js.InvokeVoidAsync(
80+
"drawChart",
81+
elevationLastThreeMonths,
82+
lastThreeMonths.Select(m => CultureInfo.CurrentCulture.DateTimeFormat.GetMonthName(m)).ToList(),
83+
"totalElevationChart",
84+
"Total Elevation [m]");
85+
await Js.InvokeVoidAsync(
86+
"drawChart",
87+
timeLastThreeMonths,
88+
lastThreeMonths.Select(m => CultureInfo.CurrentCulture.DateTimeFormat.GetMonthName(m)).ToList(),
89+
"totalTimeChart",
90+
"Total Time [h]");
91+
}
92+
}
93+
}

Tevling/Pages/Statistics.razor.css

Whitespace-only changes.

Tevling/Pages/Statistics.razor.js

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
window.drawChart = function (activityData, labels, chartName, chartTitle) {
2+
var ctx = document.getElementById(chartName).getContext('2d');
3+
var canvas = document.getElementById(chartName);
4+
5+
// Destroy existing chart instance if it exists
6+
if (window[chartName] instanceof Chart) {
7+
window[chartName].destroy();
8+
}
9+
10+
let datasets = [];
11+
12+
for (const [key, value] of Object.entries(activityData)) {
13+
datasets.push({
14+
label: key, data: value
15+
});
16+
}
17+
18+
window[chartName] = new Chart(ctx, {
19+
type: 'line',
20+
data: {
21+
labels: labels, datasets: datasets
22+
}, options: {
23+
plugins: {
24+
title: {
25+
display: true, text: chartTitle, color: 'rgba(54, 162, 235, 1)', font: {
26+
size: 20
27+
},
28+
},
29+
}, responsive: true, scales: {
30+
y: {
31+
beginAtZero: true,
32+
33+
}
34+
},
35+
36+
}
37+
});
38+
};

Tevling/Services/ActivityService.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,10 +129,12 @@ public async Task<Activity[]> GetActivitiesAsync(
129129
.If(filter.IncludeFollowing, q => q.Include(a => a.Following))
130130
.FirstOrDefaultAsync(a => a.Id == filter.AthleteId, ct) ??
131131
throw new Exception($"Unknown athlete ID {filter.AthleteId}");
132+
DateTimeOffset now = DateTimeOffset.Now;
132133

133134
Activity[] activities = await dataContext.Activities
134135
.Include(a => a.Athlete)
135136
.ThenInclude(a => a!.Following)
137+
.If(filter.From.HasValue, q => q.Where(a => a.Details.StartDate >= filter.From), x => x)
136138
.Where(
137139
activity => activity.AthleteId == athlete.Id ||
138140
(filter.IncludeFollowing && athlete.Following!.Select(a => a.Id).Contains(activity.AthleteId)))

Tevling/Shared/NavMenu.razor

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,11 @@
2121
<i class="bi bi-person-arms-up me-2" aria-hidden="true"></i> Activities
2222
</NavLink>
2323
</div>
24+
<div class="nav-item">
25+
<NavLink class="nav-link" href="statistics">
26+
<i class="bi bi-bar-chart me-2" aria-hidden="true"></i> Statistics
27+
</NavLink>
28+
</div>
2429
<div class="nav-item">
2530
<NavLink class="nav-link" href="challenges">
2631
<i class="bi bi-trophy-fill me-2" aria-hidden="true"></i> Challenges

0 commit comments

Comments
 (0)