Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save ryanjohnston/a098165816ae408cea7ec5e31d2a0c7b to your computer and use it in GitHub Desktop.
Save ryanjohnston/a098165816ae408cea7ec5e31d2a0c7b to your computer and use it in GitHub Desktop.
// ==UserScript==
// @name SOJM
// @namespace salembeats
// @version 2.11
// @description Compare your earnings to the crowd, to tell whether it's "Slow, Or Just Me?"
// @author Cuyler Stuwe (salembeats)
// @include https://worker.mturk.com/dashboard*
// @grant GM_info
// ==/UserScript==
const globals = {
CLIENT_VERSION: "new-hosting-trial",
API_BASE: "https://www.cuylerstuwe.com:28443",
SCRIPT_VERSION_API_BASE: "https://hujttp86jl.execute-api.us-west-2.amazonaws.com/dev",
SCRIPT_VERSION: GM_info.script.version
};
// Minified, synchronous SHA-256 transform function copied from: http://geraintluff.github.io/sha256/
var sha256=function a(b){function c(a,b){return a>>>b|a<<32-b}for(var d,e,f=Math.pow,g=f(2,32),h="length",i="",j=[],k=8*b[h],l=a.h=a.h||[],m=a.k=a.k||[],n=m[h],o={},p=2;64>n;p++)if(!o[p]){for(d=0;313>d;d+=p)o[d]=p;l[n]=f(p,.5)*g|0,m[n++]=f(p,1/3)*g|0}for(b+="\x80";b[h]%64-56;)b+="\x00";for(d=0;d<b[h];d++){if(e=b.charCodeAt(d),e>>8)return;j[d>>2]|=e<<(3-d)%4*8}for(j[j[h]]=k/g|0,j[j[h]]=k,e=0;e<j[h];){var q=j.slice(e,e+=16),r=l;for(l=l.slice(0,8),d=0;64>d;d++){var s=q[d-15],t=q[d-2],u=l[0],v=l[4],w=l[7]+(c(v,6)^c(v,11)^c(v,25))+(v&l[5]^~v&l[6])+m[d]+(q[d]=16>d?q[d]:q[d-16]+(c(s,7)^c(s,18)^s>>>3)+q[d-7]+(c(t,17)^c(t,19)^t>>>10)|0),x=(c(u,2)^c(u,13)^c(u,22))+(u&l[1]^u&l[2]^l[1]&l[2]);l=[w+x|0].concat(l),l[4]=l[4]+w|0}for(d=0;8>d;d++)l[d]=l[d]+r[d]|0}for(d=0;8>d;d++)for(e=3;e+1;e--){var y=l[d]>>8*e&255;i+=(16>y?0:"")+y.toString(16)}return i};
function isDEDetected() {
return !!document.querySelector("#TodaysActivityAdditionalInfo");
}
async function findEarningsWithoutDE() {
let resolver;
let promiseReturned = new Promise((resolve, reject) => {
resolver = resolve;
});
Object.assign(String.prototype, {
toNumberDE () {
return Number(this.replace(/[^0-9.]/g, ``));
}
});
Object.assign(Number.prototype, {
toNumberDE () {
return this;
}
});
const statusDetailsTable = document.querySelector(
`div[data-react-class="require('reactComponents/dailyWorkerStatisticsTable/DailyWorkerStatisticsTable')['default']"]`
);
document.body.insertAdjacentHTML("afterbegin", `<div id="sojmStatus" style="width: 100vw; border: 1px solid black; text-align: center;">Calculating work stats for SOJM...</div>`);
const today = await new Promise((resolve, reject) => {
const observer = new MutationObserver((mutationsList, observer) => {
mutationsList.forEach((mutation) => {
const addedNode = mutation.addedNodes[0];
if (addedNode && addedNode.classList.contains(`expanded-row`)) {
resolve(addedNode.querySelector("a"));
observer.disconnect();
}
});
});
observer.observe(statusDetailsTable, { childList: true, subtree: true });
document.querySelector(".p-x-sm.column.date-column").click();
});
const date = today.href.match(/[0-9]{4}-[0-9]{2}-[0-9]{2}/)[0];
let hitLog = date === localStorage.WMTD_date ? localStorage.WMTD_hitLog ? JSON.parse(localStorage.WMTD_hitLog) : {} : {};
async function get(page, rescan) {
try {
page = Number.isInteger(page) ? page : 1;
console.log(`Calculating Page ${page}`);
const fetchURL = new URL(`https://worker.mturk.com/status_details/${date}`);
fetchURL.searchParams.append(`page_number`, page);
fetchURL.searchParams.append(`format`, `json`);
const response = await fetch(fetchURL, {
credentials: `include`
});
if (response.status === 429) {
return setTimeout(get, 2000, page, rescan);
}
const json = await response.json();
for (const hit of json.results) {
hitLog[hit.hit_id] = hit;
}
const logLength = Object.keys(hitLog).length;
const expectedLength = page.toNumberDE() * 20 - 20 + json.num_results;
if (!rescan && logLength !== expectedLength) {
return get(1, true);
}
else {
localStorage.WMTD_hitLog = JSON.stringify(hitLog);
}
localStorage.WMTD_lastPage = page;
if (json.results.length === 20) {
return get(++ page, rescan);
}
else if (logLength !== json.total_num_results) {
hitLog = new Object();
return get(1, true);
}
else {
let projectedEarnings = 0;
const reqLog = {};
for (const key in hitLog) {
const hit = hitLog[key];
if (hit.status !== `Rejected`) {
projectedEarnings += hit.reward.amount_in_dollars;
}
if (!reqLog[hit.requester_id]){
reqLog[hit.requester_id] = {
requester_id: hit.requester_id,
requester_name: hit.requester_name,
reward: hit.reward.amount_in_dollars,
submitted: 1,
};
}
else {
reqLog[hit.requester_id].submitted += 1;
reqLog[hit.requester_id].reward += hit.reward.amount_in_dollars;
}
}
const pe = (`$${projectedEarnings.toLocaleString(`en-US`, { minimumFractionDigits: 2 })}`);
document.getElementById("sojmStatus").outerHTML = (`<div id="sojmStatus" style="width: 100vw; border: 1px solid black; text-align: center;">Last PE found for SOJM: ${pe} on ${date}.</div>`);
resolver();
}
}
catch (error) {
console.log(error);
}
}
get(date === localStorage.WMTD_date ? localStorage.WMTD_lastPage ? localStorage.WMTD_lastPage.toNumberDE() : 1 : 1, false);
localStorage.WMTD_date = date;
return promiseReturned;
}
async function fetchLatestVersionPayload() {
const responseStream = await fetch(globals.SCRIPT_VERSION_API_BASE, {
method: "GET",
});
const response = responseStream.json() || JSON.parse(`{ "latestVersion": "0.0", "downloadLink": "http://example.com/file.user.js", "changelog": "you should never receive this demo response" }`);
return response;
}
function doesNewVersionExist(versionPayload) {
return ( (!globals.SCRIPT_VERSION.includes("dev")) &&(globals.SCRIPT_VERSION !== versionPayload.latestVersion) );
}
function exists(thing) {
return thing !== undefined && thing !== null;
}
function amazonDate() {
return new Date(Date.now() + (new Date().getTimezoneOffset()*60*1000) - (420*60*1000));
}
function medianOfSortedValues(sortedValuesArray) {
const middlePosition = Math.floor(sortedValuesArray.length/2);
if(sortedValuesArray.length % 2) {
return sortedValuesArray[middlePosition];
}
else {
return (sortedValuesArray[middlePosition-1] + sortedValuesArray[middlePosition]) / 2.0;
}
}
function sidesOfMedian(sortedValuesArray) {
const hasEvenNumberOfItems = ( sortedValuesArray.length % 2 === 0 );
if(hasEvenNumberOfItems) {
return [
sortedValuesArray.slice(0, (sortedValuesArray.length / 2)),
sortedValuesArray.slice((sortedValuesArray.length / 2), sortedValuesArray.length)
];
}
else {
const indexToSplitOn = ((sortedValuesArray.length + 1) / 2) - 1;
return [
sortedValuesArray.slice(0, indexToSplitOn),
sortedValuesArray.slice(indexToSplitOn + 1, sortedValuesArray.length)
];
}
}
function hitLogTotalUsd() {
const hitLog = JSON.parse(localStorage.getItem("WMTD_hitLog"));
return Object.keys(hitLog).reduce((acc, currentHitLogKey) => {
return (acc + hitLog[currentHitLogKey].reward.amount_in_dollars);
}, 0);
}
function dateYYYYMMDD(date) {
const now = date;
const [year, month, day] = [now.getFullYear(), now.getMonth() + 1, now.getDate()].map(num => num.toLocaleString("en-US", {minimumIntegerDigits: 2, useGrouping: false}));
const todaysDateStr = `${year}-${month}-${day}`;
return todaysDateStr;
}
function todaysDateYYYYMMDD() {
return dateYYYYMMDD(new Date());
}
function amazonDateYYYYMMDD() {
return dateYYYYMMDD(amazonDate());
}
function lastWorkLoggedDateYYYYMMDD() {
return localStorage.getItem("WMTD_date");
}
function todaysTotal() {
const lastRecordedDate = lastWorkLoggedDateYYYYMMDD();
if(lastRecordedDate === amazonDateYYYYMMDD()) {
return hitLogTotalUsd();
}
else {
return null;
}
}
function todaysTotalMTS() {
const mtsProjectedEarnings = (document.getElementById('mts-ht-earnings') || {textContent: ""}).textContent;
return Number(mtsProjectedEarnings.replace(/[^0-9.]/g, ''));
}
function workerId() {
const workerIdCopyElement = document.querySelector(`[data-react-class="require('reactComponents/common/CopyText')['default']"]`);
return JSON.parse(workerIdCopyElement.dataset.reactProps).textToCopy;
}
function workerIdHash() {
return sha256(workerId());
}
function userEarningsPayload() {
const payload = JSON.stringify({
idHash: workerIdHash(),
date: lastWorkLoggedDateYYYYMMDD(),
total: todaysTotal() || todaysTotalMTS(),
clientVersion: globals.CLIENT_VERSION
});
return payload;
}
async function submitUserEarnings() {
const apiEndpoint = `${globals.API_BASE}/store`;
const responseStream = await fetch(apiEndpoint, {
method: "POST",
body: userEarningsPayload(),
headers: {
"Content-Type": "application/json"
}
});
let responseText;
let response;
try {
responseText = await responseStream.text();
response = await JSON.parse(responseText);
} catch (err) {
response = {errorMessage: responseText};
}
return response;
}
function injectAveragePERow() {
const el = document.getElementById("TodaysActivityAdditionalInfo") || document.getElementById('dashboard-available-earnings').querySelector('.border-gray-lightest').children[0];
const motds = [
["The finest diamonds are formed under the most intense pressure."],
["Champions do not become champions when they win the event, but in the hours, weeks, months and years they spend preparing for it. The victorious performance itself is merely the demonstration of their championship character."],
["A horse never runs so fast as when he has other horses to catch up to and outpace."],
["Winning is fun…. Sure. But winning is not the point. Wanting to win is the point. Not giving up is the point. Never letting up is the point. Never being satisfied with what you’ve done is the point.", "Pat Summitt"],
["The time your game is most vulnerable is when you’re ahead. Never let up.", "Rod Laver"],
["The real competition starts when you want to stop."],
["<a href='https://youtu.be/Zh6lZ5mTSAI?t=331' target='_blank'>I always smiled when I worked. No matter how hard I worked, I always had a great time.</a>", "Arnold Schwarzenegger"]
];
const selectedMotdIndex = Math.floor(Math.random()*motds.length);
const selectedMotd = motds[selectedMotdIndex];
el.insertAdjacentHTML("beforebegin", `
<hr/>
<div id="sojmTitle" style="text-align: center;">
<span style="display: block;"><strong>SOJM Competitive</strong> </span>
<span style="text-align: center; margin-left: auto; margin-right: auto; display: block; max-width: 450px;"> <i>“${selectedMotd[0]}”${selectedMotd[1] ? `- ${selectedMotd[1]}` : ""}</i> </span>
</div>
<hr/>
<div class="row m-b-sm">
<div class="col-xs-7 col-sm-6 col-lg-7">
<strong>Yesterday's Podium</strong>
</div>
<div class="col-xs-5 col-sm-6 col-lg-5 text-xs-right">
<span id="yesterdaysPodium" style="display: inline-block; margin: 0; padding: 0; font-size: 0.8rem;">&nbsp;</span>
</div>
</div>
<div class="row m-b-sm">
<div class="col-xs-7 col-sm-6 col-lg-7">
<strong>Today's Community Average</strong>
</div>
<div class="col-xs-5 col-sm-6 col-lg-5 text-xs-right">
<a id="retrieveCommunityAverage" href="javascript:void(0);">Sync Community Data</a>
</div>
</div>
<div class="row m-b-sm">
<div class="col-xs-7 col-sm-6 col-lg-7">
<strong>Today's Community Median</strong>
</div>
<div class="col-xs-5 col-sm-6 col-lg-5 text-xs-right">
<div id="tcLowMedHigh">&nbsp;</div>
</div>
</div>
<div class="row m-b-sm">
<div class="col-xs-7 col-sm-6 col-lg-7">
<strong>I'm In <span style="color: white; background: rgb(127,127,127);">Normal Range</span> [ <a target="_blank" href="#" id="boxplotImage">&nbsp;</a> ] ?</strong>
</div>
<div class="col-xs-5 col-sm-6 col-lg-5 text-xs-right">
<div id="normalRange">&nbsp;</div>
</div>
</div>
<hr/>
<div class="row m-b-sm">
<div class="col-xs-7 col-sm-6 col-lg-7">
<strong>League Rank</strong>
</div>
<div class="col-xs-5 col-sm-6 col-lg-5 text-xs-right">
<div id="leagueRank"></div>
</div>
</div>
<div class="row m-b-sm">
<div class="col-xs-7 col-sm-6 col-lg-7">
<strong>League Leaderboard</strong>
</div>
<div class="col-xs-5 col-sm-6 col-lg-5 text-xs-right">
<select id="leagueLeaderboard"></select>
</div>
</div>
<hr/>
<div class="row m-b-sm">
<div class="col-xs-7 col-sm-6 col-lg-7">
<strong>Overall Rank</strong>
</div>
<div class="col-xs-5 col-sm-6 col-lg-5 text-xs-right">
<div id="rank"></div>
</div>
</div>
<div class="row m-b-sm">
<div class="col-xs-7 col-sm-6 col-lg-7">
<strong>Overall Leaderboard</strong>
</div>
<div class="col-xs-5 col-sm-6 col-lg-5 text-xs-right">
<select id="leaderboard"></select>
</div>
</div>
<hr/>
<div class="row m-b-sm">
<div class="col-xs-7 col-sm-6 col-lg-7">
<strong>Nearest Competitor</strong>
</div>
<div class="col-xs-5 col-sm-6 col-lg-5 text-xs-right">
<div id="nearestCompetitor"></div>
</div>
</div>
<hr/>
<div class="row m-b-sm">
<div class="col-xs-7 col-sm-6 col-lg-7">
<strong>This Computer's Best</strong>
</div>
<div class="col-xs-5 col-sm-6 col-lg-5 text-xs-right">
<div id="localBest"></div>
</div>
</div>
<div class="row m-b-sm">
<div class="col-xs-7 col-sm-6 col-lg-7">
<strong>This Computer's Past Avg</strong>
</div>
<div class="col-xs-5 col-sm-6 col-lg-5 text-xs-right">
<div id="localAverage"></div>
</div>
</div>
`);
}
function boxplotRenderUrl(params = {
low: undefined,
high: undefined,
median: undefined,
firstQuartile: undefined,
thirdQuartile: undefined
}) {
const [imageWidth, imageHeight] = [ 1920, 540 ];
const tickDistance = 10;
return (`http://www.imathas.com/stattools/boxplot.php?n=1&showlabels=0&title0=&ds0q0=${params.low.replace("$", "")}&ds0q1=${params.firstQuartile.replace("$", "")}&ds0q2=${params.median.replace("$", "")}&ds0q3=${params.thirdQuartile.replace("$", "")}&ds0q4=${params.high.replace("$", "")}&title1=&ds1q0=&ds1q1=&ds1q2=&ds1q3=&ds1q4=&title2=&ds2q0=&ds2q1=&ds2q2=&ds2q3=&ds2q4=&xmin=0&xmax=${Math.floor((+params.high.replace("$", "")) + 5)}&ticks=${tickDistance}&axistitle=&imgwidth=${imageWidth}&imgheight=${imageHeight}`);
}
function embedBoxAndWhiskerData(params = {
low: undefined,
high: undefined,
median: undefined,
firstQuartile: undefined,
thirdQuartile: undefined}) {
const boxplotImageElement = document.getElementById("boxplotImage");
boxplotImageElement.innerText = "Box";
boxplotImageElement.setAttribute("href", boxplotRenderUrl(params));
}
function rank(userEarnings, allUsersAscendingSortedEarnings) {
return ( [...allUsersAscendingSortedEarnings].reverse().indexOf(userEarnings) + 1 );
}
function nearestCompetitorTotalAndGapTuple(userEarnings, allUsersAscendingSortedTuples) {
const allUsersAscendingSortedEarnings = allUsersAscendingSortedTuples.map(tuple => tuple.earnings);
const hasCompetitors = ( allUsersAscendingSortedEarnings.length > 1 );
if(!hasCompetitors) { return [userEarnings, 0]; }
const userEarningsIndex = allUsersAscendingSortedEarnings.indexOf(userEarnings);
const isLeader = ( userEarningsIndex === allUsersAscendingSortedEarnings.length - 1 );
const tiedEarningsErrorFactor = 0.01;
let nearestCompetitorEarningsIndex;
let nearestCompetitorEarnings;
let nearestCompetitorName;
if(isLeader) {
const reversedEarnings = [...allUsersAscendingSortedEarnings].reverse();
nearestCompetitorEarningsIndex = reversedEarnings.length - 1 - reversedEarnings.findIndex(earnings => {
return earnings < ( userEarnings - tiedEarningsErrorFactor );
});
}
else {
nearestCompetitorEarningsIndex = allUsersAscendingSortedEarnings.findIndex(earnings => {
return earnings > ( userEarnings + tiedEarningsErrorFactor );
});
}
nearestCompetitorEarnings = allUsersAscendingSortedTuples[nearestCompetitorEarningsIndex].earnings;
nearestCompetitorName = allUsersAscendingSortedTuples[nearestCompetitorEarningsIndex].nickname;
const nearestCompetitorGap = nearestCompetitorEarnings - userEarnings;
return [nearestCompetitorEarnings, nearestCompetitorGap, nearestCompetitorName];
}
function displayRank(rank, numberOfPositions) {
const relativeThresholdAtWhichToDisplayTopPercentile = 0.5;
const topPercentileZeroToOne = ( 1 - ( (numberOfPositions - rank) / numberOfPositions ) );
const formattedTopPercentile = topPercentileZeroToOne.toLocaleString("en-US", {style: "percent"});
document.getElementById("rank").innerHTML = `# <strong>${rank}</strong> / ${numberOfPositions} ${topPercentileZeroToOne <= relativeThresholdAtWhichToDisplayTopPercentile ? `( Top <strong>${formattedTopPercentile}</strong> )` : ""}`;
}
function displayNearestCompetitorInfo(nearestCompetitorTotalAndGapTuple) {
const [nearestCompetitorTotal, nearestCompetitorGap, nearestCompetitorName] = nearestCompetitorTotalAndGapTuple;
const sign = ( nearestCompetitorGap >= 0 ? "+" : "-" );
const color = ( sign === "+" ? "green" : "red" );
const absoluteGap = Math.abs(nearestCompetitorGap);
const [nearestCompetitorTotalFormatted, absoluteGapFormatted] = [nearestCompetitorTotal, absoluteGap].map(val => val.toLocaleString("en-US", {style: "currency", currency: "USD"}));
document.getElementById("nearestCompetitor").innerHTML = `"<strong>${nearestCompetitorName}</strong>": ${nearestCompetitorTotalFormatted} ( <span style="color: ${color};">${sign} ${absoluteGapFormatted}</span> )`;
}
function leaderboardOptionElementsFrom(allUsersAscendingSortedEarnings, yourEarnings) {
const descendingEarnings = [...allUsersAscendingSortedEarnings].reverse();
return descendingEarnings.map((user, index) => {
return (`
<option value="${user.earnings}">[#${index+1}]: ${user.earnings.toLocaleString("en-US", {style: "currency", currency: "USD"})}${user.nickname ? " (" + user.nickname + ")" : (user.earnings === yourEarnings ? " (Me)" : "") }</option>
`);
}).join("");
}
function leaderboardOptionElementsWithLeagueSplitsFrom(leagues, yourEarnings) {
let optionsHTML = "";
let leagueLeaderOffset = 0;
for(let i = leagues.length - 1; i >= 0; i--) {
const league = leagues[i];
const leagueTitle = leagueIndexToTitle(i, leagues.length);
const leagueSymbol = leagueTitleToSymbol(leagueTitle);
const leagueDescendingEarnings = [...league].reverse();
optionsHTML += `<option value=${leagueTitle}>${leagueSymbol} ${leagueTitle} ${leagueSymbol}</option>`;
optionsHTML += leagueDescendingEarnings.map((user, index) => {
return (`
<option value="${user.earnings}">[#${index+1+leagueLeaderOffset}]: ${user.earnings.toLocaleString("en-US", {style: "currency", currency: "USD"})}${user.nickname ? " (" + user.nickname + ")" : (user.earnings === yourEarnings ? " (Me)" : "") }</option>
`);
}).join("");
leagueLeaderOffset += league.length;
}
return optionsHTML;
}
function displayLeaderboard(allUsersAscendingSortedEarnings, yourEarnings) {
document.getElementById("leaderboard").innerHTML = leaderboardOptionElementsFrom(allUsersAscendingSortedEarnings, yourEarnings);
}
function displayLeagueLeaderboard(allUsersAscendingSortedEarnings, yourEarnings) {
document.getElementById("leagueLeaderboard").innerHTML = leaderboardOptionElementsFrom(allUsersAscendingSortedEarnings, yourEarnings);
}
function displayOverallLeaderboardWithLeagueSplits(leagues, yourEarnings) {
document.getElementById("leaderboard").innerHTML = leaderboardOptionElementsWithLeagueSplitsFrom(leagues, yourEarnings);
}
function localAllTimeBest(usdAmountToSetTo) {
if(exists(usdAmountToSetTo)) {
localStorage.setItem("sojmLocalAllTimeBest", usdAmountToSetTo);
}
return +(localStorage.getItem("sojmLocalAllTimeBest") || "0");
}
function updateLocalAllTimeBestIfAppropriateTo(usdAmountToSetTo) {
if(usdAmountToSetTo > localAllTimeBest()) {
return localAllTimeBest(usdAmountToSetTo);
}
else {
return localAllTimeBest();
}
}
function getLocalDailyHighs() {
return JSON.parse(localStorage.getItem("sojmLocalDailyHighs") || "{}");
}
function includeInLocalDailyHigh(amount, dateYYYYMMDD) {
const highs = getLocalDailyHighs();
highs[dateYYYYMMDD] = amount;
localStorage.setItem("sojmLocalDailyHighs", JSON.stringify(highs));
}
function getHistoricalLocalDailyHighAverage() {
const highsExceptToday = getLocalDailyHighs();
delete highsExceptToday[amazonDateYYYYMMDD()];
const datesTracked = Object.keys(highsExceptToday);
const numberOfDatesTracked = datesTracked.length;
const totalAmountTracked = datesTracked.reduce((acc, dateTracked) => {
return (acc + highsExceptToday[dateTracked]);
}, 0);
return ( totalAmountTracked / numberOfDatesTracked );
}
function displayLocalBest() {
document.getElementById("localBest").innerHTML = `<span style="color: gray;">${(localAllTimeBest() || 0).toLocaleString("en-US", {style: "currency", currency: "USD"})}</span>`;
}
function displayLocalAverage() {
document.getElementById("localAverage").innerHTML = `<span style="color: gray;">${(getHistoricalLocalDailyHighAverage() || 0).toLocaleString("en-US", {style: "currency", currency: "USD"})}</span>`;
}
function gaps(sortedAscendingEarnings) {
const ascendingSortedGaps = sortedAscendingEarnings.reduce((acc, num) => {
return { prevNumber: num, gaps: [ ...acc.gaps, num - acc.prevNumber ] }
}, { prevNumber: 0, gaps: [] }).gaps.sort((a, b) => (a-b));
return ascendingSortedGaps;
}
function averageGap(ascendingSortedValues) {
const gapsFromSortedValues = gaps(ascendingSortedValues);
const numberOfGaps = gapsFromSortedValues.length;
const sumOfGaps = gapsFromSortedValues.reduce((acc, gap) => {
return ( acc + gap );
}, 0);
return ( sumOfGaps / numberOfGaps );
}
function medianGap(ascendingSortedValues) {
const gapsFromSortedValues = gaps(ascendingSortedValues);
return medianOfSortedValues(gapsFromSortedValues);
}
function indicesOfInflectionPoints(sortedAscendingEarnings) {
const avgGap = averageGap(sortedAscendingEarnings);
const inflectionPointIndices = [];
for(let i = 0; i < sortedAscendingEarnings.length - 1; i++) {
const earningsAtI = sortedAscendingEarnings[i];
const earningsAboveI = sortedAscendingEarnings[i + 1];
const gap = earningsAboveI - earningsAtI;
if(gap >= avgGap) {
inflectionPointIndices.push(i + 1);
}
}
return inflectionPointIndices;
}
function hasInflectionPoints(ascendingSortedValues) {
return ( indicesOfInflectionPoints(ascendingSortedValues).length > 0 );
}
function splitAtFirstInflectionPoint(ascendingSortedValues) {
const firstInflectionPointIndex = indicesOfInflectionPoints(ascendingSortedValues)[0];
return [
ascendingSortedValues.slice(0, firstInflectionPointIndex),
ascendingSortedValues.slice(firstInflectionPointIndex)
];
}
function splitIntoLeagues(ascendingSortedUsers, maximumLeagueCount) {
const ascendingSortedEarnings = ascendingSortedUsers.map(user => user.earnings);
if(!hasInflectionPoints(ascendingSortedEarnings) || maximumLeagueCount === 1) {
return [ascendingSortedUsers];
}
else {
const firstInflectionPointIndex = indicesOfInflectionPoints(ascendingSortedEarnings)[0];
const [lowerLeague, upperLeague] = [
ascendingSortedUsers.slice(0, firstInflectionPointIndex),
ascendingSortedUsers.slice(firstInflectionPointIndex)
];
return [lowerLeague, ...splitIntoLeagues(upperLeague, (exists(maximumLeagueCount) ? maximumLeagueCount - 1 : null))];
}
}
function findLeagueAndRankIndex(earnings, leagues) {
for(let leagueIndex = 0; leagueIndex < leagues.length; leagueIndex++) {
const league = leagues[leagueIndex];
for(let rankIndex = 0; rankIndex < league.length; rankIndex++) {
const earningsAtRank = league[rankIndex].earnings;
if(earnings === earningsAtRank) {
return {
leagueIndex,
rankIndex
};
}
}
}
return undefined;
}
function leagueIndexToTitle(leagueIndex, numberOfLeagues) {
const maximumNumberOfLeagues = 5;
const offset = maximumNumberOfLeagues - numberOfLeagues;
const adjustedLeagueIndex = leagueIndex + offset;
return ([
"Bronze",
"Silver",
"Gold",
"Platinum",
"Diamond"
])[adjustedLeagueIndex];
}
function leagueTitleToCSSColor(leagueTitle) {
return ({
"Bronze": "brown",
"Silver": "gray",
"Gold": "orange",
"Platinum": "silver",
"Diamond": "aqua"
})[leagueTitle];
}
function leagueTitleToSymbol(leagueTitle) {
return ({
"Bronze": "🥉",
"Silver": "🥈",
"Gold": "🥇",
"Platinum": "✨",
"Diamond": "💎"
})[leagueTitle];
}
function displayYesterdaysPodium(podiumData) {
const as$ = num => (num.toLocaleString("en-US", {style: "currency", currency: "USD"}));
const innerHTML = [["🥇", "yellow"], ["🥈", "silver"], ["🥉", "tan"]].map((medalAndColorTuple, idx) => {
const [medalEmoji, color] = medalAndColorTuple;
return `<span style="min-width: 140px; display: block; text-align: left; background: ${color}; ">${medalEmoji} #${idx+1}: <strong>${podiumData[idx].nickname}</strong><span style="float: right;">&nbsp;(${as$(podiumData[idx].earnings)})</span></span>`;
}).join("");
document.getElementById("yesterdaysPodium").innerHTML = innerHTML;
}
function displayLeagueRank(leagueTitle, leagueSymbol, leaguePosition, leagueSize) {
document.getElementById("leagueRank").innerHTML = `<span style="font-weight: 700; color: ${leagueTitleToCSSColor(leagueTitle)};">${leagueTitle}</span> ${leagueSymbol}: ( # <strong>${leaguePosition}</strong> / ${leagueSize} )`;
}
function goToEarningsOnLeaderboard(earnings, leaderboardElement) {
leaderboardElement.value = earnings;
}
function displayNormalRangeVsYou(firstQuartile, thirdQuartile, total) {
const generateUserTotalHTML = (lowMedHigh) => `<span style="background: black; color: white;">${total.toLocaleString("en-US", {style: "currency", currency: "USD"})}</span>`;
const generateNormalBoundHTML = (value) => `<span style="color: white; background: rgb(127,127,127);">${value}</span>`;
const isBelowNormal = ( total < +firstQuartile.replace("$", "") );
const isAboveNormal = ( total > +thirdQuartile.replace("$", "") );
const isWithinNormal = !isBelowNormal && !isAboveNormal;
let normalRangeHTML = `<div style="display: inline-block;">`;
if(isBelowNormal) {
normalRangeHTML += `😰 - `;
}
normalRangeHTML += `<strong>[</strong> ${generateNormalBoundHTML(firstQuartile)} -`;
if(isWithinNormal) {
normalRangeHTML += ` 🙂 -`;
}
normalRangeHTML += ` ${generateNormalBoundHTML(thirdQuartile)} <strong>]</strong>`;
if(isAboveNormal) {
normalRangeHTML += ` - 🤑`;
}
normalRangeHTML += "</div>";
document.getElementById("normalRange").innerHTML = normalRangeHTML;
}
async function main() {
// Give Dashboard Enhancer a chance to be detected regardless of where it exists in the execution order by deferring the execution of this script's main function.
await new Promise(resolve => setTimeout(() => resolve(), 1));
if(!isDEDetected()) {
await findEarningsWithoutDE();
}
injectAveragePERow();
const latestVersionPayload = fetchLatestVersionPayload();
latestVersionPayload.then(versionInfo => {
if(doesNewVersionExist(versionInfo)) {
document.getElementById("sojmTitle").insertAdjacentHTML("afterend", `
<div style="text-align: center;"><span style="color: white; background: green;">New version available!</span> ( <span style="font-weight: 700;"><a href="${versionInfo.downloadLink}">Install</a></span> )</div>
<div><strong>You are using version:</strong> <code>${globals.SCRIPT_VERSION}</code></div>
<div><strong>Latest version available:</strong> <code>${versionInfo.latestVersion}</code></div>
<div><strong>Patch Notes:</strong></div>
<div>${versionInfo.changelog}</div>
<div></div>
`);
}
});
document.getElementById("retrieveCommunityAverage").addEventListener("click", async e => {
const earningsApiResponse = await submitUserEarnings();
if(exists(earningsApiResponse.averageEarningsForToday)) {
let zippedEarningsAndNicknames = [];
for(let i = 0; i < earningsApiResponse.individualEarningsForToday.length; i++) {
zippedEarningsAndNicknames.push({
earnings: earningsApiResponse.individualEarningsForToday[i],
nickname: earningsApiResponse.nicknames[i]
});
}
const sortedZippedIndividualEarnings = [...zippedEarningsAndNicknames].sort((a, b) => (a.earnings - b.earnings));
const sortedIndividualEarnings = sortedZippedIndividualEarnings.map(zipped => zipped.earnings);
const sortedNicknames = sortedZippedIndividualEarnings.map(zipped => zipped.nickname);
const lowestIndividualEarnings = sortedIndividualEarnings[0].toLocaleString("en-US", {style: "currency", currency: "USD"});
const medianIndividualEarnings = medianOfSortedValues(sortedIndividualEarnings).toLocaleString("en-US", {style: "currency", currency: "USD"});
const highestIndividualEarnings = sortedIndividualEarnings[sortedIndividualEarnings.length-1].toLocaleString("en-US", {style: "currency", currency: "USD"});
const [firstQuartile, thirdQuartile] = sidesOfMedian(sortedIndividualEarnings).map(side => medianOfSortedValues(side).toLocaleString("en-US", {style: "currency", currency: "USD"}));
embedBoxAndWhiskerData({low: lowestIndividualEarnings,
high: highestIndividualEarnings,
median: medianIndividualEarnings,
firstQuartile,
thirdQuartile});
document.getElementById("tcLowMedHigh").innerHTML = `
<span style='color: gray;'>${medianIndividualEarnings}</span>`;
const communityAverageElement = e.target;
communityAverageElement.outerHTML = `<div style="color: gray;"><a href="${window.location.href}">🔄</a> ${earningsApiResponse.averageEarningsForToday.toLocaleString("en-US", {style: "currency", currency: "USD"})}</div>`;
const total = ( todaysTotal() || todaysTotalMTS() );
displayNormalRangeVsYou( firstQuartile, thirdQuartile, total );
displayRank( rank(total, sortedIndividualEarnings), sortedIndividualEarnings.length );
displayNearestCompetitorInfo( nearestCompetitorTotalAndGapTuple(total, sortedZippedIndividualEarnings) );
includeInLocalDailyHigh( total, amazonDateYYYYMMDD() );
updateLocalAllTimeBestIfAppropriateTo( total );
displayLocalBest();
displayLocalAverage();
displayYesterdaysPodium(earningsApiResponse.podium);
const maxNumberOfLeagues = 5;
const leagues = splitIntoLeagues( sortedZippedIndividualEarnings, maxNumberOfLeagues );
const numberOfLeagues = leagues.length;
const { leagueIndex, rankIndex } = findLeagueAndRankIndex( total, leagues );
const league = leagues[leagueIndex];
const leagueSize = league.length;
const leaguePosition = leagueSize - rankIndex;
const leagueTitle = leagueIndexToTitle( leagueIndex, numberOfLeagues );
const leagueSymbol = leagueTitleToSymbol( leagueTitle );
displayOverallLeaderboardWithLeagueSplits( leagues, total );
goToEarningsOnLeaderboard( total, document.getElementById("leaderboard") );
displayLeagueRank(leagueTitle, leagueSymbol, leaguePosition, leagueSize);
displayLeagueLeaderboard( league, total );
goToEarningsOnLeaderboard( total, document.getElementById("leagueLeaderboard") );
}
else if(["Your request is missing fields: total", "Your report is outdated"].includes(earningsApiResponse.errorMessage)) {
e.target.innerText = "Work before comparing!";
}
else {
e.target.innerText = earningsApiResponse.errorMessage;
}
});
}
main();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment