add date stamp to performance graphs / increase number of performance rating snapshots / localize graph timestamps

This commit is contained in:
RaidMax 2022-07-10 17:06:46 -05:00
parent 4e44bb5ea1
commit 1a72faee60
10 changed files with 172 additions and 101 deletions

View File

@ -7,7 +7,7 @@ namespace Data.Models.Client.Stats
{ {
public class EFClientRankingHistory: AuditFields public class EFClientRankingHistory: AuditFields
{ {
public const int MaxRankingCount = 30; public const int MaxRankingCount = 1728;
[Key] [Key]
public long ClientRankingHistoryId { get; set; } public long ClientRankingHistoryId { get; set; }
@ -28,4 +28,4 @@ namespace Data.Models.Client.Stats
public double? ZScore { get; set; } public double? ZScore { get; set; }
public double? PerformanceMetric { get; set; } public double? PerformanceMetric { get; set; }
} }
} }

View File

@ -19,8 +19,14 @@ namespace IW4MAdmin.Plugins.Stats.Web.Dtos
public int Kills { get; set; } public int Kills { get; set; }
public int Deaths { get; set; } public int Deaths { get; set; }
public int RatingChange { get; set; } public int RatingChange { get; set; }
public List<double> PerformanceHistory { get; set; } public List<PerformanceHistory> PerformanceHistory { get; set; }
public double? ZScore { get; set; } public double? ZScore { get; set; }
public long? ServerId { get; set; } public long? ServerId { get; set; }
} }
public class PerformanceHistory
{
public double? Performance { get; set; }
public DateTime OccurredAt { get; set; }
}
} }

View File

@ -79,6 +79,7 @@ namespace Stats.Helpers
.Where(r => r.ServerId == serverId) .Where(r => r.ServerId == serverId)
.Where(r => r.Ranking != null) .Where(r => r.Ranking != null)
.OrderByDescending(r => r.UpdatedDateTime) .OrderByDescending(r => r.UpdatedDateTime)
.Take(250)
.ToListAsync(); .ToListAsync();
var mostRecentRanking = ratings.FirstOrDefault(ranking => ranking.Newest); var mostRecentRanking = ratings.FirstOrDefault(ranking => ranking.Newest);

View File

@ -188,29 +188,32 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
var finished = statsInfo var finished = statsInfo
.OrderByDescending(stat => rankingsDict[stat.ClientId].Last().PerformanceMetric) .OrderByDescending(stat => rankingsDict[stat.ClientId].Last().PerformanceMetric)
.Select((s, index) => new TopStatsInfo() .Select((s, index) => new TopStatsInfo
{ {
ClientId = s.ClientId, ClientId = s.ClientId,
Id = (int?) serverId ?? 0, Id = (int?)serverId ?? 0,
Deaths = s.Deaths, Deaths = s.Deaths,
Kills = s.Kills, Kills = s.Kills,
KDR = Math.Round(s.KDR, 2), KDR = Math.Round(s.KDR, 2),
LastSeen = (DateTime.UtcNow - (s.UpdatedAt ?? rankingsDict[s.ClientId].Last().LastConnection)) LastSeen = (DateTime.UtcNow - (s.UpdatedAt ?? rankingsDict[s.ClientId].Last().LastConnection))
.HumanizeForCurrentCulture(1, TimeUnit.Week, TimeUnit.Second, ",", false), .HumanizeForCurrentCulture(1, TimeUnit.Week, TimeUnit.Second, ",", false),
LastSeenValue = DateTime.UtcNow - (s.UpdatedAt ?? rankingsDict[s.ClientId].Last().LastConnection), LastSeenValue = DateTime.UtcNow - (s.UpdatedAt ?? rankingsDict[s.ClientId].Last().LastConnection),
Name = rankingsDict[s.ClientId].First().Name, Name = rankingsDict[s.ClientId].First().Name,
Performance = Math.Round(rankingsDict[s.ClientId].Last().PerformanceMetric ?? 0, 2), Performance = Math.Round(rankingsDict[s.ClientId].Last().PerformanceMetric ?? 0, 2),
RatingChange = (rankingsDict[s.ClientId].First().Ranking - RatingChange = (rankingsDict[s.ClientId].First().Ranking -
rankingsDict[s.ClientId].Last().Ranking) ?? 0, rankingsDict[s.ClientId].Last().Ranking) ?? 0,
PerformanceHistory = rankingsDict[s.ClientId].Select(ranking => ranking.PerformanceMetric ?? 0).ToList(), PerformanceHistory = rankingsDict[s.ClientId].Select(ranking => new PerformanceHistory
TimePlayed = Math.Round(s.TotalTimePlayed / 3600.0, 1).ToString("#,##0"), { Performance = ranking.PerformanceMetric ?? 0, OccurredAt = ranking.CreatedDateTime })
TimePlayedValue = TimeSpan.FromSeconds(s.TotalTimePlayed), .ToList(),
Ranking = index + start + 1, TimePlayed = Math.Round(s.TotalTimePlayed / 3600.0, 1).ToString("#,##0"),
ZScore = rankingsDict[s.ClientId].Last().ZScore, TimePlayedValue = TimeSpan.FromSeconds(s.TotalTimePlayed),
ServerId = serverId Ranking = index + start + 1,
}) ZScore = rankingsDict[s.ClientId].Last().ZScore,
.OrderBy(r => r.Ranking) ServerId = serverId
.ToList(); })
.OrderBy(r => r.Ranking)
.Take(60)
.ToList();
return finished; return finished;
} }
@ -289,7 +292,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
var finished = topPlayers.Select(s => new TopStatsInfo() var finished = topPlayers.Select(s => new TopStatsInfo()
{ {
ClientId = s.ClientId, ClientId = s.ClientId,
Id = (int?) serverId ?? 0, Id = (int?)serverId ?? 0,
Deaths = s.Deaths, Deaths = s.Deaths,
Kills = s.Kills, Kills = s.Kills,
KDR = Math.Round(s.KDR, 2), KDR = Math.Round(s.KDR, 2),
@ -302,9 +305,19 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
ratingInfo.First(r => r.Key == s.ClientId).Ratings.Last().Ranking, ratingInfo.First(r => r.Key == s.ClientId).Ratings.Last().Ranking,
PerformanceHistory = ratingInfo.First(r => r.Key == s.ClientId).Ratings.Count() > 1 PerformanceHistory = ratingInfo.First(r => r.Key == s.ClientId).Ratings.Count() > 1
? ratingInfo.First(r => r.Key == s.ClientId).Ratings.OrderBy(r => r.When) ? ratingInfo.First(r => r.Key == s.ClientId).Ratings.OrderBy(r => r.When)
.Select(r => r.Performance).ToList() .Select(r => new PerformanceHistory { Performance = r.Performance, OccurredAt = r.When })
: new List<double>() .ToList()
{clientRatingsDict[s.ClientId].Performance, clientRatingsDict[s.ClientId].Performance}, : new List<PerformanceHistory>
{
new()
{
Performance = clientRatingsDict[s.ClientId].Performance, OccurredAt = DateTime.UtcNow
},
new()
{
Performance = clientRatingsDict[s.ClientId].Performance, OccurredAt = DateTime.UtcNow
}
},
TimePlayed = Math.Round(s.TotalTimePlayed / 3600.0, 1).ToString("#,##0"), TimePlayed = Math.Round(s.TotalTimePlayed / 3600.0, 1).ToString("#,##0"),
TimePlayedValue = TimeSpan.FromSeconds(s.TotalTimePlayed) TimePlayedValue = TimeSpan.FromSeconds(s.TotalTimePlayed)
}) })

View File

@ -7,6 +7,8 @@
@using Humanizer.Localisation @using Humanizer.Localisation
@using IW4MAdmin.Plugins.Stats @using IW4MAdmin.Plugins.Stats
@using WebfrontCore.ViewModels @using WebfrontCore.ViewModels
@using System.Text.Json
@using IW4MAdmin.Plugins.Stats.Web.Dtos
@model Stats.Dtos.AdvancedStatsInfo @model Stats.Dtos.AdvancedStatsInfo
@{ @{
@ -122,11 +124,11 @@
var spm = Model.ServerId != null ? serverLegacyStat?.SPM.ToNumericalString() : Model.LegacyStats.WeightValueByPlaytime(nameof(EFClientStatistics.SPM), 0).ToNumericalString(); var spm = Model.ServerId != null ? serverLegacyStat?.SPM.ToNumericalString() : Model.LegacyStats.WeightValueByPlaytime(nameof(EFClientStatistics.SPM), 0).ToNumericalString();
var performanceHistory = Model.Ratings var performanceHistory = Model.Ratings
.Select(rating => rating.PerformanceMetric); .Select(rating => new PerformanceHistory { Performance = rating.PerformanceMetric, OccurredAt = rating.CreatedDateTime });
if (performance != null) if (performance != null)
{ {
performanceHistory = performanceHistory.Append(performance.Value); performanceHistory = performanceHistory.Append(new PerformanceHistory { Performance = performance.Value, OccurredAt = DateTime.UtcNow });
} }
var score = allPerServer.Any() var score = allPerServer.Any()
@ -284,7 +286,7 @@
@if (performanceHistory.Count() > 5) @if (performanceHistory.Count() > 5)
{ {
<div class="w-half m-auto ml-lg-auto " id="client_performance_history_container"> <div class="w-half m-auto ml-lg-auto " id="client_performance_history_container">
<canvas id="client_performance_history" data-history="@Html.Raw(Json.Serialize(performanceHistory))"></canvas> <canvas id="client_performance_history" data-history="@(JsonSerializer.Serialize(performanceHistory))"></canvas>
</div> </div>
} }
</div> </div>

View File

@ -1,4 +1,6 @@
@using IW4MAdmin.Plugins.Stats @using IW4MAdmin.Plugins.Stats
@using System.Text.Json.Serialization
@using System.Text.Json
@model List<IW4MAdmin.Plugins.Stats.Web.Dtos.TopStatsInfo> @model List<IW4MAdmin.Plugins.Stats.Web.Dtos.TopStatsInfo>
@{ @{
Layout = null; Layout = null;
@ -83,7 +85,8 @@
</div> </div>
</div> </div>
</div> </div>
<div class="w-full w-md-half client-rating-graph" id="rating_history_@(stat.ClientId + "_" + stat.Id)" data-history="@Html.Raw(Json.Serialize(stat.PerformanceHistory))"> <div class="w-full w-md-half client-rating-graph pt-10 pb-10">
<canvas id="rating_history_@(stat.ClientId + "_" + stat.Id)" data-history="@(JsonSerializer.Serialize(stat.PerformanceHistory))"></canvas>
</div> </div>
<div class="w-quarter align-self-center d-flex justify-content-center"> <div class="w-quarter align-self-center d-flex justify-content-center">
<img class="w-100 h-100" src="~/images/stats/ranks/rank_@(stat.ZScore.RankIconIndexForZScore()).png" alt="@stat.Performance"/> <img class="w-100 h-100" src="~/images/stats/ranks/rank_@(stat.ZScore.RankIconIndexForZScore()).png" alt="@stat.Performance"/>

View File

@ -154,7 +154,7 @@
</div> </div>
<environment include="Development"> <environment include="Development">
<script type="text/javascript" src="~/lib/jquery/dist/jquery.js"></script> <script type="text/javascript" src="~/lib/jquery/dist/jquery.js"></script>
<script type="text/javascript" src="~/lib/moment.js/moment.js"></script> <script type="text/javascript" src="~/lib/moment.js/min/moment-with-locales.js"></script>
<script type="text/javascript" src="~/lib/moment-timezone/moment-timezone.js"></script> <script type="text/javascript" src="~/lib/moment-timezone/moment-timezone.js"></script>
<script type="text/javascript" src="~/lib/chart.js/dist/Chart.bundle.min.js"></script> <script type="text/javascript" src="~/lib/chart.js/dist/Chart.bundle.min.js"></script>
<script type="text/javascript" src="~/lib/halfmoon/js/halfmoon.js"></script> <script type="text/javascript" src="~/lib/halfmoon/js/halfmoon.js"></script>
@ -171,6 +171,7 @@
$.each(_localizationTmp.set, function (key, value) { $.each(_localizationTmp.set, function (key, value) {
_localization[key] = value; _localization[key] = value;
}); });
moment.locale('@Utilities.CurrentLocalization.LocalizationName');
</script> </script>
@await RenderSectionAsync("scripts", required: false) @await RenderSectionAsync("scripts", required: false)
@Html.Raw(ViewBag.ScriptInjection) @Html.Raw(ViewBag.ScriptInjection)

View File

@ -12,7 +12,7 @@
"outputFileName": "wwwroot/js/global.min.js", "outputFileName": "wwwroot/js/global.min.js",
"inputFiles": [ "inputFiles": [
"wwwroot/lib/jquery/dist/jquery.js", "wwwroot/lib/jquery/dist/jquery.js",
"wwwroot/lib/moment.js/moment.min.js", "wwwroot/lib/moment.js/moment-with-locales.min.js",
"wwwroot/lib/moment-timezone/moment-timezone.min.js", "wwwroot/lib/moment-timezone/moment-timezone.min.js",
"wwwroot/lib/chart.js/dist/Chart.bundle.min.js", "wwwroot/lib/chart.js/dist/Chart.bundle.min.js",
"wwwroot/lib/halfmoon/js/halfmoon.min.js", "wwwroot/lib/halfmoon/js/halfmoon.min.js",

View File

@ -321,13 +321,16 @@ function renderPerformanceChart() {
} }
const labels = []; const labels = [];
const values = [];
data.forEach(function (item, i) { data.forEach(function (item, i) {
labels.push(i); labels.push(item.OccurredAt);
values.push(item.Performance)
}); });
const padding = 4; const padding = 4;
let dataMin = Math.min(...data); let dataMin = Math.min(...values);
const dataMax = Math.max(...data); const dataMax = Math.max(...values);
if (dataMax - dataMin === 0) { if (dataMax - dataMin === 0) {
dataMin = 0; dataMin = 0;
@ -341,7 +344,7 @@ function renderPerformanceChart() {
const chartData = { const chartData = {
labels: labels, labels: labels,
datasets: [{ datasets: [{
data: data, data: values,
pointBackgroundColor: 'rgba(255, 255, 255, 0)', pointBackgroundColor: 'rgba(255, 255, 255, 0)',
pointBorderColor: 'rgba(255, 255, 255, 0)', pointBorderColor: 'rgba(255, 255, 255, 0)',
pointHoverRadius: 5, pointHoverRadius: 5,
@ -356,8 +359,8 @@ function renderPerformanceChart() {
legend: false, legend: false,
tooltips: { tooltips: {
callbacks: { callbacks: {
label: (tooltipItem) => Math.round(tooltipItem.yLabel) + ' ' + _localization["PLUGINS_STATS_COMMANDS_PERFORMANCE"], label: context => moment.utc(context.label).local().calendar(),
title: () => '' title: items => Math.round(items[0].yLabel) + ' ' + _localization["PLUGINS_STATS_COMMANDS_PERFORMANCE"],
}, },
mode: 'nearest', mode: 'nearest',
intersect: false, intersect: false,

View File

@ -1,83 +1,125 @@
function getStatsChart(id, width, height) { function getClosestMultiple(baseValue, value) {
return Math.round(value / baseValue) * baseValue;
}
function getStatsChart(id) {
const data = $('#' + id).data('history'); const data = $('#' + id).data('history');
if (data === undefined) { if (data === undefined) {
return; return;
} }
if (data.length <= 1) {
let fixedData = []; // only 0 perf
data.forEach(function (item, i) { return;
fixedData[i] = { x: i, y: Math.floor(item) }; }
});
let dataMin = Math.min(...data); const labels = [];
const dataMax = Math.max(...data); const values = [];
data.forEach(function (item, i) {
labels.push(item.OccurredAt);
values.push(item.Performance)
});
const padding = 4;
let dataMin = Math.min(...values);
const dataMax = Math.max(...values);
if (dataMax - dataMin === 0) { if (dataMax - dataMin === 0) {
dataMin = 0; dataMin = 0;
} }
const padding = (dataMax - dataMin) * 0.5; dataMin = Math.max(0, dataMin);
const min = Math.max(0, dataMin - padding);
const max = dataMax + padding;
let interval = Math.floor((max - min) / 2);
if (interval < 1) const min = getClosestMultiple(padding, dataMin - padding);
interval = 1; const max = getClosestMultiple(padding, dataMax + padding);
return new CanvasJS.Chart(id, { const chartData = {
backgroundColor: 'transparent', labels: labels,
height: height, datasets: [{
width: width, data: values,
animationEnabled: false, pointBackgroundColor: 'rgba(255, 255, 255, 0)',
toolTip: { pointBorderColor: 'rgba(255, 255, 255, 0)',
contentFormatter: function (e) { pointHoverRadius: 5,
return `${_localization['WEBFRONT_ADV_STATS_RANKING_METRIC']} ${Math.round(e.entries[0].dataPoint.y, 1)}`; pointHoverBackgroundColor: 'rgba(255, 255, 255, 1)',
}]
};
const options = {
defaultFontFamily: '-apple-system, BlinkMacSystemFont, "Open Sans", "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"',
responsive: true,
maintainAspectRatio: false,
legend: false,
tooltips: {
callbacks: {
label: context => moment.utc(context.label).local().calendar(),
title: items => Math.round(items[0].yLabel) + ' ' + _localization['WEBFRONT_ADV_STATS_RANKING_METRIC']
},
mode: 'nearest',
intersect: false,
animationDuration: 0,
cornerRadius: 0,
displayColors: false
},
hover: {
mode: 'nearest',
intersect: false
},
elements: {
line: {
fill: false,
borderColor: halfmoon.getPreferredMode() === "light-mode" ? 'rgba(0, 0, 0, 0.85)' : 'rgba(255, 255, 255, 0.75)',
borderWidth: 2
},
point: {
radius: 5
} }
}, },
title: { scales: {
fontSize: 0 xAxes: [{
display: false,
}],
yAxes: [{
gridLines: {
display: false
},
position: 'right',
ticks: {
callback: function (value, index, values) {
if (index === values.length - 1) {
return min;
} else if (index === 0) {
return max;
} else {
return '';
}
},
fontColor: 'rgba(255, 255, 255, 0.25)'
}
}]
}, },
axisX: { layout: {
gridThickness: 0, padding: {
lineThickness: 0, left: 15
tickThickness: 0, }
margin: 0,
valueFormatString: ' '
}, },
axisY: { };
labelFontSize: 12,
interval: interval, new Chart(id, {
gridThickness: 0, type: 'line',
lineThickness: 0.5, data: chartData,
valueFormatString: '#,##0', options: options
minimum: min, });
maximum: max
},
legend: {
dockInsidePlotArea: true
},
data: [{
type: 'spline',
color: '#c0c0c0',
markerSize: 0,
dataPoints: fixedData,
lineThickness: 2
}]
});
} }
$(document).ready(function () { $(document).ready(function () {
$('.client-rating-graph').each(function (i, element) { $('.client-rating-graph').each(function (i, element) {
getStatsChart($(element).attr('id'), $(element).width(), $(element).height()).render(); getStatsChart($(element).children('canvas').attr('id'));
});
$(window).resize(function () {
$('.client-rating-graph').each(function (index, element) {
getStatsChart($(element).attr('id'), $(element).width(), $(element).height()).render();
});
}); });
$('.top-players-link').click(function (event) { $('.top-players-link').click(function (event) {
$($(this).attr('href')).html(''); $($(this).attr('href')).html('');
initLoader('/Stats/GetTopPlayersAsync?serverId=' + $(this).data('serverid'), $(this).attr('href'), 10, 0); initLoader('/Stats/GetTopPlayersAsync?serverId=' + $(this).data('serverid'), $(this).attr('href'), 10, 0);
@ -88,6 +130,6 @@ $(document).ready(function () {
$(document).on("loaderFinished", function (event, response) { $(document).on("loaderFinished", function (event, response) {
const ids = $.map($(response).find('.client-rating-graph'), function (elem) { return $(elem).attr('id'); }); const ids = $.map($(response).find('.client-rating-graph'), function (elem) { return $(elem).attr('id'); });
ids.forEach(function (item, index) { ids.forEach(function (item, index) {
getStatsChart(item, $(item).width(), $(item).height()).render(); getStatsChart($(item).children('canvas').attr('id'));
}); });
}); });