MediaWiki:Gadget-GECharts-core.js

From Old School Near-Reality Wiki
Jump to navigation Jump to search

Note: After publishing, you may have to bypass your browser's cache to see the changes.

  • Firefox / Safari: Hold Shift while clicking Reload, or press either Ctrl-F5 or Ctrl-R (⌘-R on a Mac)
  • Google Chrome: Press Ctrl-Shift-R (⌘-Shift-R on a Mac)
  • Internet Explorer / Edge: Hold Ctrl while clicking Refresh, or press Ctrl-F5
  • Opera: Press Ctrl-F5.
   1 /** <nowiki>
   2  * Grand Exchange Charts
   3  * Displays price data of item(s) in a chart
   4  *
   5  * Highstock docs <https://api.highcharts.com/highstock/>
   6  * Highstock change log <https://www.highcharts.com/blog/changelog/#highstock>
   7  *
   8  * @author Joeytje50
   9  * @author Cqm
  10  * @author JaydenKieran
  11  *
  12  * @todo move Highcharts to a core ResourceLoader module
  13  *
  14  * @todo use a consistent variable for the chart id
  15  *       currently it's one of c, i or id
  16  * @todo remove script URLs (javascript:func) in favour of onclick events
  17  *       may require attaching the events after the some parts have loaded
  18  * @todo fix averages
  19  */
  20 
  21 /*global jQuery, mediaWiki, rswiki, Highcharts, wgPageName, wgTitle, wgNamespaceNumber */
  22 
  23 'use strict';
  24 
  25 /**
  26  * Cache mw.config variables
  27  */
  28 var conf = mw.config.get([
  29         'wgNamespaceNumber',
  30         'wgPageName',
  31         'wgTitle',
  32         'wgSiteName'
  33     ]),
  34 
  35     // Are we on OSRS? Impacts selectors and volume labels / multipliers
  36     isOSRS = conf.wgSiteName == "Old School RuneScape Wiki",
  37 
  38     // Volume label depends on which wiki we're on
  39     volumeLabel = isOSRS ? "Daily volume" : "7-day volume",
  40     gameVersion = isOSRS ? 'osrs' : 'rs',
  41 
  42     /**
  43      * <doc>
  44      *
  45      * @todo replace `_GEC` wih this
  46      */
  47     gec = {},
  48 
  49     // @todo document each of these
  50     _GEC = {
  51         AIQueue: [],
  52         AILoaded: [],
  53         AIData: [],
  54         addedData: [],
  55         average: parseInt((location.hash.match(/#a=([^#]*)/) || [])[1], 10) || '',
  56         urlCache: {}
  57     },
  58 
  59     /**
  60      * Startup methods
  61      */
  62     self = {
  63         /**
  64          * Loads and implements any required dependencies
  65          */
  66         deps: function () {
  67             if (!mw.loader.getState('rs.highcharts')) {
  68                 mw.loader.implement(
  69                     'rs.highcharts',
  70                     ['https://code.highcharts.com/stock/highstock.js'],
  71                     {}, {}
  72                 );
  73             }
  74 
  75             mw.loader.using(['mediawiki.util', 'mediawiki.api', 'rs.highcharts'], self.init);
  76         },
  77 
  78         /**
  79          * Initial loading function
  80          */
  81         init: function (req) {
  82             window.Highcharts = req('rs.highcharts');
  83             (function () {
  84                 var newhash = location.hash
  85                     .replace(/\.([0-9a-f]{2})/gi, function (_, first) {
  86                         return String.fromCharCode(parseInt(first, 16));
  87                     })
  88                     .replace(/ /g, '_');
  89                 if (newhash && newhash.match(/#[aiz]=/)) {
  90                     location.hash = newhash;
  91                 }
  92             }());
  93 
  94             $('.GEdatachart').attr('id', function (c) {
  95                 return 'GEdatachart' + c;
  96             });
  97             $('.GEdataprices').attr('id', function (c) {
  98                 return 'GEdataprices' + c;
  99             });
 100             $('.GEChartBox').each(function (c) {
 101                 $(this).find('.GEChartItems').attr('id', 'GEChartItems' + c);
 102             });
 103 
 104             Highcharts.setOptions({
 105                 lang: {
 106                     // @todo can this be done with CSS?
 107                     resetZoom: null,
 108                     numericSymbols: ['K', 'M', 'B', 'T', 'Qd', 'Qt'],
 109                 }
 110             });
 111 
 112             // globals to maintain javascript hrefs
 113             window._GEC = _GEC;
 114             window.popupChart = popupChart;
 115             window.addItem = chart.addItem;
 116             window.removeGraphItem = chart.removeItem;
 117 
 118             self.buildPopup();
 119             self.setupCharts();
 120         },
 121 
 122         /**
 123          * <doc>
 124          */
 125         makeOOUI: function (c) {
 126             var averageRangeInput, addItemInput, submitButton, resetButton, fieldset, permalink;
 127             averageRangeInput = new OO.ui.NumberInputWidget({
 128                 min: 1,
 129                 value: 30,
 130                 id: 'average' + c
 131             });
 132             averageRangeInput.$element.data('ooui-elem', averageRangeInput);
 133             addItemInput = new OO.ui.TextInputWidget({
 134                 id: 'extraItem' + c
 135             });
 136             addItemInput.$element.data('ooui-elem', addItemInput);
 137             submitButton = new OO.ui.ButtonInputWidget({
 138                 label: 'Submit',
 139                 flags: ['primary', 'progressive']
 140             });
 141             resetButton = new OO.ui.ButtonInputWidget({
 142                 label: 'Reset'
 143             });
 144             permalink = new OO.ui.ButtonInputWidget({
 145                 label: 'Permanent link',
 146                 title: 'Permanent link to the current chart settings and items. Right click to copy the url.',
 147                 id: 'GEPermLink' + c
 148             });
 149             permalink.$element.data('ooui-elem', permalink);
 150             permalink.setData('/w/RuneScape:Grand_Exchange_Market_Watch/Chart');
 151             permalink.on('click', function () {
 152                 window.open(permalink.getData(), '_blank');
 153             });
 154 
 155             averageRangeInput.on('enter', function () {
 156                 addItem(c);
 157             });
 158             addItemInput.on('enter', function () {
 159                 addItem(c);
 160             });
 161             submitButton.on('click', function () {
 162                 addItem(c);
 163             });
 164 
 165             resetButton.on('click', function () {
 166                 addItemInput.setValue('');
 167                 averageRangeInput.setValue(30);
 168             });
 169 
 170             fieldset = new OO.ui.FieldsetLayout();
 171             fieldset.addItems([
 172                 new OO.ui.FieldLayout(averageRangeInput, {label: 'Average (days)'}),
 173                 new OO.ui.FieldLayout(addItemInput, {label: 'Add new item'})
 174             ]);
 175             fieldset.$element.append(submitButton.$element).append(resetButton.$element).append(permalink.$element);
 176 
 177             fieldset.$element.css('width', '50%');
 178             return fieldset.$element;
 179         },
 180         buildPopup: function () {
 181             var close;
 182             close = new OO.ui.ButtonWidget({
 183                 icon: 'close'
 184             });
 185             close.on('click', function () {
 186                 popupChart(false);
 187             });
 188 
 189 
 190             $('body').append(
 191                 $('<div>')
 192                     .attr('id', 'GEchartpopup')
 193                     .css('display', 'none')
 194                     .append(
 195                         $('<div>')
 196                             .attr('id', 'closepopup')
 197                             .append(close.$element),
 198                         self.makeOOUI('popup'),
 199                         $('<div>')
 200                             .attr('id', 'addedItemspopup'),
 201                         $('<div>')
 202                             .attr('id', 'GEpopupchart')
 203                     )
 204             );
 205         },
 206 
 207         /**
 208          * <doc>
 209          */
 210         setupCharts: function () {
 211 
 212             $('div.GEdatachart').each(function (c) {
 213 
 214                 var $dataPrices = $('#GEdataprices' + c),
 215                     $dataChart = $('#GEdatachart' + c),
 216                     dataItem = $dataPrices.attr('data-item'),
 217                     isSmall = $dataChart.hasClass('smallChart'),
 218                     isMedium = $dataChart.hasClass('mediumChart'),
 219                     isIndexChart = /index/i.test(dataItem),
 220                     selector = isOSRS ? '.infobox *, .infobar *, .infobox-switch-resources.infobox-resources-Infobox_Item *' : '.infobox *, .infobar *, .rsw-infobox *, .infobox-switch-resources.infobox-resources-Infobox_Item *',
 221                     isInfobox = $dataPrices.is(selector),
 222                     itemName = dataItem || conf.wgTitle.split('/')[0],
 223                     dataList,
 224                     yAxis,
 225                     zoom;
 226 
 227 
 228                 if (!$dataPrices.length) {
 229                     return;
 230                 }
 231 
 232                 // setting up the form and chart elements
 233                 if (!isSmall && !isMedium) {
 234                     $dataChart.before(
 235                         self.makeOOUI(c),
 236                         $('<div>')
 237                             .attr('id', 'addedItems' + c)
 238                     );
 239                 }
 240 
 241                 getData(c, isSmall, isMedium, undefined, function(data) {
 242                     var dataList = data[0];
 243                     var yAxis = data[1];
 244                     if (itemName.toLowerCase() !== 'blank') {
 245                         zoom = parseInt((location.hash.match(/#z=([^#]*)/) || [])[1]);
 246                         zoom = zoom && zoom <= 6 && zoom >= 0 ?
 247                             zoom - 1 :
 248                             (zoom === 0 ?
 249                                 0 :
 250                                 2);
 251                     }
 252                     
 253                     var enlarge = $('<a>')
 254                     	.attr("id", "gec-enlarge-" + c)
 255                     	.css("text-decoration", "underline")
 256                     	.css("color", "inherit")
 257                     	.css("font-size", "inherit")
 258                     	.text("Enlarge chart");
 259                     	
 260                     // @todo this doesn't do anything on small charts
 261                     //       is it supposed to?
 262                     //var zoomOut = '<a href="javascript:_GEC.chart' + c + '.zoomOut();" style="text-decoration:underline;color:inherit;font-size:inherit;">Zoom out</a>';
 263 
 264                     //generating the chart
 265                     _GEC['chart' + c] = new Highcharts.StockChart({
 266                         chart: {
 267                             renderTo: 'GEdatachart' + c,
 268                             backgroundColor: 'white',
 269                             plotBackgroundColor: 'white',
 270                             zoomType: '',
 271                             //height: isSmall?210:null,
 272                             events: {
 273                                 redraw: function () {
 274                                     _GEC.thisid = this.renderTo.id.replace('GEdatachart', '').replace('GEpopupchart', 'popup');
 275                                     setTimeout(function () {
 276                                         setChartExtremes(_GEC.thisid);
 277                                     }, 0);
 278                                 }
 279                             },
 280                             marginBottom: 0,
 281                         },
 282                         legend: {
 283                             enabled: !isSmall && !isMedium,
 284                             backgroundColor: 'white',
 285                             align: 'right',
 286                             layout: 'vertical',
 287                             verticalAlign: 'top',
 288                             y: 85
 289                         },
 290                         responsive: {
 291                             rules: [{
 292                                 condition: {
 293                                     //maxWidth: 500
 294                                 },
 295                                 chartOptions: {
 296                                     legend: {
 297                                         align: 'center',
 298                                         verticalAlign: 'bottom',
 299                                         layout: 'horizontal'
 300                                     }
 301                                 }
 302                             }]
 303                         },
 304                         title: {
 305                             text: (isSmall || isMedium) ? ((isInfobox || isMedium) ? enlarge[0].outerHTML : itemName) : 'Grand Exchange Market Watch',
 306                             useHTML: true,
 307                             style: {
 308                                 color: 'black',
 309                                 fontSize: isSmall ? (enlarge ? '13px' : '15px') : '18px',
 310                             },
 311                         },
 312                         subtitle: {
 313                             text: isSmall ? (isInfobox ? '' : enlarge[0].outerHTML) : (itemName.toLowerCase() == 'blank' ? 'Historical chart' : itemName),
 314                             useHTML: true,
 315                             y: 35,
 316                             style: {
 317                                 color: '#666',
 318                                 fontSize: isSmall ? '13px' : '15px',
 319                             },
 320                         },
 321                         rangeSelector: {
 322                             enabled: !isSmall && !isMedium,
 323                             selected: zoom,
 324                             inputBoxStyle: {
 325                                 right: '15px',
 326                                 display: (isSmall || isMedium) ? 'none' : 'block'
 327                             },
 328                             inputStyle: {
 329                                 width: '100px',
 330                             },
 331                             inputDateFormat: "%e-%b-%Y",
 332                             buttonTheme: {
 333                                 class: 'zoomButton',
 334                             },
 335                             buttons: [{
 336                                 type: 'month',
 337                                 count: 1,
 338                                 text: '1m'
 339                             }, {
 340                                 type: 'month',
 341                                 count: 2,
 342                                 text: '2m'
 343                             }, {
 344                                 type: 'month',
 345                                 count: 3,
 346                                 text: '3m'
 347                             }, {
 348                                 type: 'month',
 349                                 count: 6,
 350                                 text: '6m'
 351                             }, {
 352                                 type: 'year',
 353                                 count: 1,
 354                                 text: '1y'
 355                             }, {
 356                                 type: 'all',
 357                                 text: 'All'
 358                             }]
 359                         },
 360                         plotOptions: {
 361                             series: {
 362                                 enableMouseTracking: !isSmall,
 363                                 dataGrouping: {
 364                                     dateTimeLabelFormats: {
 365                                         day: ['%A, %e %B %Y', '%A, %e %B', '-%A, %e %B %Y'],
 366                                         week: ['Week from %A, %e %B %Y', '%A, %e %B', '-%A, %e %B %Y'],
 367                                         month: ['%B %Y', '%B', '-%B %Y'],
 368                                         year: ['%Y', '%Y', '-%Y']
 369                                     }
 370                                 }
 371                             }
 372                         },
 373                         tooltip: {
 374                             enabled: !isSmall,
 375                             valueDecimals: isIndexChart ? 2 : 0,
 376                             headerFormat: '<span style="font-size: 12px">{point.key}</span><br/>',
 377                             xDateFormat: "%A, %e %B %Y",
 378                         },
 379                         navigator: {
 380                             xAxis: {
 381                                 dateTimeLabelFormats: {
 382                                     day: "%e-%b",
 383                                     week: "%e-%b",
 384                                     month: "%b-%Y",
 385                                     year: "%Y",
 386                                 },
 387                                 minTickInterval: 24 * 3600 * 1000, //1 day
 388                             },
 389                             maskFill: 'none',
 390                             enabled: !(isSmall || isMedium)
 391                         },
 392                         credits: {
 393                             enabled: false,
 394                         },
 395                         xAxis: [{
 396                             lineColor: '#666',
 397                             tickColor: '#666',
 398                             dateTimeLabelFormats: {
 399                                 day: "%e-%b",
 400                                 week: "%e-%b",
 401                                 month: "%b-%Y",
 402                                 year: "%Y",
 403                             },
 404                             minTickInterval: 24 * 3600 * 1000, //1 day
 405                             scrollbar: {
 406                                 enabled: false,
 407                                 showFull: false
 408                             },
 409                         }],
 410                         yAxis: yAxis,
 411                         series: dataList,
 412                         colors: window.GEMWChartColors || ['#4572A7', '#AA4643', '#89A54E', '#80699B', '#3D96AE', '#DB843D', '#92A8CD', '#A47D7C', '#B5CA92']
 413                     });
 414 
 415                     var items = ($('#GEChartItems' + c).html() || '').split(',');
 416                     var noAdd = [];
 417                     var i;
 418 
 419                     for (i = 0; i < items.length; i++) {
 420                         items[i] = items[i].trim();
 421 
 422                         if (items[i]) {
 423                             addItem(c, items[i]);
 424                         } else {
 425                             noAdd.push(1);
 426                         }
 427                     }
 428                     if (items.length == noAdd.length && _GEC['chart' + c].series[0].name.toLowerCase() != 'blank') setChartRange(c);
 429                     
 430                     //adjusting the axes extremes (initial load)
 431                     setChartExtremes(c);
 432 
 433                     //loading the chart and additional price info when the page is ready
 434                     if (((conf.wgNamespaceNumber == 112 && conf.wgTitle.split('/')[1] == 'Data') || conf.wgPageName == 'RuneScape:Grand_Exchange_Market_Watch/Chart') && location.hash.match('#i=') !== null) {
 435                         var hash = location.hash;
 436                         items = decodeURIComponent((hash.match(/#i=([^#]*)/) || [])[1] || '').replace(/_/g, ' ').split(',');
 437                         for (i = 0; i < items.length; i++) {
 438                             if (items[i].match(/^\s*$/) === null) addItem(0, items[i]);
 439                         }
 440                     }
 441                     
 442                     var $enlargeEle = $("#gec-enlarge-" + c);
 443                     if ($enlargeEle.length) {
 444 	                	$enlargeEle.on("click", function () {
 445 	                		popupChart(c);
 446 	                	});
 447                     };
 448                 });
 449 
 450             });
 451 
 452         }
 453     },
 454 
 455     /**
 456      * General helper methods
 457      */
 458     util = {
 459         /**
 460          * <doc>
 461          *
 462          * @todo replace with $.extend
 463          *
 464          * @param a {object}
 465          * @param b {object} (optional)
 466          *
 467          * @return {object}
 468          */
 469         cloneObj: function (a, b) {
 470             if (typeof a !== 'object') {
 471                 return '';
 472             }
 473 
 474             if (typeof b !== 'object') {
 475                 b = {};
 476             }
 477 
 478             for (var key in a) {
 479                 if (a.hasOwnProperty(key)) {
 480                     b[key] = a[key];
 481                 }
 482             }
 483 
 484             return b;
 485         },
 486 
 487         /**
 488          * Averages prices across a specified time interval
 489          *
 490          * @param arr {array} Array of arrays, where each member of `arr`
 491          *                    is in the format [time, price]
 492          *                    Which is how we store the price data
 493          *                    @example [x-coord, y-coord]
 494          * @param amt {number} Interval to average across in days
 495          * @param round {number} (optional) Number of decimal places to round to
 496          *                       Defaults to 0
 497          *
 498          * @return {array} Array of arrays, where each member of the return array
 499          *                 is in the format [time, price] (as above)
 500          *                 and
 501          */
 502         avg: function (arr, amt, round) {
 503             amt = amt || arr.length;
 504             // convert `round` into a number we can use for rounding
 505             round = Math.pow(10, round || 0);
 506 
 507             var avgs = [],
 508                 list = [],
 509                 i;
 510 
 511             // adds each price to `list`
 512             // when `amt` is reached, average the contents of `list`
 513             //
 514             // each iteration after `amt` is reached averages the contents of `list`
 515             // which is continuously being updated as each iteration
 516             // after `amt` is reached replaces a member of `list`
 517             // @example when `i` is 31 the current price replaces `list[1]`
 518             //          when `i` is 35 the current price replaces `list[5]`
 519             for (i = 0; i < arr.length; i++) {
 520                 list[i % amt] = arr[i][1];
 521 
 522                 if (i >= amt) {
 523                     avgs.push([
 524                         // don't modify the time (y-coord)
 525                         arr[i][0],
 526                         Math.round((util.sum(list) / list.length) * round) / round
 527                     ]);
 528                 }
 529             }
 530 
 531             return avgs;
 532         },
 533 
 534         /**
 535          * Finds the sum of numbers in an array
 536          * Only called by `util.avg`
 537          *
 538          * @param arr {array} Array of number to find the sum of
 539          *
 540          * @return {number} Sum of the numbers in `arr`
 541          */
 542         sum: function (arr) {
 543             var total = 0,
 544                 i;
 545 
 546             for (i = 0; i < arr.length; i++) {
 547                 total += parseFloat(arr[i], 10);
 548             }
 549 
 550             return total;
 551         },
 552 
 553         /**
 554          * Rounds and formats numbers
 555          *
 556          * @example 12345        -> 12.3K
 557          * @example 1234567      -> 1.2M
 558          * @example 123456789012 -> 123.4M
 559          *
 560          * @param num {number|string} Number to format
 561          *
 562          * @return {string} Formatted number
 563          */
 564         toKMB: function (num) {
 565             // strip commas from number string
 566             // as `parseInt` will interpret them as a decimal separator
 567             // pass numbers and string to `parseInt` to convert floats too
 568             num = parseInt((typeof num === 'string' ? num.replace(/,/g, '') : num), 10);
 569             var neg = num < 0 ? '-' : '';
 570 
 571             num = Math.abs(num);
 572 
 573             // `1eX` is shorthand for `Math.pow( 10, X )`
 574             if (num >= 1e10) {
 575                 num = Math.round(num / 1e8) / 10;
 576                 num += 'B';
 577             } else if (num >= 1e7) {
 578                 num = Math.round(num / 1e5) / 10;
 579                 num += 'M';
 580             } else if (num >= 1e4) {
 581                 num = Math.round(num / 100) / 10;
 582                 num += 'K';
 583             }
 584 
 585             return rs.addCommas(neg + num);
 586         },
 587 
 588         /**
 589          * Capitalises first character of a string
 590          *
 591          * @source <https://stackoverflow.com/a/1026087>
 592          *
 593          * @param str {string}
 594          *
 595          * @return {string}
 596          */
 597         ucFirst: function (str) {
 598             return str.charAt(0).toUpperCase() + str.slice(1);
 599         },
 600 
 601         /**
 602          * Sort data points in the graph data before passing it to the charts api
 603          */
 604         sortPoints: function (a, b) {
 605             a = a.replace(/'/g, '').split(':')[0];
 606             b = b.replace(/'/g, '').split(':')[0];
 607 
 608             return a - b;
 609         }
 610     },
 611 
 612     /**
 613      * Chart methods
 614      */
 615     chart = {
 616         /**
 617          * <doc>
 618          *
 619          * @param id {string|number}
 620          * @param match {string} is normally the 'line' that isn't an item's price data
 621          *                       such as average or volume
 622          *
 623          * @return {number}
 624          */
 625         getSeriesIndex: function (id, match) {
 626             var chart = _GEC['chart' + id],
 627                 series = chart.series,
 628                 i;
 629 
 630             if (chart) {
 631                 for (i = 0; i < series.length; i++) {
 632                     if (series[i].name.match(match)) {
 633                         return i;
 634                     }
 635                 }
 636 
 637                 return -1;
 638             }
 639 
 640             // @todo what happens if !chart
 641         },
 642 
 643         /**
 644          * Creates a URL with preset options
 645          *
 646          * @todo change to url params
 647          * @todo document the individual params/options
 648          *
 649          * @param id {number|string}
 650          *
 651          * @return {string}
 652          */
 653         permLinkUrl: function (id) {
 654             var chart = _GEC['chart' + id],
 655                 xt = chart.xAxis[0].getExtremes(),
 656                 series = chart.series,
 657                 minDate = (new Date(xt.min))
 658                     .toDateString()
 659                     .split(' ')
 660                     .slice(1)
 661                     .join('_'),
 662                 maxDate = (new Date(xt.max))
 663                     .toDateString()
 664                     .split(' ')
 665                     .slice(1)
 666                     .join('_'),
 667                 inputAvg = $('#average' + id).data('ooui-elem').getNumericValue(),
 668                 urlHash = '#t=' + minDate + ',' + maxDate,
 669                 items = '',
 670                 i;
 671 
 672             if (!isNaN(inputAvg)) {
 673                 urlHash += '#a=' + inputAvg;
 674             }
 675 
 676             for (i = 0; i < series.length; i++) {
 677                 if (series[i].name == 'Navigator' || series[i].name.match('average')) {
 678                     continue;
 679                 }
 680 
 681                 // separate items with commas
 682                 if (items) {
 683                     items += ',';
 684                 }
 685 
 686                 // @todo url encode this?
 687                 items += series[i].name.replace(/ /g, '_');
 688             }
 689 
 690             urlHash += '#i=' + items;
 691 
 692             // @todo hide the redirect h2
 693             return '/w/RuneScape:Grand_Exchange_Market_Watch/Chart' + urlHash;
 694         },
 695 
 696         /**
 697          * Add a new item to the chart
 698          *
 699          * @param i
 700          * @param it {string} (optional)
 701          */
 702         addItem: function (i, it) {
 703             _GEC.chartid = i;
 704             var OOUIextraItemPresent = $('#extraItem' + i).length > 0,
 705                 OOUIextraItem = $('#extraItem' + i).data('ooui-elem'),
 706                 item = (it || '').trim() || OOUIextraItem.getValue(),
 707                 dataItems = [
 708                     '#addedItems' + i + ' [data-item]',
 709                     '#GEdataprices' + i + '[data-item]'
 710                 ],
 711                 $dataItems = $(dataItems.join(',')).map(function () {
 712                     return $(this).attr('data-item').toLowerCase();
 713                 }),
 714                 $addedItems = $('#addedItems' + i),
 715                 id,
 716                 data,
 717                 series,
 718                 seriesIndex,
 719                 gecchartid = i,
 720                 index;
 721 
 722             if (item && item.length) {
 723                 index = -1;
 724                 for (var i2 = 0; i2 < _GEC.AIQueue.length; i2++) {
 725                     if (_GEC.AIQueue[i2] == item.toLowerCase()) {
 726                         index = i2;
 727                         break;
 728                     }
 729                 }
 730 
 731                 if (
 732                     // @todo should a number passed to .get()
 733                     $dataItems.get().indexOf(item.toLowerCase()) !== -1 ||
 734                     index !== -1
 735                 ) {
 736                     if (!it) {
 737                         alert(item + ' is already in the graph.');
 738                     }
 739 
 740                     if (OOUIextraItemPresent) { 
 741                         OOUIextraItem.setValue(''); 
 742                     }
 743 
 744                     return false;
 745                 }
 746 
 747                 if (OOUIextraItemPresent) { 
 748                     OOUIextraItem.setDisabled(true);    
 749                 }
 750 
 751                 $.get(
 752                     '/api.php',
 753                     {
 754                         action: 'query',
 755                         prop: 'revisions',
 756                         rvprop: 'content',
 757                         format: 'json',
 758                         titles: 'Module:Exchange/' + util.ucFirst(item)
 759                     }
 760                 ).then(function(data, textStatus) {
 761                     var OOUIextraItem = $('#extraItem' + gecchartid).data('ooui-elem'),
 762                         pages = data.query.pages;
 763                     if (textStatus !== 'success') {
 764                         alert('An error occured while loading ' + item);
 765                         mw.log(data);
 766                     }
 767                     var matches = []
 768                     var pageMissing = false;
 769                     if (pages[-1]) {
 770                         pageMissing = true;
 771                     } else {
 772                         var exchangeData = pages[Object.keys(pages)[0]]
 773                                             .revisions[0]['*'];
 774                         matches = exchangeData.match(/itemId\D*(\d*)/);
 775                         if (matches.length !== 2) {
 776                             pageMissing = true;
 777                         }
 778                     }
 779                     // page not found
 780                     if (pageMissing) {
 781                         if (OOUIextraItem.getValue().length) {
 782                             alert('The item ' + item + ' doesn\'t exist on our Grand Exchange database.');
 783                             OOUIextraItem.setDisabled(false).setValue('');
 784                             return false;
 785                         }
 786 
 787                         _GEC.AILoaded.push(false);
 788 
 789                         if (
 790                             _GEC.AIData.length &&
 791                             _GEC.AIQueue.length == _GEC.AILoaded.length
 792                         ) {
 793                             loadChartsQueueComplete(gecchartid);
 794                         } else if (!_GEC.AIData.length) {
 795                             setChartRange(gecchartid);
 796                         }
 797 
 798                         OOUIextraItem.setDisabled(false).setValue('');
 799 
 800                         return false;
 801                     }
 802 
 803                     var itemId = matches[1];
 804                     return $.getJSON("https://api.weirdgloop.org/exchange/history/" + gameVersion + "/all?compress=true&id=" + itemId);
 805                 }).then(function(data, textStatus) {
 806                     if (data === false) return;
 807                     _GEC.AIData.push({
 808                         name: item,
 809                         data: Object.values(data)[0],
 810                         id: item,
 811                         gecchartid: gecchartid,
 812                         lineWidth: 2
 813                     });
 814 
 815                     _GEC.AILoaded.push(item);
 816 
 817                     if (getSeriesIndex(gecchartid, 'average') !== -1) {
 818                         _GEC['chart' + gecchartid]
 819                             .series[getSeriesIndex(gecchartid, 'average')]
 820                             .remove();
 821                     }
 822 
 823                     if (_GEC.AIQueue.length === _GEC.AILoaded.length) {
 824                         // This is always true when only 1 item is being loaded.
 825                         loadChartsQueueComplete(gecchartid);
 826                     }
 827                 })
 828 
 829                 _GEC.AIQueue.push({item: item.toLowerCase(), chart: gecchartid});
 830 
 831                 // @todo when does this happen
 832                 /* This happens when there are no further items added to the charts, i.e. when the original item is the only one.
 833                    This is indeed a flawed test, since it won't work on GEMW/C, where there is no original item in the chart.
 834                    This should be replaced with another test that also works on GEMW/C.
 835                  */
 836             } else if (
 837                 $addedItems.html().match(/^\s*$/) ||
 838                 (
 839                     conf.wgPageName == 'RuneScape:Grand_Exchange_Market_Watch/Chart' &&
 840                     $addedItems.find('a').length === 1
 841                 )
 842             ) {
 843                 id = (i === 'popup' ? $('#GEchartpopup').attr('data-chartid') : i);
 844                 getData(id, false, false, i, function(data) {
 845                     series = _GEC['chart' + i].series;
 846                     seriesIndex = getSeriesIndex(i, 'average');
 847 
 848                     //remove an average line if it already exists
 849                     if (seriesIndex !== -1) {
 850                         series[seriesIndex].remove();
 851                     }
 852 
 853                     //add average line when there is only 1 item in the chart
 854                     _GEC['chart' + i].addSeries(data[0][1]);
 855                 });
 856             }
 857         },
 858 
 859         /**
 860          * <doc>
 861          *
 862          * @param c {number|string}
 863          */
 864         loadQueueComplete: function (cin, addeditembyscript) {
 865             var cnum = (cin !== 'popup'),  //if cin is a number, we're probably at initial load of one/many charts on a page, so we need to iterate over the entire queue
 866                 c = cnum ? _GEC.AIQueue.length : cin, //if not a number, its almost certainly 'popup', for which we only need to reload the popup
 867                 id,
 868                 chartdata,
 869                 isSmall = [],
 870                 isMedium = [],
 871                 data = [],
 872                 i,
 873                 index,
 874                 itemhash,
 875                 $addedItems,
 876                 iname,
 877                 hadBlank;
 878 
 879             if (cnum) { //this structure repeats throughout the method: if cnum then loop else do once. probably a better way to do this
 880                 for (i = 0; i < c; i++) {
 881                     isSmall[i] = $('#GEdatachart' + i).hasClass('smallChart');
 882                     isMedium[i] = $('#GEdatachart' + i).hasClass('mediumChart');
 883                 }
 884             } else {
 885                 isSmall = $('#GEdatachart' + c).hasClass('smallChart');
 886                 isMedium = $('#GEdatachart' + c).hasClass('mediumChart');
 887             }
 888 
 889             if (cnum) {
 890                 for (i = 0; i < c; i++) {
 891                     if (getSeriesIndex(_GEC.AIQueue[i].chart, volumeLabel) !== -1) {
 892                         id = i === 'popup' ? $('#GEchartpopup').attr('data-chartid') : i;
 893                         getData(id, true, undefined, undefined, function(data) {
 894                             data[1].title.text = 'Price history';
 895 
 896                             reloadChart(i, {
 897                                 series: data[0],
 898                                 yAxis: data[1]
 899                             });
 900                         });
 901                     }
 902                 }
 903             } else {
 904                 if (getSeriesIndex(c, volumeLabel) !== -1) {
 905                     id = c === 'popup' ? $('#GEchartpopup').attr('data-chartid') : c;
 906                     getData(id, true, undefined, undefined, function(data) {
 907                         data[1].title.text = 'Price history';
 908                         reloadChart(c, {
 909                             series: data[0],
 910                             yAxis: data[1]
 911                         });
 912                     });
 913                 }
 914             }
 915 
 916             for (i = 0; i < _GEC.AIData.length; i++) {
 917                 index = -1;
 918                 for (var i2 = 0; i2 < _GEC.AIQueue.length; i2++) {
 919                     if (_GEC.AIQueue[i2].item === (_GEC.AIData[i] || {name: ''}).name.toLowerCase()) {
 920                         index = i2;
 921                         break;
 922                     }
 923                 }
 924                 data[index !== -1 ? index : data.length] = _GEC.AIData[i];
 925             }
 926 
 927             // @todo should this be `Array.isArray`
 928             //       or should it default to `{}`
 929             // @todo test if isSmall is needed in the conditional
 930             if (cnum) {
 931                 for (i = 0; i < c; i++) {
 932                     if (data[i] === undefined) continue;
 933                     if ((isSmall[data[i].gecchartid] && isMedium[data[i].gecchartid]) && typeof Array.isArray(_GEC.addedData[data[i].gecchartid])) {
 934                         _GEC.addedData[data[i].gecchartid] = [];
 935                     }
 936                 }
 937             } else {
 938                 if ((isSmall || isMedium) && typeof Array.isArray(_GEC.addedData[data[c].gecchartid])) {
 939                     _GEC.addedData[data[c].gecchartid] = [];
 940                 }
 941 
 942             }
 943 
 944             for (i = 0; i < data.length; i++) {
 945                 if (data[i]) {
 946                     _GEC['chart' + data[i].gecchartid].addSeries(data[i]);
 947                 }
 948 
 949                 if (cnum && isSmall[data[i].gecchartid]) {
 950                     _GEC.addedData[data[i].gecchartid][i] = data[i];
 951                 }
 952             }
 953 
 954             if (cnum) {
 955                 for (i = 0; i < c; i++) {
 956                     setChartExtremes(data[i].gecchartid);
 957                     $('#extraItem' + data[i].gecchartid).data('ooui-elem').setDisabled(false).setValue('');
 958                 }
 959             } else {
 960                 setChartExtremes(c);
 961                 $('#extraItem' + c).data('ooui-elem').setDisabled(false).setValue('');
 962             }
 963             itemhash = (location.hash.match(/#i=[^#]*/) || [])[0] || location.hash + '#i=';
 964             $addedItems = $('#addedItems' + c);
 965 
 966             for (i = 0; i < data.length; i++) {
 967                 if (!data[i]) {
 968                     continue;
 969                 }
 970 
 971                 iname = data[i].name;
 972 
 973                 if (!$addedItems.text().trim()) {
 974                     $addedItems.append(
 975                         'Remove items from graph: ',
 976                         $('<a>')
 977                             .attr({
 978                                 href: 'javascript:removeGraphItem("' + iname + '","' + c + '")',
 979                                 'data-item': iname
 980                             })
 981                             .text(iname)
 982                     );
 983                     itemhash = '#i=' + iname;
 984                 } else {
 985                     $addedItems.append(
 986                         ', ',
 987                         $('<a>')
 988                             .attr({
 989                                 href: 'javascript:removeGraphItem("' + iname + '","' + c + '")',
 990                                 'data-item': iname
 991                             })
 992                             .text(iname)
 993                     );
 994                     itemhash += ',' + iname;
 995                 }
 996             }
 997 
 998             if (location.hash.match(/#i=/)) {
 999                 itemhash = location.hash
1000                     .replace(/#i=[^#]*/, itemhash)
1001                     .replace(/ /g, '_');
1002             } else {
1003                 itemhash = location.hash + itemhash;
1004             }
1005 
1006             if (
1007                 (
1008                     conf.wgNamespaceNumber == 112 && conf.wgTitle.split('/')[1] == 'Data' ||
1009                     conf.wgPageName == 'RuneScape:Grand_Exchange_Market_Watch/Chart'
1010                 ) &&
1011                 itemhash.replace('#i=', '').length
1012             ) {
1013                 location.hash = itemhash;
1014             }
1015 
1016             _GEC.AIQueue = [];
1017             _GEC.AILoaded = [];
1018             _GEC.AIData = [];
1019 
1020             if (cnum) {
1021                 for (i = 0; i < c; i++) {
1022                     hadBlank = removeGraphItem('Blank', data[i].gecchartid);
1023 
1024                     if (hadBlank) {
1025                         setChartRange(data[i].gecchartid);
1026                     }
1027                 }
1028             } else {
1029                 hadBlank = removeGraphItem('Blank', c);
1030 
1031                 if (hadBlank) {
1032                     setChartRange(c);
1033                 }
1034             }
1035         },
1036 
1037         /**
1038          * <doc>
1039          *
1040          * @param c {number|string}
1041          *
1042          * @return {boolean}
1043          */
1044         setRange: function (c) {
1045             var zoom = parseInt((location.hash.match(/#z=([^#]*)/) || [])[1], 10);
1046             zoom = zoom && zoom <= 6 && zoom >= 0 ? zoom - 1 : (zoom === 0 ? 0 : 2);
1047             var hash = location.hash;
1048             var hasT = (conf.wgNamespaceNumber === 112 && conf.wgTitle.split('/')[1] === 'Data') || conf.wgPageName === 'RuneScape:Grand_Exchange_Market_Watch/Chart';
1049             if (typeof c === 'number' && (hasT && !hash.match('#t=') || !hasT)) {
1050                 $('#GEdatachart' + c + ' .zoomButton').eq(zoom).click();
1051                 return true;
1052             }
1053 
1054             var timespan = decodeURIComponent((hash.match(/#t=([^#]*)/) || [])[1] || '')
1055                 .replace(/_/g, ' ')
1056                 .split(',');
1057             var dates = [new Date(timespan[0]), new Date(timespan[1])];
1058             var d = new Date(timespan[0]);
1059             var extremes = _GEC['chart' + c].xAxis[0].getExtremes();
1060 
1061             if (dates[0] !== 'Invalid Date' && dates[1] === 'Invalid Date' && typeof zoom === 'number') {
1062                 var button = _GEC['chart' + c].rangeSelector.buttonOptions[zoom];
1063 
1064                 if (button.type === 'month') {
1065                     d.setMonth(d.getMonth() + button.count);
1066                 } else if (button.type === 'year') {
1067                     d.setYear(d.getFullYear() + button.count);
1068                 } else if (button.type === 'all') {
1069                     d = new Date(extremes.dataMax);
1070                 }
1071 
1072                 dates[1] = d;
1073             }
1074 
1075             if (dates[0] !== 'Invalid Date' && dates[1] !== 'Invalid Date') {
1076                 _GEC['chart' + c].xAxis[0].setExtremes(dates[0].getTime(), dates[1].getTime());
1077                 return true;
1078             }
1079 
1080             return false;
1081         },
1082 
1083         /**
1084          * <doc>
1085          *
1086          * @param c {number|string}
1087          * @param change {object}
1088          */
1089         reload: function (c, change) {
1090             var options = _GEC['chart' + c].options;
1091 
1092             if (!options) {
1093                 // @todo do we need to return `false` here
1094                 // @todo when does this happen
1095                 return false;
1096             }
1097 
1098             $.extend(options, change);
1099             _GEC['chart' + c] = new Highcharts.StockChart(options);
1100         },
1101 
1102         /**
1103          * <doc>
1104          *
1105          * @param item {string}
1106          * @param c {number|string}
1107          *
1108          * @return {boolean}
1109          */
1110         removeItem: function (item, c) {
1111             var series = _GEC['chart' + c].series,
1112                 id,
1113                 i,
1114                 newhash,
1115                 data;
1116 
1117             // find the item we want to remove
1118             for (i = 0; i < series.length; i++) {
1119                 if (series[i].name.match(item)) {
1120                     id = i;
1121                 }
1122             }
1123 
1124             // @todo when does this happen
1125             //       when we can't find the item?
1126             if (typeof id !== 'number') {
1127                 return false;
1128             }
1129 
1130             // remove item from url hash
1131             newhash = location.hash
1132                 .replace(/_/g, ' ')
1133                 .replace(new RegExp('(#i=[^#]*),?' + item, 'i'), '$1')
1134                 .replace(/,,/g, ',')
1135                 .replace(/,#/g, '#')
1136                 .replace(/#i=,/g, '#i=')
1137                 .replace(/#i=($|#)/, '$1')
1138                 .replace(/ /g, '_');
1139 
1140             if (newhash.replace('#i=', '').length) {
1141                 location.hash = newhash;
1142             } else if (location.hash.length) {
1143                 location.hash = '';
1144             }
1145 
1146             // remove the item from the chart
1147             series[id].remove();
1148             // reset extremes?
1149             setChartExtremes(c);
1150 
1151             // @todo can we cache #addedItems somehow
1152             // remove item from list at top of graph
1153             $('#addedItems' + c + ' [data-item="' + item + '"]').remove();
1154             // cleanup list
1155             $('#addedItems' + c).html(
1156                 $('#addedItems' + c)
1157                     .html()
1158                     .replace(/, , /g, ', ')
1159                     .replace(/, $/, '')
1160                     .replace(': , ', ': ')
1161             );
1162 
1163             // if the list is empty show average, volume and item stats again
1164             if (!$('#addedItems' + c + ' [data-item]').length) {
1165                 $('#addedItems' + c).empty();
1166                 id = c == 'popup' ? $('#GEchartpopup').attr('data-chartid') : c;
1167                 data = getData(id, false, false, 'popup', function(data) {
1168                     reloadChart(c, {
1169                         series: data[0],
1170                         yAxis: data[1]
1171                     });
1172                 });
1173 
1174             }
1175 
1176             return true;
1177         },
1178 
1179         /**
1180          * <doc>
1181          *
1182          * @param i {number|string}
1183          */
1184         popup: function () {
1185         },
1186 
1187         /**
1188          * <doc>
1189          *
1190          * @param i
1191          */
1192         setExtremes: function (i) {
1193             var ch = _GEC['chart' + i],
1194                 exts = _GEC['chart' + i].yAxis[0].getExtremes();
1195 
1196             if (
1197                 exts.dataMin * 0.95 !== exts.userMin ||
1198                 exts.dataMax * 1.05 !== exts.userMax
1199             ) {
1200                 ch.yAxis[0].setExtremes(exts.dataMin * 0.95, exts.dataMax * 1.05);
1201 
1202                 if (ch.yAxis[2]) {
1203                     exts = ch.yAxis[1].getExtremes();
1204                     ch.yAxis[1].setExtremes(0, exts.dataMax * 1.05);
1205                 }
1206             }
1207 
1208             if (i === 'popup') {
1209                 // @todo use onclick event
1210                 $('#GEPermLink' + i).data('ooui-elem').setData(chartPermLinkUrl(i));
1211             }
1212         },
1213 
1214         /**
1215          * <doc>
1216          *
1217          * @param c {number|string}
1218          * @param isSmall {boolean}
1219          * @param avginput {number|string} (optional)
1220          *        number component of input element used for altering the average interval
1221          *        when the interval is in days
1222          *        when is this different to `c`?
1223          *
1224          * @return {array} 2 item array containing X and Y respectively
1225          *                 @todo expand on what X and Y are
1226          */
1227         getData: function () {
1228         }
1229     },
1230 
1231     // map old functions to new locations until uses are fixed
1232     getSeriesIndex = chart.getSeriesIndex,
1233     chartPermLinkUrl = chart.permLinkUrl,
1234     addItem = chart.addItem,
1235     removeGraphItem = chart.removeItem,
1236     reloadChart = chart.reload,
1237     setChartRange = chart.setRange,
1238     setChartExtremes = chart.setExtremes,
1239     loadChartsQueueComplete = chart.loadQueueComplete;
1240 // popupChart = chart.popup;
1241 // getData = chart.getData;
1242 
1243 // chart-related general functions
1244 
1245 function popupChart(i) {
1246     var $popup = $('#GEchartpopup'),
1247         $overlay = $('#overlay'),
1248         options,
1249         data,
1250         n;
1251 
1252     if (!$popup.length) {
1253         return false;
1254     }
1255 
1256     if ($overlay.length) {
1257         $overlay.toggle();
1258     } else {
1259         $popup.before(
1260             $('<div>')
1261                 .attr('id', 'overlay')
1262                 .css('display', 'block')
1263         );
1264         $overlay = $('#overlay');
1265     }
1266 
1267     $overlay.on('click', function () {
1268         popupChart(false);
1269     });
1270 
1271     if (typeof i === 'number') {
1272         $(document).keydown(function (e) {
1273             // Esc
1274             if (e.which === 27) {
1275                 popupChart(false);
1276             }
1277         });
1278     } else {
1279         // @todo only remove our event
1280         $(document).off('keydown');
1281     }
1282 
1283     if (typeof i === 'boolean' && !i) {
1284         $popup.hide();
1285         $('#addedItemspopup').html('');
1286     } else {
1287         $popup.toggle();
1288     }
1289 
1290     if (typeof i === 'number' && $popup.attr('data-chartid') !== i) {
1291         $('#averagepopup').data('ooui-elem').setValue(_GEC.average);
1292         $popup.attr('data-chartid', i);
1293 
1294         options = {};
1295         getData(i, false, false, 'popup', function(data) {
1296             var dataList = data[0];
1297             var yAxis = data[1];
1298             // @todo can this be replaced with $.extend?
1299             // @todo what is this supposed to do?
1300             util.cloneObj(_GEC['chart' + i].options, options);
1301 
1302             options.chart.renderTo = 'GEpopupchart';
1303             options.legend.enabled = true;
1304             options.title.text = 'Grand Exchange Market Watch';
1305             options.title.style.fontSize = '18px';
1306             options.subtitle.text = options.series[0].name;
1307             options.subtitle.style.fontSize = '15px;';
1308             options.chart.zoomType = '';
1309             options.rangeSelector.enabled = true;
1310             options.rangeSelector.inputBoxStyle.display = 'block';
1311             options.plotOptions.series.enableMouseTracking = true;
1312             options.tooltip.enabled = true;
1313             options.navigator.enabled = true;
1314             options.credits.enabled = false;
1315             options.series = [{}];
1316             options.series = _GEC.addedData[i] ? [dataList[0]] : dataList;
1317             options.yAxis = yAxis;
1318 
1319             _GEC.chartpopup = new Highcharts.StockChart(options);
1320 
1321             if (_GEC.addedData[i]) {
1322                 for (n = 0; n < _GEC.addedData[i].length; n++) {
1323                     _GEC.chartpopup.addSeries(_GEC.addedData[i][n]);
1324                 }
1325             }
1326 
1327             setChartExtremes('popup');
1328             _GEC.chartpopup.redraw();
1329             $('#GEPermLinkpopup').data('ooui-elem').setData(chartPermLinkUrl('popup'));
1330         });
1331     }
1332 }
1333 
1334 function rg(num) {
1335     var colour = 'red';
1336 
1337     if (num > 0) {
1338         colour = 'green';
1339     } else if (num === 0) {
1340         colour = 'blue';
1341     }
1342 
1343     return colour;
1344 }
1345 
1346 function getData(cin, isSmall, isMedium, avginput, callback) {
1347     var c = cin === 'popup' ? $('#GEchartpopup').attr('data-chartid') : cin,
1348         $dataPrices = $('#GEdataprices' + c),
1349         dataItem = $dataPrices.attr('data-item'),
1350         dataItemId = $dataPrices.attr('data-itemId') || ('GE ' + dataItem),
1351         isIndexChart = /index/i.test(dataItem),
1352         itemName = dataItem || conf.wgTitle.split('/')[0],
1353         ch = _GEC['chart' + c],
1354         chartLoaded = !!(ch && ch.series && ch.series.length),
1355         prices = [],
1356         i,
1357         data = [],
1358         thisprice,
1359         volumes = [],
1360         dataList,
1361         inputAvg,
1362         newhash,
1363         yAxis,
1364         chartPageData;
1365 
1366     // happens when the first chart isSmall
1367     // and the average input id is actually the popup chart
1368     // the chart's id is popup, but the input's id is 0
1369     avginput = avginput || cin;
1370 
1371     var pricesToDataList = function(prices) {
1372     	_GEC.urlCache[url] = prices;
1373     	prices = Object.values(prices)[0];
1374         var volumeMultiplier = isOSRS ? 1 : 1000000
1375         for (i = 0; i < prices.length; i++) {
1376             data.push([
1377                 // time
1378                 prices[i][0],
1379                 // @todo should this be parseInt?
1380                 // price
1381                 prices[i][1]
1382             ]);
1383 
1384             if (prices[i][2] && !isSmall) {
1385                 volumes.push([
1386                     // time
1387                     prices[i][0],
1388                     // volume
1389                     // volumes are in millions
1390                     prices[i][2] * volumeMultiplier
1391                 ]);
1392             }
1393         }
1394 
1395         // datalist's elements are essentially each line on the chart
1396         // so price, 30-day-average and volume
1397         dataList = [{
1398             name: itemName,
1399             data: data,
1400             lineWidth: isSmall ? 2 : 3
1401         }];
1402 
1403         if (itemName.toLowerCase() === 'blank' && !chartLoaded) {
1404             dataList[0].color = '#000000';
1405         }
1406 
1407         if (!isSmall && !isMedium && (itemName.toLowerCase() !== 'blank' || chartLoaded)) {
1408             inputAvg = $('#average' + avginput).data('ooui-elem').getNumericValue();
1409 
1410             // @todo should this be isNaN?
1411             if (inputAvg) {
1412                 newhash = location.hash
1413                     .replace(/#a=[^#]*|$/, '#a=' + inputAvg)
1414                     .replace(/ /g, '_');
1415 
1416                 if (newhash.length) {
1417                     location.hash = newhash;
1418                 }
1419             }
1420 
1421             inputAvg = inputAvg || 30;
1422             dataList.push({
1423                 name: inputAvg + '-day average',
1424                 data: util.avg(data, inputAvg, isIndexChart ? 2 : 0),
1425                 lineWidth: 2,
1426                 dashStyle: 'shortdash',
1427             });
1428 
1429             if (volumes.length >= 10) {
1430                 dataList.push({
1431                     name: volumeLabel,
1432                     data: volumes,
1433                     type: 'area',
1434                     color: '#cc8400',
1435                     fillColor: {
1436                         linearGradient: {
1437                             x1: 0,
1438                             y1: 0,
1439                             x2: 0,
1440                             y2: 1
1441                         },
1442                         stops: [
1443                             [0, '#ffa500'],
1444                             [1, 'white']
1445                         ],
1446                     },
1447                     // display on separate y-axis
1448                     yAxis: 1,
1449                 });
1450             }
1451         }
1452 
1453         // create y-axis for price data
1454         yAxis = {
1455             title: {
1456                 text: isSmall ? null : (isIndexChart ? 'Index history' : 'Price history'),
1457                 offset: 60,
1458                 rotation: 270,
1459                 style: {
1460                     color: 'black',
1461                     fontSize: '12px',
1462                 },
1463             },
1464             opposite: false,
1465             labels: {
1466                 align: 'right',
1467                 x: -8,
1468                 y: 4,
1469             },
1470             allowDecimals: false,
1471             // 1 coin
1472             minTickInterval: 1,
1473             showLastLabel: 1,
1474             lineWidth: 1,
1475             lineColor: '#E0E0E0'
1476         };
1477 
1478         // volume data is plotted on a seperate y-axis
1479         if (volumes.length >= 10 && !isSmall) {
1480             // set height to allow room for second y-axis
1481             yAxis.height = 200;
1482 
1483             // convert to array and add volume data
1484             yAxis = [yAxis, {
1485                 title: {
1486                     text: volumeLabel,
1487                     offset: 60,
1488                     rotation: 270,
1489                     style: {
1490                         color: 'black',
1491                         fontSize: '12px'
1492                     }
1493                 },
1494                 opposite: false,
1495                 labels: {
1496                     align: 'right',
1497                     x: -8,
1498                     y: 4,
1499                 },
1500                 showEmpty: 0,
1501                 showLastLabel: 1,
1502                 offset: 0,
1503                 lineWidth: 1,
1504                 lineColor: '#E0E0E0',
1505                 height: 50,
1506                 top: 325,
1507                 min: 0
1508             }];
1509         }
1510         return [dataList, yAxis];
1511     }
1512 	
1513 	var isPopup = !isSmall && !isMedium;
1514 	var dataType = isPopup ? 'all' : 'sample';
1515     var url = "https://api.weirdgloop.org/exchange/history/" + gameVersion + "/" + dataType + "?compress=true&id=" + dataItemId;
1516     var pricesPromise;
1517     if (chartLoaded && itemName.toLowerCase() === 'blank') {
1518         chartPageData = _GEC['chart' + c].series[
1519             getSeriesIndex(c, $('#addedItems' + c).find('a').data('item'))
1520             ];
1521 
1522         for (i = 0; i < chartPageData.xData.length; i++) {
1523             prices.push(chartPageData.xData[i] + ':' + chartPageData.yData[i]);
1524         }
1525         pricesPromise = Promise.resolve(prices);
1526     } else {
1527         if (_GEC.urlCache[url]) {
1528             return callback(pricesToDataList(_GEC.urlCache[url]))
1529         }
1530         $.getJSON(url).then(pricesToDataList).then(callback)
1531     }
1532 
1533 }
1534 
1535 $(self.deps);