HIT Database Analytics

Analytics for HIT Database--makes pretty graphs

当前为 2015-09-23 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name HIT Database Analytics
  3. // @author feihtality
  4. // @namespace https://greasyfork.org/en/users/12709
  5. // @version 0.5.002
  6. // @description Analytics for HIT Database--makes pretty graphs
  7. // @match https://www.mturk.com/mturk/dashboard
  8. // @grant none
  9. // ==/UserScript==
  10.  
  11.  
  12.  
  13. ((D) => {
  14. 'use strict';
  15.  
  16. if (!('decRound' in Math) || !('Status' in window) || !('Progress' in window)) {
  17. console.log('execution order is either too high or HITDB MKII is not detected');
  18. return;
  19. }
  20.  
  21. const DB_NAME = "HITDB_TESTING";
  22. Date.prototype.toISODateString = function() { return this.getUTCFullYear()+"-"+pad(this.getUTCMonth()+1)+"-"+pad(this.getUTCDate()); };
  23. var pad = n => ("00"+n).substr(-2),
  24. digitGroup = function(n) {
  25. n = String(n).split('.');
  26. if (n[0].length < 4) return n.join('.');
  27. n[0] = n[0].replace(/(\d)(?=(\d{3})+$)/g, '$1,');
  28. return n.join('.');
  29. },
  30. dec = (n,l) => Number(Math.decRound(n,l)).toFixed(l);
  31.  
  32. // inject interface
  33. var
  34. insertion = D.getElementById('hdbCSVInput'),
  35. searchbtn = D.getElementById('hdbSearch'),
  36. acheckbox = insertion.parentNode.insertBefore(D.createElement('INPUT'), insertion.nextSibling),
  37. alabel = insertion.parentNode.insertBefore(D.createElement('LABEL'), insertion.nextSibling),
  38. metrics = null;
  39.  
  40. acheckbox.type = 'checkbox';
  41. acheckbox.id = 'hdbAnalytics';
  42. acheckbox.style.verticalAlign = 'middle';
  43. alabel.textContent = 'Analyze';
  44. alabel.htmlFor = 'hdbAnalytics';
  45. alabel.style.verticalAlign = 'middle';
  46.  
  47. acheckbox.onclick = function() {
  48. if (searchbtn.textContent === "Export CSV") insertion.click();
  49. if (searchbtn.textContent === "Analyze") searchbtn.textContent = 'Search';
  50. else searchbtn.textContent = 'Analyze';
  51. };
  52. searchbtn.addEventListener('click', getData);
  53.  
  54. function getData(e) {
  55. if (e.target.textContent !== "Analyze") 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. range = null;
  61. } else if (dbrange[0] && !dbrange[1]) {
  62. dbrange[2] = dbrange[0]+'>>';
  63. range = window.IDBKeyRange.lowerBound(dbrange[0]);
  64. } else if (!dbrange[0] && dbrange[1]) {
  65. dbrange[2] = '<<'+dbrange[1];
  66. range = window.IDBKeyRange.upperBound(dbrange[1]);
  67. } else {
  68. dbrange[2] = dbrange[0]+':'+dbrange[1];
  69. range = window.IDBKeyRange.bound(dbrange[0],dbrange[1]);
  70. }
  71.  
  72. sets = new Sets();
  73. window.Progress.show();
  74. metrics = new window.Metrics('database_analytics');
  75. metrics.mark('dbrecall', 'start');
  76. dbrecall("HIT", {range: range}).then(analyzeData);//.then(drawCharts);
  77. }
  78.  
  79. /* . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . */
  80.  
  81. var
  82. Sets = function() {//{{{ main data obj
  83. this.year = {
  84. aggregate: { def: {}, req: {} },
  85. pay: { labels: [], data: [] },
  86. hits: { labels: [], data: [] },
  87. rpay: {},
  88. rhits: {}
  89. };
  90. this.month = {
  91. aggregate: { def: {}, req: {} },
  92. pay: { labels: [], data: [] },
  93. hits: { labels: [], data: [] },
  94. rpay: {},
  95. rhits: {}
  96. };
  97. this.week = {
  98. aggregate: { def: {}, req: {} },
  99. pay: { labels: [], data: [] },
  100. hits: { labels: [], data: [] },
  101. rpay: {},
  102. rhits: {}
  103. };
  104. this.day = {
  105. aggregate: { def: {}, req: {} },
  106. pay: { labels: [], data: [] },
  107. hits: { labels: [], data: [] },
  108. rpay: {},
  109. rhits: {}
  110. };
  111. this.all = {
  112. aggregate: [],
  113. distribution: {
  114. hits: {'1':0,'2-5':0,'6-10':0,'11-15':0,'16-20':0,'21-25':0,'26-30':0,'31-35':0,
  115. '36-40':0,'41-45':0,'46-50':0,'51-100':0,'101-150':0,'151-200':0,'201-250':0,'251-300':0,
  116. '301-350':0,'351-400':0,'401-450':0,'451-500':0,'501-550':0,'551-600':0,'601-650':0,'651-700':0,
  117. '701-750':0,'751-800':0,'801-850':0,'851-900':0,'901-950':0,'951-1000':0,'1000+':0},
  118. 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,
  119. '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,
  120. '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,
  121. '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,
  122. '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,
  123. '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,
  124. '4.76-5.00':0,'5.01+':0},
  125. data: { hits: [], labelsh: [], pay: [], labelsp: [] } },
  126. hits: { total: 0, rejected: 0, pending: 0, titles: {}, batch: [] },
  127. pay : { total: 0, rejected: 0, pending: 0 },
  128. hitsPerRequester: { avg: 0, data: [], sd: 0, se: 0 },
  129. payPerHit: { avg: 0, data: [], sd: 0, se: 0 }
  130. };
  131. },//}}}
  132. sets = new Sets(),
  133. disth = Object.keys(sets.all.distribution.hits),
  134. distp = Object.keys(sets.all.distribution.pay),
  135. ymwdLabels = Object.keys(sets), dbrange;
  136.  
  137. function dbrecall(os, options) {
  138. options = options || {};
  139. var
  140. index = options.index || "date",
  141. range = options.range || null,
  142. wRange = [null, null];
  143.  
  144. var total = 0;
  145. return new Promise( y => {
  146. window.indexedDB.open(DB_NAME).onsuccess = function() {
  147. this.result.transaction(os, "readonly").objectStore(os).index(index).openCursor(range).onsuccess = function() {
  148. if (this.result) {
  149. window.Status.message = 'Aggregating data... [ '+(total++)+' ]';
  150. wRange = aggregateCursor(this.result.value, wRange);
  151. this.result.continue();
  152. }
  153. else y(1);
  154. };
  155. this.result.close();
  156. };
  157. });
  158. }
  159.  
  160. function analyzeData(r) {
  161. void(r);
  162. metrics.mark('dbrecall','end');
  163. //var sets = data.getDatasets();
  164. console.log(sets);
  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: ['#4773ED', '#EDC147'] }]
  196. }
  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: '#FA5583', hoverBackgroundColor: '#C7ED3E' }]
  203. }, options: { scales: { xAxes: [{ labels: {fontSize: 0}, categorySpacing: 1 }] }}
  204. };
  205. chartopt.hdist = {
  206. type: 'bar', data: {
  207. labels: sets.all.distribution.data.labelsh,
  208. datasets: [{ data: sets.all.distribution.data.hits,
  209. backgroundColor: '#FA5583', hoverBackgroundColor: '#C7ED3E' }]
  210. }, options: { scales: { xAxes: [{ labels: {fontSize: 0}, categorySpacing: 1}] }}
  211. };
  212. continue;
  213. }
  214. Object.assign(sets[k].hits, hitlineopt);
  215. Object.assign(sets[k].pay, paylineopt);
  216. chartopt[k] = {
  217. type:'line', data: { labels: sets[k].hits.labels, datasets: [sets[k].hits, sets[k].pay] },
  218. options: {
  219. stacked: false, scales: {
  220. xAxes: [{ gridLines: { offsetGridLines: false }, display: true, scaleSteps: 10,
  221. labels: {fontSize: /*/^[ym]/.test(k) ? 5 :*/ 0, show: /^[ym]/.test(k) ? true : false} }],
  222. yAxes: [{ type: 'linear', display: true, position: 'left', id: 'hits' },
  223. { type: 'linear', display: true, position: 'right', id: 'pay', gridLines: { drawOnChartArea: false } }]
  224. }
  225. }
  226. };
  227. }
  228. var html = ['<head>',
  229. '<link href="https://fonts.googleapis.com/css?family=Open+Sans:300,400" rel="stylesheet" type="text/css">',
  230. '<script src="https://cdn.jsdelivr.net/chart.js/2.0.0-alpha3/Chart.min.js"></script>',
  231. '<style>',
  232. '.caption {width:60%;margin:auto;padding:5px;text-align:center;font-size:0.75em}',
  233. '.hc {color:#9559F0} .pc {color:#C8F230}',
  234. '.bc {color:#4773ED} .sc {color:#EDC147}',
  235. '.container {display:flex;flex-wrap:wrap;align-content:center;justify-content:space-around}',
  236. '.mi {font-size:0.6em}',
  237. '.title {background-color:#EEE8D5;color:#073642;width:90%;margin:1% 6% 0% 5%;padding-left:2%;font-weight:400;}',
  238. '</style>',
  239. '<title>HIT Database Analytics | '+dbrange[2]+'</title>',
  240. '</head>',
  241. '<body style="color:#fff;background-color:#073642;font-size:100%;font-family:\'Open Sans\',Arial;font-weight:300">',
  242. // '<i style="color:#fff;">Sigma = &#963;</i> <span style="color:#fff;font-family:"\'Times New Roman\', Arial;">&#963;</span>',
  243. '<div class="title">GENERAL STATISTICS</div>',
  244. '<div class="container">',
  245. '<div style="margin-top:5%;margin-left:5%;flex:1">',
  246. '<div class="container" style="margin-right:10%">',
  247. '<span style="text-align:right;color:#BBD12A;flex:1">'+digitGroup(sets.all.hits.total)+'</span>',
  248. '<span style="text-align:center;flex:1">SUBMITTED</span>',
  249. '<span style="text-align:left;color:#BBD12A;flex:1">$'+digitGroup(dec(sets.all.pay.total,2))+'</span>',
  250. '</div><div class="container" style="margin-right:10%">',
  251. '<span style="text-align:right;color:#F23054;flex:1">'+digitGroup(sets.all.hits.rejected)+'</span>',
  252. '<span style="text-align:center;flex:1">REJECTED</span>',
  253. '<span style="text-align:left;color:#F23054;flex:1">$'+digitGroup(dec(sets.all.pay.rejected,2))+'</span>',
  254. '</div><div class="container" style="margin-right:10%">',
  255. '<span style="text-align:right;color:#E0B438;flex:1">'+digitGroup(sets.all.hits.pending)+'</span>',
  256. '<span style="text-align:center;flex:1">PENDING</span>',
  257. '<span style="text-align:left;color:#E0B438;flex:1">$'+digitGroup(dec(sets.all.pay.pending,2))+'</span>',
  258. '</div>',
  259. '<span class="mi" style="text-align:justify"><span style="color:#30CEF2">NOTE:</span> DOLLAR AMOUNT FOR \'PENDING\' MAY ',
  260. 'REFLECT VALUES FOR HITS WHICH HAVE ALREADY BEEN APPROVED. THESE FUNDS HAVE NOT BEEN FULLY CLEARED ',
  261. 'AND CREDITED TO YOUR ACCOUNT, AND THUS ARE STILL TECHNICALLY PENDING.</span>',
  262. '<div style="margin:10% 10% 10% 0;flex:1;text-align:center;">',
  263. 'AVERAGE SUBMISSION OF <span style="color:#C7ED3E">'+dec(sets.all.hitsPerRequester.avg,3)+'</span> ',
  264. '(<span style="color:#C7ED3E">&#177;'+dec(sets.all.hitsPerRequester.se,3)+'</span>) HITS PER REQUESTER',
  265. '<br><span class="mi">STANDARD DEVIATION: <span style="color:#FA5583">'+dec(sets.all.hitsPerRequester.sd,3)+'</span></span>',
  266. '<hr style="width:45%;color:#fff;background-color:#fff">',
  267. 'AVERAGE PAY OF <span style="color:#C7ED3E">$'+dec(sets.all.payPerHit.avg,3)+'</span> ',
  268. '(<span style="color:#C7ED3E">&#177;$'+dec(sets.all.payPerHit.se,3)+'</span>) PER HIT',
  269. '<br><span class="mi">STANDARD DEVIATION: <span style="color:#FA5583">'+dec(sets.all.payPerHit.sd,3)+'</span></span>',
  270. '<hr style="width:45%;color:#fff;background-color:#fff">',
  271. 'AVERAGING <span style="color:#C7ED3E">'+Math.ceil(1/(sets.all.payPerHit.avg+sets.all.payPerHit.se))+'</span>',
  272. '<span id="minbound"> TO <span style="color:#C7ED3E">'+Math.ceil(1/(sets.all.payPerHit.avg-sets.all.payPerHit.se))+
  273. '</span></span> HITS PER $1.00',
  274. '</div>',
  275. '</div>',
  276. '<div style="margin-top:1%;flex:1">',
  277. '<div><canvas id="hitdist"></canvas><div class="caption">HIT DISTRIBUTION PER REQUESTER</div></div>',
  278. '<div><canvas id="paydist"></canvas><div class="caption">PAY DISTRIBUTION PER HIT</div></div>',
  279. '</div>',
  280. '<div style="margin-top:10%;flex:1">',
  281. '<canvas id="batchpie"></canvas>',
  282. '<div class="caption"><span class="bc">BATCH</span> : <span class="sc">SURVEY</span><br>RATIO APPROXIMATION</div>',
  283. '</div>',
  284. '</div>',
  285. '<div class="title">ACTIVITY</div>',
  286. '<div class="container">',
  287. '<div style="margin-top:1%;margin-left:5%;flex:1">',
  288. '<canvas id="dailyline"></canvas>',
  289. '<div class="caption"><span class="hc">HITS SUBMITTED</span> | <span class="pc">TOTAL PAY</span><br>DAILY</div>',
  290. '</div>',
  291. '<div style="margin-top:1%;margin-right:6%;flex:1">',
  292. '<canvas id="weeklyline"></canvas>',
  293. '<div class="caption"><span class="hc">HITS SUBMITTED</span> | <span class="pc">TOTAL PAY</span><br>WEEKLY</div>',
  294. '</div>',
  295. '</div>',
  296. '<div class="container">',
  297. '<div style="margin-top:1%;margin-left:5%;flex:1">',
  298. '<canvas id="monthlyline"></canvas>',
  299. '<div class="caption"><span class="hc">HITS SUBMITTED</span> | <span class="pc">TOTAL PAY</span><br>MONTHLY</div>',
  300. '</div>',
  301. '<div style="margin-top:1%;margin-right:6%;flex:1">',
  302. '<canvas id="yearlyline"></canvas>',
  303. '<div class="caption"><span class="hc">HITS SUBMITTED</span> | <span class="pc">TOTAL PAY</span><br>YEARLY</div>',
  304. '</div>',
  305. '</div>',
  306. '<div class="title">TOP REQUESTERS</div>',
  307. '<div class="container">',
  308. '<table id="req10pay" style="font-weight:300;margin-top:1%;margin-left:5%;">',
  309. '<thead><tr><th colspan="2" style="text-align:left;padding-left:15">PAY</th></tr></thead><tbody></tbody></table>',
  310. '<table id="req10hit" style="font-weight:300;margin-top:1%;margin-right:6%;">',
  311. '<thead><tr><th colspan="2" style="text-align:left;padding-left:15">HITS</th></tr></thead><tbody></tbody></table>',
  312. '</div>',
  313. '<script>',
  314. 'if (!NodeList.prototype[Symbol.iterator]) NodeList.prototype[Symbol.iterator] = Array.prototype[Symbol.iterator];',
  315. 'var chartopt = '+JSON.stringify(chartopt)+',',
  316. ' dLines = new Chart(document.getElementById("dailyline").getContext("2d"), chartopt.day),',
  317. ' mLines = new Chart(document.getElementById("monthlyline").getContext("2d"), chartopt.month),',
  318. ' wLines = new Chart(document.getElementById("weeklyline").getContext("2d"), chartopt.week),',
  319. ' yLines = new Chart(document.getElementById("yearlyline").getContext("2d"), chartopt.year),',
  320. ' bsPie = new Chart(document.getElementById("batchpie").getContext("2d"), chartopt.bsratio),',
  321. ' hdist = new Chart(document.getElementById("hitdist").getContext("2d"), chartopt.hdist),',
  322. ' pdist = new Chart(document.getElementById("paydist").getContext("2d"), chartopt.pdist),',
  323. ' r10hit = document.getElementById("req10hit").tBodies[0], r10hHtml = [],',
  324. ' r10pay = document.getElementById("req10pay").tBodies[0], r10pHtml = [],',
  325. ' payArr = '+JSON.stringify(sets.all.rpay)+',',
  326. ' hitArr = '+JSON.stringify(sets.all.rhits)+',',
  327. ' mb = document.getElementById("minbound");',
  328. 'if (mb.children[0].textContent === mb.previousSibling.textContent) mb.style.display = "none";',
  329. 'payArr.forEach(v => r10pHtml',
  330. '.push("<tr><td style=\\"text-align:right;padding-right:10;color:#47EDE1\\">$"+v.pay+"</td><td>"+v.name+"</td></tr>"));',
  331. 'hitArr.forEach(v => r10hHtml',
  332. '.push("<tr><td style=\\"text-align:right;padding-right:10;color:#47EDE1\\">"+v.hits+"</td><td>"+v.name+"</td></tr>"));',
  333. 'r10hit.innerHTML = r10hHtml.join(""); r10pay.innerHTML = r10pHtml.join("");',
  334. '</script>',
  335. '</body>'];
  336. var blob = new Blob(html, {type:'text/html'}), _a = D.body.appendChild(D.createElement('A'));
  337. _a.href = URL.createObjectURL(blob);
  338. _a.target = '_blank';
  339. _a.click();
  340. _a.remove();
  341. }//}}}
  342.  
  343. function aggregateCursor(dat, wRange) {//{{{
  344. var ymwd = [dat.date.substr(0,4), dat.date.substr(0,7), null, dat.date],
  345. pay = isNaN(dat.reward) ? +dat.reward.pay : +dat.reward;
  346. // determine weekly scales
  347. if (!wRange[0])
  348. wRange = getWeekRange(dat.date);
  349. if (dat.date >= wRange[1])
  350. wRange = getWeekRange(dat.date);
  351. ymwd[2] = wRange.join(':');
  352.  
  353. // populate scaled aggregates
  354. for (var i=0; i<ymwd.length; i++) {
  355. // ... basic
  356. if (!sets[ymwdLabels[i]].aggregate.def[ymwd[i]])
  357. sets[ymwdLabels[i]].aggregate.def[ymwd[i]] = { hits:0, pay:0 };
  358. sets[ymwdLabels[i]].aggregate.def[ymwd[i]].hits += 1;
  359. sets[ymwdLabels[i]].aggregate.def[ymwd[i]].pay += pay;
  360. // ... by requester
  361. if (i > 1) continue; // skip--don't need this for day or week
  362. if (!sets[ymwdLabels[i]].aggregate.req[ymwd[i]]) {
  363. sets[ymwdLabels[i]].aggregate.req[ymwd[i]] = [];
  364. sets[ymwdLabels[i]].rpay[ymwd[i]] = [];
  365. sets[ymwdLabels[i]].rhits[ymwd[i]] = [];
  366. }
  367. var reqArr = sets[ymwdLabels[i]].aggregate.req[ymwd[i]],
  368. reqLoc = reqArr.findIndex(v => v.req === dat.requesterId);
  369. if (~reqLoc) {
  370. reqArr[reqLoc].hits += 1;
  371. reqArr[reqLoc].pay += pay;
  372. }
  373. else
  374. reqArr.push({ req: dat.requesterId, name: dat.requesterName, hits: 1, pay: pay });
  375. }// end for ymwd loop
  376. // repeat requester aggregation for 'all'
  377. reqLoc = sets.all.aggregate.findIndex(v => v.req === dat.requesterId);
  378. if (~reqLoc) {
  379. sets.all.aggregate[reqLoc].hits += 1;
  380. sets.all.aggregate[reqLoc].pay += pay;
  381. }
  382. else
  383. sets.all.aggregate.push({ req: dat.requesterId, name: dat.requesterName, hits: 1, pay: pay });
  384.  
  385. // populate pay distribution data
  386. // hit distribution needs to wait until later--after aggregation
  387. sets.all.payPerHit.data.push(pay);
  388. distp.forEach( v => {
  389. var range = v.split('-');
  390. if (pay === 0) { sets.all.distribution.pay['0'] += 1; return; }
  391. if (pay > 5) { sets.all.distribution.pay['5.01+'] += 1; return; }
  392. if (pay >= range[0] && pay <= range[1]) { sets.all.distribution.pay[v] += 1; return; }
  393. });
  394. // increment general stats
  395. var _t = dat.title.trim();
  396. sets.all.hits.total += 1;
  397. sets.all.hits.rejected += dat.status === 'Rejected' ? 1 : 0;
  398. sets.all.hits.pending += dat.status === 'Pending Approval' ? 1 : 0;
  399. sets.all.hits.titles[_t] = sets.all.hits.titles[_t] + 1 || 1;
  400. sets.all.pay.total += pay;
  401. sets.all.pay.rejected += dat.status === 'Rejected' ? pay : 0;
  402. sets.all.pay.pending += /pending/i.test(dat.status) ? pay : 0;
  403. // populate data for batch approximation
  404. var hitLoc = sets.all.hits.batch.findIndex(v => v.title === _t && v.req === dat.requesterId);
  405. if (~hitLoc)
  406. sets.all.hits.batch[hitLoc].count += 1;
  407. else
  408. sets.all.hits.batch.push({ title: _t, req: dat.requesterId, count: 1 });
  409.  
  410. return wRange;
  411. }//}}}
  412.  
  413. function DBResult() {
  414. this.results = [];
  415. this.add = v => this.results.push(v);
  416. this.getDatasets = () => {// {{{
  417. return sets;
  418.  
  419. };//}}} getDatasets
  420. }// DBResult
  421.  
  422. function getWeekRange(date) {
  423. var _d = new Date(date), _ws, _we;
  424. _d.setUTCDate(_d.getUTCDate()-_d.getUTCDay());
  425. _ws = _d.toISODateString();
  426. _d.setUTCDate(_d.getUTCDate()+7);
  427. _we = _d.toISODateString();
  428. return [ _ws, _we ];
  429. }
  430.  
  431. //set up a worker thread to prevent interface locking on large datasets
  432. var datasetsWorker = [//{{{
  433. 'var dec = '+dec+', digitGroup = '+digitGroup+';',
  434. 'Math.decRound = '+Math.decRound+';',
  435. // calculate the standard deviation
  436. 'function stdev(avg, N, data) {',
  437. 'var sum = 0;',
  438. 'data.forEach(v => sum += Math.pow(v-avg, 2));',
  439. 'return Math.sqrt(sum/(N-1));',
  440. '}',
  441.  
  442. 'onmessage = (e) => {',
  443. 'var sets = e.data, disth = '+JSON.stringify(disth)+', distp = '+JSON.stringify(distp)+';',
  444. // sort aggregates
  445. 'for (var period of Object.keys(sets)) {',
  446. 'if (period === \'all\') { ',
  447. // calculate averages, sd, se
  448. 'postMessage( {type: "status", data: "Calculating averages..." });',
  449. 'var _pph = sets.all.payPerHit, _hpr = sets.all.hitsPerRequester;',
  450. '_pph.avg = sets.all.pay.total / sets.all.hits.total;',
  451. '_pph.sd = stdev(_pph.avg, _pph.data.length, _pph.data);',
  452. '_pph.se = _pph.sd/Math.sqrt(_pph.data.length);',
  453. '_hpr.data = sets.all.aggregate.map( v => v.hits );',
  454. '_hpr.avg = sets.all.hits.total / _hpr.data.length;',
  455. '_hpr.sd = stdev(_hpr.avg, _hpr.data.length, _hpr.data);',
  456. '_hpr.se = _hpr.sd/Math.sqrt(_hpr.data.length);',
  457. // get hit distribution
  458. 'postMessage( {type: "status", data: "Populating distributions..." });',
  459. 'sets.all.aggregate.forEach(v => {',
  460. 'disth.forEach(w => {',
  461. 'var range = w.split(\'-\');',
  462. 'if (range.length === 1) range.push(\'1\');',
  463. 'if (v.hits > 1000) { sets.all.distribution.hits[\'1001+\'] += 1; return; }',
  464. 'if (v.hits >= range[0] && v.hits <= range[1]) { sets.all.distribution.hits[w] += 1; return; }',
  465. '});',
  466. '});',
  467. // extract raw data for distributions
  468. 'disth.forEach(v => sets.all.distribution.data.hits.push(sets.all.distribution.hits[v]));',
  469. 'distp.forEach(v => sets.all.distribution.data.pay.push(sets.all.distribution.pay[v]));',
  470. 'sets.all.distribution.data.labelsh = Object.keys(sets.all.distribution.hits);',
  471. 'sets.all.distribution.data.labelsp = Object.keys(sets.all.distribution.pay);',
  472. // get 'top' lists
  473. 'sets.all.rpay = sets.all.aggregate.sort((a,b) => b.pay - a.pay ).slice(0,10);',
  474. 'sets.all.rhits = sets.all.aggregate.sort((a,b) => b.hits - a.hits).slice(0,10);',
  475. 'sets.all.rpay.forEach(v => v.pay = dec(v.pay,2));',
  476. 'sets.all.rhits.forEach(v => v.hits = digitGroup(v.hits));',
  477. 'continue;',
  478. '}',
  479. // populate labels/data for graphing
  480. 'postMessage({ type: "status", data: "Sorting by "+period+"..." });',
  481. 'var labels = Object.keys(sets[period].aggregate.def);',
  482. 'for (k of labels) {',
  483. 'if (~[\'hits\',\'pay\'].indexOf(k)) continue;',
  484. 'sets[period].pay.labels.push(k);',
  485. 'sets[period].hits.labels.push(k);',
  486. 'sets[period].pay.data.push(Math.decRound(sets[period].aggregate.def[k].pay,2));',
  487. 'sets[period].hits.data.push(sets[period].aggregate.def[k].hits);',
  488.  
  489. // sort req lists/period
  490. 'if (~[\'week\',\'day\'].indexOf(period)) continue;',
  491. 'var len = period === \'year\' ? 10 : 5;',
  492. 'reqArr = sets[period].aggregate.req[k];',
  493. 'sets[period].rpay[k] = reqArr.sort((a,b) => b.pay - a.pay).slice(0,len);',
  494. 'sets[period].rhits[k] = reqArr.map(v => v).sort((a,b) => b.hits - a.hits).slice(0,len);',
  495. '}',
  496. '}',
  497. // if there are more than 5 hits with the same title under the same requester, assume it's a batch
  498. 'sets.all.hits.batch = sets.all.hits.batch.filter(v => v.count > 5);',
  499. // workers fail to transfer nonenumerable properties :(
  500. //'Object.defineProperty(sets.all.hits.batch, \'total\', { configurable: true, writable: true, value: 0 });',
  501. 'sets.all.hits.batch.total = 0;',
  502. 'sets.all.hits.batch.forEach(v => {if (typeof v === "object") sets.all.hits.batch.total += v.count});',
  503.  
  504. 'postMessage({ type: "obj", data: sets });',
  505.  
  506. '}'];//}}}
  507. datasetsWorker = new Blob(datasetsWorker, {type:'text/javascript'});
  508. var workerURL = URL.createObjectURL(datasetsWorker);
  509. var dsWorker = new Worker(workerURL);
  510. dsWorker.onmessage = (e) => {
  511. if (e.data.type === 'status') {
  512. window.Status.message = e.data.data;
  513. console.log(e.data.data);
  514. } else {
  515. metrics.mark('dsWorker','end');
  516. metrics.stop();metrics.report();
  517. window.Status.message = 'Done!';
  518. window.Progress.hide();
  519. drawCharts(e.data.data);
  520. }
  521. };
  522.  
  523. })(document);
  524.  
  525. // vim: ts=2:sw=2:et:fdm=marker:noai
  526.  
  527. ((D) => {
  528. 'use strict';
  529.  
  530. if (!('decRound' in Math) || !('Status' in window) || !('Progress' in window)) {
  531. console.log('execution order is either too high or HITDB MKII is not detected');
  532. return;
  533. }
  534.  
  535. const DB_NAME = "DATA_TESTING";
  536. Date.prototype.toISODateString = function() { return this.getUTCFullYear()+"-"+pad(this.getUTCMonth()+1)+"-"+pad(this.getUTCDate()); };
  537. var pad = n => ("00"+n).substr(-2),
  538. digitGroup = function(n) {
  539. n = String(n).split('.');
  540. if (n[0].length < 4) return n.join('.');
  541. n[0] = n[0].replace(/(\d)(?=(\d{3})+$)/g, '$1,');
  542. return n.join('.');
  543. },
  544. dec = (n,l) => Number(Math.decRound(n,l)).toFixed(l);
  545.  
  546. // inject interface
  547. var
  548. insertion = D.getElementById('hdbCSVInput'),
  549. searchbtn = D.getElementById('hdbSearch'),
  550. acheckbox = insertion.parentNode.insertBefore(D.createElement('INPUT'), insertion.nextSibling),
  551. alabel = insertion.parentNode.insertBefore(D.createElement('LABEL'), insertion.nextSibling),
  552. metrics = null;
  553.  
  554. acheckbox.type = 'checkbox';
  555. acheckbox.id = 'hdbAnalytics';
  556. acheckbox.style.verticalAlign = 'middle';
  557. alabel.textContent = 'Analyze';
  558. alabel.htmlFor = 'hdbAnalytics';
  559. alabel.style.verticalAlign = 'middle';
  560.  
  561. acheckbox.onclick = function() {
  562. if (searchbtn.textContent === "Export CSV") insertion.click();
  563. if (searchbtn.textContent === "Analyze") searchbtn.textContent = 'Search';
  564. else searchbtn.textContent = 'Analyze';
  565. };
  566. searchbtn.addEventListener('click', getData);
  567.  
  568. function getData(e) {
  569. if (e.target.textContent !== "Analyze") return;
  570. var range;
  571. dbrange = [D.getElementById('hdbMinDate').value || undefined, D.getElementById('hdbMaxDate').value || undefined, ''];
  572. if (!dbrange[0] && !dbrange[1]) {
  573. dbrange[2] = 'ALL';
  574. range = null;
  575. } else if (dbrange[0] && !dbrange[1]) {
  576. dbrange[2] = dbrange[0]+'>>';
  577. range = window.IDBKeyRange.lowerBound(dbrange[0]);
  578. } else if (!dbrange[0] && dbrange[1]) {
  579. dbrange[2] = '<<'+dbrange[1];
  580. range = window.IDBKeyRange.upperBound(dbrange[1]);
  581. } else {
  582. dbrange[2] = dbrange[0]+':'+dbrange[1];
  583. range = window.IDBKeyRange.bound(dbrange[0],dbrange[1]);
  584. }
  585.  
  586. sets = new Sets();
  587. window.Progress.show();
  588. metrics = new window.Metrics('database_analytics');
  589. metrics.mark('dbrecall', 'start');
  590. dbrecall("HIT", {range: range}).then(analyzeData);//.then(drawCharts);
  591. }
  592.  
  593. /* . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . */
  594.  
  595. var
  596. Sets = function() {//{{{ main data obj
  597. this.year = {
  598. aggregate: { def: {}, req: {} },
  599. pay: { labels: [], data: [] },
  600. hits: { labels: [], data: [] },
  601. rpay: {},
  602. rhits: {}
  603. };
  604. this.month = {
  605. aggregate: { def: {}, req: {} },
  606. pay: { labels: [], data: [] },
  607. hits: { labels: [], data: [] },
  608. rpay: {},
  609. rhits: {}
  610. };
  611. this.week = {
  612. aggregate: { def: {}, req: {} },
  613. pay: { labels: [], data: [] },
  614. hits: { labels: [], data: [] },
  615. rpay: {},
  616. rhits: {}
  617. };
  618. this.day = {
  619. aggregate: { def: {}, req: {} },
  620. pay: { labels: [], data: [] },
  621. hits: { labels: [], data: [] },
  622. rpay: {},
  623. rhits: {}
  624. };
  625. this.all = {
  626. aggregate: [],
  627. distribution: {
  628. hits: {'1':0,'2-5':0,'6-10':0,'11-15':0,'16-20':0,'21-25':0,'26-30':0,'31-35':0,
  629. '36-40':0,'41-45':0,'46-50':0,'51-100':0,'101-150':0,'151-200':0,'201-250':0,'251-300':0,
  630. '301-350':0,'351-400':0,'401-450':0,'451-500':0,'501-550':0,'551-600':0,'601-650':0,'651-700':0,
  631. '701-750':0,'751-800':0,'801-850':0,'851-900':0,'901-950':0,'951-1000':0,'1000+':0},
  632. 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,
  633. '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,
  634. '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,
  635. '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,
  636. '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,
  637. '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,
  638. '4.76-5.00':0,'5.01+':0},
  639. data: { hits: [], labelsh: [], pay: [], labelsp: [] } },
  640. hits: { total: 0, rejected: 0, pending: 0, titles: {}, batch: [] },
  641. pay : { total: 0, rejected: 0, pending: 0 },
  642. hitsPerRequester: { avg: 0, data: [], sd: 0, se: 0 },
  643. payPerHit: { avg: 0, data: [], sd: 0, se: 0 }
  644. };
  645. },//}}}
  646. sets = new Sets(),
  647. disth = Object.keys(sets.all.distribution.hits),
  648. distp = Object.keys(sets.all.distribution.pay),
  649. ymwdLabels = Object.keys(sets), dbrange;
  650.  
  651. function dbrecall(os, options) {
  652. options = options || {};
  653. var
  654. index = options.index || "date",
  655. range = options.range || null,
  656. wRange = [null, null];
  657.  
  658. var total = 0;
  659. return new Promise( y => {
  660. window.indexedDB.open(DB_NAME).onsuccess = function() {
  661. this.result.transaction(os, "readonly").objectStore(os).index(index).openCursor(range).onsuccess = function() {
  662. if (this.result) {
  663. window.Status.message = 'Aggregating data... [ '+(total++)+' ]';
  664. wRange = aggregateCursor(this.result.value, wRange);
  665. this.result.continue();
  666. }
  667. else y(1);
  668. };
  669. this.result.close();
  670. };
  671. });
  672. }
  673.  
  674. function analyzeData(r) {
  675. void(r);
  676. metrics.mark('dbrecall','end');
  677. //var sets = data.getDatasets();
  678. console.log(sets);
  679. metrics.mark('dsWorker', 'start');
  680. dsWorker.postMessage(sets);
  681. }
  682.  
  683. function drawCharts(sets) {//{{{
  684. var
  685. hitlineopt = {
  686. label: 'HITs Submitted',
  687. yAxixID: 'hits',
  688. backgroundColor: 'rgba(149,89,240,0.1)'/*'rgba(42,161,152,0.1)'*/,
  689. borderColor : 'rgba(149,89,240,0.5)'/*'rgba(42,161,152,0.5)'*/,
  690. pointBackgroundColor : 'rgba(149,89,240,1)'/*'rgba(42,161,152,1)'*/,
  691. pointHoverBackgroundColor: 'rgba(92,0,230,1)'/*'rgba(38,139,210,1)'*/
  692. },
  693. paylineopt = {
  694. label: 'Total Pay',
  695. yAxisID: 'pay',
  696. backgroundColor: 'rgba(200,242,48,0.1)'/*'rgba(181,137,0,0.1)'*/,
  697. borderColor : 'rgba(200,242,48,0.5)'/*'rgba(181,137,0,0.5)'*/,
  698. pointBackgroundColor : 'rgba(200,242,48,1)'/*'rgba(181,137,0,1)'*/,
  699. pointHoverBackgroundColor: 'rgba(129,163,5,1)'/*'rgba(133,153,0,1)'*/
  700. },
  701. chartopt = {};
  702. for (var k of Object.keys(sets)) {
  703. if (k === 'all') {
  704. var a = sets.all.hits;
  705. chartopt.bsratio = {
  706. type: 'pie', data: {
  707. labels: ['batch %', 'survey %'],
  708. datasets: [{ data: [Math.decRound(100*a.batch.total/a.total,2), Math.decRound(100*(a.total-a.batch.total)/a.total,2)],
  709. backgroundColor: ['#4773ED', '#EDC147'] }]
  710. }
  711. };
  712. chartopt.pdist = {
  713. type: 'bar', data: {
  714. labels: sets.all.distribution.data.labelsp,
  715. datasets: [{ data: sets.all.distribution.data.pay,
  716. backgroundColor: '#FA5583', hoverBackgroundColor: '#C7ED3E' }]
  717. }, options: { scales: { xAxes: [{ labels: {fontSize: 0}, categorySpacing: 1 }] }}
  718. };
  719. chartopt.hdist = {
  720. type: 'bar', data: {
  721. labels: sets.all.distribution.data.labelsh,
  722. datasets: [{ data: sets.all.distribution.data.hits,
  723. backgroundColor: '#FA5583', hoverBackgroundColor: '#C7ED3E' }]
  724. }, options: { scales: { xAxes: [{ labels: {fontSize: 0}, categorySpacing: 1}] }}
  725. };
  726. continue;
  727. }
  728. Object.assign(sets[k].hits, hitlineopt);
  729. Object.assign(sets[k].pay, paylineopt);
  730. chartopt[k] = {
  731. type:'line', data: { labels: sets[k].hits.labels, datasets: [sets[k].hits, sets[k].pay] },
  732. options: {
  733. stacked: false, scales: {
  734. xAxes: [{ gridLines: { offsetGridLines: false }, display: true, scaleSteps: 10,
  735. labels: {fontSize: /*/^[ym]/.test(k) ? 5 :*/ 0, show: /^[ym]/.test(k) ? true : false} }],
  736. yAxes: [{ type: 'linear', display: true, position: 'left', id: 'hits' },
  737. { type: 'linear', display: true, position: 'right', id: 'pay', gridLines: { drawOnChartArea: false } }]
  738. }
  739. }
  740. };
  741. }
  742. var html = ['<head>',
  743. '<link href="https://fonts.googleapis.com/css?family=Open+Sans:300,400" rel="stylesheet" type="text/css">',
  744. '<script src="https://cdn.jsdelivr.net/chart.js/2.0.0-alpha3/Chart.min.js"></script>',
  745. '<style>',
  746. '.caption {width:60%;margin:auto;padding:5px;text-align:center;font-size:0.75em}',
  747. '.hc {color:#9559F0} .pc {color:#C8F230}',
  748. '.bc {color:#4773ED} .sc {color:#EDC147}',
  749. '.container {display:flex;flex-wrap:wrap;align-content:center;justify-content:space-around}',
  750. '.mi {font-size:0.6em}',
  751. '.title {background-color:#EEE8D5;color:#073642;width:90%;margin:1% 6% 0% 5%;padding-left:2%;font-weight:400;}',
  752. '</style>',
  753. '<title>HIT Database Analytics | '+dbrange[2]+'</title>',
  754. '</head>',
  755. '<body style="color:#fff;background-color:#073642;font-size:100%;font-family:\'Open Sans\',Arial;font-weight:300">',
  756. // '<i style="color:#fff;">Sigma = &#963;</i> <span style="color:#fff;font-family:"\'Times New Roman\', Arial;">&#963;</span>',
  757. '<div class="title">GENERAL STATISTICS</div>',
  758. '<div class="container">',
  759. '<div style="margin-top:5%;margin-left:5%;flex:1">',
  760. '<div class="container" style="margin-right:10%">',
  761. '<span style="text-align:right;color:#BBD12A;flex:1">'+digitGroup(sets.all.hits.total)+'</span>',
  762. '<span style="text-align:center;flex:1">SUBMITTED</span>',
  763. '<span style="text-align:left;color:#BBD12A;flex:1">$'+digitGroup(dec(sets.all.pay.total,2))+'</span>',
  764. '</div><div class="container" style="margin-right:10%">',
  765. '<span style="text-align:right;color:#F23054;flex:1">'+digitGroup(sets.all.hits.rejected)+'</span>',
  766. '<span style="text-align:center;flex:1">REJECTED</span>',
  767. '<span style="text-align:left;color:#F23054;flex:1">$'+digitGroup(dec(sets.all.pay.rejected,2))+'</span>',
  768. '</div><div class="container" style="margin-right:10%">',
  769. '<span style="text-align:right;color:#E0B438;flex:1">'+digitGroup(sets.all.hits.pending)+'</span>',
  770. '<span style="text-align:center;flex:1">PENDING</span>',
  771. '<span style="text-align:left;color:#E0B438;flex:1">$'+digitGroup(dec(sets.all.pay.pending,2))+'</span>',
  772. '</div>',
  773. '<span class="mi" style="text-align:justify"><span style="color:#30CEF2">NOTE:</span> DOLLAR AMOUNT FOR \'PENDING\' MAY ',
  774. 'REFLECT VALUES FOR HITS WHICH HAVE ALREADY BEEN APPROVED. THESE FUNDS HAVE NOT BEEN FULLY CLEARED ',
  775. 'AND CREDITED TO YOUR ACCOUNT, AND THUS ARE STILL TECHNICALLY PENDING.</span>',
  776. '<div style="margin:10% 10% 10% 0;flex:1;text-align:center;">',
  777. 'AVERAGE SUBMISSION OF <span style="color:#C7ED3E">'+dec(sets.all.hitsPerRequester.avg,3)+'</span> ',
  778. '(<span style="color:#C7ED3E">&#177;'+dec(sets.all.hitsPerRequester.se,3)+'</span>) HITS PER REQUESTER',
  779. '<br><span class="mi">STANDARD DEVIATION: <span style="color:#FA5583">'+dec(sets.all.hitsPerRequester.sd,3)+'</span></span>',
  780. '<hr style="width:45%;color:#fff;background-color:#fff">',
  781. 'AVERAGE PAY OF <span style="color:#C7ED3E">$'+dec(sets.all.payPerHit.avg,3)+'</span> ',
  782. '(<span style="color:#C7ED3E">&#177;$'+dec(sets.all.payPerHit.se,3)+'</span>) PER HIT',
  783. '<br><span class="mi">STANDARD DEVIATION: <span style="color:#FA5583">'+dec(sets.all.payPerHit.sd,3)+'</span></span>',
  784. '<hr style="width:45%;color:#fff;background-color:#fff">',
  785. 'AVERAGING <span style="color:#C7ED3E">'+Math.ceil(1/(sets.all.payPerHit.avg+sets.all.payPerHit.se))+'</span>',
  786. '<span id="minbound"> TO <span style="color:#C7ED3E">'+Math.ceil(1/(sets.all.payPerHit.avg-sets.all.payPerHit.se))+
  787. '</span></span> HITS PER $1.00',
  788. '</div>',
  789. '</div>',
  790. '<div style="margin-top:1%;flex:1">',
  791. '<div><canvas id="hitdist"></canvas><div class="caption">HIT DISTRIBUTION PER REQUESTER</div></div>',
  792. '<div><canvas id="paydist"></canvas><div class="caption">PAY DISTRIBUTION PER HIT</div></div>',
  793. '</div>',
  794. '<div style="margin-top:10%;flex:1">',
  795. '<canvas id="batchpie"></canvas>',
  796. '<div class="caption"><span class="bc">BATCH</span> : <span class="sc">SURVEY</span><br>RATIO APPROXIMATION</div>',
  797. '</div>',
  798. '</div>',
  799. '<div class="title">ACTIVITY</div>',
  800. '<div class="container">',
  801. '<div style="margin-top:1%;margin-left:5%;flex:1">',
  802. '<canvas id="dailyline"></canvas>',
  803. '<div class="caption"><span class="hc">HITS SUBMITTED</span> | <span class="pc">TOTAL PAY</span><br>DAILY</div>',
  804. '</div>',
  805. '<div style="margin-top:1%;margin-right:6%;flex:1">',
  806. '<canvas id="weeklyline"></canvas>',
  807. '<div class="caption"><span class="hc">HITS SUBMITTED</span> | <span class="pc">TOTAL PAY</span><br>WEEKLY</div>',
  808. '</div>',
  809. '</div>',
  810. '<div class="container">',
  811. '<div style="margin-top:1%;margin-left:5%;flex:1">',
  812. '<canvas id="monthlyline"></canvas>',
  813. '<div class="caption"><span class="hc">HITS SUBMITTED</span> | <span class="pc">TOTAL PAY</span><br>MONTHLY</div>',
  814. '</div>',
  815. '<div style="margin-top:1%;margin-right:6%;flex:1">',
  816. '<canvas id="yearlyline"></canvas>',
  817. '<div class="caption"><span class="hc">HITS SUBMITTED</span> | <span class="pc">TOTAL PAY</span><br>YEARLY</div>',
  818. '</div>',
  819. '</div>',
  820. '<div class="title">TOP REQUESTERS</div>',
  821. '<div class="container">',
  822. '<table id="req10pay" style="font-weight:300;margin-top:1%;margin-left:5%;">',
  823. '<thead><tr><th colspan="2" style="text-align:left;padding-left:15">PAY</th></tr></thead><tbody></tbody></table>',
  824. '<table id="req10hit" style="font-weight:300;margin-top:1%;margin-right:6%;">',
  825. '<thead><tr><th colspan="2" style="text-align:left;padding-left:15">HITS</th></tr></thead><tbody></tbody></table>',
  826. '</div>',
  827. '<script>',
  828. 'if (!NodeList.prototype[Symbol.iterator]) NodeList.prototype[Symbol.iterator] = Array.prototype[Symbol.iterator];',
  829. 'var chartopt = '+JSON.stringify(chartopt)+',',
  830. ' dLines = new Chart(document.getElementById("dailyline").getContext("2d"), chartopt.day),',
  831. ' mLines = new Chart(document.getElementById("monthlyline").getContext("2d"), chartopt.month),',
  832. ' wLines = new Chart(document.getElementById("weeklyline").getContext("2d"), chartopt.week),',
  833. ' yLines = new Chart(document.getElementById("yearlyline").getContext("2d"), chartopt.year),',
  834. ' bsPie = new Chart(document.getElementById("batchpie").getContext("2d"), chartopt.bsratio),',
  835. ' hdist = new Chart(document.getElementById("hitdist").getContext("2d"), chartopt.hdist),',
  836. ' pdist = new Chart(document.getElementById("paydist").getContext("2d"), chartopt.pdist),',
  837. ' r10hit = document.getElementById("req10hit").tBodies[0], r10hHtml = [],',
  838. ' r10pay = document.getElementById("req10pay").tBodies[0], r10pHtml = [],',
  839. ' payArr = '+JSON.stringify(sets.all.rpay)+',',
  840. ' hitArr = '+JSON.stringify(sets.all.rhits)+',',
  841. ' mb = document.getElementById("minbound");',
  842. 'if (mb.children[0].textContent === mb.previousSibling.textContent) mb.style.display = "none";',
  843. 'payArr.forEach(v => r10pHtml',
  844. '.push("<tr><td style=\\"text-align:right;padding-right:10;color:#47EDE1\\">$"+v.pay+"</td><td>"+v.name+"</td></tr>"));',
  845. 'hitArr.forEach(v => r10hHtml',
  846. '.push("<tr><td style=\\"text-align:right;padding-right:10;color:#47EDE1\\">"+v.hits+"</td><td>"+v.name+"</td></tr>"));',
  847. 'r10hit.innerHTML = r10hHtml.join(""); r10pay.innerHTML = r10pHtml.join("");',
  848. '</script>',
  849. '</body>'];
  850. var blob = new Blob(html, {type:'text/html'}), _a = D.body.appendChild(D.createElement('A'));
  851. _a.href = URL.createObjectURL(blob);
  852. _a.target = '_blank';
  853. _a.click();
  854. _a.remove();
  855. }//}}}
  856.  
  857. function aggregateCursor(dat, wRange) {//{{{
  858. var ymwd = [dat.date.substr(0,4), dat.date.substr(0,7), null, dat.date],
  859. pay = isNaN(dat.reward) ? +dat.reward.pay : +dat.reward;
  860. // determine weekly scales
  861. if (!wRange[0])
  862. wRange = getWeekRange(dat.date);
  863. if (dat.date >= wRange[1])
  864. wRange = getWeekRange(dat.date);
  865. ymwd[2] = wRange.join(':');
  866.  
  867. // populate scaled aggregates
  868. for (var i=0; i<ymwd.length; i++) {
  869. // ... basic
  870. if (!sets[ymwdLabels[i]].aggregate.def[ymwd[i]])
  871. sets[ymwdLabels[i]].aggregate.def[ymwd[i]] = { hits:0, pay:0 };
  872. sets[ymwdLabels[i]].aggregate.def[ymwd[i]].hits += 1;
  873. sets[ymwdLabels[i]].aggregate.def[ymwd[i]].pay += pay;
  874. // ... by requester
  875. if (i > 1) continue; // skip--don't need this for day or week
  876. if (!sets[ymwdLabels[i]].aggregate.req[ymwd[i]]) {
  877. sets[ymwdLabels[i]].aggregate.req[ymwd[i]] = [];
  878. sets[ymwdLabels[i]].rpay[ymwd[i]] = [];
  879. sets[ymwdLabels[i]].rhits[ymwd[i]] = [];
  880. }
  881. var reqArr = sets[ymwdLabels[i]].aggregate.req[ymwd[i]],
  882. reqLoc = reqArr.findIndex(v => v.req === dat.requesterId);
  883. if (~reqLoc) {
  884. reqArr[reqLoc].hits += 1;
  885. reqArr[reqLoc].pay += pay;
  886. }
  887. else
  888. reqArr.push({ req: dat.requesterId, name: dat.requesterName, hits: 1, pay: pay });
  889. }// end for ymwd loop
  890. // repeat requester aggregation for 'all'
  891. reqLoc = sets.all.aggregate.findIndex(v => v.req === dat.requesterId);
  892. if (~reqLoc) {
  893. sets.all.aggregate[reqLoc].hits += 1;
  894. sets.all.aggregate[reqLoc].pay += pay;
  895. }
  896. else
  897. sets.all.aggregate.push({ req: dat.requesterId, name: dat.requesterName, hits: 1, pay: pay });
  898.  
  899. // populate pay distribution data
  900. // hit distribution needs to wait until later--after aggregation
  901. sets.all.payPerHit.data.push(pay);
  902. distp.forEach( v => {
  903. var range = v.split('-');
  904. if (pay === 0) { sets.all.distribution.pay['0'] += 1; return; }
  905. if (pay > 5) { sets.all.distribution.pay['5.01+'] += 1; return; }
  906. if (pay >= range[0] && pay <= range[1]) { sets.all.distribution.pay[v] += 1; return; }
  907. });
  908. // increment general stats
  909. var _t = dat.title.trim();
  910. sets.all.hits.total += 1;
  911. sets.all.hits.rejected += dat.status === 'Rejected' ? 1 : 0;
  912. sets.all.hits.pending += dat.status === 'Pending Approval' ? 1 : 0;
  913. sets.all.hits.titles[_t] = sets.all.hits.titles[_t] + 1 || 1;
  914. sets.all.pay.total += pay;
  915. sets.all.pay.rejected += dat.status === 'Rejected' ? pay : 0;
  916. sets.all.pay.pending += /pending/i.test(dat.status) ? pay : 0;
  917. // populate data for batch approximation
  918. var hitLoc = sets.all.hits.batch.findIndex(v => v.title === _t && v.req === dat.requesterId);
  919. if (~hitLoc)
  920. sets.all.hits.batch[hitLoc].count += 1;
  921. else
  922. sets.all.hits.batch.push({ title: _t, req: dat.requesterId, count: 1 });
  923.  
  924. return wRange;
  925. }//}}}
  926.  
  927. function DBResult() {
  928. this.results = [];
  929. this.add = v => this.results.push(v);
  930. this.getDatasets = () => {// {{{
  931. return sets;
  932.  
  933. };//}}} getDatasets
  934. }// DBResult
  935.  
  936. function getWeekRange(date) {
  937. var _d = new Date(date), _ws, _we;
  938. _d.setUTCDate(_d.getUTCDate()-_d.getUTCDay());
  939. _ws = _d.toISODateString();
  940. _d.setUTCDate(_d.getUTCDate()+7);
  941. _we = _d.toISODateString();
  942. return [ _ws, _we ];
  943. }
  944.  
  945. //set up a worker thread to prevent interface locking on large datasets
  946. var datasetsWorker = [//{{{
  947. 'var dec = '+dec+', digitGroup = '+digitGroup+';',
  948. 'Math.decRound = '+Math.decRound+';',
  949. // calculate the standard deviation
  950. 'function stdev(avg, N, data) {',
  951. 'var sum = 0;',
  952. 'data.forEach(v => sum += Math.pow(v-avg, 2));',
  953. 'return Math.sqrt(sum/(N-1));',
  954. '}',
  955.  
  956. 'onmessage = (e) => {',
  957. 'var sets = e.data, disth = '+JSON.stringify(disth)+', distp = '+JSON.stringify(distp)+';',
  958. // sort aggregates
  959. 'for (var period of Object.keys(sets)) {',
  960. 'if (period === \'all\') { ',
  961. // calculate averages, sd, se
  962. 'postMessage( {type: "status", data: "Calculating averages..." });',
  963. 'var _pph = sets.all.payPerHit, _hpr = sets.all.hitsPerRequester;',
  964. '_pph.avg = sets.all.pay.total / sets.all.hits.total;',
  965. '_pph.sd = stdev(_pph.avg, _pph.data.length, _pph.data);',
  966. '_pph.se = _pph.sd/Math.sqrt(_pph.data.length);',
  967. '_hpr.data = sets.all.aggregate.map( v => v.hits );',
  968. '_hpr.avg = sets.all.hits.total / _hpr.data.length;',
  969. '_hpr.sd = stdev(_hpr.avg, _hpr.data.length, _hpr.data);',
  970. '_hpr.se = _hpr.sd/Math.sqrt(_hpr.data.length);',
  971. // get hit distribution
  972. 'postMessage( {type: "status", data: "Populating distributions..." });',
  973. 'sets.all.aggregate.forEach(v => {',
  974. 'disth.forEach(w => {',
  975. 'var range = w.split(\'-\');',
  976. 'if (range.length === 1) range.push(\'1\');',
  977. 'if (v.hits > 1000) { sets.all.distribution.hits[\'1001+\'] += 1; return; }',
  978. 'if (v.hits >= range[0] && v.hits <= range[1]) { sets.all.distribution.hits[w] += 1; return; }',
  979. '});',
  980. '});',
  981. // extract raw data for distributions
  982. 'disth.forEach(v => sets.all.distribution.data.hits.push(sets.all.distribution.hits[v]));',
  983. 'distp.forEach(v => sets.all.distribution.data.pay.push(sets.all.distribution.pay[v]));',
  984. 'sets.all.distribution.data.labelsh = Object.keys(sets.all.distribution.hits);',
  985. 'sets.all.distribution.data.labelsp = Object.keys(sets.all.distribution.pay);',
  986. // get 'top' lists
  987. 'sets.all.rpay = sets.all.aggregate.sort((a,b) => b.pay - a.pay ).slice(0,10);',
  988. 'sets.all.rhits = sets.all.aggregate.sort((a,b) => b.hits - a.hits).slice(0,10);',
  989. 'sets.all.rpay.forEach(v => v.pay = dec(v.pay,2));',
  990. 'sets.all.rhits.forEach(v => v.hits = digitGroup(v.hits));',
  991. 'continue;',
  992. '}',
  993. // populate labels/data for graphing
  994. 'postMessage({ type: "status", data: "Sorting by "+period+"..." });',
  995. 'var labels = Object.keys(sets[period].aggregate.def);',
  996. 'for (k of labels) {',
  997. 'if (~[\'hits\',\'pay\'].indexOf(k)) continue;',
  998. 'sets[period].pay.labels.push(k);',
  999. 'sets[period].hits.labels.push(k);',
  1000. 'sets[period].pay.data.push(Math.decRound(sets[period].aggregate.def[k].pay,2));',
  1001. 'sets[period].hits.data.push(sets[period].aggregate.def[k].hits);',
  1002.  
  1003. // sort req lists/period
  1004. 'if (~[\'week\',\'day\'].indexOf(period)) continue;',
  1005. 'var len = period === \'year\' ? 10 : 5;',
  1006. 'reqArr = sets[period].aggregate.req[k];',
  1007. 'sets[period].rpay[k] = reqArr.sort((a,b) => b.pay - a.pay).slice(0,len);',
  1008. 'sets[period].rhits[k] = reqArr.map(v => v).sort((a,b) => b.hits - a.hits).slice(0,len);',
  1009. '}',
  1010. '}',
  1011. // if there are more than 5 hits with the same title under the same requester, assume it's a batch
  1012. 'sets.all.hits.batch = sets.all.hits.batch.filter(v => v.count > 5);',
  1013. // workers fail to transfer nonenumerable properties :(
  1014. //'Object.defineProperty(sets.all.hits.batch, \'total\', { configurable: true, writable: true, value: 0 });',
  1015. 'sets.all.hits.batch.total = 0;',
  1016. 'sets.all.hits.batch.forEach(v => {if (typeof v === "object") sets.all.hits.batch.total += v.count});',
  1017.  
  1018. 'postMessage({ type: "obj", data: sets });',
  1019.  
  1020. '}'];//}}}
  1021. datasetsWorker = new Blob(datasetsWorker, {type:'text/javascript'});
  1022. var workerURL = URL.createObjectURL(datasetsWorker);
  1023. var dsWorker = new Worker(workerURL);
  1024. dsWorker.onmessage = (e) => {
  1025. if (e.data.type === 'status') {
  1026. window.Status.message = e.data.data;
  1027. console.log(e.data.data);
  1028. } else {
  1029. metrics.mark('dsWorker','end');
  1030. metrics.stop();metrics.report();
  1031. window.Status.message = 'Done!';
  1032. window.Progress.hide();
  1033. drawCharts(e.data.data);
  1034. }
  1035. };
  1036.  
  1037. })(document);
  1038.  
  1039. // vim: ts=2:sw=2:et:fdm=marker:noai