Skip to content

Instantly share code, notes, and snippets.

@marcelrf
Last active July 2, 2023 05:13
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save marcelrf/49738d14116fd547fe6d to your computer and use it in GitHub Desktop.
Save marcelrf/49738d14116fd547fe6d to your computer and use it in GitHub Desktop.
<!--
This is a sample application that shows how to use the Pageview API.
Read more here: https://wikitech.wikimedia.org/wiki/Analytics/AQS/Pageview_API
The most important code is inside the updateChart function, towards the end of the file.
The rest of the code is just html, css and input setup.
Disclaimer: This is just sample code, it has not been tested in all browsers/devices.
Distributed under the Unlicense: http://unlicense.org/
-->
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>ARTICLE COMPARISON - Sample App for Pageview API</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.5/css/bootstrap.min.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/select2/4.0.0/css/select2.min.css"/>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap-daterangepicker/2.1.13/daterangepicker.min.css"/>
<style>
/* Add legend colors to article tags */
.select2-selection__choice:nth-of-type(1) { background: #bccbda !important; }
.select2-selection__choice:nth-of-type(2) { background: #e0ad91 !important; }
.select2-selection__choice:nth-of-type(3) { background: #c1aa78 !important; }
.select2-selection__choice:nth-of-type(4) { background: #8da075 !important; }
.select2-selection__choice:nth-of-type(5) { background: #998a6f !important; }
/* Fix chart position */
.aqs-chart {
margin-left: -12px;
}
/* Fix date range selector style */
.aqs-date-range-selector {
border-radius: 4px;
border-width: 1px;
border-style: solid;
border-color: rgb(170, 170, 170);
line-height: 25px;
padding: 0 10px 0 10px;
}
/* Improve vertical spacing */
.aqs-row {
padding-bottom: 10px;
}
/* Avoid select2 collapsing with narrow screens */
.aqs-article-selector {
width: 100% !important;
}
</style>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/2.1.4/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.5/js/bootstrap.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/select2/4.0.0/js/select2.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.10.6/moment.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap-daterangepicker/2.1.13/daterangepicker.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/1.0.2/Chart.min.js"></script>
</head>
<body>
<div class="container"><br>
<!-- Header -->
<div class="row aqs-row">
<div class="col-lg-3 col-lg-offset-2">
<h4><strong>ARTICLE COMPARISON</strong></h4>
</div>
<div class="col-lg-5 text-right">
<h4>Sample App for Pageview API</h4>
</div>
</div>
<!-- Project selector -->
<div class="row aqs-row">
<div class="col-lg-1 col-lg-offset-2">Project</div>
<div class="col-lg-7">
<select class="aqs-project-selector">
<option value="enwiki" class="active">enwiki</option>
</select>
</div>
</div>
<!-- Date range selector -->
<div class="row aqs-row">
<div class="col-lg-1 col-lg-offset-2">Dates</div>
<div class="col-lg-7">
<input class="aqs-date-range-selector">
</div>
</div>
<!-- Article selector -->
<div class="row aqs-row">
<div class="col-lg-1 col-lg-offset-2">Articles</div>
<div class="col-lg-7">
<select class="aqs-article-selector col-lg-12" multiple="multiple"></select>
</div>
</div>
<!-- Chart -->
<div class="col-lg-8 col-lg-offset-2">
<canvas class="aqs-chart"></canvas>
</div>
</div>
<script>
var config = {
// For more information on the list of all Wikimedia languages and projects, see:
// https://www.mediawiki.org/wiki/Extension:SiteMatrix
// https://en.wikipedia.org/w/api.php?action=sitematrix&formatversion=2
projects: ['aawiki', 'abwiki', 'acewiki', 'afwiki', 'akwiki', 'alswiki', 'amwiki', 'angwiki', 'anwiki', 'arcwiki', 'arwiki', 'arzwiki', 'astwiki', 'aswiki', 'avwiki', 'aywiki', 'azwiki', 'barwiki', 'bawiki', 'bclwiki', 'bewiki', 'bgwiki', 'bhwiki', 'biwiki', 'bjnwiki', 'bmwiki', 'bnwiki', 'bowiki', 'bpywiki', 'brwiki', 'bswiki', 'bugwiki', 'bxrwiki', 'cawiki', 'cdowiki', 'cebwiki', 'cewiki', 'chowiki', 'chrwiki', 'chwiki', 'chywiki', 'ckbwiki', 'cowiki', 'crhwiki', 'crwiki', 'csbwiki', 'cswiki', 'cuwiki', 'cvwiki', 'cywiki', 'dawiki', 'dewiki', 'diqwiki', 'dsbwiki', 'dvwiki', 'dzwiki', 'eewiki', 'elwiki', 'emlwiki', 'enwiki', 'eowiki', 'eswiki', 'etwiki', 'euwiki', 'extwiki', 'fawiki', 'ffwiki', 'fiwiki', 'fjwiki', 'fowiki', 'frpwiki', 'frrwiki', 'frwiki', 'furwiki', 'fywiki', 'gagwiki', 'ganwiki', 'gawiki', 'gdwiki', 'glkwiki', 'glwiki', 'gnwiki', 'gotwiki', 'guwiki', 'gvwiki', 'hakwiki', 'hawiki', 'hawwiki', 'hewiki', 'hifwiki', 'hiwiki', 'howiki', 'hrwiki', 'hsbwiki', 'htwiki', 'huwiki', 'hywiki', 'hzwiki', 'iawiki', 'idwiki', 'iewiki', 'igwiki', 'iiwiki', 'ikwiki', 'ilowiki', 'iowiki', 'iswiki', 'itwiki', 'iuwiki', 'jawiki', 'jbowiki', 'jvwiki', 'kaawiki', 'kabwiki', 'kawiki', 'kbdwiki', 'kgwiki', 'kiwiki', 'kjwiki', 'kkwiki', 'klwiki', 'kmwiki', 'knwiki', 'koiwiki', 'kowiki', 'krcwiki', 'krwiki', 'kshwiki', 'kswiki', 'kuwiki', 'kvwiki', 'kwwiki', 'kywiki', 'ladwiki', 'lawiki', 'lbewiki', 'lbwiki', 'lezwiki', 'lgwiki', 'lijwiki', 'liwiki', 'lmowiki', 'lnwiki', 'lowiki', 'ltgwiki', 'ltwiki', 'lvwiki', 'mdfwiki', 'mgwiki', 'mhrwiki', 'mhwiki', 'minwiki', 'miwiki', 'mkwiki', 'mlwiki', 'mnwiki', 'mowiki', 'mrjwiki', 'mrwiki', 'mswiki', 'mtwiki', 'muswiki', 'mwlwiki', 'myvwiki', 'mywiki', 'mznwiki', 'nahwiki', 'napwiki', 'nawiki', 'ndswiki', 'newiki', 'newwiki', 'ngwiki', 'nlwiki', 'nnwiki', 'novwiki', 'nowiki', 'nrmwiki', 'nsowiki', 'nvwiki', 'nywiki', 'ocwiki', 'omwiki', 'orwiki', 'oswiki', 'pagwiki', 'pamwiki', 'papwiki', 'pawiki', 'pcdwiki', 'pdcwiki', 'pflwiki', 'pihwiki', 'piwiki', 'plwiki', 'pmswiki', 'pnbwiki', 'pntwiki', 'pswiki', 'ptwiki', 'quwiki', 'rmwiki', 'rmywiki', 'rnwiki', 'rowiki', 'ruewiki', 'ruwiki', 'rwwiki', 'sahwiki', 'sawiki', 'scnwiki', 'scowiki', 'scwiki', 'sdwiki', 'sewiki', 'sgwiki', 'shwiki', 'siwiki', 'skwiki', 'slwiki', 'smwiki', 'snwiki', 'sowiki', 'sqwiki', 'srnwiki', 'srwiki', 'sswiki', 'stqwiki', 'stwiki', 'suwiki', 'svwiki', 'swwiki', 'szlwiki', 'tawiki', 'tenwiki', 'tetwiki', 'tewiki', 'tgwiki', 'thwiki', 'tiwiki', 'tkwiki', 'tlwiki', 'tnwiki', 'towiki', 'tpiwiki', 'trwiki', 'tswiki', 'ttwiki', 'tumwiki', 'twwiki', 'tyvwiki', 'tywiki', 'udmwiki', 'ugwiki', 'ukwiki', 'urwiki', 'uzwiki', 'vecwiki', 'vepwiki', 'vewiki', 'viwiki', 'vlswiki', 'vowiki', 'warwiki', 'wawiki', 'wowiki', 'wuuwiki', 'xalwiki', 'xhwiki', 'xmfwiki', 'yiwiki', 'yowiki', 'zawiki', 'zeawiki', 'zhwiki', 'zuwiki'],
colors: ['rgba(188,203,218,1)', 'rgba(224,173,145,1)', 'rgba(193,170,120,1)', 'rgba(141,160,117,1)', 'rgba(153,138,111,1)'],
projectSelector: '.aqs-project-selector',
dateRangeSelector: '.aqs-date-range-selector',
articleSelector: '.aqs-article-selector',
chart: '.aqs-chart',
minDate: moment('2015-10-01'),
maxDate: moment().subtract(1, 'days'),
timestampFormat: 'YYYYMMDD00',
daysAgo: 20
};
function getLanguageCode () {
var project = $(config.projectSelector).val();
// Remove the last 4 characters (wiki) from the project code to
// get the language code ('enwiki' -> 'en', 'cebwiki' -> 'ceb').
return project.substring(0, project.length - 4);
}
function setupProjectSelector () {
var projectSelector = $(config.projectSelector);
var data = config.projects.map(function (elem) {
return {id: elem, text: elem};
});
projectSelector.select2({data: data});
projectSelector.on('change', function () {
resetArticleSelector();
// This will call updateChart() itself.
});
}
function setupDateRangeSelector () {
var dateRangeSelector = $(config.dateRangeSelector);
dateRangeSelector.daterangepicker({
startDate: moment().subtract(config.daysAgo, 'days'),
minDate: config.minDate,
maxDate: config.maxDate
});
dateRangeSelector.on('change', function () {
updateChart();
});
}
function setupArticleSelector () {
var articleSelector = $(config.articleSelector);
articleSelector.select2({
placeholder: 'Type article names...',
maximumSelectionLength: 5,
// This ajax call queries the Mediawiki API for article name
// suggestions given the search term inputed in the selector.
ajax: {
url: 'https://' + getLanguageCode() + '.wikipedia.org/w/api.php',
dataType: 'jsonp',
delay: 200,
jsonpCallback: 'articleSuggestionCallback',
data: function (search) {
return {
'action': 'query',
'list': 'search',
'format': 'json',
'srsearch': search.term
};
},
processResults: function (data) {
// Processes Mediawiki API results into Select2 format.
var results = [];
if (data.query && data.query.search) {
results = data.query.search.map(function (elem) {
return {
id: elem.title.replace(/ /g, '_'),
text: elem.title
};
});
}
return {results: results};
},
cache: true
}
});
articleSelector.on('change', function () {
updateChart();
});
}
// Select2 library prints "Uncaught TypeError: XYZ is not a function" errors
// caused by race conditions between consecutive ajax calls. They are actually
// not critical and can be avoided with this empty function.
function articleSuggestionCallback (data) {}
function resetArticleSelector () {
var articleSelector = $(config.articleSelector);
articleSelector.unbind('change');
articleSelector.select2('val', null);
articleSelector.select2('data', null);
articleSelector.select2('destroy');
setupArticleSelector();
updateChart();
}
function setArticleSelectorDefaults (defaults) {
// Caveat: This method only works with single-word article names.
var articleSelectorQuery = config.articleSelector;
defaults.forEach(function (elem) {
var escapedText = $('<div>').text(elem).html();
$('<option>' + escapedText + '</option>').appendTo(articleSelectorQuery);
});
var articleSelector = $(articleSelectorQuery);
articleSelector.select2('val', defaults);
articleSelector.select2('close');
}
function updateChart () {
// Collect parameters from inputs.
var languageCode = getLanguageCode();
var dateRangeSelector = $(config.dateRangeSelector);
var startDate = dateRangeSelector.data('daterangepicker').startDate;
var endDate = dateRangeSelector.data('daterangepicker').endDate;
var articles = $(config.articleSelector).select2('val') || [];
// Destroy previous chart, if needed.
if (config.articleComparisonChart) {
config.articleComparisonChart.destroy();
delete config.articleComparisonChart;
}
// Asynchronously collect the data from Analytics Query Service API,
// process it to Chart.js format and initialize the chart.
var labels = []; // Labels (dates) for the x-axis.
var datasets = []; // Data for each article timeseries.
articles.forEach(function (article, index) {
var uriEncodedArticle = encodeURIComponent(article);
// Url to query the API.
var url = (
'https://wikimedia.org/api/rest_v1/metrics/pageviews/per-article/' +
languageCode + '.wikipedia/all-access/all-agents/' + uriEncodedArticle + '/daily/' +
startDate.format(config.timestampFormat) + '/' + endDate.format(config.timestampFormat)
);
$.ajax({
url: url,
dataType: 'json',
success: function (data) {
fillInNulls(data, startDate, endDate);
// Get the labels from the first call.
if (labels.length == 0) {
labels = data.items.map(function (elem) {
return moment(elem.timestamp, config.timestampFormat).format('YYYY-MM-DD');
});
}
// Build the article's dataset.
var values = data.items.map(function (elem) { return elem.views; });
var color = config.colors[index];
datasets.push({
fillColor: 'rgba(0,0,0,0)',
strokeColor: color,
pointColor: color,
pointStrokeColor: '#fff',
pointHighlightFill: '#fff',
pointHighlightStroke: color,
data: values
});
// When all article datasets have been collected,
// initialize the chart.
if (datasets.length == articles.length) {
var data = {labels: labels, datasets: datasets};
var options = {bezierCurve: false};
var context = $(config.chart).get(0).getContext('2d');
config.articleComparisonChart = new Chart(context).Line(data, options);
}
}
});
});
}
// Fills in null values to a timeseries, see:
// https://wikitech.wikimedia.org/wiki/Analytics/AQS/Pageview_API#Gotchas
function fillInNulls (data, startDate, endDate) {
// Extract the dates that are already in the timeseries
var alreadyThere = {};
data.items.forEach(function (elem) {
var date = moment(elem.timestamp, config.timestampFormat);
alreadyThere[date] = elem;
});
data.items = [];
// Reconstruct the timeseries adding nulls
// for the dates that are not in the timeseries
for (var date = moment(startDate); date.isBefore(endDate); date.add(1, 'd')) {
if (alreadyThere[date]) {
data.items.push(alreadyThere[date]);
} else if (date != endDate) {
data.items.push({
timestamp: date.format(config.timestampFormat),
views: null
});
}
}
}
$(document).ready(function() {
$.extend(Chart.defaults.global, {animation: false, responsive: true});
setupProjectSelector();
setupDateRangeSelector();
setupArticleSelector();
var defaults = null;
if (location.hash) {
try {
defaults = location.hash.substring(1).split(',');
} catch (e) {
defaults = null;
}
}
defaults = defaults || ['Cat', 'Dog'];
setArticleSelectorDefaults(defaults);
});
</script>
</body>
</html>
@Ainali
Copy link

Ainali commented Nov 16, 2015

It would be awesome if it had shareable URLs for the settings I make.

@stuartjamesmiller
Copy link

This doesn't seem to work in the latest Firefox (58.0b13). I've come across this as I was having trouble making cross origin requests to the API endpoints this gist demonstrates. Cross origin requests seem to work to other APIs and the Wikimedia API cross origin calls work fine in Chrome.

The only thing that I can think of is that there is an mismatch between Firefox cross origin required headers and the ones returned from the API.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment