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 const int MaxRankingCount = 30;
public const int MaxRankingCount = 1728;
[Key]
public long ClientRankingHistoryId { get; set; }
@ -28,4 +28,4 @@ namespace Data.Models.Client.Stats
public double? ZScore { 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 Deaths { 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 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.Ranking != null)
.OrderByDescending(r => r.UpdatedDateTime)
.Take(250)
.ToListAsync();
var mostRecentRanking = ratings.FirstOrDefault(ranking => ranking.Newest);

View File

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

View File

@ -7,6 +7,8 @@
@using Humanizer.Localisation
@using IW4MAdmin.Plugins.Stats
@using WebfrontCore.ViewModels
@using System.Text.Json
@using IW4MAdmin.Plugins.Stats.Web.Dtos
@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 performanceHistory = Model.Ratings
.Select(rating => rating.PerformanceMetric);
.Select(rating => new PerformanceHistory { Performance = rating.PerformanceMetric, OccurredAt = rating.CreatedDateTime });
if (performance != null)
{
performanceHistory = performanceHistory.Append(performance.Value);
performanceHistory = performanceHistory.Append(new PerformanceHistory { Performance = performance.Value, OccurredAt = DateTime.UtcNow });
}
var score = allPerServer.Any()
@ -284,7 +286,7 @@
@if (performanceHistory.Count() > 5)
{
<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>

View File

@ -1,4 +1,6 @@
@using IW4MAdmin.Plugins.Stats
@using System.Text.Json.Serialization
@using System.Text.Json
@model List<IW4MAdmin.Plugins.Stats.Web.Dtos.TopStatsInfo>
@{
Layout = null;
@ -83,7 +85,8 @@
</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 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"/>

View File

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

View File

@ -12,7 +12,7 @@
"outputFileName": "wwwroot/js/global.min.js",
"inputFiles": [
"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/chart.js/dist/Chart.bundle.min.js",
"wwwroot/lib/halfmoon/js/halfmoon.min.js",

View File

@ -321,13 +321,16 @@ function renderPerformanceChart() {
}
const labels = [];
const values = [];
data.forEach(function (item, i) {
labels.push(i);
labels.push(item.OccurredAt);
values.push(item.Performance)
});
const padding = 4;
let dataMin = Math.min(...data);
const dataMax = Math.max(...data);
let dataMin = Math.min(...values);
const dataMax = Math.max(...values);
if (dataMax - dataMin === 0) {
dataMin = 0;
@ -341,7 +344,7 @@ function renderPerformanceChart() {
const chartData = {
labels: labels,
datasets: [{
data: data,
data: values,
pointBackgroundColor: 'rgba(255, 255, 255, 0)',
pointBorderColor: 'rgba(255, 255, 255, 0)',
pointHoverRadius: 5,
@ -356,8 +359,8 @@ function renderPerformanceChart() {
legend: false,
tooltips: {
callbacks: {
label: (tooltipItem) => Math.round(tooltipItem.yLabel) + ' ' + _localization["PLUGINS_STATS_COMMANDS_PERFORMANCE"],
title: () => ''
label: context => moment.utc(context.label).local().calendar(),
title: items => Math.round(items[0].yLabel) + ' ' + _localization["PLUGINS_STATS_COMMANDS_PERFORMANCE"],
},
mode: 'nearest',
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');
if (data === undefined) {
return;
}
let fixedData = [];
data.forEach(function (item, i) {
fixedData[i] = { x: i, y: Math.floor(item) };
});
if (data.length <= 1) {
// only 0 perf
return;
}
let dataMin = Math.min(...data);
const dataMax = Math.max(...data);
const labels = [];
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) {
dataMin = 0;
}
const padding = (dataMax - dataMin) * 0.5;
const min = Math.max(0, dataMin - padding);
const max = dataMax + padding;
let interval = Math.floor((max - min) / 2);
dataMin = Math.max(0, dataMin);
if (interval < 1)
interval = 1;
const min = getClosestMultiple(padding, dataMin - padding);
const max = getClosestMultiple(padding, dataMax + padding);
return new CanvasJS.Chart(id, {
backgroundColor: 'transparent',
height: height,
width: width,
animationEnabled: false,
toolTip: {
contentFormatter: function (e) {
return `${_localization['WEBFRONT_ADV_STATS_RANKING_METRIC']} ${Math.round(e.entries[0].dataPoint.y, 1)}`;
const chartData = {
labels: labels,
datasets: [{
data: values,
pointBackgroundColor: 'rgba(255, 255, 255, 0)',
pointBorderColor: 'rgba(255, 255, 255, 0)',
pointHoverRadius: 5,
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: {
fontSize: 0
scales: {
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: {
gridThickness: 0,
lineThickness: 0,
tickThickness: 0,
margin: 0,
valueFormatString: ' '
layout: {
padding: {
left: 15
}
},
axisY: {
labelFontSize: 12,
interval: interval,
gridThickness: 0,
lineThickness: 0.5,
valueFormatString: '#,##0',
minimum: min,
maximum: max
},
legend: {
dockInsidePlotArea: true
},
data: [{
type: 'spline',
color: '#c0c0c0',
markerSize: 0,
dataPoints: fixedData,
lineThickness: 2
}]
});
};
new Chart(id, {
type: 'line',
data: chartData,
options: options
});
}
$(document).ready(function () {
$('.client-rating-graph').each(function (i, element) {
getStatsChart($(element).attr('id'), $(element).width(), $(element).height()).render();
});
$(window).resize(function () {
$('.client-rating-graph').each(function (index, element) {
getStatsChart($(element).attr('id'), $(element).width(), $(element).height()).render();
});
getStatsChart($(element).children('canvas').attr('id'));
});
$('.top-players-link').click(function (event) {
$($(this).attr('href')).html('');
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) {
const ids = $.map($(response).find('.client-rating-graph'), function (elem) { return $(elem).attr('id'); });
ids.forEach(function (item, index) {
getStatsChart(item, $(item).width(), $(item).height()).render();
getStatsChart($(item).children('canvas').attr('id'));
});
});