/** * 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 = $(""); row.append($("").html(count[i].name)); var votesFill = "" + count[i].votes.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",") + ""; row.append($("").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);