// ==UserScript==
// @name mTurk Slow or Just Me? ("SOJM")
// @namespace salembeats
// @version 1.67
// @description UPDATE: Quick HUD added to compare yourself to the "normal" range (the middle 50% of responses, represented by the box in the box/whisker plot). NOTE: Requires Kadauchi's "MTurk Dashboard Enhancer" script to function properly.
// @author Cuyler Stuwe (salembeats)
// @include https://worker.mturk.com/dashboard*
// ==/UserScript==
const globals = {
CLIENT_VERSION: "synchronicity",
API_BASE: "https://ne26dv9hq6.execute-api.us-west-2.amazonaws.com/dev"
};
// 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 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;
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"
}
});
const response = await responseStream.json();
return response;
}
function injectAveragePERow() {
const el = document.getElementById("TodaysActivityAdditionalInfo") || document.getElementById('dashboard-available-earnings').querySelector('.border-gray-lightest').children[0];
el.insertAdjacentHTML("beforebegin", `
<hr/>
<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);">Retrieve Community Average</a>
</div>
</div>
<div class="row m-b-sm">
<div class="col-xs-7 col-sm-6 col-lg-7">
<strong>TC Low, Med, High</strong>
</div>
<div class="col-xs-5 col-sm-6 col-lg-5 text-xs-right">
<div id="tcLowMedHigh"> </div>
</div>
</div>
<div class="row m-b-sm">
<div class="col-xs-7 col-sm-6 col-lg-7">
<strong><span style="color: white; background: rgb(127,127,127);">Normal Range</span> vs. <span style="background: black; color: white;">You</span></strong>
</div>
<div class="col-xs-5 col-sm-6 col-lg-5 text-xs-right">
<div id="normalRange"> </div>
</div>
</div>
<div class="row m-b-sm">
<div class="col-xs-7 col-sm-6 col-lg-7">
<strong>Box+Whisker Plot</strong>
</div>
<div style="text-align: center;">
<img id="boxplotImage" style="max-width: 270px;" src="">
</div>
</div>
<div class="row m-b-sm">
<div class="col-xs-7 col-sm-6 col-lg-7">
<strong>TC 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>Nearest Competitor</strong>
</div>
<div class="col-xs-5 col-sm-6 col-lg-5 text-xs-right">
<div id="nearestCompetitor"></div>
</div>
</div>
<div class="row m-b-sm">
<div class="col-xs-7 col-sm-6 col-lg-7">
<strong>TC 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>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>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
}) {
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=5&axistitle=&imgwidth=300&imgheight=120`);
}
function embedBoxAndWhiskerData(params = {
low: undefined,
high: undefined,
median: undefined,
firstQuartile: undefined,
thirdQuartile: undefined}) {
document.getElementById("tcLowMedHigh").dataset.boxAndWhiskerPoints = JSON.stringify(params);
document.getElementById("boxplotImage").setAttribute("src", boxplotRenderUrl(params));
}
function rank(userEarnings, allUsersAscendingSortedEarnings) {
return ( [...allUsersAscendingSortedEarnings].reverse().indexOf(userEarnings) + 1 );
}
function nearestCompetitorTotalAndGapTuple(userEarnings, allUsersAscendingSortedEarnings) {
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 nearestCompetitorEarnings;
if(isLeader) {
nearestCompetitorEarnings = [...allUsersAscendingSortedEarnings].reverse().find(earnings => {
return earnings < ( userEarnings - tiedEarningsErrorFactor );
});
}
else {
nearestCompetitorEarnings = allUsersAscendingSortedEarnings.find(earnings => {
return earnings > ( userEarnings + tiedEarningsErrorFactor );
});
}
const nearestCompetitorGap = nearestCompetitorEarnings - userEarnings;
return [nearestCompetitorEarnings, nearestCompetitorGap];
}
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] = 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 = `${nearestCompetitorTotalFormatted} ( <span style="color: ${color};">${sign} ${absoluteGapFormatted}</span> )`;
}
function leaderboardOptionElementsFrom(allUsersAscendingSortedEarnings) {
const descendingEarnings = [...allUsersAscendingSortedEarnings].reverse();
return descendingEarnings.map((earnings, index) => {
return (`
<option>[#${index+1}]: ${earnings.toLocaleString("en-US", {style: "currency", currency: "USD"})}</option>
`);
}).join("");
}
function displayLeaderboard(allUsersAscendingSortedEarnings) {
document.getElementById("leaderboard").innerHTML = leaderboardOptionElementsFrom(allUsersAscendingSortedEarnings);
}
function displayLeagueLeaderboard(allUsersAscendingSortedEarnings) {
document.getElementById("leagueLeaderboard").innerHTML = leaderboardOptionElementsFrom(allUsersAscendingSortedEarnings);
}
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(ascendingSortedValues, maximumLeagueCount) {
if(!hasInflectionPoints(ascendingSortedValues) || maximumLeagueCount === 1) {
return [ascendingSortedValues];
}
else {
const [lowerLeague, upperLeague] = splitAtFirstInflectionPoint(ascendingSortedValues);
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];
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 displayLeagueRank(leagueTitle, leaguePosition, leagueSize) {
document.getElementById("leagueRank").innerHTML = `<span style="font-weight: 700; color: ${leagueTitleToCSSColor(leagueTitle)};">${leagueTitle}</span>: ( # <strong>${leaguePosition}</strong> / ${leagueSize} )`;
}
function displayNormalRangeVsYou(firstQuartile, thirdQuartile, total) {
const generateUserTotalHTML = (lowMedHigh) => `<span style="background: black; color: ${lowMedHigh === "low" ? ("rgb(255, 150, 150)") : ( lowMedHigh === "med" ? "rgb(150, 255, 150)" : "rgb(150, 150, 255)" )}">${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 = ``;
if(isBelowNormal) {
normalRangeHTML += `${generateUserTotalHTML("low")} - `;
}
normalRangeHTML += `${generateNormalBoundHTML(firstQuartile)} -`;
if(isWithinNormal) {
normalRangeHTML += ` ${generateUserTotalHTML("med")} -`;
}
normalRangeHTML += ` ${generateNormalBoundHTML(thirdQuartile)}`;
if(isAboveNormal) {
normalRangeHTML += ` - ${generateUserTotalHTML("high")}`;
}
document.getElementById("normalRange").innerHTML = normalRangeHTML;
}
async function main() {
injectAveragePERow();
document.getElementById("retrieveCommunityAverage").addEventListener("click", async e => {
const earningsApiResponse = await submitUserEarnings();
if(exists(earningsApiResponse.averageEarningsForToday)) {
const sortedIndividualEarnings = [...earningsApiResponse.individualEarningsForToday].sort((a,b) => a-b);
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: red;'>${lowestIndividualEarnings}</span>,
<span style='color: orange;'>${medianIndividualEarnings}</span>,
<span style='color: green;'>${highestIndividualEarnings}</span>`;
e.target.innerText = earningsApiResponse.averageEarningsForToday.toLocaleString("en-US", {style: "currency", currency: "USD"});
const total = ( todaysTotal() || todaysTotalMTS() );
displayNormalRangeVsYou( firstQuartile, thirdQuartile, total );
displayRank( rank(total, sortedIndividualEarnings), sortedIndividualEarnings.length );
displayNearestCompetitorInfo( nearestCompetitorTotalAndGapTuple(total, sortedIndividualEarnings) );
displayLeaderboard( sortedIndividualEarnings );
includeInLocalDailyHigh( total, amazonDateYYYYMMDD() );
updateLocalAllTimeBestIfAppropriateTo( total );
displayLocalBest();
displayLocalAverage();
const maxNumberOfLeagues = 5;
const leagues = splitIntoLeagues( sortedIndividualEarnings, 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 );
displayLeagueRank(leagueTitle, leaguePosition, leagueSize);
displayLeagueLeaderboard( league );
}
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();