diff --git a/javascripts/config.js b/javascripts/config.js index 544a0b4d8..5c87c91b8 100644 --- a/javascripts/config.js +++ b/javascripts/config.js @@ -13,7 +13,6 @@ const pv = require('./shared/pv'); * @type {Object} */ const config = { - articleSelector: '.aqs-article-selector', chart: '.aqs-chart', chartConfig: { Line: { @@ -126,10 +125,11 @@ const config = { tooltipTemplate: '<%if (label){%><%=label%>: <%}%><%= formatNumber(value) %>' }, linearCharts: ['Line', 'Bar', 'Radar'], - minDate: moment('2015-08-01').startOf('day'), + minDate: moment('2015-07-01').startOf('day'), maxDate: moment().subtract(1, 'days').startOf('day'), platformSelector: '#platform-select', projectInput: '.aqs-project-input', + select2Input: '.aqs-article-selector', specialRanges: { 'last-week': [moment().subtract(1, 'week').startOf('week'), moment().subtract(1, 'week').endOf('week')], 'this-month': [moment().startOf('month'), moment().subtract(1, 'days').startOf('day')], diff --git a/javascripts/langviews/config.js b/javascripts/langviews/config.js index 06a2af82b..76f7e6bef 100644 --- a/javascripts/langviews/config.js +++ b/javascripts/langviews/config.js @@ -27,6 +27,7 @@ const config = { } }, cookieExpiry: 30, // num days + dateLimit: 31, // num days dateRangeSelector: '#range_input', defaults: { dateFormat: 'YYYY-MM-DD', @@ -46,7 +47,7 @@ const config = { } } }, - minDate: moment('2015-08-01'), + minDate: moment('2015-07-01'), maxDate: moment().subtract(1, 'days'), platformSelector: '#platform_select', projectInput: '#project_input', diff --git a/javascripts/langviews/langviews.js b/javascripts/langviews/langviews.js index 5d340dd43..f2404a2cd 100644 --- a/javascripts/langviews/langviews.js +++ b/javascripts/langviews/langviews.js @@ -451,19 +451,6 @@ class LangViews extends Pv { return params; } - /** - * Simple metric to see how many use it (pageviews of the pageview, a meta-pageview, if you will :) - * @return {null} nothing - */ - patchUsage() { - if (location.host !== 'localhost') { - $.ajax({ - url: `//tools.wmflabs.org/musikanimal/api/lv_uses/${this.project}`, - method: 'PATCH' - }); - } - } - /** * Parses the URL query string and sets all the inputs accordingly * Should only be called on initial page load, until we decide to support pop states (probably never) @@ -489,8 +476,8 @@ class LangViews extends Pv { } else if (params.start) { startDate = moment(params.start || moment().subtract(config.defaults.daysAgo, 'days')); endDate = moment(params.end || Date.now()); - if (startDate < moment('2015-08-01') || endDate < moment('2015-08-01')) { - this.addSiteNotice('danger', $.i18n('param-error-1'), $.i18n('invalid-params'), true); + if (startDate < config.minDate || endDate < config.minDate) { + this.addSiteNotice('danger', $.i18n('param-error-1', `${$.i18n('july')} 2015`), $.i18n('invalid-params'), true); return; } else if (startDate > endDate) { this.addSiteNotice('warning', $.i18n('param-error-2'), $.i18n('invalid-params'), true); @@ -558,76 +545,9 @@ class LangViews extends Pv { * @returns {null} - nothing */ setupDateRangeSelector() { - const dateRangeSelector = $(config.dateRangeSelector); - - /** transform config.specialRanges to have i18n as keys */ - let ranges = {}; - Object.keys(config.specialRanges).forEach(key => { - ranges[$.i18n(key)] = config.specialRanges[key]; - }); - - dateRangeSelector.daterangepicker({ - locale: { - format: this.dateFormat, - applyLabel: $.i18n('apply'), - cancelLabel: $.i18n('cancel'), - customRangeLabel: $.i18n('custom-range'), - dateLimit: { days: 31 }, - daysOfWeek: [ - $.i18n('su'), - $.i18n('mo'), - $.i18n('tu'), - $.i18n('we'), - $.i18n('th'), - $.i18n('fr'), - $.i18n('sa') - ], - monthNames: [ - $.i18n('january'), - $.i18n('february'), - $.i18n('march'), - $.i18n('april'), - $.i18n('may'), - $.i18n('june'), - $.i18n('july'), - $.i18n('august'), - $.i18n('september'), - $.i18n('october'), - $.i18n('november'), - $.i18n('december') - ] - }, - startDate: moment().subtract(config.defaults.daysAgo, 'days'), - minDate: config.minDate, - maxDate: config.maxDate, - ranges: ranges - }); - - /** so people know why they can't query data older than August 2015 */ - $('.daterangepicker').append( - $('
') - .addClass('daterange-notice') - .html($.i18n('date-notice', document.title, "stats.grok.se")) - ); - - /** - * The special date range options (buttons the right side of the daterange picker) - * - * WARNING: we're unable to add class names or data attrs to the range options, - * so checking which was clicked is hardcoded based on the index of the LI, - * as defined in config.specialRanges - */ - $('.daterangepicker .ranges li').on('click', e => { - const index = $('.daterangepicker .ranges li').index(e.target), - container = this.daterangepicker.container, - inputs = container.find('.daterangepicker_input input'); - this.specialRange = { - range: Object.keys(config.specialRanges)[index], - value: `${inputs[0].value} - ${inputs[1].value}` - }; - }); + super.setupDateRangeSelector(); - dateRangeSelector.on('apply.daterangepicker', (e, action) => { + $(config.dateRangeSelector).on('apply.daterangepicker', (e, action) => { if (action.chosenLabel === $.i18n('custom-range')) { this.specialRange = null; diff --git a/javascripts/pageviews.js b/javascripts/pageviews.js index be065fed0..1393bf927 100644 --- a/javascripts/pageviews.js +++ b/javascripts/pageviews.js @@ -52,7 +52,7 @@ class PageViews extends Pv { initialize() { this.setupProjectInput(); this.setupDateRangeSelector(); - this.setupArticleSelector(); + this.setupSelect2(); this.setupSettingsModal(); this.setupSelect2Colors(); this.popParams(); @@ -140,20 +140,6 @@ class PageViews extends Pv { return jsonContent; } - /** - * Fill in values within settings modal with what's in the session object - * @returns {null} nothing - */ - fillInSettings() { - $.each($('#settings-modal input'), (index, el) => { - if (el.type === 'checkbox') { - el.checked = this[el.name] === 'true'; - } else { - el.checked = this[el.name] === el.value; - } - }); - } - /** * Fills in zero value to a timeseries, see: * https://wikitech.wikimedia.org/wiki/Analytics/AQS/Pageview_API#Gotchas @@ -208,24 +194,6 @@ class PageViews extends Pv { ); } - /** - * Gets the date headings as strings - i18n compliant - * @param {boolean} localized - whether the dates should be localized per browser language - * @returns {Array} the date headings as strings - */ - getDateHeadings(localized) { - const dateHeadings = []; - - for (let date = moment(this.daterangepicker.startDate); date.isBefore(this.daterangepicker.endDate); date.add(1, 'd')) { - if (localized) { - dateHeadings.push(date.format(this.dateFormat)); - } else { - dateHeadings.push(date.format('YYYY-MM-DD')); - } - } - return dateHeadings; - } - /** * Link to /langviews for given page and chosen daterange * @param {String} page - page title @@ -317,19 +285,6 @@ class PageViews extends Pv { return params; } - /** - * Simple metric to see how many use it (pageviews of the pageview, a meta-pageview, if you will :) - * @return {null} nothing - */ - patchUsage() { - if (location.host !== 'localhost') { - $.ajax({ - url: `//tools.wmflabs.org/musikanimal/api/pv_uses/${this.project}`, - method: 'PATCH' - }); - } - } - /** * Parses the URL hash and sets all the inputs accordingly * Should only be called on initial page load, until we decide to support pop states (probably never) @@ -355,8 +310,8 @@ class PageViews extends Pv { } else if (params.start) { startDate = moment(params.start || moment().subtract(config.defaults.daysAgo, 'days')); endDate = moment(params.end || Date.now()); - if (startDate < moment('2015-08-01') || endDate < moment('2015-08-01')) { - this.addSiteNotice('danger', $.i18n('param-error-1'), $.i18n('invalid-params'), true); + if (startDate < config.minDate || endDate < config.minDate) { + this.addSiteNotice('danger', $.i18n('param-error-1', `${$.i18n('july')} 2015`), $.i18n('invalid-params'), true); this.resetView(); return; } else if (startDate > endDate) { @@ -374,17 +329,17 @@ class PageViews extends Pv { $(config.platformSelector).val(params.platform || 'all-access'); $('#agent-select').val(params.agent || 'user'); - this.resetArticleSelector(); + this.resetSelect2(); if (!params.pages || params.pages.length === 1 && !params.pages[0]) { // only set default of Cat and Dog for enwiki if (this.project === 'en.wikipedia') { params.pages = ['Cat', 'Dog']; - this.setArticleSelectorDefaults(params.pages); + this.setSelect2Defaults(params.pages); } } else if (this.normalized) { params.pages = this.underscorePageNames(params.pages); - this.setArticleSelectorDefaults(params.pages); + this.setSelect2Defaults(params.pages); } else { this.normalizePageNames(params.pages).then(data => { this.normalized = true; @@ -395,7 +350,7 @@ class PageViews extends Pv { this.chartType = this.getFromLocalStorage('pageviews-chart-preference') || 'Bar'; } - this.setArticleSelectorDefaults(this.underscorePageNames(params.pages)); + this.setSelect2Defaults(this.underscorePageNames(params.pages)); }); } } @@ -474,7 +429,7 @@ class PageViews extends Pv { * @returns {null} nothing */ pushParams() { - const pages = $(config.articleSelector).select2('val') || [], + const pages = $(config.select2Input).select2('val') || [], escapedPages = pages.join('|').replace(/[&%]/g, escape); if (window.history && window.history.replaceState) { @@ -486,21 +441,6 @@ class PageViews extends Pv { $('.permalink').prop('href', `#${$.param(this.getPermaLink())}&pages=${escapedPages}`); } - /** - * Removes all article selector related stuff then adds it back - * Also calls updateChart - * @returns {null} nothing - */ - resetArticleSelector() { - const articleSelector = $(config.articleSelector); - articleSelector.off('change'); - articleSelector.select2('val', null); - articleSelector.select2('data', null); - articleSelector.select2('destroy'); - $('.data-links').hide(); - this.setupArticleSelector(); - } - /** * Removes chart, messages, and resets article selections * @returns {null} nothing @@ -510,78 +450,15 @@ class PageViews extends Pv { $('.chart-container').removeClass('loading'); $('#chart-legend').html(''); $('.message-container').html(''); - this.resetArticleSelector(); - } - - /** - * Save a particular setting to session and localStorage - * - * @param {string} key - settings key - * @param {string|boolean} value - value to save - * @returns {null} nothing - */ - saveSetting(key, value) { - this[key] = value; - this.setLocalStorage(`pageviews-settings-${key}`, value); - } - - /** - * Save the selected settings within the settings modal - * Prefer this implementation over a large library like serializeObject or serializeJSON - * @returns {null} nothing - */ - saveSettings() { - /** track if we're changing to no_autocomplete mode */ - const wasAutocomplete = this.autocomplete === 'no_autocomplete'; - - $.each($('#settings-modal input'), (index, el) => { - if (el.type === 'checkbox') { - this.saveSetting(el.name, el.checked ? 'true' : 'false'); - } else if (el.checked) { - this.saveSetting(el.name, el.value); - } - }); - - this.daterangepicker.locale.format = this.dateFormat; - this.daterangepicker.updateElement(); - this.setupSelect2Colors(); - - /** - * If we changed to/from no_autocomplete we have to reset the article selector entirely - * as setArticleSelectorDefaults is super buggy due to Select2 constraints - * So let's only reset if we have to - */ - if ((this.autocomplete === 'no_autocomplete') !== wasAutocomplete) { - this.resetArticleSelector(); - } - - this.updateChart(true); - } - - /** - * Directly set articles in article selector - * Currently is not able to remove underscore from page names - * - * @param {array} pages - page titles - * @returns {array} - untouched array of pages - */ - setArticleSelectorDefaults(pages) { - pages.forEach(page => { - const escapedText = $('
').text(page).html(); - $('').appendTo(config.articleSelector); - }); - $(config.articleSelector).select2('val', pages); - $(config.articleSelector).select2('close'); - - return pages; + this.resetSelect2(); } /** * Sets up the article selector and adds listener to update chart * @returns {null} - nothing */ - setupArticleSelector() { - const articleSelector = $(config.articleSelector); + setupSelect2() { + const select2Input = $(config.select2Input); let params = { ajax: this.getArticleSelectorAjax(), @@ -591,13 +468,13 @@ class PageViews extends Pv { minimumInputLength: 1 }; - articleSelector.select2(params); - articleSelector.on('change', this.updateChart.bind(this)); + select2Input.select2(params); + select2Input.on('change', this.updateChart.bind(this)); } /** - * Get ajax parameters to be used in setupArticleSelector, based on this.autocomplete - * @return {object|null} to be passed in as the value for `ajax` in setupArticleSelector + * Get ajax parameters to be used in setupSelect2, based on this.autocomplete + * @return {object|null} to be passed in as the value for `ajax` in setupSelect2 */ getArticleSelectorAjax() { if (this.autocomplete !== 'no_autocomplete') { @@ -620,96 +497,14 @@ class PageViews extends Pv { } } - /** - * Attempt to fine-tune the pointer detection spacing based on how cluttered the chart is - * @returns {null} nothing - */ - setChartPointDetectionRadius() { - if (this.chartType !== 'Line') return; - - if (this.numDaysInRange() > 50) { - Chart.defaults.Line.pointHitDetectionRadius = 3; - } else if (this.numDaysInRange() > 30) { - Chart.defaults.Line.pointHitDetectionRadius = 5; - } else if (this.numDaysInRange() > 20) { - Chart.defaults.Line.pointHitDetectionRadius = 10; - } else { - Chart.defaults.Line.pointHitDetectionRadius = 20; - } - } - /** * sets up the daterange selector and adds listeners * @returns {null} - nothing */ setupDateRangeSelector() { - const dateRangeSelector = $(config.dateRangeSelector); - - /** transform config.specialRanges to have i18n as keys */ - let ranges = {}; - Object.keys(config.specialRanges).forEach(key => { - ranges[$.i18n(key)] = config.specialRanges[key]; - }); - - dateRangeSelector.daterangepicker({ - locale: { - format: this.dateFormat, - applyLabel: $.i18n('apply'), - cancelLabel: $.i18n('cancel'), - customRangeLabel: $.i18n('custom-range'), - daysOfWeek: [ - $.i18n('su'), - $.i18n('mo'), - $.i18n('tu'), - $.i18n('we'), - $.i18n('th'), - $.i18n('fr'), - $.i18n('sa') - ], - monthNames: [ - $.i18n('january'), - $.i18n('february'), - $.i18n('march'), - $.i18n('april'), - $.i18n('may'), - $.i18n('june'), - $.i18n('july'), - $.i18n('august'), - $.i18n('september'), - $.i18n('october'), - $.i18n('november'), - $.i18n('december') - ] - }, - startDate: moment().subtract(config.defaults.daysAgo, 'days'), - minDate: config.minDate, - maxDate: config.maxDate, - ranges: ranges - }); + super.setupDateRangeSelector(); - /** so people know why they can't query data older than August 2015 */ - $('.daterangepicker').append( - $('
') - .addClass('daterange-notice') - .html($.i18n('date-notice', document.title, "stats.grok.se")) - ); - - /** - * The special date range options (buttons the right side of the daterange picker) - * - * WARNING: we're unable to add class names or data attrs to the range options, - * so checking which was clicked is hardcoded based on the index of the LI, - * as defined in config.specialRanges - */ - $('.daterangepicker .ranges li').on('click', e => { - const index = $('.daterangepicker .ranges li').index(e.target), - container = this.daterangepicker.container, - inputs = container.find('.daterangepicker_input input'); - this.specialRange = { - range: Object.keys(config.specialRanges)[index], - value: `${inputs[0].value} - ${inputs[1].value}` - }; - }); + const dateRangeSelector = $(config.dateRangeSelector); /** the "Latest N days" links */ $('.date-latest a').on('click', e => { @@ -774,29 +569,6 @@ class PageViews extends Pv { }); } - /** - * Setup colors for Select2 entries so we can dynamically change them - * This is a necessary evil, as we have to mark them as !important - * and since there are any number of entires, we need to use nth-child selectors - * @returns {CSSStylesheet} our new stylesheet - */ - setupSelect2Colors() { - /** first delete old stylesheet, if present */ - if (this.colorsStyleEl) this.colorsStyleEl.remove(); - - /** create new stylesheet */ - this.colorsStyleEl = document.createElement('style'); - this.colorsStyleEl.appendChild(document.createTextNode('')); // WebKit hack :( - document.head.appendChild(this.colorsStyleEl); - - /** add color rules */ - config.colors.forEach((color, index) => { - this.colorsStyleEl.sheet.insertRule(`.select2-selection__choice:nth-of-type(${index + 1}) { background: ${color} !important }`, 0); - }); - - return this.colorsStyleEl.sheet; - } - /** * Set values of form based on localStorage or defaults, add listeners * @returns {null} nothing @@ -818,7 +590,7 @@ class PageViews extends Pv { * @returns {null} - nothin */ updateChart(force) { - let articles = $(config.articleSelector).select2('val') || []; + let articles = $(config.select2Input).select2('val') || []; this.pushParams(); diff --git a/javascripts/shared/pv.js b/javascripts/shared/pv.js index e9ea9dc9b..83ec6e5c8 100644 --- a/javascripts/shared/pv.js +++ b/javascripts/shared/pv.js @@ -78,6 +78,20 @@ class Pv { return $(this.config.dateRangeSelector).data('daterangepicker'); } + /** + * Fill in values within settings modal with what's in the session object + * @returns {null} nothing + */ + fillInSettings() { + $.each($('#settings-modal input'), (index, el) => { + if (el.type === 'checkbox') { + el.checked = this[el.name] === 'true'; + } else { + el.checked = this[el.name] === el.value; + } + }); + } + /** * Format number based on current settings, e.g. localize with comma delimeters * @param {number|string} num - number to format @@ -91,6 +105,24 @@ class Pv { } } + /** + * Gets the date headings as strings - i18n compliant + * @param {boolean} localized - whether the dates should be localized per browser language + * @returns {Array} the date headings as strings + */ + getDateHeadings(localized) { + const dateHeadings = []; + + for (let date = moment(this.daterangepicker.startDate); date.isBefore(this.daterangepicker.endDate); date.add(1, 'd')) { + if (localized) { + dateHeadings.push(date.format(this.dateFormat)); + } else { + dateHeadings.push(date.format('YYYY-MM-DD')); + } + } + return dateHeadings; + } + /** * Get the explanded wiki URL given the page name * This should be used instead of getPageURL when you want to chain query string parameters @@ -120,7 +152,7 @@ class Pv { get project() { const project = $(this.config.projectInput).val(); /** Get the first 2 characters from the project code to get the language */ - return project.toLowerCase().replace(/.org$/, ''); + return project ? project.toLowerCase().replace(/.org$/, '') : null; } getLocaleDateString() { @@ -448,6 +480,69 @@ class Pv { return this.daterangepicker.endDate.diff(this.daterangepicker.startDate, 'days') + 1; } + /** + * Simple metric to see how many use it (pageviews of the pageview, a meta-pageview, if you will :) + * @return {null} nothing + */ + patchUsage() { + if (location.host !== 'localhost') { + $.ajax({ + url: `//tools.wmflabs.org/musikanimal/api/pv_uses/${this.project || i18nLang}`, + method: 'PATCH' + }); + } + } + + /** + * Adapted from http://jsfiddle.net/dandv/47cbj/ courtesy of dandv + * + * Same as _.debounce but queues and executes all function calls + * @param {Function} fn - function to debounce + * @param {delay} delay - delay duration of milliseconds + * @param {object} context - scope the function should refer to + * @return {Function} rate-limited function to call instead of your function + */ + rateLimit(fn, delay, context) { + let queue = [], timer; + + const processQueue = () => { + const item = queue.shift(); + if (item) { + fn.apply(item.context, item.arguments); + } + if (queue.length === 0) { + clearInterval(timer), timer = null; + } + }; + + return function limited() { + queue.push({ + context: context || this, + arguments: [].slice.call(arguments) + }); + + if (!timer) { + processQueue(); // start immediately on the first invocation + timer = setInterval(processQueue, delay); + } + }; + } + + /** + * Removes all Select2 related stuff then adds it back + * Also might result in the chart being re-rendered + * @returns {null} nothing + */ + resetSelect2() { + const select2Input = $(this.config.select2Input); + select2Input.off('change'); + select2Input.select2('val', null); + select2Input.select2('data', null); + select2Input.select2('destroy'); + $('.data-links').hide(); + this.setupSelect2(); + } + /** * Change alpha level of an rgba value * @@ -459,11 +554,92 @@ class Pv { return value.replace(/,\s*\d\)/, `, ${alpha})`); } + /** + * Save a particular setting to session and localStorage + * + * @param {string} key - settings key + * @param {string|boolean} value - value to save + * @returns {null} nothing + */ + saveSetting(key, value) { + this[key] = value; + this.setLocalStorage(`pageviews-settings-${key}`, value); + } + + /** + * Save the selected settings within the settings modal + * Prefer this implementation over a large library like serializeObject or serializeJSON + * @returns {null} nothing + */ + saveSettings() { + /** track if we're changing to no_autocomplete mode */ + const wasAutocomplete = this.autocomplete === 'no_autocomplete'; + + $.each($('#settings-modal input'), (index, el) => { + if (el.type === 'checkbox') { + this.saveSetting(el.name, el.checked ? 'true' : 'false'); + } else if (el.checked) { + this.saveSetting(el.name, el.value); + } + }); + + this.daterangepicker.locale.format = this.dateFormat; + this.daterangepicker.updateElement(); + this.setupSelect2Colors(); + + /** + * If we changed to/from no_autocomplete we have to reset Select2 entirely + * as setSelect2Defaults is super buggy due to Select2 constraints + * So let's only reset if we have to + */ + if ((this.autocomplete === 'no_autocomplete') !== wasAutocomplete) { + this.resetSelect2(); + } + + this.updateChart(true); + } + + /** + * Attempt to fine-tune the pointer detection spacing based on how cluttered the chart is + * @returns {null} nothing + */ + setChartPointDetectionRadius() { + if (this.chartType !== 'Line') return; + + if (this.numDaysInRange() > 50) { + Chart.defaults.Line.pointHitDetectionRadius = 3; + } else if (this.numDaysInRange() > 30) { + Chart.defaults.Line.pointHitDetectionRadius = 5; + } else if (this.numDaysInRange() > 20) { + Chart.defaults.Line.pointHitDetectionRadius = 10; + } else { + Chart.defaults.Line.pointHitDetectionRadius = 20; + } + } + + /** + * Directly set items in Select2 + * Currently is not able to remove underscore from page names + * + * @param {array} items - page titles + * @returns {array} - untouched array of items + */ + setSelect2Defaults(items) { + items.forEach(item => { + const escapedText = $('
').text(item).html(); + $('').appendTo(this.config.select2Input); + }); + $(this.config.select2Input).select2('val', items); + $(this.config.select2Input).select2('close'); + + return items; + } + /** * Sets the daterange picker values and this.specialRange based on provided special range key * WARNING: not to be called on daterange picker GUI events (e.g. special range buttons) * - * @param {string} type - one of special ranges defined in config.specialRanges, + * @param {string} type - one of special ranges defined in this.config.specialRanges, * including dynamic latest range, such as `latest-15` for latest 15 days * @returns {object|null} updated this.specialRange object or null if type was invalid */ @@ -495,60 +671,26 @@ class Pv { } /** - * Splash in console, just for fun - * @returns {String} output - */ - splash() { - const style = 'background: #222; color: #bada55; padding: 4px; font-family:dejavu sans mono'; - console.log('%c ___ __ _ _ ', style); - console.log('%c | _ \\ __ _ / _` | ___ __ __ (_) ___ __ __ __ ___ ', style); - console.log('%c | _/ / _` | \\__, | / -_) \\ V / | | / -_) \\ V V / (_-< ', style); - console.log('%c _|_|_ \\__,_| |___/ \\___| _\\_/_ _|_|_ \\___| \\_/\\_/ /__/_ ', style); - console.log('%c _| """ |_|"""""|_|"""""|_|"""""|_|"""""|_|"""""|_|"""""|_|"""""|_|"""""| ', style); - console.log('%c "`-0-0-\'"`-0-0-\'"`-0-0-\'"`-0-0-\'"`-0-0-\'"`-0-0-\'"`-0-0-\'"`-0-0-\'"`-0-0-\' ', style); - console.log('%c ___ _ _ _ _ ', style); - console.log('%c o O O / \\ _ _ __ _ | || | | | ___ (_) ___ ', style); - console.log('%c o | - | | \' \\ / _` | \\_, | | | (_-< | | (_-< ', style); - console.log('%c TS__[O] |_|_| |_||_| \\__,_| _|__/ _|_|_ /__/_ _|_|_ /__/_ ', style); - console.log('%c {======|_|"""""|_|"""""|_|"""""|_| """"|_|"""""|_|"""""|_|"""""|_|"""""| ', style); - console.log('%c ./o--000\'"`-0-0-\'"`-0-0-\'"`-0-0-\'"`-0-0-\'"`-0-0-\'"`-0-0-\'"`-0-0-\'"`-0-0-\' ', style); - console.log('%c ', style); - console.log(`%c Copyright © ${new Date().getFullYear()} MusikAnimal, Kaldari, Marcel Ruiz Forns `, style); - } - - /** - * Adapted from http://jsfiddle.net/dandv/47cbj/ courtesy of dandv - * - * Same as _.debounce but queues and executes all function calls - * @param {Function} fn - function to debounce - * @param {delay} delay - delay duration of milliseconds - * @param {object} context - scope the function should refer to - * @return {Function} rate-limited function to call instead of your function + * Setup colors for Select2 entries so we can dynamically change them + * This is a necessary evil, as we have to mark them as !important + * and since there are any number of entires, we need to use nth-child selectors + * @returns {CSSStylesheet} our new stylesheet */ - rateLimit(fn, delay, context) { - let queue = [], timer; - - const processQueue = () => { - const item = queue.shift(); - if (item) { - fn.apply(item.context, item.arguments); - } - if (queue.length === 0) { - clearInterval(timer), timer = null; - } - }; - - return function limited() { - queue.push({ - context: context || this, - arguments: [].slice.call(arguments) - }); + setupSelect2Colors() { + /** first delete old stylesheet, if present */ + if (this.colorsStyleEl) this.colorsStyleEl.remove(); + + /** create new stylesheet */ + this.colorsStyleEl = document.createElement('style'); + this.colorsStyleEl.appendChild(document.createTextNode('')); // WebKit hack :( + document.head.appendChild(this.colorsStyleEl); + + /** add color rules */ + this.config.colors.forEach((color, index) => { + this.colorsStyleEl.sheet.insertRule(`.select2-selection__choice:nth-of-type(${index + 1}) { background: ${color} !important }`, 0); + }); - if (!timer) { - processQueue(); // start immediately on the first invocation - timer = setInterval(processQueue, delay); - } - }; + return this.colorsStyleEl.sheet; } /** @@ -571,6 +713,138 @@ class Pv { }); } + /** + * Get a value from localStorage, using a temporary storage if localStorage is not supported + * @param {string} key - key for the value to retrieve + * @returns {Mixed} stored value + */ + getFromLocalStorage(key) { + // See if localStorage is supported and enabled + try { + return localStorage.getItem(key); + } catch (err) { + return this.storage[key]; + } + } + + /** + * Set a value to localStorage, using a temporary storage if localStorage is not supported + * @param {string} key - key for the value to set + * @param {Mixed} value - value to store + * @returns {Mixed} stored value + */ + setLocalStorage(key, value) { + // See if localStorage is supported and enabled + try { + return localStorage.setItem(key, value); + } catch (err) { + return this.storage[key] = value; + } + } + + /** + * sets up the daterange selector and adds listeners + * @returns {null} - nothing + */ + setupDateRangeSelector() { + const dateRangeSelector = $(this.config.dateRangeSelector); + + /** transform this.config.specialRanges to have i18n as keys */ + let ranges = {}; + Object.keys(this.config.specialRanges).forEach(key => { + ranges[$.i18n(key)] = this.config.specialRanges[key]; + }); + + let datepickerOptions = { + locale: { + format: this.dateFormat, + applyLabel: $.i18n('apply'), + cancelLabel: $.i18n('cancel'), + customRangeLabel: $.i18n('custom-range'), + daysOfWeek: [ + $.i18n('su'), + $.i18n('mo'), + $.i18n('tu'), + $.i18n('we'), + $.i18n('th'), + $.i18n('fr'), + $.i18n('sa') + ], + monthNames: [ + $.i18n('january'), + $.i18n('february'), + $.i18n('march'), + $.i18n('april'), + $.i18n('may'), + $.i18n('june'), + $.i18n('july'), + $.i18n('august'), + $.i18n('september'), + $.i18n('october'), + $.i18n('november'), + $.i18n('december') + ] + }, + startDate: moment().subtract(this.config.defaults.daysAgo, 'days'), + minDate: this.config.minDate, + maxDate: this.config.maxDate, + ranges: ranges + }; + + if (this.config.dateLimit) datepickerOptions.dateLimit = { days: this.config.dateLimit }; + + dateRangeSelector.daterangepicker(datepickerOptions); + + /** so people know why they can't query data older than July 2015 */ + $('.daterangepicker').append( + $('
') + .addClass('daterange-notice') + .html($.i18n('date-notice', document.title, + "stats.grok.se", + `${$.i18n('july')} 2015` + )) + ); + + /** + * The special date range options (buttons the right side of the daterange picker) + * + * WARNING: we're unable to add class names or data attrs to the range options, + * so checking which was clicked is hardcoded based on the index of the LI, + * as defined in this.config.specialRanges + */ + $('.daterangepicker .ranges li').on('click', e => { + const index = $('.daterangepicker .ranges li').index(e.target), + container = this.daterangepicker.container, + inputs = container.find('.daterangepicker_input input'); + this.specialRange = { + range: Object.keys(this.config.specialRanges)[index], + value: `${inputs[0].value} - ${inputs[1].value}` + }; + }); + } + + /** + * Splash in console, just for fun + * @returns {String} output + */ + splash() { + const style = 'background: #222; color: #bada55; padding: 4px; font-family:dejavu sans mono'; + console.log('%c ___ __ _ _ ', style); + console.log('%c | _ \\ __ _ / _` | ___ __ __ (_) ___ __ __ __ ___ ', style); + console.log('%c | _/ / _` | \\__, | / -_) \\ V / | | / -_) \\ V V / (_-< ', style); + console.log('%c _|_|_ \\__,_| |___/ \\___| _\\_/_ _|_|_ \\___| \\_/\\_/ /__/_ ', style); + console.log('%c _| """ |_|"""""|_|"""""|_|"""""|_|"""""|_|"""""|_|"""""|_|"""""|_|"""""| ', style); + console.log('%c "`-0-0-\'"`-0-0-\'"`-0-0-\'"`-0-0-\'"`-0-0-\'"`-0-0-\'"`-0-0-\'"`-0-0-\'"`-0-0-\' ', style); + console.log('%c ___ _ _ _ _ ', style); + console.log('%c o O O / \\ _ _ __ _ | || | | | ___ (_) ___ ', style); + console.log('%c o | - | | \' \\ / _` | \\_, | | | (_-< | | (_-< ', style); + console.log('%c TS__[O] |_|_| |_||_| \\__,_| _|__/ _|_|_ /__/_ _|_|_ /__/_ ', style); + console.log('%c {======|_|"""""|_|"""""|_|"""""|_| """"|_|"""""|_|"""""|_|"""""|_|"""""| ', style); + console.log('%c ./o--000\'"`-0-0-\'"`-0-0-\'"`-0-0-\'"`-0-0-\'"`-0-0-\'"`-0-0-\'"`-0-0-\'"`-0-0-\' ', style); + console.log('%c ', style); + console.log(`%c Copyright © ${new Date().getFullYear()} MusikAnimal, Kaldari, Marcel Ruiz Forns `, style); + } + /** * Replace spaces with underscores * @@ -613,35 +887,6 @@ class Pv { `
${message}
` ); } - - /** - * Get a value from localStorage, using a temporary storage if localStorage is not supported - * @param {string} key - key for the value to retrieve - * @returns {Mixed} stored value - */ - getFromLocalStorage(key) { - // See if localStorage is supported and enabled - try { - return localStorage.getItem(key); - } catch (err) { - return this.storage[key]; - } - } - - /** - * Set a value to localStorage, using a temporary storage if localStorage is not supported - * @param {string} key - key for the value to set - * @param {Mixed} value - value to store - * @returns {Mixed} stored value - */ - setLocalStorage(key, value) { - // See if localStorage is supported and enabled - try { - return localStorage.setItem(key, value); - } catch (err) { - return this.storage[key] = value; - } - } } module.exports = Pv; diff --git a/javascripts/siteviews/config.js b/javascripts/siteviews/config.js index c9e2986e0..94057bd65 100644 --- a/javascripts/siteviews/config.js +++ b/javascripts/siteviews/config.js @@ -127,11 +127,11 @@ const config = { tooltipTemplate: '<%if (label){%><%=label%>: <%}%><%= formatNumber(value) %>' }, linearCharts: ['Line', 'Bar', 'Radar'], - minDate: moment('2015-08-01').startOf('day'), + minDate: moment('2015-07-01').startOf('day'), maxDate: moment().subtract(1, 'days').startOf('day'), platformSelector: '#platform-select', projectInput: '.aqs-project-input', - siteSelector: '.aqs-site-selector', + select2Input: '.aqs-site-selector', specialRanges: { 'last-week': [moment().subtract(1, 'week').startOf('week'), moment().subtract(1, 'week').endOf('week')], 'this-month': [moment().startOf('month'), moment().subtract(1, 'days').startOf('day')], diff --git a/javascripts/siteviews/siteviews.js b/javascripts/siteviews/siteviews.js index 931faa631..ae1aa5ac3 100644 --- a/javascripts/siteviews/siteviews.js +++ b/javascripts/siteviews/siteviews.js @@ -49,7 +49,7 @@ class SiteViews extends Pv { */ initialize() { this.setupDateRangeSelector(); - this.setupSiteSelector(); + this.setupSelect2(); this.setupSettingsModal(); this.setupSelect2Colors(); this.setupDataSourceSelector(); @@ -137,20 +137,6 @@ class SiteViews extends Pv { return jsonContent; } - /** - * Fill in values within settings modal with what's in the session object - * @returns {null} nothing - */ - fillInSettings() { - $.each($('#settings-modal input'), (index, el) => { - if (el.type === 'checkbox') { - el.checked = this[el.name] === 'true'; - } else { - el.checked = this[el.name] === el.value; - } - }); - } - /** * Fills in zero value to a timeseries, see: * https://wikitech.wikimedia.org/wiki/Analytics/AQS/Pageview_API#Gotchas @@ -205,24 +191,6 @@ class SiteViews extends Pv { ); } - /** - * Gets the date headings as strings - i18n compliant - * @param {boolean} localized - whether the dates should be localized per browser language - * @returns {Array} the date headings as strings - */ - getDateHeadings(localized) { - const dateHeadings = []; - - for (let date = moment(this.daterangepicker.startDate); date.isBefore(this.daterangepicker.endDate); date.add(1, 'd')) { - if (localized) { - dateHeadings.push(date.format(this.dateFormat)); - } else { - dateHeadings.push(date.format('YYYY-MM-DD')); - } - } - return dateHeadings; - } - /** * Get data formatted for a linear chart (Line, Bar, Radar) * @@ -287,19 +255,6 @@ class SiteViews extends Pv { return params; } - /** - * Simple metric to see how many use it (pageviews of the pageview, a meta-pageview, if you will :) - * @return {null} nothing - */ - patchUsage() { - if (location.host !== 'localhost') { - $.ajax({ - url: `//tools.wmflabs.org/musikanimal/api/sv_uses/${i18nLang}`, - method: 'PATCH' - }); - } - } - /** * Parses the URL query string and sets all the inputs accordingly * Should only be called on initial page load, until we decide to support pop states (probably never) @@ -322,8 +277,8 @@ class SiteViews extends Pv { } else if (params.start) { startDate = moment(params.start || moment().subtract(config.defaults.daysAgo, 'days')); endDate = moment(params.end || Date.now()); - if (startDate < moment('2015-08-01') || endDate < moment('2015-08-01')) { - this.addSiteNotice('danger', $.i18n('param-error-1'), $.i18n('invalid-params'), true); + if (startDate < config.minDate || endDate < config.minDate) { + this.addSiteNotice('danger', $.i18n('param-error-1', `${$.i18n('july')} 2015`), $.i18n('invalid-params'), true); this.resetView(); return; } else if (startDate > endDate) { @@ -349,16 +304,16 @@ class SiteViews extends Pv { $(config.dataSourceSelector).trigger('change'); } - this.resetSiteSelector(); + this.resetSelect2(); if (!params.sites || params.sites.length === 1 && !params.sites[0]) { params.sites = config.defaults.projects; - this.setSiteSelectorDefaults(params.sites); + this.setSelect2Defaults(params.sites); } else { if (params.sites.length === 1) { this.chartType = this.getFromLocalStorage('pageviews-chart-preference') || 'Bar'; } - this.setSiteSelectorDefaults(params.sites); + this.setSelect2Defaults(params.sites); } } @@ -402,7 +357,7 @@ class SiteViews extends Pv { * @returns {null} nothing */ pushParams() { - const sites = $(config.siteSelector).select2('val') || []; + const sites = $(config.select2Input).select2('val') || []; if (window.history && window.history.replaceState) { window.history.replaceState({}, document.title, @@ -413,21 +368,6 @@ class SiteViews extends Pv { $('.permalink').prop('href', `?${$.param(this.getPermaLink())}&sites=${sites.join('|')}`); } - /** - * Removes all site selector related stuff then adds it back - * Also calls updateChart - * @returns {null} nothing - */ - resetSiteSelector() { - const siteSelector = $(config.siteSelector); - siteSelector.off('change'); - siteSelector.select2('val', null); - siteSelector.select2('data', null); - siteSelector.select2('destroy'); - $('.data-links').hide(); - this.setupSiteSelector(); - } - /** * Removes chart, messages, and resets site selections * @returns {null} nothing @@ -437,77 +377,15 @@ class SiteViews extends Pv { $('.chart-container').removeClass('loading'); $('#chart-legend').html(''); $('.message-container').html(''); - this.resetSiteSelector(); - } - - /** - * Save a particular setting to session and localStorage - * - * @param {string} key - settings key - * @param {string|boolean} value - value to save - * @returns {null} nothing - */ - saveSetting(key, value) { - this[key] = value; - this.setLocalStorage(`pageviews-settings-${key}`, value); - } - - /** - * Save the selected settings within the settings modal - * Prefer this implementation over a large library like serializeObject or serializeJSON - * @returns {null} nothing - */ - saveSettings() { - /** track if we're changing to no_autocomplete mode */ - const wasAutocomplete = this.autocomplete === 'no_autocomplete'; - - $.each($('#settings-modal input'), (index, el) => { - if (el.type === 'checkbox') { - this.saveSetting(el.name, el.checked ? 'true' : 'false'); - } else if (el.checked) { - this.saveSetting(el.name, el.value); - } - }); - - this.daterangepicker.locale.format = this.dateFormat; - this.daterangepicker.updateElement(); - this.setupSelect2Colors(); - - /** - * If we changed to/from no_autocomplete we have to reset the site selector entirely - * as setSiteSelectorDefaults is super buggy due to Select2 constraints - * So let's only reset if we have to - */ - if ((this.autocomplete === 'no_autocomplete') !== wasAutocomplete) { - this.resetSiteSelector(); - } - - this.updateChart(true); - } - - /** - * Directly set sites in site selector - * - * @param {array} sites - site titles - * @returns {array} - untouched array of sites - */ - setSiteSelectorDefaults(sites) { - sites.forEach(page => { - const escapedText = $('
').text(page).html(); - $('').appendTo(config.siteSelector); - }); - $(config.siteSelector).select2('val', sites); - $(config.siteSelector).select2('close'); - - return sites; + this.resetSelect2(); } /** * Sets up the site selector and adds listener to update chart * @returns {null} - nothing */ - setupSiteSelector() { - const siteSelector = $(config.siteSelector); + setupSelect2() { + const select2Input = $(config.select2Input); let params = { ajax: { @@ -530,26 +408,8 @@ class SiteViews extends Pv { minimumInputLength: 1 }; - siteSelector.select2(params); - siteSelector.on('change', this.updateChart.bind(this)); - } - - /** - * Attempt to fine-tune the pointer detection spacing based on how cluttered the chart is - * @returns {null} nothing - */ - setChartPointDetectionRadius() { - if (this.chartType !== 'Line') return; - - if (this.numDaysInRange() > 50) { - Chart.defaults.Line.pointHitDetectionRadius = 3; - } else if (this.numDaysInRange() > 30) { - Chart.defaults.Line.pointHitDetectionRadius = 5; - } else if (this.numDaysInRange() > 20) { - Chart.defaults.Line.pointHitDetectionRadius = 10; - } else { - Chart.defaults.Line.pointHitDetectionRadius = 20; - } + select2Input.select2(params); + select2Input.on('change', this.updateChart.bind(this)); } /** @@ -557,66 +417,9 @@ class SiteViews extends Pv { * @returns {null} - nothing */ setupDateRangeSelector() { - const dateRangeSelector = $(config.dateRangeSelector); - - /** transform config.specialRanges to have i18n as keys */ - let ranges = {}; - Object.keys(config.specialRanges).forEach(key => { - ranges[$.i18n(key)] = config.specialRanges[key]; - }); + super.setupDateRangeSelector(); - dateRangeSelector.daterangepicker({ - locale: { - format: this.dateFormat, - applyLabel: $.i18n('apply'), - cancelLabel: $.i18n('cancel'), - customRangeLabel: $.i18n('custom-range'), - daysOfWeek: [ - $.i18n('su'), - $.i18n('mo'), - $.i18n('tu'), - $.i18n('we'), - $.i18n('th'), - $.i18n('fr'), - $.i18n('sa') - ], - monthNames: [ - $.i18n('january'), - $.i18n('february'), - $.i18n('march'), - $.i18n('april'), - $.i18n('may'), - $.i18n('june'), - $.i18n('july'), - $.i18n('august'), - $.i18n('september'), - $.i18n('october'), - $.i18n('november'), - $.i18n('december') - ] - }, - startDate: moment().subtract(config.defaults.daysAgo, 'days'), - minDate: config.minDate, - maxDate: config.maxDate, - ranges: ranges - }); - - /** - * The special date range options (buttons the right side of the daterange picker) - * - * WARNING: we're unable to add class names or data attrs to the range options, - * so checking which was clicked is hardcoded based on the index of the LI, - * as defined in config.specialRanges - */ - $('.daterangepicker .ranges li').on('click', e => { - const index = $('.daterangepicker .ranges li').index(e.target), - container = this.daterangepicker.container, - inputs = container.find('.daterangepicker_input input'); - this.specialRange = { - range: Object.keys(config.specialRanges)[index], - value: `${inputs[0].value} - ${inputs[1].value}` - }; - }); + const dateRangeSelector = $(config.dateRangeSelector); /** the "Latest N days" links */ $('.date-latest a').on('click', e => { @@ -693,29 +496,6 @@ class SiteViews extends Pv { // window.onpopstate = popParams(); } - /** - * Setup colors for Select2 entries so we can dynamically change them - * This is a necessary evil, as we have to mark them as !important - * and since there are any number of entires, we need to use nth-child selectors - * @returns {CSSStylesheet} our new stylesheet - */ - setupSelect2Colors() { - /** first delete old stylesheet, if present */ - if (this.colorsStyleEl) this.colorsStyleEl.remove(); - - /** create new stylesheet */ - this.colorsStyleEl = document.createElement('style'); - this.colorsStyleEl.appendChild(document.createTextNode('')); // WebKit hack :( - document.head.appendChild(this.colorsStyleEl); - - /** add color rules */ - config.colors.forEach((color, index) => { - this.colorsStyleEl.sheet.insertRule(`.select2-selection__choice:nth-of-type(${index + 1}) { background: ${color} !important }`, 0); - }); - - return this.colorsStyleEl.sheet; - } - /** * Set values of form based on localStorage or defaults, add listeners * @returns {null} nothing @@ -737,7 +517,7 @@ class SiteViews extends Pv { * @returns {null} - nothin */ updateChart(force) { - let sites = $(config.siteSelector).select2('val') || []; + let sites = $(config.select2Input).select2('val') || []; this.pushParams(); diff --git a/javascripts/topviews/config.js b/javascripts/topviews/config.js index 5f7d7b93a..aa409ab4a 100644 --- a/javascripts/topviews/config.js +++ b/javascripts/topviews/config.js @@ -15,6 +15,7 @@ const config = { articleSelector: '.aqs-article-selector', dateRangeSelector: '.aqs-date-range-selector', cookieExpiry: 30, // num days + dateLimit: 31, // num days defaults: { dateFormat: 'YYYY-MM-DD', dateRange: 'last-week', @@ -24,7 +25,7 @@ const config = { numericalFormatting: 'true', project: 'en.wikipedia.org' }, - minDate: moment('2015-08-01'), + minDate: moment('2015-07-01'), maxDate: moment().subtract(1, 'days'), pageSize: 20, projectInput: '.aqs-project-input', diff --git a/javascripts/topviews/topviews.js b/javascripts/topviews/topviews.js index c3685d07d..83a41742c 100644 --- a/javascripts/topviews/topviews.js +++ b/javascripts/topviews/topviews.js @@ -165,19 +165,6 @@ class TopViews extends Pv { return params; } - /** - * Simple metric to see how many use it (pageviews of the pageview, a meta-pageview, if you will :) - * @return {null} nothing - */ - patchUsage() { - if (location.host !== 'localhost') { - $.ajax({ - url: `//tools.wmflabs.org/musikanimal/api/tv_uses/${this.project}`, - method: 'PATCH' - }); - } - } - /** * Parses the URL query string and sets all the inputs accordingly * Should only be called on initial page load, until we decide to support pop states (probably never) @@ -203,8 +190,8 @@ class TopViews extends Pv { } else if (params.start) { startDate = moment(params.start || moment().subtract(config.defaults.daysAgo, 'days')); endDate = moment(params.end || Date.now()); - if (startDate < moment('2015-08-01') || endDate < moment('2015-08-01')) { - this.addSiteNotice('danger', $.i18n('param-error-1'), $.i18n('invalid-params'), true); + if (startDate < config.minDate || endDate < config.minDate) { + this.addSiteNotice('danger', $.i18n('param-error-1', `${$.i18n('july')} 2015`), $.i18n('invalid-params'), true); this.resetView(); return; } else if (startDate > endDate) { @@ -356,74 +343,9 @@ class TopViews extends Pv { * @returns {null} - nothing */ setupDateRangeSelector() { - const dateRangeSelector = $(config.dateRangeSelector); - - /** transform config.specialRanges to have i18n as keys */ - let ranges = {}; - Object.keys(config.specialRanges).forEach(key => { - ranges[$.i18n(key)] = config.specialRanges[key]; - }); - - dateRangeSelector.daterangepicker({ - locale: { - format: this.dateFormat, - applyLabel: $.i18n('apply'), - cancelLabel: $.i18n('cancel'), - customRangeLabel: $.i18n('custom-range'), - dateLimit: { days: 31 }, - daysOfWeek: [ - $.i18n('su'), - $.i18n('mo'), - $.i18n('tu'), - $.i18n('we'), - $.i18n('th'), - $.i18n('fr'), - $.i18n('sa') - ], - monthNames: [ - $.i18n('january'), - $.i18n('february'), - $.i18n('march'), - $.i18n('april'), - $.i18n('may'), - $.i18n('june'), - $.i18n('july'), - $.i18n('august'), - $.i18n('september'), - $.i18n('october'), - $.i18n('november'), - $.i18n('december') - ] - }, - startDate: moment().subtract(config.defaults.daysAgo, 'days'), - minDate: config.minDate, - maxDate: config.maxDate, - ranges: ranges - }); - - /** so people know why they can't query data older than August 2015 */ - $('.daterangepicker').append( - $('
') - .addClass('daterange-notice') - .html($.i18n('date-notice', document.title, "stats.grok.se")) - ); + super.setupDateRangeSelector(); - /** - * The special date range options (buttons the right side of the daterange picker) - * - * WARNING: we're unable to add class names or data attrs to the range options, - * so checking which was clicked is hardcoded based on the index of the LI, - * as defined in config.specialRanges - */ - $('.daterangepicker .ranges li').on('click', e => { - const index = $('.daterangepicker .ranges li').index(e.target), - container = this.daterangepicker.container, - inputs = container.find('.daterangepicker_input input'); - this.specialRange = { - range: Object.keys(config.specialRanges)[index], - value: `${inputs[0].value} - ${inputs[1].value}` - }; - }); + const dateRangeSelector = $(config.dateRangeSelector); /** the "Latest N days" links */ $('.date-latest a').on('click', function(e) { diff --git a/messages/en.json b/messages/en.json index c54de0192..48d407d07 100644 --- a/messages/en.json +++ b/messages/en.json @@ -63,9 +63,9 @@ "save": "Save", "apply": "Apply", "article-placeholder": "Type page names...", - "date-notice": "$1 provides data from August 2015 forward. For older data, try $2.", + "date-notice": "$1 provides data from $3 forward. For older data, try $2.", "invalid-params": "Invalid parameters!", - "param-error-1": "Pageviews API does not contain data older than August 2015. Sorry.", + "param-error-1": "Pageviews API does not contain data older than $1. Sorry.", "param-error-2": "Start date must be older than the end date.", "param-error-3": "Invalid range, using default range instead. See the documentation for more information.", "totals": "Totals", @@ -124,8 +124,8 @@ "langviews-throttle-notice": "Requests are currently being throttled for performance reasons.
See $1 for more information.", "langviews-throttle-wait": "Please wait $1 seconds before submitting another request.
Apologies for the inconvenience. This is a temporary throttling tactic.
See $2 for more information.", "faq-return-to": "Return to $1", - "faq-old-data-title": "Why can't I view data older than August 2015?", - "faq-old-data-body": "The Wikimedia pageviews API was introduced in August 2015 and does not include data from before that time. You will have to rely on other tools such as stats.grok.se to view data older than this. Whether or not they work is unfortunately outside the scope of this tool, and beyond the control of the maintainers.", + "faq-old-data-title": "Why can't I view data older than $1?", + "faq-old-data-body": "The Wikimedia pageviews API was introduced in August 2015 and has backfilled data to $1. You will have to rely on other tools such as stats.grok.se to view data older than this. Whether or not they work is unfortunately outside the scope of this tool, and beyond the control of the maintainers.", "faq-todays-date-title": "Why can't I view data for today's date?", "faq-todays-date-body": "The Wikimedia pageviews API generally takes a full 24 hours to populate, sometimes longer. In some situations you may see data missing for yesterday's date as well, which will be left blank rather than showing a count of zero views.", "faq-agents-title": "What are the \"Agents\"?", diff --git a/public_html/_footer.php b/public_html/_footer.php index 793ab0456..eb5f41174 100644 --- a/public_html/_footer.php +++ b/public_html/_footer.php @@ -27,18 +27,21 @@