ScriptUsage

```
require(['wikia.window', 'jquery', 'mw'], function (window, $, mw) {

 'use strict';

 // Load Chart.js, I18n-js
 $.when(
 $.getScript('/load.php?mode=articles&articles=u:dev:Chart.js&only=scripts'),
 $.getScript('/load.php?mode=articles&articles=u:dev:I18n-js/code.js&only=scripts')
 ).done(function() {

 // Config
 const config = $.extend({}, mw.config.get(['wgLegalTitleChars', 'wgPageName']), {token: mw.user.tokens.get('editToken')});

 // More reliable wgServer for non-English wikis
 config.wgServer = window.location.href.split(/(\/wiki\/)|(\/index)/)[0];

 // Global constants
 const body = $('body'),
 charts = {},
 curDate = new Date(),
 curMonthId = new Date(curDate.getUTCFullYear(), curDate.getUTCMonth(), 1).getTime() / 1000;

 const local = {

 css: {

 /\*\*
 \* Returns the width of string in px
 \*
 \* @param {string} str - string to calculate width of
 \* @param {string} font - font properties
 \*
 \* @returns {number} width of text (px)
 \*/
 getTextWidth: function(str, font) {

 // Ensure font is defined
 font = font || body.css('font');

 // Unique id for element
 const id = $('.scriptusage-getTextWidth').length;

 body.append('<span id="scriptusage-getTextWidth-' + id + '" class="scriptusage-getTextWidth" style="font:' + font + ';">' + str.replace(/\s/, '&nbsp;') + '</span>');
 const textWidth = $('#scriptusage-getTextWidth-' + id).width();
 $('#scriptusage-getTextWidth-' + id).remove();
 return textWidth;

 },

 },

 date: {

 /\*\*
 \* Formats numeric date id into string.
 \*
 \* @param {number} dateId - Date of data in seconds
 \* @param {boolean} useLocal - Whether to format date with local time or UTC
 \*
 \* @returns {string} formatted date
 \*/
 formatDate: function(dateId, useLocal, omitDay) {

 if (Number.isNaN(parseInt(dateId, 10)) === false && dateId !== '') {
 const dateObj = new Date(dateId \* 1000);
 var monthName,
 displayDate;

 if (useLocal === true) {
 monthName = local.date.getMonthName(dateObj.getMonth());
 displayDate = monthName + ' ' + dateObj.getFullYear();
 } else {
 monthName = local.date.getMonthName(dateObj.getUTCMonth());
 displayDate = monthName + ' ' + dateObj.getUTCFullYear();
 }

 return displayDate;
 } else {
 return '';
 }

 },

 /\*\*
 \* Gets the # of days in a specified month.
 \*
 \* @param {number} m - Month #
 \* @param {number} y - Year #
 \*
 \* @returns {number} # of days in month
 \*/
 getDaysInMonth: function(m, y) {

 y = y + Math.floor(m / 12);
 m = Math.abs((m % 12) + 12) % 12;

 const date = new Date(y, m, 28);
 var days = 27;

 while (date.getMonth() === m) {

 date.setDate(date.getDate() + 1);
 days++;

 }

 return days;

 },

 /\*\*
 \* Gets name of month from month #.
 \*
 \* @param {number} m - Month #
 \*
 \* @returns {string} Month name
 \*/
 getMonthName: function(m) {

 m = Math.abs((m % 12) + 12) % 12;

 switch (m) {

 case 0:

 return 'Jan.';

 case 1:

 return 'Feb.';

 case 2:

 return 'Mar.';

 case 3:

 return 'Apr.';

 case 4:

 return 'May';

 case 5:

 return 'June';

 case 6:

 return 'July';

 case 7:

 return 'Aug.';

 case 8:

 return 'Sept.';

 case 9:

 return 'Oct.';

 case 10:

 return 'Nov.';

 case 11:

 return 'Dec.';

 }

 },
 },

 input: {

 /\*\*
 \* Clears all errors in a container.
 \*
 \* @param {string} container - Selector of error container
 \*
 \* @returns {void}
 \*/
 clearErrors: function(container) {

 $('.scriptusage-util-error', $(container)).addClass('scriptusage-util-error-hidden');
 $('.scriptusage-util-error', $(container)).fadeOut(200,
 function() {

 $(this).remove();

 }
 );

 },

 /\*\*
 \* Displays an error upon invalid user input.
 \*
 \* @param {string} text - Error text to display to user.
 \* @param {string} type - Type of error; used to prevent duplicate errors.
 \* @param {string} container - Selector of error container
 \*
 \* @returns {void}
 \*/
 error: function(text, type, container) {

 if ($('#scriptusage-util-error-' + type, $(container)).length === 0 || ($('#scriptusage-util-error-' + type, $(container)).length > 0 && $('#scriptusage-util-error-' + type, $(container)).hasClass('scriptusage-util-error-hidden') === true)) {
 $('<div class="scriptusage-util-error" id="scriptusage-util-error-' + type + '">' + text + '</div>')
 .prependTo($(container))
 .slideDown(500);
 }

 },

 /\*\*
 \* Checks whether an inputted page name exists.
 \*
 \* @param {string} pageName - Name of the page to be checked.
 \*
 \* @returns {boolean} Whether page exists
 \*/
 pageExists: function(pageName, callback) {

 if (pageName.length <= 0 || (new RegExp('[^' + config.wgLegalTitleChars + ']', 'i')).test(pageName) === true) {
 callback(false);
 }

 $.get(config.wgServer + '/api.php?action=query&format=json&titles=' + encodeURIComponent(pageName))
 .done(function(searchResults) {

 if (Object.keys(searchResults.query.pages)[0] > -1) {
 callback(true);
 } else {
 callback(false);
 }

 })
 .error(function() {

 callback(false);

 });

 },

 },

 time: {

 /\*\*
 \* Converts seconds to days, hours, minutes, and seconds.
 \*
 \* @param {number} sec - # of seconds
 \*
 \* @returns {Object} Days, hours, minutes, seconds as object
 \*/
 convertTime: function(sec) {

 sec = local.parseNum(sec);

 if (Number.isNaN(sec) === true) {
 mw.log('RailWAM API: "math.convertTime" was aborted. "sec" must be numeric. Invalid value: ' + sec);

 return {};
 }

 // Convert to days, hours, min, sec
 const days = Math.floor(sec / 86400),
 hours = Math.floor((sec - (days \* 86400)) / 3600),
 min = Math.floor((sec - (days \* 86400) - (hours \* 3600)) / 60),
 remainder = Math.floor(sec - (days \* 86400) - (hours \* 3600) - (min \* 60));

 // Return object with time values
 return {
 d: days,
 h: hours,
 m: min,
 s: remainder,
 };

 },

 /\*\*
 \* Creates an HTML timer.
 \*
 \* @param {Object} time - Use the following properties:
 \* d - days
 \* h - hours
 \* m - minutes
 \* s - seconds
 \*
 \* @returns {string} HTML of timer
 \*/
 createTimer: function(time) {

 const days = time.d > 0 ? time.d + ' ' + i18n.msg('days').plain() + ', ' : '',
 hours = time.h > 0 ? time.h + ' ' + i18n.msg('hours').plain() + ', ' : '',
 min = time.m > 0 ? time.m + ' ' + i18n.msg('minutes').plain() + ', ' : '';

 return days + hours + min + time.s + ' ' + i18n.msg('seconds').plain();

 },

 },
 };

 // CSS constants
 const bodyTextColor = body.css('color'),
 link = $('<div style="display: none;"><a></a></div>').appendTo('body'),
 linkTextColor = $('a', link).css('color'),
 scrollbarContainer = $('<div style="overflow: scroll; position: absolute; visibility: hidden; width: 100px;"></div>').appendTo('body'),
 scrollbarWidth = 100 - $('<div style="width: 100%"></div>').appendTo(scrollbarContainer).width();

 // Remove extra elements
 link.remove();
 scrollbarContainer.remove();

 // Global datepicker variables
 var dpStartDate = '',
 dpEndDate = '';

 // Create chart plugins
 Chart.defaults.plugins = Chart.defaults.plugins || {};

 // Average line plugin
 Chart.plugins.register([{
 id: 'avgLine',

 afterDatasetDraw: function (chart) {

 const dataset = chart.data.datasets[0] || {data: []},
 scale = chart.scales['x-axis-0'],
 options = chart.config.options.plugins.avgLine || {};

 if (options.showAvgLine === true) {

 // Calculate average of data
 var i = dataset.data.length,
 avgVal = 0,
 avgY = 0,
 validVals = 0;

 const meta = chart.getDatasetMeta(0);

 while (i--) {
 if (isNaN(parseInt(dataset.data[i], 10)) === false) {
 avgVal += parseInt(dataset.data[i], 10);
 avgY += parseInt(meta.data[i].\_model.y, 10);
 validVals++;
 }
 }
 avgVal /= validVals;
 avgY /= validVals;

 // Draw line
 chart.ctx.beginPath();
 chart.ctx.moveTo(scale.left, avgY);
 chart.ctx.strokeStyle = 'rgba(0, 155, 190, 0.5)';
 chart.ctx.lineWidth = 2;
 chart.ctx.lineTo(chart.chartArea.right, avgY);
 chart.ctx.stroke();

 // Average text
 chart.ctx.textBaseline = 'bottom';
 chart.ctx.textAlign = 'center';
 chart.ctx.fillStyle = 'rgba(0, 155, 190, 0.5)';
 chart.ctx.fillText('average', scale.left + 35, avgY);

 }

 }
 },
 {
 id: 'vertPosLine',

 afterDraw: function(chart) {

 const options = chart.config.options.plugins.vertPosLine || {};

 if (options.showVertPosLine === true && chart.tooltip.\_active && chart.tooltip.\_active.length > 0) {

 var activePt = chart.tooltip.\_active[0],
 xPos = activePt.tooltipPosition().x,
 topY = chart.scales['y-axis-0'].top,
 bottomY = chart.scales['y-axis-0'].bottom;

 // Draw vertical line
 chart.ctx.save();
 chart.ctx.beginPath();
 chart.ctx.moveTo(xPos, topY);
 chart.ctx.lineTo(xPos, bottomY);
 chart.ctx.setLineDash([6, 6]);
 chart.ctx.lineWidth = 2;
 chart.ctx.strokeStyle = 'rgba(0, 155, 190, 0.4)';
 chart.ctx.stroke();
 chart.ctx.restore();

 }

 },
 }]);

 local.datepicker = {

 /\*\*
 \* Initializes datepicker with given id.
 \*
 \* @param {string} id - Unique id after "scriptusage-datepicker-"
 \* @param {number} min - Minimum date in datepicker (in seconds)
 \* @param {number} y - Year datepicker should start at; defaults to current year
 \*
 \* @returns {void}
 \*/
 init: function(id, min) {

 min = isNaN(parseInt(min, 10)) === false ? min : 1325462400;

 // Set up datepicker
 const datepicker = $('#scriptusage-datepicker-' + id);

 // Make sure datepicker has not already been set up
 if (datepicker.children().length === 0) {

 // Add scroll container
 datepicker.append('<div class="scriptusage-datepicker-scroll-container"></div>');

 datepicker.css('padding-left', scrollbarWidth);

 // Add years
 local.datepicker.populate(dpStartDate, dpEndDate, id, min);

 // Add datepicker events
 local.datepicker.addEvents(id, min);

 }

 },

 /\*\*
 \* Adds events for datepicker
 \*
 \* @param {string} id - Unique ID of datepicker
 \* @param {number} min - Date of minimum date for datepicker in seconds
 \*
 \* @returns {void}
 \*/
 addEvents: function(id, min) {

 // Placeholder hasVal class
 $('#scriptusage-datepicker-' + id).siblings('.scriptusage-util-input').change(
 function() {

 if ($(this).val() === '') {
 $(this).removeClass('scriptusage-util-hasVal');
 } else {
 $(this).addClass('scriptusage-util-hasVal');
 }

 }
 );

 $('#scriptusage-datepicker-' + id).siblings('.scriptusage-util-input').focus(
 function() {

 $(this).addClass('scriptusage-util-hasVal');
 $('#scriptusage-datepicker-' + id).css({'opacity': 1, 'pointer-events': 'auto', 'visibility': 'visible'});

 }
 );

 body.on('click', '#scriptusage-datepicker-' + id + ' .scriptusage-datepicker-months th:not(:empty)',
 function() {

 var y = parseInt($(this).parents('.scriptusage-datepicker-year-calendar').attr('year-id'), 10);

 if ($(this).text() !== '') {

 const dpStartDateElm = $('#scriptusage-datepicker-startdate', $('#scriptusage-datepicker-' + id)),
 dpEndDateElm = $('#scriptusage-datepicker-enddate', $('#scriptusage-datepicker-' + id)),
 rangeInputElm = $('#scriptusage-datepicker-' + id).siblings('.scriptusage-util-input');

 if (typeof dpStartDate === 'number' && Date.UTC(y, $(this).attr('month-id'), 1) / 1000 > dpStartDate) {

 if (dpEndDate !== '') {
 dpStartDateElm.attr('id', '');
 dpEndDateElm.attr('id', '');
 $('#scriptusage-datepicker-' + id + ' .scriptusage-datepicker-selected').css('background', '').removeClass('scriptusage-datepicker-selected');
 $(this).attr('id', 'scriptusage-datepicker-startdate');
 dpStartDate = Date.UTC(y, $(this).attr('month-id'), 1) / 1000;
 dpEndDate = '';
 } else {

 if (parseInt(dpStartDateElm.parents('.scriptusage-datepicker-year-calendar').attr('year-id'), 10) === y) {

 if (dpStartDateElm.parent()[0] === $(this).parent()[0]) {
 (dpStartDateElm.nextUntil($(this))).addClass('scriptusage-datepicker-selected');
 } else {
 $('th:not(:empty)', ((dpStartDateElm.parent()).nextUntil($(this).parent()))).addClass('scriptusage-datepicker-selected');
 dpStartDateElm.nextAll('th:not(:empty)').addClass('scriptusage-datepicker-selected');
 $(this).prevAll('#scriptusage-datepicker-' + id + ' .scriptusage-datepicker-months th:not(:empty)').addClass('scriptusage-datepicker-selected');
 }

 } else {
 dpStartDateElm.nextAll('th:not(:empty)').addClass('scriptusage-datepicker-selected');
 dpStartDateElm.parent().nextUntil($('.scriptusage-datepicker-months tr:last-child')).addClass('scriptusage-datepicker-selected');

 // If start date elm is not in the last row
 if (typeof dpStartDateElm.parent().siblings('tr:last-child')[0] === 'object') {
 $('.scriptusage-datepicker-months tr:last-child th:not(:empty)', $('#scriptusage-datepicker-year-' + dpStartDateElm.parents('.scriptusage-datepicker-year-calendar').attr('year-id'))).addClass('scriptusage-datepicker-selected');
 }

 $(this).prevAll('#scriptusage-datepicker-' + id + ' .scriptusage-datepicker-months th:not(:empty)').addClass('scriptusage-datepicker-selected');
 $('th:not(:empty)', $(this).parent().prevAll()).addClass('scriptusage-datepicker-selected');

 // Select intermediate years
 for (var i = parseInt(dpStartDateElm.parents('.scriptusage-datepicker-year-calendar').attr('year-id'), 10) + 1; i < y; i++) {

 $('#scriptusage-datepicker-year-' + i + ' .scriptusage-datepicker-months th:not(:empty)').addClass('scriptusage-datepicker-selected');

 }

 }

 $('#scriptusage-datepicker-' + id + ' .scriptusage-datepicker-selected').css('background', 'rgba(226, 229, 233, 0.7)');
 $(this).attr('id', 'scriptusage-datepicker-enddate');
 dpEndDate = Date.UTC(y, $(this).attr('month-id'), 1) / 1000;

 // Expand input if necessary
 const minWidth = local.css.getTextWidth(local.date.formatDate(dpStartDate) + ' - ' + local.date.formatDate(dpEndDate), rangeInputElm.css('font'));
 if (minWidth > rangeInputElm.width()) {
 rangeInputElm.width(minWidth);
 }

 }

 } else {
 dpStartDateElm.attr('id', '');
 dpEndDateElm.attr('id', '');
 $(this).attr('id', 'scriptusage-datepicker-startdate');
 dpStartDate = Date.UTC(y, $(this).attr('month-id'), 1) / 1000;
 dpEndDate = '';
 $('#scriptusage-datepicker-' + id + ' .scriptusage-datepicker-selected').css('background', '').removeClass('scriptusage-datepicker-selected');
 }

 }

 // Add/remove hasVal class
 $('#scriptusage-datepicker-' + id).siblings('.scriptusage-util-input').change();

 mw.hook('su.dp.monthClicked').fire();

 }
 );

 body.on('mousedown',
 function(evt) {

 if ($(evt.target).closest('#scriptusage-datepicker-' + id).length === 0 && $(evt.target).closest($('#scriptusage-datepicker-' + id).siblings('.scriptusage-util-input')).length === 0) {

 if ($('#scriptusage-datepicker-' + id).css('visibility') === 'visible') {
 $('#scriptusage-datepicker-' + id).css({'opacity': 0, 'pointer-events': 'none', 'visibility': 'hidden'});

 mw.hook('su.dp.datepickerClosed').fire();
 }

 if ($('#scriptusage-datepicker-' + id).siblings('.scriptusage-util-input').val() === '') {
 $('#scriptusage-datepicker-' + id).siblings('.scriptusage-util-input').removeClass('scriptusage-util-hasVal');
 }

 } else {

 if ($('#scriptusage-datepicker-' + id).css('visibility') === 'hidden') {
 $('#scriptusage-datepicker-' + id).css({'opacity': 1, 'pointer-events': 'auto', 'visibility': 'visible'});

 mw.hook('su.dp.datepickerOpened').fire();
 }

 mw.hook('su.dp.datepickerClicked').fire();

 }

 }
 );

 },

 /\*\*
 \* Populates datepicker with year data
 \*
 \* @param {number} startDate - id of starting date
 \* @param {number} endDate - id of ending date
 \* @param {string} id - unique id of datepicker after "scriptusage-datepicker-"
 \* @param {number} min - minimum date of datepicker (in seconds)
 \*
 \* @returns {void}
 \*/
 populate: function(startDate, endDate, id, min) {

 if ($('.scriptusage-datepicker').length > 0) {

 // Get datepicker by id
 const datepicker = $('#scriptusage-datepicker-' + id).length > 0 ? $('#scriptusage-datepicker-' + id) : $('.scriptusage-datepicker'),
 minDate = new Date(min \* 1000);

 // Clear old months
 $('.scriptusage-datepicker-scroll-container', datepicker).empty();

 var y = minDate.getUTCFullYear();

 while (y <= curDate.getUTCFullYear() && y >= minDate.getUTCFullYear()) {

 $('.scriptusage-datepicker-scroll-container', datepicker).append('<div class="scriptusage-datepicker-year-calendar" id="scriptusage-datepicker-year-' + y + '" year-id="' + y + '"><div class="scriptusage-datepicker-year-container"><span class="scriptusage-datepicker-yearname"></span></div><table class="scriptusage-datepicker-month-table"><tbody class="scriptusage-datepicker-months"></tbody></table></div>');

 const monthContainerElm = $('#scriptusage-datepicker-year-' + y + ' .scriptusage-datepicker-months', datepicker);

 $('#scriptusage-datepicker-year-' + y + ' .scriptusage-datepicker-yearname', datepicker).text(y);

 $('.WikiaArticle').css('overflow', 'visible');
 monthContainerElm.append('<tr></tr>');

 const firstDate = new Date(minDate.getTime());
 firstDate.setUTCMonth(firstDate.getUTCMonth() - 1);

 // Blank months before minimum date
 while (firstDate.getUTCFullYear() === minDate.getUTCFullYear()) {

 $('#scriptusage-datepicker-year-' + y + ' .scriptusage-datepicker-months tr', datepicker).prepend('<th></th>');
 firstDate.setUTCMonth(firstDate.getUTCMonth() - 1);

 }

 // Add this year's months
 const startDateObj = new Date(startDate \* 1000),
 endDateObj = new Date(endDate \* 1000);
 for (var m = 0; m < 12; m++) {

 var startEndId = '';

 if (startDateObj.getUTCMonth() === m && startDateObj.getUTCFullYear() === y) {
 startEndId = ' id="scriptusage-datepicker-startdate"';
 }

 if (endDateObj.getUTCMonth() === m && endDateObj.getUTCFullYear() === y) {
 startEndId = ' id="scriptusage-datepicker-enddate"';
 }

 var monthCell = '<th' + startEndId + ' month-id="' + m + '">' + local.date.getMonthName(m) + '</th>';

 if ($('#scriptusage-datepicker-year-' + y + ' .scriptusage-datepicker-months tr:last-child th', datepicker).length === 6) {
 monthContainerElm.append('<tr>' + monthCell + '</tr>');
 } else {
 $('#scriptusage-datepicker-year-' + y + ' .scriptusage-datepicker-months tr:last-child', datepicker).append(monthCell);
 }
 }

 }

 // Fill rest of calendar
 while ($('#scriptusage-datepicker-year-' + y + ' .scriptusage-datepicker-months tr:last-child th', datepicker).length < 6) {

 $('#scriptusage-datepicker-year-' + y + ' .scriptusage-datepicker-months tr:last-child', datepicker).append('<th></th>');

 }

 const startDateElm = $('#scriptusage-datepicker-year-' + y + ' #scriptusage-datepicker-startdate', datepicker),
 endDateElm = $('#scriptusage-datepicker-year-' + y + ' #scriptusage-datepicker-enddate', datepicker);
 if (startDateElm.length > 0) {

 if (endDateElm.length > 0) {
 if (startDateElm.parent()[0] === endDateElm.parent()[0]) {
 (startDateElm.nextUntil(endDateElm)).addClass('scriptusage-datepicker-selected');
 } else {
 $('#scriptusage-datepicker-year-' + y + ' th:not(:empty)', (startDateElm.parent()).nextUntil(endDateElm.parent())).addClass('scriptusage-datepicker-selected');
 startDateElm.nextAll().addClass('scriptusage-datepicker-selected');
 endDateElm.prevAll('.scriptusage-datepicker-months th:not(:empty)').addClass('scriptusage-datepicker-selected');
 }
 } else if (isNaN(parseInt(endDate, 10)) === false) {
 startDateElm.nextAll().addClass('scriptusage-datepicker-selected');
 $('th:not(:empty)', (startDateElm.parent()).nextAll()).addClass('scriptusage-datepicker-selected');
 }

 } else {

 if (endDateElm.length > 0) {
 endDateElm.prevAll('.scriptusage-datepicker-months th:not(:empty)').addClass('scriptusage-datepicker-selected');
 $('th:not(:empty)', (endDateElm.parent()).prevAll()).addClass('scriptusage-datepicker-selected');
 } else if (new Date(y, m, 1).getTime() / 1000 > startDate && new Date(y, m + 1, -1).getTime() / 1000 < endDate) {
 $('#scriptusage-datepicker-year-' + y + ' .scriptusage-datepicker-months th:not(:empty)').addClass('scriptusage-datepicker-selected');
 }

 }
 $('#scriptusage-datepicker-year-' + y + ' .scriptusage-datepicker-selected', datepicker).css('background', 'rgba(226, 229, 233, 0.7)');

 y++;

 }

 }

 mw.hook('su.dp.populated').fire();

 },

 };

 local.log = {

 /\*\*
 \* Loads data stored on the usage log to an object.
 \*
 \* @param {Object} settings - With properties:
 \* {function} success - Function to be executed after data is loaded
 \* @param {Object} data - Usage data
 \* {function} error - Function to be executed on error
 \* @param {Object} Empty object
 \*
 \* @returns {void}
 \*/
 getData: function(settings) {

 // Initialize data
 const data = {},
 usageLogPage = 'Project:Script Usage Log';

 // Send AJAX request to usage log page
 $.ajax({

 url: config.wgServer + '/index.php?title=' + usageLogPage + '&action=raw',

 success: function(usageLog) {

 // Get elements with usage data
 const dataElms = $(usageLog).filter('.scriptusage-data-element');

 // Find log elements
 if (dataElms.length > 0) {

 $.each(dataElms, function(i2, thisElm) {

 // Get date of data in seconds as stored in element's ID
 const dateId = $(thisElm).attr('id').replace('scriptusage\_', ''),
 scriptId = $(thisElm).attr('scriptId');

 // Create object inside data object to store day's data
 data[scriptId] = {};
 data[scriptId][dateId] = {};

 // Data validation check - ensure no data has been tampered with
 var dataIsInvalid = false;

 // Create data object with element's data
 $.each($(thisElm).children(),
 function(childIndex, element) {

 const type = $(this).attr('class').replace('scriptusage\_', ''),
 val = parseInt($(this).text(), 10);

 if (isNaN(val) === false) {
 data[scriptId][dateId][type] = val;
 } else {
 dataIsInvalid = true;
 return false;
 }

 }
 );

 if (dataIsInvalid === true) {
 data[dateId] = null;
 }

 });

 }

 // Execute a function after data has been retrieved
 if (typeof settings.success === 'function') {
 settings.success(data, usageLog);
 }

 },

 error: function() {

 // Execute function even if usage log does not exist.
 if (typeof settings.error === 'function') {
 settings.error({}, '');
 }

 },

 complete: function(usageLogXHR) {

 const usageLog = typeof usageLogXHR.responseText === 'string' ? usageLogXHR.responseText : '';

 if (typeof settings.complete === 'function') {
 settings.complete(data, usageLog);
 }

 if (typeof settings.callback === 'function') {
 settings.callback(data, usageLog);
 }

 },

 cache: false,

 });

 },

 /\*\*
 \* Logs script usage data on log page in user's settings.
 \*
 \* @param {Object} settings - With properties:
 \* {Object} data - Usage data
 \* {string} summary - Edit summary
 \* {function} callback - Executed after data logged
 \* {Object} data - See "data"
 \* {function} complete - Executed if AJAX request completes (succeeds or fails)
 \* {Object} data - See "data"
 \* {function} success - Executed if logging succeeds
 \* {Object} data - See "data"
 \* {function} error - Executed if logging fails
 \* {Object} data - See "data"
 \*
 \* @returns {void}
 \*/
 addData: function(settings) {

 if (typeof settings.data !== 'object') {
 mw.log('Data logging was aborted. "settings.data" must be an object. Invalid value: ' + settings.data);

 return;
 }

 const usageLogPage = 'Project:Script Usage Log';

 // Get logged data to prevent duplicates
 local.log.getData(
 {
 complete: function (logged, responseText) {

 $.each(settings.data, function(scriptId, dates) {

 if (typeof dates === 'object' && dates !== null) {
 $.each(dates, function(dateId, dataObj) {

 // New data element
 var dataStr = '<span id="scriptusage\_' + dateId + '" class="scriptusage-data-element" scriptId="' + scriptId + '">';

 $.each(dataObj, function(key, val) {

 if (val !== null) {

 // Add data item
 dataStr += '<span class="scriptusage\_' + key + '">' + val + '</span>';

 }

 });

 // End data element
 dataStr += '</span>';

 // Add entry
 responseText += dataStr;

 });

 }

 });

 // Edit request params
 const postParams = {
 action: 'edit',
 title: usageLogPage,
 recreate: true,
 text: responseText,
 summary: settings.summary,
 format: 'json',
 token: config.token,
 };

 // Send AJAX request to edit API and add data in HTML to log page
 (new mw.Api())
 .post(postParams)
 .done(
 function() {

 if (typeof settings.success === 'function') {
 settings.success(settings.data);
 }

 }
 )
 .error (
 function() {

 if (typeof settings.error === 'function') {
 settings.error(settings.data);
 }

 }
 )
 .always(
 function() {

 if (typeof settings.complete === 'function') {
 settings.complete(settings.data);
 }

 if (typeof settings.callback === 'function') {
 settings.callback(settings.data);
 }

 }
 );
 },
 });
 }

 };

 local.search = {

 getWAMWikis: function() {

 var wikisCount = 0;

 function getURLs(list) {

 const urls = [];

 if (typeof list.wam\_index === 'object') {
 $.each(list.wam\_index,
 function(id, items) {

 wikisCount++;

 if ((/^(https?:\/\/([\w-]+?\.){0,2}(fandom\.com|wikia\.com|wikia\.org)(\/[A-Za-z0-9\-\.\_~:\/\?#\[\]@!$&'\(\)\\*\+,;\=]\*)?)/).test(items.url)) {
 urls.push(items.url);
 }

 }
 );

 if (urls.length > 0) {
 local.search.countUsage(urls);
 }
 }

 }

 (function getData(offset) {

 if (offset <= 4980) {
 setTimeout(function() {
 $.ajax({

 url: 'https://fandomusagedata.herokuapp.com/api/v1/WAM/WAMIndex?sort\_column=wam&limit=20&fetch\_admins=false&fetch\_wiki\_images=false&offset=' + offset,

 success: getURLs,

 });

 offset += 20;
 getData(offset);
 }, 10000);
 }

 }) (0);

 },

 countUsage: function(urls) {
 console.log(urls);
 }

 };

 local.ui = {

 createGraphInterface: function() {

 local.log.getData(
 {
 complete: function (logged) {

 const pageTitle = 'Graph Script Usage',
 headingText = 'Select a script',
 sitewideUsageText = 'Sitewide',
 personalUsageText = 'Personal',
 allUsageText = 'Any usage',
 dateRangeLabel = 'date range',
 scriptLabel = 'code page name',
 graphBtnLabel = 'Graph',
 allGraphTitle = 'Any Usage',
 sitewideGraphTitle = 'Sitewide Usage',
 personalGraphTitle = 'Personal Usage',
 dateRangeError = 'Select start and end dates.',
 pageNameError = 'This page could not be found. Note that page names are case-sensitive.',
 noSearchText = 'Choose a code page and a date range to graph script usage.',
 logPromptText = 'This month\'s data can\'t be displayed until it is logged. If you choose to log data, you must leave this tab open in the background until logging finishes. Otherwise, older data will still be available.',
 logBtnLabel = 'Log data',
 dismissBtnLabel = 'Dismiss';

 $('title').text(pageTitle + ' | Fandom Developers Wiki | FANDOM powered by Wikia');
 $('.page-header\_\_title').text(pageTitle);

 $('#mw-content-text').html('<div class="scriptusage-flex-container" id="scriptusage-log-prompt-container"><div id="scriptusage-log-prompt">' + logPromptText + '</div><div id="scriptusage-log-button-container"><div class="scriptusage-util-btn" id="scriptusage-log-btn-log">' + logBtnLabel + '</div><div class="scriptusage-util-btn" id="scriptusage-log-btn-dismiss">' + dismissBtnLabel + '</div></div></div><div class="scriptusage-flex-container"><div id="scriptusage-graph-options" class="scriptusage-graph-main-cont"><span class="scriptusage-util-heading">' + headingText + '</span><div class="scriptusage-util-tab-container"><div class="scriptusage-util-tab scriptusage-util-tab-active" tabval="all">' + allUsageText + '</div><div class="scriptusage-util-tab" tabval="sitewide">' + sitewideUsageText + '</div><div class="scriptusage-util-tab" tabval="personal">' + personalUsageText + '</div></div><span class="scriptusage-util-filter scriptusage-graph-filter" id="scriptusage-graph-script-filter-cont"><input class="scriptusage-util-filter-input scriptusage-util-input" id="scriptusage-graph-script" type="text" required ><span class="scriptusage-util-placeholder scriptusage-util-filter-ph">' + scriptLabel + '</span></span><div id="scriptusage-graph-script-errors"></div><span class="scriptusage-util-filter scriptusage-graph-filter" id="scriptusage-graph-daterange-filter-cont"><input class="scriptusage-util-filter-input scriptusage-util-input" id="scriptusage-graph-daterange" type="text" required readonly="readonly"><div class="scriptusage-datepicker" id="scriptusage-datepicker-graphrange"></div><span class="scriptusage-util-placeholder scriptusage-util-filter-ph">' + dateRangeLabel + '</span></span><div id="scriptusage-graph-daterange-errors"></div><a class="scriptusage-wds wds-button scriptusage-graph-create-btn-active" id="scriptusage-graph-create-btn">' + graphBtnLabel + '</a></div><div id="scriptusage-graph-container" class="scriptusage-graph-main-cont"><canvas id="scriptusage-graph-usage-chart"></canvas><span class="scriptusage-graph-nosearch-text">' + noSearchText + '</span></div></div>');

 /\* Check if this month's data has already been logged
 (check if max month ID is the same as this month's ID) \*/
 if (Number(Object.keys(logged[Object.keys(logged)[0]]).reduce(function(a, b) { return Math.max(a, b); })) < curMonthId) {
 $('#scriptusage-log-prompt-container').css('display', 'flex');

 $('#scriptusage-log-btn-log').click(
 function() {

 $('#scriptusage-log-prompt').html('logging');

 }
 );

 // Dismiss button click event
 $('#scriptusage-log-btn-dismiss').click(
 function() {

 $('#scriptusage-log-prompt-container').fadeOut(200,
 function() {

 $(this).remove();

 }
 );

 }
 );
 }

 // Prompt user to input filters before displaying graph
 $('.scriptusage-graph-nosearch-text', $('#scriptusage-graph-container')).css('display', 'block');
 $('#scriptusage-graph-usage-chart').css('display', 'none');
 $('#scriptusage-graph-container').addClass('scriptusage-graph-nograph');

 const scriptInput = $('#scriptusage-graph-script'),
 dateRangeInput = $('#scriptusage-graph-daterange');

 // Tab click events
 body.on('click', '.scriptusage-util-tab',
 function() {

 $('.scriptusage-util-tab-active').removeClass('scriptusage-util-tab-active');
 $(this).addClass('scriptusage-util-tab-active');

 }
 );

 // Autocomplete - based off [[w:c:dev:SearchSuggest]]
 mw.loader.using('jquery.autocomplete').then(function() {

 scriptInput.autocomplete({
 serviceUrl: config.wgServer + '/index.php?action=ajax&rs=getLinkSuggest&format=json',
 skipBadQueries: true,
 fnPreprocessResults: function(response) {
 response.query = response.query;
 response.suggestions = response.suggestions;
 response.data = response.suggestions;
 return response;
 },
 appendTo: $('#scriptusage-graph-script-filter-cont'),
 maxHeight: 1000,
 });

 });

 // Set up datepicker
 local.datepicker.init('graphrange');
 mw.hook('su.dp.monthClicked').add(
 function() {

 const displayRange = local.date.formatDate(dpStartDate) + ' - ' + local.date.formatDate(dpEndDate);
 dateRangeInput.val(displayRange).change();

 }
 );

 // Validate on blur
 scriptInput.blur(
 function() {

 local.input.pageExists(scriptInput.val().trim(), function(exists) {
 if (exists === true) {
 local.input.clearErrors('#scriptusage-graph-script-errors');
 scriptInput.removeClass('scriptusage-util-input-error');
 } else {
 local.input.error(pageNameError, 'pageName', '#scriptusage-graph-script-errors');
 scriptInput.addClass('scriptusage-util-input-error');
 }
 });

 }
 );

 mw.hook('su.dp.datepickerClosed').add(
 function() {

 // Start and/or end date not selected
 if (typeof dpStartDate !== 'number' || typeof dpEndDate !== 'number') {
 local.input.error(dateRangeError, 'range', '#scriptusage-graph-daterange-errors');
 dateRangeInput.addClass('scriptusage-util-input-error');
 return;
 }

 local.input.clearErrors('#scriptusage-graph-daterange-errors');
 dateRangeInput.removeClass('scriptusage-util-input-error');

 }
 );

 // Search button click events
 $('#scriptusage-graph-create-btn').click(
 function(){

 const canvasSelector = 'scriptusage-graph-usage-chart',
 graphType = $('.scriptusage-util-tab-active').attr('tabval'),
 pageName = scriptInput.val().trim();

 // Invalid page name
 local.input.pageExists(scriptInput.val().trim(), function(exists) {
 if (exists === false) {
 local.input.error(pageNameError, 'pageName', '#scriptusage-graph-script-errors');
 $(this).addClass('scriptusage-util-input-error');
 } else {
 local.input.clearErrors('#scriptusage-graph-script-errors');
 $(this).removeClass('scriptusage-util-input-error');

 // Start and/or end date not selected
 if (typeof dpStartDate !== 'number' || typeof dpEndDate !== 'number') {
 local.input.error(dateRangeError, 'range', '#scriptusage-graph-daterange-errors');
 return;
 }
 local.input.clearErrors('#scriptusage-graph-daterange-errors');
 $(this).removeClass('scriptusage-util-input-error');

 // Remove initial prompt
 $('.scriptusage-graph-nosearch-text', $('#scriptusage-graph-container')).css('display', 'none');
 $('#' + canvasSelector).css('display', 'block');
 $('#scriptusage-graph-container').removeClass('scriptusage-graph-nograph');

 $.get(config.wgServer + '/api.php?action=query&format=json&titles=' + encodeURIComponent(pageName))
 .done(function(searchResults) {

 const scriptId = Object.keys(searchResults.query.pages)[0],
 graphTitle = (function() {
 switch (graphType) {
 case 'all':
 return searchResults.query.pages[scriptId].title + ': ' + allGraphTitle;
 case 'sitewide':
 return searchResults.query.pages[scriptId].title + ': ' + sitewideGraphTitle;
 case 'personal':
 return searchResults.query.pages[scriptId].title + ': ' + personalGraphTitle;
 }
 })();

 local.ui.insertChart({
 name: canvasSelector,
 data: logged[scriptId],
 startDate: dpStartDate,
 endDate: dpEndDate,
 type: graphType,
 selector: '#' + canvasSelector,
 title: graphTitle,
 });

 });
 }
 });

 }
 );

 },
 });

 },

 /\*\*
 \* Creates a graph in a canvas.
 \*
 \* @param {Object} settings - With properties:
 \* {string} type - score, rank, or vertRank
 \* {number} startDate - Date to start graph at
 \* {number} endDate - Date to end graph at
 \* {Object} data - Data to graph
 \* {string} selector - ID or class of canvas element
 \* {string} title - Graph title
 \*
 \* @returns {void}
 \*/
 insertChart: function(settings) {

 const usesLabel = 'Uses',
 missingPointsText = 'Missing points',
 notEnoughPointsText = 'Not enough points';

 const chartData = [],
 chartLabels = [];
 var validPts = 0;

 for (var id = settings.startDate; id <= settings.endDate; id = new Date(id \* 1000).setUTCMonth(new Date(id \* 1000).getUTCMonth() + 1, 1) / 1000) {

 // Adjust for time zone
 chartLabels.push(local.date.formatDate(id + (curDate.getTimezoneOffset() \* 60) + 3600, true));

 if (settings.type === 'sitewide' || settings.type === 'personal') {
 if (typeof settings.data[id] === 'object' && isNaN(parseInt(settings.data[id][settings.type], 10)) === false) {

 chartData.push(settings.data[id][settings.type]);

 // Count number of valid points for graph
 validPts++;

 } else {
 chartData.push(null);
 }
 } else if (settings.type === 'all') {
 if (typeof settings.data[id] === 'object' && isNaN(parseInt(settings.data[id].sitewide, 10)) === false && isNaN(parseInt(settings.data[id].personal, 10)) === false) {

 chartData.push(parseInt(settings.data[id].sitewide, 10) + parseInt(settings.data[id].personal, 10));

 // Count number of valid points for graph
 validPts++;

 } else {
 chartData.push(null);
 }
 }

 }

 if (validPts > 0) {

 $(settings.selector).css('display', 'block');
 $(settings.selector).parent().removeClass('scriptusage-graph-nograph');
 $('.scriptusage-graph-nopoints-text', $(settings.selector).parent()).css('display', 'none');

 if (typeof charts[settings.name] === 'undefined' || charts[settings.name] === null) {

 charts[settings.name] = new Chart($(settings.selector), {
 type: 'line',
 data: {
 labels: chartLabels,
 datasets: [{
 label: usesLabel,
 data: chartData,
 backgroundColor: 'rgba(0, 155, 190, 0.7)',
 borderColor: linkTextColor,
 fill: false,
 }]
 },
 options: {
 elements: {
 line: {
 tension: 0,
 },
 point: {
 radius: 1,
 pointBackgroundColor: '#00c4f0',
 pointBorderColor: '#24d7ff',
 },
 },
 layout: {
 padding: {
 left: 10
 }
 },
 legend: {
 display: false,
 },
 plugins: {
 avgLine: {
 showAvgLine: true,
 },
 vertPosLine: {
 showVertPosLine: true,
 },
 },
 scales: {
 xAxes: [{
 ticks: {
 fontColor: bodyTextColor,
 },
 }],
 yAxes: [{
 ticks: {
 fontColor: bodyTextColor,
 stepSize: 1,
 },
 }]
 },
 title: {
 display: true,
 fontColor: bodyTextColor,
 text: settings.title,
 },
 tooltips: {
 mode: 'index',
 intersect: false,
 },
 },
 });

 } else {

 charts[settings.name].data.labels = chartLabels;
 charts[settings.name].data.datasets[0].data = chartData;
 charts[settings.name].options.title.text = settings.title;
 charts[settings.name].update();

 }

 if (validPts < (settings.endDate - settings.startDate) / 86400 + 1) {

 if ($('.scriptusage-graph-notlogged-text', $(settings.selector).parent()).length === 0) {
 $(settings.selector).after('<div class="scriptusage-graph-notlogged-text">' + missingPointsText + '</div>');
 }

 $('.scriptusage-graph-notlogged-text', $(settings.selector).parent()).css('display', 'block');
 } else {
 $('.scriptusage-graph-notlogged-text', $(settings.selector).parent()).css('display', 'none');
 }

 } else {

 if (typeof charts[settings.name] === 'object' && charts[settings.name] !== null) {
 charts[settings.name].destroy();
 charts[settings.name] = null;
 }

 if ($('.scriptusage-graph-nopoints-text', $(settings.selector).parent()).length === 0) {
 $(settings.selector).after('<span class="scriptusage-graph-nopoints-text">' + notEnoughPointsText + '</span>');
 }

 $('.scriptusage-graph-nopoints-text', $(settings.selector).parent()).css('display', 'block');
 $(settings.selector).css('display', 'none');
 $('.scriptusage-graph-notlogged-text', $(settings.selector).parent()).css('display', 'none');
 $(settings.selector).parent().addClass('scriptusage-graph-nograph');

 }

 },

 };

 const logPageWarning = 'This page intentionally appears blank and is used to store script usage data. Please do not edit it.';

 // Intentionally blank page warning on log page
 if (config.wgPageName.replace(/\_/g, ' ') === 'Project:Script Usage Log') {
 $('#mw-content-text').text(logPageWarning);
 }

 // Start
 if (config.wgPageName === 'Special:BlankPage/scriptusage') {
 local.ui.createGraphInterface();
 }

 });

});

```