// ==UserScript==
// @name IMDbTvShowStatistics
// @namespace notableTieView
// @author notableTieView
// @description Shows season statistics for TV Shows on IMDB
// @include http://www.imdb.com/title/*
// @include http://www.imdb.com/title/*/eprate*
// @version 1.6
// @grant none
// @license Creative Commons Attribution-NonCommercial 3.0 http://creativecommons.org/licenses/by-nc/3.0/
//
// This script uses the following external libraries which are available under different licenses:
// jQuery (https://jquery.com/) is provided under the MIT License https://tldrlegal.com/license/mit-license
// d3 (http://d3js.org/) is provided under the BSD 3-Clause License https://github.com/mbostock/d3/blob/master/LICENSE
// Chart.js (http://www.chartjs.org/) is provided under the MIT License http://opensource.org/licenses/MIT
// Highcharts (http://shop.highsoft.com/highcharts.html) is provided by Highsoft (http://shop.highsoft.com/) for non-commercial use under the Creative Commons Attribution-NonCommercial 3.0 license: http://creativecommons.org/licenses/by-nc/3.0/
//
// @require https://code.jquery.com/jquery-2.1.4.min.js
// @require https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.6/d3.min.js
// @require https://cdnjs.cloudflare.com/ajax/libs/Chart.js/1.0.2/Chart.min.js
// @require http://code.highcharts.com/highcharts.js
// @require http://code.highcharts.com/highcharts-more.js
// @require http://code.highcharts.com/modules/exporting.js
//
// ==/UserScript==
// compatibility
this.$ = this.jQuery = jQuery.noConflict(true);
var processedRows=0;
/*
Plot a box plot for every season (min, q1, median, q3, max)
divId - the div to add the plot to
plotData - the data
*/
function plotBoxPlot(divId, plotData, width) {
$(divId).highcharts({
chart: {
type: 'boxplot',
width: width,
height: 200
},
title: {
text: ''
},
legend: {
enabled: false
},
xAxis: {
categories: plotData.labels,
title: {
text: '' //Seasons'
}
},
yAxis: {
title: {
text: ''
},
plotLines: [
{
value: plotData.median,
color: 'red',
width: 2,
label: {
align: 'left',
style: {
color: 'gray'
}
}
}
]
},
series: [
{
name: 'Stats',
data: plotData.quartiles,
tooltip: {
headerFormat: '<em>Season {point.key}</em><br/>'
}
}
]
});
}
/*
Compute the average over an array
ar - the array (numeric)
*/
function getAverage(ar) {
var count = 0;
for (var i = 0, n = ar.length; i < n; i++) {
count += ar[i];
}
return count / ar.length;
}
/*
Get a quartile of an array
ar - array, sorted (highest first)
q - the quartile we want: e.g. 0.25, 0.5 (median), 0.75
*/
function getQuartile(q, ar) {
var realQ = (1 - q) * ar.length;
var floorQ = Math.floor(realQ);
if (realQ === floorQ) {
if (floorQ === 0) {
return ar[0];
} else if (floorQ === ar.length) {
return ar[ar.length - 1];
} else {
return getAverage([ar[floorQ - 1], ar[floorQ]]);
}
}
return ar[floorQ];
}
/*
Transform seasonData into data structure thats easy to use for plotting
seasonData - a hash with an array of ratings for each season
the index in the hash is the seaons, season 0 is for specials
*/
function getPlotData(seasonData) {
seasons = Object.keys(seasonData);
n = seasons.length;
seasonLabels = [
];
seasonAverages = [
];
seasonQuartiles = [
[]
];
allRatings = [
];
specials = false;
seasons.forEach(function (season) {
index = season - 1;
if (season == 0) {
index = n - 1;
seasonLabels[index] = 'S'; //specials';
specials = true;
} else {
seasonLabels[index] = season.toString();
}
seasonAverages[index] = getAverage(seasonData[season]);
seasonQuartiles[index] = [
Math.min.apply(null, seasonData[season]),
getQuartile(0.25, seasonData[season]),
getQuartile(0.5, seasonData[season]),
getQuartile(0.75, seasonData[season]),
Math.max.apply(null, seasonData[season])
];
allRatings = allRatings.concat(seasonData[season]);
});
allRatings.sort(function (a, b) {
return b - a
}); //sort in reverse order
averageData = {
averages: seasonAverages,
labels: seasonLabels,
quartiles: seasonQuartiles,
minimumFloor: Math.max(Math.floor(Math.min.apply(null, seasonAverages)) - 1, 0),
median: getQuartile(0.5, allRatings),
num: n,
specials: specials
};
return averageData;
}
/*
Plot a simple chart with season averages as bars
divId - the id to add the plot to
plotData - the datastructure
*/
function plotChart(divId, plotData) {
var data = {
labels: plotData.labels,
datasets: [
{
label: 'Average Season Ratings',
fillColor: 'rgba(19,108,178,0.5)',
strokeColor: 'rgba(220,220,220,0.8)',
highlightFill: 'rgba(19,108,178,0.9)',
highlightStroke: 'rgba(220,220,220,1)',
data: plotData.averages
}
]
};
var options = {
scaleOverride: true,
scaleStepWidth: 1,
scaleSteps: (10 - averageData.minimumFloor),
scaleStartValue: averageData.minimumFloor,
}
var ctx = $(divId).get(0).getContext('2d');
new Chart(ctx).Bar(data, options);
}
/*
Get the suffix for a title attribute
if the season is 0, return the title for specials,
otherwise return the title for that season
*/
function getTitleSuffix(season) {
titleSuffix=' episode of Season '.concat(season).concat('.')
if (season==0) {
titleSuffix=' special.'
}
return titleSuffix;
}
/*
Add css classes to highlight the top three episodes of a season
row - the current row in the episodes table
season - the season of that row
seasonScores - the extracted ratings for that season so far (including the current row)
Since the episodes are ordered from best to worst, the first three encountered rows of a season are the top three episodes
*/
function colorizeTopEpisodes(row, season, seasonScores) {
className='#000000';
place='';
switch(seasonScores.length) {
case 1:
className='seasonBest';
place='best';
break;
case 2:
className='seasonSecond';
place='second best';
break;
case 3:
className='seasonThird';
place='third best';
break;
default:
return;
}
$(row).children().eq(0).addClass(className);
$(row).children().eq(0).attr('title', 'The '.concat(place).concat(getTitleSuffix(season)));
}
/*
Colorize the worst Episode of each season
* scoresBySeason - a season-to-ratings map. Needed to check that there are more than three episodes in that season
* worstEpisodesRows - a season-to-row map. The row is that of the worst episode in a season.
Colored is the worst episode in a season unless the season had three or less episodes (in that case, the worst episode is one of the top three and thus colored respectively):
*/
function colorizeWorstEpisodes(scoresBySeason, worstEpisodesRows) {
seasons = Object.keys(scoresBySeason);
seasons.forEach(function(season) {
if (scoresBySeason[season].length>3) {
row=worstEpisodesRows[season];
$(row).children().eq(0).addClass("seasonWorst");
$(row).children().eq(0).attr('title', 'The worst'.concat(getTitleSuffix(season)));
}
});
}
/*
Extract the data from one table row of the ratings table
row - the row (jquery object)
scoresBySeason - the hash of arrays to append the extracted rating to
*/
function workOnTableRow(row, scoresBySeason, worstEpisodesRows) {
seasonEpisodeField = $(row).children().eq(0);
if (seasonEpisodeField.is('th')) {
$(row).children().eq(3).before('<th>User<br/>Rank</th>');
} else {
seasonEpisode = $.trim(seasonEpisodeField.text());
season = 0;
if (seasonEpisode != '-') {
season = seasonEpisode.split('.') [0];
}
rating = parseFloat($(row).children().eq(2).text());
if (season in scoresBySeason) {
scoresBySeason[season].push(rating);
} else {
scoresBySeason[season] = [
rating
];
}
colorizeTopEpisodes(row, season, scoresBySeason[season]);
worstEpisodesRows[season] = row;
processedRows++;
$(row).children().eq(3).before('<td align="right">'.concat(processedRows).concat('</td>'));
$(row).children().eq(4).attr('bgcolor', '#eeeeee');
}
}
/*
Extract a hash with rating arrays for each season from the ratings table
*/
function collectDataPointsBySeasonAndAddRanks() {
scoresBySeason = {
};
worstEpisodesRows = {};
tabRowsVar = $('#tn15content table').eq(0).find('tr');
tabRowsVar.each(function () {
workOnTableRow(this, scoresBySeason, worstEpisodesRows);
});
colorizeWorstEpisodes(scoresBySeason, worstEpisodesRows);
return scoresBySeason;
}
/*
Plot all the Charts (use on a eprate page)
*/
function addPlotsAndRanksToEpRatePage() {
var seasonData = collectDataPointsBySeasonAndAddRanks();
plotData = getPlotData(seasonData);
n = plotData.num;
width = Math.max(300, n * 30);
addGlobalStyle('.statsHeading { margin-left:10px !important; margin-bottom:10px !important; }');
addGlobalStyle('.statsDiv { float:left; max-width:100%; margin-top:10px; width:'.concat(width).concat('px;}'));
addGlobalStyle('#seasonAverage { margin-top: -3px; max-width:100%; width:'.concat(width).concat('px;}'));
addGlobalStyle('#seasonBoxPlot { margin-left: -10px; }');
addGlobalStyle('#statisticsClear { clear:both; margin-bottom: 10px; }');
//addGlobalStyle('#root td.seasonBest { color: gold; background-color: #eeeeee;}');
//addGlobalStyle('#root td.seasonSecond { color: silver;}');
//addGlobalStyle('#root td.seasonThird { color: #CD7F32;}');
addGlobalStyle('#root td.seasonBest { background-color: gold;}');
addGlobalStyle('#root td.seasonSecond { background-color: silver;}');
addGlobalStyle('#root td.seasonThird { background-color: #CD7F32;}');
addGlobalStyle('#root td.seasonWorst { background-color: red;}');
clearDivContent='';
if (plotData.specials) {
clearDivContent='(S = Specials)';
}
$('#tn15adrhs').css('display', 'none');
var statisticsHtml = $('<div style="overflow:hidden;">\
<h4>Season Statistics</h4>\
<div class="statsDiv">\
<h5 class="statsHeading">Rating Averages</h5>\
<canvas id="seasonAverage" height=190 width='.concat(width).concat('></canvas>\
</div>\
<div class="statsDiv">\
<h5 class="statsHeading">Rating Box Plots</h5>\
<div id="seasonBoxPlot"></div>\
</div>\
<div id="statisticsClear">').concat(clearDivContent).concat('</div>\
</div>'
));
$('div#tn15content h4').eq(0).before(statisticsHtml);
plotChart('#seasonAverage', plotData);
plotBoxPlot('#seasonBoxPlot', plotData, width);
}
/*
Add a Link to a regular page, linking to the eprate page
if it is a TV-Show pages
*/
function addLinkToTVShowPage(linkDest) {
episodesHeadline = $('#main_bottom .article h2');
if ((episodesHeadline != undefined) && (episodesHeadline.eq(0).text() == 'Episodes')) {
// this is a TV show
$('#overview-top .star-box-details').eq(0).append('<br/><a href=\''.concat(linkDest).concat('eprate\'>Show Episode Ranking</a>'));
}
}
function addLinkToTVShowPage(linkDest) {
episodesHeadline = $('#main_bottom .article h2');
if ((episodesHeadline != undefined) && (episodesHeadline.eq(0).text() == 'Episodes')) {
// this is a TV show
ratingBox=$('#overview-top .star-box-details');
if (ratingBox.length == 0) {
ratingBox=$('.ratings_wrapper');
}
ratingBox.eq(0).append('<br/><a href=\''.concat(linkDest).concat('eprate\'>Show Episode Ranking</a>'));
}
}
/*
Add CSS
*/
function addGlobalStyle(css) {
var head,
style;
head = document.getElementsByTagName('head') [0];
if (!head) {
return;
}
style = document.createElement('style');
style.type = 'text/css';
style.innerHTML = css;
head.appendChild(style);
}
/*
Run on every matching imdb page
*/
currURL = document.URL.split('?') [0];
if (currURL.match(/eprate/g) != undefined) {
// we are on an eprate page
addPlotsAndRanksToEpRatePage();
} else {
addLinkToTVShowPage(currURL);
}