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

From RuneRealm Wiki
Jump to navigation Jump to search
Content added Content deleted
No edit summary
Tag: Manual revert
No edit summary
 
(One intermediate revision by the same user not shown)
Line 27: Line 27:
* Cache mw.config variables
* Cache mw.config variables
*/
*/
function _typeof(o) { "@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (o) { return typeof o; } : function (o) { return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o; }, _typeof(o); }
var conf = mw.config.get([
var conf = mw.config.get(['wgNamespaceNumber', 'wgPageName', 'wgTitle', 'wgSiteName']),
'wgNamespaceNumber',
// Are we on OSRS? Impacts selectors and volume labels / multipliers
'wgPageName',
isOSRS = conf.wgSiteName == "RuneRealm Wiki",
'wgTitle',
// Volume label depends on which wiki we're on
'wgSiteName'
volumeLabel = "Daily volume",
]),
gameVersion = isOSRS ? 'osrs' : 'rs',

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

* @todo replace `_GEC` wih this
// Volume label depends on which wiki we're on
*/
volumeLabel = "Daily volume",
gec = {},
gameVersion = isOSRS ? 'osrs' : 'rs',
// @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 deps() {
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.util', 'mediawiki.api', 'rs.highcharts', 'rs.highcharts-annotations', 'oojs-ui-core', 'oojs-ui.styles.icons-media'], self.init);
},
/**
* Initial loading function
*/
init: function init(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>
* <doc>
*
* @todo replace `_GEC` wih this
*/
*/
makeOOUI: function makeOOUI(c) {
gec = {},
var averageRangeInput, addItemInput, submitButton, resetButton, annotationsToggle, fieldset, permalink;

averageRangeInput = new OO.ui.NumberInputWidget({
// @todo document each of these
_GEC = {
min: 1,
AIQueue: [],
value: 30,
AILoaded: [],
id: 'average' + c
AIData: [],
});
averageRangeInput.$element.data('ooui-elem', averageRangeInput);
addedData: [],
addItemInput = new OO.ui.TextInputWidget({
average: parseInt((location.hash.match(/#a=([^#]*)/) || [])[1], 10) || '',
urlCache: {},
id: 'extraItem' + c
});
annotationsVisible: true
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 buildPopup() {
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')));
},
},

/**
/**
* Startup methods
* <doc>
*/
*/
setupCharts: function setupCharts() {
self = {
$('div.GEdatachart').each(function (c) {
/**
var $dataPrices = $('#GEdataprices' + c),
* Loads and implements any required dependencies
$dataChart = $('#GEdatachart' + c),
*/
dataItem = $dataPrices.attr('data-item'),
deps: function () {
if (!mw.loader.getState('rs.highcharts')) {
isSmall = $dataChart.hasClass('smallChart'),
mw.loader.implement(
isMedium = $dataChart.hasClass('mediumChart'),
'rs.highcharts',
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 *',
['https://code.highcharts.com/stock/highstock.js'],
{}, {}
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 (!mw.loader.getState('rs.highcharts-annotations')) {
mw.loader.implement(
if (!isSmall && !isMedium) {
$dataChart.before(self.makeOOUI(c), $('<div>').attr('id', 'addedItems' + c));
'rs.highcharts-annotations',
}
['https://code.highcharts.com/modules/annotations.js'],
{}, {}
getData(c, isSmall, isMedium, undefined, function (data) {
);
var dataList = data[0];
}
var yAxis = data[1];
if (itemName.toLowerCase() !== 'blank') {
mw.loader.using(['mediawiki.util', 'mediawiki.api', 'rs.highcharts', 'rs.highcharts-annotations', 'oojs-ui-core', 'oojs-ui.styles.icons-media'], self.init);
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
/**
* Initial loading function
// 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>';
*/
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;
}
}());


//generating the chart
$('.GEdatachart').attr('id', function (c) {
return 'GEdatachart' + c;
_GEC['chart' + c] = new Highcharts.StockChart({
});
chart: {
$('.GEdataprices').attr('id', function (c) {
renderTo: 'GEdatachart' + c,
return 'GEdataprices' + c;
backgroundColor: 'white',
});
plotBackgroundColor: 'white',
$('.GEChartBox').each(function (c) {
zoomType: '',
//height: isSmall?210:null,
$(this).find('.GEChartItems').attr('id', 'GEChartItems' + c);
});
events: {
redraw: function redraw() {

_GEC.thisid = this.renderTo.id.replace('GEdatachart', '').replace('GEpopupchart', 'popup');
Highcharts.setOptions({
lang: {
setTimeout(function () {
// @todo can this be done with CSS?
setChartExtremes(_GEC.thisid);
resetZoom: null,
}, 0);
numericSymbols: ['K', 'M', 'B', 'T', 'Qd', 'Qt'],
}
}
});
}
},

// globals to maintain javascript hrefs
legend: {
window._GEC = _GEC;
enabled: !isSmall && !isMedium,
window.popupChart = popupChart;
backgroundColor: 'white',
window.addItem = chart.addItem;
align: 'right',
window.removeGraphItem = chart.removeItem;
layout: 'vertical',
verticalAlign: 'top',

self.buildPopup();
y: 85
self.setupCharts();
},
},
responsive: {
rules: [{

/**
condition: {
* <doc>
//maxWidth: 500
*/
},
makeOOUI: function (c) {
chartOptions: {
legend: {
var averageRangeInput, addItemInput, submitButton, resetButton, annotationsToggle, fieldset, permalink;
averageRangeInput = new OO.ui.NumberInputWidget({
align: 'center',
min: 1,
verticalAlign: 'bottom',
value: 30,
layout: 'horizontal'
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) {
title: {
text: isSmall || isMedium ? isInfobox || isMedium ? enlarge.$element[0].outerHTML : itemName : 'Grand Exchange Market Watch',
$dataChart.before(
self.makeOOUI(c),
useHTML: true,
$('<div>')
style: {
.attr('id', 'addedItems' + c)
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)
getData(c, isSmall, isMedium, undefined, function(data) {
var dataList = data[0];
setChartExtremes(c);
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);
});
};
});


//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
*/
util = {
/**
* <doc>
*
* @todo replace with $.extend
*
* @param a {object}
* @param b {object} (optional)
*
* @return {object}
*/
cloneObj: function cloneObj(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
* General helper methods
*
* @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 avg(arr, amt, round) {
util = {
/**
amt = amt || arr.length;
// convert `round` into a number we can use for rounding
* <doc>
*
round = Math.pow(10, round || 0);
var avgs = [],
* @todo replace with $.extend
*
list = [],
* @param a {object}
i;
* @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((util.sum(list) / list.length) * round) / round
]);
}
}

return avgs;
},

/**
* Finds the sum of numbers in an array
* Only called by `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;
// 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(util.sum(list) / list.length * round) / round]);
}
}
}
return avgs;
},
},
/**
* Finds the sum of numbers in an array
* Only called by `util.avg`
*
* @param arr {array} Array of number to find the sum of
*
* @return {number} Sum of the numbers in `arr`
*/
sum: function sum(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 toKMB(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
* Chart methods
*
* @source <https://stackoverflow.com/a/1026087>
*
* @param str {string}
*
* @return {string}
*/
*/
ucFirst: function ucFirst(str) {
chart = {
return str.charAt(0).toUpperCase() + str.slice(1);
/**
* <doc>
},
*
/**
* Sort data points in the graph data before passing it to the charts api
* @param id {string|number}
*/
* @param match {string} is normally the 'line' that isn't an item's price data
sortPoints: function sortPoints(a, b) {
* such as average or volume
*
a = a.replace(/'/g, '').split(':')[0];
* @return {number}
b = b.replace(/'/g, '').split(':')[0];
*/
return a - b;
}
getSeriesIndex: function (id, match) {
},
var chart = _GEC['chart' + id],
/**
series = chart.series,
i;
* 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 getSeriesIndex(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;
}


if (chart) {
// @todo what happens if !chart
},
for (i = 0; i < series.length; i++) {
/**
if (series[i].name.match(match)) {
* Creates a URL with preset options
return i;
}
*
}
* @todo change to url params
* @todo document the individual params/options
*
* @param id {number|string}
*
* @return {string}
*/
permLinkUrl: function permLinkUrl(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;
}


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


// @todo what happens if !chart
// @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;
* Creates a URL with preset options
*
},
/**
* @todo change to url params
* @todo document the individual params/options
* Add a new item to the chart
*
*
* @param id {number|string}
* @param i
*
* @param it {string} (optional)
* @return {string}
*/
addItem: function addItem(i, it) {
*/
_GEC.chartid = i;
permLinkUrl: function (id) {
var chart = _GEC['chart' + id],
var OOUIextraItemPresent = $('#extraItem' + i).length > 0,
OOUIextraItem = $('#extraItem' + i).data('ooui-elem'),
xt = chart.xAxis[0].getExtremes(),
series = chart.series,
item = (it || '').trim() || OOUIextraItem.getValue(),
dataItems = ['#addedItems' + i + ' [data-item]', '#GEdataprices' + i + '[data-item]'],
minDate = (new Date(xt.min))
$dataItems = $(dataItems.join(',')).map(function () {
.toDateString()
.split(' ')
return $(this).attr('data-item').toLowerCase();
.slice(1)
}),
.join('_'),
$addedItems = $('#addedItems' + i),
id,
maxDate = (new Date(xt.max))
.toDateString()
data,
.split(' ')
series,
.slice(1)
seriesIndex,
.join('_'),
gecchartid = i,
index;
inputAvg = $('#average' + id).data('ooui-elem').getNumericValue(),
if (item && item.length) {
urlHash = '#t=' + minDate + ',' + maxDate,
items = '',
index = -1;
i;
for (var i2 = 0; i2 < _GEC.AIQueue.length; i2++) {
if (_GEC.AIQueue[i2] == item.toLowerCase()) {

if (!isNaN(inputAvg)) {
index = i2;
urlHash += '#a=' + inputAvg;
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/' + util.ucFirst(item)
}).then(function (data, textStatus) {
var OOUIextraItem = $('#extraItem' + gecchartid).data('ooui-elem'),
pages = data.query.pages;
if (textStatus !== 'success') {
alert('An error occured while loading ' + item);
mw.log(data);
}
var matches = [];
var pageMissing = false;
if (pages[-1]) {
pageMissing = true;
} else {
var exchangeData = pages[Object.keys(pages)[0]].revisions[0]['*'];
matches = exchangeData.match(/itemId\D*(\d*)/);
if (matches.length !== 2) {
pageMissing = true;
}
}
}

for (i = 0; i < series.length; i++) {
// page not found
if (pageMissing) {
if (series[i].name == 'Navigator' || series[i].name.match('average')) {
continue;
if (OOUIextraItem.getValue().length) {
alert('The item ' + item + ' doesn\'t exist on our Grand Exchange database.');
}
OOUIextraItem.setDisabled(false).setValue('');

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

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

if (_GEC.AIData.length && _GEC.AIQueue.length == _GEC.AILoaded.length) {
urlHash += '#i=' + items;
loadChartsQueueComplete(gecchartid);

// @todo hide the redirect h2
} else if (!_GEC.AIData.length) {
setChartRange(gecchartid);
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/' + util.ucFirst(item)
}
).then(function(data, textStatus) {
var OOUIextraItem = $('#extraItem' + gecchartid).data('ooui-elem'),
pages = data.query.pages;
if (textStatus !== 'success') {
alert('An error occured while loading ' + item);
mw.log(data);
}
var matches = []
var pageMissing = false;
if (pages[-1]) {
pageMissing = true;
} else {
var exchangeData = pages[Object.keys(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.util.getUrl('Module:Exchange/'+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]);
});
}
}
OOUIextraItem.setDisabled(false).setValue('');
},
return false;

/**
}
* <doc>
var itemId = matches[1];
return $.getJSON("https://api.weirdgloop.org/exchange/history/" + gameVersion + "/all?compress=true&id=" + itemId);
*
* @param c {number|string}
}).then(function (data, textStatus) {
*/
if (data === false) return;
$.getJSON(mw.util.getUrl('Module:Exchange/' + util.ucFirst(item) + '/annotations.json', {
loadQueueComplete: function (cin, addeditembyscript) {
action: 'raw',
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
'ctype': 'application/json'
c = cnum ? _GEC.AIQueue.length : cin, //if not a number, its almost certainly 'popup', for which we only need to reload the popup
id,
})).then(function (annodata) {
chartdata,
return annodata;
isSmall = [],
}, function () {
isMedium = [],
return [];
data = [],
} //handles the page not existing
i,
).then(function (annodata) {
index,
_GEC.AIData.push({
itemhash,
name: item,
$addedItems,
data: Object.values(data)[0],
iname,
id: item,
hadBlank;
gecchartid: gecchartid,
lineWidth: 2,

annotations: formatAnnoList(annodata, Object.values(data)[0])
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');
_GEC.AILoaded.push(item);
if (getSeriesIndex(gecchartid, 'average') !== -1) {
isMedium[i] = $('#GEdatachart' + i).hasClass('mediumChart');
_GEC['chart' + gecchartid].series[getSeriesIndex(gecchartid, 'average')].remove();
}
} else {
isSmall = $('#GEdatachart' + c).hasClass('smallChart');
isMedium = $('#GEdatachart' + c).hasClass('mediumChart');
}
}
if (_GEC.AIQueue.length === _GEC.AILoaded.length) {

// This is always true when only 1 item is being loaded.
if (cnum) {
for (i = 0; i < c; i++) {
loadChartsQueueComplete(gecchartid);
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]
});
});
}
}
}
});
});
_GEC.AIQueue.push({
item: item.toLowerCase(),
chart: gecchartid
});


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


//remove an average line if it already exists
var timespan = decodeURIComponent((hash.match(/#t=([^#]*)/) || [])[1] || '')
.replace(/_/g, ' ')
if (seriesIndex !== -1) {
.split(',');
series[seriesIndex].remove();
}
var dates = [new Date(timespan[0]), new Date(timespan[1])];
var d = new Date(timespan[0]);
var extremes = _GEC['chart' + c].xAxis[0].getExtremes();


//add average line when there is only 1 item in the chart
if (dates[0] !== 'Invalid Date' && dates[1] === 'Invalid Date' && typeof zoom === 'number') {
var button = _GEC['chart' + c].rangeSelector.buttonOptions[zoom];
_GEC['chart' + i].addSeries(data[0][1]);
});
}
},
/**
* <doc>
*
* @param c {number|string}
*/
loadQueueComplete: function loadQueueComplete(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 setRange(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 reload(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 removeItem(item, c) {
var series = _GEC['chart' + c].series,
id,
i,
newhash,
data;


// find the item we want to remove
if (button.type === 'month') {
d.setMonth(d.getMonth() + button.count);
for (i = 0; i < series.length; i++) {
} else if (button.type === 'year') {
if (series[i].name.match(item)) {
id = i;
d.setYear(d.getFullYear() + button.count);
}
} else if (button.type === 'all') {
}
d = new Date(extremes.dataMax);
}


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


// remove item from url hash
if (dates[0] !== 'Invalid Date' && dates[1] !== 'Invalid Date') {
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, '_');
_GEC['chart' + c].xAxis[0].setExtremes(dates[0].getTime(), dates[1].getTime());
if (newhash.replace('#i=', '').length) {
return true;
}
location.hash = newhash;
} else if (location.hash.length) {
location.hash = '';
}


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


// @todo can we cache #addedItems somehow
/**
* <doc>
// remove item from list at top of graph
$('#addedItems' + c + ' [data-item="' + item + '"]').remove();
*
// cleanup list
* @param c {number|string}
$('#addedItems' + c).html($('#addedItems' + c).html().replace(/, , /g, ', ').replace(/, $/, '').replace(': , ', ': '));
* @param change {object}
*/
reload: function (c, change) {
var options = _GEC['chart' + c].options;


// if the list is empty show average, volume and item stats again
if (!options) {
if (!$('#addedItems' + c + ' [data-item]').length) {
// @todo do we need to return `false` here
$('#addedItems' + c).empty();
// @todo when does this happen
return false;
id = c == 'popup' ? $('#GEchartpopup').attr('data-chartid') : c;
data = getData(id, false, false, 'popup', function (data) {
}
reloadChart(c, {

$.extend(options, change);
series: data[0],
yAxis: data[1]
_GEC['chart' + c] = new Highcharts.StockChart(options);
},
});
});

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

var ch = _GEC['chart' + i],
// find the item we want to remove
for (i = 0; i < series.length; i++) {
exts = _GEC['chart' + i].yAxis[0].getExtremes();
if (series[i].name.match(item)) {
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);
id = i;
}
if (ch.yAxis[2]) {
}
exts = ch.yAxis[1].getExtremes();
ch.yAxis[1].setExtremes(0, exts.dataMax * 1.05);

// @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 () {
}
}
}
if (i === 'popup') {
// @todo use onclick event
$('#GEPermLink' + i).data('ooui-elem').setData(chartPermLinkUrl(i));
}
},
},
/**

* <doc>
// map old functions to new locations until uses are fixed
*
getSeriesIndex = chart.getSeriesIndex,
* @param c {number|string}
chartPermLinkUrl = chart.permLinkUrl,
* @param isSmall {boolean}
addItem = chart.addItem,
* @param avginput {number|string} (optional)
removeGraphItem = chart.removeItem,
* number component of input element used for altering the average interval
reloadChart = chart.reload,
* when the interval is in days
setChartRange = chart.setRange,
* when is this different to `c`?
setChartExtremes = chart.setExtremes,
*
loadChartsQueueComplete = chart.loadQueueComplete;
* @return {array} 2 item array containing X and Y respectively
* @todo expand on what X and Y are
*/
getData: function getData() {}
},
// 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;
// popupChart = chart.popup;
// getData = chart.getData;
// getData = chart.getData;
Line 1,278: Line 1,077:


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

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

} else {
if ($overlay.length) {
$popup.before($('<div>').attr('id', 'overlay').css('display', 'block'));
$overlay.toggle();
$overlay = $('#overlay');
} else {
}
$popup.before(
$overlay.on('click', function () {
$('<div>')
popupChart(false);
.attr('id', 'overlay')
});
.css('display', 'block')
);
if (typeof i === 'number') {
$(document).keydown(function (e) {
$overlay = $('#overlay');
}
// Esc
if (e.which === 27) {

$overlay.on('click', function () {
popupChart(false);
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?
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'));
});
});
}

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?
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) {
function rg(num) {
var colour = 'red';
var colour = 'red';
if (num > 0) {

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

return colour;
}
}

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

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


// happens when the first chart isSmall
// happens when the first chart isSmall
// and the average input id is actually the popup chart
// and the average input id is actually the popup chart
// the chart's id is popup, but the input's id is 0
// the chart's id is popup, but the input's id is 0
avginput = avginput || cin;
avginput = avginput || cin;
var pricesToDataList = function pricesToDataList(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
var pricesToDataList = function(inputData) {
// so price, 30-day-average and volume
var prices = inputData[0], annoData = inputData[1];
dataList = [{
_GEC.urlCache[url] = prices;
name: itemName,
_GEC.urlCache[annoUrl] = annoData;
data: data,
prices = Object.values(prices)[0];
lineWidth: isSmall ? 2 : 3
var volumeMultiplier = 1;//isOSRS ? 1 : 1000000
}];
for (i = 0; i < prices.length; i++) {
if (itemName.toLowerCase() === 'blank' && !chartLoaded) {
data.push([
dataList[0].color = '#000000';
// time
}
prices[i][0],
if (!isSmall && !isMedium && (itemName.toLowerCase() !== 'blank' || chartLoaded)) {
// @todo should this be parseInt?
inputAvg = $('#average' + avginput).data('ooui-elem').getNumericValue();
// price
prices[i][1]
]);


// @todo should this be isNaN?
if (prices[i][2] && !isSmall) {
if (inputAvg) {
volumes.push([
newhash = location.hash.replace(/#a=[^#]*|$/, '#a=' + inputAvg).replace(/ /g, '_');
// time
if (newhash.length) {
prices[i][0],
// volume
location.hash = newhash;
// volumes are in millions
prices[i][2] * volumeMultiplier
]);
}
}
}
}
inputAvg = inputAvg || 30;
dataList.push({
name: inputAvg + '-day average',
data: 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
// datalist's elements are essentially each line on the chart
yAxis = {
// so price, 30-day-average and volume
dataList = [{
title: {
text: isSmall ? null : isIndexChart ? 'Index history' : 'Price history',
name: itemName,
data: data,
offset: 60,
lineWidth: isSmall ? 2 : 3
rotation: 270,
}];
style: {
color: 'black',

fontSize: '12px'
if (itemName.toLowerCase() === 'blank' && !chartLoaded) {
dataList[0].color = '#000000';
}
}
},
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 (!isSmall && !isMedium && (itemName.toLowerCase() !== 'blank' || chartLoaded)) {
if (volumes.length >= 10 && !isSmall) {
inputAvg = $('#average' + avginput).data('ooui-elem').getNumericValue();
// set height to allow room for second y-axis
yAxis.height = 200;


// @todo should this be isNaN?
// convert to array and add volume data
if (inputAvg) {
yAxis = [yAxis, {
title: {
newhash = location.hash
text: volumeLabel,
.replace(/#a=[^#]*|$/, '#a=' + inputAvg)
.replace(/ /g, '_');
offset: 60,
rotation: 270,

if (newhash.length) {
style: {
location.hash = newhash;
color: 'black',
}
fontSize: '12px'
}
}
},

opposite: false,
inputAvg = inputAvg || 30;
dataList.push({
labels: {
name: inputAvg + '-day average',
align: 'right',
x: -8,
data: util.avg(data, inputAvg, isIndexChart ? 2 : 0),
lineWidth: 2,
y: 4
dashStyle: 'shortdash',
},
});
showEmpty: 0,
showLastLabel: 1,

offset: 0,
if (volumes.length >= 10) {
dataList.push({
lineWidth: 1,
name: volumeLabel,
lineColor: '#E0E0E0',
data: volumes,
height: 50,
type: 'area',
top: 325,
color: '#cc8400',
min: 0
}];
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 annoList = {};
var isPopup = !isSmall && !isMedium;
if (!isSmall) {
annoList = formatAnnoList(annoData, prices);
var dataType = isPopup ? 'all' : 'sample';
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 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)
}
}
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.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);
$(self.deps);

Latest revision as of 12:06, 20 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
 */
function _typeof(o) { "@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (o) { return typeof o; } : function (o) { return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o; }, _typeof(o); }
var conf = mw.config.get(['wgNamespaceNumber', 'wgPageName', 'wgTitle', 'wgSiteName']),
  // Are we on OSRS? Impacts selectors and volume labels / multipliers
  isOSRS = conf.wgSiteName == "RuneRealm 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 deps() {
      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.util', 'mediawiki.api', 'rs.highcharts', 'rs.highcharts-annotations', 'oojs-ui-core', 'oojs-ui.styles.icons-media'], self.init);
    },
    /**
     * Initial loading function
     */
    init: function init(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 makeOOUI(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 buildPopup() {
      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 setupCharts() {
      $('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 redraw() {
                  _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
   */
  util = {
    /**
     * <doc>
     *
     * @todo replace with $.extend
     *
     * @param a {object}
     * @param b {object} (optional)
     *
     * @return {object}
     */
    cloneObj: function cloneObj(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 avg(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(util.sum(list) / list.length * round) / round]);
        }
      }
      return avgs;
    },
    /**
     * Finds the sum of numbers in an array
     * Only called by `util.avg`
     *
     * @param arr {array} Array of number to find the sum of
     *
     * @return {number} Sum of the numbers in `arr`
     */
    sum: function sum(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 toKMB(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 ucFirst(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 sortPoints(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 getSeriesIndex(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 permLinkUrl(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 addItem(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/' + util.ucFirst(item)
        }).then(function (data, textStatus) {
          var OOUIextraItem = $('#extraItem' + gecchartid).data('ooui-elem'),
            pages = data.query.pages;
          if (textStatus !== 'success') {
            alert('An error occured while loading ' + item);
            mw.log(data);
          }
          var matches = [];
          var pageMissing = false;
          if (pages[-1]) {
            pageMissing = true;
          } else {
            var exchangeData = pages[Object.keys(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.util.getUrl('Module:Exchange/' + 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 loadQueueComplete(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 setRange(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 reload(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 removeItem(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 popup() {},
    /**
     * <doc>
     *
     * @param i
     */
    setExtremes: function setExtremes(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 getData() {}
  },
  // 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?
      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.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 pricesToDataList(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: 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.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);