Anonymous 5bd9affb048c5 (talk | contribs) mNo edit summary |
Anonymous 5bd9affb048c5 (talk | contribs) mNo edit summary |
||
Line 20: | Line 20: | ||
/*global jQuery, mediaWiki, Highcharts, wgPageName, wgTitle, wgNamespaceNumber */ |
/*global jQuery, mediaWiki, Highcharts, wgPageName, wgTitle, wgNamespaceNumber */ |
||
− | |||
− | define = null; |
||
;( function ( $, mw ) { |
;( function ( $, mw ) { |
||
Line 59: | Line 57: | ||
deps: function () { |
deps: function () { |
||
if ( !mw.loader.getState( 'rs.highcharts' ) ) { |
if ( !mw.loader.getState( 'rs.highcharts' ) ) { |
||
+ | define.amd = false; |
||
mw.loader.implement( |
mw.loader.implement( |
||
'rs.highcharts', |
'rs.highcharts', |
||
Line 73: | Line 72: | ||
*/ |
*/ |
||
init: function () { |
init: function () { |
||
+ | define.amd = true; |
||
( function() { |
( function() { |
||
var newhash = location.hash |
var newhash = location.hash |
Latest revision as of 00:39, 4 September 2018
/** <nowiki>
* Grand Exchange Charts
* Displays price data of item(s) in a chart
*
* Original: http://runescape.wikia.com/wiki/MediaWiki:Common.js/GECharts.js
*
* Highstock docs <http://api.highcharts.com/highstock>
* Highstock change log <http://www.highcharts.com/documentation/changelog#highstock>
*
* @author Joeytje50
* @author Azliq7
* @author Cqm
*
* @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, Highcharts, wgPageName, wgTitle, wgNamespaceNumber */
;( function ( $, mw ) {
/**
* Cache mw.config variables
*/
var conf = mw.config.get( [
'wgNamespaceNumber',
'wgPageName',
'wgTitle'
] ),
/**
* <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 ) || ''
},
/**
* Startup methods
*/
self = {
/**
* Loads and implements any required dependencies
*/
deps: function () {
if ( !mw.loader.getState( 'rs.highcharts' ) ) {
define.amd = false;
mw.loader.implement(
'rs.highcharts',
['https://code.highcharts.com/stock/highstock.js'],
{}, {}
);
}
mw.loader.using( ['mediawiki.util', 'mediawiki.api', 'rs.highcharts'], self.init );
},
/**
* Initial loading function
*/
init: function () {
define.amd = true;
( 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>
*/
buildPopup: function () {
$( 'body' ).append(
$( '<div>' )
.attr( 'id', 'GEchartpopup' )
.css( 'display', 'none' )
.append(
$( '<div>' )
.attr( 'id', 'closepopup' )
.append(
$( '<a>' )
// @todo kill script url
.attr( 'href', 'javascript:popupChart(false)' )
// @todo find something better than this
// CSS?
.text( 'Close popup [X]' )
),
$( '<div>' )
.attr( 'id', 'itemstats' ),
$( '<form>' )
.attr( {
// @todo kill script url
action: 'javascript:addItem("popup")',
id: 'chartPropertiespopup'
} )
.append(
$( '<label>' )
.append(
'Average: ',
$( '<input>' )
.attr( {
type: 'number',
min: '1',
id: 'averagepopup'
} ),
' days'
),
$( '<div>' )
.addClass( 'moreitems' )
.append(
'Add more item(s): ',
$( '<input>' )
.attr( {
type: 'text',
id: 'extraItempopup'
} ),
' ',
$( '<b>' )
.text( 'Note:' ),
' If you add more items, the averages will not be shown.'
),
$( '<button>' )
.text( 'Submit' ),
$( '<button>' )
.attr( 'type', 'reset' )
.append(
'Reset',
$( '<span>' )
.addClass( 'resetallfields' )
.text( ' all fields' )
)
),
$( '<a>' )
.addClass( 'GEPermLink' )
.attr( {
id: 'GEPermLinkpopup',
title: 'Permanent link to the current chart settings and items. Right click to copy the url.',
// @todo kill script url
href: 'javascript:void(0)'
} )
.text( 'Permanent link' ),
$( '<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' ),
isIndexChart = /index/i.test( dataItem ),
isInfobox = $dataPrices.is( '.infobox *, .infobar *' ),
itemName = dataItem || wgTitle.split( '/' )[0],
dataList = getData( c, isSmall ),
yAxis = dataList[1],
zoom;
if ( !$dataPrices.length ) {
return;
}
dataList = getData( c, isSmall );
yAxis = dataList[1];
// @todo rename this to xAxis
dataList = dataList[0];
// setting up the form and chart elements
if ( !isSmall ) {
$dataChart.before(
'<form action="javascript:addItem('+c+')" id="chartProperties'+c+'"><label>Average: <input type="number" min="1" id="average'+c+'" value="'+_GEC.average+'"> days</label>'+(isIndexChart?'':'<br/>Add more item(s): <input type="text" id="extraItem'+c+'"> <b>Note:</b> If you add more items, the averages will not be shown.')+'<br/><button>Submit</button><input type="reset" value="Reset'+(isIndexChart?'':' all fields')+'"/></form>'+(wgNamespaceNumber == 112 && wgTitle.split('/')[1] == 'Data'||wgPageName == 'Grand_Exchange_Market_Watch/Chart' ? '<a class="GEPermLink" id="GEPermLink'+c+'" title="Permanent link to the current chart settings and items. Right click to copy the url." href="javascript:void(0)">Permanent link</a>':'')+'<div id="addedItems'+c+'"></div>'
);
}
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 = '<a href="javascript:popupChart(' + c + ');" style="text-decoration:underline;color:inherit;font-size:inherit;">Enlarge chart</a>';
// @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: isSmall ? 'x' : '',
//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,
backgroundColor: 'white',
align: 'right',
layout: 'vertical',
verticalAlign: 'top',
y: 85
},
title: {
text: isSmall?(isInfobox?enlarge:itemName):'Grand Exchange Market Watch',
useHTML: true,
style: {
color: 'black',
fontSize: isSmall?(enlarge?'13px':'15px'):'18px',
},
},
subtitle: {
text: isSmall?(isInfobox?zoomOut:enlarge+' | '+zoomOut):(itemName.toLowerCase()=='blank'?'Historical chart':itemName),
useHTML: true,
y: 35,
style: {
color: '#666',
fontSize: isSmall?'13px':'15px',
},
},
rangeSelector: {
enabled: !isSmall,
selected: zoom,
inputBoxStyle: {
right: '15px',
display: isSmall?'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
},
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
}],
yAxis: yAxis,
series: dataList,
colors: window.GEMWChartColors||['#4572A7','#AA4643','#89A54E','#80699B','#3D96AE','#DB843D','#92A8CD','#A47D7C','#B5CA92']
} );
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 ((wgNamespaceNumber == 112 && wgTitle.split('/')[1] == 'Data'||wgPageName == 'Grand_Exchange_Market_Watch/Chart') && location.hash.match('#i=')) {
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*$/)) addItem(0,items[i]);
}
}
} );
}
},
/**
* General helper methods
*/
util = {
/**
* <doc>
*
* @todo replace with $.extend
*
* @param a {object}
* @param b {object} (optional)
*
* @return {object}
*/
cloneObj: function ( a, b ) {
if ( typeof a !== 'object' ) {
return '';
}
if ( typeof b !== 'object' ) {
b = {};
}
for ( var key in a ) {
if ( a.hasOwnProperty( key ) ) {
b[key] = a[key];
}
}
return b;
},
/**
* Averages prices across a specified time interval
*
* @param arr {array} Array of arrays, where each member of `arr`
* is in the format [time, price]
* Which is how we store the price data
* @example [x-coord, y-coord]
* @param amt {number} Interval to average across in days
* @param round {number} (optional) Number of decimal places to round to
* Defaults to 0
*
* @return {array} Array of arrays, where each member of the return array
* is in the format [time, price] (as above)
* and
*/
avg: function ( arr, amt, round ) {
amt = amt || arr.length;
// convert `round` into a number we can use for rounding
round = Math.pow( 10, round || 0 );
var avgs = [],
list = [],
i;
// adds each price to `list`
// when `amt` is reached, average the contents of `list`
//
// each iteration after `amt` is reached averages the contents of `list`
// which is continuously being updated as each iteration
// after `amt` is reached replaces a member of `list`
// @example when `i` is 31 the current price replaces `list[1]`
// when `i` is 35 the current price replaces `list[5]`
for ( i = 0; i < arr.length; i++ ) {
list[i % amt] = arr[i][1];
if ( i >= amt ) {
avgs.push( [
// don't modify the time (y-coord)
arr[i][0],
Math.round( ( 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 util.addCommas( neg + num );
},
/**
* Capitalises first character of a string
*
* @source <http://stackoverflow.com/a/1026087>
*
* @param str {string}
*
* @return {string}
*/
ucFirst: function ( str ) {
return str.charAt( 0 ).toUpperCase() + str.slice( 1 );
},
/**
* Adds commas to a number string
*
* @example 123456.78 -> 123,456.78
*
* @param num {number|string} A number to add commas to
*
* @returns {string} The number with commas
*/
addCommas: function (num) {
num += '';
var x = num.split('.'),
x1 = x[0],
x2 = x.length > 1
? '.' + x[1]
: '',
rgx = /(\d+)(\d{3})/;
while (rgx.test(x1)) {
x1 = x1.replace(rgx, '$1,$2');
}
return x1 + x2;
}
},
/**
* Chart methods
*/
chart = {
/**
* <doc>
*
* @param id {string|number}
* @param match {string} is normally the 'line' that isn't an item's price data
* such as average or volume
*
* @return {number}
*/
getSeriesIndex: function ( id, match ) {
var chart = _GEC['chart' + id],
series = chart.series,
i;
if ( chart ) {
for ( i = 0; i < series.length; i++ ) {
if (series[i].name.match(match)) {
return i;
}
}
return -1;
}
// @todo what happens if !chart
},
/**
* Creates a URL with preset options
*
* @todo change to url params
* @todo document the individual params/options
*
* @param id {number|string}
*
* @return {string}
*/
permLinkUrl: function ( id ) {
var chart = _GEC['chart' + id],
xt = chart.xAxis[0].getExtremes(),
series = chart.series,
minDate = ( new Date( xt.min ) )
.toDateString()
.split( ' ' )
.slice( 1 )
.join( '_' ),
maxDate = ( new Date( xt.max ) )
.toDateString()
.split( ' ' )
.slice( 1 )
.join( '_' ),
inputAvg = parseInt( $( '#average' + id ).val(), 10 ),
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 'https://oldschoolrunescape.wikia.com/wiki/GEMW/C' + urlHash;
},
/**
* Add a new item to the chart
*
* @param i
* @param it {string} (optional)
*/
addItem: function ( i, it ) {
_GEC.chartid = i;
var $extraItem = $( '#extraItem' + i ),
item = ( it || '' ).trim() || $extraItem.val(),
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.' );
}
$extraItem.val( '' );
return false;
}
$extraItem.prop( 'disabled', true );
$.get(
'/api.php',
{
action: 'query',
prop: 'revisions',
rvprop: 'content',
format: 'json',
titles: 'Module:Exchange/' + util.ucFirst( item ) + '/Data'
},
// @todo can this be split out into a separate method?
function ( resp ) {
var $extraItem = $( '#extraItem' + gecchartid ),
item = mw.util.getParamValue( 'titles', this.url )
.replace( 'Exchange:', '' )
.replace( 'Module:Exchange/', '' )
.replace( '/Data', '' ),
prices,
data = [],
pages = resp.query.pages;
// action=query doesn't throw many errors
// all of which are down to the supplied params (stuff we do here)
if ( resp.error ) {
alert( 'An error occured while loading ' + item );
mw.log( resp );
}
// page not found
if ( pages[-1] ) {
if ( $( '#extraItem' + gecchartid ).val().length ) {
alert( 'The item ' + item + ' doesn\'t exist on our Grand Exchange database.' );
$extraItem.prop( 'disabled', false ).val( '' );
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 );
}
$extraItem.prop( 'disabled', false ).val( '' );
return false;
}
// @todo what is this for
// should it be resp.parse.links?
/*
if (resp.links) {
$.get('/api.php?action=parse&format=json&page='+encodeURIComponent(resp.links[0]['*']), this.success);
return false;
}
*/
_GEC.AILoaded.push( item );
prices = resp.query.pages[Object.keys( resp.query.pages )[0]]
.revisions[0]['*'];
prices = prices
.replace( /return\s*\{/, '' )
.replace( /\}/, '' )
.split( /,/ );
data = [];
prices.forEach( function ( elem ) {
// point as in data point
// time:price:volume
var point = elem
.trim()
.replace( /'/g, '' );
if ( point ) {
point = point.split( ':' );
data.push( [
// time
parseInt( point[0], 10 ) * 1000,
// price
parseInt( point[1], 10 )
] );
}
} );
_GEC.AIData.push( {
name: item,
data: data,
id: item,
gecchartid: gecchartid,
lineWidth: 2
} );
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( i );
}
}
);
_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 == 'Grand_Exchange_Market_Watch/Chart' &&
$addedItems.find( 'a' ).length === 1
)
) {
id = (i === 'popup' ? $( '#GEchartpopup' ).attr( 'data-chartid' ) : i);
data = getData( id, false, i );
series = _GEC['chart' + i].series;
seriesIndex = getSeriesIndex( i, 'average' );
//remove an average line if it already exists
if ( seriesIndex !== -1 ) {
series[seriesIndex].remove();
}
//add average line when there is only 1 item in the chart
_GEC['chart' + i].addSeries( data[0][1] );
}
},
/**
* <doc>
*
* @param c {number|string}
*/
loadQueueComplete: function ( cin ) {
var cnum = typeof cin === 'number', //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 = [],
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' );
}
} else {
isSmall = $( '#GEdatachart' + c ).hasClass( 'smallChart' );
}
if (cnum) {
for (i = 0; i < c; i++) {
if ( getSeriesIndex( _GEC.AIQueue[i].chart, '7-day volume' ) !== -1 ) {
id = i === 'popup' ? $( '#GEchartpopup' ).attr( 'data-chartid' ) : i;
chartdata = getData( id, true );
chartdata[1].title.text = 'Price history';
reloadChart( i, {
series: chartdata[0],
yAxis: chartdata[1]
} );
}
}
} else {
if ( getSeriesIndex( c, '7-day volume' ) !== -1 ) {
id = c === 'popup' ? $( '#GEchartpopup' ).attr( 'data-chartid' ) : c;
chartdata = getData( id, true );
chartdata[1].title.text = 'Price history';
reloadChart( c, {
series: chartdata[0],
yAxis: chartdata[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 ( isSmall[data[i].gecchartid] && typeof Array.isArray(_GEC.addedData[data[i].gecchartid]) ) {
_GEC.addedData[data[i].gecchartid] = [];
}
}
} else {
if ( isSmall && typeof Array.isArray(_GEC.addedData[data[c].gecchartid]) ) {
_GEC.addedData[data[c].gecchartid] = [];
}
}
for ( i = 0; i < data.length; i++ ) {
if ( data[i] ) {
console.log("adding series for "+i+": "+data[i]);
_GEC['chart' + data[i].gecchartid].addSeries( data[i] );
}
if ( cnum && isSmall ) {
console.log("adding series for "+i+": "+data[i].gecchartid+" "+data[i] + " " + _GEC.addedData[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 ).prop( 'disabled', false ).val( '' );
}
} else {
setChartExtremes( c );
$( '#extraItem' + c ).prop( 'disabled', false ).val( '' );
}
$( '#itemstats' ).hide();
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 (
(
wgNamespaceNumber == 112 && wgTitle.split( '/' )[1] == 'Data' ||
wgPageName == 'Grand_Exchange_Market_Watch/Chart'
) &&
itemhash.replace( '#i=', '' ).length
) {
location.hash = itemhash;
}
_GEC.AIQueue = [];
_GEC.AILoaded = [];
_GEC.AIData = [];
if (cnum) {
for (i = 0; i < c; i++) {
hadBlank = removeGraphItem( 'Blank', data[i].gecchartid );
if ( hadBlank ) {
setChartRange( data[i].gecchartid );
}
}
} else {
hadBlank = removeGraphItem( 'Blank', c );
if ( hadBlank ) {
setChartRange( c );
}
}
},
/**
* <doc>
*
* @param c {number|string}
*
* @return {boolean}
*/
setRange: function ( c ) {
var zoom = parseInt( ( location.hash.match( /#z=([^#]*)/ ) || [] )[1], 10 );
zoom = zoom && zoom <= 6 && zoom >= 0 ? zoom - 1 : ( zoom === 0 ? 0 : 2 );
var hash = location.hash;
var hasT = conf.wgNamespaceNumber === 112 && conf.wgTitle.split( '/' )[1] === 'Data' || conf.wgPageName === 'Grand_Exchange_Market_Watch/Chart';
if ( typeof c === 'number' && ( hasT && !hash.match( '#t=' ) || !hasT ) ) {
$( '#GEdatachart' + c + ' .zoomButton' ).eq( zoom ).click();
return true;
}
var timespan = decodeURIComponent( ( hash.match( /#t=([^#]*)/ ) || [] )[1] || '' )
.replace( /_/g, ' ' )
.split( ',' );
var dates = [new Date( timespan[0] ), new Date( timespan[1] )];
var d = new Date( timespan[0] );
var extremes = _GEC['chart' + c].xAxis[0].getExtremes();
if ( dates[0] !== 'Invalid Date' && dates[1] === 'Invalid Date' && typeof zoom === 'number' ) {
var button = _GEC['chart' + c].rangeSelector.buttonOptions[zoom];
if ( button.type === 'month' ) {
d.setMonth( d.getMonth() + button.count );
} else if ( button.type === 'year' ) {
d.setYear( d.getFullYear() + button.count );
} else if ( button.type === 'all' ) {
d = new Date( extremes.dataMax );
}
dates[1] = d;
}
if ( dates[0] !== 'Invalid Date' && dates[1] !== 'Invalid Date') {
_GEC['chart' + c].xAxis[0].setExtremes( dates[0].getTime(), dates[1].getTime() );
return true;
}
return false;
},
/**
* <doc>
*
* @param c {number|string}
* @param change {object}
*/
reload: function ( c, change ) {
var options = _GEC['chart' + c].options;
if ( !options ) {
// @todo do we need to return `false` here
// @todo when does this happen
return false;
}
$.extend( options, change );
_GEC['chart' + c] = new Highcharts.StockChart( options );
},
/**
* <doc>
*
* @param item {string}
* @param c {number|string}
*
* @return {boolean}
*/
removeItem: function ( item, c ) {
var series = _GEC['chart' + c].series,
id,
i,
newhash,
data;
// find the item we want to remove
for ( i = 0; i < series.length; i++ ) {
if ( series[i].name.match( item ) ) {
id = i;
}
}
// @todo when does this happen
// when we can't find the item?
if ( typeof id !== 'number' ) {
return false;
}
// remove item from url hash
newhash = location.hash
.replace( /_/g, ' ' )
.replace( new RegExp( '(#i=[^#]*),?' + item, 'i' ), '$1' )
.replace( /,,/g, ',' )
.replace( /,#/g, '#' )
.replace( /#i=,/g, '#i=' )
.replace( /#i=($|#)/, '$1' )
.replace( / /g, '_' );
if ( newhash.replace( '#i=', '' ).length ) {
location.hash = newhash;
} else if ( location.hash.length ) {
location.hash = '';
}
// remove the item from the chart
series[id].remove();
// reset extremes?
setChartExtremes( c );
// @todo can we cache #addedItems somehow
// remove item from list at top of graph
$( '#addedItems' + c + ' [data-item="' + item + '"]' ).remove();
// cleanup list
$( '#addedItems' + c ).html(
$( '#addedItems' + c )
.html()
.replace( /, , /g, ', ' )
.replace( /, $/, '' )
.replace( ': , ', ': ' )
);
// if the list is empty show average, 7-day vol 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 );
reloadChart( c, {
series: data[0],
yAxis: data[1]
} );
$( '#itemstats' ).show();
}
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 ).get( 0 ).href = chartPermLinkUrl( i );
}
},
/**
* <doc>
*
* @param c {number|string}
*/
addPopupPriceInfo: function () {},
/**
* <doc>
*
* @param c {number|string}
* @param isSmall {boolean}
* @param avginput {number|string} (optional)
* number component of input element used for altering the average interval
* when the interval is in days
* when is this different to `c`?
*
* @return {array} 2 item array containing X and Y respectively
* @todo expand on what X and Y are
*/
getData: function () {}
},
// map old functions to new locations until uses are fixed
getSeriesIndex = chart.getSeriesIndex,
chartPermLinkUrl = chart.permLinkUrl,
addItem = chart.addItem,
removeGraphItem = chart.removeItem,
reloadChart = chart.reload,
setChartRange = chart.setRange,
setChartExtremes = chart.setExtremes,
loadChartsQueueComplete = chart.loadQueueComplete;
// popupChart = chart.popup;
// addPopupPriceInfo = chart.addPopupPriceInfo;
// 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' ).val( _GEC.average );
addPopupPriceInfo( i );
$popup.attr( 'data-chartid', i );
options = {};
data = getData( i, false );
// @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.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] ? [data[0][0]] : data[0];
options.yAxis = data[1];
_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' ).first().attr( 'href', chartPermLinkUrl('popup') );
}
function rg( num ) {
var colour = 'red';
if ( num > 0 ) {
colour = 'green';
} else if ( num === 0 ) {
colour = 'blue';
}
return colour;
}
function addPopupPriceInfo( c ) {
if (($('#GEdataprices'+c).attr('data-item')||'').match(/index/i)) return false;
var prices = $('#GEdataprices'+c).html().split(/,\s/);
var data = [];
var curprice, i;
for (i=0;i<prices.length;i++) {
if (prices[i].replace(/\s/g,'').length) {
curprice = prices[i].split(':');
data.push(parseInt(curprice[1]));
}
}
var datal = data.length;
curprice = data[datal-1];
var priceDiffs = [], percentDiffs = [];
var dayDiffs = [1,7,30,90,180];
for (i=0;i<dayDiffs.length;i++) {
var pthen = data[datal-dayDiffs[i]-1];
var pdiff = curprice - pthen;
if (!isNaN(pdiff)) {
priceDiffs.push(pdiff);
percentDiffs.push(Math.round(pdiff / pthen * 1000)/10);
} else {
dayDiffs.slice(0,i+1);
}
}
var itemStats = '<table class="wikitable">'+
'<caption onclick="$(this).next(\'tbody\').toggle();$(this).children().toggle()"><span style="display:none;white-space:nowrap">Price Info</span><span>Close</span></caption><tbody>'+
'<tr><th>Price</th><td>' + util.toKMB( curprice ) + '</td></tr>';
for (i=0;i<priceDiffs.length;i++) {
itemStats += '<tr><th>'+dayDiffs[i]+'-day</th><td style="color:'+rg(priceDiffs[i])+'">'+(priceDiffs[i]>0?'+':'') + util.toKMB( priceDiffs[i] ) + ' ('+percentDiffs[i]+'%)</td></tr>';
}
itemStats += $('#GEdataprices'+c).attr('data-value').length?'<tr><th><a href="/wiki/Value" target="_blank" title="Value">Value</a></th><td>'+util.toKMB($('#GEdataprices'+c).attr('data-value'))+'</td></tr>':'';
itemStats += $('#GEdataprices'+c).attr('data-limit').length?'<tr><th><a href="/wiki/Grand_Exchange#Trade_Restrictions" target="_blank" title="Grand Exchange buying limit">Buy limit</a></th><td>'+util.toKMB($('#GEdataprices'+c).attr('data-limit'))+'</td></tr>':'';
itemStats += '<tr><th colspan="2"><a href="/wiki/Exchange:'+encodeURI($('#GEdataprices'+c).attr('data-item'))+'" target="_blank" title="'+$('#GEdataprices'+c).attr('data-item')+'">More price info</a></th></tr>';
itemStats += '</tbody></table>';
$('#itemstats').html(itemStats);
}
function getData( cin, isSmall, avginput ) {
var c = cin === 'popup' ? $( '#GEchartpopup' ).attr( 'data-chartid' ) : cin,
$dataPrices = $( '#GEdataprices' + c ),
dataItem = $dataPrices.attr( 'data-item' ),
isIndexChart = /index/i.test( dataItem ),
itemName = dataItem || 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 || c;
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] );
}
} else {
// @todo convert this to looking is a data-data or data-itemdata attribute
// which allows us to ditch some CSS hacks in wikiamobile
// store the price data in this attribute instead
// see <http://stackoverflow.com/a/1496150> for practical limitations
if ( $( '#GEdataprices' + c ).attr( 'data-data' ) ) {
prices = $( '#GEdataprices' + c ).attr( 'data-data' ).split( '|' );
} else {
prices = $( '#GEdataprices' + c ).html().split( /,\s/ );
}
}
for ( i = 0; i < prices.length; i++ ) {
if ( prices[i].trim() ) {
thisprice = prices[i].split( ':' );
data.push( [
// time
parseInt( thisprice[0], 10 ) * 1000,
// @todo should this be parseInt?
// price
parseFloat( thisprice[1], 10 )
] );
if ( thisprice[2] && !isSmall ) {
volumes.push( [
// time
parseInt( thisprice[0], 10 ) * 1000,
// volume
// volumes are in millions
parseFloat( thisprice[2], 10 ) * 1000000
] );
}
}
}
// 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 && ( itemName.toLowerCase() !== 'blank' || chartLoaded ) ) {
inputAvg = parseInt( $( 'input#average' + avginput ).val(), 10 );
// @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: '7-day volume',
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: '7-day volume',
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
}];
}
return [dataList, yAxis];
}
$( self.deps );
}( jQuery, mediaWiki ) );