Add automated ban offense for anti-cheat

add EFClientStatHistory and EFClientAverageStatHistory for tracking change of stats over time
This commit is contained in:
RaidMax 2018-05-30 20:50:20 -05:00
parent 2204686b08
commit bf68e5672f
22 changed files with 1178 additions and 103 deletions

View File

@ -1024,7 +1024,8 @@ namespace IW4MAdmin
Punisher = Origin,
Active = true,
When = DateTime.UtcNow,
Link = Target.AliasLink
Link = Target.AliasLink,
AutomatedOffense = Origin.AdministeredPenalties.FirstOrDefault()?.AutomatedOffense
};
await Manager.GetPenaltyService().Create(newPenalty);

View File

@ -14,6 +14,8 @@ using System.Text.RegularExpressions;
using IW4MAdmin.Plugins.Stats.Web.Dtos;
using SharedLibraryCore.Database;
using Microsoft.EntityFrameworkCore;
using SharedLibraryCore.Database.Models;
using SharedLibraryCore.Services;
namespace IW4MAdmin.Plugins.Stats.Helpers
{
@ -50,64 +52,70 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
var thirtyDaysAgo = DateTime.UtcNow.AddMonths(-1);
var iqClientIds = (from stat in context.Set<EFClientStatistics>()
join client in context.Clients
on stat.ClientId equals client.ClientId
#if !DEBUG
where stat.TimePlayed >= 3600
where client.Level != Player.Permission.Banned
where client.LastConnection >= thirtyDaysAgo
where stat.Performance > 60
.Where(s => s.TimePlayed > 3600)
.Where(s => s.EloRating > 60.0)
#endif
group stat by stat.ClientId into s
orderby s.Average(cs => cs.Performance) descending
select s.First().ClientId)
where stat.Client.Level != Player.Permission.Banned
where stat.Client.LastConnection >= thirtyDaysAgo
group stat by stat.ClientId into sj
let performance = sj.Sum(s => (s.EloRating + s.Skill) * s.TimePlayed) / sj.Sum(st => st.TimePlayed)
orderby performance
select new
{
// sj.First().Client.CurrentAlias.Name,
sj.First().Client.ClientId,
Skill = sj.Select(s => s.Skill)
})
/*
join averageStats in context.Set<EFClientAverageStatHistory>().Include(c => c.Ratings)
on stat.ClientId equals averageStats.ClientId
where averageStats.Ratings.Count > 0
group new { stat, averageStats } by averageStats.ClientId into avg
orderby avg.Select(c => c.averageStats.Ratings.OrderByDescending(r => r.RatingId).First().Performance).First()
select new
{
avg.First().stat.Client.CurrentAlias.Name,//sj.Select(c => c.Client.CurrentAlias.Name),
avg.First().stat.ClientId,//sj.First().ClientId,
avg.First().stat.Kills,//Kills = sj.Select(s => s.Kills),
avg.First().stat.Deaths,//Deaths = sj.Select(s => s.Deaths),
avg.First().stat.Performance,//Performance = sj.Select(c => new { c.Performance, c.TimePlayed }),
// KDR = stat.Kills / stat.Deaths,//KDR = sj.Select(c => new { KDR = c.Kills / (double)c.Deaths, c.TimePlayed }),
//TotalPlayTime = sj.Select(c => c.TimePlayed),
avg.First().stat.Client.LastConnection,//sj.First().Client.LastConnection,
avg.First().stat.Client.TotalConnectionTime,//sj.First().Client.TotalConnectionTime,
avg.First().stat.TimePlayed,
RatingId = 0
// todo: eventually replace this in favor of joining
//AverageHistory = context.Set<EFClientAverageStatHistory>().SingleOrDefault(r => r.ClientId == stat.ClientId)
})*/
.Skip(start)
.Take(count);
var clientIds = await iqClientIds.ToListAsync();
var stats = await iqClientIds.ToListAsync();
var iqStats = (from stat in context.Set<EFClientStatistics>()
join client in context.Clients
on stat.ClientId equals client.ClientId
where clientIds.Contains(client.ClientId)
select new
var groupedSelection = stats.GroupBy(c => c.ClientId).Select(s =>
new TopStatsInfo()
{
/* Name = s.First().Name,
// weighted based on time played
Performance = s.OrderByDescending(r => r.RatingId).First().Performance,
// ditto
KDR = s.First().Deaths == 0 ? s.First().Kills : (double)s.First().Kills / s.First().Deaths,
ClientId = s.First().ClientId,
Deaths = s.First().Deaths,
Kills = s.First().Kills,
LastSeen = Utilities.GetTimePassed(s.First().LastConnection, false),
TimePlayed = Math.Round(s.First().TotalConnectionTime / 3600.0, 1).ToString("#,##0"),
/ PerformanceHistory = s.AverageHistory == null || s.AverageHistory?.Ratings.Count < 2 ?
new List<double>()
{
client.CurrentAlias.Name,
client.ClientId,
stat.Kills,
stat.Deaths,
stat.EloRating,
stat.Skill,
stat.TimePlayed,
client.LastConnection,
client.TotalConnectionTime
});
var stats = await iqStats.ToListAsync();
var groupedSelection = stats.GroupBy(s => s.ClientId).Select(s =>
new TopStatsInfo()
{
Name = s.Select(c => c.Name).FirstOrDefault(),
// weighted based on time played
Performance = Math.Round
(s
.Where(c => (c.Skill + c.EloRating) / 2.0 > 0)
.Sum(c => (c.Skill + c.EloRating) / 2.0 * c.TimePlayed) /
s.Where(c => (c.Skill + c.EloRating) / 2.0 > 0)
.Sum(c => c.TimePlayed), 2),
// ditto
KDR = Math.Round(s
.Where(c => c.Deaths > 0)
.Sum(c => ((c.Kills / (double)c.Deaths) * c.TimePlayed) /
s.Where(d => d.Deaths > 0)
.Sum(d => d.TimePlayed)), 2),
ClientId = s.Select(c => c.ClientId).FirstOrDefault(),
Deaths = s.Sum(cs => cs.Deaths),
Kills = s.Sum(cs => cs.Kills),
LastSeen = Utilities.GetTimePassed(s.First().LastConnection, false),
TimePlayed = Math.Round(s.First().TotalConnectionTime / 3600.0, 1).ToString("#,##0"),
});
s.Performance,
s.Performance
} :
s.AverageHistory.Ratings.Select(r => Math.Round(r.Performance, 1)).ToList()*/
});
var statList = groupedSelection.OrderByDescending(s => s.Performance).ToList();
@ -215,8 +223,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
Active = true,
HitCount = 0,
Location = hl
})
.ToList()
}).ToList()
};
// insert if they've not been added
@ -437,7 +444,16 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
case Penalty.PenaltyType.Ban:
await attacker.Ban(Utilities.CurrentLocalization.LocalizationIndex["PLUGIN_STATS_CHEAT_DETECTED"], new Player()
{
ClientId = 1
ClientId = 1,
AdministeredPenalties = new List<EFPenalty>()
{
new EFPenalty()
{
AutomatedOffense = penalty.Type == Cheat.Detection.DetectionType.Bone ?
$"{penalty.Type}-{(int)penalty.Location}-{Math.Round(penalty.Value, 2)}@{penalty.HitCount}" :
$"{penalty.Type}-{Math.Round(penalty.Value, 2)}@{penalty.HitCount}",
}
}
});
break;
case Penalty.PenaltyType.Flag:
@ -546,6 +562,15 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
attackerStats.Skill = 0.0;
}
// update their performance
#if !DEBUG
if ((DateTime.UtcNow - attackerStats.LastStatHistoryUpdate).TotalMinutes >= 10)
#endif
{
await UpdateStatHistory(attacker, attackerStats);
attackerStats.LastStatHistoryUpdate = DateTime.UtcNow;
}
// todo: do we want to save this immediately?
var clientStatsSvc = ContextThreads[serverId].ClientStatSvc;
clientStatsSvc.Update(attackerStats);
@ -553,6 +578,132 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
await clientStatsSvc.SaveChangesAsync();
}
/// <summary>
/// Update the invidual and average stat history for a client
/// </summary>
/// <param name="client">client to update</param>
/// <param name="clientStats">stats of client that is being updated</param>
/// <returns></returns>
private async Task UpdateStatHistory(Player client, EFClientStatistics clientStats)
{
int currentServerTotalPlaytime = clientStats.TimePlayed + (int)(DateTime.UtcNow - client.LastConnection).TotalSeconds;
using (var ctx = new DatabaseContext())
{
// select the individual history for current server
var iqIndividualStatHistory = from statHistory in ctx.Set<EFClientStatHistory>()
where statHistory.ClientId == client.ClientId
where statHistory.ServerId == clientStats.ServerId
select statHistory;
// select the average history for current client
var iqAverageHistory = from stat in ctx.Set<EFClientAverageStatHistory>()
where stat.ClientId == client.ClientId
select stat;
// select all stats for current client
var iqClientStats = from stats in ctx.Set<EFClientStatistics>()
where stats.ClientId == client.ClientId
where stats.ServerId != clientStats.ServerId
select new
{
stats.Performance,
stats.TimePlayed
};
// get the client ranking for the current server
int individualClientRanking = await ctx.Set<EFClientStatHistory>()
.Where(c => c.ClientId != client.ClientId)
.Where(c => c.ServerId == clientStats.ServerId)
.Where(c => c.Ratings.OrderByDescending(r => r.RatingId).FirstOrDefault().Performance > clientStats.Performance)
.CountAsync() + 1;
var currentServerHistory = await iqIndividualStatHistory
.Include(r => r.Ratings)
.FirstOrDefaultAsync() ?? new EFClientStatHistory()
{
Active = true,
ClientId = client.ClientId,
Ratings = new List<EFRating>(),
ServerId = clientStats.ServerId
};
var averageHistory = await iqAverageHistory
.Include(r => r.Ratings)
.FirstOrDefaultAsync() ?? new EFClientAverageStatHistory()
{
ClientId = client.ClientId,
Ratings = new List<EFRating>(),
Active = true,
};
if (currentServerHistory.StatHistoryId == 0)
{
ctx.Add(currentServerHistory);
}
else
{
ctx.Update(currentServerHistory);
}
if (averageHistory.Ratings.Count == 0)
{
ctx.Add(averageHistory);
}
else
{
ctx.Update(averageHistory);
}
if (currentServerHistory.Ratings.Count > 30)
{
ctx.Entry(currentServerHistory.Ratings.First()).State = EntityState.Deleted;
currentServerHistory.Ratings.Remove(currentServerHistory.Ratings.First());
}
currentServerHistory.Ratings.Add(new EFRating()
{
Performance = clientStats.Performance,
Ranking = individualClientRanking,
Active = true,
ClientId = client.ClientId,
});
var clientStatsList = await iqClientStats.ToListAsync();
clientStatsList.Add(new
{
clientStats.Performance,
TimePlayed = currentServerTotalPlaytime
});
// weight the performance based on play time
var performanceAverage = clientStatsList.Sum(p => (p.Performance * p.TimePlayed)) / clientStatsList.Sum(p => p.TimePlayed);
int overallClientRanking = await ctx.Set<EFClientAverageStatHistory>()
.Where(c => c.ClientId != client.ClientId)
.Where(c => c.Ratings.OrderByDescending(r => r.RatingId).FirstOrDefault().Performance > performanceAverage)
.CountAsync() + 1;
if (averageHistory.Ratings.Count > 30)
{
ctx.Entry(averageHistory.Ratings.First()).State = EntityState.Deleted;
averageHistory.Ratings.Remove(averageHistory.Ratings.First());
}
averageHistory.Ratings.Add(new EFRating()
{
Performance = performanceAverage,
Ranking = overallClientRanking,
Active = true,
ClientId = client.ClientId,
});
await ctx.SaveChangesAsync();
}
}
/// <summary>
/// Performs the incrementation of kills and deaths for client statistics
/// </summary>

View File

@ -0,0 +1,22 @@
using SharedLibraryCore.Database.Models;
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Text;
namespace IW4MAdmin.Plugins.Stats.Models
{
public class EFClientAverageStatHistory : SharedEntity
{
[Key]
public int ClientId { get; set; }
[ForeignKey("ClientId")]
public virtual EFClient Client { get; set; }
public virtual ICollection<EFRating> Ratings { get; set; }
[Required]
public int LastRatingId { get; set; }
[ForeignKey("LastRatingId")]
public virtual EFRating LastRating { get; set; }
}
}

View File

@ -0,0 +1,23 @@
using IW4MAdmin.Plugins.Stats.Models;
using SharedLibraryCore.Database.Models;
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Text;
namespace IW4MAdmin.Plugins.Stats.Models
{
public class EFClientStatHistory : SharedEntity
{
[Key]
public int StatHistoryId { get; set; }
public int ClientId { get; set; }
[ForeignKey("ClientId")]
public virtual EFClient Client { get; set; }
public int ServerId { get; set; }
[ForeignKey("ServerId")]
public virtual EFServer Server { get; set; }
public virtual ICollection<EFRating> Ratings { get; set; }
}
}

View File

@ -96,5 +96,7 @@ namespace IW4MAdmin.Plugins.Stats.Models
private List<int> SessionScores = new List<int>() { 0 };
[NotMapped]
public IW4Info.Team Team { get; set; }
[NotMapped]
public DateTime LastStatHistoryUpdate { get; set; } = DateTime.UtcNow;
}
}

View File

@ -0,0 +1,22 @@
using SharedLibraryCore.Database.Models;
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Text;
namespace IW4MAdmin.Plugins.Stats.Models
{
public class EFRating : SharedEntity
{
[Key]
public int RatingId { get; set; }
public int ClientId { get; set; }
[ForeignKey("ClientId")]
public EFClient Client { get; set; }
[Required]
public double Performance { get; set; }
[Required]
public int Ranking { get; set; }
}
}

View File

@ -16,7 +16,7 @@ namespace IW4MAdmin.Plugins.Stats.Web.Controllers
ViewBag.Title = Utilities.CurrentLocalization.LocalizationIndex.Set["WEBFRONT_STATS_INDEX_TITLE"];
ViewBag.Description = Utilities.CurrentLocalization.LocalizationIndex.Set["WEBFRONT_STATS_INDEX_DESC"];
return View("Index", await Plugin.Manager.GetTopStats(0, 15));
return View("Index", await Plugin.Manager.GetTopStats(0, 25));
}
[HttpGet]

View File

@ -16,5 +16,6 @@ namespace IW4MAdmin.Plugins.Stats.Web.Dtos
public string LastSeen { get; set; }
public int Kills { get; set; }
public int Deaths { get; set; }
public List<double> PerformanceHistory { get; set; }
}
}

View File

@ -1,13 +1,14 @@
@model List<IW4MAdmin.Plugins.Stats.Web.Dtos.TopStatsInfo>
<h4 class="pb-2 text-center ">@ViewBag.Title</h4>
<div id="stats_top_players" class="row border-top border-bottom">
<div id="stats_top_players" class="striped border-top border-bottom">
@await Html.PartialAsync("_List", Model)
</div>
@section scripts {
<environment include="Development">
<script type="text/javascript" src="~/js/loader.js"></script>
</environment>
<script>initLoader('/Stats/GetTopPlayersAsync', '#stats_top_players');</script>
}
<environment include="Development">
<script type="text/javascript" src="~/js/loader.js"></script>
<script type="text/javascript" src="~/js/stats.js"></script>
</environment>
<script>initLoader('/Stats/GetTopPlayersAsync', '#stats_top_players');</script>
}

View File

@ -25,26 +25,25 @@
return "0_no-place/menu_div_no_place.png";
}
}
<table class="table table-striped mb-0" style="background-color:rgba(0, 0, 0, 0.1)">
@foreach (var stat in Model)
{
<tr>
<td style="vertical-align: middle">
<div class="">
<h2 class="text-muted">#@stat.Ranking &mdash; @Html.ActionLink(stat.Name, "ProfileAsync", "Client", new { id = stat.ClientId })</h2>
<span class="text-primary">@stat.Performance</span><span class="text-muted"> @loc["PLUGINS_STATS_COMMANDS_PERFORMANCE"]</span><br />
<span class="text-primary">@stat.KDR</span><span class="text-muted"> @loc["PLUGINS_STATS_TEXT_KDR"]</span>
<span class="text-primary">@stat.Kills</span><span class="text-muted"> @loc["PLUGINS_STATS_TEXT_KILLS"]</span>
<span class="text-primary">@stat.Deaths</span><span class="text-muted"> @loc["PLUGINS_STATS_TEXT_DEATHS"]</span><br />
<span class="text-muted">@loc["WEBFRONT_PROFILE_PLAYER"]</span> <span class="text-primary"> @stat.TimePlayed </span><span class="text-muted">@loc["GLOBAL_HOURS"]</span><br />
<span class="text-muted">@loc["WEBFRONT_PROFILE_LSEEN"]</span><span class="text-primary"> @stat.LastSeen </span><span class="text-muted">@loc["WEBFRONT_PENALTY_TEMPLATE_AGO"]</span>
</div>
</td>
<td class="text-right ml-0 pl-0" style="vertical-align: middle">
<div>
<img src="/images/icons/@rankIcon(stat.Performance)" />
</div>
</td>
</tr>
}
</table>
@foreach (var stat in Model)
{
<div class="row ml-0 mr-0 pt-2 pb-2">
<div class="col-md-4 text-md-left text-center">
<h2 class="text-muted">#@stat.Ranking &mdash; @Html.ActionLink(stat.Name, "ProfileAsync", "Client", new { id = stat.ClientId })</h2>
<span class="text-primary">@stat.Performance</span><span class="text-muted"> @loc["PLUGINS_STATS_COMMANDS_PERFORMANCE"]</span><br />
<span class="text-primary">@stat.KDR</span><span class="text-muted"> @loc["PLUGINS_STATS_TEXT_KDR"]</span>
<span class="text-primary">@stat.Kills</span><span class="text-muted"> @loc["PLUGINS_STATS_TEXT_KILLS"]</span>
<span class="text-primary">@stat.Deaths</span><span class="text-muted"> @loc["PLUGINS_STATS_TEXT_DEATHS"]</span><br />
<span class="text-muted">@loc["WEBFRONT_PROFILE_PLAYER"]</span> <span class="text-primary"> @stat.TimePlayed </span><span class="text-muted">@loc["GLOBAL_HOURS"]</span><br />
<span class="text-muted">@loc["WEBFRONT_PROFILE_LSEEN"]</span><span class="text-primary"> @stat.LastSeen </span><span class="text-muted">@loc["WEBFRONT_PENALTY_TEMPLATE_AGO"]</span>
</div>
<div class="col-md-6 client-rating-graph" id="rating_history_@stat.ClientId" data-history="@Html.Raw(Json.Serialize(stat.PerformanceHistory))">
</div>
<div class="col-md-2 client-rating-icon text-md-right text-center align-items-center d-flex justify-content-center">
<img src="/images/icons/@rankIcon(stat.Performance)" />
</div>
</div>
}

View File

@ -30,6 +30,7 @@ namespace SharedLibraryCore.Database.Models
public DateTime Expires { get; set; }
[Required]
public string Offense { get; set; }
public string AutomatedOffense { get; set; }
public Objects.Penalty.PenaltyType Type { get; set; }
}
}

View File

@ -14,6 +14,7 @@ namespace SharedLibraryCore.Dtos
public int PunisherId { get; set; }
public string PunisherLevel { get; set; }
public string Offense { get; set; }
public string AutomatedOffense { get; set; }
public string Type { get; set; }
public string TimePunished { get; set; }
public string TimeRemaining { get; set; }

View File

@ -0,0 +1,536 @@
// <auto-generated />
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage;
using Microsoft.EntityFrameworkCore.Storage.Internal;
using SharedLibraryCore.Database;
using SharedLibraryCore.Objects;
using System;
namespace SharedLibraryCore.Migrations
{
[DbContext(typeof(DatabaseContext))]
[Migration("20180529233328_AddAutomatedOffenseAndRatingHistory")]
partial class AddAutomatedOffenseAndRatingHistory
{
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "2.0.2-rtm-10011");
modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFClientAverageStatHistory", b =>
{
b.Property<int>("ClientId");
b.Property<bool>("Active");
b.HasKey("ClientId");
b.ToTable("EFClientAverageStatHistory");
});
modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFClientKill", b =>
{
b.Property<long>("KillId")
.ValueGeneratedOnAdd();
b.Property<bool>("Active");
b.Property<int>("AttackerId");
b.Property<int>("Damage");
b.Property<int?>("DeathOriginVector3Id");
b.Property<int>("DeathType");
b.Property<int>("HitLoc");
b.Property<int?>("KillOriginVector3Id");
b.Property<int>("Map");
b.Property<int>("ServerId");
b.Property<int>("VictimId");
b.Property<int?>("ViewAnglesVector3Id");
b.Property<int>("Weapon");
b.Property<DateTime>("When");
b.HasKey("KillId");
b.HasIndex("AttackerId");
b.HasIndex("DeathOriginVector3Id");
b.HasIndex("KillOriginVector3Id");
b.HasIndex("ServerId");
b.HasIndex("VictimId");
b.HasIndex("ViewAnglesVector3Id");
b.ToTable("EFClientKills");
});
modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFClientMessage", b =>
{
b.Property<long>("MessageId")
.ValueGeneratedOnAdd();
b.Property<bool>("Active");
b.Property<int>("ClientId");
b.Property<string>("Message");
b.Property<int>("ServerId");
b.Property<DateTime>("TimeSent");
b.HasKey("MessageId");
b.HasIndex("ClientId");
b.HasIndex("ServerId");
b.ToTable("EFClientMessages");
});
modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFClientStatHistory", b =>
{
b.Property<int>("StatHistoryId")
.ValueGeneratedOnAdd();
b.Property<bool>("Active");
b.Property<int>("ClientId");
b.Property<int>("ServerId");
b.HasKey("StatHistoryId");
b.HasIndex("ClientId");
b.HasIndex("ServerId");
b.ToTable("EFClientStatHistory");
});
modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFClientStatistics", b =>
{
b.Property<int>("ClientId");
b.Property<int>("ServerId");
b.Property<bool>("Active");
b.Property<int>("Deaths");
b.Property<double>("EloRating");
b.Property<int>("Kills");
b.Property<double>("MaxStrain");
b.Property<double>("RollingWeightedKDR");
b.Property<double>("SPM");
b.Property<double>("Skill");
b.Property<int>("TimePlayed");
b.HasKey("ClientId", "ServerId");
b.HasIndex("ServerId");
b.ToTable("EFClientStatistics");
});
modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFHitLocationCount", b =>
{
b.Property<int>("HitLocationCountId")
.ValueGeneratedOnAdd();
b.Property<bool>("Active");
b.Property<int>("ClientId")
.HasColumnName("EFClientStatistics_ClientId");
b.Property<int>("HitCount");
b.Property<float>("HitOffsetAverage");
b.Property<int>("Location");
b.Property<float>("MaxAngleDistance");
b.Property<int>("ServerId")
.HasColumnName("EFClientStatistics_ServerId");
b.HasKey("HitLocationCountId");
b.HasIndex("ServerId");
b.HasIndex("ClientId", "ServerId");
b.ToTable("EFHitLocationCounts");
});
modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFRating", b =>
{
b.Property<int>("RatingId")
.ValueGeneratedOnAdd();
b.Property<bool>("Active");
b.Property<int>("ClientId");
b.Property<int?>("EFClientAverageStatHistoryClientId");
b.Property<int?>("EFClientStatHistoryStatHistoryId");
b.Property<double>("Performance");
b.Property<int>("Ranking");
b.HasKey("RatingId");
b.HasIndex("ClientId");
b.HasIndex("EFClientAverageStatHistoryClientId");
b.HasIndex("EFClientStatHistoryStatHistoryId");
b.ToTable("EFRating");
});
modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFServer", b =>
{
b.Property<int>("ServerId");
b.Property<bool>("Active");
b.Property<int>("Port");
b.HasKey("ServerId");
b.ToTable("EFServers");
});
modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFServerStatistics", b =>
{
b.Property<int>("StatisticId")
.ValueGeneratedOnAdd();
b.Property<bool>("Active");
b.Property<int>("ServerId");
b.Property<long>("TotalKills");
b.Property<long>("TotalPlayTime");
b.HasKey("StatisticId");
b.HasIndex("ServerId");
b.ToTable("EFServerStatistics");
});
modelBuilder.Entity("SharedLibraryCore.Database.Models.EFAlias", b =>
{
b.Property<int>("AliasId")
.ValueGeneratedOnAdd();
b.Property<bool>("Active");
b.Property<DateTime>("DateAdded");
b.Property<int>("IPAddress");
b.Property<int>("LinkId");
b.Property<string>("Name")
.IsRequired();
b.HasKey("AliasId");
b.HasIndex("LinkId");
b.ToTable("EFAlias");
});
modelBuilder.Entity("SharedLibraryCore.Database.Models.EFAliasLink", b =>
{
b.Property<int>("AliasLinkId")
.ValueGeneratedOnAdd();
b.Property<bool>("Active");
b.HasKey("AliasLinkId");
b.ToTable("EFAliasLinks");
});
modelBuilder.Entity("SharedLibraryCore.Database.Models.EFClient", b =>
{
b.Property<int>("ClientId")
.ValueGeneratedOnAdd();
b.Property<bool>("Active");
b.Property<int>("AliasLinkId");
b.Property<int>("Connections");
b.Property<int>("CurrentAliasId");
b.Property<DateTime>("FirstConnection");
b.Property<DateTime>("LastConnection");
b.Property<int>("Level");
b.Property<bool>("Masked");
b.Property<long>("NetworkId");
b.Property<string>("Password");
b.Property<string>("PasswordSalt");
b.Property<int>("TotalConnectionTime");
b.HasKey("ClientId");
b.HasIndex("AliasLinkId");
b.HasIndex("CurrentAliasId");
b.HasIndex("NetworkId")
.IsUnique();
b.ToTable("EFClients");
});
modelBuilder.Entity("SharedLibraryCore.Database.Models.EFPenalty", b =>
{
b.Property<int>("PenaltyId")
.ValueGeneratedOnAdd();
b.Property<bool>("Active");
b.Property<string>("AutomatedOffense");
b.Property<DateTime>("Expires");
b.Property<int>("LinkId");
b.Property<int>("OffenderId");
b.Property<string>("Offense")
.IsRequired();
b.Property<int>("PunisherId");
b.Property<int>("Type");
b.Property<DateTime>("When");
b.HasKey("PenaltyId");
b.HasIndex("LinkId");
b.HasIndex("OffenderId");
b.HasIndex("PunisherId");
b.ToTable("EFPenalties");
});
modelBuilder.Entity("SharedLibraryCore.Helpers.Vector3", b =>
{
b.Property<int>("Vector3Id")
.ValueGeneratedOnAdd();
b.Property<float>("X");
b.Property<float>("Y");
b.Property<float>("Z");
b.HasKey("Vector3Id");
b.ToTable("Vector3");
});
modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFClientAverageStatHistory", b =>
{
b.HasOne("SharedLibraryCore.Database.Models.EFClient", "Client")
.WithMany()
.HasForeignKey("ClientId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFClientKill", b =>
{
b.HasOne("SharedLibraryCore.Database.Models.EFClient", "Attacker")
.WithMany()
.HasForeignKey("AttackerId")
.OnDelete(DeleteBehavior.Cascade);
b.HasOne("SharedLibraryCore.Helpers.Vector3", "DeathOrigin")
.WithMany()
.HasForeignKey("DeathOriginVector3Id");
b.HasOne("SharedLibraryCore.Helpers.Vector3", "KillOrigin")
.WithMany()
.HasForeignKey("KillOriginVector3Id");
b.HasOne("IW4MAdmin.Plugins.Stats.Models.EFServer", "Server")
.WithMany()
.HasForeignKey("ServerId")
.OnDelete(DeleteBehavior.Cascade);
b.HasOne("SharedLibraryCore.Database.Models.EFClient", "Victim")
.WithMany()
.HasForeignKey("VictimId")
.OnDelete(DeleteBehavior.Cascade);
b.HasOne("SharedLibraryCore.Helpers.Vector3", "ViewAngles")
.WithMany()
.HasForeignKey("ViewAnglesVector3Id");
});
modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFClientMessage", b =>
{
b.HasOne("SharedLibraryCore.Database.Models.EFClient", "Client")
.WithMany()
.HasForeignKey("ClientId")
.OnDelete(DeleteBehavior.Cascade);
b.HasOne("IW4MAdmin.Plugins.Stats.Models.EFServer", "Server")
.WithMany()
.HasForeignKey("ServerId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFClientStatHistory", b =>
{
b.HasOne("SharedLibraryCore.Database.Models.EFClient", "Client")
.WithMany()
.HasForeignKey("ClientId")
.OnDelete(DeleteBehavior.Cascade);
b.HasOne("IW4MAdmin.Plugins.Stats.Models.EFServer", "Server")
.WithMany()
.HasForeignKey("ServerId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFClientStatistics", b =>
{
b.HasOne("SharedLibraryCore.Database.Models.EFClient", "Client")
.WithMany()
.HasForeignKey("ClientId")
.OnDelete(DeleteBehavior.Cascade);
b.HasOne("IW4MAdmin.Plugins.Stats.Models.EFServer", "Server")
.WithMany()
.HasForeignKey("ServerId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFHitLocationCount", b =>
{
b.HasOne("SharedLibraryCore.Database.Models.EFClient", "Client")
.WithMany()
.HasForeignKey("ClientId")
.OnDelete(DeleteBehavior.Cascade);
b.HasOne("IW4MAdmin.Plugins.Stats.Models.EFServer", "Server")
.WithMany()
.HasForeignKey("ServerId")
.OnDelete(DeleteBehavior.Cascade);
b.HasOne("IW4MAdmin.Plugins.Stats.Models.EFClientStatistics")
.WithMany("HitLocations")
.HasForeignKey("ClientId", "ServerId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFRating", b =>
{
b.HasOne("SharedLibraryCore.Database.Models.EFClient", "Client")
.WithMany()
.HasForeignKey("ClientId")
.OnDelete(DeleteBehavior.Cascade);
b.HasOne("IW4MAdmin.Plugins.Stats.Models.EFClientAverageStatHistory")
.WithMany("Ratings")
.HasForeignKey("EFClientAverageStatHistoryClientId");
b.HasOne("IW4MAdmin.Plugins.Stats.Models.EFClientStatHistory")
.WithMany("Ratings")
.HasForeignKey("EFClientStatHistoryStatHistoryId");
});
modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFServerStatistics", b =>
{
b.HasOne("IW4MAdmin.Plugins.Stats.Models.EFServer", "Server")
.WithMany()
.HasForeignKey("ServerId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("SharedLibraryCore.Database.Models.EFAlias", b =>
{
b.HasOne("SharedLibraryCore.Database.Models.EFAliasLink", "Link")
.WithMany("Children")
.HasForeignKey("LinkId")
.OnDelete(DeleteBehavior.Restrict);
});
modelBuilder.Entity("SharedLibraryCore.Database.Models.EFClient", b =>
{
b.HasOne("SharedLibraryCore.Database.Models.EFAliasLink", "AliasLink")
.WithMany()
.HasForeignKey("AliasLinkId")
.OnDelete(DeleteBehavior.Cascade);
b.HasOne("SharedLibraryCore.Database.Models.EFAlias", "CurrentAlias")
.WithMany()
.HasForeignKey("CurrentAliasId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("SharedLibraryCore.Database.Models.EFPenalty", b =>
{
b.HasOne("SharedLibraryCore.Database.Models.EFAliasLink", "Link")
.WithMany("ReceivedPenalties")
.HasForeignKey("LinkId")
.OnDelete(DeleteBehavior.Cascade);
b.HasOne("SharedLibraryCore.Database.Models.EFClient", "Offender")
.WithMany("ReceivedPenalties")
.HasForeignKey("OffenderId")
.OnDelete(DeleteBehavior.Restrict);
b.HasOne("SharedLibraryCore.Database.Models.EFClient", "Punisher")
.WithMany("AdministeredPenalties")
.HasForeignKey("PunisherId")
.OnDelete(DeleteBehavior.Restrict);
});
#pragma warning restore 612, 618
}
}
}

View File

@ -0,0 +1,139 @@
using Microsoft.EntityFrameworkCore.Migrations;
using System;
using System.Collections.Generic;
namespace SharedLibraryCore.Migrations
{
public partial class AddAutomatedOffenseAndRatingHistory : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "AutomatedOffense",
table: "EFPenalties",
nullable: true);
migrationBuilder.CreateTable(
name: "EFClientAverageStatHistory",
columns: table => new
{
ClientId = table.Column<int>(nullable: false),
Active = table.Column<bool>(nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_EFClientAverageStatHistory", x => x.ClientId);
table.ForeignKey(
name: "FK_EFClientAverageStatHistory_EFClients_ClientId",
column: x => x.ClientId,
principalTable: "EFClients",
principalColumn: "ClientId",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "EFClientStatHistory",
columns: table => new
{
StatHistoryId = table.Column<int>(nullable: false)
.Annotation("Sqlite:Autoincrement", true),
Active = table.Column<bool>(nullable: false),
ClientId = table.Column<int>(nullable: false),
ServerId = table.Column<int>(nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_EFClientStatHistory", x => x.StatHistoryId);
table.ForeignKey(
name: "FK_EFClientStatHistory_EFClients_ClientId",
column: x => x.ClientId,
principalTable: "EFClients",
principalColumn: "ClientId",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_EFClientStatHistory_EFServers_ServerId",
column: x => x.ServerId,
principalTable: "EFServers",
principalColumn: "ServerId",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "EFRating",
columns: table => new
{
RatingId = table.Column<int>(nullable: false)
.Annotation("Sqlite:Autoincrement", true),
Active = table.Column<bool>(nullable: false),
ClientId = table.Column<int>(nullable: false),
EFClientAverageStatHistoryClientId = table.Column<int>(nullable: true),
EFClientStatHistoryStatHistoryId = table.Column<int>(nullable: true),
Performance = table.Column<double>(nullable: false),
Ranking = table.Column<int>(nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_EFRating", x => x.RatingId);
table.ForeignKey(
name: "FK_EFRating_EFClients_ClientId",
column: x => x.ClientId,
principalTable: "EFClients",
principalColumn: "ClientId",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_EFRating_EFClientAverageStatHistory_EFClientAverageStatHistoryClientId",
column: x => x.EFClientAverageStatHistoryClientId,
principalTable: "EFClientAverageStatHistory",
principalColumn: "ClientId",
onDelete: ReferentialAction.Restrict);
table.ForeignKey(
name: "FK_EFRating_EFClientStatHistory_EFClientStatHistoryStatHistoryId",
column: x => x.EFClientStatHistoryStatHistoryId,
principalTable: "EFClientStatHistory",
principalColumn: "StatHistoryId",
onDelete: ReferentialAction.Restrict);
});
migrationBuilder.CreateIndex(
name: "IX_EFClientStatHistory_ClientId",
table: "EFClientStatHistory",
column: "ClientId");
migrationBuilder.CreateIndex(
name: "IX_EFClientStatHistory_ServerId",
table: "EFClientStatHistory",
column: "ServerId");
migrationBuilder.CreateIndex(
name: "IX_EFRating_ClientId",
table: "EFRating",
column: "ClientId");
migrationBuilder.CreateIndex(
name: "IX_EFRating_EFClientAverageStatHistoryClientId",
table: "EFRating",
column: "EFClientAverageStatHistoryClientId");
migrationBuilder.CreateIndex(
name: "IX_EFRating_EFClientStatHistoryStatHistoryId",
table: "EFRating",
column: "EFClientStatHistoryStatHistoryId");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "EFRating");
migrationBuilder.DropTable(
name: "EFClientAverageStatHistory");
migrationBuilder.DropTable(
name: "EFClientStatHistory");
migrationBuilder.DropColumn(
name: "AutomatedOffense",
table: "EFPenalties");
}
}
}

View File

@ -20,6 +20,17 @@ namespace SharedLibraryCore.Migrations
modelBuilder
.HasAnnotation("ProductVersion", "2.0.2-rtm-10011");
modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFClientAverageStatHistory", b =>
{
b.Property<int>("ClientId");
b.Property<bool>("Active");
b.HasKey("ClientId");
b.ToTable("EFClientAverageStatHistory");
});
modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFClientKill", b =>
{
b.Property<long>("KillId")
@ -92,6 +103,26 @@ namespace SharedLibraryCore.Migrations
b.ToTable("EFClientMessages");
});
modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFClientStatHistory", b =>
{
b.Property<int>("StatHistoryId")
.ValueGeneratedOnAdd();
b.Property<bool>("Active");
b.Property<int>("ClientId");
b.Property<int>("ServerId");
b.HasKey("StatHistoryId");
b.HasIndex("ClientId");
b.HasIndex("ServerId");
b.ToTable("EFClientStatHistory");
});
modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFClientStatistics", b =>
{
b.Property<int>("ClientId");
@ -153,6 +184,34 @@ namespace SharedLibraryCore.Migrations
b.ToTable("EFHitLocationCounts");
});
modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFRating", b =>
{
b.Property<int>("RatingId")
.ValueGeneratedOnAdd();
b.Property<bool>("Active");
b.Property<int>("ClientId");
b.Property<int?>("EFClientAverageStatHistoryClientId");
b.Property<int?>("EFClientStatHistoryStatHistoryId");
b.Property<double>("Performance");
b.Property<int>("Ranking");
b.HasKey("RatingId");
b.HasIndex("ClientId");
b.HasIndex("EFClientAverageStatHistoryClientId");
b.HasIndex("EFClientStatHistoryStatHistoryId");
b.ToTable("EFRating");
});
modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFServer", b =>
{
b.Property<int>("ServerId");
@ -269,6 +328,8 @@ namespace SharedLibraryCore.Migrations
b.Property<bool>("Active");
b.Property<string>("AutomatedOffense");
b.Property<DateTime>("Expires");
b.Property<int>("LinkId");
@ -311,6 +372,14 @@ namespace SharedLibraryCore.Migrations
b.ToTable("Vector3");
});
modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFClientAverageStatHistory", b =>
{
b.HasOne("SharedLibraryCore.Database.Models.EFClient", "Client")
.WithMany()
.HasForeignKey("ClientId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFClientKill", b =>
{
b.HasOne("SharedLibraryCore.Database.Models.EFClient", "Attacker")
@ -354,6 +423,19 @@ namespace SharedLibraryCore.Migrations
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFClientStatHistory", b =>
{
b.HasOne("SharedLibraryCore.Database.Models.EFClient", "Client")
.WithMany()
.HasForeignKey("ClientId")
.OnDelete(DeleteBehavior.Cascade);
b.HasOne("IW4MAdmin.Plugins.Stats.Models.EFServer", "Server")
.WithMany()
.HasForeignKey("ServerId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFClientStatistics", b =>
{
b.HasOne("SharedLibraryCore.Database.Models.EFClient", "Client")
@ -385,6 +467,22 @@ namespace SharedLibraryCore.Migrations
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFRating", b =>
{
b.HasOne("SharedLibraryCore.Database.Models.EFClient", "Client")
.WithMany()
.HasForeignKey("ClientId")
.OnDelete(DeleteBehavior.Cascade);
b.HasOne("IW4MAdmin.Plugins.Stats.Models.EFClientAverageStatHistory")
.WithMany("Ratings")
.HasForeignKey("EFClientAverageStatHistoryClientId");
b.HasOne("IW4MAdmin.Plugins.Stats.Models.EFClientStatHistory")
.WithMany("Ratings")
.HasForeignKey("EFClientStatHistoryStatHistoryId");
});
modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFServerStatistics", b =>
{
b.HasOne("IW4MAdmin.Plugins.Stats.Models.EFServer", "Server")

View File

@ -28,6 +28,7 @@ namespace SharedLibraryCore.Services
Expires = newEntity.Expires,
Offense = newEntity.Offense,
When = newEntity.When,
AutomatedOffense = newEntity.AutomatedOffense
};
if (addedEntity.Expires == DateTime.MaxValue)
@ -162,7 +163,8 @@ namespace SharedLibraryCore.Services
PunisherId = penalty.PunisherId,
Offense = penalty.Offense,
Type = penalty.Type.ToString(),
TimeRemaining = now > penalty.Expires ? "" : penalty.Expires.ToString()
TimeRemaining = now > penalty.Expires ? "" : penalty.Expires.ToString(),
AutomatedOffense = penalty.AutomatedOffense
},
When = penalty.When,
Sensitive = penalty.Type == Penalty.PenaltyType.Flag
@ -206,7 +208,8 @@ namespace SharedLibraryCore.Services
PunisherName = punisherAlias.Name,
PunisherId = penalty.PunisherId,
Offense = penalty.Offense,
Type = penalty.Type.ToString()
Type = penalty.Type.ToString(),
AutomatedOffense = penalty.AutomatedOffense
},
When = penalty.When,
Sensitive = penalty.Type == Penalty.PenaltyType.Flag

View File

@ -43,7 +43,8 @@ namespace WebfrontCore.Controllers
PunisherId = p.PunisherId,
Type = p.Type.ToString(),
TimePunished = p.When.ToString(),
TimeRemaining = p.Expires.ToString()
TimeRemaining = p.Expires.ToString(),
AutomatedOffense = p.AutomatedOffense
}).ToList();
return Json(penaltiesDto);

View File

@ -20,11 +20,12 @@ namespace WebfrontCore.ViewComponents
PunisherId = p.PunisherId,
PunisherName = p.Punisher.Name,
PunisherLevel = p.Punisher.Level.ToString(),
Offense = p.Offense,
Offense = User.Identity.IsAuthenticated && !string.IsNullOrEmpty(p.AutomatedOffense) ? p.AutomatedOffense : p.Offense,
Type = p.Type.ToString(),
TimePunished = Utilities.GetTimePassed(p.When, false),
TimeRemaining = DateTime.UtcNow > p.Expires ? "" : Utilities.TimeSpanText(p.Expires - DateTime.UtcNow),
Sensitive = p.Type == Penalty.PenaltyType.Flag
Sensitive = p.Type == Penalty.PenaltyType.Flag,
AutomatedOffense = p.AutomatedOffense
});
penaltiesDto = User.Identity.IsAuthenticated ? penaltiesDto.ToList() : penaltiesDto.Where(p => !p.Sensitive).ToList();

View File

@ -23,7 +23,8 @@
"wwwroot/js/profile.js",
"wwwroot/js/server.js",
"wwwroot/js/search.js",
"wwwroot/js/loader.js"
"wwwroot/js/loader.js",
"wwwroot/js/stats.js"
],
// Optionally specify minification options
"minify": {

View File

@ -179,7 +179,18 @@ select {
margin-top: -3px;
}
.stats-ranking-icon {
width: 32px;
height: 32px;
.striped > div:nth-child(even) {
background-color: rgba(0, 0, 0, 0.125);
}
.striped > div:nth-child(odd) {
background-color: rgba(0, 0, 0, 0.2);
}
.client-rating-graph {
min-height: 100px;
}
.client-rating-icon {
}

View File

@ -1,6 +1,6 @@
let offset = 15;
let loadCount = 15;
let isLoading = false;
let loaderOffset = 25;
let loadCount = 25;
let isLoaderLoading = false;
let loadUri = '';
let loaderResponseId = '';
@ -11,26 +11,26 @@ function initLoader(location, loaderId) {
}
function loadMoreItems() {
if (isLoading) {
if (isLoaderLoading) {
return false;
}
showLoader();
isLoading = true;
$.get(loadUri, { offset: offset, count : loadCount })
isLoaderLoading = true;
$.get(loadUri, { offset: loaderOffset, count : loadCount })
.done(function (response) {
$(loaderResponseId).append(response);
if (response.trim().length === 0) {
staleLoader();
}
hideLoader();
isLoading = false;
isLoaderLoading = false;
})
.fail(function (jqxhr, statis, error) {
errorLoader();
isLoading = false;
isLoaderLoading = false;
});
offset += loadCount;
loaderOffset += loadCount;
}
function setupListeners() {

View File

@ -0,0 +1,61 @@
function getStatsChart(id, width, height) {
const data = $('#' + id).data('history');
let fixedData = [];
data.forEach(function (item, i) {
fixedData[i] = { x: i, y: item };
});
return new CanvasJS.Chart(id, {
backgroundColor: 'transparent',
height: height,
width: width,
animationEnabled: false,
toolTip: {
contentFormatter: function (e) {
return e.entries[0].dataPoint.y;
}
},
axisX: {
interval: 1,
gridThickness: 0,
lineThickness: 0,
tickThickness: 0,
margin: 0,
valueFormatString: " "
},
axisY: {
gridThickness: 0,
lineThickness: 0,
tickThickness: 0,
minimum: Math.min(...data) - 15,
maximum: Math.max(...data) + 15,
margin: 0,
valueFormatString: " ",
labelMaxWidth: 0
},
legend: {
maxWidth: 0,
maxHeight: 0,
dockInsidePlotArea: true
},
data: [{
showInLegend: false,
type: "splineArea",
color: 'rgba(0, 122, 204, 0.25)',
markerSize: 0,
dataPoints: fixedData
}]
});
}
$(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();
});
});
});