AO3 Regraph

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

目前为 2024-05-20 提交的版本。查看 最新版本

  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.0
  7. // @history v1.0 Full bar chart completed, y-axis titled, all spaces auto-calculated, variable font size and graph size introduced. READY 06/05/2024.
  8. // @history v0.3 Basic bar chart created (minus y-axis) WITH fandom sorting, 20/04/2024.
  9. // @history v0.2 Data extraction completed 20/04/2024.
  10. // @history v0.1 Created 16/04/2024, initial testing only.
  11. // @author Ardil the Traveller
  12. // @license MIT
  13. // @grant none
  14. // ==/UserScript==
  15.  
  16. // This is how Ardil goes to hell.
  17. // 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.
  18. // I apparently will only learn any web-centric language by throwing myself wildly at it while screaming. PHP didn't deserve this abuse either.
  19.  
  20. // The only part of the webpage this script edits is the <div id="stat_chart" class="statistics chart">.
  21.  
  22. (function () {
  23. "use strict";
  24.  
  25. /* Settings */
  26.  
  27. 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.
  28. const barTextColour = "#BBAA88"; // Set the text that displays at the bottom of your bar to a colour you can read against your other colour.
  29. let numberOfBars = 8; // Don't set this above 10 unless you have a mega-wide screen...
  30.  
  31. // 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.
  32. 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.
  33. // A value of 1 will make the chart square.
  34.  
  35. // Font settings for the chart:
  36. const yaxis_title_size = 16; // font size in px for the Y-axis title.
  37. const yaxis_title_font = "sans-serif"; // change the text inside the quotes if you want another font for the Y-axis title.
  38. const data_labels_size = 12; // font size in px for the various data labels.
  39. const data_labels_font = "sans-serif"; // change the text inside the quotes if you want another font for the data labels.
  40.  
  41. /* End Settings */
  42.  
  43. /*---------------------------------------------------------------------------------------------------------------------------------------------*/
  44.  
  45. /* Function definitions */
  46. function addFancyChart(statistics, sorting, bar_count) {
  47. // magic for inserting chart goes here
  48.  
  49. let plottingdata = []; // I use this to avoid one million switch statements to pick the right data points.
  50. let plottinglabels = [];
  51. let yaxis_title = "";
  52.  
  53. // Choose what to sort on.
  54. // If we are in flat view, everything is already fine. If we are in fandom view, we will need to sort everything.
  55.  
  56. if (!sorting.flatView) {
  57. // More complicated magic for fandom mode...
  58. // This sorts ascending.
  59. switch (sorting.sortCol) {
  60. case "hits" :
  61. // Sort by hits.
  62. statistics.sort(function(a, b){return a.hits - b.hits});
  63. yaxis_title = "Hits";
  64. break;
  65. case "kudos.count" :
  66. // Sort by kudos.
  67. statistics.sort(function(a, b){return a.kudos - b.kudos});
  68. yaxis_title = "Kudos";
  69. break;
  70. case "comment_thread_count" :
  71. // Sort by comments.
  72. statistics.sort(function(a, b){return a.comments - b.comments});
  73. yaxis_title = "Comments";
  74. break;
  75. case "bookmarks.count" :
  76. // Sort by bookmarks.
  77. statistics.sort(function(a, b){return a.bookmarks - b.bookmarks});
  78. yaxis_title = "Bookmarks";
  79. break;
  80. case "subscriptions.count" :
  81. // Sort by subscriptions.
  82. statistics.sort(function(a, b){return a.subscriptions - b.subscriptions});
  83. yaxis_title = "Subscriptions";
  84. break;
  85. case "word_count" :
  86. // Sort by word count.
  87. statistics.sort(function(a, b){return a.words - b.words});
  88. yaxis_title = "Words";
  89. break;
  90. default :
  91. // What the fuck we should never be here.
  92. console.log("Sorting type not recognised! Graph aborted.");
  93. return "OW";
  94. } // End switch.
  95. if (sorting.desc) {
  96. // Descending sort. Reverse the order of everything.
  97. statistics.reverse();
  98. }
  99. }
  100.  
  101. switch (sorting.sortCol) {
  102. case "hits" :
  103. // Sort by hits.
  104. for (let i = 0; i < statistics.length; i++) {
  105. plottingdata.push(statistics[i].hits);
  106. plottinglabels.push(statistics[i].title);
  107. }
  108. yaxis_title = "Hits";
  109. break;
  110. case "kudos.count" :
  111. // Sort by kudos.
  112. for (let i = 0; i < statistics.length; i++) {
  113. plottingdata.push(statistics[i].kudos);
  114. plottinglabels.push(statistics[i].title);
  115. }
  116. yaxis_title = "Kudos";
  117. break;
  118. case "comment_thread_count" :
  119. // Sort by comments.
  120. for (let i = 0; i < statistics.length; i++) {
  121. plottingdata.push(statistics[i].comments);
  122. plottinglabels.push(statistics[i].title);
  123. }
  124. yaxis_title = "Comments";
  125. break;
  126. case "bookmarks.count" :
  127. // Sort by bookmarks.
  128. for (let i = 0; i < statistics.length; i++) {
  129. plottingdata.push(statistics[i].bookmarks);
  130. plottinglabels.push(statistics[i].title);
  131. }
  132. yaxis_title = "Bookmarks";
  133. break;
  134. case "subscriptions.count" :
  135. // Sort by subscriptions.
  136. for (let i = 0; i < statistics.length; i++) {
  137. plottingdata.push(statistics[i].subscriptions);
  138. plottinglabels.push(statistics[i].title);
  139. }
  140. yaxis_title = "Subscriptions";
  141. break;
  142. case "word_count" :
  143. // Sort by word count.
  144. for (let i = 0; i < statistics.length; i++) {
  145. plottingdata.push(statistics[i].words);
  146. plottinglabels.push(statistics[i].title);
  147. }
  148. yaxis_title = "Words";
  149. break;
  150. default :
  151. // What the fuck we should never be here.
  152. console.log("Sorting type not recognised! Graph aborted.");
  153. return "OW";
  154. } // End switch.
  155.  
  156. // Make a canvas.
  157. let boundaries = document.getElementById("stat_chart").getBoundingClientRect();
  158. //console.log("Boundaries of image area: " + JSON.stringify(boundaries));
  159. let inside_width = document.getElementById("stat_chart").clientWidth;
  160. //console.log("Client width reports: " + inside_width);
  161.  
  162. let chart_height = inside_width / chart_ratio; // Make the graph have a constant size ratio.
  163. chart_height = chart_height.toFixed(0); // And make that be an actual integer number of pixels please.
  164. document.getElementById("stat_chart").style.height = chart_height + "px";
  165.  
  166. // Build canvas instructions because I can't put a JS variable in my HTML string can I, that would be silly.
  167. document.getElementById("stat_chart").innerHTML = '<canvas id="inserted_stats" width="' + inside_width + '" height="' + chart_height + '" style="border:1px solid #AAAAAA;">';
  168.  
  169. // Set up canvas and brush for drawing.
  170. const stats_canvas = document.getElementById("inserted_stats");
  171. const brush = stats_canvas.getContext("2d");
  172. // Build the font strings.
  173. const yaxis_title_info = yaxis_title_size + "px " + yaxis_title_font;
  174. const data_labels_info = data_labels_size + "px " + data_labels_font;
  175. brush.font = data_labels_info; // Start in data label font as we only ever need the other one once.
  176.  
  177. // Sort out the bar count so that we don't try to display more bars than there are.
  178. bar_count = Math.min(bar_count, statistics.length);
  179. // Dump the data for bars we won't display so that the ymax doesn't look stupid.
  180. plottingdata = plottingdata.slice(0,bar_count);
  181. plottinglabels = plottinglabels.slice(0, bar_count);
  182.  
  183. // Determine the y-axis maximum.
  184. let ymax = 0;
  185. if (sorting.desc) {
  186. // Sorted descending.
  187. ymax = plottingdata[0];
  188. } else {
  189. // Sorted ascending.
  190. ymax = plottingdata[plottingdata.length - 1];
  191. }
  192.  
  193. // 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
  194. const left_axis_space = Math.ceil(brush.measureText(ymax).width) + yaxis_title_size + 10; // Allows a few px either side of the labels.
  195. const bottom_axis_space = data_labels_size + 4; // Allows a couple of px either side of the labels.
  196. const top_space = 10;
  197. const plot_width = inside_width - left_axis_space;
  198. const plot_height = chart_height - bottom_axis_space - top_space;
  199. //console.log("Plot height is now: " + plot_height + " px, and full chart height is " + chart_height + " px.");
  200.  
  201. // 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.
  202. // So there must be 4*bar_count + (bar_count + 1) units to divide the available space into.
  203. const pad_space = plot_width / (5 * bar_count + 1);
  204. const col_width = pad_space * 4;
  205.  
  206. // Determine the y-axis scale, using shitty rounding for approximately evenly spaced non-decimal labels.
  207. ymax = ymax.toExponential(1);
  208. let the_exponent = ymax.slice(3);
  209. ymax = parseFloat(ymax.slice(0,3)) + 0.1;
  210. ymax = ymax.toFixed(1) + the_exponent;
  211. ymax = parseFloat(ymax);
  212. let ystep = parseInt(ymax / 4);
  213.  
  214. // Draw y-axis values and major lines.
  215. const ticklen = 5;
  216. const tickgap = parseInt(plot_height / 4);
  217. brush.strokeStyle = "#888899"
  218. brush.lineWidth = 1;
  219. brush.beginPath();
  220. brush.moveTo(left_axis_space - ticklen, top_space);
  221. brush.lineTo(inside_width, top_space);
  222. brush.stroke();
  223. brush.beginPath();
  224. brush.moveTo(left_axis_space - ticklen, top_space + tickgap);
  225. brush.lineTo(inside_width, top_space + tickgap);
  226. brush.stroke();
  227. brush.beginPath();
  228. brush.moveTo(left_axis_space - ticklen, top_space + 2 * tickgap);
  229. brush.lineTo(inside_width, top_space + 2 * tickgap);
  230. brush.stroke();
  231. brush.beginPath();
  232. brush.moveTo(left_axis_space - ticklen, top_space + 3 * tickgap);
  233. brush.lineTo(inside_width, top_space + 3 * tickgap);
  234. brush.stroke();
  235.  
  236. // Draw axes. Done second to avoid axes being cut by lines.
  237. brush.strokeStyle = "black";
  238. brush.beginPath();
  239. brush.moveTo(left_axis_space, 0);
  240. brush.lineTo(left_axis_space, plot_height + top_space);
  241. brush.lineTo(inside_width, plot_height + top_space);
  242. brush.stroke();
  243. // Y-axis labels.
  244. brush.textAlign = "end"
  245. brush.textBaseline = "middle"
  246. brush.fillText(ymax, left_axis_space - ticklen, top_space);
  247. // console.log("measure text " + brush.measureText(ymax).width + "left space" + left_axis_space);
  248. brush.fillText(ymax - ystep, left_axis_space - ticklen, top_space + tickgap);
  249. brush.fillText(ymax - 2*ystep, left_axis_space - ticklen, top_space + 2*tickgap);
  250. brush.fillText(ymax - 3*ystep, left_axis_space - ticklen, top_space + 3*tickgap);
  251. brush.fillText(0, left_axis_space - ticklen, top_space + plot_height);
  252. // 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???
  253. 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.
  254. brush.textAlign = "center";
  255. brush.textBaseline = "top";
  256. brush.font = yaxis_title_info;
  257. brush.fillText(yaxis_title, - (top_space + 2*tickgap), 2);
  258. brush.font = data_labels_info;
  259. brush.rotate(90 * Math.PI / 180); // Put it back to draw the rest!
  260.  
  261. // Draw bars and x-axis labels.
  262. brush.strokeStyle = "black";
  263. brush.lineWidth = 1;
  264. brush.textAlign = "center";
  265. for (let i = 0; i < bar_count; i++) {
  266. let bar_height = plot_height * (plottingdata[i] / ymax);
  267. let bar_top = top_space + plot_height - bar_height;
  268. brush.fillStyle = barFillColour;
  269. brush.beginPath();
  270. brush.fillRect(left_axis_space + pad_space + (i * (col_width + pad_space)), bar_top, col_width, bar_height);
  271. brush.strokeRect(left_axis_space + pad_space + (i * (col_width + pad_space)), bar_top, col_width, bar_height);
  272.  
  273. // Magic number "+/- 2" is an offset to get the text away from the X-axis.
  274. brush.fillStyle = "black";
  275. brush.textBaseline = "top";
  276. brush.fillText(plottinglabels[i], left_axis_space + pad_space + (i * (col_width + pad_space)) + col_width/2, top_space + plot_height + 2);
  277.  
  278. brush.fillStyle = barTextColour;
  279. brush.textBaseline = "bottom";
  280. brush.fillText(plottingdata[i], left_axis_space + pad_space + (i * (col_width + pad_space)) + col_width/2, top_space + plot_height - 2);
  281. }
  282.  
  283. // this is shit magic for inserting text instead as a test
  284. // document.getElementById("stat_chart").innerHTML = "<p>I WILL BE PRETTY ONE DAY</p>";
  285. } //End of addFancyChart function.
  286.  
  287. function getViewType(statspage_href) {
  288. // Magic for finding out what kind of view we are displaying.
  289. // Needs to return Fandom/Flat, Sort Column, and Asc/Desc.
  290. // All of these have defaults: Fandom, Hits, and Desc.
  291. let view_description = {flatView : false, sortCol : "hits", desc : true};
  292.  
  293. // Analyse the input web address for flat view: if we're using flat view, the string "flat_view=true" will appear somewhere.
  294. if (statspage_href.search("flat_view=true") != -1) {
  295. view_description.flatView = true;
  296. }
  297.  
  298. // Analyse the input web address for sort ascending / descending:
  299. if (statspage_href.search("sort_direction=ASC") != -1) {
  300. view_description.desc = false;
  301. }
  302.  
  303. // Analyse the input web address for the sort column. This is the tricky one.
  304. let iscolsorted = statspage_href.search("sort_column=");
  305. if (iscolsorted != -1) {
  306. // A sort column has been chosen. Dice the string to find out which one.
  307. // First, dispose of everything up to and including "sort_column=".
  308. let sort_type = statspage_href.substring(iscolsorted + 12);
  309. let nextampersand = sort_type.search("&");
  310. sort_type = sort_type.substring(0, nextampersand);
  311. if (sort_type.length > 0) {
  312. view_description.sortCol = sort_type;
  313. }
  314. }
  315.  
  316. return view_description;
  317. } // End of getViewType function.
  318.  
  319. function getStats(sorting_type) {
  320. // Magic for getting the stats out of the stats page.
  321. // Might have to rewrite this if AO3 ever change the stats page layout.
  322.  
  323. let all_stories = document.getElementsByClassName("fandom listbox group");
  324. //console.log("All stories object is: " + all_stories + ", a " + typeof(all_stories) + " with length: " + all_stories.length);
  325. //console.log("All stories element 0 is: " + all_stories[0] + ", a " + typeof(all_stories[0]));
  326. //console.log("Contents of all stories [0]: " + all_stories[0].innerHTML);
  327.  
  328. const stories = []; // Create a stories array to store our stories. We'll return this at the end.
  329.  
  330. if (sorting_type.flatView) {
  331. // In this case, all_stories will have length 1, as there is only one listbox group.
  332.  
  333. // Get all the list elements. There should be one of these per story, each one containing one set of story tat.
  334. let story_list = all_stories[0].getElementsByTagName("li");
  335. let story_count = story_list.length;
  336. // I can use this to work out where the subscriptions are, and anything else that might or might not exist.
  337. // Involves too much string munging for my liking for me to do it that way for everything.
  338.  
  339. // 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).
  340. let extract_titles = all_stories[0].getElementsByTagName("a");
  341. //console.log("Extracted title 0: " + extract_titles[0].innerText);
  342.  
  343. // Get the fandoms, conveniently in class "fandom".
  344. let extract_fandoms = all_stories[0].getElementsByClassName("fandom");
  345. //console.log("Extracted fandom 0: " + extract_fandoms[0].innerText);
  346.  
  347. // Get the wordcounts, class "words".
  348. let extract_wordcounts = all_stories[0].getElementsByClassName("words");
  349. //console.log("Extracted wordcount 0: " + extract_wordcounts[0].innerText);
  350.  
  351. // Get the hits. Hits text is in dt.hits, number is in dd.hits.
  352. let extract_hits = all_stories[0].querySelectorAll("dd.hits");
  353. //console.log("Extracted hits 0: " + extract_hits[0].innerText);
  354.  
  355. // Get the kudos. Kudos text is in dt.kudos, number is in dd.kudos.
  356. let extract_kudos = all_stories[0].querySelectorAll("dd.kudos");
  357. //console.log("Extracted kudos 0: " + extract_kudos[0].innerText);
  358.  
  359. // Get the bookmarks. Bookmarks text is in dt.bookmarks, number is in dd.bookmarks.
  360. let extract_bookmarks = all_stories[0].querySelectorAll("dd.bookmarks");
  361. //console.log("Extracted bookmarks 0: " + extract_bookmarks[0].innerText);
  362.  
  363. // Get the comments. Comments text is in dt.comments, number is in dd.comments.
  364. let extract_comments = all_stories[0].querySelectorAll("dd.comments");
  365. //console.log("Extracted comments 0: " + extract_comments[0].innerText);
  366.  
  367. // Process and clean the various numerical strings to get numbers. Pop each piece of data into an object. Array of objects. Thing.
  368. for (let i = 0; i < story_count; i++) {
  369. // Cut wordcount down to numbers only. Format is "(X words)" where X is the number, so we remove [0] and everything after [-7].
  370. let wordcount = extract_wordcounts[i].innerText;
  371. wordcount = wordcount.slice(1, wordcount.length-7);
  372. // Sort out subscriptions where they exist. They don't always exist, so be careful.
  373. let subs = 0;
  374. let subs_exist = story_list[i].innerText.search("Subscriptions: ");
  375. if (subs_exist != -1) {
  376. subs = story_list[i].innerText.slice(subs_exist+15);
  377. subs = subs.slice(0, subs.search(/\s/));
  378. subs = parseInt(subs.replace(/,/g, ""));
  379. }
  380.  
  381. // Create a new story object and add it to our stories array.
  382. let the_story = {
  383. title : extract_titles[i].innerText,
  384. fandoms : extract_fandoms[i].innerText.slice(1,extract_fandoms[i].innerText.length-1),
  385. words : parseInt(wordcount.replace(/,/g, "")),
  386. hits : parseInt(extract_hits[i].innerText.replace(/,/g, "")),
  387. kudos : parseInt(extract_kudos[i].innerText.replace(/,/g, "")),
  388. bookmarks : parseInt(extract_bookmarks[i].innerText.replace(/,/g, "")),
  389. comments : parseInt(extract_comments[i].innerText.replace(/,/g, "")),
  390. subscriptions : subs
  391. };
  392. stories.push(the_story);
  393. }
  394. // And we're done with flat view!
  395.  
  396. } else {
  397. //console.log("Fandom View Detected.");
  398.  
  399. let fandom_count = all_stories.length;
  400. //console.log("Assessing stories for " + fandom_count + " fandoms.");
  401.  
  402. let titles_list = [];
  403. for (let i = 0; i < fandom_count; i++) {
  404. let fandom_name = all_stories[i].getElementsByClassName("heading");
  405. let fandom = fandom_name[0].innerText;
  406. let story_list = all_stories[i].getElementsByTagName("li");
  407. let story_count = story_list.length;
  408.  
  409. // 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).
  410. let extract_titles = all_stories[i].getElementsByTagName("a");
  411. //console.log("Extracted title 0: " + extract_titles[0].innerText);
  412.  
  413. // Get the wordcounts, class "words".
  414. let extract_wordcounts = all_stories[i].getElementsByClassName("words");
  415. //console.log("Extracted wordcount 0: " + extract_wordcounts[0].innerText);
  416.  
  417. // Get the hits. Hits text is in dt.hits, number is in dd.hits.
  418. let extract_hits = all_stories[i].querySelectorAll("dd.hits");
  419. //console.log("Extracted hits 0: " + extract_hits[0].innerText);
  420.  
  421. // Get the kudos. Kudos text is in dt.kudos, number is in dd.kudos.
  422. let extract_kudos = all_stories[i].querySelectorAll("dd.kudos");
  423. //console.log("Extracted kudos 0: " + extract_kudos[0].innerText);
  424.  
  425. // Get the bookmarks. Bookmarks text is in dt.bookmarks, number is in dd.bookmarks.
  426. let extract_bookmarks = all_stories[i].querySelectorAll("dd.bookmarks");
  427. //console.log("Extracted bookmarks 0: " + extract_bookmarks[0].innerText);
  428.  
  429. // Get the comments. Comments text is in dt.comments, number is in dd.comments.
  430. let extract_comments = all_stories[i].querySelectorAll("dd.comments");
  431. //console.log("Extracted comments 0: " + extract_comments[0].innerText);
  432. for (let j = 0; j < story_count; j++) {
  433. let title_location = titles_list.indexOf(extract_titles[j].innerText);
  434. if ( title_location == -1 ) {
  435. // No story by this title is yet recorded. Fill in the story object.
  436. // Cut wordcount down to numbers only. Format is "(X words)" where X is the number, so we remove [0] and everything after [-7].
  437. let wordcount = extract_wordcounts[j].innerText;
  438. wordcount = wordcount.slice(1, wordcount.length-7);
  439. // Find out if subscriptions are real.
  440. let subs = 0;
  441. let subs_exist = story_list[j].innerText.search("Subscriptions: ");
  442. if (subs_exist != -1) {
  443. subs = story_list[j].innerText.slice(subs_exist+15);
  444. subs = subs.slice(0, subs.search(/\s/));
  445. subs = parseInt(subs.replace(/,/g, ""));
  446. }
  447. let the_story = {
  448. title : extract_titles[j].innerText,
  449. fandoms : fandom,
  450. words : parseInt(wordcount.replace(/,/g, "")),
  451. hits : parseInt(extract_hits[j].innerText.replace(/,/g, "")),
  452. kudos : parseInt(extract_kudos[j].innerText.replace(/,/g, "")),
  453. bookmarks : parseInt(extract_bookmarks[j].innerText.replace(/,/g, "")),
  454. comments : parseInt(extract_comments[j].innerText.replace(/,/g, "")),
  455. subscriptions : subs
  456. }
  457. // Add the story to the story array.
  458. stories.push(the_story);
  459. // Add processed title to the titles list.
  460. titles_list.push(extract_titles[j].innerText);
  461. } else {
  462. // We already have a story by this title. Find it and add a fandom to it.
  463. // The story should be in the same index in stories as the title is in titles_list.
  464. stories[title_location].fandoms = stories[title_location].fandoms + ", " + fandom;
  465. }
  466. }
  467. }
  468. }
  469. // And we're done with fandom view!.
  470. return stories;
  471. }
  472. /* End function definitions */
  473.  
  474. /* Main Code
  475. * Super awesome final magic that makes it all work happens here */
  476.  
  477. // Figure out how we are displaying and sorting our data.
  478. let view_sort = getViewType(window.location.href);
  479.  
  480. //Test if it worked:
  481. //console.log("View instructions: " + JSON.stringify(view_sort));
  482.  
  483. let story_stats = getStats(view_sort);
  484. // Test that worked.
  485. //console.log("Stats exist for " + story_stats.length + " stories.");
  486. //console.log("First story: " + JSON.stringify(story_stats[0]));
  487. //console.log("Last story: " + JSON.stringify(story_stats[story_stats.length-1]));
  488.  
  489. addFancyChart(story_stats, view_sort, numberOfBars); // HOLY SHIT IT WORKED
  490. })();