11var voteTable = document . getElementById ( "votes" ) ;
22var seatTable = document . getElementById ( "seats" ) ;
3+ var teamTable = document . getElementById ( "teams" ) ;
34var voteLink = document . getElementById ( "voteslink" ) ;
45var seatLink = document . getElementById ( "seatslink" ) ;
6+ var teamLink = document . getElementById ( "teamslink" ) ;
57
68function roundDown ( number , decimals ) {
79 number = Math . floor ( number * 10 ** decimals ) / 10 ** decimals ;
810 return number . toFixed ( decimals ) ;
911} ;
1012
11- function printTable ( table , data , parties , mergeParties , mergeDistricts , mergedDistrictLabel , showFraction , showColumnTotals , showRowTotals , decimals ) {
13+ function printTable ( table , data , parties , mergeParties , mergeDistricts , mergedDistrictLabel , firstHeader , showFraction , showColumnTotals , showRowTotals , decimals ) {
1214 if ( mergeParties . length > 0 ) {
1315 var newData = { } ;
1416 for ( var district in data ) {
@@ -26,8 +28,9 @@ function printTable(table, data, parties, mergeParties, mergeDistricts, mergedDi
2628 parties . splice ( parties . indexOf ( party ) , 1 ) ; // remove from list of sorted parties
2729 }
2830 }
31+ parties = parties . slice ( ) ; // make copy
2932 parties . push ( "ANDRE" ) ; // always last
30- return printTable ( table , newData , parties , [ ] , mergeDistricts , mergedDistrictLabel , showFraction , showColumnTotals , showRowTotals , decimals ) ;
33+ return printTable ( table , newData , parties , [ ] , mergeDistricts , mergedDistrictLabel , firstHeader , showFraction , showColumnTotals , showRowTotals , decimals ) ;
3134 } else if ( mergeDistricts . length > 0 ) {
3235 var newData = { } ;
3336 newData [ mergedDistrictLabel ] = { } ;
@@ -43,7 +46,7 @@ function printTable(table, data, parties, mergeParties, mergeDistricts, mergedDi
4346 newData [ district ] = data [ district ] ;
4447 }
4548 }
46- return printTable ( table , newData , parties , mergeParties , [ ] , mergedDistrictLabel , showFraction , showColumnTotals , showRowTotals , decimals ) ;
49+ return printTable ( table , newData , parties , mergeParties , [ ] , mergedDistrictLabel , firstHeader , showFraction , showColumnTotals , showRowTotals , decimals ) ;
4750 }
4851
4952 // Make ANDRE always appear last
@@ -60,7 +63,7 @@ function printTable(table, data, parties, mergeParties, mergeDistricts, mergedDi
6063 const format = ( x , total ) => showFraction ? ( roundDown ( 100 * x / total , decimals ) + " %" ) : x . toLocaleString ( "no-NO" ) ; // round *down* so parties slightly below threshold cannot seem to be above it (e.g. show 3.999% as 3.9% instead of 4.0%)
6164 var head = table . tHead . insertRow ( ) ;
6265 var cell = document . createElement ( "th" ) ;
63- cell . innerHTML = "Valgdistrikt" ;
66+ cell . innerHTML = firstHeader ;
6467 head . appendChild ( cell ) ;
6568 for ( var party of parties ) {
6669 var cell = document . createElement ( "th" ) ;
@@ -286,6 +289,46 @@ function calculateAllSeats(votes, localSeatCounts, globalSeatCount, globalThresh
286289 return seats ;
287290} ;
288291
292+ function calculateTeams ( friends ) {
293+ function dfs ( team , teams ) {
294+ for ( var team2 of teams ) {
295+ if ( team . length == team2 . length ) {
296+ var isSubset = true ;
297+ for ( var party of team ) {
298+ if ( ! team2 . includes ( party ) ) {
299+ isSubset = false ;
300+ break ;
301+ }
302+ }
303+ if ( isSubset ) {
304+ return ; // team already explored
305+ }
306+ }
307+ }
308+
309+ teams . push ( team . slice ( ) ) ; // register team
310+
311+ // Explore new teams by adding parties that are friends with everyone already in the team
312+ for ( var newParty in friends ) {
313+ var friendsWithEveryone = true ;
314+ for ( var party of team ) {
315+ if ( ! friends [ party ] . includes ( newParty ) ) {
316+ friendsWithEveryone = false ;
317+ break ;
318+ }
319+ }
320+ if ( friendsWithEveryone ) {
321+ team . push ( newParty ) ;
322+ dfs ( team , teams ) ;
323+ team . splice ( team . indexOf ( newParty ) , 1 ) ;
324+ }
325+ }
326+ return teams ;
327+ } ;
328+
329+ return dfs ( [ ] , [ ] ) . slice ( 1 ) ; // drop empty team
330+ } ;
331+
289332function update ( ) {
290333 var threshold = parseFloat ( document . getElementById ( "threshold" ) . value ) ;
291334 var firstDivisor = parseFloat ( document . getElementById ( "firstdivisor" ) . value ) ;
@@ -297,6 +340,7 @@ function update() {
297340
298341 var [ localSeatCounts , globalSeatCount ] = calculateAllSeatCounts ( districts , localSeatCount , globalSeatsPerDistrict , areaFactor , minSeatsPerDistrict ) ;
299342 var seats = calculateAllSeats ( votes , localSeatCounts , globalSeatCount , threshold , firstDivisor , negativeGlobalSeats ) ;
343+ var globalSeats = sumLocal ( seats ) ;
300344
301345 var groupOtherParties = document . getElementById ( "groupotherparties" ) . checked ;
302346 var totalSeats = sumLocal ( seats ) ;
@@ -337,7 +381,6 @@ function update() {
337381 var globalVotes = sumLocal ( votes ) ;
338382 parties . sort ( ( party1 , party2 ) => compare ( party1 , party2 , globalVotes ) ) ;
339383 } else if ( sortParties == "Mandater" ) {
340- var globalSeats = sumLocal ( seats ) ;
341384 parties . sort ( ( party1 , party2 ) => compare ( party1 , party2 , globalSeats ) ) ;
342385 } else if ( sortParties == "Farge (subjektivt)" ) {
343386 var spectrum = [ "NKP" , "RØDT" , "SV" , "A" , "SP" , "MDG" , "KYST" , "KRF" , "V" , "H" , "FRP" , "KRISTNE" , "LIBS" , "DEMN" , "AAN" ] ;
@@ -348,16 +391,74 @@ function update() {
348391 parties . sort ( ( party1 , party2 ) => compare ( party2 , party1 , rightism ) ) ;
349392 }
350393
351- printTable ( voteTable , votes , parties , mergeParties , mergeDistricts , "Distriktsstemmer" , showFraction , true , true , decimals ) ;
352- printTable ( seatTable , seats , parties , mergeParties , mergeDistricts , "Distriktsmandater" , showFraction , true , true , decimals ) ;
394+ printTable ( voteTable , votes , parties , mergeParties , mergeDistricts , "Distriktsstemmer" , "Valgdistrikt" , showFraction , true , true , decimals ) ;
395+ printTable ( seatTable , seats , parties , mergeParties , mergeDistricts , "Distriktsmandater" , "Valgdistrikt" , showFraction , true , true , decimals ) ;
396+
397+ // Read graph of friend parties
398+ var friendsInput = document . getElementById ( "friends" ) ;
399+ var friendsText = friendsInput . value . trim ( ) ;
400+ var friends = { } ;
401+ for ( var party of parties ) {
402+ friends [ party ] = [ ] ;
403+ }
404+ var valid = true ;
405+ if ( friendsText . length > 0 ) {
406+ for ( var text of friendsText . split ( / \s * , \s * / ) ) {
407+ var friendList = text . split ( / \s * \+ \s * / ) ;
408+ for ( var party1 of friendList ) {
409+ if ( ! parties . includes ( party1 ) ) {
410+ valid = false ;
411+ break ;
412+ }
413+ for ( var party2 of friendList ) {
414+ if ( party1 != party2 && ! friends [ party1 ] . includes ( party2 ) ) { // don't be friends with oneself
415+ friends [ party1 ] . push ( party2 ) ;
416+ }
417+ }
418+ }
419+ }
420+ }
421+
422+ // Do not let unrepresented parties be part of coalitions
423+ for ( var party in friends ) {
424+ if ( globalSeats [ party ] == 0 ) {
425+ delete friends [ party ] ;
426+ }
427+ }
428+
429+ // Color input field depending on validity of input formatting
430+ friendsInput . style [ "background-color" ] = valid ? "limegreen" : "crimson" ;
431+
432+ if ( valid ) {
433+ var teams = calculateTeams ( friends ) ;
434+
435+ var teamSeats = { } ;
436+ for ( var team of teams ) {
437+ teamSeats [ team ] = 0 ;
438+ for ( var party of team ) {
439+ teamSeats [ team ] += globalSeats [ party ] ;
440+ }
441+ }
442+
443+ teams . sort ( ( team1 , team2 ) => teamSeats [ team2 ] - teamSeats [ team1 ] ) ; // TODO: guarantee sorting order in table output of teams (and districts above)
444+
445+ var teamsDict = { } ;
446+ totalSeats = sumGlobal ( totalSeats ) ;
447+ for ( var team of teams ) {
448+ teamsDict [ team . join ( " + " ) ] = { "Posisjon" : teamSeats [ team ] , "Opposisjon" : totalSeats - teamSeats [ team ] } ;
449+ }
450+ printTable ( teamTable , teamsDict , [ "Posisjon" , "Opposisjon" ] , [ ] , [ ] , "" , "Partier i posisjon" , showFraction , false , true , 0 ) ;
451+ }
353452} ;
354453
355- function showTables ( showVotes , showSeats ) {
454+ function showTables ( showVotes , showSeats , showTeams ) {
356455 voteTable . style [ "display" ] = showVotes ? "block" : "none" ;
357456 seatTable . style [ "display" ] = showSeats ? "block" : "none" ;
457+ teamTable . style [ "display" ] = showTeams ? "block" : "none" ;
358458 voteLink . style [ "color" ] = showVotes ? "black" : "gray" ;
359459 seatLink . style [ "color" ] = showSeats ? "black" : "gray" ;
460+ teamLink . style [ "color" ] = showTeams ? "black" : "gray" ;
360461} ;
361462
362- showTables ( true , false ) ;
463+ showTables ( true , false , false ) ;
363464update ( ) ; // run once on page load
0 commit comments