HIT Database Analytics

Analytics for HIT Database--makes pretty graphs

目前為 2015-10-17 提交的版本,檢視 最新版本

  1. // ==UserScript==
  2. // @name HIT Database Analytics
  3. // @author feihtality
  4. // @namespace https://greasyfork.org/en/users/12709
  5. // @version 0.6.000
  6. // @description Analytics for HIT Database--makes pretty graphs
  7. // @match https://www.mturk.com/mturk/dashboard
  8. // @require https://cdn.jsdelivr.net/momentjs/2.10.6/moment.min.js
  9. // @grant none
  10. // ==/UserScript==
  11.  
  12.  
  13.  
  14. ((D) => {
  15. 'use strict';
  16.  
  17. if (!('decRound' in Math) || !('Status' in window) || !('Progress' in window)) {
  18. console.log('execution order is either too high or HITDB MKII is not detected');
  19. return;
  20. }
  21.  
  22. var pad = n => ("00"+n).substr(-2),
  23. digitGroup = function(n) {
  24. n = String(n).split('.');
  25. if (n[0].length < 4) return n.join('.');
  26. n[0] = n[0].replace(/(\d)(?=(\d{3})+$)/g, '$1,');
  27. return n.join('.');
  28. },
  29. dec = (n,l) => Number(Math.decRound(n,l)).toFixed(l),
  30. m = this.moment;
  31.  
  32. // inject interface
  33. var insertion = D.getElementById('hdbCSVInput'),
  34. searchbtn = D.getElementById('hdbSearch'),
  35. acheckbox = insertion.parentNode.insertBefore(D.createElement('INPUT'), insertion.nextSibling),
  36. alabel = insertion.parentNode.insertBefore(D.createElement('LABEL'), insertion.nextSibling),
  37. metrics = null;
  38.  
  39. acheckbox.type = 'checkbox';
  40. acheckbox.id = 'hdbAnalytics';
  41. acheckbox.style.verticalAlign = 'middle';
  42. alabel.textContent = 'Analyze';
  43. alabel.htmlFor = 'hdbAnalytics';
  44. alabel.style.verticalAlign = 'middle';
  45.  
  46. acheckbox.onclick = function() {
  47. if (searchbtn.textContent === "Export CSV") insertion.click();
  48. if (searchbtn.textContent === "Analyze") searchbtn.textContent = 'Search';
  49. else searchbtn.textContent = 'Analyze';
  50. };
  51. searchbtn.addEventListener('click', getData);
  52.  
  53. function getData(e) {
  54. if (e.target.textContent !== "Analyze") return;
  55. if (!window.HITStorage.db) { window.Status.push('AccessViolation: Database is not defined.', 'red'); return; }
  56. var range;
  57. dbrange = [D.getElementById('hdbMinDate').value || undefined, D.getElementById('hdbMaxDate').value || undefined];
  58. if (!dbrange[0] && !dbrange[1]) {
  59. dbrange[2] = 'ALL';
  60. dbrange[3] = ['9999-99-99', '0000-00-00'];
  61. range = null;
  62. } else if (dbrange[0] && !dbrange[1]) {
  63. dbrange[2] = dbrange[0]+'>>';
  64. dbrange[3] = [dbrange[0], '0000-00-00'];
  65. range = window.IDBKeyRange.lowerBound(dbrange[0]);
  66. } else if (!dbrange[0] && dbrange[1]) {
  67. dbrange[2] = '<<'+dbrange[1];
  68. dbrange[3] = ['9999-99-99', dbrange[1]];
  69. range = window.IDBKeyRange.upperBound(dbrange[1]);
  70. } else {
  71. dbrange[2] = dbrange[0]+':'+dbrange[1];
  72. dbrange[3] = [dbrange[0], dbrange[1]];
  73. range = window.IDBKeyRange.bound(dbrange[0],dbrange[1]);
  74. }
  75.  
  76. sets = new Sets();
  77. window.Progress.show();
  78. metrics = new window.Metrics('database_analytics');
  79. metrics.mark('dbrecall', 'start');
  80. dbrecall("HIT", {range: range}).then(analyzeData);
  81. }
  82.  
  83. /* . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . */
  84.  
  85. var
  86. Sets = function() {//{{{ main data obj
  87. this.year = {
  88. aggregate: { def: {}, req: {} },
  89. pay: { labels: [], data: [] },
  90. hits: { labels: [], data: [] },
  91. rpay: {},
  92. rhits: {}
  93. };
  94. this.month = {
  95. aggregate: { def: {}, req: {} },
  96. pay: { labels: [], data: [] },
  97. hits: { labels: [], data: [] },
  98. rpay: {},
  99. rhits: {}
  100. };
  101. this.week = {
  102. aggregate: { def: {}, req: {} },
  103. pay: { labels: [], data: [] },
  104. hits: { labels: [], data: [] },
  105. rpay: {},
  106. rhits: {}
  107. };
  108. this.day = {
  109. aggregate: { def: {}, req: {} },
  110. pay: { labels: [], data: [] },
  111. hits: { labels: [], data: [] },
  112. rpay: {},
  113. rhits: {}
  114. };
  115. this.all = {
  116. aggregate: [],
  117. distribution: {
  118. hits: {'1':0,'2-5':0,'6-10':0,'11-15':0,'16-20':0,'21-25':0,'26-30':0,'31-35':0,
  119. '36-40':0,'41-45':0,'46-50':0,'51-100':0,'101-150':0,'151-200':0,'201-250':0,'251-300':0,
  120. '301-350':0,'351-400':0,'401-450':0,'451-500':0,'501-550':0,'551-600':0,'601-650':0,'651-700':0,
  121. '701-750':0,'751-800':0,'801-850':0,'851-900':0,'901-950':0,'951-1000':0,'1001+':0},
  122. pay: {'0':0, '0.01-0.05':0,'0.06-0.10':0,'0.11-0.15':0,'0.16-0.20':0,'0.21-0.25':0,
  123. '0.26-0.30':0,'0.31-0.35':0,'0.36-0.40':0,'0.41-0.45':0,'0.46-0.50':0,'0.51-0.55':0,
  124. '0.56-0.60':0,'0.61-0.65':0,'0.66-0.70':0,'0.71-0.75':0,'0.76-0.80':0,'0.81-0.85':0,
  125. '0.86-0.90':0,'0.91-0.95':0,'0.96-1.00':0,'1.01-1.25':0,'1.26-1.50':0,'1.51-1.75':0,
  126. '1.76-2.00':0,'2.01-2.25':0,'2.26-2.50':0,'2.51-2.75':0,'2.76-3.00':0,'3.01-3.25':0,
  127. '3.26-3.50':0,'3.51-3.75':0,'3.76-4.00':0,'4.01-4.25':0,'4.26-4.50':0,'4.51-4.75':0,
  128. '4.76-5.00':0,'5.01+':0},
  129. data: { hits: [], labelsh: [], pay: [], labelsp: [] } },
  130. hits: { total: 0, rejected: 0, pending: 0, bonus: 0, titles: {}, batch: [] },
  131. pay : { total: 0, rejected: 0, pending: 0, bonus: 0 },
  132. hitsPerRequester: { avg: 0, data: [], sd: 0, se: 0 },
  133. payPerHit: { avg: 0, data: [], sd: 0, se: 0 }
  134. };
  135. },//}}}
  136. sets = new Sets(),
  137. disth = Object.keys(sets.all.distribution.hits),
  138. distp = Object.keys(sets.all.distribution.pay),
  139. ymwdLabels = Object.keys(sets), dbrange;
  140.  
  141. function dbrecall(os, options) {
  142. options = options || {};
  143. var
  144. index = options.index || "date",
  145. range = options.range || null;
  146. //wRange = [null, null];
  147.  
  148. var total = 0;
  149. return new Promise( y => {
  150. window.HITStorage.db.transaction(os, "readonly").objectStore(os).index(index).openCursor(range).onsuccess = function() {
  151. if (this.result) {
  152. window.Status.message = 'Aggregating data... [ '+(total++)+' ]';
  153. aggregateCursor(this.result.value);
  154. this.result.continue();
  155. }
  156. else y(1);
  157. };
  158. });
  159. }
  160.  
  161. function analyzeData(r) {
  162. void(r);
  163. metrics.mark('dbrecall','end');
  164. //var sets = data.getDatasets();
  165. metrics.mark('dsWorker', 'start');
  166. dsWorker.postMessage(sets);
  167. }
  168.  
  169. function drawCharts(sets) {//{{{
  170. var
  171. hitlineopt = {
  172. label: 'HITs Submitted',
  173. yAxixID: 'hits',
  174. backgroundColor: 'rgba(149,89,240,0.1)'/*'rgba(42,161,152,0.1)'*/,
  175. borderColor : 'rgba(149,89,240,0.5)'/*'rgba(42,161,152,0.5)'*/,
  176. pointBackgroundColor : 'rgba(149,89,240,1)'/*'rgba(42,161,152,1)'*/,
  177. pointHoverBackgroundColor: 'rgba(92,0,230,1)'/*'rgba(38,139,210,1)'*/
  178. },
  179. paylineopt = {
  180. label: 'Total Pay',
  181. yAxisID: 'pay',
  182. backgroundColor: 'rgba(200,242,48,0.1)'/*'rgba(181,137,0,0.1)'*/,
  183. borderColor : 'rgba(200,242,48,0.5)'/*'rgba(181,137,0,0.5)'*/,
  184. pointBackgroundColor : 'rgba(200,242,48,1)'/*'rgba(181,137,0,1)'*/,
  185. pointHoverBackgroundColor: 'rgba(129,163,5,1)'/*'rgba(133,153,0,1)'*/
  186. },
  187. chartopt = {};
  188. for (var k of Object.keys(sets)) {
  189. if (k === 'all') {
  190. var a = sets.all.hits;
  191. chartopt.bsratio = {
  192. type: 'pie', data: {
  193. labels: ['batch', 'survey'],
  194. datasets: [{ data: [Math.decRound(100*a.batch.total/a.total,2), Math.decRound(100*(a.total-a.batch.total)/a.total,2)],
  195. backgroundColor: ['#6A90F7', '#EDC147'] }]
  196. }, options: { tooltips: { template: '<%= label %>: <%= value %>%', fontSize: 12 } }
  197. };
  198. chartopt.pdist = {
  199. type: 'bar', data: {
  200. labels: sets.all.distribution.data.labelsp,
  201. datasets: [{ data: sets.all.distribution.data.pay,
  202. backgroundColor: '#47EDE1', hoverBackgroundColor: '#C7ED3E' }]
  203. }, options: { scales: { xAxes: [{ ticks: {show: false}, categorySpacing: 1 }] },
  204. tooltips: { multiTemplate: '<%= datasetLabel %><%= value %>%' }
  205. }
  206. };
  207. chartopt.hdist = {
  208. type: 'bar', data: {
  209. labels: sets.all.distribution.data.labelsh,
  210. datasets: [{ data: sets.all.distribution.data.hits,
  211. backgroundColor: '#47EDE1', hoverBackgroundColor: '#C7ED3E' }]
  212. }, options: { scales: { xAxes: [{ ticks: {show: false}, categorySpacing: 1}] },
  213. tooltips: { multiTemplate: '<%= datasetLabel %><%= value %>%' }
  214. }
  215. };
  216. continue;
  217. }
  218. var timeOpts;
  219. switch (k) {
  220. case 'day': timeOpts = { format: 'YYYY-MM-DD', displayFormat: 'MMMDD', unit: k }; break;
  221. case 'week': timeOpts = { format: 'YYYY[ week ]ww', displayFormat: 'YY[w]ww', unit: k }; break;
  222. case 'month': timeOpts = { format: 'YYYY-MM', displayFormat: 'MMM[\']YY', unit: k }; break;
  223. case 'year': timeOpts = { format: 'YYYY', displayFormat: 'YYYY', unit: k }; break;
  224. }
  225. Object.assign(sets[k].hits, hitlineopt);
  226. Object.assign(sets[k].pay, paylineopt);
  227. chartopt[k] = {
  228. type:'line', data: { labels: sets[k].hits.labels, datasets: [sets[k].hits, sets[k].pay] },
  229. options: {
  230. stacked: false, scales: {
  231. xAxes: [{ gridLines: { offsetGridLines: false }, display: true, type: 'time', time: timeOpts,
  232. scaleLabel: { show: false, labelString: k } }],
  233. yAxes: [{ type: 'linear', display: true, position: 'left', id: 'hits',
  234. /*scaleLabel: {show: true, labelString: 'hits', fontColor: '#EEE8d5', fontFamily: 'Arial'}*/ },
  235. { type: 'linear', display: true, position: 'right', id: 'pay', gridLines: { drawOnChartArea: false },
  236. /*scaleLabel: {show:true, labelString: 'pay', fontColor: '#EEE8d5', fontFamily: 'Arial'}*/ }]
  237. }
  238. }
  239. };
  240. }
  241. var html = ['<head>',
  242. '<link href="https://fonts.googleapis.com/css?family=Open+Sans:300,400" rel="stylesheet" type="text/css">',
  243. '<script src="https://cdn.jsdelivr.net/g/momentjs@2.10.6,chart.js@2.0.0-alpha4"></script>',
  244. '<style>',
  245. '.caption {width:60%;margin:auto;padding:5px;text-align:center;font-size:0.75em}',
  246. '.hc {color:#9559F0} .pc {color:#C8F230}',
  247. '.bc {color:#6A90F7} .sc {color:#EDC147}',
  248. '.container {display:flex;flex-wrap:wrap;align-content:center;justify-content:space-around}',
  249. '.mi {font-size:0.6em}',
  250. '.title {background-color:#EEE8D5;color:#073642;width:90%;margin:1% 6% 0% 5%;padding-left:2%;font-weight:400;}',
  251. '</style>',
  252. '<title>HIT Database Analytics | '+dbrange[2]+'</title>',
  253. '</head>',
  254. '<body style="color:#fff;background-color:#073642;font-size:100%;font-family:\'Open Sans\',Arial;font-weight:300">',
  255. '<h2 style="margin:1% 15%">MTurk HIT Database Analytics</h2>',
  256. '<h4 style="margin:0.5% 20%">'+dbrange[3][0]+' to '+dbrange[3][1]+'</h4>',
  257. '<div class="title">GENERAL STATISTICS</div>',
  258. '<div class="container">',
  259. '<div style="margin-top:5%;margin-left:5%;flex:1">',
  260. '<div class="container" style="margin-right:10%">',
  261. '<span style="text-align:right;color:#C7ED3E;flex:1">'+digitGroup(sets.all.hits.total)+'</span>',
  262. '<span style="text-align:center;flex:1">SUBMITTED</span>',
  263. '<span style="text-align:left;color:#C7ED3E;flex:1">$'+digitGroup(dec(sets.all.pay.total,2))+'</span>',
  264. '</div><div class="container" style="margin-right:10%">',
  265. '<span style="text-align:right;color:#C7ED3E;flex:1">'+digitGroup(sets.all.hits.bonus)+'</span>',
  266. '<span style="text-align:center;flex:1">BONUSES</span>',
  267. '<span style="text-align:left;color:#C7ED3E;flex:1">$'+digitGroup(dec(sets.all.pay.bonus,2))+'</span>',
  268. '</div><div class="container" style="margin-right:10%">',
  269. '<span style="text-align:right;color:#FF6666;flex:1">'+digitGroup(sets.all.hits.rejected)+'</span>',
  270. '<span style="text-align:center;flex:1">REJECTED</span>',
  271. '<span style="text-align:left;color:#FF6666;flex:1">$'+digitGroup(dec(sets.all.pay.rejected,2))+'</span>',
  272. '</div><div class="container" style="margin-right:10%">',
  273. '<span style="text-align:right;color:#E0B438;flex:1">'+digitGroup(sets.all.hits.pending)+'</span>',
  274. '<span style="text-align:center;flex:1">PENDING</span>',
  275. '<span style="text-align:left;color:#E0B438;flex:1">$'+digitGroup(dec(sets.all.pay.pending,2))+'</span>',
  276. '</div>',
  277. '<span class="mi" style="text-align:justify"><span style="color:#30CEF2">NOTE:</span> DOLLAR AMOUNT FOR \'PENDING\' MAY ',
  278. 'REFLECT VALUES FOR HITS WHICH HAVE ALREADY BEEN APPROVED. THESE FUNDS HAVE NOT BEEN FULLY CLEARED ',
  279. 'AND CREDITED TO YOUR ACCOUNT, AND THUS ARE STILL TECHNICALLY PENDING.</span>',
  280. '<div style="margin:10% 10% 10% 0;flex:1;text-align:center;">',
  281. 'AVERAGE SUBMISSION OF <span style="color:#C7ED3E">'+dec(sets.all.hitsPerRequester.avg,3)+'</span> ',
  282. '(<span style="color:#C7ED3E">&#177;'+dec(sets.all.hitsPerRequester.se,3)+'</span>) HITS PER REQUESTER',
  283. '<br><span class="mi">STANDARD DEVIATION: <span style="color:#FA5583">'+dec(sets.all.hitsPerRequester.sd,3)+'</span></span>',
  284. '<hr style="width:45%;color:#fff;background-color:#fff">',
  285. 'AVERAGE PAY OF <span style="color:#C7ED3E">$'+dec(sets.all.payPerHit.avg,3)+'</span> ',
  286. '(<span style="color:#C7ED3E">&#177;$'+dec(sets.all.payPerHit.se,3)+'</span>) PER HIT',
  287. '<br><span class="mi">STANDARD DEVIATION: <span style="color:#FA5583">'+dec(sets.all.payPerHit.sd,3)+'</span></span>',
  288. '<hr style="width:45%;color:#fff;background-color:#fff">',
  289. 'AVERAGING <span style="color:#C7ED3E">'+Math.ceil(1/(sets.all.payPerHit.avg+sets.all.payPerHit.se))+'</span>',
  290. '<span id="minbound"> TO <span style="color:#C7ED3E">'+Math.ceil(1/(sets.all.payPerHit.avg-sets.all.payPerHit.se))+
  291. '</span></span> HITS PER $1.00',
  292. '</div>',
  293. '</div>',
  294. '<div style="margin-top:3%;flex:1">',
  295. '<div><canvas id="hitdist"></canvas><div class="caption">HIT DISTRIBUTION PER REQUESTER</div></div>',
  296. '<div><canvas id="paydist"></canvas><div class="caption">PAY DISTRIBUTION PER HIT</div></div>',
  297. '</div>',
  298. '<div style="margin-top:10%;flex:1">',
  299. '<canvas id="batchpie"></canvas>',
  300. '<div class="caption"><span class="bc">BATCH</span> : <span class="sc">SURVEY</span><br>RATIO APPROXIMATION</div>',
  301. '</div>',
  302. '</div>',
  303. '<div class="title">ACTIVITY</div>',
  304. '<div class="container">',
  305. '<div style="margin-top:1%;margin-left:5%;flex:1">',
  306. '<canvas id="dailyline"></canvas>',
  307. '<div class="caption"><span class="hc">HITS SUBMITTED</span> | <span class="pc">TOTAL PAY</span><br>DAILY</div>',
  308. '</div>',
  309. '<div style="margin-top:1%;margin-right:6%;flex:1">',
  310. '<canvas id="monthlyline"></canvas>',
  311. '<div class="caption"><span class="hc">HITS SUBMITTED</span> | <span class="pc">TOTAL PAY</span><br>MONTHLY</div>',
  312. '</div>',
  313. '</div>',
  314. '<div class="container">',
  315. '<div style="margin-top:1%;margin-left:5%;flex:1">',
  316. '<canvas id="weeklyline"></canvas>',
  317. '<div class="caption"><span class="hc">HITS SUBMITTED</span> | <span class="pc">TOTAL PAY</span><br>WEEKLY</div>',
  318. '</div>',
  319. '<div style="margin-top:1%;margin-right:6%;flex:1">',
  320. '<canvas id="yearlyline"></canvas>',
  321. '<div class="caption"><span class="hc">HITS SUBMITTED</span> | <span class="pc">TOTAL PAY</span><br>YEARLY</div>',
  322. '</div>',
  323. '</div>',
  324. '<div class="title">TOP REQUESTERS</div>',
  325. '<div class="container">',
  326. '<table id="req10pay" style="font-weight:300;margin-top:1%;margin-left:5%;">',
  327. '<thead><tr><th colspan="2" style="text-align:left;padding-left:15">PAY</th></tr></thead><tbody></tbody></table>',
  328. '<table id="req10hit" style="font-weight:300;margin-top:1%;">',
  329. '<thead><tr><th colspan="2" style="text-align:left;padding-left:15">HITS</th></tr></thead><tbody></tbody></table>',
  330. '<table id="req10bns" style="font-weight:300;margin-top:1%;">',
  331. '<thead><tr><th colspan="2" style="text-align:left;padding-left:15">BONUS</th></tr></thead><tbody></tbody></table>',
  332. '<table id="req10rej" style="font-weight:300;margin-top:1%;margin-right:5%;">',
  333. '<thead><tr><th colspan="2" style="text-align:left;padding-left:15">REJECTIONS</th></tr></thead><tbody></tbody></table>',
  334. '</div>',
  335. '<script>',
  336. 'if (!NodeList.prototype[Symbol.iterator]) NodeList.prototype[Symbol.iterator] = Array.prototype[Symbol.iterator];',
  337. 'var chartopt = '+JSON.stringify(chartopt)+',',
  338. ' dLines = new Chart(document.getElementById("dailyline").getContext("2d"), chartopt.day),',
  339. ' mLines = new Chart(document.getElementById("monthlyline").getContext("2d"), chartopt.month),',
  340. ' wLines = new Chart(document.getElementById("weeklyline").getContext("2d"), chartopt.week),',
  341. ' yLines = new Chart(document.getElementById("yearlyline").getContext("2d"), chartopt.year),',
  342. ' bsPie = new Chart(document.getElementById("batchpie").getContext("2d"), chartopt.bsratio),',
  343. ' hdist = new Chart(document.getElementById("hitdist").getContext("2d"), chartopt.hdist),',
  344. ' pdist = new Chart(document.getElementById("paydist").getContext("2d"), chartopt.pdist),',
  345. ' r10hit = document.getElementById("req10hit").tBodies[0], r10hHtml = [],',
  346. ' r10pay = document.getElementById("req10pay").tBodies[0], r10pHtml = [],',
  347. ' r10bns = document.getElementById("req10bns").tBodies[0], r10bHtml = [],',
  348. ' r10rej = document.getElementById("req10rej").tBodies[0], r10rHtml = [],',
  349. ' payArr = '+JSON.stringify(sets.all.rpay)+',',
  350. ' hitArr = '+JSON.stringify(sets.all.rhits)+',',
  351. ' bnsArr = '+JSON.stringify(sets.all.rbonus)+',',
  352. ' rejArr = '+JSON.stringify(sets.all.rrej)+',',
  353. ' mb = document.getElementById("minbound");',
  354. 'if (mb.children[0].textContent === mb.previousSibling.textContent) mb.style.display = "none";',
  355. 'payArr.forEach(v => r10pHtml',
  356. '.push("<tr><td style=\\"text-align:right;padding-right:10;color:#47EDE1\\">$"+v.pay+"</td>',
  357. '<td style=\\"max-width:200\\">"+v.name+"</td></tr>"));',
  358. 'hitArr.forEach(v => r10hHtml',
  359. '.push("<tr><td style=\\"text-align:right;padding-right:10;color:#47EDE1\\">"+v.hits+"</td>',
  360. '<td style=\\"max-width:200\\">"+v.name+"</td></tr>"));',
  361. 'bnsArr.forEach(v => r10bHtml',
  362. '.push("<tr><td style=\\"text-align:right;padding-right:10;color:#47EDE1\\">$"+v.bonus+"</td>',
  363. '<td style=\\"max-width:200\\">"+v.name+"</td></tr>"));',
  364. 'rejArr.forEach(v => r10rHtml',
  365. '.push("<tr><td style=\\"text-align:right;padding-right:10;color:#FF6666\\">"+v.rej+',
  366. '" <span style=\\"color:#fff\\">(</span>$"+v.rejAmt+"<span style=\\"color:#fff\\">)</span></td>',
  367. '<td style=\\"max-width:200\\">"+v.name+"</td></tr>"));',
  368. 'if (r10bHtml.length === 0) r10bns.parentNode.style.display="none";',
  369. 'if (r10rHtml.length === 0) r10rej.parentNode.style.display="none";',
  370. 'r10hit.innerHTML = r10hHtml.join(""); r10pay.innerHTML = r10pHtml.join("");',
  371. 'r10bns.innerHTML = r10bHtml.join(""); r10rej.innerHTML = r10rHtml.join("");',
  372. '</script>',
  373. '</body>'];
  374. var blob = new Blob(html, {type:'text/html'}), _a = D.body.appendChild(D.createElement('A'));
  375. _a.href = URL.createObjectURL(blob);
  376. _a.target = '_blank';
  377. _a.click();
  378. _a.remove();
  379. }//}}}
  380.  
  381. function aggregateCursor(dat) {//{{{
  382. var ymwd = [dat.date.substr(0,4), dat.date.substr(0,7), dat.date.substr(0,4)+' week '+m(dat.date, 'YYYY-MM-DD').week(), dat.date],
  383. pay = isNaN(dat.reward) ? +dat.reward.pay : +dat.reward,
  384. bonus = isNaN(dat.reward) ? +dat.reward.bonus : 0;
  385.  
  386. if (dat.date < dbrange[3][0]) dbrange[3][0] = dat.date;
  387. else if (dat.date > dbrange[3][1]) dbrange[3][1] = dat.date;
  388.  
  389. // populate scaled aggregates
  390. for (var i=0; i<ymwd.length; i++) {
  391. // ... basic
  392. var def = sets[ymwdLabels[i]].aggregate.def,
  393. req = sets[ymwdLabels[i]].aggregate.req;
  394. if (!([ymwd[i]] in def)) {
  395. def[ymwd[i]] = { hits:0, pay:0 };
  396. if (i < 2) req[ymwd[i]] = {};
  397. }
  398. def[ymwd[i]].hits += 1;
  399. def[ymwd[i]].pay += pay;
  400. // ... by requester
  401. /*** data currently unused ***
  402. if (i > 1) continue; // skip--don't need this for day or week
  403. if (!(dat.requesterId in req[ymwd[i]]))
  404. req[ymwd[i]][dat.requesterId] = { name: dat.requesterName, hits: 0, pay: 0 };
  405. req[ymwd[i]][dat.requesterId].hits += 1;
  406. req[ymwd[i]][dat.requesterId].pay += pay;
  407. */
  408. }// end for ymwd loop
  409. // repeat requester aggregation for 'all'
  410. if (!(dat.requesterId in sets.all.aggregate))
  411. sets.all.aggregate[dat.requesterId] = { name: dat.requesterName, hits: 0, pay: 0, bonus: 0, rej: 0, rejAmt: 0};
  412. sets.all.aggregate[dat.requesterId].hits += 1;
  413. sets.all.aggregate[dat.requesterId].pay += pay;
  414. sets.all.aggregate[dat.requesterId].bonus += bonus;
  415. sets.all.aggregate[dat.requesterId].rej += dat.status === "Rejected" ? 1 : 0;
  416. sets.all.aggregate[dat.requesterId].rejAmt += dat.status === "Rejected" ? pay : 0;
  417.  
  418. // populate pay distribution data
  419. // hit distribution needs to wait until later--after aggregation
  420. sets.all.payPerHit.data.push(pay);
  421. distp.forEach( v => {
  422. var range = v.split('-');
  423. if (pay === 0) { sets.all.distribution.pay['0'] += 1; return; }
  424. if (pay > 5) { sets.all.distribution.pay['5.01+'] += 1; return; }
  425. if (pay >= range[0] && pay <= range[1]) { sets.all.distribution.pay[v] += 1; return; }
  426. });
  427. // increment general stats
  428. var _t = dat.title.trim();
  429. sets.all.hits.total += 1;
  430. sets.all.hits.rejected += dat.status === 'Rejected' ? 1 : 0;
  431. sets.all.hits.pending += dat.status === 'Pending Approval' ? 1 : 0;
  432. sets.all.hits.bonus += bonus ? 1 : 0;
  433. sets.all.hits.titles[_t] = sets.all.hits.titles[_t] + 1 || 1;
  434. sets.all.pay.total += pay;
  435. sets.all.pay.rejected += dat.status === 'Rejected' ? pay : 0;
  436. sets.all.pay.pending += /pending/i.test(dat.status) ? pay : 0;
  437. sets.all.pay.bonus += bonus;
  438. // populate data for batch approximation
  439. var hitLoc = sets.all.hits.batch.findIndex(v => v.title === _t && v.req === dat.requesterId);
  440. if (~hitLoc)
  441. sets.all.hits.batch[hitLoc].count += 1;
  442. else
  443. sets.all.hits.batch.push({ title: _t, req: dat.requesterId, count: 1 });
  444. }//}}}
  445.  
  446.  
  447. //set up a worker thread to prevent interface locking on large datasets
  448. var datasetsWorker = [//{{{
  449. 'var dec = '+dec+', digitGroup = '+digitGroup+';',
  450. 'Math.decRound = '+Math.decRound+';',
  451. // calculate the standard deviation
  452. 'function stdev(avg, N, data) {',
  453. 'var sum = 0;',
  454. 'data.forEach(v => sum += Math.pow(v-avg, 2));',
  455. 'return Math.sqrt(sum/(N-1));',
  456. '}',
  457.  
  458. 'function arrayFromObj(obj) {',
  459. 'var arr = [];',
  460. 'for (var k in obj) {',
  461. 'if (obj.hasOwnProperty(k))',
  462. 'arr.push(obj[k]);',
  463. '}',
  464. 'return arr;',
  465. '}',
  466.  
  467. 'onmessage = (e) => {',
  468. 'var sets = e.data, disth = '+JSON.stringify(disth)+', distp = '+JSON.stringify(distp)+', reqArr;',
  469. // sort aggregates
  470. 'for (var period of Object.keys(sets)) {',
  471. 'if (period === \'all\') { ',
  472. 'reqArr = arrayFromObj(sets.all.aggregate);',
  473. // calculate averages, sd, se
  474. 'postMessage( {type: "status", data: "Calculating averages..." });',
  475. 'var _pph = sets.all.payPerHit, _hpr = sets.all.hitsPerRequester;',
  476. '_pph.avg = sets.all.pay.total / sets.all.hits.total;',
  477. '_pph.sd = stdev(_pph.avg, _pph.data.length, _pph.data);',
  478. '_pph.se = _pph.sd/Math.sqrt(_pph.data.length);',
  479. '_hpr.data = reqArr.map( v => v.hits );',
  480. '_hpr.avg = sets.all.hits.total / _hpr.data.length;',
  481. '_hpr.sd = stdev(_hpr.avg, _hpr.data.length, _hpr.data);',
  482. '_hpr.se = _hpr.sd/Math.sqrt(_hpr.data.length);',
  483. // get hit distribution
  484. 'postMessage( {type: "status", data: "Populating distributions..." });',
  485. 'reqArr.forEach(v => {',
  486. 'disth.forEach(w => {',
  487. 'var range = w.split(\'-\');',
  488. 'if (range.length === 1) range.push(\'1\');',
  489. 'if (v.hits > 1000) { sets.all.distribution.hits[\'1001+\'] += 1; return; }',
  490. 'if (v.hits >= range[0] && v.hits <= range[1]) { sets.all.distribution.hits[w] += 1; return; }',
  491. '});',
  492. '});',
  493. // extract raw data for distributions
  494. 'disth.forEach(v => sets.all.distribution.data.hits.push(Math.decRound(100*sets.all.distribution.hits[v]/reqArr.length,3)));',
  495. 'distp.forEach(v => sets.all.distribution.data.pay.push(Math.decRound(100*sets.all.distribution.pay[v]/sets.all.hits.total,3)));',
  496. 'sets.all.distribution.data.labelsh = Object.keys(sets.all.distribution.hits);',
  497. 'sets.all.distribution.data.labelsp = Object.keys(sets.all.distribution.pay);',
  498. // get 'top' lists
  499. 'sets.all.rpay = reqArr.sort((a,b) => b.pay - a.pay ).slice(0,10);',
  500. 'sets.all.rhits = reqArr.sort((a,b) => b.hits - a.hits).slice(0,10);',
  501. 'sets.all.rbonus= reqArr.filter(e => e.bonus > 0).sort((a,b) => b.bonus - a.bonus).slice(0,10);',
  502. 'sets.all.rrej = reqArr.filter(e => e.rej > 0).sort((a,b) => b.rej - a.rej).slice(0,10);',
  503. 'sets.all.rpay.forEach(v => v.pay = digitGroup(dec(v.pay,2)));',
  504. 'sets.all.rhits.forEach(v => v.hits = digitGroup(v.hits));',
  505. 'sets.all.rbonus.forEach(v => v.bonus = digitGroup(dec(v.bonus,2)));',
  506. 'sets.all.rrej.forEach(v => v.rejAmt = dec(v.rejAmt,2));',
  507. 'continue;',
  508. '}',
  509. // populate labels/data for graphing
  510. 'postMessage({ type: "status", data: "Sorting by "+period+"..." });',
  511. 'var labels = Object.keys(sets[period].aggregate.def);',
  512. 'for (k of labels) {',
  513. 'if (~[\'hits\',\'pay\'].indexOf(k)) continue;',
  514. 'sets[period].pay.labels.push(k);',
  515. 'sets[period].hits.labels.push(k);',
  516. 'sets[period].pay.data.push(Math.decRound(sets[period].aggregate.def[k].pay,2));',
  517. 'sets[period].hits.data.push(sets[period].aggregate.def[k].hits);',
  518.  
  519. // sort req lists/period
  520. /*********** not currently used
  521. 'if (~[\'week\',\'day\'].indexOf(period)) continue;',
  522. 'var len = period === \'year\' ? 10 : 5;',
  523. 'reqArr = arrayFromObj(sets[period].aggregate.req[k]);',
  524. 'sets[period].rpay[k] = reqArr.sort((a,b) => b.pay - a.pay).slice(0,len);',
  525. 'sets[period].rhits[k] = reqArr.map(v => v).sort((a,b) => b.hits - a.hits).slice(0,len);',
  526. */
  527. '}',
  528. '}',
  529. // if there are more than 5 hits with the same title under the same requester, assume it's a batch
  530. 'sets.all.hits.batch = sets.all.hits.batch.filter(v => v.count > 5);',
  531. 'sets.all.hits.batch.total = 0;',
  532. 'sets.all.hits.batch.forEach(v => {if (typeof v === "object") sets.all.hits.batch.total += v.count});',
  533.  
  534. 'postMessage({ type: "obj", data: sets });',
  535.  
  536. '}'];//}}}
  537. datasetsWorker = new Blob(datasetsWorker, {type:'text/javascript'});
  538. var workerURL = URL.createObjectURL(datasetsWorker);
  539. var dsWorker = new Worker(workerURL);
  540. dsWorker.onmessage = (e) => {
  541. if (e.data.type === 'status') {
  542. window.Status.message = e.data.data;
  543. console.log(e.data.data);
  544. } else {
  545. metrics.mark('dsWorker','end');
  546. metrics.stop();metrics.report();
  547. window.Status.message = 'Done!';
  548. window.Progress.hide();
  549. drawCharts(e.data.data);
  550. }
  551. };
  552.  
  553. })(document);
  554.  
  555. // vim: ts=2:sw=2:et:fdm=marker:noai