/**
* Created by Owen Gannon (https://github.com/gannono2) for The Irish Times on 11/12/2015.
*
* This plugin is licensed under a Creative Commons Attribution-ShareAlike 4.0 International License
* http://creativecommons.org/licenses/by-sa/4.0/
*/
(function($){
var counts = 0;
var countResults = [];
var quota = 0;
var original = [];
var countIndex = 0;
var numCandidates = 0;
var rowHeight = 35; // 24px by default
var rowPadding = 10; // 10px by default
var intervalDuration = 2000; // 2000ms by default
var animationDuration = 1500; // 1500ms by default
var electedClass = "ganimation-candidate-elected";
var notElectedClass = "ganimation-candidate-not-elected";
var eliminatedClass = "ganimation-candidate-eliminated";
var loop;
var $this;
$.fn.extend({
// Hit the API for the constituency results...
initConstituencyAnimation: function(options, callback){
// Use the options passed in as parameters
if(options.electionCode === undefined || options.electionCode === null) {
return callback({err: true, msg: "ERR: Election code must be specified in options."});
}
if(options.constituencyCode === undefined || options.constituencyCode === null) {
return callback({err: true, msg: "ERR: Constituency code must be specified in options."});
}
$this = $(this);
var ts = new Date().getTime();
var url = "/cstatic/election-api/api/public/v1/elections/"
+ options.electionCode
+ "/constituencies/"
+ options.constituencyCode
+ "/fulldata.js"
+ "?ts=" + ts;
$.getJSON(url, function(res){
if(res["error_msg"]) {
return callback({ err: true });
}
if(res['filled'] !== undefined && res['meta'].seats !== undefined){
if(res['filled'].length < res['meta'].seats) {
return callback({err: true, msg: "Constituency not complete yet."});
}
} else {
return callback({err: true, msg: "Constituency not complete yet."});
}
countResults = res['countresults'];
counts = countResults.length;
quota = res['meta'].quota;
seats = res['meta'].seats;
// Build an array of candidates that'll be used to update votes and order
var candidates = [];
for (var i = 0; i < Object.keys(countResults[0]).length; i++) {
if (Object.keys(countResults[0])[i] === i.toString()) {
countResults[0][i].id = i;
candidates.push(countResults[0][i]);
numCandidates++;
}
}
original = $.fn.cloneOriginalCandidatesList(candidates);
// Sort the candidates by their 1st preference votes (for now)
candidates.sort(function(a, b){
return b["votes"] - a["votes"];
});
// Apply a sort order to each candidate
for(var k = 0; k < candidates.length; k++){
candidates[k].order = k;
}
// Persist the sorted order for the first preference votes
countResults.candidates = candidates;
return callback($.fn.displayPreCount(countResults[0], false));
}).fail(function(){ return { err: true }; });
},
// When the plugin is called, build the table of candidates and their votes
displayPreCount: function(count){
var subHeading = $("
Story of the count
");
$this.append(subHeading);
var buttons = $("");
buttons.append($(""));
buttons.append($(""));
buttons.append($(""));
buttons.append($("Count: " + (countIndex + 1) + " / " + counts + ""));
$this.append(buttons);
// Construct the table
var keys = Object.keys(count);
var table = $("
");
for(var i=0; i < keys.length; i++){
if(keys[i] === i.toString()) {
var row = $("
").html(votesFill));
table.append(row);
}
}
$this.append(table);
// Set the table's height depending on the amount of candidates
var height = (rowHeight * numCandidates + (numCandidates * rowPadding)); // + (rowPadding * numCandidates);
table.css("height", height + "px");
// Set the row height
var candidates = $(".candidate-row");
candidates.css({"height": rowHeight + "px", "line-height": rowHeight + "px"});
$(".candidate-row td").css({"line-height": rowHeight + "px"});
// Get and set the candidates
$.fn.setCandidatesPositions(candidates, false, function(){});
// Add in the elected and quota borders
table.append("");
table.append("");
var top = ((((seats * rowHeight) + (seats * rowPadding - (rowPadding / 2))) / height) * 100);
$('.ganimation-elected-bar').css({top: top + "%"});
},
// When the user clicks the play button, kick off the animation
playCountAnimation: function(){
$.fn.kickoffAnimation();
loop = setInterval($.fn.kickoffAnimation, intervalDuration);
},
// When the user clicks the pause button, clear the loop, but keep the counter where it is
pauseCountAnimation: function(){
clearInterval(loop);
},
// When the user clicks the reset button, clear the loop and set the counter back to zero
restartCountAnimation: function(){
// Kill the loop
clearInterval(loop);
// Reset the counting index
countIndex = 0;
// Update count indicator after reset
$(".ganimation-count-id").text("Count: 1 / " + counts);
// Go back to the first instance
var results = original;
// Extract only the candidate elements from the results
var count = [];
var keys = Object.keys(results);
for (var i = 0; i < keys.length; i++) {
if (keys[i] === i.toString()) {
count.push(results[i]);
}
}
var candidates = $('.candidate-row');
$.each(candidates, function(candidate){
$(this).removeClass(electedClass);
$(this).removeClass(notElectedClass);
$(this).removeClass(eliminatedClass);
// Animate back to zero
$(this).stop(true, false);
var fill = $(this).find(".candidate-votes-fill");
fill.text(0);
fill.attr("data-votes", 0);
});
$.fn.setCandidatesPositions(candidates, true,function(){
// Animate back to original positions
$.fn.reorderCandidates(count, true, function(){});
});
},
// Kick off the animation
kickoffAnimation: function(){
var countIdElement = ".ganimation-count-id";
if(countIndex < counts){
// Update count indicator
if(countIndex != 0){
$(countIdElement).css("display", "inline-block");
$(countIdElement).text("Count: " + (countIndex+1) + " / " + counts);
} else {
$(countIdElement).css("display", "inline-block");
$(countIdElement).text("Count: 1 / " + counts);
}
var results = countResults[countIndex++];
// Extract only the candidate elements from the results
var count = [];
var keys = Object.keys(results);
for (var i = 0; i < keys.length; i++) {
if (keys[i] === i.toString()) {
count.push(results[i]);
}
}
$.fn.reorderCandidates(count, false, function(){});
} else {
// Update count indicator for the last count
$(countIdElement).css("display", "inline-block");
$(countIdElement).text("Counts: " + counts);
// No more counts - stop looping!
clearInterval(loop);
// Disable the pause button (it's redundant now)
$(".ganimation-pause-btn").attr('disabled', true);
}
},
// Assign a sort order to the candidates based on their votes
reorderCandidates: function(count, reset, callback){
// loop through the candidates in the count
for(var i = 0; i < count.length; i++){
var candidate = count[i];
// Match each candidate to its matching candidate in the main countResults.candidates array
for(var j = 0; j < countResults.candidates.length; j++){
// Double match the candidates from the newest count by name
if(candidate.name === countResults.candidates[j].name){
// and by party
if(candidate.code === countResults.candidates[j].code){
// Apply the new votes value
countResults.candidates[j].votes = candidate.votes;
countResults.candidates[j].elected = candidate.elected;
countResults.candidates[j].eliminated = candidate.eliminated;
}
}
}
}
// Apply the new order of candidates based on updated votes
if(!reset){
countResults.candidates.sort(function(a, b){
return b["votes"] - a["votes"];
});
} else {
countResults.candidates.sort(function(a, b){
if(a.name < b.name) return -1;
if(a.name > b.name) return 1;
return 0;
});
}
// Retrieve the list of candidates on the page
var candidateElements = $(".candidate-row");
var votesElement = null;
// Loop through the main candidates array again
for(var x = 0; x < countResults.candidates.length; x++){
var c = countResults.candidates[x];
// Set the new order
c.order = x;
// loop through the candidates from the page
for(var y = 0; y < candidateElements.length; y++){
// match them up
var id = $(candidateElements[y]).attr("data-candidate-id");
if(c.id == id){
// update the data-sort-order attribute
$(candidateElements[y]).attr("data-sort-order", c.order);
votesElement = $(candidateElements[y]).find(".candidate-votes-fill");
var oldVotes = parseInt(votesElement.attr("data-votes"), 10);
if(oldVotes !== c.votes){
var comma_separator_number_step = $.animateNumber.numberStepFactories.separator(',');
votesElement.prop('number', oldVotes).animateNumber({
number: c.votes,
numberStep: comma_separator_number_step
}, 'slow');
votesElement.attr("data-votes", c.votes);
}
// Have they been elected, not elected or eliminated?
if(!reset){
if (c.elected === 1) { $(candidateElements[y]).addClass(electedClass); }
else if (c.eliminated === 1) { $(candidateElements[y]).addClass(eliminatedClass); }
}
}
}
}
// Update their positions
$.fn.setCandidatesPositions(candidateElements, true, function(){
return callback();
});
},
// Reorganise candidates according to their data-sort-order attribute
setCandidatesPositions: function(candidates, animate, callback){
for (var i = 0; i < candidates.length; i++) {
$.fn.setCandidatesPosition($(candidates[i]), animate);
}
return callback();
},
setCandidatesPosition: function(candidate, animate){
var order = parseInt(candidate.attr("data-sort-order"), 10);
var top = (order * (rowHeight + rowPadding));
// Either animate or just update the table with candidates
animate === true ? candidate.velocity({top: top}, animationDuration)
: candidate.css({top: top});
// Animate the candidate's new width due to the increase in votes
var fill = candidate.find(".candidate-votes-fill");
$.fn.animateWidth(fill);
},
// Animate the width of a candidate's votes
animateWidth: function(element){
var votes = parseInt(element.attr("data-votes"));
var w = (votes / quota) * 100;
element.velocity({width: w + "%"}, animationDuration);
},
// Clone an array without any future references to it
// http://stackoverflow.com/questions/728360/most-elegant-way-to-clone-a-javascript-object
cloneOriginalCandidatesList: function(obj){
if (null == obj || "object" != typeof obj) return obj;
var copy = obj.constructor();
for (var attr in obj) {
if (obj.hasOwnProperty(attr)) copy[attr] = $.fn.cloneOriginalCandidatesList(obj[attr]);
}
return copy;
}
});
})(jQuery);