MediaWiki:Common.js/GECharts.js

/** * 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  * Highstock change log  * * @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'        ] ),

/**        *          *         * @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' ) ) { mw.loader.implement(                       'rs.highcharts',                        ['http://code.highcharts.com/stock/highstock.js'],                        {}, {}                    ); }

mw.loader.using( ['mediawiki.util', 'mediawiki.api', 'rs.highcharts'], self.init ); },

/**            * Initial loading function */           init: function  { ( 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; },

/**            *              */            buildPopup: function  { $( 'body' ).append(                   $( ' ' )                        .attr( 'id', 'GEchartpopup' )                        .css( 'display', 'none' )                        .append( $( ' ' )                               .attr( 'id', 'closepopup' ) .append(                                   $( '' )                                        // @todo kill script url                                        .attr( 'href', 'javascript:popupChart(false)' )                                        // @todo find something better than this                                        //       CSS?                                        .text( 'Close popup [X]' )                                ), $( ' ' )                               .attr( 'id', 'itemstats' ), $( ' ' )                               .attr( {                                    // @todo kill script url                                    action: 'javascript:addItem("popup")',                                    id: 'chartPropertiespopup'                                } ) .append(                                   $( ' ' )                                        .append( 'Average: ', $( ' ' )                                               .attr( {                                                    type: 'number',                                                    min: '1',                                                    id: 'averagepopup'                                                } ), ' days' ),                                   $( ' ' )                                        .addClass( 'moreitems' )                                        .append( 'Add more item(s): ', $( ' ' )                                               .attr( {                                                    type: 'text',                                                    id: 'extraItempopup'                                                } ), ' ',                                           $( '' ) .text( 'Note:' ), ' If you add more items, the averages will not be shown.' ),                                   $( ' ' )                                        .text( 'Submit' ),                                    $( ' ' )                                        .attr( 'type', 'reset' )                                        .append( 'Reset', $( ' ' )                                               .addClass( 'resetallfields' ) .text( ' all fields' ) )                               ),                            $( '' ) .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' ), $( ' ' )                               .attr( 'id', 'addedItemspopup' ), $( ' ' )                               .attr( 'id', 'GEpopupchart' ) )               );            },

/**            *              */            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(                           ' Average:  days '+(isIndexChart?:' Add more item(s):  Note: If you add more items, the averages will not be shown.')+' Submit  '+(wgNamespaceNumber == 112 && wgTitle.split('/')[1] == 'Data'||wgPageName == 'Grand_Exchange_Market_Watch/Chart' ? 'Permanent link':'')+' '                       ); }

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 = 'Enlarge chart'; // @todo this doesn't do anything on small charts //      is it supposed to? var zoomOut = 'Zoom out';

//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: ' {point.key} ', 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 = { /**            *              *             * @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 = { /**            *              *             * @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 'http://rs.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] ); }           },

/**            *              *             * @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: ',                            $( '' )                                .attr( { href: 'javascript:removeGraphItem("' + iname + '","' + c + '")', 'data-item': iname } )                               .text( iname )                        ); itemhash = '#i=' + iname; } else { $addedItems.append(                           ', ',                            $( '' )                                .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 ); }               }            },

/**            *              *             * @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; },

/**            *              *             * @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 ); },

/**            *              *             * @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; },

/**            *              *             * @param i {number|string} */           popup: function  {},

/**            *              *             * @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 ); }           },

/**            *              *             * @param c {number|string} */           addPopupPriceInfo: function  {},

/**            *              *             * @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(               $( ' ' )                    .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 = ' '; $('#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 ) );