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