ProportionalApprovalVoting.html (8236B)
1 <!DOCTYPE html> 2 3 <!-- 4 Sources: 5 + https://gist.github.com/cmatskas/8725a6ee4f5f1a8e1ceahttps://gist.github.com/cmatskas/8725a6ee4f5f1a8e1cea 6 + cdnjs 7 --> 8 9 <head> 10 <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.0.0-alpha1/jquery.min.js"></script> 11 <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery-csv/0.71/jquery.csv-0.71.min.js"></script> 12 <script type="text/javascript"> 13 $(document).ready(function() { 14 15 // The event listener for the file upload 16 document.getElementById('txtFileUpload').addEventListener('change', upload, false); 17 18 document.getElementById('txtFileUpload').addEventListener('click', reset, false); 19 20 function reset(){ 21 document.getElementById("txtFileUpload").value = null; 22 // This way, the event change fires even if you upload the same file twice 23 } 24 25 // Method that checks that the browser supports the HTML5 File API 26 function browserSupportFileUpload() { 27 var isCompatible = false; 28 if (window.File && window.FileReader && window.FileList && window.Blob) { 29 isCompatible = true; 30 } 31 return isCompatible; 32 } 33 34 // Method that reads and processes the selected file 35 function upload(evt) { 36 uploadedSameFileTwice = false; 37 if (!browserSupportFileUpload()) { 38 alert('The File APIs are not fully supported in this browser!'); 39 } else { 40 // alert("Checkpoint Charlie"); 41 // var data = null; 42 data = null; 43 var file = evt.target.files[0]; 44 var reader = new FileReader(); 45 reader.readAsText(file); 46 reader.onload = function(event) { 47 var csvData = event.target.result; 48 data = $.csv.toArrays(csvData); 49 if (data && data.length > 0) { 50 // alert('Imported -' + data.length + '- rows successfully!'); 51 wrapperProportionalApprovalVoting(data); 52 } else { 53 alert('No data to import!'); 54 } 55 }; 56 reader.onerror = function() { 57 alert('Unable to read ' + file.fileName); 58 }; 59 } 60 } 61 62 function wrapperProportionalApprovalVoting(data){ 63 let dataColumn1 = data.map(x => x[1]); 64 // this gets us the first column (columns start at 0). 65 // data[][1] breaks the thing without throwing an error in the browser. 66 67 let dataColumn1Split = dataColumn1.map( element => element.split(", ")); 68 // One row of the first column might be "Candidate1, Candidate2". 69 // This transforms it to ["Candidate1", "Candidate2"] 70 71 72 let uniqueCandidates = findUnique(dataColumn1Split); 73 // Finds all the candidates 74 75 // In this voting method, all voters start with a weight of 1, which changes as candidates are elected 76 // So that voters who have had one of their candidates elected have less influence for the next candidates. 77 78 let weights = Array(dataColumn1Split.length).fill(1); 79 80 // Find the most popular one, given the weights. Update the weights 81 82 //alert("\n"+dataColumn1Split[0]); 83 84 let n = document.getElementById("numWinners").value; 85 let winners = []; 86 87 for(i=0; i<n; i++){ 88 let newWinner = findTheNextMostPopularOneGivenTheWeights(dataColumn1Split, weights, uniqueCandidates, winners); 89 winners.push(newWinner); 90 weights = updateWeightsGivenTheNewWinner(dataColumn1Split, weights, newWinner); 91 } 92 //alert(winners); 93 94 // Display the winners. 95 displayWinners(winners); 96 } 97 98 function displayWinners(winners){ 99 100 // Header 101 102 103 // Ordered list with the winners 104 ///alert(document.getElementsByTagName("OL")[0]); 105 106 if(document.getElementsByTagName("OL")[0]==undefined){ 107 headerH3 = document.createElement("h3"); 108 headerH3.innerHTML = "Winners under Proportional Approval Voting:"; 109 110 document.body.appendChild(headerH3); 111 112 orderedList = document.createElement("OL"); // Creates an ordered list 113 for(let i =0; i<winners.length; i++){ 114 HTMLWinner = document.createElement("li"); 115 HTMLWinner.appendChild(document.createTextNode(winners[i])); 116 orderedList.appendChild(HTMLWinner); 117 } 118 119 document.body.appendChild(orderedList); 120 121 }else{ 122 123 oldOL = document.getElementsByTagName("OL")[0]; 124 oldOL.remove(); 125 126 orderedList = document.createElement("OL"); // Creates an ordered list 127 for(let i =0; i<winners.length; i++){ 128 HTMLWinner = document.createElement("li"); 129 HTMLWinner.appendChild(document.createTextNode(winners[i])); 130 orderedList.appendChild(HTMLWinner); 131 } 132 133 document.body.appendChild(orderedList); 134 135 136 } 137 138 } 139 140 function findTheNextMostPopularOneGivenTheWeights(arrayOfArrays, weights, uniqueCandidates, winners){ 141 let popularity = Array(uniqueCandidates.length).fill(0); 142 for(let i = 0; i<uniqueCandidates.length; i++){ 143 for(let j=1; j<arrayOfArrays.length; j++){ 144 // j = 1 because we don't want to include the title 145 //alert("array = "+arrayOfArrays[j]); 146 if(arrayOfArrays[j].includes(uniqueCandidates[i])){ 147 popularity[i]+= 1/weights[j]; 148 } 149 } 150 } 151 152 for(let i = 0; i<popularity.length; i++){ 153 //alert("popularity["+uniqueCandidates[i]+"] =" +popularity[i]); 154 } 155 156 let maxPopularity = 0; 157 let winner = undefined; 158 //alert(popularity + "\n"+uniqueCandidates); 159 for(let i=0; i<uniqueCandidates.length; i++){ 160 if(popularity[i]>=maxPopularity && !winners.includes(uniqueCandidates[i])){ 161 // Note, this breaks a tie pretty arbitrarily 162 // Tie breaking mechanism: so obscure as to be random. 163 winner = uniqueCandidates[i]; 164 //alert("new better:" +uniqueCandidates[i]); 165 maxPopularity = popularity[i]; 166 } 167 } 168 //alert(winner); 169 return winner; 170 } 171 172 function updateWeightsGivenTheNewWinner(arrayOfArrays, weights, newWinner){ 173 for(let i=0; i<arrayOfArrays.length; i++){ 174 175 if(arrayOfArrays[i].includes(newWinner)){ 176 weights[i] = weights[i]+1; 177 } 178 179 } 180 return weights; 181 } 182 183 function findUnique(arrayOfArrays){ 184 let uniqueElements = []; 185 186 for(let i = 1; i<arrayOfArrays.length; i++){ // We start with the second row (i=1, instead of i=0, because we take the first row to be a header) 187 for(let j=0; j<arrayOfArrays[i].length; j++){ 188 if(!uniqueElements.includes(arrayOfArrays[i][j])){ 189 uniqueElements.push(arrayOfArrays[i][j]); 190 } 191 } 192 } 193 return uniqueElements; 194 195 } 196 }); 197 198 </script> 199 </head> 200 <body> 201 202 <h1>Proportional Approval Voting MVP</h1> 203 <h2>What is this? How does this work?</h2> 204 <p>This is the simplest version of a program which computes the result of an election, under the <a href="https://www.electionscience.org/learn/electoral-system-glossary/#proportional_approval_voting" target="_blank">Proportional Approval Voting</a> method.</p> 205 <p>It takes a csv (comma separated value) file, with the same format as <a href="https://docs.google.com/spreadsheets/d/11pBOP6UJ8SSaHIY-s4dYwgBr4PHodh6cIXf-D4yl7HU/edit?usp=sharing" target="_blank">this one</a>, which might be produced by a Google Forms like <a href="https://docs.google.com/forms/d/1_-B5p8ePHnE1jXTGVT_kfRrMRqJuxmm8DPKn-MR1Pok/edit" target="_blank">this one.</a></p> 206 <p>It computes the result using client-side JavaScript, which means that all operations are run in your browser, as opposed to in a server which is not under your control. In effect, all this webpage does is provide you with a bunch of functions. In fact, you could just load this page, disconnect from the internet, upload your files, and you could still use the webpage to get the results you need.</p> 207 <div id="dvImportSegments" class="fileupload "> 208 <fieldset> 209 <legend>Upload your CSV File to compute the result</legend> 210 <label>Number of winners: </label><input type="number" id="numWinners" value="2"> 211 <!-- This is not really aesthetic; change. --> 212 <br> 213 <input type="file" name="File Upload" id="txtFileUpload" accept=".csv" /> 214 </fieldset> 215 </div> 216 <p>Proudly created by <a href="https://nunosempere.github.io">Nuño Sempere.</a></p> 217 </body> 218 </html> 219