AHL7GZHU4BEZRHHZJMSHOGN6LKHRTAM32ENCQNG43STOQAVHZ54AC
VLR6TJHWXTTIRSNQAPA5QWYJTCGEDY73KTFLLHXLBNB7YXELTVIQC
PKMGWEOGUJV5BZ2IAPJFLYK5X4LC6WTCBT7TLLXNBSFPVGVKX56QC
B2VAHDYSPO7POKVPB2UBFJ2RFKMDUINBWXNBBXS4UL3LXBOYMDTAC
2GEG6ZQ27IJCPN5ZJIVTQT7EB3ML2X3SGIFUM2EWWEKU5Q5HRRJAC
N7TZV5WJKLYKSG6SIVAID4ZCC76YHZU5U5WWQGG6TFKAV6CE3Y6AC
B7W4Q522DLB6DKH2TFDOCTSZFZLFTOLCT6CCZEOC3V3UUMSAOOFAC
BZWC6XMOUXEPOI7P65MREJZ45TSQRFMMR2SQ7LML7RMCKUL57VHAC
7QS7R2ORWCZMTCE3SD5PJ5ZL27N3RS7YCWBHXH3JDPTHDQ3KBAMQC
BYVNJI7UUHLRECHFPANFHBW5MTI6P335HTWZWNZABC3CHGXE3MCQC
HXRDRHIVGSBEAMTKJFZK43MSB53BKON6F77AARUQNWFUPSTZSBPAC
62OPHDLT2IIHK2OEH76NWB6V7E2Z5USUBNHB4X3DP2XLDI3YHY7QC
GGREOYIRZE2GH62X7YXQA7IUSSB25ENLQ5OKVJYWPNI462DP37FAC
ZCUDVIEBTLDSQPW472DG7SLMX3FE33J4DESMLCIBBJJFMGDW76VQC
OWZCU6OX33FKWEEI7LUWIZZTBPUP2OSLJIADPHIBYOWGPPRIPMIAC
4Y7NTBMDC33ZZSLJE7MQEL6MBXEZCEKQ5Y5IK5QLZ5VQW5FAHAXAC
Q4BYGYNX3UZXX47EMX2GWX6M6ICSMIMCLQ7HVOYS4HB4RHYI4NEQC
OLK5DJL5RF42B5I3NFVXJYPC7NWSJZ4VN77U3CQJH2YWMA7VC33AC
}
#[derive(SimpleObject, Clone, Debug, QueryableByName)]
pub struct DistInfo {
#[sql_type = "Double"]
pub min: f64,
#[sql_type = "Double"]
pub max: f64,
#[sql_type = "BigInt"]
pub count: i64,
#[sql_type = "Array<Double>"]
pub quantiles: Vec<f64>,
#[sql_type = "Nullable<Double>"]
pub actual_pct: Option<f64>,
#[derive(Debug)]
pub struct DistributionData<'a> {
stats: &'a EncounterStats,
}
fn dist_info_for(
con: &PgConnection,
actual: i32,
encounter_id: i32,
difficulty: i32,
column: &str,
) -> Result<Option<DistInfo>> {
// god, what have i wrought
let agg = diesel::sql_query(format!(
"with a as (select count(*) as c from encounter_stats where encounter_id = {encounter_id} and difficulty = {difficulty} and kill_log is not null and {column} <= {actual})
select count({column}) as count,
min({column})::double precision as min,
max({column})::double precision as max,
percentile_cont(array[0.25, 0.5, 0.75]) within group (order by {column}) as quantiles,
(select c::double precision from a) / count({column})::double precision as actual_pct
from encounter_stats where encounter_id = {encounter_id} and difficulty = {difficulty} and kill_log is not null;",
actual = actual,
column = column,
encounter_id = encounter_id,
difficulty = difficulty
))
.get_result::<DistInfo>(con)
.optional()?;
Ok(agg)
}
#[Object]
impl<'a> DistributionData<'a> {
pub async fn pull_count(&self, ctx: &Context<'_>) -> Result<Option<DistInfo>> {
let Data { conpool, .. } = ctx.data()?;
dist_info_for(
&*conpool.get()?,
self.stats.pull_count,
self.stats.encounter_id,
self.stats.difficulty,
"pull_count",
)
}
pub async fn prog_time(&self, ctx: &Context<'_>) -> Result<Option<DistInfo>> {
let Data { conpool, .. } = ctx.data()?;
dist_info_for(
&*conpool.get()?,
self.stats.prog_time,
self.stats.encounter_id,
self.stats.difficulty,
"prog_time",
)
}
}
team, encounterId, difficulty, killLog { iid, startTime }, killWeek, pullCount, progTime, ilvlMin, ilvlMax
team, encounterId, difficulty, killLog { iid, startTime }, killWeek, pullCount, progTime, ilvlMin, ilvlMax,
distributions {
pullCount { count, min, max, quantiles, actualPct },
progTime { count, min, max, quantiles, actualPct }
}
const DistPlot = createClassFromSpec({
mode: 'vega-lite',
spec: {
bounds: "flush",
config: {
padding: 1,
view: {
stroke: "transparent"
},
},
width: 60,
height: 12,
background: '#00000000',
data: { name: 'dist' },
layer: [
{
mark: { 'type': 'rule', opacity: 0 },
encoding: {
x: { field: 'min', 'type': 'quantitative', 'scale': { zero: false }, title: null, sort: "descending" },
'x2': { field: 'max', 'type': 'quantitative' }
}
},
{
mark: 'rule',
encoding: {
x: { field: 'lower', 'type': 'quantitative', 'scale': { zero: false }, title: null, axis: null },
'x2': { field: 'upper', 'type': 'quantitative' }
}
},
{
mark: { 'type': 'bar', color: 'gray', fill: 'lightGray'},
encoding: {
x: { field: 'q1', 'type': 'quantitative', 'scale': { zero: false }, title: null },
'x2': { field: 'q3', 'type': 'quantitative' }
}
},
{
mark: { 'type': 'tick', color: 'black' },
encoding: {
x: { field: 'median', 'type': 'quantitative' }
}
},
{
mark: { 'type': 'point', color: 'rgb(31, 119, 180)', fill: 'rgb(31, 119, 180)', opacity: 1 },
data: { name: 'actual' },
encoding: {
x: { field: 'x', 'type': 'quantitative' }
}
}
]
}
});
export function EncounterDistChart({ id, dist, actual }: { actual: number; dist: DistInfo; id: string; }) {
if(dist.count <= 100 && process.env.NODE_ENV !== "development") {
return null;
}
const scaled_pct = dist.actualPct ? Math.floor((1 - dist.actualPct) * 100) : 0;
const suffix = (scaled_pct >= 10 && scaled_pct <= 20) ? 'th' : ["st", "nd", "rd"][(scaled_pct % 10) - 1] || "th";
const iqr = dist.quantiles[2] - dist.quantiles[0];
const lower = dist.quantiles[0] - 1.5 * iqr;
const upper = dist.quantiles[2] + 1.5 * iqr;
const data = [
{
min: dist.min,
max: dist.max,
lower, upper,
'q1': dist.quantiles[0],
'q3': dist.quantiles[2],
median: dist.quantiles[1],
},
];
return (
<span data-tip={`${scaled_pct}${suffix} Percentile`} data-for={id}><DistPlot data={{ dist: data, actual: [{ x: actual }] }} actions={false} className="align-text-bottom" /></span>
);
}
<dl className="grid grid-cols-2 gap-x-2">
<dt>Killed On:</dt>
{killLog ? <dd>{formatDate(new Date(killLog.startTime))}</dd> : <dd><em>In Progress</em></dd>}
<dt>Week of Tier:</dt>
{killWeek ? <dd>Week {killWeek}</dd> : <dd><em>In Progress</em></dd>}
<dt>Pull Count:</dt>
<LightTooltip id={pctid} />
<dl className="grid grid-cols-4 gap-x-2">
<dt className="col-span-2">Killed On:</dt>
<dd className="col-span-2">{killLog ? formatDate(new Date(killLog.startTime)) : <em>In Progress</em>}</dd>
<dt className="col-span-2">Week of Tier:</dt>
<dd className="col-span-2">{killWeek ? <>Week {killWeek}</> : <em>In Progress</em>}</dd>
<dt className="col-span-2">Pull Count:</dt>
{
date: '2021-02-28T00:42-05:00',
messages: ['Added distribution information for pull count and progression time to each encounter overview, along with (inverted) percentile information. See the <a class="underline" href="/faq#What-percentile-is-best-for-pull-counts-and-progression-times">FAQ</a> for details.']
},
{
date: '2021-02-27T20:47:00-05:00',
messages: ['Fixed an issue loading guilds on realms with dashes in the name. Wowprogress uses a different encoding than every other WoW site, and I accidentally broke everything else trying to work around that. Oops.']
},
</QA>
<QA question={"What is the little boxy thing next to my pull count / progression time?"}>
<p>This <EncounterDistChart id={"qa-tooltip"} dist={{ count: 200, min: 0, max: 100, quantiles: [25, 50, 75], actualPct: 0.3 }} actual={30} /> is a <em>boxplot</em>, a succinct way of representing the distribution of a set of values. You might recognize it as being the same thing as is on the <a className="underline" href="https://www.warcraftlogs.com/zone/statistics/26#boss=2399">WarcraftLogs DPS Statistics</a> page.</p>
<p>A traditional boxplot has three key parts: <ol className="list-decimal ml-8">
<li>The <strong>whiskers</strong>—the gray lines—which show the rough upper and lower bounds of the data after excluding outliers.</li>
<li>The <strong>box</strong>, which contains all data from the 25th to 75th percentile. The median (or 50th percentile) is shown as the center vertical line.</li>
<li>The <strong>outliers</strong>, which are omitted from these charts.</li>
</ol>
Your guild's performance relative to the rest of the community is shown as the blue dot.
</p>
</QA>
<QA question={"What percentile is best for pull counts and progression times?"}>
<p>
<strong>Short Version:</strong> To keep the values consistent with how the community already uses percentiles, 0th percentile is worst (nobody took longer than you) and 100th percentile is best (everybody took longer than you). This matches how percentiles are presented in WarcraftLogs.</p>
<p className="mt-4">
<em>Slightly Longer Version:</em> The scale of a percentile is
based on the thing being measured. When you measure DPS, bigger is
(usually) better, so 100th percentile is best and 0th is worst. When you
measure pull count, this is flipped: fewer pulls is better, so 0th
percentile would be best and 100th percentile would be worst. That is not
how <em>anyone</em> in the WoW community uses those numbers, however. Even
WarcraftLogs flips the metric for kill speed (lower kill time is better,
but they show 100th percentile as best), simply because it makes it
easier for people to use. We do the same here, for largely the same reason.
</p>
</QA>
<QA question={"Why don't all bosses show boxplots and percentiles?"}>
<p>Boxplots and percentiles are not shown until sufficient data is collected. WarcraftLogs applies an asterisk to such data like so: <Mono>87*</Mono> .</p>
<p>We omit statistics with insufficient data instead.</p>
<QA question={"Aren't these percentile values unfair to guilds that killed bosses before nerfs?"}>
<p>
Yes, they are. Hotfix information is shown in addition to other metrics to help contextualize things, but this is obviously an incomplete solution.
</p>
<p className="mt-2">
In the future, we hope to introduce adjustments to pull count and
progression time to account for the impact of hotfixes on the
difficulty of bosses. However, in the meantime it should simply be
understood that guilds that killed the boss pre-nerf are likely
performing better than their percentile ranking would indicate.
</p>
</QA>