AO3 Regraph

Draw the statistics bar chart on your AO3 stats page without using google and with more user options.

  1. // ==UserScript==
  2. // @name AO3 Regraph
  3. // @description Draw the statistics bar chart on your AO3 stats page without using google and with more user options.
  4. // @namespace AO3 Userscripts
  5. // @match http*://archiveofourown.org/users/*/stats*
  6. // @version 1.4.1
  7. // @history v1.4.1 Fixed a typo that broke fancy mode 1. 2024-08-09.
  8. // @history v1.4 Support added for gradient or rainbow bar charts, plus a hook for inserting user code. 2024-08-08.
  9. // @history v1.3 Improved y-axis numbering at low numbers; locale support for number format display added. 2024-08-07.
  10. // @history v1.2 Word wrap support added for titles. Only wraps on spaces; will not attempt to wrap at hyphens, etc. Will DEFINITELY not hyphenate for you. Adds a fudge factor to cope with word lengths, so may occasionally produce an extra line. 2024-05-22
  11. // @history v1.1 Slightly kudgy fix so date sorting doesn't kill the graph. Date sorting in flat view displays date-sorted data; in fandom view it reverts to normal. 2024-05-20
  12. // @history v1.0 Full bar chart completed, y-axis titled, all spaces auto-calculated, variable font size and graph size introduced. READY 2024-05-06.
  13. // @history v0.3 Basic bar chart created (minus y-axis) WITH fandom sorting, 2024-04-20.
  14. // @history v0.2 Data extraction completed 2024-04-20.
  15. // @history v0.1 Created 2024-04-16, initial testing only.
  16. // @author Ardil the Traveller
  17. // @license MIT
  18. // @grant none
  19. // ==/UserScript==
  20.  
  21. /* This is how Ardil goes to hell.
  22. * If you are reading this you are probably less of a noob at scripting than me. I apologise for the suffering you are about to endure.
  23. * I apparently will only learn any web-centric language by throwing myself wildly at it while screaming. PHP didn't deserve this abuse either. */
  24.  
  25. // The only part of the webpage this script edits is the <div id="stat_chart" class="statistics chart">.
  26.  
  27. (function () {
  28. "use strict";
  29.  
  30. /* Settings */
  31. // BE SURE TO COPY YOUR SETTINGS BEFORE UPDATING THE SCRIPT! Updates will overwrite user settings with the defaults.
  32.  
  33. const barFillColour = "#993333"; // Insert the hex value of your favourite colour here. At last your stats chart can be eye-melting yellow if you want.
  34. const barTextColour = "#BBAA88"; // Set the text that displays at the bottom of your bar to a colour you can read against your other colour.
  35. let numberOfBars = 8; // Don't set this above 10 unless you have a mega-wide screen...
  36.  
  37. // Vertical height settings for the graph. The width is read from the webpage so that it still fits in whatever the original horizontal space on your screen was.
  38. const chart_ratio = 5; /* Height will be width divided by this number. The google value on my screen is about 10. I like 5 or so, personally.
  39. * A value of 1 will make the chart square. */
  40.  
  41. // Font and number settings for the chart:
  42. const yaxis_title_size = 16; // font size in px for the Y-axis title.
  43. const yaxis_title_font = "sans-serif"; // change the text inside the quotes if you want another font for the Y-axis title.
  44. const data_labels_size = 12; // font size in px for the various data labels.
  45. const data_labels_font = "sans-serif"; // change the text inside the quotes if you want another font for the data labels.
  46. const number_format = "en-GB"; // change this to your locale to use the thousands separators or other number formatting rules of your language!
  47.  
  48. // Stat to display when sorting by date. I haven't found where AO3 keeps the date field, so fandom view date sorting doesn't work.
  49. const dateDisplays = "hits"; /* Your options are "hits", "kudos.count" (kudos), "comment_thread_count" (comments), "bookmarks.count" (bookmarks),
  50. * "subscriptions.count" (subscriptions), or "word_count" (words).
  51. * Inputting anything else other than one of EXACTLY THESE will break the graph. */
  52.  
  53. // SUPER EXPERIMENTAL FANCY BARS
  54. // DO NOT TOUCH THIS UNLESS YOU ARE HAPPY WITH FAIRLY COMPLEX RULES. Otherwise leave enableFancyMode = 0 (off).
  55. const enableFancyMode = 0; /* Set this to 1 or 2, define your fanciness below, touch wood. When it inevitably breaks, cry, then message Ardil
  56. * with what went wrong. I'll try to work out the bug and fix it for you. */
  57. // Fancy Mode = 1: For all bars to share a single gradient from hex colour grad_start to hex colour grad_end:
  58. const grad_start = "#FFFFFF"; // Choose your initial hex colour.
  59. const grad_end = "#000000"; // Choose your final hex colour.
  60. const grad_type = "graphMax"; /* Options are only "graphMax" or "barMax". "graphMax" means that only the highest bar gets the full gradient;
  61. * "barMax" means that all bars get the full gradient. Defaults to "graphMax".*/
  62. // Fancy Mode = 2: Bars gradiented individually. Choose a hex colour for the start and end of each bar. If you use fewer colours than you have bars,
  63. // it will repeat them until it has enough.
  64. const grad_starts = ["#FFFFFF", "#FF0000", "#FF00FF"]; // I recommend using as many starts as you have set numberOfBars, but it will cope if you don't.
  65. const grad_ends = ["#000000", "#00FFFF", "#00FF00"]; // I recommend using as many ends as you have starts, but it will cope (potentially humourously) if you don't.
  66. // Fancy Mode = 3: If you would rather fully define your own fanciness, ctrl-F "User Fanciness" in this document and edit the instructions
  67. // displayed in the "case 3:" block, between the "case 3:" and the "break;".
  68.  
  69. /* End Settings */
  70.  
  71. /*---------------------------------------------------------------------------------------------------------------------------------------------*/
  72.  
  73. /* Function definitions */
  74.  
  75. function addFancyChart(statistics, sorting, bar_count) {
  76. // magic for inserting chart goes here
  77.  
  78. let plottingdata = []; // I use this to avoid one million switch statements to pick the right data points.
  79. let plottinglabels = [];
  80. let yaxis_title = "";
  81.  
  82. // Choose what to sort on.
  83. // If we are in flat view, everything is already fine. If we are in fandom view, we will need to sort everything.
  84.  
  85. // Kludge fix so we get _a_ graph for date sorting, even if it's not the one we'd like. I will fix this if I ever find a way to get the dates out of AO3.
  86. if (sorting.sortCol == "date") {
  87. sorting.sortCol = dateDisplays;
  88. }
  89.  
  90. if (!sorting.flatView) {
  91. // More complicated magic for fandom mode...
  92.  
  93. // This sorts ascending.
  94. switch (sorting.sortCol) {
  95. case "hits" :
  96. // Sort by hits.
  97. statistics.sort(function(a, b){return a.hits - b.hits});
  98. yaxis_title = "Hits";
  99. break;
  100. case "kudos.count" :
  101. // Sort by kudos.
  102. statistics.sort(function(a, b){return a.kudos - b.kudos});
  103. yaxis_title = "Kudos";
  104. break;
  105. case "comment_thread_count" :
  106. // Sort by comments.
  107. statistics.sort(function(a, b){return a.comments - b.comments});
  108. yaxis_title = "Comments";
  109. break;
  110. case "bookmarks.count" :
  111. // Sort by bookmarks.
  112. statistics.sort(function(a, b){return a.bookmarks - b.bookmarks});
  113. yaxis_title = "Bookmarks";
  114. break;
  115. case "subscriptions.count" :
  116. // Sort by subscriptions.
  117. statistics.sort(function(a, b){return a.subscriptions - b.subscriptions});
  118. yaxis_title = "Subscriptions";
  119. break;
  120. case "word_count" :
  121. // Sort by word count.
  122. statistics.sort(function(a, b){return a.words - b.words});
  123. yaxis_title = "Words";
  124. break;
  125. default :
  126. // What the fuck we should never be here.
  127. console.log("Sorting type not recognised! Graph aborted.");
  128. return "OW";
  129. } // End switch.
  130. if (sorting.desc) {
  131. // Descending sort. Reverse the order of everything.
  132. statistics.reverse();
  133. }
  134. }
  135.  
  136. switch (sorting.sortCol) {
  137. case "hits" :
  138. // Sort by hits.
  139. for (let i = 0; i < statistics.length; i++) {
  140. plottingdata.push(statistics[i].hits);
  141. plottinglabels.push(statistics[i].title);
  142. }
  143. yaxis_title = "Hits";
  144. break;
  145. case "kudos.count" :
  146. // Sort by kudos.
  147. for (let i = 0; i < statistics.length; i++) {
  148. plottingdata.push(statistics[i].kudos);
  149. plottinglabels.push(statistics[i].title);
  150. }
  151. yaxis_title = "Kudos";
  152. break;
  153. case "comment_thread_count" :
  154. // Sort by comments.
  155. for (let i = 0; i < statistics.length; i++) {
  156. plottingdata.push(statistics[i].comments);
  157. plottinglabels.push(statistics[i].title);
  158. }
  159. yaxis_title = "Comments";
  160. break;
  161. case "bookmarks.count" :
  162. // Sort by bookmarks.
  163. for (let i = 0; i < statistics.length; i++) {
  164. plottingdata.push(statistics[i].bookmarks);
  165. plottinglabels.push(statistics[i].title);
  166. }
  167. yaxis_title = "Bookmarks";
  168. break;
  169. case "subscriptions.count" :
  170. // Sort by subscriptions.
  171. for (let i = 0; i < statistics.length; i++) {
  172. plottingdata.push(statistics[i].subscriptions);
  173. plottinglabels.push(statistics[i].title);
  174. }
  175. yaxis_title = "Subscriptions";
  176. break;
  177. case "word_count" :
  178. // Sort by subscriptions.
  179. for (let i = 0; i < statistics.length; i++) {
  180. plottingdata.push(statistics[i].words);
  181. plottinglabels.push(statistics[i].title);
  182. }
  183. yaxis_title = "Words";
  184. break;
  185. default :
  186. // What the fuck we should never be here.
  187. console.log("Sorting type not recognised! Graph aborted.");
  188. return "OW";
  189. } // End switch.
  190.  
  191. // Make a canvas.
  192. let boundaries = document.getElementById("stat_chart").getBoundingClientRect();
  193. //console.log("Boundaries of image area: " + JSON.stringify(boundaries));
  194. let inside_width = document.getElementById("stat_chart").clientWidth;
  195. //console.log("Client width reports: " + inside_width);
  196.  
  197. let chart_height = inside_width / chart_ratio; // Make the graph have a constant size ratio.
  198. chart_height = chart_height.toFixed(0); // And make that be an actual integer number of pixels please.
  199. document.getElementById("stat_chart").style.height = chart_height + "px";
  200.  
  201. // Build canvas instructions because I can't put a JS variable in my HTML string can I, that would be silly.
  202. document.getElementById("stat_chart").innerHTML = '<canvas id="inserted_stats" width="' + inside_width + '" height="' + chart_height + '" style="border:1px solid #AAAAAA;">';
  203.  
  204. // Set up canvas and brush for drawing.
  205. const stats_canvas = document.getElementById("inserted_stats");
  206. const brush = stats_canvas.getContext("2d");
  207. // Build the font strings.
  208. const yaxis_title_info = yaxis_title_size + "px " + yaxis_title_font;
  209. const data_labels_info = data_labels_size + "px " + data_labels_font;
  210. brush.font = data_labels_info; // Start in data label font as we only ever need the other one once.
  211.  
  212. // Sort out the bar count so that we don't try to display more bars than there are.
  213. bar_count = Math.min(bar_count, statistics.length);
  214. // Dump the data for bars we won't display so that the ymax doesn't look stupid.
  215. plottingdata = plottingdata.slice(0,bar_count);
  216. plottinglabels = plottinglabels.slice(0, bar_count);
  217.  
  218. // Determine the y-axis maximum.
  219. let ymax = 0;
  220. ymax = Math.max(...plottingdata);
  221. //console.log(plottingdata, Math.max(...plottingdata));
  222.  
  223. // Horizontal chart spacings:
  224. // There should be space for the left axis. Subtract that off first. We can work out the space needed using the width of the longest bit of text, i.e. max y
  225. const left_axis_space = Math.ceil(brush.measureText(Intl.NumberFormat(number_format).format(ymax)).width) + yaxis_title_size + 10; // Allows a few px either side of the labels.
  226. const plot_width = inside_width - left_axis_space;
  227. // There are bar_count columns and bar_count+1 padding spaces. Padding space should be, let's say, 1/4 the width of a column.
  228. // So there must be 4*bar_count + (bar_count + 1) units to divide the available space into.
  229. const pad_space = plot_width / (5 * bar_count + 1);
  230. const col_width = pad_space * 4;
  231. const title_width = col_width + pad_space;
  232.  
  233. // Vertical chart spacings:
  234. const top_space = 10; // Allow a small amount of space so the chart doesn't touch the element above.
  235. // Find out if we will need to word wrap and by how much, then set the bottom_axis_space to suit.
  236. let max_title_lines = 0;
  237. for (let i = 0; i < plottinglabels.length; i++) {
  238. if (Math.ceil(brush.measureText(plottinglabels[i]).width / title_width + 0.25) > max_title_lines) {
  239. // 0.25 is a fudge factor for long words messing up clipping. Just gives us a little extra breathing room.
  240. max_title_lines = Math.ceil(brush.measureText(plottinglabels[i]).width / title_width);
  241. }
  242. }
  243. // console.log("Maximum number of title lines is " + max_title_lines);
  244. const bottom_axis_space = max_title_lines*data_labels_size + 4; // Allows a couple of px either side of the labels.
  245. const plot_height = chart_height - bottom_axis_space - top_space;
  246. //console.log("Plot height is now: " + plot_height + " px, and full chart height is " + chart_height + " px.");
  247.  
  248. // Determine the y-axis scale, now with new and improved number handling and modulo 4 support for numbers < 100.
  249. ymax = ymax.toExponential(1); // Produces, e.g., 1.2E+2 from 196. Will always be 6 characters long.
  250. let the_exponent = ymax.slice(3); // Record the last three characters, i.e. the exponent (E+2, say).
  251. ymax = parseFloat(ymax.slice(0,3)) + 0.1; // Make sure the maximum is just a little higher than the highest value.
  252. ymax = ymax.toFixed(1) + the_exponent; // Put the exponent back on in string form.
  253. ymax = parseFloat(ymax); // Turn this back into a number.
  254. if (ymax < 1000) { // If the maximum is not guaranteed to be a multiple of 100, do some modulo 4 arithmetic to remove any chance of awkward decimals.
  255. ymax = Math.ceil(ymax/4) * 4;
  256. }
  257. // test international number formatting
  258. // console.log("ymax when formatted is: " + Intl.NumberFormat("en-GB").format(ymax));
  259.  
  260. let ystep = parseInt(ymax / 4);
  261.  
  262. // Draw y-axis values and major lines.
  263. const ticklen = 5;
  264. const tickgap = parseInt(plot_height / 4);
  265. brush.strokeStyle = "#888899"
  266. brush.lineWidth = 1;
  267. brush.beginPath();
  268. brush.moveTo(left_axis_space - ticklen, top_space);
  269. brush.lineTo(inside_width, top_space);
  270. brush.stroke();
  271. brush.beginPath();
  272. brush.moveTo(left_axis_space - ticklen, top_space + tickgap);
  273. brush.lineTo(inside_width, top_space + tickgap);
  274. brush.stroke();
  275. brush.beginPath();
  276. brush.moveTo(left_axis_space - ticklen, top_space + 2 * tickgap);
  277. brush.lineTo(inside_width, top_space + 2 * tickgap);
  278. brush.stroke();
  279. brush.beginPath();
  280. brush.moveTo(left_axis_space - ticklen, top_space + 3 * tickgap);
  281. brush.lineTo(inside_width, top_space + 3 * tickgap);
  282. brush.stroke();
  283.  
  284. // Draw axes. Done second to avoid axes being cut by lines.
  285. brush.strokeStyle = "black";
  286. brush.beginPath();
  287. brush.moveTo(left_axis_space, 0);
  288. brush.lineTo(left_axis_space, plot_height + top_space);
  289. brush.lineTo(inside_width, plot_height + top_space);
  290. brush.stroke();
  291. // Y-axis labels.
  292. brush.textAlign = "end"
  293. brush.textBaseline = "middle"
  294. brush.fillText(Intl.NumberFormat(number_format).format(ymax), left_axis_space - ticklen, top_space);
  295. // console.log("measure text " + brush.measureText(ymax).width + "left space" + left_axis_space);
  296. brush.fillText(Intl.NumberFormat(number_format).format(ymax - ystep), left_axis_space - ticklen, top_space + tickgap);
  297. brush.fillText(Intl.NumberFormat(number_format).format(ymax - 2*ystep), left_axis_space - ticklen, top_space + 2*tickgap);
  298. brush.fillText(Intl.NumberFormat(number_format).format(ymax - 3*ystep), left_axis_space - ticklen, top_space + 3*tickgap);
  299. brush.fillText(0, left_axis_space - ticklen, top_space + plot_height);
  300. // Y-axis text involves rotating, which is complicated as it rotates as if it were THE WHOLE PICTURE. Is there a saner way to do this???
  301. brush.rotate(-90 * Math.PI / 180); //(0,0) now in top right corner, so the y-coord is our x value and the x-coord is our -y.
  302. brush.textAlign = "center";
  303. brush.textBaseline = "top";
  304. brush.font = yaxis_title_info;
  305. brush.fillText(yaxis_title, - (top_space + 2*tickgap), 2);
  306. brush.font = data_labels_info;
  307. brush.rotate(90 * Math.PI / 180); // Put it back to draw the rest!
  308.  
  309. // Draw bars and x-axis labels.
  310. brush.strokeStyle = "black";
  311. brush.lineWidth = 1;
  312. brush.textAlign = "center";
  313.  
  314. // Some fancy mode setup stuff, in case you haven't chosen the same number of gradients as you have bars.
  315. // This just replicates your gradient choices until there are enugh in the array. If you have mismatched start and end counts, it might be funny.
  316. let gradient_starts = [];
  317. let gradient_ends = [];
  318. if ( (enableFancyMode == 2) && ((grad_starts.length < numberOfBars) || (grad_ends.length < numberOfBars)) ) {
  319. let starts_repeat = Math.ceil(numberOfBars / grad_starts.length);
  320. let ends_repeat = Math.ceil(numberOfBars / grad_ends.length);
  321. for (let i = 0; i < Math.max(starts_repeat, ends_repeat); i++) {
  322. gradient_starts.push(grad_starts);
  323. gradient_ends.push(grad_ends);
  324. }
  325. }
  326. gradient_starts = gradient_starts.flat();
  327. gradient_ends = gradient_ends.flat();
  328.  
  329. // Enter the drawing loop. Each iteration draws one bar.
  330. for (let i = 0; i < bar_count; i++) {
  331. let bar_height = plot_height * (plottingdata[i] / ymax);
  332. let bar_top = top_space + plot_height - bar_height;
  333. let bar_left = left_axis_space + pad_space + (i * (col_width + pad_space));
  334. switch (enableFancyMode) {
  335. case 1:
  336. let gradient1 = brush.createLinearGradient(0, top_space + plot_height, 0, top_space);
  337. // x0, y0, x1, y1 - start and end coordinates for gradient.
  338. // If it doesn't change with x, keep both x-coordinates 0. If it doesn't change with y, keep both y-coordinates 0.
  339. if (grad_type == "barMax") { // Change it to vary per bar.
  340. gradient1 = brush.createLinearGradient(0, top_space + plot_height, 0, bar_top);
  341. }
  342. gradient1.addColorStop(0, grad_start);
  343. gradient1.addColorStop(1, grad_end);
  344. brush.fillStyle = gradient1;
  345. brush.beginPath();
  346. brush.fillRect(bar_left, bar_top, col_width, bar_height);
  347. brush.strokeRect(bar_left, bar_top, col_width, bar_height);
  348. break;
  349. case 2:
  350. let gradient2 = brush.createLinearGradient(0, top_space + plot_height, 0, bar_top);
  351. // x0, y0, x1, y1 - start and end coordinates for gradient.
  352. // If it doesn't change with x, keep both x-coordinates 0. If it doesn't change with y, keep both y-coordinates 0.
  353. gradient2.addColorStop(0, gradient_starts[i]);
  354. gradient2.addColorStop(1, gradient_ends[i]);
  355. brush.fillStyle = gradient2;
  356. brush.beginPath();
  357. brush.fillRect(bar_left, bar_top, col_width, bar_height);
  358. brush.strokeRect(bar_left, bar_top, col_width, bar_height);
  359. break;
  360. case 3:
  361. // User Fanciness: Insert your own code here.
  362. /* The position of the x-axis is (top_space + plot_height), and the position of the top of the bar you are working on is bar_top.
  363. * The position of the left side of the bar you are working on is bar_left, and the right side is (bar_left + col_width).
  364. * The bar is col_width pixels wide. The command you are using to draw with is "brush".
  365. * Check out the other case statements for examples of how it works. */
  366. console.log("User has not defined fanciness! Graph cannot be drawn in this mode."); // Remove this warning when you have written code.
  367. break;
  368. default:
  369. // Standard single-colour bars.
  370. brush.fillStyle = barFillColour;
  371. brush.beginPath();
  372. brush.fillRect(bar_left, bar_top, col_width, bar_height);
  373. brush.strokeRect(bar_left, bar_top, col_width, bar_height);
  374. break;
  375. }
  376.  
  377. // Magic number "+/- 2" is an offset to get the text away from the X-axis.
  378. brush.fillStyle = "black";
  379. brush.textBaseline = "top";
  380. // Check if we need to wrap a title.
  381. if (brush.measureText(plottinglabels[i]).width > title_width) {
  382. // Insert title wrap magic here.
  383. // console.log("Entering title wrap mode for title " + plottinglabels[i]);
  384. let split_title = [""];
  385. let title_line = 0;
  386. let title_words = plottinglabels[i].split(" "); // Splits on spaces, so there will be no spaces after the fact and we'll need to put them back.
  387. for (let j = 0; j < title_words.length; j++) {
  388. if ((brush.measureText(split_title[title_line] + " " + title_words[j]).width > title_width) && (split_title[title_line].length > 0)) {
  389. // Change onto a new line, UNLESS there's nothing in this line and it's just that the whole word is too long.
  390. // If the whole word is too long, print the word anyway, maybe you should display fewer columns. (Proper hyphenation is too hard.
  391. // Splitting on hyphens is also just a whole 'nother problem I don't want to deal with right now...)
  392. split_title.push(title_words[j]);
  393. title_line = title_line + 1;
  394. } else {
  395. // Continue on this line.
  396. if (j == 0) {
  397. split_title[title_line] = split_title[title_line] + title_words[j];
  398. } else {
  399. split_title[title_line] = split_title[title_line] + " " + title_words[j];
  400. }
  401. }
  402. } // Title suitably broken up. Hopefully. Now print the word-wrapped title.
  403. for (let j = 0; j < split_title.length; j++) {
  404. brush.fillText(split_title[j], bar_left + col_width/2, top_space + plot_height + 2 + j*data_labels_size);
  405. }
  406. } else {
  407. // We didn't need to word wrap. Just print the title.
  408. brush.fillText(plottinglabels[i], bar_left + col_width/2, top_space + plot_height + 2);
  409. }
  410.  
  411. brush.fillStyle = barTextColour;
  412. brush.textBaseline = "bottom";
  413. brush.fillText(Intl.NumberFormat(number_format).format(plottingdata[i]), bar_left + col_width/2, top_space + plot_height - 2);
  414. } // Bar plotting complete!
  415.  
  416. // this is shit magic for inserting text instead as a test
  417. // document.getElementById("stat_chart").innerHTML = "<p>I WILL BE PRETTY ONE DAY</p>";
  418. } //End of addFancyChart function.
  419.  
  420. function getViewType(statspage_href) {
  421. // Magic for finding out what kind of view we are displaying.
  422. // Needs to return Fandom/Flat, Sort Column, and Asc/Desc.
  423. // All of these have defaults: Fandom, Hits, and Desc.
  424. let view_description = {flatView : false, sortCol : "hits", desc : true};
  425.  
  426. // Analyse the input web address for flat view: if we're using flat view, the string "flat_view=true" will appear somewhere.
  427. if (statspage_href.search("flat_view=true") != -1) {
  428. view_description.flatView = true;
  429. }
  430.  
  431. // Analyse the input web address for sort ascending / descending:
  432. if (statspage_href.search("sort_direction=ASC") != -1) {
  433. view_description.desc = false;
  434. }
  435.  
  436. // Analyse the input web address for the sort column. This is the tricky one.
  437. let iscolsorted = statspage_href.search("sort_column=");
  438. if (iscolsorted != -1) {
  439. // A sort column has been chosen. Dice the string to find out which one.
  440. // First, dispose of everything up to and including "sort_column=".
  441. let sort_type = statspage_href.substring(iscolsorted + 12);
  442. let nextampersand = sort_type.search("&");
  443. sort_type = sort_type.substring(0, nextampersand);
  444. if (sort_type.length > 0) {
  445. view_description.sortCol = sort_type;
  446. }
  447. }
  448.  
  449. return view_description;
  450. } // End of getViewType function.
  451.  
  452. function getStats(sorting_type) {
  453. // Magic for getting the stats out of the stats page.
  454. // Might have to rewrite this if AO3 ever change the stats page layout.
  455.  
  456. let all_stories = document.getElementsByClassName("fandom listbox group");
  457. //console.log("All stories object is: " + all_stories + ", a " + typeof(all_stories) + " with length: " + all_stories.length);
  458. //console.log("All stories element 0 is: " + all_stories[0] + ", a " + typeof(all_stories[0]));
  459. //console.log("Contents of all stories [0]: " + all_stories[0].innerHTML);
  460.  
  461. const stories = []; // Create a stories array to store our stories. We'll return this at the end.
  462.  
  463. if (sorting_type.flatView) {
  464. // In this case, all_stories will have length 1, as there is only one listbox group.
  465.  
  466. // Get all the list elements. There should be one of these per story, each one containing one set of story tat.
  467. let story_list = all_stories[0].getElementsByTagName("li");
  468. let story_count = story_list.length;
  469. // I can use this to work out where the subscriptions are, and anything else that might or might not exist.
  470. // Involves too much string munging for my liking for me to do it that way for everything.
  471.  
  472. // To get the fic names, find all of the <a href>s - these should only contain the names (and fic IDs if I get what's in the href).
  473. let extract_titles = all_stories[0].getElementsByTagName("a");
  474. //console.log("Extracted title 0: " + extract_titles[0].innerText);
  475.  
  476. // Get the fandoms, conveniently in class "fandom".
  477. let extract_fandoms = all_stories[0].getElementsByClassName("fandom");
  478. //console.log("Extracted fandom 0: " + extract_fandoms[0].innerText);
  479.  
  480. // Get the wordcounts, class "words".
  481. let extract_wordcounts = all_stories[0].getElementsByClassName("words");
  482. //console.log("Extracted wordcount 0: " + extract_wordcounts[0].innerText);
  483.  
  484. // Get the hits. Hits text is in dt.hits, number is in dd.hits.
  485. let extract_hits = all_stories[0].querySelectorAll("dd.hits");
  486. //console.log("Extracted hits 0: " + extract_hits[0].innerText);
  487.  
  488. // Get the kudos. Kudos text is in dt.kudos, number is in dd.kudos.
  489. let extract_kudos = all_stories[0].querySelectorAll("dd.kudos");
  490. //console.log("Extracted kudos 0: " + extract_kudos[0].innerText);
  491.  
  492. // Get the bookmarks. Bookmarks text is in dt.bookmarks, number is in dd.bookmarks.
  493. let extract_bookmarks = all_stories[0].querySelectorAll("dd.bookmarks");
  494. //console.log("Extracted bookmarks 0: " + extract_bookmarks[0].innerText);
  495.  
  496. // Get the comments. Comments text is in dt.comments, number is in dd.comments.
  497. let extract_comments = all_stories[0].querySelectorAll("dd.comments");
  498. //console.log("Extracted comments 0: " + extract_comments[0].innerText);
  499.  
  500. // Process and clean the various numerical strings to get numbers. Pop each piece of data into an object. Array of objects. Thing.
  501. for (let i = 0; i < story_count; i++) {
  502. // Cut wordcount down to numbers only. Format is "(X words)" where X is the number, so we remove [0] and everything after [-7].
  503. let wordcount = extract_wordcounts[i].innerText;
  504. wordcount = wordcount.slice(1, wordcount.length-7);
  505. // Sort out subscriptions where they exist. They don't always exist, so be careful.
  506. let subs = 0;
  507. let subs_exist = story_list[i].innerText.search("Subscriptions: ");
  508. if (subs_exist != -1) {
  509. subs = story_list[i].innerText.slice(subs_exist+15);
  510. subs = subs.slice(0, subs.search(/\s/));
  511. subs = parseInt(subs.replace(/,/g, ""));
  512. }
  513.  
  514. // Create a new story object and add it to our stories array.
  515. let the_story = {
  516. title : extract_titles[i].innerText,
  517. fandoms : extract_fandoms[i].innerText.slice(1,extract_fandoms[i].innerText.length-1),
  518. words : parseInt(wordcount.replace(/,/g, "")),
  519. hits : parseInt(extract_hits[i].innerText.replace(/,/g, "")),
  520. kudos : parseInt(extract_kudos[i].innerText.replace(/,/g, "")),
  521. bookmarks : parseInt(extract_bookmarks[i].innerText.replace(/,/g, "")),
  522. comments : parseInt(extract_comments[i].innerText.replace(/,/g, "")),
  523. subscriptions : subs
  524. };
  525. stories.push(the_story);
  526. }
  527. // And we're done with flat view!
  528.  
  529. } else {
  530. //console.log("Fandom View Detected.");
  531.  
  532. let fandom_count = all_stories.length;
  533. //console.log("Assessing stories for " + fandom_count + " fandoms.");
  534.  
  535. let titles_list = [];
  536. for (let i = 0; i < fandom_count; i++) {
  537. let fandom_name = all_stories[i].getElementsByClassName("heading");
  538. let fandom = fandom_name[0].innerText;
  539. let story_list = all_stories[i].getElementsByTagName("li");
  540. let story_count = story_list.length;
  541.  
  542. // To get the fic names, find all of the <a href>s - these should only contain the names (and fic IDs if I get what's in the href).
  543. let extract_titles = all_stories[i].getElementsByTagName("a");
  544. //console.log("Extracted title 0: " + extract_titles[0].innerText);
  545.  
  546. // Get the wordcounts, class "words".
  547. let extract_wordcounts = all_stories[i].getElementsByClassName("words");
  548. //console.log("Extracted wordcount 0: " + extract_wordcounts[0].innerText);
  549.  
  550. // Get the hits. Hits text is in dt.hits, number is in dd.hits.
  551. let extract_hits = all_stories[i].querySelectorAll("dd.hits");
  552. //console.log("Extracted hits 0: " + extract_hits[0].innerText);
  553.  
  554. // Get the kudos. Kudos text is in dt.kudos, number is in dd.kudos.
  555. let extract_kudos = all_stories[i].querySelectorAll("dd.kudos");
  556. //console.log("Extracted kudos 0: " + extract_kudos[0].innerText);
  557.  
  558. // Get the bookmarks. Bookmarks text is in dt.bookmarks, number is in dd.bookmarks.
  559. let extract_bookmarks = all_stories[i].querySelectorAll("dd.bookmarks");
  560. //console.log("Extracted bookmarks 0: " + extract_bookmarks[0].innerText);
  561.  
  562. // Get the comments. Comments text is in dt.comments, number is in dd.comments.
  563. let extract_comments = all_stories[i].querySelectorAll("dd.comments");
  564. //console.log("Extracted comments 0: " + extract_comments[0].innerText);
  565. for (let j = 0; j < story_count; j++) {
  566. let title_location = titles_list.indexOf(extract_titles[j].innerText);
  567. if ( title_location == -1 ) {
  568. // No story by this title is yet recorded. Fill in the story object.
  569. // Cut wordcount down to numbers only. Format is "(X words)" where X is the number, so we remove [0] and everything after [-7].
  570. let wordcount = extract_wordcounts[j].innerText;
  571. wordcount = wordcount.slice(1, wordcount.length-7);
  572. // Find out if subscriptions are real.
  573. let subs = 0;
  574. let subs_exist = story_list[j].innerText.search("Subscriptions: ");
  575. if (subs_exist != -1) {
  576. subs = story_list[j].innerText.slice(subs_exist+15);
  577. subs = subs.slice(0, subs.search(/\s/));
  578. subs = parseInt(subs.replace(/,/g, ""));
  579. }
  580. let the_story = {
  581. title : extract_titles[j].innerText,
  582. fandoms : fandom,
  583. words : parseInt(wordcount.replace(/,/g, "")),
  584. hits : parseInt(extract_hits[j].innerText.replace(/,/g, "")),
  585. kudos : parseInt(extract_kudos[j].innerText.replace(/,/g, "")),
  586. bookmarks : parseInt(extract_bookmarks[j].innerText.replace(/,/g, "")),
  587. comments : parseInt(extract_comments[j].innerText.replace(/,/g, "")),
  588. subscriptions : subs
  589. }
  590. // Add the story to the story array.
  591. stories.push(the_story);
  592. // Add processed title to the titles list.
  593. titles_list.push(extract_titles[j].innerText);
  594. } else {
  595. // We already have a story by this title. Find it and add a fandom to it.
  596. // The story should be in the same index in stories as the title is in titles_list.
  597. stories[title_location].fandoms = stories[title_location].fandoms + ", " + fandom;
  598. }
  599. }
  600. }
  601. }
  602. // And we're done with fandom view!.
  603. return stories;
  604. }
  605. /* End function definitions */
  606.  
  607. /* Main Code
  608. * Super awesome final magic that makes it all work happens here */
  609.  
  610. // Figure out how we are displaying and sorting our data.
  611. let view_sort = getViewType(window.location.href);
  612.  
  613. //Test if it worked:
  614. //console.log("View instructions: " + JSON.stringify(view_sort));
  615.  
  616. let story_stats = getStats(view_sort);
  617. // Test that worked.
  618. //console.log("Stats exist for " + story_stats.length + " stories.");
  619. //console.log("First story: " + JSON.stringify(story_stats[0]));
  620. //console.log("Last story: " + JSON.stringify(story_stats[story_stats.length-1]));
  621.  
  622. addFancyChart(story_stats, view_sort, numberOfBars); // HOLY SHIT IT WORKED
  623. })();