MediaWiki:Gadget-GECharts-sandbox.js: Difference between revisions

From RuneRealm Wiki
Jump to navigation Jump to search
Content added Content deleted
(Created page with "/** * Grand Exchange Charts * Displays price data of item(s) in a chart * * Highstock docs <https://api.highcharts.com/highstock/> * Highstock change log <https://www.highcharts.com/blog/changelog/#highstock> * * @author Joeytje50 * @author Cqm * @author JaydenKieran * @author Gaz Lloyd * * @todo move Highcharts to a core ResourceLoader module * * @todo use a consistent variable for the chart id * currently it's one of c, i or id * @todo remove scr...")
 
No edit summary
Tag: Reverted
Line 82: Line 82:
);
);
}
}
mw.loader.using(['mediawiki.util', 'mediawiki.api', 'rs.highcharts', 'rs.highcharts-annotations', 'oojs-ui-core', 'oojs-ui.styles.icons-media'], self.init);
mw.loader.using(['mediawiki.mw.util', 'mediawiki.api', 'rs.highcharts', 'rs.highcharts-annotations', 'oojs-ui-core', 'oojs-ui.styles.icons-media'], self.init);
},
},


Line 482: Line 482:
* General helper methods
* General helper methods
*/
*/
util = {
mw.util = {
/**
/**
* <doc>
* <doc>
Line 550: Line 550:
// don't modify the time (y-coord)
// don't modify the time (y-coord)
arr[i][0],
arr[i][0],
Math.round((util.sum(list) / list.length) * round) / round
Math.round((mw.util.sum(list) / list.length) * round) / round
]);
]);
}
}
Line 560: Line 560:
/**
/**
* Finds the sum of numbers in an array
* Finds the sum of numbers in an array
* Only called by `util.avg`
* Only called by `mw.util.avg`
*
*
* @param arr {array} Array of number to find the sum of
* @param arr {array} Array of number to find the sum of
Line 782: Line 782:
rvprop: 'content',
rvprop: 'content',
format: 'json',
format: 'json',
titles: 'Module:Exchange/' + util.ucFirst(item)
titles: 'Module:Exchange/' + mw.util.ucFirst(item)
}
}
).then(function(data, textStatus) {
).then(function(data, textStatus) {
var OOUIextraItem = $('#extraItem' + gecchartid).data('ooui-elem'),
var OOUIextraItem = $('#extraItem' + gecchartid).data('ooui-elem'),
pages = data.query.pages;
mw.pages = data.query.mw.pages;
if (textStatus !== 'success') {
if (textStatus !== 'success') {
alert('An error occured while loading ' + item);
alert('An error occured while loading ' + item);
Line 793: Line 793:
var matches = []
var matches = []
var pageMissing = false;
var pageMissing = false;
if (pages[-1]) {
if (mw.pages[-1]) {
pageMissing = true;
pageMissing = true;
} else {
} else {
var exchangeData = pages[Object.keys(pages)[0]]
var exchangeData = mw.pages[Object.keys(mw.pages)[0]]
.revisions[0]['*'];
.revisions[0]['*'];
matches = exchangeData.match(/itemId\D*(\d*)/);
matches = exchangeData.match(/itemId\D*(\d*)/);
Line 831: Line 831:
}).then(function(data, textStatus) {
}).then(function(data, textStatus) {
if (data === false) return;
if (data === false) return;
$.getJSON(mw.util.getUrl('Module:Exchange/'+util.ucFirst(item)+'/annotations.json', {action:'raw', 'ctype':'application/json'})).then(
$.getJSON(mw.mw.util.getUrl('Module:Exchange/'+mw.util.ucFirst(item)+'/annotations.json', {action:'raw', 'ctype':'application/json'})).then(
function(annodata){return annodata},
function(annodata){return annodata},
function(){return []} //handles the page not existing
function(){return []} //handles the page not existing
Line 1,332: Line 1,332:
// @todo can this be replaced with $.extend?
// @todo can this be replaced with $.extend?
// @todo what is this supposed to do?
// @todo what is this supposed to do?
util.cloneObj(_GEC['chart' + i].options, options);
mw.util.cloneObj(_GEC['chart' + i].options, options);


options.chart.renderTo = 'GEpopupchart';
options.chart.renderTo = 'GEpopupchart';
Line 1,399: Line 1,399:
labelUrl = ad.link;
labelUrl = ad.link;
} else {
} else {
labelUrl = mw.util.getUrl(ad.link);
labelUrl = mw.mw.util.getUrl(ad.link);
}
}
label = '<a href="'+labelUrl+'" target="_blank">'+ad.title+'</a>';
label = '<a href="'+labelUrl+'" target="_blank">'+ad.title+'</a>';
Line 1,518: Line 1,518:
dataList.push({
dataList.push({
name: inputAvg + '-day average',
name: inputAvg + '-day average',
data: util.avg(data, inputAvg, isIndexChart ? 2 : 0),
data: mw.util.avg(data, inputAvg, isIndexChart ? 2 : 0),
lineWidth: 2,
lineWidth: 2,
dashStyle: 'shortdash',
dashStyle: 'shortdash',
Line 1,616: Line 1,616:
var dataType = isPopup ? 'all' : 'sample';
var dataType = isPopup ? 'all' : 'sample';
var url = "https://api.weirdgloop.org/exchange/history/" + gameVersion + "/" + dataType + "?compress=true&id=" + dataItemId;
var url = "https://api.weirdgloop.org/exchange/history/" + gameVersion + "/" + dataType + "?compress=true&id=" + dataItemId;
var annoUrl = mw.util.getUrl('Module:Exchange/'+itemName+'/annotations.json', {action:'raw', 'ctype':'application/json'});
var annoUrl = mw.mw.util.getUrl('Module:Exchange/'+itemName+'/annotations.json', {action:'raw', 'ctype':'application/json'});
var pricesPromise;
var pricesPromise;
if (chartLoaded && itemName.toLowerCase() === 'blank') {
if (chartLoaded && itemName.toLowerCase() === 'blank') {

Revision as of 00:10, 17 October 2024

/**
 * Grand Exchange Charts
 * Displays price data of item(s) in a chart
 *
 * Highstock docs <https://api.highcharts.com/highstock/>
 * Highstock change log <https://www.highcharts.com/blog/changelog/#highstock>
 *
 * @author Joeytje50
 * @author Cqm
 * @author JaydenKieran
 * @author Gaz Lloyd
 * 
 * @todo move Highcharts to a core ResourceLoader module
 *
 * @todo use a consistent variable for the chart id
 *       currently it's one of c, i or id
 * @todo remove script URLs (javascript:func) in favour of onclick events
 *       may require attaching the events after the some parts have loaded
 * @todo fix averages
 */

/*global jQuery, mediaWiki, rswiki, Highcharts, wgPageName, wgTitle, wgNamespaceNumber */

'use strict';

/**
 * Cache mw.config variables
 */
var conf = mw.config.get([
        'wgNamespaceNumber',
        'wgPageName',
        'wgTitle',
        'wgSiteName'
    ]),

    // Are we on OSRS? Impacts selectors and volume labels / multipliers
    isOSRS = conf.wgSiteName == "Old School RuneScape Wiki",

    // Volume label depends on which wiki we're on
    volumeLabel = "Daily volume",
    gameVersion = isOSRS ? 'osrs' : 'rs',

    /**
     * <doc>
     *
     * @todo replace `_GEC` wih this
     */
    gec = {},

    // @todo document each of these
    _GEC = {
        AIQueue: [],
        AILoaded: [],
        AIData: [],
        addedData: [],
        average: parseInt((location.hash.match(/#a=([^#]*)/) || [])[1], 10) || '',
        urlCache: {},
        annotationsVisible: true
    },

    /**
     * Startup methods
     */
    self = {
        /**
         * Loads and implements any required dependencies
         */
        deps: function () {
            if (!mw.loader.getState('rs.highcharts')) {
                mw.loader.implement(
                    'rs.highcharts',
                    ['https://code.highcharts.com/stock/highstock.js'],
                    {}, {}
                );
            }

            if (!mw.loader.getState('rs.highcharts-annotations')) {
                mw.loader.implement(
                    'rs.highcharts-annotations',
                    ['https://code.highcharts.com/modules/annotations.js'],
                    {}, {}
                );
            }
            mw.loader.using(['mediawiki.mw.util', 'mediawiki.api', 'rs.highcharts', 'rs.highcharts-annotations', 'oojs-ui-core', 'oojs-ui.styles.icons-media'], self.init);
        },

        /**
         * Initial loading function
         */
        init: function (req) {
            window.Highcharts = req('rs.highcharts');
            req('rs.highcharts-annotations')(window.Highcharts); //load the annotations module, shouldn't need to store the return value anywhere
            (function () {
                var newhash = location.hash
                    .replace(/\.([0-9a-f]{2})/gi, function (_, first) {
                        return String.fromCharCode(parseInt(first, 16));
                    })
                    .replace(/ /g, '_');
                if (newhash && newhash.match(/#[aiz]=/)) {
                    location.hash = newhash;
                }
            }());

            $('.GEdatachart').attr('id', function (c) {
                return 'GEdatachart' + c;
            });
            $('.GEdataprices').attr('id', function (c) {
                return 'GEdataprices' + c;
            });
            $('.GEChartBox').each(function (c) {
                $(this).find('.GEChartItems').attr('id', 'GEChartItems' + c);
            });

            Highcharts.setOptions({
                lang: {
                    // @todo can this be done with CSS?
                    resetZoom: null,
                    numericSymbols: ['K', 'M', 'B', 'T', 'Qd', 'Qt'],
                }
            });

            // globals to maintain javascript hrefs
            window._GEC = _GEC;
            window.popupChart = popupChart;
            window.addItem = chart.addItem;
            window.removeGraphItem = chart.removeItem;

            self.buildPopup();
            self.setupCharts();
        },

        /**
         * <doc>
         */
        makeOOUI: function (c) {
            var averageRangeInput, addItemInput, submitButton, resetButton, annotationsToggle, fieldset, permalink;
            averageRangeInput = new OO.ui.NumberInputWidget({
                min: 1,
                value: 30,
                id: 'average' + c
            });
            averageRangeInput.$element.data('ooui-elem', averageRangeInput);
            addItemInput = new OO.ui.TextInputWidget({
                id: 'extraItem' + c
            });
            addItemInput.$element.data('ooui-elem', addItemInput);
            submitButton = new OO.ui.ButtonInputWidget({
                label: 'Submit',
                flags: ['primary', 'progressive']
            });
            resetButton = new OO.ui.ButtonInputWidget({
                label: 'Reset'
            });
            permalink = new OO.ui.ButtonInputWidget({
                label: 'Permanent link',
                title: 'Permanent link to the current chart settings and items. Right click to copy the url.',
                id: 'GEPermLink' + c
            });
            permalink.$element.data('ooui-elem', permalink);
            permalink.setData('/w/RuneScape:Grand_Exchange_Market_Watch/Chart');
            permalink.on('click', function () {
                window.open(permalink.getData(), '_blank');
            });

            averageRangeInput.on('enter', function () {
                addItem(c);
            });
            addItemInput.on('enter', function () {
                addItem(c);
            });
            submitButton.on('click', function () {
                addItem(c);
            });

            resetButton.on('click', function () {
                addItemInput.setValue('');
                averageRangeInput.setValue(30);
            });
            
            annotationsToggle = new OO.ui.ToggleSwitchWidget({
                value: true
            });
            annotationsToggle.on('change', function(){
            	window._GEC.annotationsVisible = annotationsToggle.getValue();
            	for (var i = 0; i<window._GEC['chart'+c].annotations.length; i++) {
            		window._GEC['chart'+c].annotations[i].setVisibility(window._GEC.annotationsVisible);
            	}
            });

            fieldset = new OO.ui.FieldsetLayout();
            fieldset.addItems([
                new OO.ui.FieldLayout(averageRangeInput, {label: 'Average (days)'}),
                new OO.ui.FieldLayout(addItemInput, {label: 'Add new item'}),
                new OO.ui.HorizontalLayout({items: [
                    submitButton,
                    resetButton,
                    permalink
                ]}),
                new OO.ui.FieldLayout(annotationsToggle, {label: 'Show annotations?'})
            ]);

            fieldset.$element.css('width', '50%');
            return fieldset.$element;
        },
        buildPopup: function () {
            var close;
            close = new OO.ui.ButtonWidget({
                icon: 'close'
            });
            close.on('click', function () {
                popupChart(false);
            });


            $('body').append(
                $('<div>')
                    .attr('id', 'GEchartpopup')
                    .css('display', 'none')
                    .append(
                        $('<div>')
                            .attr('id', 'closepopup')
                            .append(close.$element),
                        self.makeOOUI('popup'),
                        $('<div>')
                            .attr('id', 'addedItemspopup'),
                        $('<div>')
                            .attr('id', 'GEpopupchart')
                    )
            );
        },

        /**
         * <doc>
         */
        setupCharts: function () {

            $('div.GEdatachart').each(function (c) {

                var $dataPrices = $('#GEdataprices' + c),
                    $dataChart = $('#GEdatachart' + c),
                    dataItem = $dataPrices.attr('data-item'),
                    isSmall = $dataChart.hasClass('smallChart'),
                    isMedium = $dataChart.hasClass('mediumChart'),
                    isIndexChart = /index/i.test(dataItem),
                    selector = isOSRS ? '.infobox *, .infobar *, .infobox-switch-resources.infobox-resources-Infobox_Item *' : '.infobox *, .infobar *, .rsw-infobox *, .infobox-switch-resources.infobox-resources-Infobox_Item *',
                    isInfobox = $dataPrices.is(selector),
                    itemName = dataItem || conf.wgTitle.split('/')[0],
                    dataList,
                    yAxis,
                    zoom;


                if (!$dataPrices.length) {
                    return;
                }

                // setting up the form and chart elements
                if (!isSmall && !isMedium) {
                    $dataChart.before(
                        self.makeOOUI(c),
                        $('<div>')
                            .attr('id', 'addedItems' + c)
                    );
                }

                getData(c, isSmall, isMedium, undefined, function(data) {
                    var dataList = data[0];
                    var yAxis = data[1];
                    if (itemName.toLowerCase() !== 'blank') {
                        zoom = parseInt((location.hash.match(/#z=([^#]*)/) || [])[1]);
                        zoom = zoom && zoom <= 6 && zoom >= 0 ?
                            zoom - 1 :
                            (zoom === 0 ?
                                0 :
                                2);
                    }
                    
                    var enlarge = new OO.ui.ButtonWidget( { 
						label: 'Enlarge chart',
						icon: 'fullScreen',
						id: 'gec-enlarge-' + c
					} );
					
					enlarge.$element.css("font-size", "13px");
                        
                    // @todo this doesn't do anything on small charts
                    //       is it supposed to?
                    //var zoomOut = '<a href="javascript:_GEC.chart' + c + '.zoomOut();" style="text-decoration:underline;color:inherit;font-size:inherit;">Zoom out</a>';

                    //generating the chart
                    _GEC['chart' + c] = new Highcharts.StockChart({
                        chart: {
                            renderTo: 'GEdatachart' + c,
                            backgroundColor: 'white',
                            plotBackgroundColor: 'white',
                            zoomType: '',
                            //height: isSmall?210:null,
                            events: {
                                redraw: function () {
                                    _GEC.thisid = this.renderTo.id.replace('GEdatachart', '').replace('GEpopupchart', 'popup');
                                    setTimeout(function () {
                                        setChartExtremes(_GEC.thisid);
                                    }, 0);
                                }
                            },
                        },
                        legend: {
                            enabled: !isSmall && !isMedium,
                            backgroundColor: 'white',
                            align: 'right',
                            layout: 'vertical',
                            verticalAlign: 'top',
                            y: 85
                        },
                        responsive: {
                            rules: [{
                                condition: {
                                    //maxWidth: 500
                                },
                                chartOptions: {
                                    legend: {
                                        align: 'center',
                                        verticalAlign: 'bottom',
                                        layout: 'horizontal'
                                    }
                                }
                            }]
                        },
                        title: {
                            text: (isSmall || isMedium) ? ((isInfobox || isMedium) ? enlarge.$element[0].outerHTML : itemName) : 'Grand Exchange Market Watch',
                            useHTML: true,
                            style: {
                                color: 'black',
                                fontSize: isSmall ? (enlarge ? '13px' : '15px') : '18px',
                            },
                        },
                        subtitle: {
                            text: isSmall ? (isInfobox ? '' : enlarge.$element[0].outerHTML) : (itemName.toLowerCase() == 'blank' ? 'Historical chart' : itemName),
                            useHTML: true,
                            y: 50,
                            style: {
                                color: '#666',
                                fontSize: isSmall ? '13px' : '15px',
                            },
                        },
                        rangeSelector: {
                            enabled: !isSmall && !isMedium,
                            selected: zoom,
                            inputBoxStyle: {
                                right: '15px',
                                display: (isSmall || isMedium) ? 'none' : 'block'
                            },
                            inputStyle: {
                                width: '100px',
                            },
                            inputDateFormat: "%e-%b-%Y",
                            buttonTheme: {
                                class: 'zoomButton',
                            },
                            buttons: [{
                                type: 'month',
                                count: 1,
                                text: '1m'
                            }, {
                                type: 'month',
                                count: 2,
                                text: '2m'
                            }, {
                                type: 'month',
                                count: 3,
                                text: '3m'
                            }, {
                                type: 'month',
                                count: 6,
                                text: '6m'
                            }, {
                                type: 'year',
                                count: 1,
                                text: '1y'
                            }, {
                                type: 'all',
                                text: 'All'
                            }]
                        },
                        plotOptions: {
                            series: {
                                enableMouseTracking: !isSmall,
                                dataGrouping: {
                                    dateTimeLabelFormats: {
                                        day: ['%A, %e %B %Y', '%A, %e %B', '-%A, %e %B %Y'],
                                        week: ['Week from %A, %e %B %Y', '%A, %e %B', '-%A, %e %B %Y'],
                                        month: ['%B %Y', '%B', '-%B %Y'],
                                        year: ['%Y', '%Y', '-%Y']
                                    }
                                }
                            }
                        },
                        tooltip: {
                            enabled: !isSmall,
                            valueDecimals: isIndexChart ? 2 : 0,
                            headerFormat: '<span style="font-size: 12px">{point.key}</span><br/>',
                            xDateFormat: "%A, %e %B %Y",
                        },
                        navigator: {
                            xAxis: {
                                dateTimeLabelFormats: {
                                    day: "%e-%b",
                                    week: "%e-%b",
                                    month: "%b-%Y",
                                    year: "%Y",
                                },
                                minTickInterval: 24 * 3600 * 1000, //1 day
                            },
                            maskFill: 'none',
                            enabled: !(isSmall || isMedium)
                        },
                        credits: {
                            enabled: false,
                        },
                        xAxis: [{
                            lineColor: '#666',
                            tickColor: '#666',
                            dateTimeLabelFormats: {
                                day: "%e-%b",
                                week: "%e-%b",
                                month: "%b-%Y",
                                year: "%Y",
                            },
                            minTickInterval: 24 * 3600 * 1000, //1 day
                            scrollbar: {
                                enabled: false,
                                showFull: false
                            },
                        }],
                        yAxis: yAxis,
                        series: dataList,
                        colors: window.GEMWChartColors || ['#4572A7', '#AA4643', '#89A54E', '#80699B', '#3D96AE', '#DB843D', '#92A8CD', '#A47D7C', '#B5CA92'],
                        annotations: [data[2]],
                    });

                    var items = ($('#GEChartItems' + c).html() || '').split(',');
                    var noAdd = [];
                    var i;

                    for (i = 0; i < items.length; i++) {
                        items[i] = items[i].trim();

                        if (items[i]) {
                            addItem(c, items[i]);
                        } else {
                            noAdd.push(1);
                        }
                    }
                    if (items.length == noAdd.length && _GEC['chart' + c].series[0].name.toLowerCase() != 'blank') setChartRange(c);
                    
                    //adjusting the axes extremes (initial load)
                    setChartExtremes(c);

                    //loading the chart and additional price info when the page is ready
                    if (((conf.wgNamespaceNumber == 112 && conf.wgTitle.split('/')[1] == 'Data') || conf.wgPageName == 'RuneScape:Grand_Exchange_Market_Watch/Chart') && location.hash.match('#i=') !== null) {
                        var hash = location.hash;
                        items = decodeURIComponent((hash.match(/#i=([^#]*)/) || [])[1] || '').replace(/_/g, ' ').split(',');
                        for (i = 0; i < items.length; i++) {
                            if (items[i].match(/^\s*$/) === null) addItem(0, items[i]);
                        }
                    }
                    
                    var $enlargeEle = $("#gec-enlarge-" + c);
                    if ($enlargeEle.length) {
                        $enlargeEle.on("click", function () {
                            popupChart(c);
                        });
                    };
                });

            });

        }
    },

    /**
     * General helper methods
     */
    mw.util = {
        /**
         * <doc>
         *
         * @todo replace with $.extend
         *
         * @param a {object}
         * @param b {object} (optional)
         *
         * @return {object}
         */
        cloneObj: function (a, b) {
            if (typeof a !== 'object') {
                return '';
            }

            if (typeof b !== 'object') {
                b = {};
            }

            for (var key in a) {
                if (a.hasOwnProperty(key)) {
                    b[key] = a[key];
                }
            }

            return b;
        },

        /**
         * Averages prices across a specified time interval
         *
         * @param arr {array} Array of arrays, where each member of `arr`
         *                    is in the format [time, price]
         *                    Which is how we store the price data
         *                    @example [x-coord, y-coord]
         * @param amt {number} Interval to average across in days
         * @param round {number} (optional) Number of decimal places to round to
         *                       Defaults to 0
         *
         * @return {array} Array of arrays, where each member of the return array
         *                 is in the format [time, price] (as above)
         *                 and
         */
        avg: function (arr, amt, round) {
            amt = amt || arr.length;
            // convert `round` into a number we can use for rounding
            round = Math.pow(10, round || 0);

            var avgs = [],
                list = [],
                i;

            // adds each price to `list`
            // when `amt` is reached, average the contents of `list`
            //
            // each iteration after `amt` is reached averages the contents of `list`
            // which is continuously being updated as each iteration
            // after `amt` is reached replaces a member of `list`
            // @example when `i` is 31 the current price replaces `list[1]`
            //          when `i` is 35 the current price replaces `list[5]`
            for (i = 0; i < arr.length; i++) {
                list[i % amt] = arr[i][1];

                if (i >= amt) {
                    avgs.push([
                        // don't modify the time (y-coord)
                        arr[i][0],
                        Math.round((mw.util.sum(list) / list.length) * round) / round
                    ]);
                }
            }

            return avgs;
        },

        /**
         * Finds the sum of numbers in an array
         * Only called by `mw.util.avg`
         *
         * @param arr {array} Array of number to find the sum of
         *
         * @return {number} Sum of the numbers in `arr`
         */
        sum: function (arr) {
            var total = 0,
                i;

            for (i = 0; i < arr.length; i++) {
                total += parseFloat(arr[i], 10);
            }

            return total;
        },

        /**
         * Rounds and formats numbers
         *
         * @example 12345        -> 12.3K
         * @example 1234567      -> 1.2M
         * @example 123456789012 -> 123.4M
         *
         * @param num {number|string} Number to format
         *
         * @return {string} Formatted number
         */
        toKMB: function (num) {
            // strip commas from number string
            // as `parseInt` will interpret them as a decimal separator
            // pass numbers and string to `parseInt` to convert floats too
            num = parseInt((typeof num === 'string' ? num.replace(/,/g, '') : num), 10);
            var neg = num < 0 ? '-' : '';

            num = Math.abs(num);

            // `1eX` is shorthand for `Math.pow( 10, X )`
            if (num >= 1e10) {
                num = Math.round(num / 1e8) / 10;
                num += 'B';
            } else if (num >= 1e7) {
                num = Math.round(num / 1e5) / 10;
                num += 'M';
            } else if (num >= 1e4) {
                num = Math.round(num / 100) / 10;
                num += 'K';
            }

            return rs.addCommas(neg + num);
        },

        /**
         * Capitalises first character of a string
         *
         * @source <https://stackoverflow.com/a/1026087>
         *
         * @param str {string}
         *
         * @return {string}
         */
        ucFirst: function (str) {
            return str.charAt(0).toUpperCase() + str.slice(1);
        },

        /**
         * Sort data points in the graph data before passing it to the charts api
         */
        sortPoints: function (a, b) {
            a = a.replace(/'/g, '').split(':')[0];
            b = b.replace(/'/g, '').split(':')[0];

            return a - b;
        }
    },

    /**
     * Chart methods
     */
    chart = {
        /**
         * <doc>
         *
         * @param id {string|number}
         * @param match {string} is normally the 'line' that isn't an item's price data
         *                       such as average or volume
         *
         * @return {number}
         */
        getSeriesIndex: function (id, match) {
            var chart = _GEC['chart' + id],
                series = chart.series,
                i;

            if (chart) {
                for (i = 0; i < series.length; i++) {
                    if (series[i].name.match(match)) {
                        return i;
                    }
                }

                return -1;
            }

            // @todo what happens if !chart
        },

        /**
         * Creates a URL with preset options
         *
         * @todo change to url params
         * @todo document the individual params/options
         *
         * @param id {number|string}
         *
         * @return {string}
         */
        permLinkUrl: function (id) {
            var chart = _GEC['chart' + id],
                xt = chart.xAxis[0].getExtremes(),
                series = chart.series,
                minDate = (new Date(xt.min))
                    .toDateString()
                    .split(' ')
                    .slice(1)
                    .join('_'),
                maxDate = (new Date(xt.max))
                    .toDateString()
                    .split(' ')
                    .slice(1)
                    .join('_'),
                inputAvg = $('#average' + id).data('ooui-elem').getNumericValue(),
                urlHash = '#t=' + minDate + ',' + maxDate,
                items = '',
                i;

            if (!isNaN(inputAvg)) {
                urlHash += '#a=' + inputAvg;
            }

            for (i = 0; i < series.length; i++) {
                if (series[i].name == 'Navigator' || series[i].name.match('average')) {
                    continue;
                }

                // separate items with commas
                if (items) {
                    items += ',';
                }

                // @todo url encode this?
                items += series[i].name.replace(/ /g, '_');
            }

            urlHash += '#i=' + items;

            // @todo hide the redirect h2
            return '/w/RuneScape:Grand_Exchange_Market_Watch/Chart' + urlHash;
        },

        /**
         * Add a new item to the chart
         *
         * @param i
         * @param it {string} (optional)
         */
        addItem: function (i, it) {
            _GEC.chartid = i;
            var OOUIextraItemPresent = $('#extraItem' + i).length > 0,
                OOUIextraItem = $('#extraItem' + i).data('ooui-elem'),
                item = (it || '').trim() || OOUIextraItem.getValue(),
                dataItems = [
                    '#addedItems' + i + ' [data-item]',
                    '#GEdataprices' + i + '[data-item]'
                ],
                $dataItems = $(dataItems.join(',')).map(function () {
                    return $(this).attr('data-item').toLowerCase();
                }),
                $addedItems = $('#addedItems' + i),
                id,
                data,
                series,
                seriesIndex,
                gecchartid = i,
                index;

            if (item && item.length) {
                index = -1;
                for (var i2 = 0; i2 < _GEC.AIQueue.length; i2++) {
                    if (_GEC.AIQueue[i2] == item.toLowerCase()) {
                        index = i2;
                        break;
                    }
                }

                if (
                    // @todo should a number passed to .get()
                    $dataItems.get().indexOf(item.toLowerCase()) !== -1 ||
                    index !== -1
                ) {
                    if (!it) {
                        alert(item + ' is already in the graph.');
                    }

                    if (OOUIextraItemPresent) { 
                        OOUIextraItem.setValue(''); 
                    }

                    return false;
                }

                if (OOUIextraItemPresent) { 
                    OOUIextraItem.setDisabled(true);    
                }

                $.get(
                    '/api.php',
                    {
                        action: 'query',
                        prop: 'revisions',
                        rvprop: 'content',
                        format: 'json',
                        titles: 'Module:Exchange/' + mw.util.ucFirst(item)
                    }
                ).then(function(data, textStatus) {
                    var OOUIextraItem = $('#extraItem' + gecchartid).data('ooui-elem'),
                        mw.pages = data.query.mw.pages;
                    if (textStatus !== 'success') {
                        alert('An error occured while loading ' + item);
                        mw.log(data);
                    }
                    var matches = []
                    var pageMissing = false;
                    if (mw.pages[-1]) {
                        pageMissing = true;
                    } else {
                        var exchangeData = mw.pages[Object.keys(mw.pages)[0]]
                                            .revisions[0]['*'];
                        matches = exchangeData.match(/itemId\D*(\d*)/);
                        if (matches.length !== 2) {
                            pageMissing = true;
                        }
                    }
                    // page not found
                    if (pageMissing) {
                        if (OOUIextraItem.getValue().length) {
                            alert('The item ' + item + ' doesn\'t exist on our Grand Exchange database.');
                            OOUIextraItem.setDisabled(false).setValue('');
                            return false;
                        }

                        _GEC.AILoaded.push(false);

                        if (
                            _GEC.AIData.length &&
                            _GEC.AIQueue.length == _GEC.AILoaded.length
                        ) {
                            loadChartsQueueComplete(gecchartid);
                        } else if (!_GEC.AIData.length) {
                            setChartRange(gecchartid);
                        }

                        OOUIextraItem.setDisabled(false).setValue('');

                        return false;
                    }

                    var itemId = matches[1];
                    return $.getJSON("https://api.weirdgloop.org/exchange/history/" + gameVersion + "/all?compress=true&id=" + itemId);
                }).then(function(data, textStatus) {
                    if (data === false) return;
                    $.getJSON(mw.mw.util.getUrl('Module:Exchange/'+mw.util.ucFirst(item)+'/annotations.json', {action:'raw', 'ctype':'application/json'})).then(
                    	function(annodata){return annodata},
                    	function(){return []} //handles the page not existing
                    ).then(function(annodata){
                        _GEC.AIData.push({
                            name: item,
                            data: Object.values(data)[0],
                            id: item,
                            gecchartid: gecchartid,
                            lineWidth: 2,
                            annotations: formatAnnoList(annodata, Object.values(data)[0])
                        });
    
                        _GEC.AILoaded.push(item);
    
                        if (getSeriesIndex(gecchartid, 'average') !== -1) {
                            _GEC['chart' + gecchartid]
                                .series[getSeriesIndex(gecchartid, 'average')]
                                .remove();
                        }
    
                        if (_GEC.AIQueue.length === _GEC.AILoaded.length) {
                            // This is always true when only 1 item is being loaded.
                            loadChartsQueueComplete(gecchartid);
                        }
                    });
                })

                _GEC.AIQueue.push({item: item.toLowerCase(), chart: gecchartid});

                // @todo when does this happen
                /* This happens when there are no further items added to the charts, i.e. when the original item is the only one.
                   This is indeed a flawed test, since it won't work on GEMW/C, where there is no original item in the chart.
                   This should be replaced with another test that also works on GEMW/C.
                 */
            } else if (
                $addedItems.html().match(/^\s*$/) ||
                (
                    conf.wgPageName == 'RuneScape:Grand_Exchange_Market_Watch/Chart' &&
                    $addedItems.find('a').length === 1
                )
            ) {
                id = (i === 'popup' ? $('#GEchartpopup').attr('data-chartid') : i);
                getData(id, false, false, i, function(data) {
                    series = _GEC['chart' + i].series;
                    seriesIndex = getSeriesIndex(i, 'average');

                    //remove an average line if it already exists
                    if (seriesIndex !== -1) {
                        series[seriesIndex].remove();
                    }

                    //add average line when there is only 1 item in the chart
                    _GEC['chart' + i].addSeries(data[0][1]);
                });
            }
        },

        /**
         * <doc>
         *
         * @param c {number|string}
         */
        loadQueueComplete: function (cin, addeditembyscript) {
            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
                c = cnum ? _GEC.AIQueue.length : cin, //if not a number, its almost certainly 'popup', for which we only need to reload the popup
                id,
                chartdata,
                isSmall = [],
                isMedium = [],
                data = [],
                i,
                index,
                itemhash,
                $addedItems,
                iname,
                hadBlank;

            if (cnum) { //this structure repeats throughout the method: if cnum then loop else do once. probably a better way to do this
                for (i = 0; i < c; i++) {
                    isSmall[i] = $('#GEdatachart' + i).hasClass('smallChart');
                    isMedium[i] = $('#GEdatachart' + i).hasClass('mediumChart');
                }
            } else {
                isSmall = $('#GEdatachart' + c).hasClass('smallChart');
                isMedium = $('#GEdatachart' + c).hasClass('mediumChart');
            }

            if (cnum) {
                for (i = 0; i < c; i++) {
                    if (getSeriesIndex(_GEC.AIQueue[i].chart, volumeLabel) !== -1) {
                        id = i === 'popup' ? $('#GEchartpopup').attr('data-chartid') : i;
                        getData(id, true, undefined, undefined, function(data) {
                            data[1].title.text = 'Price history';

                            reloadChart(i, {
                                series: data[0],
                                yAxis: data[1]
                            });
                        });
                    }
                }
            } else {
                if (getSeriesIndex(c, volumeLabel) !== -1) {
                    id = c === 'popup' ? $('#GEchartpopup').attr('data-chartid') : c;
                    getData(id, true, undefined, undefined, function(data) {
                        data[1].title.text = 'Price history';
                        reloadChart(c, {
                            series: data[0],
                            yAxis: data[1]
                        });
                    });
                }
            }

            for (i = 0; i < _GEC.AIData.length; i++) {
                index = -1;
                for (var i2 = 0; i2 < _GEC.AIQueue.length; i2++) {
                    if (_GEC.AIQueue[i2].item === (_GEC.AIData[i] || {name: ''}).name.toLowerCase()) {
                        index = i2;
                        break;
                    }
                }
                data[index !== -1 ? index : data.length] = _GEC.AIData[i];
            }
            // @todo should this be `Array.isArray`
            //       or should it default to `{}`
            // @todo test if isSmall is needed in the conditional
            if (cnum) {
                for (i = 0; i < c; i++) {
                    if (data[i] === undefined) continue;
                    if ((isSmall[data[i].gecchartid] && isMedium[data[i].gecchartid]) && typeof Array.isArray(_GEC.addedData[data[i].gecchartid])) {
                        _GEC.addedData[data[i].gecchartid] = [];
                    }
                }
            } else {
                if ((isSmall || isMedium) && typeof Array.isArray(_GEC.addedData[data[c].gecchartid])) {
                    _GEC.addedData[data[c].gecchartid] = [];
                }

            }

            for (i = 0; i < data.length; i++) {
                if (data[i]) {
                    _GEC['chart' + data[i].gecchartid].addSeries(data[i]);
                    if (data[i].annotations && data[i].annotations.labels && data[i].annotations.labels.length) {
                    	 _GEC['chart' + data[i].gecchartid].addAnnotation(data[i].annotations);
                    }
                }

                if (cnum && isSmall[data[i].gecchartid]) {
                    _GEC.addedData[data[i].gecchartid][i] = data[i];
                }
            }

            if (cnum) {
                for (i = 0; i < c; i++) {
                    setChartExtremes(data[i].gecchartid);
                    $('#extraItem' + data[i].gecchartid).data('ooui-elem').setDisabled(false).setValue('');
                }
            } else {
                setChartExtremes(c);
                $('#extraItem' + c).data('ooui-elem').setDisabled(false).setValue('');
            }
            itemhash = (location.hash.match(/#i=[^#]*/) || [])[0] || location.hash + '#i=';
            $addedItems = $('#addedItems' + c);

            for (i = 0; i < data.length; i++) {
                if (!data[i]) {
                    continue;
                }

                iname = data[i].name;

                if (!$addedItems.text().trim()) {
                    $addedItems.append(
                        'Remove items from graph: ',
                        $('<a>')
                            .attr({
                                href: 'javascript:removeGraphItem("' + iname + '","' + c + '")',
                                'data-item': iname
                            })
                            .text(iname)
                    );
                    itemhash = '#i=' + iname;
                } else {
                    $addedItems.append(
                        ', ',
                        $('<a>')
                            .attr({
                                href: 'javascript:removeGraphItem("' + iname + '","' + c + '")',
                                'data-item': iname
                            })
                            .text(iname)
                    );
                    itemhash += ',' + iname;
                }
            }

            if (location.hash.match(/#i=/)) {
                itemhash = location.hash
                    .replace(/#i=[^#]*/, itemhash)
                    .replace(/ /g, '_');
            } else {
                itemhash = location.hash + itemhash;
            }

            if (
                (
                    conf.wgNamespaceNumber == 112 && conf.wgTitle.split('/')[1] == 'Data' ||
                    conf.wgPageName == 'RuneScape:Grand_Exchange_Market_Watch/Chart'
                ) &&
                itemhash.replace('#i=', '').length
            ) {
                location.hash = itemhash;
            }

            _GEC.AIQueue = [];
            _GEC.AILoaded = [];
            _GEC.AIData = [];

            if (cnum) {
                for (i = 0; i < c; i++) {
                    hadBlank = removeGraphItem('Blank', data[i].gecchartid);

                    if (hadBlank) {
                        setChartRange(data[i].gecchartid);
                    }
                }
            } else {
                hadBlank = removeGraphItem('Blank', c);

                if (hadBlank) {
                    setChartRange(c);
                }
            }
        },

        /**
         * <doc>
         *
         * @param c {number|string}
         *
         * @return {boolean}
         */
        setRange: function (c) {
            var zoom = parseInt((location.hash.match(/#z=([^#]*)/) || [])[1], 10);
            zoom = zoom && zoom <= 6 && zoom >= 0 ? zoom - 1 : (zoom === 0 ? 0 : 2);
            var hash = location.hash;
            var hasT = (conf.wgNamespaceNumber === 112 && conf.wgTitle.split('/')[1] === 'Data') || conf.wgPageName === 'RuneScape:Grand_Exchange_Market_Watch/Chart';
            if (typeof c === 'number' && (hasT && !hash.match('#t=') || !hasT)) {
                $('#GEdatachart' + c + ' .zoomButton').eq(zoom).click();
                return true;
            }

            var timespan = decodeURIComponent((hash.match(/#t=([^#]*)/) || [])[1] || '')
                .replace(/_/g, ' ')
                .split(',');
            var dates = [new Date(timespan[0]), new Date(timespan[1])];
            var d = new Date(timespan[0]);
            var extremes = _GEC['chart' + c].xAxis[0].getExtremes();

            if (dates[0] !== 'Invalid Date' && dates[1] === 'Invalid Date' && typeof zoom === 'number') {
                var button = _GEC['chart' + c].rangeSelector.buttonOptions[zoom];

                if (button.type === 'month') {
                    d.setMonth(d.getMonth() + button.count);
                } else if (button.type === 'year') {
                    d.setYear(d.getFullYear() + button.count);
                } else if (button.type === 'all') {
                    d = new Date(extremes.dataMax);
                }

                dates[1] = d;
            }

            if (dates[0] !== 'Invalid Date' && dates[1] !== 'Invalid Date') {
                _GEC['chart' + c].xAxis[0].setExtremes(dates[0].getTime(), dates[1].getTime());
                return true;
            }

            return false;
        },

        /**
         * <doc>
         *
         * @param c {number|string}
         * @param change {object}
         */
        reload: function (c, change) {
            var options = _GEC['chart' + c].options;

            if (!options) {
                // @todo do we need to return `false` here
                // @todo when does this happen
                return false;
            }

            $.extend(options, change);
            _GEC['chart' + c] = new Highcharts.StockChart(options);
        },

        /**
         * <doc>
         *
         * @param item {string}
         * @param c {number|string}
         *
         * @return {boolean}
         */
        removeItem: function (item, c) {
            var series = _GEC['chart' + c].series,
                id,
                i,
                newhash,
                data;

            // find the item we want to remove
            for (i = 0; i < series.length; i++) {
                if (series[i].name.match(item)) {
                    id = i;
                }
            }

            // @todo when does this happen
            //       when we can't find the item?
            if (typeof id !== 'number') {
                return false;
            }

            // remove item from url hash
            newhash = location.hash
                .replace(/_/g, ' ')
                .replace(new RegExp('(#i=[^#]*),?' + item, 'i'), '$1')
                .replace(/,,/g, ',')
                .replace(/,#/g, '#')
                .replace(/#i=,/g, '#i=')
                .replace(/#i=($|#)/, '$1')
                .replace(/ /g, '_');

            if (newhash.replace('#i=', '').length) {
                location.hash = newhash;
            } else if (location.hash.length) {
                location.hash = '';
            }

            // remove the item from the chart
            series[id].remove();
            // reset extremes?
            setChartExtremes(c);

            // @todo can we cache #addedItems somehow
            // remove item from list at top of graph
            $('#addedItems' + c + ' [data-item="' + item + '"]').remove();
            // cleanup list
            $('#addedItems' + c).html(
                $('#addedItems' + c)
                    .html()
                    .replace(/, , /g, ', ')
                    .replace(/, $/, '')
                    .replace(': , ', ': ')
            );

            // if the list is empty show average, volume and item stats again
            if (!$('#addedItems' + c + ' [data-item]').length) {
                $('#addedItems' + c).empty();
                id = c == 'popup' ? $('#GEchartpopup').attr('data-chartid') : c;
                data = getData(id, false, false, 'popup', function(data) {
                    reloadChart(c, {
                        series: data[0],
                        yAxis: data[1]
                    });
                });

            }

            return true;
        },

        /**
         * <doc>
         *
         * @param i {number|string}
         */
        popup: function () {
        },

        /**
         * <doc>
         *
         * @param i
         */
        setExtremes: function (i) {
            var ch = _GEC['chart' + i],
                exts = _GEC['chart' + i].yAxis[0].getExtremes();

            if (
                exts.dataMin * 0.95 !== exts.userMin ||
                exts.dataMax * 1.05 !== exts.userMax
            ) {
                ch.yAxis[0].setExtremes(exts.dataMin * 0.95, exts.dataMax * 1.05);

                if (ch.yAxis[2]) {
                    exts = ch.yAxis[1].getExtremes();
                    ch.yAxis[1].setExtremes(0, exts.dataMax * 1.05);
                }
            }

            if (i === 'popup') {
                // @todo use onclick event
                $('#GEPermLink' + i).data('ooui-elem').setData(chartPermLinkUrl(i));
            }
        },

        /**
         * <doc>
         *
         * @param c {number|string}
         * @param isSmall {boolean}
         * @param avginput {number|string} (optional)
         *        number component of input element used for altering the average interval
         *        when the interval is in days
         *        when is this different to `c`?
         *
         * @return {array} 2 item array containing X and Y respectively
         *                 @todo expand on what X and Y are
         */
        getData: function () {
        }
    },

    // map old functions to new locations until uses are fixed
    getSeriesIndex = chart.getSeriesIndex,
    chartPermLinkUrl = chart.permLinkUrl,
    addItem = chart.addItem,
    removeGraphItem = chart.removeItem,
    reloadChart = chart.reload,
    setChartRange = chart.setRange,
    setChartExtremes = chart.setExtremes,
    loadChartsQueueComplete = chart.loadQueueComplete;
// popupChart = chart.popup;
// getData = chart.getData;

// chart-related general functions

function popupChart(i) {
    var $popup = $('#GEchartpopup'),
        $overlay = $('#overlay'),
        options,
        data,
        n;

    if (!$popup.length) {
        return false;
    }

    if ($overlay.length) {
        $overlay.toggle();
    } else {
        $popup.before(
            $('<div>')
                .attr('id', 'overlay')
                .css('display', 'block')
        );
        $overlay = $('#overlay');
    }

    $overlay.on('click', function () {
        popupChart(false);
    });

    if (typeof i === 'number') {
        $(document).keydown(function (e) {
            // Esc
            if (e.which === 27) {
                popupChart(false);
            }
        });
    } else {
        // @todo only remove our event
        $(document).off('keydown');
    }

    if (typeof i === 'boolean' && !i) {
        $popup.hide();
        $('#addedItemspopup').html('');
    } else {
        $popup.toggle();
    }

    if (typeof i === 'number' && $popup.attr('data-chartid') !== i) {
        $('#averagepopup').data('ooui-elem').setValue(_GEC.average);
        $popup.attr('data-chartid', i);

        options = {};
        getData(i, false, false, 'popup', function(data) {
            var dataList = data[0];
            var yAxis = data[1];
            // @todo can this be replaced with $.extend?
            // @todo what is this supposed to do?
            mw.util.cloneObj(_GEC['chart' + i].options, options);

            options.chart.renderTo = 'GEpopupchart';
            options.legend.enabled = true;
            options.title.text = 'Grand Exchange Market Watch';
            options.title.style.fontSize = '18px';
            options.subtitle.text = options.series[0].name;
            options.subtitle.y = 35;
            options.subtitle.style.fontSize = '15px;';
            options.chart.zoomType = '';
            options.rangeSelector.enabled = true;
            options.rangeSelector.inputBoxStyle.display = 'block';
            options.plotOptions.series.enableMouseTracking = true;
            options.tooltip.enabled = true;
            options.navigator.enabled = true;
            options.credits.enabled = false;
            options.series = [{}];
            options.series = _GEC.addedData[i] ? [dataList[0]] : dataList;
            options.yAxis = yAxis;
            options.annotations = [data[2]];

            _GEC.chartpopup = new Highcharts.StockChart(options);

            if (_GEC.addedData[i]) {
                for (n = 0; n < _GEC.addedData[i].length; n++) {
                    _GEC.chartpopup.addSeries(_GEC.addedData[i][n]);
                }
            }

            setChartExtremes('popup');
            _GEC.chartpopup.redraw();
            $('#GEPermLinkpopup').data('ooui-elem').setData(chartPermLinkUrl('popup'));
        });
    }
}

function rg(num) {
    var colour = 'red';

    if (num > 0) {
        colour = 'green';
    } else if (num === 0) {
        colour = 'blue';
    }

    return colour;
}

function formatAnnoList(annoData, prices) {
    var annoList = {
        labelOptions: {
            verticalAlign: 'bottom',
            overflow: 'justify',
            y: -5
        },
        draggable: '',
        visible: window._GEC.annotationsVisible,
        labels: []
    }
    for (var i = 0; i<annoData.length; i++){
        var ad = annoData[i];
        var label = ad.title;
        if (!(ad.link === undefined || ad.link === null || ad.link === '')){
            var labelUrl;
            if (ad.link.startsWith('https://') || ad.link.startsWith('http://')) {
                labelUrl = ad.link;
            } else {
                labelUrl = mw.mw.util.getUrl(ad.link);
            }
            label = '<a href="'+labelUrl+'" target="_blank">'+ad.title+'</a>';
        }
        var ts = Date.parse(ad.timestamp);
        if (!isNaN(ts)) {
            var priceAtTs = 0;
            for (var i2 = 0; i2 < prices.length; i2++) {
                if (prices[i2][0] === ts) {
                    priceAtTs = prices[i2][1];
                    break;
                }
                if (prices[i2][0] > ts) {
                    if (i2 > 0) {
                        if (prices[i2-1][0] < ts) {
                            priceAtTs = prices[i2][1];
                            break;
                        }
                    } else {
                        priceAtTs = prices[i2][1];
                        break;
                    }
                }
            }
            var anno = {
                text: label,
                point: {
                    xAxis: 0,
                    yAxis: 0,
                    x: ts,
                    y: priceAtTs
                }
            };
            annoList.labels.push(anno);
        }
    }
    return annoList;
}

function getData(cin, isSmall, isMedium, avginput, callback) {
    var c = cin === 'popup' ? $('#GEchartpopup').attr('data-chartid') : cin,
        $dataPrices = $('#GEdataprices' + c),
        dataItem = $dataPrices.attr('data-item'),
        dataItemId = $dataPrices.attr('data-itemId') || ('GE ' + dataItem),
        isIndexChart = /index/i.test(dataItem),
        itemName = dataItem || conf.wgTitle.split('/')[0],
        ch = _GEC['chart' + c],
        chartLoaded = !!(ch && ch.series && ch.series.length),
        prices = [],
        i,
        data = [],
        thisprice,
        volumes = [],
        dataList,
        inputAvg,
        newhash,
        yAxis,
        chartPageData;

    // happens when the first chart isSmall
    // and the average input id is actually the popup chart
    // the chart's id is popup, but the input's id is 0
    avginput = avginput || cin;

    var pricesToDataList = function(inputData) {
        var prices = inputData[0], annoData = inputData[1];
        _GEC.urlCache[url] = prices;
        _GEC.urlCache[annoUrl] = annoData;
        prices = Object.values(prices)[0];
        var volumeMultiplier = 1;//isOSRS ? 1 : 1000000
        for (i = 0; i < prices.length; i++) {
            data.push([
                // time
                prices[i][0],
                // @todo should this be parseInt?
                // price
                prices[i][1]
            ]);

            if (prices[i][2] && !isSmall) {
                volumes.push([
                    // time
                    prices[i][0],
                    // volume
                    // volumes are in millions
                    prices[i][2] * volumeMultiplier
                ]);
            }
        }

        // datalist's elements are essentially each line on the chart
        // so price, 30-day-average and volume
        dataList = [{
            name: itemName,
            data: data,
            lineWidth: isSmall ? 2 : 3
        }];

        if (itemName.toLowerCase() === 'blank' && !chartLoaded) {
            dataList[0].color = '#000000';
        }

        if (!isSmall && !isMedium && (itemName.toLowerCase() !== 'blank' || chartLoaded)) {
            inputAvg = $('#average' + avginput).data('ooui-elem').getNumericValue();

            // @todo should this be isNaN?
            if (inputAvg) {
                newhash = location.hash
                    .replace(/#a=[^#]*|$/, '#a=' + inputAvg)
                    .replace(/ /g, '_');

                if (newhash.length) {
                    location.hash = newhash;
                }
            }

            inputAvg = inputAvg || 30;
            dataList.push({
                name: inputAvg + '-day average',
                data: mw.util.avg(data, inputAvg, isIndexChart ? 2 : 0),
                lineWidth: 2,
                dashStyle: 'shortdash',
            });

            if (volumes.length >= 10) {
                dataList.push({
                    name: volumeLabel,
                    data: volumes,
                    type: 'area',
                    color: '#cc8400',
                    fillColor: {
                        linearGradient: {
                            x1: 0,
                            y1: 0,
                            x2: 0,
                            y2: 1
                        },
                        stops: [
                            [0, '#ffa500'],
                            [1, 'white']
                        ],
                    },
                    // display on separate y-axis
                    yAxis: 1,
                });
            }
        }

        // create y-axis for price data
        yAxis = {
            title: {
                text: isSmall ? null : (isIndexChart ? 'Index history' : 'Price history'),
                offset: 60,
                rotation: 270,
                style: {
                    color: 'black',
                    fontSize: '12px',
                },
            },
            opposite: false,
            labels: {
                align: 'right',
                x: -8,
                y: 4,
            },
            allowDecimals: false,
            // 1 coin
            minTickInterval: 1,
            showLastLabel: 1,
            lineWidth: 1,
            lineColor: '#E0E0E0'
        };

        // volume data is plotted on a seperate y-axis
        if (volumes.length >= 10 && !isSmall) {
            // set height to allow room for second y-axis
            yAxis.height = 200;

            // convert to array and add volume data
            yAxis = [yAxis, {
                title: {
                    text: volumeLabel,
                    offset: 60,
                    rotation: 270,
                    style: {
                        color: 'black',
                        fontSize: '12px'
                    }
                },
                opposite: false,
                labels: {
                    align: 'right',
                    x: -8,
                    y: 4,
                },
                showEmpty: 0,
                showLastLabel: 1,
                offset: 0,
                lineWidth: 1,
                lineColor: '#E0E0E0',
                height: 50,
                top: 325,
                min: 0
            }];
        }
        
        var annoList = {};
        if (!isSmall) {
        	annoList = formatAnnoList(annoData, prices);
        }
        
        return [dataList, yAxis, annoList];
    }
    
    var isPopup = !isSmall && !isMedium;
    var dataType = isPopup ? 'all' : 'sample';
    var url = "https://api.weirdgloop.org/exchange/history/" + gameVersion + "/" + dataType + "?compress=true&id=" + dataItemId;
    var annoUrl = mw.mw.util.getUrl('Module:Exchange/'+itemName+'/annotations.json', {action:'raw', 'ctype':'application/json'});
    var pricesPromise;
    if (chartLoaded && itemName.toLowerCase() === 'blank') {
        chartPageData = _GEC['chart' + c].series[
            getSeriesIndex(c, $('#addedItems' + c).find('a').data('item'))
            ];

        for (i = 0; i < chartPageData.xData.length; i++) {
            prices.push(chartPageData.xData[i] + ':' + chartPageData.yData[i]);
        }
        pricesPromise = Promise.resolve(prices);
    } else {
        if (_GEC.urlCache[url]) {
            return callback(pricesToDataList([_GEC.urlCache[url], _GEC.urlCache[annoUrl]]))
        }
        Promise.all([
            // combine these two calls
            // this fetches the price data from the API
            // and the annotation data from the wiki
            // the wiki annotation page may not exist, which returns the second part of the then
            $.getJSON(url),
            $.getJSON(annoUrl).then(function(d){return d}, function(){return []}) //on exception, just use an empty list
        ]).then(pricesToDataList).then(callback)
    }

}

$(self.deps);