AO3: Quality score (Adjusted Kudos/Hits ratio)

Uses the kudos/hits ratio, number of chapters, and statistical evaluation to score and sort AO3 works.

  1. // ==UserScript==
  2. // @name AO3: Quality score (Adjusted Kudos/Hits ratio)
  3. // @description Uses the kudos/hits ratio, number of chapters, and statistical evaluation to score and sort AO3 works.
  4. // @namespace https://greasyfork.org/scripts/3144-ao3-kudos-hits-ratio
  5. // @author Min (Small edits made by TheShinySnivy)
  6. // @version 1.4
  7. // @history 1.4 - always show hits on stats page, require jquery (for firefox)
  8. // @history 1.3 - works for statistics, option to show hitcount
  9. // @history 1.2 - makes use of new stats classes
  10. // @grant none
  11. // @require https://ajax.googleapis.com/ajax/libs/jquery/1.9.0/jquery.min.js
  12. // @include http://archiveofourown.org/*
  13. // @include https://archiveofourown.org/*
  14. // ==/UserScript==
  15.  
  16. // ~~ SETTINGS ~~ //
  17.  
  18. // count kudos/hits automatically: true/false
  19. var always_count = true;
  20.  
  21. // sort works on this page by kudos/hits ratio in descending order automatically: true/false
  22. var always_sort = false;
  23.  
  24. // hide hitcount: true/false
  25. var hide_hitcount = true;
  26.  
  27. // colour background depending on percentage: true/false
  28. var colourbg = true;
  29.  
  30. // lvl1 & lvl2 - percentage levels separating red, yellow and green background; ratio_red, ratio_yellow, ratio_green - background colours
  31. var ratio_red = '#ffdede';
  32. var lvl1 = 4;
  33. var ratio_yellow = '#fdf2a3';
  34. var lvl2 = 7;
  35. var ratio_green = '#c4eac3';
  36.  
  37. // ~~ END OF SETTINGS ~~ //
  38.  
  39.  
  40.  
  41. // STUFF HAPPENS BELOW //
  42.  
  43. (function($) {
  44.  
  45. // check user settings
  46. if (typeof(Storage) !== 'undefined') {
  47.  
  48. var always_count_set = localStorage.getItem('alwayscountlocal');
  49. var always_sort_set = localStorage.getItem('alwayssortlocal');
  50. var hide_hitcount_set = localStorage.getItem('hidehitcountlocal');
  51.  
  52. if (always_count_set == 'no') {
  53. always_count = false;
  54. }
  55.  
  56. if (always_sort_set == 'yes') {
  57. always_sort = true;
  58. }
  59.  
  60. if (hide_hitcount_set == 'no') {
  61. hide_hitcount = false;
  62. }
  63. }
  64.  
  65. // set defaults for countableness and sortableness
  66. var countable = false;
  67. var sortable = false;
  68. var stats_page = false;
  69.  
  70. // check if it's a list of works or bookmarks, or header on work page, and attach the menu
  71. checkCountable();
  72.  
  73. // if set to automatic
  74. if (always_count) {
  75. countRatio();
  76.  
  77. if (always_sort) {
  78. sortByRatio();
  79. }
  80. }
  81.  
  82.  
  83.  
  84.  
  85. // check if it's a list of works/bookmarks/statistics, or header on work page
  86. function checkCountable() {
  87.  
  88. var found_stats = $('dl.stats');
  89.  
  90. if (found_stats.length) {
  91.  
  92. if (found_stats.closest('li').is('.work') || found_stats.closest('li').is('.bookmark')) {
  93. countable = true;
  94. sortable = true;
  95.  
  96. addRatioMenu();
  97. }
  98. else if (found_stats.parents('.statistics').length) {
  99. countable = true;
  100. sortable = true;
  101. stats_page = true;
  102.  
  103. addRatioMenu();
  104. }
  105. else if (found_stats.parents('dl.work').length) {
  106. countable = true;
  107.  
  108. addRatioMenu();
  109. }
  110. }
  111. }
  112.  
  113.  
  114. function countRatio() {
  115.  
  116. if (countable) {
  117.  
  118. $('dl.stats').each(function() {
  119.  
  120. var hits_value = $(this).find('dd.hits');
  121. var kudos_value = $(this).find('dd.kudos');
  122.  
  123. var chapters_value = $(this).find('dd.chapters');
  124. var split_string = chapters_value.text().split("/");
  125. var chapters_string = split_string[0];
  126.  
  127.  
  128. // if hits and kudos were found
  129. if (kudos_value.length && hits_value.length && hits_value.text() !== '0') {
  130.  
  131. // get counts
  132. var hits_count = parseInt(hits_value.text());
  133. var kudos_count = parseInt(kudos_value.text());
  134. var chapters_count = parseInt(chapters_string.toString());
  135.  
  136. console.log("Hits: " + hits_count + "Kudos: " + kudos_count + "Chapters:" + chapters_count);
  137.  
  138. var new_hits_count = hits_count / Math.sqrt(chapters_count);
  139.  
  140. console.log("New hits count: " + new_hits_count);
  141.  
  142. // count percentage
  143. var percents = 100*kudos_count/new_hits_count;
  144. if (kudos_count < 11) {
  145. percents = 1;
  146. }
  147. var p_value = getPValue(new_hits_count, kudos_count, chapters_count);
  148. if (p_value < 0.05) {
  149. percents = 1;
  150. }
  151.  
  152. // get percentage with one decimal point
  153. var percents_print = percents.toFixed(1).replace('.',',');
  154.  
  155. // add ratio stats
  156. var ratio_label = $('<dt class="kudoshits"></dt>').text('Score:');
  157. var ratio_value = $('<dd class="kudoshits"></dd>').text(percents_print + '');
  158. hits_value.after('\n', ratio_label, '\n', ratio_value);
  159.  
  160. if (colourbg) {
  161. // colour background depending on percentage
  162. if (percents >= lvl2) {
  163. ratio_value.css('background-color', ratio_green);
  164. }
  165. else if (percents >= lvl1) {
  166. ratio_value.css('background-color', ratio_yellow);
  167. }
  168. else {
  169. ratio_value.css('background-color', ratio_red);
  170. }
  171. }
  172.  
  173. if (hide_hitcount && !stats_page) {
  174. // hide hitcount label and value
  175. $(this).find('.hits').css('display', 'none');
  176. }
  177.  
  178. // add attribute to the blurb for sorting
  179. $(this).closest('li').attr('kudospercent', percents);
  180. }
  181. else {
  182. // add attribute to the blurb for sorting
  183. $(this).closest('li').attr('kudospercent', 0);
  184. }
  185. });
  186. }
  187. }
  188.  
  189. var null_hyp = 0.04;
  190.  
  191. function getPValue(hits, kudos, chapters) {
  192. var test_prop = kudos / hits;
  193. var z_value = (test_prop - null_hyp) / Math.sqrt( (null_hyp * (1 - null_hyp)) / hits );
  194. return normalcdf(0, -1 * z_value, 1);
  195. }
  196.  
  197. function normalcdf(mean, upper_bound, standard_dev) {
  198. var z = (standard_dev-mean)/Math.sqrt(2*upper_bound*upper_bound);
  199. var t = 1/(1+0.3275911*Math.abs(z));
  200. var a1 = 0.254829592;
  201. var a2 = -0.284496736;
  202. var a3 = 1.421413741;
  203. var a4 = -1.453152027;
  204. var a5 = 1.061405429;
  205. var erf = 1-(((((a5*t + a4)*t) + a3)*t + a2)*t + a1)*t*Math.exp(-z*z);
  206. var sign = 1;
  207. if(z < 0)
  208. {
  209. sign = -1;
  210. }
  211. return (1/2)*(1+sign*erf);
  212. }
  213.  
  214.  
  215. function sortByRatio(ascending) {
  216.  
  217. if (sortable) {
  218.  
  219. var sortable_lists = $('dl.stats').closest('li').parent();
  220.  
  221. sortable_lists.each(function() {
  222.  
  223. var list_elements = $(this).children('li');
  224.  
  225. // sort by kudos/hits ratio in descending order
  226. list_elements.sort(function(a, b) {
  227. return parseFloat(b.getAttribute('kudospercent')) - parseFloat(a.getAttribute('kudospercent'));
  228. });
  229.  
  230. if (ascending) {
  231. $(list_elements.get().reverse()).detach().appendTo($(this));
  232. }
  233. else {
  234. list_elements.detach().appendTo($(this));
  235. }
  236. });
  237. }
  238. }
  239.  
  240.  
  241. // attach the menu
  242. function addRatioMenu() {
  243.  
  244. // get the header menu
  245. var header_menu = $('ul.primary.navigation.actions');
  246.  
  247. // create and insert menu button
  248. var ratio_menu = $('<li class="dropdown"></li>').html('<a>Kudos/hits</a>');
  249. header_menu.find('li.search').before(ratio_menu);
  250.  
  251. // create and append dropdown menu
  252. var drop_menu = $('<ul class="menu dropdown-menu"></li>');
  253. ratio_menu.append(drop_menu);
  254.  
  255. // create button - count
  256. var button_count = $('<li></li>').html('<a>Count on this page</a>');
  257. button_count.click(function() {countRatio();});
  258.  
  259. // create button - sort
  260. var button_sort = $('<li></li>').html('<a>Sort on this page</a>');
  261. button_sort.click(function() {sortByRatio();});
  262.  
  263. // create button - settings
  264. var button_settings = $('<li></li>').html('<a style="padding: 0.5em 0.5em 0.25em; text-align: center; font-weight: bold;">&mdash; Settings (click to change): &mdash;</a>');
  265.  
  266. // create button - always count
  267. var button_count_yes = $('<li class="count-yes"></li>').html('<a>Count automatically: YES</a>');
  268. drop_menu.on('click', 'li.count-yes', function() {
  269. localStorage.setItem('alwayscountlocal', 'no');
  270. button_count_yes.replaceWith(button_count_no);
  271. });
  272.  
  273. // create button - not always count
  274. var button_count_no = $('<li class="count-no"></li>').html('<a>Count automatically: NO</a>');
  275. drop_menu.on('click', 'li.count-no', function() {
  276. localStorage.setItem('alwayscountlocal', 'yes');
  277. button_count_no.replaceWith(button_count_yes);
  278. });
  279.  
  280. // create button - always sort
  281. var button_sort_yes = $('<li class="sort-yes"></li>').html('<a>Sort automatically: YES</a>');
  282. drop_menu.on('click', 'li.sort-yes', function() {
  283. localStorage.setItem('alwayssortlocal', 'no');
  284. button_sort_yes.replaceWith(button_sort_no);
  285. });
  286.  
  287. // create button - not always sort
  288. var button_sort_no = $('<li class="sort-no"></li>').html('<a>Sort automatically: NO</a>');
  289. drop_menu.on('click', 'li.sort-no', function() {
  290. localStorage.setItem('alwayssortlocal', 'yes');
  291. button_sort_no.replaceWith(button_sort_yes);
  292. });
  293.  
  294. // create button - hide hitcount
  295. var button_hide_yes = $('<li class="hide-yes"></li>').html('<a>Hide hitcount: YES</a>');
  296. drop_menu.on('click', 'li.hide-yes', function() {
  297. localStorage.setItem('hidehitcountlocal', 'no');
  298. $('.stats .hits').css('display', '');
  299. button_hide_yes.replaceWith(button_hide_no);
  300. });
  301.  
  302. // create button - don't hide hitcount
  303. var button_hide_no = $('<li class="hide-no"></li>').html('<a>Hide hitcount: NO</a>');
  304. drop_menu.on('click', 'li.hide-no', function() {
  305. localStorage.setItem('hidehitcountlocal', 'yes');
  306. $('.stats .hits').css('display', 'none');
  307. button_hide_no.replaceWith(button_hide_yes);
  308. });
  309.  
  310. // append buttons to the dropdown menu
  311. drop_menu.append(button_count);
  312.  
  313. if (sortable) {
  314. drop_menu.append(button_sort);
  315. }
  316.  
  317. if (typeof(Storage) !== 'undefined') {
  318.  
  319. drop_menu.append(button_settings);
  320.  
  321. if (always_count) {
  322. drop_menu.append(button_count_yes);
  323. }
  324. else {
  325. drop_menu.append(button_count_no);
  326. }
  327.  
  328. if (always_sort) {
  329. drop_menu.append(button_sort_yes);
  330. }
  331. else {
  332. drop_menu.append(button_sort_no);
  333. }
  334.  
  335. if (hide_hitcount) {
  336. drop_menu.append(button_hide_yes);
  337. }
  338. else {
  339. drop_menu.append(button_hide_no);
  340. }
  341. }
  342.  
  343. // add button for statistics
  344. if ($('#main').is('.stats-index')) {
  345.  
  346. var button_sort_stats = $('<li></li>').html('<a>↓&nbsp;Kudos/hits</a>');
  347. button_sort_stats.click(function() {
  348. sortByRatio();
  349. button_sort_stats.after(button_sort_stats_asc).detach();
  350. });
  351.  
  352. var button_sort_stats_asc = $('<li></li>').html('<a>↑&nbsp;Kudos/hits</a>');
  353. button_sort_stats_asc.click(function() {
  354. sortByRatio(true);
  355. button_sort_stats_asc.after(button_sort_stats).detach();
  356. });
  357.  
  358. $('ul.sorting.actions li:nth-child(3)').after('\n', button_sort_stats);
  359. }
  360. }
  361.  
  362. })(jQuery);