summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorschneefux <schneefux+commit@schneefux.xyz>2018-06-16 20:47:59 +0200
committerschneefux <schneefux+commit@schneefux.xyz>2018-12-13 17:09:20 +0100
commita8135ad40cbeb6c0c464d57010032dc033993439 (patch)
treee9bf2c11e277b7b60bb58ab37818c0eb1c998574 /src
parent37fda8b8f9d7a6cf812478cdcfb46a32b8e9d259 (diff)
downloadbrokentalents-a8135ad40cbeb6c0c464d57010032dc033993439.tar.gz
brokentalents-a8135ad40cbeb6c0c464d57010032dc033993439.zip
Calculate and show level scaling
Diffstat (limited to 'src')
-rw-r--r--src/HeroTalentTable.vue29
-rw-r--r--src/Highlights.vue44
-rw-r--r--src/ReportService.js111
-rw-r--r--src/ReportTable.vue27
-rw-r--r--src/WinPickScatter.vue4
-rw-r--r--src/maps/maps.js18
6 files changed, 158 insertions, 75 deletions
diff --git a/src/HeroTalentTable.vue b/src/HeroTalentTable.vue
index 0e4509b..5c3545b 100644
--- a/src/HeroTalentTable.vue
+++ b/src/HeroTalentTable.vue
@@ -8,18 +8,27 @@
:mobile-cards="false">
<template slot-scope="props">
<b-table-column field="Talent" label="Talent">
- <div style="display: flex; align-items: center; justify-content: space-between;">
- <span>{{ getTalentName(props.row.Talent) }}</span>
- <talent-image :entry="props.row" :size="48"></talent-image>
- </div>
+ <talent-image :entry="props.row" :size="48"></talent-image>
</b-table-column>
- <b-table-column field="Winner" label="Win Rate" sortable numeric>
- <span class="tag is-warning mdi mdi-gauge-empty mdi-18px"
- title="Very low pick rate"
- v-if="100 * playersPerMatch * props.row.Count / totalPicks < 0.5"></span>
- {{ Math.round(100 * props.row.Winner) }}%
+ <b-table-column field="Winner" label="Level 1 Win Rate" sortable numeric>
+ <template v-if="isNaN(props.row.TalentWinrateBase)">
+ {{ (100 * props.row.Winner).toFixed(2) }}%
+ </template>
+ <template v-else>
+ {{ (100 * props.row.TalentWinrateBase).toFixed(2) }}%
+ </template>
</b-table-column>
+
+ <b-table-column field="TalentWinrateScaling" label="Win Rate Advantage" sortable numeric>
+ <template v-if="isNaN(props.row.TalentWinrateScaling) || props.row.SampleTooSmall">
+ <span class="mdi mdi-gauge-empty mdi-18px" title="Not enough data"></span>
+ </template>
+ <template v-else>
+ {{ (props.row.TalentWinrateScaling > 0? '+' : '') + (100 * props.row.TalentWinrateScaling / getLevelBuckets()).toFixed(2) }}% <small>for {{ getLevelsPerBucket(props.row) + (getLevelsPerBucket(props.row) > 1? ' Levels' : ' Level') }}</small>
+ </template>
+ </b-table-column>
+
</template>
</b-table>
</div>
@@ -38,6 +47,8 @@ export default Vue.component('hero-talent-table', {
data: function() {
return {
getTalentName: maps.getTalentName,
+ getLevelBuckets: ReportService.getLevelBuckets,
+ getLevelsPerBucket: (entry) => maps.getMaxLevel(entry) / this.getLevelBuckets(),
};
},
computed: {
diff --git a/src/Highlights.vue b/src/Highlights.vue
index b074b70..0aa8a5f 100644
--- a/src/Highlights.vue
+++ b/src/Highlights.vue
@@ -5,7 +5,7 @@
title="Overpowered"
type="Win Rate"
label="%"
- :value="Math.round(100 * stats.topWin.Winner)"
+ :value="Math.round(100 * stats.topWin.TotalWinner)"
:entry="stats.topWin" />
</div>
@@ -18,29 +18,31 @@
:entry="stats.topPick" />
</div>
- <div class="column" v-if="!!stats.topUnpopularWin">
+ <div class="column" v-if="!!stats.topLowLevel">
<talent-box class="is-dark notification"
- title="Hidden Gem"
- type="Win Rate"
+ title="Beginner"
+ type="Level 1 Win Rate"
label="%"
- :value="Math.round(100 * stats.topUnpopularWin.Winner)"
- :entry="stats.topUnpopularWin" />
+ :value="Math.round(100 * stats.topLowLevel.Winner)"
+ :entry="stats.topLowLevel" />
</div>
- <div class="column" v-if="!!stats.highestLevelAvg">
+ <div class="column" v-if="!!stats.topScaling">
<talent-box class="is-dark notification"
- title="Highest Level"
- type="Average Level"
- :value="stats.highestLevelAvg.Level.toFixed(2)"
- :entry="stats.highestLevelAvg" />
+ title="Spender"
+ type="Win Rate per Level"
+ label="%"
+ :value="'+' + Math.round(100 * stats.topScaling.TalentWinrateLevelScaling)"
+ :entry="stats.topScaling" />
</div>
- <div class="column" v-if="!!stats.lowestLevelAvg">
+ <div class="column" v-if="!!stats.topUnpopularWin">
<talent-box class="is-dark notification"
- title="Lowest Level"
- type="Average Level"
- :value="stats.lowestLevelAvg.Level.toFixed(2)"
- :entry="stats.lowestLevelAvg" />
+ title="Hidden Gem"
+ type="Win Rate"
+ label="%"
+ :value="Math.round(100 * stats.topUnpopularWin.TotalWinner)"
+ :entry="stats.topUnpopularWin" />
</div>
<div class="column" v-if="!!stats.topRareWin">
@@ -48,7 +50,7 @@
title="Best Rare"
type="Win Rate"
label="%"
- :value="Math.round(100 * stats.topRareWin.Winner)"
+ :value="Math.round(100 * stats.topRareWin.TotalWinner)"
:entry="stats.topRareWin" />
</div>
@@ -57,7 +59,7 @@
title="Best Epic"
type="Win Rate"
label="%"
- :value="Math.round(100 * stats.topEpicWin.Winner)"
+ :value="Math.round(100 * stats.topEpicWin.TotalWinner)"
:entry="stats.topEpicWin" />
</div>
@@ -66,7 +68,7 @@
title="Best Legendary"
type="Win Rate"
label="%"
- :value="Math.round(100 * stats.topLegendaryWin.Winner)"
+ :value="Math.round(100 * stats.topLegendaryWin.TotalWinner)"
:entry="stats.topLegendaryWin" />
</div>
@@ -89,11 +91,11 @@ export default Vue.component('highlights', {
topUnpopularWin: ReportService.getTopUnpopularWin(this.selectedMode),
topPick: ReportService.getTopPick(this.selectedMode),
totalPicks: ReportService.getTotalPicks(this.selectedMode),
- highestLevelAvg: ReportService.getHighestLevelAvg(this.selectedMode),
topRareWin: ReportService.getTopRareWins(this.selectedMode),
topEpicWin: ReportService.getTopEpicWins(this.selectedMode),
topLegendaryWin: ReportService.getTopLegendaryWins(this.selectedMode),
- lowestLevelAvg: ReportService.getLowestLevelAvg(this.selectedMode),
+ topLowLevel: ReportService.getTopLowLevel(this.selectedMode),
+ topScaling: ReportService.getTopScaling(this.selectedMode),
};
},
playersPerMatch: function() {
diff --git a/src/ReportService.js b/src/ReportService.js
index e0bd299..8bd9514 100644
--- a/src/ReportService.js
+++ b/src/ReportService.js
@@ -1,9 +1,10 @@
import * as maps from './maps/maps';
-import * as metadata from '../data/78f0edf0/metadata.json';
+import * as metadata from '../data/2d73896c/metadata.json';
const POPULAR_THRESHOLD = 1.0; // percent
+const PICKS_THRESHOLD = 200; // picks
-const report = require('../data/78f0edf0/report.json')
+const report = require('../data/2d73896c/report.json')
.filter((entry) => entry.Actor != undefined); // bad data from API downtime
const reports = new Map();
@@ -14,45 +15,95 @@ const topWins = new Map();
const topRareWins = new Map();
const topEpicWins = new Map();
const topLegendaryWins = new Map();
-const topLeveledUp = new Map();
-const topLeveledDown = new Map();
+const topLowLevel = new Map();
+const topScaling = new Map();
const topUnpopularWins = new Map();
let totalMatches = 0;
let actors = [];
const modes = metadata.config.api.modes;
for(let mode of modes) {
- const relevancy = (entry) => (entry.Count / totalPicks.get(mode)) * entry.Winner;
+ const relevancy = (entry) => (entry.TotalPicks / totalPicks.get(mode)) * entry.TotalWinner;
const playersPerMatch = maps.playersPerMatch(mode);
- reports.set(mode, report.filter((entry) => entry.Mode == mode));
- totalPicks.set(mode, report.map((entry) => entry.Count).reduce((agg, cur) => agg + cur, 0));
+ const reportFilterMode = report.filter((entry) => entry.Mode == mode);
+
+ const baseLevelForEntry = (entry) => reportFilterMode
+ .filter((entry2) => entry2.Actor == entry.Actor &&
+ entry2.Talent == entry.Talent)
+ .map((entry2) => entry2.LevelBucket)
+ .reduce((agg, cur) => cur < agg? cur : agg);
+
+ const reportFilterModeBaseLevel = reportFilterMode.filter((entry) => entry.LevelBucket == baseLevelForEntry(entry));
+
+ // using the "base level" (bucket 0) report, add information about diff to 'NoTalent' and scaling
+ const reportFilterModeRegressed = reportFilterModeBaseLevel.map((entry) => {
+ const sum = (agg, cur) => agg + cur;
+ const range = (n) => [...Array(n).keys()];
+
+ // perform a weighted linear regression. x: level bucket (scaled 0 - 1) y: win rate weights: picks
+ const actorReport = reportFilterMode
+ .filter((entry2) =>
+ entry2.Actor == entry.Actor &&
+ entry2.Talent == entry.Talent &&
+ entry2.LevelBucket >= 0);
+ const buckets = actorReport.map((entry) => entry.LevelBucket).sort();
+ const findActorWithBucket = (bucket) => actorReport.find((entry) => entry.LevelBucket == bucket);
+ const weights = buckets.map((bucket) => findActorWithBucket(bucket).Count);
+ const sum_weights = weights.reduce(sum, 0);
+ const xs = [].concat(...buckets.map((bucket, index) => range(weights[index]).map((_) => bucket / metadata.config.self.levelBuckets)));
+ const ys = [].concat(...buckets.map((bucket, index) => range(weights[index]).map((_) => findActorWithBucket(bucket).Winner)));
+ const n = xs.length;
+ const sum_x = xs.reduce(sum, 0);
+ const sum_y = ys.reduce(sum, 0);
+ const sum_xy = xs.map((x, index) => x * ys[index]).reduce(sum, 0);
+ const sum_xx = xs.map((x) => x * x).reduce(sum, 0);
+ const sum_yy = ys.map((y) => y * y).reduce(sum, 0);
+
+ const slope = (n * sum_xy - sum_x * sum_y) / (n * sum_xx - sum_x * sum_x);
+ const intercept = (sum_y - slope * sum_x) / n;
+
+ return Object.assign({}, entry, {
+ TalentWinrateBase: intercept,
+ TalentWinrateScaling: slope, // to 1 = max level
+ TalentWinrateLevelScaling: slope / maps.getMaxLevel(entry),
+ TotalPicks: sum_weights || entry.Count, // NoTalent has no weights
+ TotalWinner: sum_y / sum_weights || entry.Winner,
+ SampleTooSmall: sum_weights < PICKS_THRESHOLD,
+ });
+ });
+ reports.set(mode, reportFilterModeRegressed);
+
+ totalPicks.set(mode, reports.get(mode)
+ .map((entry) => entry.TotalPicks)
+ .reduce((agg, cur) => agg + cur, 0));
totalMatches += totalPicks.get(mode) / maps.playersPerMatch(mode);
top10Relevancy.set(mode, reports.get(mode)
.sort((entry1, entry2) => relevancy(entry2) - relevancy(entry1))
.slice(0, 10));
- topPicks.set(mode, reports.get(mode).sort((entry1, entry2) => entry2.Count - entry1.Count)[0]);
+ topPicks.set(mode, reports.get(mode).sort((entry1, entry2) => entry2.TotalPicks - entry1.TotalPicks)[0]);
topWins.set(mode, reports.get(mode)
- .filter((entry) => 100 * playersPerMatch * entry.Count / totalPicks.get(mode) > POPULAR_THRESHOLD)
- .sort((entry1, entry2) => entry2.Winner - entry1.Winner)[0]);
+ .filter((entry) => 100 * playersPerMatch * entry.TotalPicks / totalPicks.get(mode) > POPULAR_THRESHOLD)
+ .sort((entry1, entry2) => entry2.TotalWinner - entry1.TotalWinner)[0]);
topUnpopularWins.set(mode, reports.get(mode)
- .filter((entry) => 100 * playersPerMatch * entry.Count / totalPicks.get(mode) <= POPULAR_THRESHOLD)
- .sort((entry1, entry2) => entry2.Winner - entry1.Winner)[0]);
+ .filter((entry) => 100 * playersPerMatch * entry.TotalPicks / totalPicks.get(mode) <= POPULAR_THRESHOLD &&
+ entry.TotalPicks > PICKS_THRESHOLD)
+ .sort((entry1, entry2) => entry2.TotalWinner - entry1.TotalWinner)[0]);
topRareWins.set(mode, reports.get(mode)
- .filter((entry) => maps.getTalentRarity(entry.Talent) == 'Rare' && 100 * playersPerMatch * entry.Count / totalPicks.get(mode))
- .sort((entry1, entry2) => entry2.Winner - entry1.Winner)[0]);
+ .filter((entry) => maps.getTalentRarity(entry.Talent) == 'Rare' && 100 * playersPerMatch * entry.TotalPicks / totalPicks.get(mode))
+ .sort((entry1, entry2) => entry2.TotalWinner - entry1.TotalWinner)[0]);
topEpicWins.set(mode, reports.get(mode)
- .filter((entry) => maps.getTalentRarity(entry.Talent) == 'Epic' && 100 * playersPerMatch * entry.Count / totalPicks.get(mode))
- .sort((entry1, entry2) => entry2.Winner - entry1.Winner)[0]);
+ .filter((entry) => maps.getTalentRarity(entry.Talent) == 'Epic' && 100 * playersPerMatch * entry.TotalPicks / totalPicks.get(mode))
+ .sort((entry1, entry2) => entry2.TotalWinner - entry1.TotalWinner)[0]);
topLegendaryWins.set(mode, reports.get(mode)
- .filter((entry) => maps.getTalentRarity(entry.Talent) == 'Legendary' && 100 * playersPerMatch * entry.Count / totalPicks.get(mode))
- .sort((entry1, entry2) => entry2.Winner - entry1.Winner)[0]);
- topLeveledUp.set(mode, reports.get(mode)
- .filter((entry) => maps.getTalentRarity(entry.Talent) != 'None')
- .sort((entry1, entry2) => maps.getScaledLevel(entry2) - maps.getScaledLevel(entry1))[0]);
- topLeveledDown.set(mode, reports.get(mode)
- .filter((entry) => maps.getTalentRarity(entry.Talent) != 'None')
- .sort((entry1, entry2) => maps.getScaledLevel(entry1) - maps.getScaledLevel(entry2))[0]);
+ .filter((entry) => maps.getTalentRarity(entry.Talent) == 'Legendary' && 100 * playersPerMatch * entry.TotalPicks / totalPicks.get(mode))
+ .sort((entry1, entry2) => entry2.TotalWinner - entry1.TotalWinner)[0]);
+ topLowLevel.set(mode, reports.get(mode)
+ .filter((entry) => !entry.SampleTooSmall)
+ .sort((entry1, entry2) => entry2.TalentWinrateBase - entry1.TalentWinrateBase)[0]);
+ topScaling.set(mode, reports.get(mode)
+ .filter((entry) => !entry.SampleTooSmall)
+ .sort((entry1, entry2) => entry2.TalentWinrateLevelScaling - entry1.TalentWinrateLevelScaling)[0]);
if (actors.length == 0) {
actors = [...new Set(reports.get(mode).map((entry) => entry.Actor))];
@@ -76,12 +127,12 @@ export default {
return top10Relevancy.get(mode);
},
- getHighestLevelAvg(mode) {
- return topLeveledUp.get(mode);
+ getTopLowLevel(mode) {
+ return topLowLevel.get(mode);
},
- getLowestLevelAvg(mode) {
- return topLeveledDown.get(mode);
+ getTopScaling(mode) {
+ return topScaling.get(mode);
},
getTopWin(mode) {
@@ -123,4 +174,8 @@ export default {
getLastUpdate() {
return metadata.lastUpdate;
},
+
+ getLevelBuckets() {
+ return metadata.config.self.levelBuckets;
+ },
};
diff --git a/src/ReportTable.vue b/src/ReportTable.vue
index 75a1369..4cbc280 100644
--- a/src/ReportTable.vue
+++ b/src/ReportTable.vue
@@ -1,7 +1,7 @@
<template>
<section>
<b-table :data="report"
- :default-sort="['Count', 'desc']"
+ :default-sort="['TotalPicks', 'desc']"
:default-sort-directon="'desc'"
:paginated="true">
<template slot-scope="props">
@@ -39,21 +39,26 @@
</div>
</b-table-column>
- <b-table-column field="Level" label="Average Level" :visible="hasTalents" sortable numeric>
- <template v-if="!!props.row.Level">
- {{ props.row.Level.toFixed(2) }}
+ <b-table-column field="Winner" label="Level 1 Win Rate" sortable numeric>
+ <template v-if="isNaN(props.row.TalentWinrateBase) || props.row.SampleTooSmall">
+ {{ (100 * props.row.Winner).toFixed(2) }}%
</template>
- <template v-if="!props.row.Level">
- 0
+ <template v-else>
+ {{ (100 * props.row.TalentWinrateBase).toFixed(2) }}%
</template>
</b-table-column>
- <b-table-column field="Winner" label="Win Rate" sortable numeric>
- {{ (100 * props.row.Winner).toFixed(2) }}%
+ <b-table-column field="TalentWinrateScaling" label="Win Rate Advantage" sortable numeric>
+ <template v-if="isNaN(props.row.TalentWinrateScaling) || props.row.SampleTooSmall">
+ <span class="mdi mdi-gauge-empty mdi-18px" title="Not enough data"></span>
+ </template>
+ <template v-else>
+ {{ (props.row.TalentWinrateScaling > 0? '+' : '') + (100 * props.row.TalentWinrateScaling / getLevelBuckets()).toFixed(2) }}% <small>for {{ getLevelsPerBucket(props.row) + (getLevelsPerBucket(props.row) > 1? ' Levels' : ' Level') }}</small>
+ </template>
</b-table-column>
- <b-table-column field="Count" label="Pick Rate" sortable numeric>
- {{ (100 * playersPerMatch * props.row.Count / totalPicks).toFixed(2) }}%
+ <b-table-column field="TotalPicks" label="Pick Rate" sortable numeric>
+ {{ (100 * playersPerMatch * props.row.TotalPicks / totalPicks).toFixed(2) }}%
</b-table-column>
</template>
</b-table>
@@ -74,6 +79,8 @@ export default Vue.component('report-table', {
getTalentName: maps.getTalentName,
getTalentRarityIndex: maps.getTalentRarityIndex,
getHero: maps.getHero,
+ getLevelBuckets: ReportService.getLevelBuckets,
+ getLevelsPerBucket: (entry) => maps.getMaxLevel(entry) / this.getLevelBuckets(),
};
},
computed: {
diff --git a/src/WinPickScatter.vue b/src/WinPickScatter.vue
index f2d495c..7c2f906 100644
--- a/src/WinPickScatter.vue
+++ b/src/WinPickScatter.vue
@@ -33,8 +33,8 @@ export default Vue.component('win-pick-scatter', {
const dataWithRarity = (rarity) => data.filter((entry) => maps.getTalentRarity(entry.Talent) == rarity);
const traces = maps.RARITIES.map((rarity) => ({
- x: dataWithRarity(rarity).map((entry) => entry.Winner),
- y: dataWithRarity(rarity).map((entry) => maps.playersPerMatch(this.selectedMode) * entry.Count / total),
+ x: dataWithRarity(rarity).map((entry) => entry.TotalWinner || entry.Winner),
+ y: dataWithRarity(rarity).map((entry) => maps.playersPerMatch(this.selectedMode) * (entry.TotalCount || entry.Count) / total),
text: dataWithRarity(rarity).map((entry) => maps.getHero(entry.Actor)),
name: rarity,
mode: 'markers',
diff --git a/src/maps/maps.js b/src/maps/maps.js
index 08ce779..ce91c24 100644
--- a/src/maps/maps.js
+++ b/src/maps/maps.js
@@ -28,7 +28,7 @@ function getTalentName(talent) {
}
function getTalentRarity(talent) {
- if (talent == 'NoTalent') {
+ if (!talent || talent == 'NoTalent') {
return 'None';
}
@@ -45,14 +45,21 @@ function getTalentRarity(talent) {
}
}
-function getScaledLevel(entry) {
+function getMaxLevel(entry) {
switch (getTalentRarity(entry.Talent)) {
case 'unknown':
case 'None': return -1;
- case 'Rare': return entry.Level / 20;
- case 'Epic': return entry.Level / 10;
- case 'Legendary': return entry.Level / 5;
+ case 'Rare': return 20;
+ case 'Epic': return 10;
+ case 'Legendary': return 5;
+ }
+};
+
+function getScaledLevel(entry) {
+ if (['unknown', 'None'].includes(getTalentRarity(entry.Talent))) {
+ return 0;
}
+ return entry.Level / getMaxLevel(entry);
};
function getHero(actor) {
@@ -75,6 +82,7 @@ module.exports = {
RARITIES,
getTalentName,
getTalentRarity,
+ getMaxLevel,
getScaledLevel,
getHero,
getMode,