diff --git a/Data/Models/Client/Stats/EFClientRankingHistory.cs b/Data/Models/Client/Stats/EFClientRankingHistory.cs index 1991a5b31..4bff2626d 100644 --- a/Data/Models/Client/Stats/EFClientRankingHistory.cs +++ b/Data/Models/Client/Stats/EFClientRankingHistory.cs @@ -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; } } -} \ No newline at end of file +} diff --git a/Plugins/Stats/Dtos/TopStatsInfo.cs b/Plugins/Stats/Dtos/TopStatsInfo.cs index fdd357a12..56403ed71 100644 --- a/Plugins/Stats/Dtos/TopStatsInfo.cs +++ b/Plugins/Stats/Dtos/TopStatsInfo.cs @@ -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 PerformanceHistory { get; set; } + public List 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; } + } } diff --git a/Plugins/Stats/Helpers/AdvancedClientStatsResourceQueryHelper.cs b/Plugins/Stats/Helpers/AdvancedClientStatsResourceQueryHelper.cs index f1e24ffa7..452727d12 100644 --- a/Plugins/Stats/Helpers/AdvancedClientStatsResourceQueryHelper.cs +++ b/Plugins/Stats/Helpers/AdvancedClientStatsResourceQueryHelper.cs @@ -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); diff --git a/Plugins/Stats/Helpers/StatManager.cs b/Plugins/Stats/Helpers/StatManager.cs index 36a1bfe50..e2d8cb2fa 100644 --- a/Plugins/Stats/Helpers/StatManager.cs +++ b/Plugins/Stats/Helpers/StatManager.cs @@ -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() - {clientRatingsDict[s.ClientId].Performance, clientRatingsDict[s.ClientId].Performance}, + .Select(r => new PerformanceHistory { Performance = r.Performance, OccurredAt = r.When }) + .ToList() + : new List + { + 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) }) diff --git a/WebfrontCore/Views/Client/Statistics/Advanced.cshtml b/WebfrontCore/Views/Client/Statistics/Advanced.cshtml index 0976350f0..03efa8894 100644 --- a/WebfrontCore/Views/Client/Statistics/Advanced.cshtml +++ b/WebfrontCore/Views/Client/Statistics/Advanced.cshtml @@ -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) {
- +
} diff --git a/WebfrontCore/Views/Client/Statistics/Components/TopPlayers/_List.cshtml b/WebfrontCore/Views/Client/Statistics/Components/TopPlayers/_List.cshtml index 981ff145d..e428ebe1f 100644 --- a/WebfrontCore/Views/Client/Statistics/Components/TopPlayers/_List.cshtml +++ b/WebfrontCore/Views/Client/Statistics/Components/TopPlayers/_List.cshtml @@ -1,4 +1,6 @@ @using IW4MAdmin.Plugins.Stats +@using System.Text.Json.Serialization +@using System.Text.Json @model List @{ Layout = null; @@ -83,7 +85,8 @@ -
+
+
@stat.Performance diff --git a/WebfrontCore/Views/Shared/_Layout.cshtml b/WebfrontCore/Views/Shared/_Layout.cshtml index 425b1700e..73731668d 100644 --- a/WebfrontCore/Views/Shared/_Layout.cshtml +++ b/WebfrontCore/Views/Shared/_Layout.cshtml @@ -154,7 +154,7 @@
- + @@ -171,6 +171,7 @@ $.each(_localizationTmp.set, function (key, value) { _localization[key] = value; }); + moment.locale('@Utilities.CurrentLocalization.LocalizationName'); @await RenderSectionAsync("scripts", required: false) @Html.Raw(ViewBag.ScriptInjection) diff --git a/WebfrontCore/bundleconfig.json b/WebfrontCore/bundleconfig.json index a980657c9..7fc13e529 100644 --- a/WebfrontCore/bundleconfig.json +++ b/WebfrontCore/bundleconfig.json @@ -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", diff --git a/WebfrontCore/wwwroot/js/advanced_stats.js b/WebfrontCore/wwwroot/js/advanced_stats.js index d7269915c..744453a04 100644 --- a/WebfrontCore/wwwroot/js/advanced_stats.js +++ b/WebfrontCore/wwwroot/js/advanced_stats.js @@ -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, diff --git a/WebfrontCore/wwwroot/js/stats.js b/WebfrontCore/wwwroot/js/stats.js index 22fca485e..4e66526fb 100644 --- a/WebfrontCore/wwwroot/js/stats.js +++ b/WebfrontCore/wwwroot/js/stats.js @@ -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')); }); });